Plan update; Loading screen for scene switching door is finally fixed

This commit is contained in:
2026-10-26 16:25:28 +03:00
parent e918ef8c4b
commit 3dfd6c12bd
5 changed files with 138 additions and 17 deletions
+35 -9
View File
@@ -357,6 +357,14 @@ EDITSCENE_PROFILE=1 ./demoTaxi --headless --benchmark
# scene graph update + cull + draw submission), systems (the
# profiled steps), swapWait (frameRenderingQueued end -> frameEnded
# = the vsync/GPU-bound wait) and OGRE tris/batches averages.
EDITSCENE_SWITCH_TRACE=1 <any demo or the editor>
# env var: log the game-mode scene-switch loading-cover timeline
# ([SwitchTrace] markers: arm / coverDraw / defer / loadStart /
# loadDone / frameStart / frameEnd on a shared seconds clock) to
# stderr, so the cover-vs-load frame order can be verified on the
# real machine; correct order is arm -> coverDraw -> defer ->
# coverDraw -> loadStart -> loadDone -> coverDraw... (see
# "Loading cover" in editScene/AGENTS.md).
./demoTaxi --headless --benchmark-drive [--drive-speed mps]
[--drive-frames N] [--drive-csv path]
# (phase 5.5) Continuous road-tour probe: builds an edge-coverage
@@ -2841,7 +2849,9 @@ Tests: `tests/global_state_test.cpp` (target `global_state_test`, CTest
### Scene Switching
`EditorApp::switchScene(path, opts)` (queued; executed at the top of the next
`frameRenderingQueued`) switches the running scene. It is exposed in the editor
`frameRenderingQueued` — in game mode one loop iteration later still, so the
loading cover gets swapped to the screen first, see the Loading cover bullet)
switches the running scene. It is exposed in the editor
menu (**File -> Switch Scene...**) and in Lua:
```lua
@@ -2884,14 +2894,30 @@ ecs.switch_scene(path, { position = ..., rotation = { w=1, x=0, y=0, z=0 } })
(`EditorApp::renderSceneSwitchCover`, drawn from the ImGui render listener,
headless-safe) hides the scene for `SCENE_SWITCH_COVER_FRAMES` (45) frames
after each switch, covering the characters' initial T-pose while animations
warm up. It is armed TWICE: once at `switchScene()` queue time (game mode),
so the frame that queued the switch already presents with the cover up —
the switch itself then runs synchronously at the top of the next
`frameRenderingQueued` and its teardown+load hitch hides behind the cover
instead of freezing the last scenery frame on screen — and again at the
end of `performSceneSwitch`, which keeps the post-load coverage duration
unchanged. The counter is counted down in `frameRenderingQueued` after
`processPendingSceneSwitch()`.
warm up. It is armed TWICE: once at `switchScene()` queue time (game mode)
and again at the end of `performSceneSwitch`, which keeps the post-load
coverage duration unchanged. The counter is counted down in
`frameRenderingQueued` after `processPendingSceneSwitch()`.
**Why the queued switch waits one full loop iteration
(`m_sceneSwitchWaitPresent`):** OGRE presents a frame only AFTER
`frameRenderingQueued` (`Root::_updateAllRenderTargets` submits all
targets without swapping, fires `frameRenderingQueued`, then calls
`_swapAllRenderTargetBuffers`). Executing the pending switch in the
same iteration that raised the cover left the WHOLE teardown+load plus
the post-load setup (streaming pages, colliders: seconds with slow
scenes) running between the cover's draw submission and its swap — the
screen kept showing the last *presented* frame (frozen pre-switch
scenery) and the cover only appeared once loading was over.
`processPendingSceneSwitch()` therefore consumes the wait flag on the
first post-arm tick WITHOUT executing: that iteration's render pass
submits the covered frame and its own swap puts it on the screen, and
only the next tick runs `performSceneSwitch` behind an already
presented cover. Debug timeline aid: run with
`EDITSCENE_SWITCH_TRACE=1` to log `arm / defer + coverDraw /
loadStart / loadDone / frameStart / frameEnd` markers (shared clock)
and verify the order; with the fix the log reads arm → coverDraw →
defer → coverDraw → loadStart → loadDone → coverDraw…
The `switchScene` behavior tree node (name=scene path) queues the same switch
from AI trees (SmartObject/GOAP) and actuator/player-action trees; it fires
+77 -2
View File
@@ -1,3 +1,5 @@
#include <cstdio>
#include <cstdarg>
#include <iostream>
#include <filesystem>
#include <fstream>
@@ -162,6 +164,33 @@
#include "lua/LuaItemApi.hpp"
#include "lua/LuaTerrainApi.hpp"
/* EDITSCENE_SWITCH_TRACE=1 logs the loading-cover timeline (arm,
* coverDraw, loadStart/loadDone, frame markers) so the cover timing can
* be verified against the actually presented frames. All markers share
* one clock started at the first trace call */
static bool switchTraceEnabled(void)
{
static const bool enabled =
getenv("EDITSCENE_SWITCH_TRACE") != nullptr;
return enabled;
}
static double switchTraceClock(void)
{
static Ogre::Timer timer;
return (double)timer.getMilliseconds() / 1000.0;
}
static void switchTrace(const char *fmt, ...)
{
std::va_list ap;
std::fprintf(stderr, "[SwitchTrace] +%.3fs ", switchTraceClock());
va_start(ap, fmt);
std::vfprintf(stderr, fmt, ap);
va_end(ap);
std::fprintf(stderr, "\n");
}
//=============================================================================
// ImGuiRenderListener Implementation
//=============================================================================
@@ -1396,15 +1425,27 @@ bool EditorApp::switchScene(const Ogre::String &scenePath,
m_sceneSwitchPending = true;
/* Raise the loading cover NOW (game mode): the switch itself runs
* synchronously at the top of the next frame and stalls rendering
* synchronously at the top of a later frame and stalls rendering
* for its whole teardown+load duration, so the last presented
* frame (empty scenery with the open door) would otherwise freeze
* on screen through the entire hitch. Arming at queue time makes
* the frame that queued the switch present with the cover already
* up; the re-arm at the end of performSceneSwitch keeps the full
* post-load coverage unchanged. */
if (m_gameMode == GameMode::Game)
if (m_gameMode == GameMode::Game) {
m_sceneSwitchCoverFrames = SCENE_SWITCH_COVER_FRAMES;
/* The present (swap) of this iteration's render pass only
* happens after frameRenderingQueued, so defer the actual
* switch by one full loop iteration: the covered frame
* above is submitted before this frame's tick and reaches
* the screen with this iteration's swap, ahead of the
* teardown+load and the post-load setup work. */
m_sceneSwitchWaitPresent = true;
if (switchTraceEnabled())
switchTrace("arm cover=%d path=%s",
m_sceneSwitchCoverFrames,
scenePath.c_str());
}
return true;
}
@@ -1412,9 +1453,32 @@ void EditorApp::processPendingSceneSwitch()
{
if (!m_sceneSwitchPending)
return;
/* One-iteration presentation wait: the cover was armed while this
* frame's render pass was already drawn, so wait for the next
* loop iteration. The covered frame is submitted before tick
* and reaches the screen with the iteration's own swap (Ogre
* presents only after frameRenderingQueued); running the load in
* the arm frame would put the whole teardown+load hitch between
* the cover submission and its swap, freezing the old scenery on
* screen until loading is over. */
if (m_sceneSwitchWaitPresent &&
m_gameMode == GameMode::Game) {
m_sceneSwitchWaitPresent = false;
if (switchTraceEnabled())
switchTrace("defer cover=%d",
m_sceneSwitchCoverFrames);
return;
}
m_sceneSwitchPending = false;
if (switchTraceEnabled())
switchTrace("loadStart cover=%d",
m_sceneSwitchCoverFrames);
double loadStartMs = switchTraceClock();
performSceneSwitch(m_pendingSceneSwitchPath,
m_pendingSceneSwitchOptions);
if (switchTraceEnabled())
switchTrace("loadDone loadMs=%.0f",
(switchTraceClock() - loadStartMs) * 1000.0);
}
void EditorApp::renderSceneSwitchCover()
@@ -1440,6 +1504,8 @@ void EditorApp::renderSceneSwitchCover()
ImVec2 textPos(vp->Pos.x + (vp->Size.x - textSize.x) * 0.5f,
vp->Pos.y + (vp->Size.y - textSize.y) * 0.5f);
drawList->AddText(textPos, IM_COL32(255, 255, 255, 255), text);
if (switchTraceEnabled())
switchTrace("coverDraw cover=%d", m_sceneSwitchCoverFrames);
}
void EditorApp::destroySceneEntities()
@@ -2330,6 +2396,11 @@ void EditorApp::createAxes()
bool EditorApp::frameStarted(const Ogre::FrameEvent &evt)
{
bool ret = OgreBites::ApplicationContext::frameStarted(evt);
if (switchTraceEnabled() &&
(m_sceneSwitchPending || m_sceneSwitchCoverFrames > 0))
switchTrace("frameStart cover=%d pending=%d",
m_sceneSwitchCoverFrames,
(int)(m_sceneSwitchPending ? 1 : 0));
m_profFrameStartUs = m_profTimer.getMicroseconds();
return ret;
}
@@ -2351,6 +2422,10 @@ bool EditorApp::frameEnded(const Ogre::FrameEvent & /*evt*/)
m_profBatchesAccum += (double)st.batchCount;
}
}
if (switchTraceEnabled() &&
(m_sceneSwitchPending || m_sceneSwitchCoverFrames > 0))
switchTrace("frameEnd cover=%d",
m_sceneSwitchCoverFrames);
return true;
}
+12 -1
View File
@@ -582,7 +582,10 @@ private:
/* Scene switch state. switchScene() only queues a request; the
* actual teardown + load runs at the top of the next
* frameRenderingQueued() so callers (Lua scripts, ImGui menus)
* never mutate the world mid-frame. */
* never mutate the world mid-frame. In game mode the pending
* switch additionally waits one full loop iteration after the
* loading cover is raised (m_sceneSwitchWaitPresent), because
* OGRE presents a frame only after frameRenderingQueued. */
bool m_sceneSwitchPending = false;
Ogre::String m_pendingSceneSwitchPath;
SceneSwitchOptions m_pendingSceneSwitchOptions;
@@ -599,6 +602,14 @@ private:
* takes a few frames to build. */
int m_sceneSwitchCoverFrames = 0;
static const int SCENE_SWITCH_COVER_FRAMES = 45;
/* When true, a queued game-mode switch waits one more loop
* iteration before executing: OGRE presents (swaps) a frame only
* AFTER frameRenderingQueued, so the covered frame submitted in
* this iteration's render pass reaches the screen at the end of
* this iteration and the load must not run in the same iteration
* it was raised in (the last presented frame would stay the old
* scenery through the entire hitch). */
bool m_sceneSwitchWaitPresent = false;
/* When > 0, frameRenderingQueued uses this fixed delta instead of
* the wall-clock delta (headless test determinism). */
+9 -3
View File
@@ -821,9 +821,15 @@ sub-millisecond while swings are real-time).
`isOpen && sceneSwitchPending`) and only then calls
`EditorApp::switchScene(sceneSwitchPath, {targetEntityName:
sceneSwitchTarget})`. The loading cover is armed at switchScene() queue
time, so the screen goes black the frame the door completes opening —
before the synchronous tear-down/load hitch can freeze the last scenery
frame on screen.
time AND the pending switch is deferred by one full loop iteration
(`m_sceneSwitchWaitPresent`): OGRE presents a frame only after
`frameRenderingQueued`, so the covered frame submitted in the arm's
next render pass must be swapped to the screen (end of that defer
iteration) before the synchronous tear-down/load hitch — and the
seconds of post-load streaming setup — can run behind it; otherwise
the last presented frame (frozen scenery) stayed on screen for the
whole load and the black cover only showed up afterwards
(verify with `EDITSCENE_SWITCH_TRACE=1`).
- **Black occluder:** `buildDoorEntities()` (and the F2 standalone builder)
additionally creates a black occluder entity childed to the door entity,
for doors with a non-empty `sceneSwitchPath`. All of them get the same
@@ -3871,7 +3871,9 @@ implementation — the DSL side especially needs thought):
- it should be possible to declare macro blocks in DSL similar to Lisp macros to reuse parametrized DSL fragments in many places.
- this is relevant to exterior scene
- schedule uses symbolic names, like home, job, leisure, etc.
- per-NPC schedule uses symbolic names, like home, job, leisure, etc.
- POIs should count their usage in the schedules of NPCs
- if POI is not home, when schedule is built, prefer POIs with less usage and less distance to NPC "home". That serves a purpose of making the town more interesting and spreading NPCs around town.
- POIs are declared with tags with DSL or node set up for interiors and node-based for exterior. Should easily handle large number of POIs in editor UX and performance.
- symbolic names from schedule are searched as POI tags
- home is declared as house where NPC sleeps in and starts game in, should be set from the start for that NPC.
@@ -3889,7 +3891,8 @@ This phase should be moved to a position where we think we have enough narration
- First we need to point-out missing modelling/animation pieces.
- Second we need to check and finish our dialogue system based on ImGui as usual and Ink interpreter based on Lua (src/features/editScene/lua-scripts/narrator)
- Third we need to create and polish a framework to create stories and narrative missions like that without major development work with minimal efforts to keep ourselves productive. Probably another DSL on top of Lua or Ink-based narrative solution, which should be very clear to use and implement, without writing tons of scripts, C++ code and .ink files everywhere.
- Note that not whole narration is text, often it won't start with dialogue option or actuator, it might be time event or occurance at special place or near NPC matching some parameters.
- Note that not whole narration is text, often it won't start with dialogue option or actuator, it might be time event or occurance at special place or near NPC matching some parameters..
pd
- The story: player character wakes up in startup exterior scene. He gets his own thought message that he needs to visit student dormitory to put out ad about rent for bed vacancy for a girl as he has heavy comfort issues he can't solve himself buf is fully capable of paying the rent. He arrives to the place and sees how one girl is rudely evicted from the dormitory by three other girls. The player character protects that girl or confronts her and brings her home:
- alternatives:
- she agrees to go with player character in the car as passenger and enters his initial house as permanent resident.