From 512595234f8deb0cc38022a4378b025c1668b489 Mon Sep 17 00:00:00 2001 From: maj00r Date: Mon, 22 Jun 2026 23:23:36 +0200 Subject: [PATCH] Stream visual scenery in the driver (play starts after infrastructure pass) Splits the progressive load across modes: the loader runs only the first (infrastructure) pass and then hands off to the driver, so play begins as soon as tracks/traction/events/signals/the player train are ready. The deferred visual nodes (3d models, terrain shapes/lines) then stream in from the driver, a small budget per frame, so the world fills in without a long up-front wait. - state_serializer: the infrastructure pass now returns to the caller (instead of chaining the visual pass), restarting the twin for the visual pass; the visual pass uses a small ~8 ms/frame budget and marks the load done at the end. A text/compile load (no twin) stays single-pass and finishes as before. - state_manager: retains the load state across the loader->driver hand-off and exposes loading_visuals()/continue_loading_visuals(). - driver_mode::update(): advances the deferred visual load each frame. Verified on td.scn (playable in ~2 s, visuals complete a few seconds later, no duplicate objects) and on the large tomaszewo scenery (flora/terrain/models deferred while infrastructure loads). Inserts run on the main thread before the render step, so progressive loading needs no locking. Co-Authored-By: Claude Opus 4.8 --- application/drivermode.cpp | 4 ++++ simulation/simulation.cpp | 23 ++++++++++++++++++++++- simulation/simulation.h | 7 +++++++ simulation/simulationstateserializer.cpp | 16 +++++++++++----- simulation/simulationstateserializer.h | 2 ++ 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/application/drivermode.cpp b/application/drivermode.cpp index 0aa033e1..35e174f4 100644 --- a/application/drivermode.cpp +++ b/application/drivermode.cpp @@ -171,6 +171,10 @@ bool driver_mode::update() simulation::State.update_scripting_interface(); simulation::Environment.update(); + // progressive loading: stream in the deferred visual nodes a little each frame, so + // the world fills in after play has already started on the infrastructure pass + simulation::State.continue_loading_visuals(); + if (deltatime != 0.0) { // jak pauza, to nie ma po co tego przeliczać diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index 51b7945f..c4a40500 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -56,7 +56,8 @@ bool is_ready { false }; std::shared_ptr state_manager::deserialize_begin(std::string const &Scenariofile) { - return m_serializer.deserialize_begin( Scenariofile ); + m_loadstate = m_serializer.deserialize_begin( Scenariofile ); + return m_loadstate; } bool @@ -65,6 +66,26 @@ state_manager::deserialize_continue(std::shared_ptr state) { return m_serializer.deserialize_continue(state); } +// true while a progressive load still has visual nodes to stream in (driver phase) +bool +state_manager::loading_visuals() const { + + return ( m_loadstate && m_loadstate->visualphase && ( false == m_loadstate->done ) ); +} + +// advances the deferred visual-node load by one budgeted step; clears the state when done. +// called every frame from the driver so the world's visuals fill in after play starts. +void +state_manager::continue_loading_visuals() { + + if( false == loading_visuals() ) { return; } + m_serializer.deserialize_continue( m_loadstate ); + if( m_loadstate->done ) { + WriteLog( "Progressive visual load complete" ); + m_loadstate = nullptr; + } +} + // stores class data in specified file, in legacy (text) format void state_manager::export_as_text( std::string const &Scenariofile ) const { diff --git a/simulation/simulation.h b/simulation/simulation.h index 09ea4d42..ae9a6958 100644 --- a/simulation/simulation.h +++ b/simulation/simulation.h @@ -55,6 +55,12 @@ public: // continues deserialization for given context, amount limited by time, returns true if needs to be called again bool deserialize_continue(std::shared_ptr state); + // progressive load: true while deferred visual nodes are still streaming in (driver phase) + bool + loading_visuals() const; + // advances the deferred visual-node load by one budgeted step (call once per frame) + void + continue_loading_visuals(); // stores class data in specified file, in legacy (text) format void export_as_text( std::string const &Scenariofile ) const; @@ -62,6 +68,7 @@ public: private: // members state_serializer m_serializer; + std::shared_ptr m_loadstate; // retained across loader->driver for visual streaming struct { std::shared_ptr weather, time, date; } m_scriptinginterface; diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 78864177..58fdfd44 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -148,7 +148,10 @@ state_serializer::deserialize_continue(std::shared_ptr state } auto timenow = std::chrono::steady_clock::now(); - if( std::chrono::duration_cast( timenow - timelast ).count() >= 200 ) { + // small per-frame budget while streaming visuals in the driver (avoid stutter), + // generous budget while the loading screen is up (infrastructure pass) + auto const budget = ( state->visualphase ? 8 : 200 ); + if( std::chrono::duration_cast( timenow - timelast ).count() >= budget ) { Application.set_progress( Input.getProgress(), Input.getFullProgress() ); return true; } @@ -161,13 +164,15 @@ state_serializer::deserialize_continue(std::shared_ptr state deserialize_firstinit( Input, Scratchpad ); } - // first (infrastructure) pass finished: run a second pass over the same twin to load - // the visual nodes that were skipped. only possible when replaying a binary twin; a - // text/compile load did everything in one pass (restartReplay returns false). + // first (infrastructure) pass finished: the scenario is now playable (tracks, events, + // signals, the player train are all loaded). hand control back so the loader can switch + // to the driver; the visual nodes load progressively from the driver via a second pass + // over the same twin. only possible when replaying a binary twin -- a text/compile load + // did everything in one pass (restartReplay returns false). if( ( false == state->visualphase ) && ( true == Input.restartReplay( scene::scenery_load_pass::visual ) ) ) { state->visualphase = true; - return true; // continue with the visual pass + return false; // infrastructure ready -> go to driver; visuals continue there } scene::Groups.close(); @@ -182,6 +187,7 @@ state_serializer::deserialize_continue(std::shared_ptr state // before we report the scenario as loaded scene::scenerybinary_wait_all(); + state->done = true; return false; } diff --git a/simulation/simulationstateserializer.h b/simulation/simulationstateserializer.h index 346aee54..81e23299 100644 --- a/simulation/simulationstateserializer.h +++ b/simulation/simulationstateserializer.h @@ -25,6 +25,8 @@ struct deserializer_state { // progressive (two-pass) load over a binary twin: first pass loads infrastructure, // second pass loads visual nodes. false while in the first (infrastructure) pass. bool visualphase { false }; + // set once the whole load (both passes / single text pass) has fully finished + bool done { false }; deserializer_state(std::string const &File, cParser::buffertype const Type, const std::string &Path, bool const Loadtraction) : scenariofile(File), input(File, Type, Path, Loadtraction) { }