diff --git a/src/features/editScene/AGENTS.md b/src/features/editScene/AGENTS.md index 7e5390b..77ab565 100644 --- a/src/features/editScene/AGENTS.md +++ b/src/features/editScene/AGENTS.md @@ -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 +# 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 diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 1b9364e..305dea8 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -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; } diff --git a/src/features/editScene/EditorApp.hpp b/src/features/editScene/EditorApp.hpp index a320ea7..cc1ab92 100644 --- a/src/features/editScene/EditorApp.hpp +++ b/src/features/editScene/EditorApp.hpp @@ -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). */ diff --git a/src/features/editScene/GameFeatures202609.md b/src/features/editScene/GameFeatures202609.md index 93297e7..768a691 100644 --- a/src/features/editScene/GameFeatures202609.md +++ b/src/features/editScene/GameFeatures202609.md @@ -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 diff --git a/src/features/editScene/demos/demo-taxi/PLAN.md b/src/features/editScene/demos/demo-taxi/PLAN.md index 10f474e..2391d6d 100644 --- a/src/features/editScene/demos/demo-taxi/PLAN.md +++ b/src/features/editScene/demos/demo-taxi/PLAN.md @@ -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.