diff --git a/scene/scene.cpp b/scene/scene.cpp index 32e56b9e..749568f8 100644 --- a/scene/scene.cpp +++ b/scene/scene.cpp @@ -275,6 +275,16 @@ basic_cell::insert( shape_node Shape ) { shapedata.translucent ? m_shapestranslucent : m_shapesopaque ); + // if this cell's geometry was already baked (deferred visual streaming inserting into a + // section the renderer already finalised), don't try to merge into an existing shape + // whose CPU-side vertices were freed at bake time -- add the shape standalone and upload + // it straight into the live bank, otherwise it would never become visible. + if( m_geometrybank != null_handle ) { + Shape.origin( m_area.center ); + shapes.emplace_back( Shape ); + shapes.back().create_geometry( m_geometrybank ); + return; + } for( auto &targetshape : shapes ) { // try to merge shapes with matching view ranges... auto const &targetshapedata { targetshape.data() }; @@ -302,6 +312,14 @@ basic_cell::insert( lines_node Lines ) { m_active = true; auto const &linesdata { Lines.data() }; + // see the matching note in insert( shape_node ): once the cell is baked, append the new + // lines straight into the live bank rather than merging into vertex-freed geometry. + if( m_geometrybank != null_handle ) { + Lines.origin( m_area.center ); + m_lines.emplace_back( Lines ); + m_lines.back().create_geometry( m_geometrybank ); + return; + } for( auto &targetlines : m_lines ) { // try to merge shapes with matching view ranges... auto const &targetlinesdata { targetlines.data() }; @@ -632,6 +650,10 @@ basic_cell::center( glm::dvec3 Center ) { void basic_cell::create_geometry( gfx::geometrybank_handle const &Bank ) { + // remember the bank for *all* cells (even ones empty at bake time): a deferred visual + // node may activate this cell later, and insert() needs the live bank to upload into. + m_geometrybank = Bank; + if( false == m_active ) { return; } // nothing to do here for( auto &shape : m_shapesopaque ) { shape.create_geometry( Bank ); } @@ -850,6 +872,14 @@ basic_section::insert( shape_node Shape ) { } else { // large, opaque shapes are placed on section level + // if the section was already baked (deferred visual streaming), append straight into + // the live bank instead of merging into vertex-freed geometry -- see basic_cell::insert. + if( true == m_geometrycreated ) { + Shape.origin( m_area.center ); + m_shapes.emplace_back( Shape ); + m_shapes.back().create_geometry( m_geometrybank ); + return; + } for( auto &shape : m_shapes ) { // check first if the shape can't be merged with one of the shapes already present in the section if( true == shape.merge( Shape ) ) { diff --git a/scene/scene.h b/scene/scene.h index ef16fcd9..5f9bef41 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -234,6 +234,11 @@ public: } m_directories; // animation of owned items (legacy code, clean up along with track refactoring) bool m_geometrycreated { false }; + // bank this cell's geometry was baked into; remembered so shapes/lines inserted + // *after* the section was first rendered (deferred visual streaming) can be appended + // straight into the live bank instead of being silently dropped. null until the owning + // section bakes its geometry, which doubles as the "already baked" signal. + gfx::geometrybank_handle m_geometrybank {}; unsigned int m_framestamp { 0 }; // id of last rendered gfx frame TTrack *tTrackAnim = nullptr; // obiekty do przeliczenia animacji command_relay m_relay; diff --git a/scene/scenenodegroups.h b/scene/scenenodegroups.h index 350fa3d3..8e1bad60 100644 --- a/scene/scenenodegroups.h +++ b/scene/scenenodegroups.h @@ -40,6 +40,13 @@ public: // returns current active group, or null_handle if group stack is empty group_handle handle() const; + // makes Group the active group for subsequent inserts, without creating a new group. + // used by deferred (camera-ordered) visual loading to re-attach a node to the group it + // was originally read under during the enumeration pass. must be paired with pop_active(). + void + push_active( scene::group_handle const Group ) { m_activegroup.push( Group ); } + void + pop_active() { if( false == m_activegroup.empty() ) { m_activegroup.pop(); } } // places provided node in specified group void insert( scene::group_handle const Group, scene::basic_node *Node ); diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 7e640af9..fe371221 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -29,6 +29,10 @@ http://mozilla.org/MPL/2.0/. #include "rendering/renderer.h" #include "utilities/Logs.h" +#include +#include +#include + namespace simulation { std::shared_ptr @@ -50,9 +54,15 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) { state->scenariofile = Scenariofile; state->scratchpad.name = Scenariofile; // first pass loads infrastructure (tracks/traction/events/memcells/sounds + directives); - // visual nodes are skipped by the reader and loaded in a second pass. for a text/compile - // load (no twin) this is a no-op and everything loads in a single pass. - state->input.setReplayPass( scene::scenery_load_pass::infrastructure ); + // visual nodes are skipped by the reader and loaded in a second pass. this two-pass split + // is only valid when the top-level file is itself a replayable twin, because the visual + // pass is started via restartReplay() which needs a top-level reader. for a text/compile + // load (no top twin) we MUST stay in a single 'all' pass and load everything at once; + // otherwise visual nodes served by included twins (.incb) would be skipped in the infra + // pass and never rebuilt (restartReplay returns false), and all those models go missing. + if( true == state->input.isReplaying() ) { + state->input.setReplayPass( scene::scenery_load_pass::infrastructure ); + } scene::Groups.create(); if( false == state->input.ok() ) @@ -108,6 +118,12 @@ state_serializer::deserialize_continue(std::shared_ptr state cParser &Input = state->input; scene::scratch_data &Scratchpad = state->scratchpad; + // camera-ordered build phase: the visual replay has been enumerated into records and + // sorted; build them nearest-camera first, a budgeted slice per call. + if( state->enumdone ) { + return build_visual_records( state ); + } + // stateful directives that build objects/lists; on the visual (second) pass they are // skipped wholesale so their side effects (trainsets, events, cameras, ...) don't // duplicate. transform/group directives (origin/rotate/scale/group) and idempotent @@ -137,6 +153,18 @@ state_serializer::deserialize_continue(std::shared_ptr state token = Input.getToken(); continue; } + if( ( true == state->enumerate ) && ( token == "node" ) ) { + // capture the visual node (text + transform/group snapshot) for later, + // camera-ordered building, instead of building it in file order now + enumerate_visual_node( *state ); + auto timenow = std::chrono::steady_clock::now(); + if( std::chrono::duration_cast( timenow - timelast ).count() >= 8 ) { + Application.set_progress( Input.getProgress(), Input.getFullProgress() ); + return true; + } + token = Input.getToken(); + continue; + } } auto lookup = state->functionmap.find( token ); @@ -159,6 +187,28 @@ state_serializer::deserialize_continue(std::shared_ptr state token = Input.getToken(); } + if( ( true == state->visualphase ) && ( true == state->enumerate ) ) { + // visual replay exhausted: order the captured nodes by distance to the camera so + // the player's surroundings stream in first (terrain shapes ahead of models, so the + // ground appears before the props on it), then switch to the build phase. + auto const eye { Global.pCamera.Pos }; + for( auto &rec : state->records ) { + auto const d { rec.worldpos - eye }; + rec.sortkey = + ( rec.isshape ? + -1.0 : // shapes (terrain) first, regardless of distance + ( d.x * d.x + d.y * d.y + d.z * d.z ) ); + } + std::stable_sort( + std::begin( state->records ), std::end( state->records ), + []( visual_record const &L, visual_record const &R ) { + return L.sortkey < R.sortkey; } ); + WriteLog( "Progressive visual load: " + std::to_string( state->records.size() ) + " deferred nodes enumerated, building nearest-camera first" ); + state->enumerate = false; + state->enumdone = true; + return true; // proceed to the build phase + } + if( false == Scratchpad.initialized ) { // manually perform scenario initialization deserialize_firstinit( Input, Scratchpad ); @@ -172,6 +222,7 @@ state_serializer::deserialize_continue(std::shared_ptr state if( ( false == state->visualphase ) && ( true == Input.restartReplay( scene::scenery_load_pass::visual ) ) ) { state->visualphase = true; + state->enumerate = true; // capture deferred visual nodes, build them camera-ordered // rebuild the transform state from scratch for the visual pass: the directives // (origin/rotate/scale) are replayed in order, so resetting here reproduces the // single-pass placement exactly. without this, an unbalanced origin left on the @@ -188,6 +239,11 @@ state_serializer::deserialize_continue(std::shared_ptr state scene::Groups.update_map(); Region->create_map_geometry(); + // all nodes (including visual model instances) are now loaded, so initialise the + // events that bind to model instances; in a single-pass (text/compile) load nothing + // was deferred, but InitEvents() still skipped them, so do it here too + simulation::Events.InitInstanceEvents(); + // loading finished: flush the top-level scenario's binary twin now rather than // waiting for the parser to be destroyed (the loader keeps the state around) Input.flushBinaryTwin(); @@ -199,6 +255,166 @@ state_serializer::deserialize_continue(std::shared_ptr state return false; } +// captures one visual node (the "node" token already consumed) into a record for later, +// camera-ordered building: stores the node's verbatim tokens plus the transform/group +// context it was read under. numbers come through cParser losslessly, so the rebuild is +// exact. only model/triangles/lines reach the visual pass. +void +state_serializer::enumerate_visual_node( deserializer_state &State ) { + + cParser &Input = State.input; + scene::scratch_data &Scratchpad = State.scratchpad; + + // node header, original case preserved (model file paths are case-sensitive at replay) + auto const smax { Input.getToken( false ) }; + auto const smin { Input.getToken( false ) }; + auto const sname { Input.getToken( false ) }; + auto const stype { Input.getToken( false ) }; + + auto lower = []( std::string s ) { + for( auto &c : s ) { c = static_cast( std::tolower( static_cast( c ) ) ); } + return s; }; + + auto const typelc { lower( stype ) }; + std::string endtok; + bool ismodel { false }; + bool istriangles { false }; + if( typelc == "model" ) { + endtok = "endmodel"; ismodel = true; + } + else if( ( typelc == "triangles" ) || ( typelc == "triangle_strip" ) || ( typelc == "triangle_fan" ) ) { + endtok = "endtri"; istriangles = true; + } + else if( ( typelc == "lines" ) || ( typelc == "line_strip" ) || ( typelc == "line_loop" ) ) { + endtok = "endline"; + } + + // mirror deserialize_node's switch-trackbed skip here, while the source file name is + // still known (Input.Name() is the live include during replay; the per-record rebuild + // parser has no name). without this, fixed trackbed geometry from scenery/zwr*.inc that + // the in-order load drops would be rebuilt and duplicate the procedural trackbeds. + bool skipnode { false }; + if( ( true == istriangles ) && ( true == Global.CreateSwitchTrackbeds ) ) { + auto const name { Input.Name() }; + skipnode = + ( name.size() >= 15 ) + && name.starts_with( "scenery/zwr" ) + && name.ends_with( ".inc" ); + } + + // appends one token to the rebuilt node text, re-quoting it if it carries a token break + // (a quoted source token such as a name/path with spaces arrives here unquoted; without + // re-quoting it the rebuild parser would split it into several tokens) + auto appendtok = []( std::string &Text, std::string const &Token ) { + bool const needsquote { + Token.empty() + || ( Token.find_first_of( "\n\r\t ;" ) != std::string::npos ) }; + Text.push_back( ' ' ); + if( true == needsquote ) { Text.push_back( '\"' ); } + Text.append( Token ); + if( true == needsquote ) { Text.push_back( '\"' ); } }; + + std::string text; + text.reserve( 96 ); + text.append( "node" ); + appendtok( text, smax ); + appendtok( text, smin ); + appendtok( text, sname ); + appendtok( text, stype ); + + glm::dvec3 localpos { 0.0 }; + int posread { 0 }; + // read the rest of the node verbatim until its terminator (or the first end* token if the + // type is unrecognised), capturing a model's local position (the first 3 numbers) + for( ;; ) { + auto const tok { Input.getToken( false ) }; + if( tok.empty() ) { break; } // safety: input ran out + appendtok( text, tok ); + if( ( true == ismodel ) && ( posread < 3 ) ) { + localpos[ posread ] = std::atof( tok.c_str() ); + ++posread; + } + auto const toklc { lower( tok ) }; + if( false == endtok.empty() ) { + if( toklc == endtok ) { break; } + } + else if( ( toklc.size() >= 3 ) && ( 0 == toklc.compare( 0, 3, "end" ) ) ) { + break; + } + } + + if( true == skipnode ) { + // node consumed from the stream above; intentionally not recorded (matches the + // in-order load, which skips this geometry in favour of procedural trackbeds) + return; + } + + visual_record rec; + rec.text = std::move( text ); + rec.group = scene::Groups.handle(); + rec.has_offset = ( false == Scratchpad.location.offset.empty() ); + if( true == rec.has_offset ) { rec.offset = Scratchpad.location.offset.top(); } + rec.has_scale = ( false == Scratchpad.location.scale.empty() ); + if( true == rec.has_scale ) { rec.scale = Scratchpad.location.scale.top(); } + rec.rotation = Scratchpad.location.rotation; + rec.isshape = ( false == ismodel ); + if( true == ismodel ) { + rec.worldpos = transform( localpos, Scratchpad ); + } + State.records.emplace_back( std::move( rec ) ); +} + +// builds the enumerated visual records in (already-sorted) nearest-camera-first order, a +// budgeted slice per call. each node is rebuilt through the normal node path with its +// captured transform/group restored, so placement, grouping and the per-cell instance +// buckets come out identical to an in-order load. +bool +state_serializer::build_visual_records( std::shared_ptr state ) { + + scene::scratch_data &Scratchpad = state->scratchpad; + auto timelast { std::chrono::steady_clock::now() }; + + while( state->buildcursor < state->records.size() ) { + auto &rec = state->records[ state->buildcursor ]; + // restore the transform context captured for this node + Scratchpad.location.offset = {}; + if( true == rec.has_offset ) { Scratchpad.location.offset.push( rec.offset ); } + Scratchpad.location.scale = {}; + if( true == rec.has_scale ) { Scratchpad.location.scale.push( rec.scale ); } + Scratchpad.location.rotation = rec.rotation; + // re-attach to the node's original group and build via the normal node path (this + // routes instanceable models into their per-cell instance bucket automatically) + scene::Groups.push_active( rec.group ); + { + cParser nodeparser( rec.text, cParser::buffer_TEXT ); + nodeparser.getToken(); // consume "node" + deserialize_node( nodeparser, Scratchpad ); + } + scene::Groups.pop_active(); + std::string().swap( rec.text ); // release the captured text as we go + ++state->buildcursor; + + auto timenow = std::chrono::steady_clock::now(); + if( std::chrono::duration_cast( timenow - timelast ).count() >= 8 ) { + return true; // yield; resume next frame + } + } + + // every deferred visual node is now built: finalise the scenario + scene::Groups.close(); + scene::Groups.update_map(); + Region->create_map_geometry(); + // the visual model instances now exist, so initialise the events that bind to them + // (lights/animation/texture/visible) -- these were deferred during the infrastructure + // pass when their target models hadn't been streamed in yet + simulation::Events.InitInstanceEvents(); + state->input.flushBinaryTwin(); + scene::scenerybinary_wait_all(); + std::vector().swap( state->records ); + state->done = true; + return false; +} + void state_serializer::deserialize_isolated( cParser &Input, scene::scratch_data &Scratchpad ) { // first parameter specifies name of parent piece... diff --git a/simulation/simulationstateserializer.h b/simulation/simulationstateserializer.h index 81e23299..e1835bf8 100644 --- a/simulation/simulationstateserializer.h +++ b/simulation/simulationstateserializer.h @@ -14,6 +14,24 @@ http://mozilla.org/MPL/2.0/. namespace simulation { +// a deferred visual node captured during the visual enumeration pass, to be (re)built +// later in camera-distance order. holds the node's fully resolved tokens as text -- numbers +// come through cParser losslessly (std::to_chars shortest round-trip), so rebuilding from +// this text reproduces the exact same node -- plus a snapshot of the transform/group context +// it was read under, so the out-of-order rebuild places and groups it identically. +struct visual_record { + std::string text; // "node ... end" + glm::dvec3 offset { 0.0 }; // top of the origin stack at capture (identity if none) + glm::vec3 scale { 1.f }; // top of the scale stack at capture (identity if none) + glm::vec3 rotation { 0.f }; // active rotation at capture + bool has_offset { false }; + bool has_scale { false }; + scene::group_handle group {}; // group the node was read under + glm::dvec3 worldpos { 0.0 }; // transformed position (for models; sort key source) + bool isshape { false }; // terrain shape/lines -> load before models (ground first) + double sortkey { 0.0 }; // squared distance to camera; lower = built sooner +}; + struct deserializer_state { std::string scenariofile; cParser input; @@ -27,6 +45,13 @@ struct deserializer_state { bool visualphase { false }; // set once the whole load (both passes / single text pass) has fully finished bool done { false }; + // visual phase sub-state: while enumerate is true the visual pass captures deferred + // nodes into records instead of building them; once the replay is exhausted they are + // sorted by camera distance and built in that order (enumdone). + bool enumerate { false }; + bool enumdone { false }; + std::vector records; + std::size_t buildcursor { 0 }; deserializer_state(std::string const &File, cParser::buffertype const Type, const std::string &Path, bool const Loadtraction) : scenariofile(File), input(File, Type, Path, Loadtraction) { } @@ -68,6 +93,10 @@ private: void deserialize_endgroup( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_light( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_node( cParser &Input, scene::scratch_data &Scratchpad ); + // visual streaming (camera-ordered deferred load): capture one visual node (already + // past its "node" token) into a record; build budgeted records in sorted order. + void enumerate_visual_node( deserializer_state &State ); + bool build_visual_records( std::shared_ptr State ); void deserialize_origin( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_endorigin( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_scale( cParser &Input, scene::scratch_data &Scratchpad ); diff --git a/utilities/parser.h b/utilities/parser.h index 4488adb3..079a845f 100644 --- a/utilities/parser.h +++ b/utilities/parser.h @@ -54,6 +54,9 @@ class cParser //: public std::stringstream // open include child. used to run a second pass (visual) over an already-loaded twin. // returns false if this parser isn't replaying a twin (no second pass possible). bool restartReplay( scene::scenery_load_pass Pass ); + // true when this (top-level) file is served from a binary twin, i.e. a second + // (visual) pass via restartReplay() is possible. false for a text/compile load. + bool isReplaying() const { return m_replay; } // methods: template cParser & diff --git a/world/Event.cpp b/world/Event.cpp index 3cbb53ab..6e37de28 100644 --- a/world/Event.cpp +++ b/world/Event.cpp @@ -2493,6 +2493,21 @@ void event_manager::InitEvents() { //łączenie eventów z pozostałymi obiektami for( auto *event : m_events ) { + // events binding to model instances are deferred: with progressive loading their + // target models stream in after this (infrastructure) pass, so binding them now + // would fail. InitInstanceEvents() initialises them once the visuals are in place. + if( true == event->binds_to_instances() ) { continue; } + event->init(); + if( event->m_delay < 0 ) { AddToQuery( event, nullptr ); } + } +} + +// initializes the events deferred by InitEvents() (those binding to model instances), +// after the (progressively loaded) visual nodes have been built so the models exist. +void +event_manager::InitInstanceEvents() { + for( auto *event : m_events ) { + if( false == event->binds_to_instances() ) { continue; } event->init(); if( event->m_delay < 0 ) { AddToQuery( event, nullptr ); } } diff --git a/world/Event.h b/world/Event.h index d04b4c87..395d4365 100644 --- a/world/Event.h +++ b/world/Event.h @@ -55,6 +55,13 @@ public: virtual void init() = 0; + // true for events that bind to visual model instances (lights/animation/texture/ + // visible). With progressive loading these models are streamed in after the + // infrastructure pass, so such events must be initialised only once the deferred + // visual nodes exist -- not during the early InitEvents() of the infrastructure pass. + virtual + bool + binds_to_instances() const { return false; } // executes event virtual void @@ -393,6 +400,7 @@ public: // methods // prepares event for use void init() override; + bool binds_to_instances() const override { return true; } private: // types @@ -427,6 +435,7 @@ public: // methods // prepares event for use void init() override; + bool binds_to_instances() const override { return true; } private: // methods @@ -456,6 +465,7 @@ public: // methods // prepares event for use void init() override; + bool binds_to_instances() const override { return true; } private: // methods @@ -550,6 +560,7 @@ public: // methods // prepares event for use void init() override; + bool binds_to_instances() const override { return true; } private: // methods @@ -690,9 +701,15 @@ public: // legacy method, executes queued events bool CheckQuery(); - // legacy method, initializes events after deserialization from scenario file + // legacy method, initializes events after deserialization from scenario file. + // events that bind to visual model instances are skipped here and initialised later + // by InitInstanceEvents(), once the (progressively loaded) visual nodes are in place. void InitEvents(); + // initializes the events skipped by InitEvents() (those binding to model instances). + // called once the deferred visual nodes have been built, so their target models exist. + void + InitInstanceEvents(); // legacy method, initializes event launchers after deserialization from scenario file void InitLaunchers();