/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" #include "scene/eu7/eu7_section_stream.h" #include "scene/eu7/eu7_chunks.h" #include "scene/eu7/eu7_load_stats.h" #include "scene/eu7/eu7_pack_bench.h" #include "model/AnimModel.h" #include "model/MdlMngr.h" #include "scene/eu7/eu7_pack_mesh_loader.h" #include "scene/eu7/eu7_model_prefetch.h" #include "scene/eu7/eu7_reader.h" #include "scene/eu7/eu7_section.h" #include "simulation/simulation.h" #include "simulation/simulationstateserializer.h" #include "utilities/Globals.h" #include "utilities/Logs.h" #include "utilities/Timer.h" #include "utilities/utilities.h" #include "vehicle/DynObj.h" #include "world/Track.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace scene::eu7 { [[nodiscard]] bool ring_section_in_radius( int row, int column, int center_row, int center_column, int radius_km ); namespace { constexpr double kDrainBudgetMs { 12.0 }; constexpr double kLoaderDrainBudgetMs { 256.0 }; constexpr double kFrameDrainBudgetMs { 10.0 }; constexpr std::size_t kLoaderSectionsPerDrain { 8 }; constexpr double kGameplayApplyBudgetMs { 6.0 }; constexpr double kCatchupApplyBudgetMs { 8.0 }; constexpr std::size_t kGameplaySliceInstances { 48 }; constexpr std::size_t kCatchupSliceInstances { 48 }; constexpr std::size_t kFrameSliceInstances { 24 }; constexpr std::size_t kLoaderSliceInstances { 768 }; constexpr std::size_t kGameplaySliceColdMeshes { 4 }; constexpr std::size_t kCatchupSliceColdMeshes { 6 }; constexpr std::size_t kFrameSliceColdMeshes { 2 }; constexpr std::size_t kLoaderSliceColdMeshes { 32 }; constexpr double kGameplayColdBudgetMs { 6.0 }; constexpr double kCatchupColdBudgetMs { 8.0 }; constexpr double kFrameColdBudgetMs { 4.0 }; constexpr int kInitialBootstrapEnqueueRadius { 2 }; constexpr int kInitialBootstrapRadius { 3 }; constexpr float kEarlyDismissRingProgress { 0.65f }; constexpr int kStreamRadius { kSectionStreamTargetRadiusKm }; constexpr int kMaintenanceLookaheadMax { 6 }; constexpr double kRadiusFillApplyBudgetMs { 8.0 }; constexpr std::size_t kRadiusFillSliceInstances { 16 }; constexpr std::size_t kRadiusFillMaxChunksPerDrain { 2 }; constexpr std::size_t kRadiusFillMaxInFlight { 24 }; constexpr std::size_t kRadiusFillMaxReady { 14 }; constexpr float kRadiusFillOuterTarget { 0.98f }; constexpr int kSectionUnloadMarginKm { 3 }; constexpr int kSectionUnloadCameraGuardKm { 5 }; constexpr int kSectionUnloadMaxSpeedExtraKm { 10 }; constexpr double kSectionUnloadKeepRadiusDecayKmPerSec { 2.0 }; constexpr int kSectionUnloadTeleportJumpKm { 4 }; constexpr std::size_t kMeshCacheLruCap { 2000 }; constexpr std::size_t kNodedataCacheLruCap { 2000 }; constexpr int kMovementLookahead { 10 }; constexpr std::size_t kMaxPackStreamWorkers { 16 }; constexpr std::size_t kMaxInFlightSections { 20 }; constexpr std::size_t kMaxReadySections { 10 }; constexpr double kStreamStatusLogIntervalSec { 5.0 }; constexpr double kReenqueueDistanceM { 500.0 }; constexpr double kCatchupReenqueueDistanceM { 40.0 }; constexpr std::size_t kHeavySectionModelThreshold { 32 }; constexpr std::size_t kPackTextureWarmSlice { 8 }; constexpr double kPackTextureWarmBudgetMs { 4.0 }; constexpr double kUrgentApplyBudgetMs { 10.0 }; constexpr double kRingDeficitDrainBudgetMs { 8.0 }; constexpr double kSmoothApplyTargetMs { 7.0 }; constexpr double kSmoothApplyDeficitTargetMs { 8.0 }; constexpr std::size_t kUrgentSliceInstances { 48 }; constexpr std::size_t kUrgentSliceColdMeshes { 4 }; constexpr double kUrgentColdBudgetMs { 6.0 }; constexpr std::size_t kUrgentMaxChunksPerDrain { 2 }; constexpr std::size_t kFrameMaxChunksPerDrain { 1 }; constexpr std::size_t kRingDeficitMaxChunksPerDrain { 2 }; constexpr double kReadyTexturePrefetchBudgetMs { 14.0 }; constexpr std::size_t kReadyTexturePrefetchSlice { 256 }; constexpr int kUmesPrefetchMaxDistanceKm { 6 }; constexpr double kReadyUmesPrefetchBudgetMs { 16.0 }; constexpr std::size_t kReadyUmesPrefetchMaxMeshes { 64 }; constexpr std::size_t kCatchupMaxInFlightSections { 32 }; constexpr std::size_t kCatchupMaxReadySections { 16 }; constexpr int kUrgentSectionDistanceKm { 1 }; constexpr int kFastFlightApplyMaxDistanceKm { 3 }; constexpr int kFarApplySlowDistanceKm { 4 }; constexpr int kPreemptPendingDistanceKm { 2 }; constexpr float kRingGatedOuterThreshold { 0.35f }; constexpr float kRingGatedInnerTarget { 0.70f }; constexpr float kRingBoostOuterThreshold { 0.55f }; constexpr float kRingStrongBoostOuterThreshold { 0.75f }; constexpr std::size_t kPreemptReadyQueueThreshold { 8 }; constexpr int kWorkerContinueMaxDistanceKm { 8 }; constexpr std::size_t kFastFlightMaxFarInFlight { 3 }; constexpr double kFastFlightRingGateSpeedMps { 600.0 }; constexpr double kRingBoostInFlightSpeedMps { 600.0 }; constexpr std::size_t kRingBoostMaxInFlight { 12 }; constexpr std::size_t kRingStrongBoostMaxInFlight { 16 }; constexpr std::size_t kRingBoostMaxReady { 8 }; constexpr std::size_t kRingStrongBoostMaxReady { 10 }; constexpr double kPreemptDisableSpeedMps { 400.0 }; constexpr double kTeleportReenqueueDistanceM { 2000.0 }; constexpr float kStationaryCatchupRingThreshold { 0.70f }; constexpr double kStationaryCatchupSpeedMps { 5.0 }; constexpr std::size_t kBootstrapDrainMs { 32 }; constexpr std::size_t kBootstrapTimeoutMs { 120000 }; constexpr std::size_t kBootstrapApplyModelThreshold { 400 }; constexpr std::chrono::milliseconds kPresentableHoldMs { 0 }; constexpr std::chrono::seconds kLoadingScreenMaxBlockSec { 90 }; std::optional g_ring_ready_since; std::optional g_loading_block_started; bool g_loading_screen_dismissed { false }; bool g_stream_catchup { false }; std::size_t g_stream_max_in_flight { kMaxInFlightSections }; std::size_t g_stream_max_ready { kMaxReadySections }; struct PackSectionJob { int row { 0 }; int column { 0 }; std::size_t section_idx { 0 }; int priority { 0 }; std::uint32_t next_chunk { 0 }; }; struct PackSectionReady { int row { 0 }; int column { 0 }; std::size_t section_idx { 0 }; std::unique_ptr> models; std::vector unique_textures; std::shared_ptr path_tables; bool failed { false }; bool section_final { true }; std::size_t apply_offset { 0 }; std::size_t texture_warm_offset { 0 }; std::size_t umes_prefetch_offset { 0 }; }; struct SectionStreamState { Eu7Module const *module { nullptr }; std::string path; simulation::state_serializer *serializer { nullptr }; std::unordered_set loaded_sections; std::unordered_set in_flight_sections; int center_row { -1 }; int center_column { -1 }; int radius { kStreamRadius }; bool active { false }; bool bootstrap_active { false }; bool bootstrap_pending { false }; bool radius_fill_active { false }; std::optional pending_apply; std::size_t pending_apply_offset { 0 }; glm::dvec3 last_enqueue_position {}; bool has_last_enqueue_position { false }; glm::dvec3 anchor_position {}; bool has_anchor_position { false }; float last_inner_ring { 1.f }; float last_outer_ring { 1.f }; int unload_keep_radius { 0 }; double unload_keep_radius_floor { 0.0 }; double unload_keep_radius_last_update { 0.0 }; std::unordered_map mesh_cache; std::unordered_map nodedata_cache; std::list mesh_lru; std::unordered_map::iterator> mesh_lru_iters; std::unordered_map mesh_ref_counts; std::unordered_map> section_mesh_keys; std::list nodedata_lru; std::unordered_map::iterator> nodedata_lru_iters; std::unordered_map nodedata_ref_counts; std::unordered_map> section_nodedata_keys; std::vector workers; std::atomic worker_exit { false }; threading::lockable> jobs; threading::lockable> ready; threading::condition_variable work_cv; }; SectionStreamState g_stream; [[nodiscard]] int section_manhattan_sections( int const row, int const column, int const center_row, int const center_column ); [[nodiscard]] std::size_t count_far_in_flight_sections( int const max_distance_km ); [[nodiscard]] bool should_block_far_enqueue( int const row, int const column, bool const lookahead_enqueue ); void push_prioritized_job( PackSectionJob job ); void reprioritize_job_queue(); [[nodiscard]] bool enqueue_section_if_needed( int const row, int const column, int const priority, bool const lookahead_enqueue = false ); void sync_stream_limits( glm::dvec3 const &world_position ); void maybe_unload_distant_sections( int const prev_center_row, int const prev_center_column ); void evict_unreferenced_stream_caches(); [[nodiscard]] bool try_dequeue_ready_batch(); void finalize_section( PackSectionReady const &batch ); void fail_section( std::size_t const section_idx, int const row = -1, int const column = -1 ); void enqueue_failed_section( PackSectionJob const &job ); void release_pending_buffer(); void prefetch_ready_queue_umes_worker( double budget_ms, std::size_t max_meshes ); [[nodiscard]] bool gameplay_stream_mode(); [[nodiscard]] std::size_t warm_pack_section_textures( PackSectionReady &batch, std::size_t const model_offset, std::size_t const model_count, double const budget_ms = 0.0 ) { if( batch.models == nullptr || model_count == 0 ) { return 0; } auto const begin { model_offset }; if( begin >= batch.models->size() ) { return 0; } auto const remaining { std::min( model_count, batch.models->size() - begin ) }; auto const warmed { warm_pack_textures_main( batch.models->data() + begin, remaining, budget_ms ) }; return warmed; } [[nodiscard]] bool section_has_pack_models( int const row, int const column ); [[nodiscard]] std::size_t pending_section_total(); [[nodiscard]] double camera_stream_speed_mps(); [[nodiscard]] glm::dvec3 rotate_velocity_y( glm::dvec3 const &velocity, double const yaw ) { auto const s { std::sin( yaw ) }; auto const c { std::cos( yaw ) }; return { c * velocity.x + s * velocity.z, velocity.y, c * velocity.z - s * velocity.x }; } [[nodiscard]] std::size_t stream_worker_count() { auto const hardware { std::thread::hardware_concurrency() }; auto const threads { hardware > 0 ? hardware : 1u }; auto const percent { static_cast( std::clamp( Global.eu7_pack_stream_workers_percent, 1, 100 ) ) }; auto const requested { std::max( std::size_t { 1 }, static_cast( threads ) * percent / 100 ) }; return std::min( requested, kMaxPackStreamWorkers ); } void touch_string_lru( std::string const &key, std::list &order, std::unordered_map::iterator> &iters ) { auto const found { iters.find( key ) }; if( found == iters.end() ) { order.push_back( key ); iters.emplace( key, std::prev( order.end() ) ); return; } order.splice( order.end(), order, found->second ); } void retain_cache_key( std::size_t const section_idx, std::string key, std::unordered_map> §ion_keys, std::unordered_map &ref_counts, std::list &lru_order, std::unordered_map::iterator> &lru_iters ) { if( key.empty() ) { return; } auto &keys { section_keys[ section_idx ] }; if( false == keys.insert( key ).second ) { touch_string_lru( key, lru_order, lru_iters ); return; } ++ref_counts[ key ]; touch_string_lru( key, lru_order, lru_iters ); } void release_section_key_refs( std::size_t const section_idx, std::unordered_map> §ion_keys, std::unordered_map &ref_counts ) { auto const found { section_keys.find( section_idx ) }; if( found == section_keys.end() ) { return; } for( auto const &key : found->second ) { auto ref_it { ref_counts.find( key ) }; if( ref_it == ref_counts.end() ) { continue; } if( ref_it->second <= 1 ) { ref_counts.erase( ref_it ); } else { --ref_it->second; } } section_keys.erase( found ); } void evict_cache_lru_if_needed( std::unordered_map &ref_counts, std::unordered_map *const mesh_cache, std::unordered_map *const nodedata_cache, std::list &lru_order, std::unordered_map::iterator> &lru_iters, std::size_t const cap, std::uint64_t Eu7PackBench::*const eviction_counter ) { while( lru_order.size() > cap ) { bool evicted { false }; for( auto it { lru_order.begin() }; it != lru_order.end(); ++it ) { auto const &key { *it }; auto const ref_it { ref_counts.find( key ) }; if( ref_it != ref_counts.end() && ref_it->second > 0 ) { continue; } lru_iters.erase( key ); lru_order.erase( it ); if( mesh_cache != nullptr ) { mesh_cache->erase( key ); } if( nodedata_cache != nullptr ) { nodedata_cache->erase( key ); } pack_bench_inc( eviction_counter ); evicted = true; break; } if( false == evicted ) { break; } } } void retain_section_mesh_keys( std::size_t const section_idx, Eu7Model const *const models, std::size_t const offset, std::size_t const count ) { if( models == nullptr || count == 0 ) { return; } for( std::size_t i { 0 }; i < count; ++i ) { auto model_file { models[ offset + i ].model_file }; if( model_file.empty() || model_file == "notload" ) { continue; } replace_slashes( model_file ); retain_cache_key( section_idx, model_file, g_stream.section_mesh_keys, g_stream.mesh_ref_counts, g_stream.mesh_lru, g_stream.mesh_lru_iters ); } } void retain_section_nodedata_keys( std::size_t const section_idx, Eu7Model const *const models, std::size_t const offset, std::size_t const count ) { if( models == nullptr || count == 0 ) { return; } for( std::size_t i { 0 }; i < count; ++i ) { retain_cache_key( section_idx, pack_nodedata_cache_key( models[ offset + i ] ), g_stream.section_nodedata_keys, g_stream.nodedata_ref_counts, g_stream.nodedata_lru, g_stream.nodedata_lru_iters ); } } void release_section_cache_refs( std::size_t const section_idx ) { release_section_key_refs( section_idx, g_stream.section_mesh_keys, g_stream.mesh_ref_counts ); release_section_key_refs( section_idx, g_stream.section_nodedata_keys, g_stream.nodedata_ref_counts ); evict_unreferenced_stream_caches(); } void evict_unreferenced_stream_caches() { evict_cache_lru_if_needed( g_stream.mesh_ref_counts, &g_stream.mesh_cache, nullptr, g_stream.mesh_lru, g_stream.mesh_lru_iters, kMeshCacheLruCap, &Eu7PackBench::stream_mesh_cache_evictions ); evict_cache_lru_if_needed( g_stream.nodedata_ref_counts, nullptr, &g_stream.nodedata_cache, g_stream.nodedata_lru, g_stream.nodedata_lru_iters, kNodedataCacheLruCap, &Eu7PackBench::stream_nodedata_cache_evictions ); } [[nodiscard]] PackMeshLoadWait stream_mesh_load_wait_policy() { return gameplay_stream_mode() ? PackMeshLoadWait::NonBlocking : PackMeshLoadWait::BlockUntilReady; } [[nodiscard]] bool pack_mesh_ready_for_slice( std::string const &model_file ) { if( false == try_adopt_pack_mesh_for_slice( model_file, g_stream.mesh_cache ) ) { return false; } auto const found { g_stream.mesh_cache.find( model_file ) }; return found != g_stream.mesh_cache.end() && found->second != nullptr && TModelsManager::IsModelCached( model_file ); } [[nodiscard]] bool ensure_stream_pack_mesh( std::string model_file, double const block_budget_ms = 0.0 ) { if( model_file.empty() || model_file == "notload" ) { return false; } replace_slashes( model_file ); auto *const mesh { ensure_pack_mesh_in_session_cache( model_file, g_stream.mesh_cache, stream_mesh_load_wait_policy(), block_budget_ms ) }; if( mesh != nullptr ) { touch_string_lru( model_file, g_stream.mesh_lru, g_stream.mesh_lru_iters ); } return mesh != nullptr; } void cancel_section_stream_work( std::size_t const section_idx ) { { std::lock_guard lock { g_stream.jobs.mutex }; auto &jobs { g_stream.jobs.data }; jobs.erase( std::remove_if( jobs.begin(), jobs.end(), [&]( PackSectionJob const &job ) { return job.section_idx == section_idx; } ), jobs.end() ); } { std::lock_guard lock { g_stream.ready.mutex }; for( auto it { g_stream.ready.data.begin() }; it != g_stream.ready.data.end(); ) { if( it->section_idx != section_idx ) { ++it; continue; } if( it->models != nullptr ) { it->models->clear(); it->models->shrink_to_fit(); it->models.reset(); } it = g_stream.ready.data.erase( it ); } } if( g_stream.pending_apply.has_value() && g_stream.pending_apply->section_idx == section_idx ) { release_pending_buffer(); } g_stream.in_flight_sections.erase( section_idx ); g_stream.loaded_sections.erase( section_idx ); } [[nodiscard]] std::size_t unload_pack_section( int const row, int const column ) { auto const section_idx { section_index( row, column ) }; if( false == g_stream.loaded_sections.contains( section_idx ) ) { return 0; } std::size_t removed { 0 }; if( g_stream.serializer != nullptr ) { removed = g_stream.serializer->unload_eu7_pack_section( row, column ); } release_section_cache_refs( section_idx ); cancel_section_stream_work( section_idx ); if( removed > 0 ) { if( load_stats().pack_models >= removed ) { load_stats().pack_models -= removed; } else { load_stats().pack_models = 0; } pack_bench_inc( &Eu7PackBench::stream_unload_instances, removed ); } pack_bench_inc( &Eu7PackBench::stream_sections_unloaded ); return removed; } void update_unload_keep_radius_hysteresis() { if( g_stream.center_row < 0 || g_stream.center_column < 0 ) { return; } auto const speed { camera_stream_speed_mps() }; auto const speed_extra { std::min( static_cast( speed / 150.0 ), kSectionUnloadMaxSpeedExtraKm ) }; auto const desired_keep_radius { static_cast( g_stream.radius + kSectionUnloadMarginKm + speed_extra ) }; auto const now { Timer::GetTime() }; if( g_stream.unload_keep_radius_floor <= 0.0 ) { g_stream.unload_keep_radius_floor = desired_keep_radius; g_stream.unload_keep_radius_last_update = now; g_stream.unload_keep_radius = static_cast( std::ceil( desired_keep_radius ) ); return; } auto const dt { std::max( 0.0, now - g_stream.unload_keep_radius_last_update ) }; g_stream.unload_keep_radius_last_update = now; if( desired_keep_radius > g_stream.unload_keep_radius_floor ) { g_stream.unload_keep_radius_floor = desired_keep_radius; } else if( desired_keep_radius < g_stream.unload_keep_radius_floor && dt > 0.0 ) { g_stream.unload_keep_radius_floor = std::max( desired_keep_radius, g_stream.unload_keep_radius_floor - dt * kSectionUnloadKeepRadiusDecayKmPerSec ); } g_stream.unload_keep_radius = static_cast( std::ceil( g_stream.unload_keep_radius_floor ) ); } void maybe_unload_distant_sections( int const prev_center_row, int const prev_center_column ) { if( false == g_loading_screen_dismissed || g_stream.bootstrap_active ) { return; } if( g_stream.center_row < 0 || g_stream.center_column < 0 ) { return; } auto const [cam_row, cam_col] { section_row_column( Global.pCamera.Pos ) }; auto const keep_radius { g_stream.unload_keep_radius }; auto const center_jump { prev_center_row >= 0 && prev_center_column >= 0 ? section_manhattan_sections( g_stream.center_row, g_stream.center_column, prev_center_row, prev_center_column ) : 0 }; if( center_jump >= kSectionUnloadTeleportJumpKm ) { return; } std::vector to_unload; to_unload.reserve( g_stream.loaded_sections.size() ); for( auto const section_idx : g_stream.loaded_sections ) { auto const row { static_cast( section_idx / static_cast( kRegionSideSectionCount ) ) }; auto const column { static_cast( section_idx % static_cast( kRegionSideSectionCount ) ) }; if( ring_section_in_radius( row, column, cam_row, cam_col, kSectionUnloadCameraGuardKm ) ) { continue; } if( ring_section_in_radius( row, column, g_stream.center_row, g_stream.center_column, keep_radius ) ) { continue; } to_unload.push_back( section_idx ); } if( to_unload.empty() ) { return; } static double s_last_unload_log { 0.0 }; auto const now { Timer::GetTime() }; auto const should_log { now - s_last_unload_log >= kStreamStatusLogIntervalSec }; std::size_t total_removed { 0 }; for( auto const section_idx : to_unload ) { auto const row { static_cast( section_idx / static_cast( kRegionSideSectionCount ) ) }; auto const column { static_cast( section_idx % static_cast( kRegionSideSectionCount ) ) }; total_removed += unload_pack_section( row, column ); } if( should_log && total_removed > 0 ) { s_last_unload_log = now; WriteLog( "EU7 PACK: unload " + std::to_string( to_unload.size() ) + " sekcji, inst=" + std::to_string( total_removed ) + ", loaded=" + std::to_string( g_stream.loaded_sections.size() ) + ", mesh_cache=" + std::to_string( g_stream.mesh_cache.size() ) ); } } void reset_stream_fields(); void note_apply_progress(); void reset_stream_fields() { g_stream.module = nullptr; g_stream.path.clear(); g_stream.serializer = nullptr; g_stream.loaded_sections.clear(); g_stream.in_flight_sections.clear(); g_stream.center_row = -1; g_stream.center_column = -1; g_stream.radius = kStreamRadius; g_stream.active = false; g_stream.bootstrap_active = false; g_stream.bootstrap_pending = false; g_stream.radius_fill_active = false; g_stream.pending_apply.reset(); g_stream.pending_apply_offset = 0; g_stream.has_last_enqueue_position = false; g_stream.anchor_position = {}; g_stream.has_anchor_position = false; g_stream.last_inner_ring = 1.f; g_stream.last_outer_ring = 1.f; g_stream.unload_keep_radius = 0; g_stream.unload_keep_radius_floor = 0.0; g_stream.unload_keep_radius_last_update = 0.0; g_stream.mesh_cache.clear(); g_stream.nodedata_cache.clear(); g_stream.mesh_lru.clear(); g_stream.mesh_lru_iters.clear(); g_stream.mesh_ref_counts.clear(); g_stream.section_mesh_keys.clear(); g_stream.nodedata_lru.clear(); g_stream.nodedata_lru_iters.clear(); g_stream.nodedata_ref_counts.clear(); g_stream.section_nodedata_keys.clear(); reset_pack_texture_warm_cache(); note_apply_progress(); } [[nodiscard]] Eu7Module const & stream_module() { if( g_stream.module == nullptr ) { throw std::runtime_error( "EU7 PACK: stream module nie jest ustawiony" ); } return *g_stream.module; } void release_pending_buffer() { if( false == g_stream.pending_apply.has_value() ) { g_stream.pending_apply_offset = 0; return; } if( g_stream.pending_apply->models != nullptr ) { g_stream.pending_apply->models->clear(); g_stream.pending_apply->models.reset(); } g_stream.pending_apply.reset(); g_stream.pending_apply_offset = 0; } [[nodiscard]] std::size_t ready_queue_size() { std::lock_guard lock { g_stream.ready.mutex }; return g_stream.ready.data.size(); } [[nodiscard]] bool camera_section_needs_priority() { if( g_stream.center_row < 0 || g_stream.center_column < 0 ) { return false; } if( camera_stream_speed_mps() <= 200.0 ) { return false; } auto const section_idx { section_index( g_stream.center_row, g_stream.center_column ) }; if( g_stream.loaded_sections.contains( section_idx ) ) { return false; } return section_has_pack_models( g_stream.center_row, g_stream.center_column ); } [[nodiscard]] bool should_throttle_far_stream_jobs( int const row, int const column ) { if( false == camera_section_needs_priority() ) { return false; } if( row == g_stream.center_row && column == g_stream.center_column ) { return false; } return section_manhattan_sections( row, column, g_stream.center_row, g_stream.center_column ) > kFastFlightApplyMaxDistanceKm; } [[nodiscard]] bool stream_backpressure() { if( g_stream.bootstrap_active ) { return false; } if( camera_section_needs_priority() ) { if( g_stream.in_flight_sections.size() >= 6 ) { return true; } if( ready_queue_size() >= 4 ) { return true; } } if( false == g_stream.bootstrap_active && g_stream.in_flight_sections.size() >= g_stream_max_in_flight ) { return true; } return ready_queue_size() >= g_stream_max_ready; } [[nodiscard]] double camera_stream_speed_mps() { auto const rotated { rotate_velocity_y( Global.pCamera.Velocity, Global.pCamera.Angle.y ) }; return glm::length( rotated ) * 5.0; } [[nodiscard]] bool stream_ring_deficit(); [[nodiscard]] bool pending_apply_is_urgent(); [[nodiscard]] bool in_radius_fill_phase(); [[nodiscard]] bool in_radius_fill_phase() { return g_stream.radius_fill_active; } [[nodiscard]] double frame_drain_budget_ms() { if( in_radius_fill_phase() ) { return kRadiusFillApplyBudgetMs; } if( stream_ring_deficit() ) { return kRingDeficitDrainBudgetMs; } auto const last_chunk { pack_bench_stream().last_chunk_ms }; if( last_chunk >= 32.0 ) { return 3.0; } if( last_chunk >= 16.0 ) { return 5.0; } if( last_chunk >= 12.0 ) { return 7.0; } return kFrameDrainBudgetMs; } [[nodiscard]] double gameplay_apply_budget_ms(); [[nodiscard]] bool stream_ring_deficit() { if( in_radius_fill_phase() ) { return g_stream.last_outer_ring < kRadiusFillOuterTarget; } return g_stream.last_inner_ring < kRingGatedInnerTarget; } [[nodiscard]] int umes_prefetch_max_distance_km() { auto const speed { camera_stream_speed_mps() }; if( g_stream_catchup || speed > 80.0 ) { return std::min( g_stream.radius, kWorkerContinueMaxDistanceKm ); } return kUmesPrefetchMaxDistanceKm; } [[nodiscard]] double ready_texture_prefetch_budget_ms() { auto const speed { camera_stream_speed_mps() }; auto const ring_deficit { stream_ring_deficit() }; if( ring_deficit && speed > 200.0 ) { return kPackTextureWarmBudgetMs; } if( speed > 500.0 && pending_apply_is_urgent() ) { return kPackTextureWarmBudgetMs * 0.5; } double budget { kReadyTexturePrefetchBudgetMs * 0.5 }; if( g_stream_catchup || speed > 80.0 ) { budget = kReadyTexturePrefetchBudgetMs; } if( g_stream_catchup ) { auto const apply_budget { pending_apply_is_urgent() ? kUrgentApplyBudgetMs : gameplay_apply_budget_ms() }; return std::min( budget, apply_budget * 0.25 ); } return std::min( budget, kDrainBudgetMs * 0.35 ); } [[nodiscard]] glm::dvec3 camera_travel_forward() { if( glm::length2( Global.pCamera.Velocity ) > 0.01 ) { auto const rotated { rotate_velocity_y( Global.pCamera.Velocity, Global.pCamera.Angle.y ) }; auto horizontal { glm::dvec3 { rotated.x, 0.0, rotated.z } }; auto const length { glm::length( horizontal ) }; if( length > 0.01 ) { return horizontal / length; } } auto const yaw { Global.pCamera.Angle.y }; return glm::dvec3 { std::sin( yaw ), 0.0, std::cos( yaw ) }; } [[nodiscard]] double gameplay_apply_budget_ms() { if( in_radius_fill_phase() ) { return kRadiusFillApplyBudgetMs; } double budget { kGameplayApplyBudgetMs }; if( g_stream_catchup ) { if( stream_ring_deficit() ) { budget = pending_apply_is_urgent() ? kUrgentApplyBudgetMs : 8.0; } else if( g_stream.last_outer_ring < kRingBoostOuterThreshold && camera_stream_speed_mps() > kPreemptDisableSpeedMps ) { budget = 8.0; } else { budget = kCatchupApplyBudgetMs; } } if( g_stream.last_inner_ring < 0.5f ) { budget = std::max( budget, 12.0 ); } return budget; } [[nodiscard]] std::size_t gameplay_slice_instances() { return g_stream_catchup ? kCatchupSliceInstances : kGameplaySliceInstances; } [[nodiscard]] std::size_t gameplay_slice_cold_meshes() { return g_stream_catchup ? kCatchupSliceColdMeshes : kGameplaySliceColdMeshes; } [[nodiscard]] double gameplay_cold_budget_ms() { return g_stream_catchup ? kCatchupColdBudgetMs : kGameplayColdBudgetMs; } [[nodiscard]] bool pending_section_near_camera( int const max_distance_km = kUrgentSectionDistanceKm ) { if( false == g_stream.pending_apply.has_value() ) { return false; } auto const &batch { *g_stream.pending_apply }; return section_manhattan_sections( batch.row, batch.column, g_stream.center_row, g_stream.center_column ) <= max_distance_km; } [[nodiscard]] int pending_section_distance_km() { if( false == g_stream.pending_apply.has_value() ) { return std::numeric_limits::max(); } auto const &batch { *g_stream.pending_apply }; return section_manhattan_sections( batch.row, batch.column, g_stream.center_row, g_stream.center_column ); } [[nodiscard]] bool pending_apply_is_urgent() { if( in_radius_fill_phase() ) { return pending_section_near_camera( kSectionStreamGameplayRadiusKm ); } if( stream_ring_deficit() ) { return true; } auto const speed { camera_stream_speed_mps() }; if( speed <= 80.0 && false == g_stream_catchup ) { return false; } auto const max_dist { speed > 600.0 ? kFastFlightApplyMaxDistanceKm : kUrgentSectionDistanceKm }; return pending_section_near_camera( max_dist ); } [[nodiscard]] std::size_t apply_time_target_slice( double const target_ms, std::size_t const ceiling ) { auto const &bench { pack_bench_stream() }; auto effective_apply_ms { bench.last_apply_ms }; if( bench.last_chunk_ms > effective_apply_ms && bench.last_chunk_instances > 0 ) { effective_apply_ms = bench.last_chunk_ms; } if( effective_apply_ms >= 1.0 && bench.last_chunk_instances > 0 ) { auto const scaled { static_cast( static_cast( bench.last_chunk_instances ) * ( target_ms / effective_apply_ms ) ) }; return std::clamp( scaled, std::size_t { 8 }, ceiling ); } return std::min( ceiling, std::size_t { 24 } ); } [[nodiscard]] std::size_t gameplay_max_chunks_per_drain() { if( in_radius_fill_phase() ) { return kRadiusFillMaxChunksPerDrain; } auto const &bench { pack_bench_stream() }; if( stream_ring_deficit() ) { if( bench.last_chunk_ms >= 8.0 ) { return 1; } return kRingDeficitMaxChunksPerDrain; } if( pending_apply_is_urgent() ) { if( bench.last_chunk_ms >= 8.0 ) { return 1; } return kUrgentMaxChunksPerDrain; } if( bench.last_chunk_ms >= 12.0 ) { return 1; } return kFrameMaxChunksPerDrain; } [[nodiscard]] std::size_t scene_apply_pressure_slice_cap() { auto const applied { load_stats().pack_models }; if( applied >= 300000 ) { return 24; } if( applied >= 200000 ) { return 32; } if( applied >= 120000 ) { return 48; } if( applied >= 80000 ) { return 64; } if( applied >= 40000 ) { return 80; } return std::numeric_limits::max(); } [[nodiscard]] std::size_t adaptive_slice_instances( std::size_t const section_total, bool const urgent = false ) { auto const ring_deficit { stream_ring_deficit() }; if( urgent ) { auto limit { kUrgentSliceInstances }; auto const speed { camera_stream_speed_mps() }; if( speed > 1200.0 ) { limit = std::min( limit, std::size_t { 64 } ); } else if( speed > 600.0 ) { limit = std::min( limit, std::size_t { 48 } ); } if( section_total > 4000 ) { limit = std::min( limit, std::size_t { 48 } ); } else if( section_total > 2000 ) { limit = std::min( limit, std::size_t { 48 } ); } auto const last_ms { pack_bench_stream().last_chunk_ms }; if( false == ring_deficit ) { if( last_ms >= 24.0 ) { limit = std::min( limit, std::size_t { 32 } ); } else if( last_ms >= 12.0 ) { limit = std::min( limit, std::size_t { 48 } ); } limit = std::min( limit, scene_apply_pressure_slice_cap() ); auto const last_apply { pack_bench_stream().last_apply_ms }; if( last_apply >= 20.0 ) { limit = std::min( limit, std::size_t { 16 } ); } else if( last_apply >= 12.0 ) { limit = std::min( limit, std::size_t { 24 } ); } } if( ring_deficit ) { auto const target_ms { g_stream.last_inner_ring < 0.5f ? kSmoothApplyDeficitTargetMs : kSmoothApplyTargetMs }; auto const ceiling { std::size_t { 28 } }; limit = std::min( limit, apply_time_target_slice( target_ms, ceiling ) ); auto const last_apply { pack_bench_stream().last_apply_ms }; if( last_apply >= 10.0 ) { limit = std::min( limit, apply_time_target_slice( kSmoothApplyTargetMs * 0.6, std::size_t { 20 } ) ); } if( pack_bench_stream().last_chunk_ms >= 12.0 ) { limit = std::min( limit, std::size_t { 20 } ); } return std::max( limit, std::size_t { 8 } ); } limit = std::min( limit, apply_time_target_slice( kSmoothApplyTargetMs, std::size_t { 48 } ) ); return std::max( limit, std::size_t { 16 } ); } auto limit { gameplay_slice_instances() }; if( g_stream_catchup ) { auto const speed { camera_stream_speed_mps() }; if( speed > 1200.0 ) { limit = std::min( limit, kCatchupSliceInstances ); } else if( speed > 600.0 ) { limit = std::min( limit, std::size_t { 48 } ); } else { limit = std::min( limit, std::size_t { 96 } ); } if( pack_bench_stream().last_chunk_ms >= 12.0 ) { limit = std::min( limit, std::size_t { 32 } ); } } if( section_total > 4000 ) { limit = std::min( limit, std::size_t { 48 } ); } else if( section_total > 2000 ) { limit = std::min( limit, std::size_t { 64 } ); } else if( section_total > 800 ) { limit = std::min( limit, std::size_t { 80 } ); } auto const last_ms { pack_bench_stream().last_chunk_ms }; if( false == ring_deficit ) { if( last_ms >= 24.0 ) { limit = std::min( limit, std::size_t { 32 } ); } else if( last_ms >= 12.0 ) { limit = std::min( limit, std::size_t { 64 } ); } auto const last_apply { pack_bench_stream().last_apply_ms }; if( last_apply >= 24.0 ) { limit = std::min( limit, std::size_t { 16 } ); } else if( last_apply >= 16.0 ) { limit = std::min( limit, std::size_t { 24 } ); } else if( last_apply >= 10.0 ) { limit = std::min( limit, std::size_t { 32 } ); } limit = std::min( limit, scene_apply_pressure_slice_cap() ); } if( ring_deficit ) { auto const target_ms { g_stream.last_inner_ring < 0.5f ? kSmoothApplyDeficitTargetMs : kSmoothApplyTargetMs }; limit = std::min( limit, apply_time_target_slice( target_ms, std::size_t { 28 } ) ); if( pack_bench_stream().last_chunk_ms >= 12.0 ) { limit = std::min( limit, std::size_t { 20 } ); } return std::max( limit, std::size_t { 8 } ); } return std::max( limit, std::size_t { 16 } ); } [[nodiscard]] std::size_t adaptive_cold_meshes( bool const urgent = false ) { auto limit { urgent ? kUrgentSliceColdMeshes : gameplay_slice_cold_meshes() }; if( stream_ring_deficit() ) { limit = std::max( limit, urgent ? std::size_t { 12 } : std::size_t { 8 } ); } if( pack_bench_stream().main_chunk_cold_ms >= 12.0 ) { limit = std::min( limit, std::size_t { 2 } ); } else if( pack_bench_stream().main_chunk_cold_ms >= 6.0 ) { limit = std::min( limit, std::max( limit / 2, std::size_t { 3 } ) ); } return std::max( limit, std::size_t { 2 } ); } [[nodiscard]] std::size_t pending_section_total() { if( false == g_stream.pending_apply.has_value() || g_stream.pending_apply->models == nullptr ) { return 0; } return g_stream.pending_apply->models->size(); } [[nodiscard]] std::size_t preload_slice_cold_meshes( scene::eu7::Eu7Model const *const models, std::size_t const count, std::size_t const max_cold_meshes, double const cold_budget_ms, std::size_t &slice_count, Eu7PackSectionPathTables const *path_tables = nullptr ) { slice_count = 0; if( models == nullptr || count == 0 ) { return 0; } (void)drain_pack_mesh_loader_ready( g_stream.mesh_cache ); auto mesh_ready { [&]( std::size_t const index ) -> bool { if( eu7_pack_model_needs_full_load( models[ index ] ) ) { return true; } return pack_mesh_ready_for_slice( std::string( pack_model_mesh_path( models[ index ], path_tables ) ) ); } }; std::size_t cold_loaded { 0 }; auto const cold_started { std::chrono::steady_clock::now() }; for( std::size_t i { 0 }; i < count; ++i ) { if( eu7_pack_model_needs_full_load( models[ i ] ) ) { auto model_file { std::string( pack_model_mesh_path( models[ i ], path_tables ) ) }; if( false == model_file.empty() && model_file != "notload" ) { replace_slashes( model_file ); if( false == pack_mesh_ready_for_slice( model_file ) ) { request_pack_mesh_load( model_file ); } } continue; } if( mesh_ready( i ) ) { continue; } if( cold_loaded >= max_cold_meshes ) { break; } auto remaining_budget_ms { cold_budget_ms }; if( cold_budget_ms > 0.0 && cold_loaded > 0 ) { auto const elapsed_ms { std::chrono::duration( std::chrono::steady_clock::now() - cold_started ).count() }; if( elapsed_ms >= cold_budget_ms ) { break; } remaining_budget_ms = cold_budget_ms - elapsed_ms; } auto model_file { std::string( pack_model_mesh_path( models[ i ], path_tables ) ) }; replace_slashes( model_file ); if( ensure_stream_pack_mesh( model_file, remaining_budget_ms ) ) { ++cold_loaded; pack_bench_inc( &Eu7PackBench::main_cold_getmodel_calls ); continue; } request_pack_mesh_load( model_file ); if( gameplay_stream_mode() ) { break; } } for( std::size_t i { 0 }; i < count; ++i ) { if( false == mesh_ready( i ) ) { slice_count = i; break; } slice_count = i + 1; } if( slice_count == 0 && count > 0 && pending_apply_is_urgent() ) { auto model_file { std::string( pack_model_mesh_path( models[ 0 ], path_tables ) ) }; if( model_file.empty() || model_file == "notload" ) { slice_count = 1; } else { replace_slashes( model_file ); auto const wait_budget { std::min( cold_budget_ms, 8.0 ) }; if( ensure_stream_pack_mesh( model_file, wait_budget ) ) { ++cold_loaded; } if( mesh_ready( 0 ) ) { slice_count = 1; } } } return cold_loaded; } template void for_each_chunk_mesh_path( PackSectionReady const &batch, Fn const &fn ) { if( batch.models == nullptr ) { return; } std::unordered_set seen; seen.reserve( batch.models->size() ); for( auto const &model : *batch.models ) { if( eu7_pack_model_needs_full_load( model ) ) { continue; } auto const model_file { pack_model_mesh_path( model, batch.path_tables.get() ) }; if( model_file.empty() || model_file == "notload" ) { continue; } auto normalized { std::string( model_file ) }; replace_slashes( normalized ); if( false == seen.insert( normalized ).second ) { continue; } fn( normalized ); } } [[nodiscard]] bool file_chunk_meshes_ready( PackSectionReady const &batch, std::size_t const model_offset = 0, std::size_t const model_count = std::numeric_limits::max() ); void request_missing_file_chunk_meshes( PackSectionReady const &batch, std::size_t const model_offset = 0, std::size_t const model_count = std::numeric_limits::max() ); [[nodiscard]] bool has_nearer_mesh_ready_batch( int const pending_dist ) { std::lock_guard lock { g_stream.ready.mutex }; for( auto const &ready : g_stream.ready.data ) { if( ready.failed || ready.models == nullptr || ready.models->empty() ) { continue; } if( false == file_chunk_meshes_ready( ready ) ) { continue; } if( ready.row == g_stream.center_row && ready.column == g_stream.center_column ) { return true; } auto const dist { section_manhattan_sections( ready.row, ready.column, g_stream.center_row, g_stream.center_column ) }; if( dist + 1 < pending_dist ) { return true; } } return false; } void suspend_pending_apply_to_ready() { if( false == g_stream.pending_apply.has_value() ) { return; } g_stream.pending_apply->apply_offset = g_stream.pending_apply_offset; PackSectionReady suspended { std::move( *g_stream.pending_apply ) }; g_stream.pending_apply.reset(); g_stream.pending_apply_offset = 0; note_apply_progress(); { std::lock_guard lock { g_stream.ready.mutex }; g_stream.ready.data.push_back( std::move( suspended ) ); } pack_bench_inc( &Eu7PackBench::stream_reenqueue ); } [[nodiscard]] bool file_chunk_meshes_ready( PackSectionReady const &batch, std::size_t const model_offset, std::size_t const model_count ) { if( batch.models == nullptr || batch.models->empty() ) { return false; } auto const end { std::min( batch.models->size(), model_offset + model_count ) }; if( model_offset >= end ) { return true; } bool needs_mesh { false }; bool all_ready { true }; std::unordered_set seen; seen.reserve( std::min( end - model_offset, std::size_t { 64 } ) ); for( std::size_t i { model_offset }; i < end; ++i ) { auto const &model { ( *batch.models )[ i ] }; if( eu7_pack_model_needs_full_load( model ) ) { continue; } auto const model_file { pack_model_mesh_path( model, batch.path_tables.get() ) }; if( model_file.empty() || model_file == "notload" ) { continue; } auto normalized { std::string( model_file ) }; replace_slashes( normalized ); if( false == seen.insert( normalized ).second ) { continue; } needs_mesh = true; if( false == pack_mesh_ready_for_slice( normalized ) ) { all_ready = false; } } return needs_mesh ? all_ready : true; } void request_missing_file_chunk_meshes( PackSectionReady const &batch, std::size_t const model_offset, std::size_t const model_count ) { auto const dist { section_manhattan_sections( batch.row, batch.column, g_stream.center_row, g_stream.center_column ) }; auto const end { std::min( batch.models->size(), model_offset + model_count ) }; if( batch.models == nullptr || model_offset >= end ) { return; } std::unordered_set seen; seen.reserve( std::min( end - model_offset, std::size_t { 64 } ) ); for( std::size_t i { model_offset }; i < end; ++i ) { auto const &model { ( *batch.models )[ i ] }; if( eu7_pack_model_needs_full_load( model ) ) { continue; } auto const model_file { pack_model_mesh_path( model, batch.path_tables.get() ) }; if( model_file.empty() || model_file == "notload" ) { continue; } auto normalized { std::string( model_file ) }; replace_slashes( normalized ); if( false == seen.insert( normalized ).second ) { continue; } if( false == pack_mesh_ready_for_slice( normalized ) ) { request_pack_mesh_load( normalized, dist ); } } } void block_until_file_chunk_meshes( PackSectionReady const &batch, double const block_budget_ms ) { for_each_chunk_mesh_path( batch, [&]( std::string const &path ) { if( pack_mesh_ready_for_slice( path ) ) { return; } (void)ensure_stream_pack_mesh( path, block_budget_ms ); } ); } [[nodiscard]] bool batch_apply_meshes_ready( PackSectionReady const &batch, std::size_t const offset ) { (void)offset; return file_chunk_meshes_ready( batch ); } void request_missing_batch_meshes( PackSectionReady const &batch, std::size_t const offset ) { (void)offset; request_missing_file_chunk_meshes( batch ); } void prefetch_pack_section_meshes( Eu7Module const &module, int const row, int const column ) { auto const dist { section_manhattan_sections( row, column, g_stream.center_row, g_stream.center_column ) }; for_each_pack_section_unique_mesh( module, row, column, [&]( std::string const &path ) { request_pack_mesh_load( path, dist ); } ); } void note_apply_progress() { } [[nodiscard]] bool apply_pending_chunk( double const budget_ms, std::size_t const max_instances, std::size_t const max_cold_meshes, double const cold_budget_ms, std::size_t const max_chunks ) { (void)max_cold_meshes; (void)cold_budget_ms; if( g_stream.serializer == nullptr ) { return false; } if( false == g_stream.pending_apply.has_value() && false == try_dequeue_ready_batch() ) { return false; } auto const started { std::chrono::steady_clock::now() }; bool applied_work { false }; std::size_t chunks_done { 0 }; while( chunks_done < max_chunks ) { if( false == g_stream.pending_apply.has_value() ) { if( false == try_dequeue_ready_batch() ) { return applied_work; } } auto &batch { *g_stream.pending_apply }; if( batch.failed || batch.models == nullptr || batch.models->empty() ) { if( batch.failed ) { fail_section( batch.section_idx, batch.row, batch.column ); } else if( batch.section_final ) { finalize_section( batch ); } release_pending_buffer(); applied_work = true; continue; } scene::eu7::Eu7Model const * const models_ptr { batch.models->data() }; auto const total { batch.models->size() }; auto const offset { g_stream.pending_apply_offset }; if( offset >= total ) { if( batch.section_final ) { finalize_section( batch ); } release_pending_buffer(); applied_work = true; continue; } if( offset >= total ) { if( batch.section_final ) { finalize_section( batch ); } release_pending_buffer(); applied_work = true; continue; } auto const remaining { total - offset }; auto const is_urgent { pending_apply_is_urgent() }; auto const loading { false == g_loading_screen_dismissed }; std::size_t slice_limit { remaining }; if( loading ) { slice_limit = std::min( remaining, max_instances > 0 ? max_instances : kLoaderSliceInstances ); } else if( max_instances > 0 ) { slice_limit = std::min( remaining, max_instances ); } else { slice_limit = std::min( remaining, adaptive_slice_instances( total, is_urgent ) ); } std::size_t const chunk_count { slice_limit }; auto const planned_inst { chunk_count }; pack_bench_inc( &Eu7PackBench::stream_inst_planned, planned_inst ); if( is_urgent ) { pack_bench_inc( &Eu7PackBench::stream_chunks_urgent ); } else if( chunk_count >= kHeavySectionModelThreshold ) { pack_bench_inc( &Eu7PackBench::stream_chunks_heavy ); } else { pack_bench_inc( &Eu7PackBench::stream_chunks_light ); } pack_bench_note_pending_total( total ); auto const chunk_started { std::chrono::steady_clock::now() }; double cold_ms { 0.0 }; double warm_ms { 0.0 }; double apply_ms { 0.0 }; std::size_t cold_loads { 0 }; std::size_t tex_fetches { 0 }; auto const pending_dist { section_manhattan_sections( batch.row, batch.column, g_stream.center_row, g_stream.center_column ) }; resolve_pack_model_paths( batch.models->data() + offset, chunk_count, batch.path_tables.get() ); { auto const phase_started { std::chrono::steady_clock::now() }; auto const loading_phase { false == g_loading_screen_dismissed }; auto const gameplay { gameplay_stream_mode() }; (void)drain_pack_mesh_loader_ready( g_stream.mesh_cache ); if( false == file_chunk_meshes_ready( batch, offset, chunk_count ) ) { pack_bench_inc( &Eu7PackBench::stream_apply_deferred ); request_missing_file_chunk_meshes( batch, offset, chunk_count ); if( gameplay ) { suspend_pending_apply_to_ready(); return applied_work; } if( has_nearer_mesh_ready_batch( pending_dist ) ) { suspend_pending_apply_to_ready(); return applied_work; } block_until_file_chunk_meshes( batch, std::max( budget_ms, 16.0 ) ); if( false == file_chunk_meshes_ready( batch, offset, chunk_count ) ) { if( has_nearer_mesh_ready_batch( pending_dist ) ) { suspend_pending_apply_to_ready(); } return applied_work; } } else if( loading_phase ) { (void)pump_pack_mesh_loader( 8.0, 4 ); } cold_ms = std::chrono::duration( std::chrono::steady_clock::now() - phase_started ).count(); } pack_bench_inc( &Eu7PackBench::stream_inst_after_cold, chunk_count ); { auto const phase_started { std::chrono::steady_clock::now() }; if( false == gameplay_stream_mode() ) { tex_fetches = warm_pack_section_textures( batch, offset, chunk_count ); } else { tex_fetches = warm_pack_section_textures( batch, offset, chunk_count, kPackTextureWarmBudgetMs ); } warm_ms = std::chrono::duration( std::chrono::steady_clock::now() - phase_started ).count(); } pack_bench_inc( &Eu7PackBench::main_chunk_tex_fetches, tex_fetches ); scene::scratch_data scratch; simulation::eu7_pack_apply_session const session { &g_stream.mesh_cache, &g_stream.nodedata_cache, batch.section_idx }; { auto const phase_started { std::chrono::steady_clock::now() }; g_stream.serializer->apply_eu7_pack_models( models_ptr, offset, chunk_count, scratch, &session ); apply_ms = std::chrono::duration( std::chrono::steady_clock::now() - phase_started ).count(); } retain_section_mesh_keys( batch.section_idx, models_ptr, offset, chunk_count ); retain_section_nodedata_keys( batch.section_idx, models_ptr, offset, chunk_count ); if( g_stream.mesh_lru.size() > kMeshCacheLruCap || g_stream.nodedata_lru.size() > kNodedataCacheLruCap ) { evict_unreferenced_stream_caches(); } auto const chunk_wall_ms { std::chrono::duration( std::chrono::steady_clock::now() - chunk_started ).count() }; pack_bench_note_chunk( chunk_wall_ms, chunk_count, cold_loads, cold_ms, warm_ms, apply_ms ); if( chunk_wall_ms >= 12.0 && pack_bench_stream_phase_active() ) { WriteLog( "EU7 PACK [hitch]: chunk_ms=" + std::to_string( static_cast( chunk_wall_ms ) ) + " inst=" + std::to_string( chunk_count ) + ( planned_inst != chunk_count ? ( "/" + std::to_string( planned_inst ) ) : std::string{} ) + " urg=" + std::to_string( is_urgent ? 1 : 0 ) + " heavy=" + std::to_string( chunk_count >= kHeavySectionModelThreshold ? 1 : 0 ) + " cold=" + std::to_string( cold_loads ) + "(" + std::to_string( static_cast( cold_ms ) ) + "ms)" + " warm=" + std::to_string( tex_fetches ) + "(" + std::to_string( static_cast( warm_ms ) ) + "ms)" + " apply=" + std::to_string( static_cast( apply_ms ) ) + "ms" + " ring=" + std::to_string( static_cast( g_stream.last_inner_ring * 100.f ) ) + "/" + std::to_string( static_cast( g_stream.last_outer_ring * 100.f ) ) + " pending=" + std::to_string( offset + chunk_count ) + "/" + std::to_string( total ) + " sec=" + std::to_string( batch.row ) + "," + std::to_string( batch.column ) ); } load_stats().pack_models += chunk_count; if( load_stats().pack_models == chunk_count && offset == 0 ) { WriteLog( "EU7 PACK: pierwszy apply CHNK inst=" + std::to_string( chunk_count ) + " sec=" + std::to_string( batch.row ) + "," + std::to_string( batch.column ) ); } g_stream.pending_apply_offset = offset + chunk_count; batch.apply_offset = g_stream.pending_apply_offset; note_apply_progress(); applied_work = true; ++chunks_done; if( g_stream.pending_apply_offset >= total ) { if( batch.section_final ) { finalize_section( batch ); } release_pending_buffer(); } if( budget_ms <= 0.0 ) { return applied_work; } auto const elapsed_ms { std::chrono::duration( std::chrono::steady_clock::now() - started ).count() }; if( elapsed_ms >= budget_ms ) { pack_bench_inc( &Eu7PackBench::drain_budget_stops ); return applied_work; } continue; } return applied_work; } void maybe_log_loading_stream_status() { if( false == g_stream.active || g_loading_screen_dismissed ) { return; } static double s_last_log { 0.0 }; auto const now { Timer::GetTime() }; if( now - s_last_log < kStreamStatusLogIntervalSec ) { return; } s_last_log = now; std::size_t pending_total { 0 }; if( g_stream.pending_apply.has_value() && g_stream.pending_apply->models != nullptr ) { pending_total = g_stream.pending_apply->models->size(); } std::size_t job_count { 0 }; { std::lock_guard lock { g_stream.jobs.mutex }; job_count = g_stream.jobs.data.size(); } WriteLog( "EU7 PACK [loading]: pack_models=" + std::to_string( load_stats().pack_models ) + " ready_q=" + std::to_string( ready_queue_size() ) + " in_flight=" + std::to_string( g_stream.in_flight_sections.size() ) + " pending=" + std::to_string( g_stream.pending_apply_offset ) + "/" + std::to_string( pending_total ) + " jobs=" + std::to_string( job_count ) ); } void maybe_log_stream_status( glm::dvec3 const &world_position ) { if( false == pack_bench_stream_phase_active() ) { return; } static double s_last_status_log { 0.0 }; auto const now { Timer::GetTime() }; if( now - s_last_status_log < kStreamStatusLogIntervalSec ) { return; } s_last_status_log = now; auto const speed { camera_stream_speed_mps() }; auto const inner_ring { section_stream_ring_progress( world_position, kSectionStreamGameplayRadiusKm ) }; auto const outer_ring { section_stream_ring_progress( world_position, kSectionStreamTargetRadiusKm ) }; std::size_t pending_total { 0 }; std::size_t pending_offset { g_stream.pending_apply_offset }; if( g_stream.pending_apply.has_value() && g_stream.pending_apply->models != nullptr ) { pending_total = g_stream.pending_apply->models->size(); } log_pack_stream_status( world_position, speed, inner_ring, outer_ring, ready_queue_size(), g_stream.in_flight_sections.size(), pending_offset, pending_total, gameplay_apply_budget_ms(), g_stream.pending_apply.has_value() && g_stream.pending_apply->models != nullptr ? g_stream.pending_apply->models->size() : std::size_t { 0 } ); } [[nodiscard]] bool section_has_pack_models( int const row, int const column ) { if( g_stream.module == nullptr ) { return false; } auto const entry { find_pack_entry( *g_stream.module, row, column ) }; return entry.has_value() && entry->model_count > 0; } [[nodiscard]] bool is_bootstrap_ring_loaded( int const center_row, int const center_column, int const radius ) { for( int row { center_row - radius }; row <= center_row + radius; ++row ) { if( row < 0 || row >= kRegionSideSectionCount ) { continue; } for( int column { center_column - radius }; column <= center_column + radius; ++column ) { if( column < 0 || column >= kRegionSideSectionCount ) { continue; } if( false == section_has_pack_models( row, column ) ) { continue; } if( false == g_stream.loaded_sections.contains( section_index( row, column ) ) ) { return false; } } } return true; } void worker_loop( std::stop_token const stop_token ) { while( false == stop_token.stop_requested() && false == g_stream.worker_exit.load() ) { PackSectionJob job; bool has_job { false }; { std::lock_guard lock { g_stream.jobs.mutex }; if( false == g_stream.jobs.data.empty() ) { job = g_stream.jobs.data.front(); g_stream.jobs.data.pop_front(); has_job = true; } } if( false == has_job ) { g_stream.work_cv.wait_for( std::chrono::milliseconds( 50 ) ); continue; } if( ready_queue_size() >= g_stream_max_ready ) { { std::lock_guard lock { g_stream.jobs.mutex }; g_stream.jobs.data.push_front( job ); } g_stream.work_cv.wait_for( std::chrono::milliseconds( 8 ) ); continue; } try { auto const &module { stream_module() }; if( job.next_chunk == 0 ) { prefetch_pack_section_meshes( module, job.row, job.column ); } auto const entry { find_pack_entry( module, job.row, job.column ) }; if( false == entry.has_value() || entry->model_count == 0 ) { enqueue_failed_section( job ); continue; } std::unique_ptr> models; std::vector unique_textures; std::shared_ptr path_tables; std::uint32_t chunk_count { 1 }; { PackBenchTimer const read_timer { &Eu7PackBench::worker_read_pack_ms }; auto chunk { read_pack_section_chunk_load( module, job.row, job.column, job.next_chunk ) }; chunk_count = chunk.chunk_count; unique_textures = std::move( chunk.unique_textures ); path_tables = std::move( chunk.path_tables ); if( false == chunk.models.empty() ) { models = std::make_unique>( std::move( chunk.models ) ); } } PackSectionReady result; result.row = job.row; result.column = job.column; result.section_idx = job.section_idx; result.models = std::move( models ); result.unique_textures = std::move( unique_textures ); result.path_tables = std::move( path_tables ); result.failed = result.models == nullptr; result.section_final = job.next_chunk + 1 >= chunk_count; if( result.failed ) { ErrorLog( "EU7 PACK: pusty odczyt sekcji " + std::to_string( job.row ) + "," + std::to_string( job.column ) + " chunk=" + std::to_string( job.next_chunk ) + " (PIDX model_count=" + std::to_string( entry->model_count ) + ")" ); enqueue_failed_section( job ); continue; } pack_bench_inc( &Eu7PackBench::worker_models_decoded, result.models->size() ); auto const section_final { result.section_final }; { std::lock_guard lock { g_stream.ready.mutex }; g_stream.ready.data.push_back( std::move( result ) ); } pack_bench_inc( &Eu7PackBench::worker_chunks_decoded ); if( section_final ) { pack_bench_inc( &Eu7PackBench::worker_sections_done ); } if( job.next_chunk + 1 < chunk_count ) { PackSectionJob continue_job { job }; continue_job.next_chunk = job.next_chunk + 1; auto const dist { section_manhattan_sections( continue_job.row, continue_job.column, g_stream.center_row, g_stream.center_column ) }; if( dist > kWorkerContinueMaxDistanceKm ) { push_prioritized_job( continue_job ); pack_bench_inc( &Eu7PackBench::stream_worker_deferred_distant ); } else { std::lock_guard lock { g_stream.jobs.mutex }; g_stream.jobs.data.push_front( std::move( continue_job ) ); } g_stream.work_cv.notify_one(); } } catch( std::exception const &ex ) { ErrorLog( std::string{ "EU7 PACK: sekcja " + std::to_string( job.row ) + "," + std::to_string( job.column ) + ": " } + ex.what() ); enqueue_failed_section( job ); } } } void stop_workers() { stop_pack_mesh_loader(); g_stream.worker_exit = true; g_stream.work_cv.notify_all(); g_stream.workers.clear(); g_stream.worker_exit = false; reset_pack_section_read_cache(); { std::lock_guard lock { g_stream.jobs.mutex }; g_stream.jobs.data.clear(); } { std::lock_guard lock { g_stream.ready.mutex }; for( auto &batch : g_stream.ready.data ) { if( batch.models != nullptr ) { batch.models->clear(); batch.models->shrink_to_fit(); batch.models.reset(); } } g_stream.ready.data.clear(); } g_stream.in_flight_sections.clear(); release_pending_buffer(); g_stream.bootstrap_active = false; g_ring_ready_since.reset(); } void start_workers() { stop_workers(); if( false == g_stream.active ) { return; } auto const worker_count { stream_worker_count() }; g_stream.workers.reserve( worker_count ); for( std::size_t worker_idx { 0 }; worker_idx < worker_count; ++worker_idx ) { g_stream.workers.emplace_back( worker_loop ); } start_pack_mesh_loader(); WriteLog( "EU7 PACK: mesh queue loader started (main-thread), stream_workers=" + std::to_string( worker_count ) + ", radius=" + std::to_string( kSectionStreamTargetRadiusKm ) + "km, bootstrap_enqueue=" + std::to_string( kInitialBootstrapEnqueueRadius ) + "km" + ", bootstrap_replenish=" + std::to_string( kInitialBootstrapRadius ) + "km" + ", maintenance_inner=" + std::to_string( kSectionStreamGameplayRadiusKm ) + "km" + ", lookahead=" + std::to_string( kMovementLookahead ) ); } void finalize_section( PackSectionReady const &batch ) { g_stream.loaded_sections.insert( batch.section_idx ); g_stream.in_flight_sections.erase( batch.section_idx ); ++load_stats().pack_sections_loaded; pack_bench_inc( &Eu7PackBench::sections_finalized ); } void fail_section( std::size_t const section_idx, int const row, int const column ) { if( g_stream.loaded_sections.contains( section_idx ) ) { return; } if( row >= 0 && column >= 0 ) { WriteLog( "EU7 PACK: failed section " + std::to_string( row ) + "," + std::to_string( column ) ); } else { WriteLog( "EU7 PACK: failed section idx=" + std::to_string( section_idx ) ); } g_stream.loaded_sections.insert( section_idx ); g_stream.in_flight_sections.erase( section_idx ); } void enqueue_failed_section( PackSectionJob const &job ) { PackSectionReady result; result.row = job.row; result.column = job.column; result.section_idx = job.section_idx; result.failed = true; { std::lock_guard lock { g_stream.ready.mutex }; g_stream.ready.data.push_back( std::move( result ) ); } pack_bench_inc( &Eu7PackBench::worker_failures ); } [[nodiscard]] int section_manhattan_sections( int const row, int const column, int const center_row, int const center_column ) { return std::abs( row - center_row ) + std::abs( column - center_column ); } [[nodiscard]] std::size_t count_far_in_flight_sections( int const max_distance_km ) { if( g_stream.center_row < 0 || g_stream.center_column < 0 ) { return 0; } std::size_t count { 0 }; for( auto const section_idx : g_stream.in_flight_sections ) { auto const row { static_cast( section_idx / static_cast( kRegionSideSectionCount ) ) }; auto const column { static_cast( section_idx % static_cast( kRegionSideSectionCount ) ) }; if( section_manhattan_sections( row, column, g_stream.center_row, g_stream.center_column ) > max_distance_km ) { ++count; } } return count; } [[nodiscard]] bool should_block_far_enqueue( int const row, int const column, bool const lookahead_enqueue ) { if( g_stream.bootstrap_active || false == g_loading_screen_dismissed ) { return false; } if( g_stream.center_row < 0 || g_stream.center_column < 0 ) { return false; } auto const dist { section_manhattan_sections( row, column, g_stream.center_row, g_stream.center_column ) }; if( dist <= kSectionStreamGameplayRadiusKm ) { return false; } if( in_radius_fill_phase() ) { return false; } if( lookahead_enqueue && g_stream.last_inner_ring < kRingGatedInnerTarget ) { pack_bench_inc( &Eu7PackBench::stream_jobs_blocked_ring_inner ); return true; } auto const speed { camera_stream_speed_mps() }; if( speed > kFastFlightRingGateSpeedMps && dist > kFastFlightApplyMaxDistanceKm && count_far_in_flight_sections( kFastFlightApplyMaxDistanceKm ) >= kFastFlightMaxFarInFlight ) { pack_bench_inc( &Eu7PackBench::stream_jobs_blocked_far ); return true; } return false; } void push_prioritized_job( PackSectionJob job ) { std::lock_guard lock { g_stream.jobs.mutex }; auto const insert_before { std::find_if( g_stream.jobs.data.begin(), g_stream.jobs.data.end(), [&]( PackSectionJob const &existing ) { return existing.priority > job.priority; } ) }; g_stream.jobs.data.insert( insert_before, std::move( job ) ); } void reprioritize_job_queue() { std::lock_guard lock { g_stream.jobs.mutex }; if( g_stream.jobs.data.empty() ) { return; } for( auto &job : g_stream.jobs.data ) { job.priority = section_manhattan_sections( job.row, job.column, g_stream.center_row, g_stream.center_column ); } std::stable_sort( g_stream.jobs.data.begin(), g_stream.jobs.data.end(), []( PackSectionJob const &lhs, PackSectionJob const &rhs ) { return lhs.priority < rhs.priority; } ); } [[nodiscard]] bool enqueue_section_if_needed( int const row, int const column, int const priority, bool const lookahead_enqueue ) { if( row < 0 || row >= kRegionSideSectionCount || column < 0 || column >= kRegionSideSectionCount ) { return false; } if( stream_backpressure() ) { return false; } if( should_throttle_far_stream_jobs( row, column ) ) { pack_bench_inc( &Eu7PackBench::stream_jobs_blocked_far ); return false; } if( should_block_far_enqueue( row, column, lookahead_enqueue ) ) { return false; } auto const section_idx { section_index( row, column ) }; if( g_stream.loaded_sections.contains( section_idx ) ) { return false; } // Pusta komorka siatki (brak wpisu PACK) — nie kolejkuj, oznacz jako zaladowana. if( false == section_has_pack_models( row, column ) ) { g_stream.loaded_sections.insert( section_idx ); return false; } if( g_stream.in_flight_sections.contains( section_idx ) ) { { std::lock_guard lock { g_stream.jobs.mutex }; for( auto &queued : g_stream.jobs.data ) { if( queued.section_idx != section_idx ) { continue; } if( priority < queued.priority ) { queued.priority = priority; std::stable_sort( g_stream.jobs.data.begin(), g_stream.jobs.data.end(), []( PackSectionJob const &lhs, PackSectionJob const &rhs ) { return lhs.priority < rhs.priority; } ); } return false; } } return false; } if( g_stream.pending_apply.has_value() && g_stream.pending_apply->section_idx == section_idx ) { return false; } g_stream.in_flight_sections.insert( section_idx ); if( g_stream.module != nullptr ) { prefetch_pack_section_meshes( *g_stream.module, row, column ); } PackSectionJob job; job.row = row; job.column = column; job.section_idx = section_idx; job.priority = priority; if( row == g_stream.center_row && column == g_stream.center_column && priority == 0 ) { std::lock_guard lock { g_stream.jobs.mutex }; g_stream.jobs.data.push_front( std::move( job ) ); } else { push_prioritized_job( std::move( job ) ); } g_stream.work_cv.notify_all(); return true; } void enqueue_sections_around( int const center_row, int const center_column, int const ring_radius, glm::dvec3 const &world_position ) { struct SectionCandidate { int row { 0 }; int column { 0 }; int distance { 0 }; }; std::vector candidates; candidates.reserve( static_cast( ( ring_radius * 2 + 1 ) * ( ring_radius * 2 + 1 ) ) ); for( int row { center_row - ring_radius }; row <= center_row + ring_radius; ++row ) { if( row < 0 || row >= kRegionSideSectionCount ) { continue; } for( int column { center_column - ring_radius }; column <= center_column + ring_radius; ++column ) { if( column < 0 || column >= kRegionSideSectionCount ) { continue; } auto const section_center_x { ( static_cast( column ) - kRegionSideSectionCount / 2 ) * kSectionSizeM + kSectionSizeM * 0.5 }; auto const section_center_z { ( static_cast( row ) - kRegionSideSectionCount / 2 ) * kSectionSizeM + kSectionSizeM * 0.5 }; auto const dx { world_position.x - section_center_x }; auto const dz { world_position.z - section_center_z }; auto const dist { static_cast( ( std::abs( dx ) + std::abs( dz ) ) / kSectionSizeM ) }; candidates.push_back( { row, column, dist } ); } } std::sort( candidates.begin(), candidates.end(), []( SectionCandidate const &lhs, SectionCandidate const &rhs ) { return lhs.distance < rhs.distance; } ); for( auto const &candidate : candidates ) { auto priority { candidate.distance }; if( candidate.row == center_row && candidate.column == center_column ) { priority = 0; } enqueue_section_if_needed( candidate.row, candidate.column, priority ); if( stream_backpressure() ) { break; } } } void replenish_bootstrap_ring( glm::dvec3 const &world_position ) { if( world_position.x == 0.0 && world_position.y == 0.0 && world_position.z == 0.0 ) { return; } auto const [row, column] { section_row_column( world_position ) }; g_stream.center_row = row; g_stream.center_column = column; if( false == g_loading_screen_dismissed ) { enqueue_sections_around( row, column, kInitialBootstrapEnqueueRadius, world_position ); } else if( in_radius_fill_phase() ) { enqueue_sections_around( row, column, kSectionStreamTargetRadiusKm, world_position ); } else { enqueue_sections_around( row, column, kSectionStreamGameplayRadiusKm, world_position ); } } [[nodiscard]] float section_ring_apply_fraction( int const row, int const column ) { auto const section_idx { section_index( row, column ) }; if( g_stream.loaded_sections.contains( section_idx ) ) { return 1.0f; } if( g_stream.pending_apply.has_value() && g_stream.pending_apply->section_idx == section_idx && g_stream.pending_apply->models != nullptr && false == g_stream.pending_apply->models->empty() ) { auto const total { g_stream.pending_apply->models->size() }; if( g_stream.pending_apply_offset >= total ) { return 1.0f; } return static_cast( g_stream.pending_apply_offset ) / static_cast( total ); } { std::lock_guard lock { g_stream.ready.mutex }; for( auto const &batch : g_stream.ready.data ) { if( batch.section_idx == section_idx ) { return 0.25f; } } } if( g_stream.in_flight_sections.contains( section_idx ) ) { return 0.1f; } return 0.f; } [[nodiscard]] bool center_section_stream_satisfied( glm::dvec3 const &world_position ) { auto const [center_row, center_column] { section_row_column( world_position ) }; if( false == section_has_pack_models( center_row, center_column ) ) { return true; } auto const section_idx { section_index( center_row, center_column ) }; if( g_stream.loaded_sections.contains( section_idx ) ) { return true; } if( g_stream.pending_apply.has_value() && g_stream.pending_apply->section_idx == section_idx && g_stream.pending_apply->models != nullptr && false == g_stream.pending_apply->models->empty() ) { auto const total { g_stream.pending_apply->models->size() }; if( g_stream.pending_apply_offset >= total ) { return true; } } return false; } [[nodiscard]] std::pair movement_section_delta( glm::dvec3 const &forward_hint ) { auto const horizontal { glm::dvec3 { forward_hint.x, 0.0, forward_hint.z } }; auto const length { glm::length( horizontal ) }; if( length < 0.01 ) { return { 0, 0 }; } auto const fwd { horizontal / length }; if( std::abs( fwd.x ) >= std::abs( fwd.z ) ) { return { 0, fwd.x > 0.0 ? 1 : -1 }; } return { fwd.z > 0.0 ? 1 : -1, 0 }; } [[nodiscard]] glm::dvec3 guess_travel_forward() { if( simulation::Train != nullptr && simulation::Train->Dynamic() != nullptr ) { auto *const vehicle { simulation::Train->Dynamic() }; if( std::abs( vehicle->GetVelocity() ) > 0.5 ) { auto forward { vehicle->VectorFront() }; if( vehicle->DirectionGet() < 0 ) { forward = -forward; } return forward; } } if( Global.pCamera.m_owner == nullptr || FreeFlyModeFlag ) { return camera_travel_forward(); } return {}; } void enqueue_movement_lookahead( int const center_row, int const center_column, int const ring_radius, glm::dvec3 const &forward_hint, int const max_steps ) { auto const [drow, dcolumn] { movement_section_delta( forward_hint ) }; if( drow == 0 && dcolumn == 0 ) { return; } auto lookahead_steps { std::min( max_steps, kMovementLookahead ) }; if( simulation::Train != nullptr && simulation::Train->Dynamic() != nullptr ) { auto const speed { std::abs( simulation::Train->Dynamic()->GetVelocity() ) }; if( speed > 25.0 ) { lookahead_steps = std::min( max_steps, 10 ); } else if( speed > 15.0 ) { lookahead_steps = std::min( max_steps, 8 ); } else if( speed > 8.0 ) { lookahead_steps = std::min( max_steps, 6 ); } } { auto const cam_speed { camera_stream_speed_mps() }; if( cam_speed > 200.0 ) { lookahead_steps = max_steps; } else if( cam_speed > 80.0 ) { lookahead_steps = std::max( lookahead_steps, std::min( max_steps, 10 ) ); } } for( int step { 1 }; step <= lookahead_steps; ++step ) { auto const row { center_row + drow * ( ring_radius + step ) }; auto const column { center_column + dcolumn * ( ring_radius + step ) }; auto const priority { section_manhattan_sections( row, column, center_row, center_column ) }; if( enqueue_section_if_needed( row, column, priority, true ) ) { pack_bench_inc( &Eu7PackBench::stream_lookahead_enqueue ); } if( stream_backpressure() ) { break; } } } [[nodiscard]] glm::dvec3 guess_initial_stream_position( Eu7Module const &root_module ) { auto const &saved_camera { Global.FreeCameraInit[ 0 ] }; if( saved_camera.x != 0.0 || saved_camera.y != 0.0 || saved_camera.z != 0.0 ) { return saved_camera; } if( false == Global.local_start_vehicle.empty() && Global.local_start_vehicle != "ghostview" ) { if( auto *vehicle { simulation::Vehicles.find( Global.local_start_vehicle ) }; vehicle != nullptr ) { return vehicle->GetPosition(); } } for( auto const &trainset : root_module.scene.trainsets ) { if( trainset.track.empty() ) { continue; } if( auto *track { simulation::Paths.find( trainset.track ) }; track != nullptr ) { return glm::dvec3 { track->location() }; } } for( auto *vehicle : simulation::Vehicles.sequence() ) { if( vehicle != nullptr ) { return vehicle->GetPosition(); } } return {}; } [[nodiscard]] bool try_dequeue_ready_batch() { if( g_stream.pending_apply.has_value() ) { return true; } { std::lock_guard lock { g_stream.ready.mutex }; for( auto it { g_stream.ready.data.begin() }; it != g_stream.ready.data.end(); ) { if( it->failed || it->models == nullptr || it->models->empty() ) { if( it->failed ) { fail_section( it->section_idx, it->row, it->column ); } else if( it->section_final ) { finalize_section( *it ); } it = g_stream.ready.data.erase( it ); continue; } ++it; } if( g_stream.ready.data.empty() ) { return false; } auto const gameplay { gameplay_stream_mode() }; auto const pick_best { [&]( int const max_distance, bool const require_mesh_ready ) -> std::deque::iterator { auto best_it { g_stream.ready.data.end() }; auto best_dist { std::numeric_limits::max() }; auto best_mesh_ready { false }; for( auto it { g_stream.ready.data.begin() }; it != g_stream.ready.data.end(); ++it ) { if( it->failed || it->models == nullptr || it->models->empty() ) { continue; } auto const dist { section_manhattan_sections( it->row, it->column, g_stream.center_row, g_stream.center_column ) }; if( max_distance >= 0 && dist > max_distance ) { continue; } auto const mesh_ready { file_chunk_meshes_ready( *it ) }; if( gameplay && require_mesh_ready && false == mesh_ready ) { continue; } if( dist < best_dist || ( dist == best_dist && mesh_ready && false == best_mesh_ready ) || ( dist == best_dist && best_it == g_stream.ready.data.end() ) ) { best_dist = dist; best_it = it; best_mesh_ready = mesh_ready; } else if( dist == best_dist && best_it != g_stream.ready.data.end() && it->section_idx < best_it->section_idx && mesh_ready == best_mesh_ready ) { best_it = it; } } return best_it; } }; auto const pick_camera_section { [&]() -> std::deque::iterator { for( auto it { g_stream.ready.data.begin() }; it != g_stream.ready.data.end(); ++it ) { if( it->failed || it->models == nullptr || it->models->empty() ) { continue; } if( it->row == g_stream.center_row && it->column == g_stream.center_column ) { if( gameplay && false == file_chunk_meshes_ready( *it ) ) { return g_stream.ready.data.end(); } return it; } } return g_stream.ready.data.end(); } }; auto const cam_speed { camera_stream_speed_mps() }; auto const ring_deficit { stream_ring_deficit() }; auto const radius_fill { in_radius_fill_phase() }; auto max_apply_dist { cam_speed > 600.0 ? kFastFlightApplyMaxDistanceKm : ( cam_speed > 80.0 ? 3 : kSectionStreamGameplayRadiusKm ) }; if( ring_deficit && false == radius_fill ) { max_apply_dist = cam_speed > 200.0 ? kFastFlightApplyMaxDistanceKm : kUrgentSectionDistanceKm; } auto best_it { pick_camera_section() }; if( best_it != g_stream.ready.data.end() ) { pack_bench_inc( &Eu7PackBench::stream_dequeue_camera ); } else if( radius_fill ) { best_it = pick_best( -1, true ); } else { best_it = pick_best( max_apply_dist, true ); } if( best_it == g_stream.ready.data.end() && false == radius_fill ) { auto const wider_dist { ring_deficit ? std::max( max_apply_dist, kFastFlightApplyMaxDistanceKm ) : kSectionStreamGameplayRadiusKm }; best_it = pick_best( wider_dist, true ); } if( best_it == g_stream.ready.data.end() && false == g_stream.ready.data.empty() ) { best_it = pick_best( -1, true ); } if( best_it == g_stream.ready.data.end() && gameplay && false == g_stream.ready.data.empty() ) { best_it = pick_best( -1, false ); } if( best_it == g_stream.ready.data.end() ) { return false; } g_stream.pending_apply.emplace( std::move( *best_it ) ); g_stream.ready.data.erase( best_it ); } g_stream.pending_apply_offset = g_stream.pending_apply->apply_offset; auto const dequeue_dist { section_manhattan_sections( g_stream.pending_apply->row, g_stream.pending_apply->column, g_stream.center_row, g_stream.center_column ) }; if( dequeue_dist <= kFastFlightApplyMaxDistanceKm ) { pack_bench_inc( &Eu7PackBench::stream_dequeue_near ); } else { pack_bench_inc( &Eu7PackBench::stream_dequeue_far ); } return true; } void prefetch_ready_queue_textures( double const budget_ms ) { if( budget_ms <= 0.0 || GfxRenderer == nullptr ) { return; } PackBenchTimer const prefetch_timer { &Eu7PackBench::stream_prefetch_ready_tex_ms }; auto const started { std::chrono::steady_clock::now() }; std::vector::iterator> candidates; candidates.reserve( ready_queue_size() ); { std::lock_guard lock { g_stream.ready.mutex }; for( auto it { g_stream.ready.data.begin() }; it != g_stream.ready.data.end(); ++it ) { if( it->failed || it->models == nullptr || it->models->empty() ) { continue; } auto const tex_total { false == it->unique_textures.empty() ? it->unique_textures.size() : it->models->size() }; if( it->texture_warm_offset >= tex_total ) { continue; } candidates.push_back( it ); } } if( candidates.empty() ) { return; } std::sort( candidates.begin(), candidates.end(), []( std::deque::iterator const &lhs, std::deque::iterator const &rhs ) { auto const lhs_dist { section_manhattan_sections( lhs->row, lhs->column, g_stream.center_row, g_stream.center_column ) }; auto const rhs_dist { section_manhattan_sections( rhs->row, rhs->column, g_stream.center_row, g_stream.center_column ) }; return lhs_dist < rhs_dist; } ); for( auto const it : candidates ) { auto &batch { *it }; auto const use_utex { false == batch.unique_textures.empty() }; while( true ) { auto const tex_total { use_utex ? batch.unique_textures.size() : batch.models->size() }; if( batch.texture_warm_offset >= tex_total ) { break; } auto const remaining { tex_total - batch.texture_warm_offset }; auto const slice { std::min( remaining, kReadyTexturePrefetchSlice ) }; if( use_utex ) { (void)warm_pack_texture_paths_main( batch.unique_textures.data() + batch.texture_warm_offset, slice ); } else { (void)warm_pack_textures_main( batch.models->data() + batch.texture_warm_offset, slice ); } batch.texture_warm_offset += slice; auto const elapsed_ms { std::chrono::duration( std::chrono::steady_clock::now() - started ).count() }; if( elapsed_ms >= budget_ms ) { return; } } } } void prefetch_ready_queue_umes_worker( double const budget_ms, std::size_t const max_meshes ) { if( budget_ms <= 0.0 || max_meshes == 0 ) { return; } PackBenchTimer const prefetch_timer { &Eu7PackBench::stream_prefetch_ready_umes_ms }; auto const started { std::chrono::steady_clock::now() }; std::size_t loads { 0 }; std::lock_guard lock { g_stream.ready.mutex }; std::vector::iterator> candidates; candidates.reserve( g_stream.ready.data.size() ); for( auto it { g_stream.ready.data.begin() }; it != g_stream.ready.data.end(); ++it ) { if( it->failed || it->models == nullptr || it->models->empty() ) { continue; } if( it->umes_prefetch_offset >= it->models->size() ) { continue; } if( g_stream.center_row >= 0 && g_stream.center_column >= 0 ) { auto const dist { section_manhattan_sections( it->row, it->column, g_stream.center_row, g_stream.center_column ) }; if( dist > umes_prefetch_max_distance_km() ) { continue; } } candidates.push_back( it ); } if( candidates.empty() ) { return; } std::sort( candidates.begin(), candidates.end(), []( std::deque::iterator const &lhs, std::deque::iterator const &rhs ) { auto const lhs_dist { section_manhattan_sections( lhs->row, lhs->column, g_stream.center_row, g_stream.center_column ) }; auto const rhs_dist { section_manhattan_sections( rhs->row, rhs->column, g_stream.center_row, g_stream.center_column ) }; return lhs_dist < rhs_dist; } ); for( auto const it : candidates ) { auto &batch { *it }; while( batch.umes_prefetch_offset < batch.models->size() ) { auto const &model { ( *batch.models )[ batch.umes_prefetch_offset ] }; if( eu7_pack_model_needs_full_load( model ) ) { ++batch.umes_prefetch_offset; continue; } auto const &model_file { model.model_file }; if( model_file.empty() || model_file == "notload" ) { ++batch.umes_prefetch_offset; continue; } if( pack_mesh_ready_for_slice( model_file ) ) { pack_bench_inc( &Eu7PackBench::stream_mesh_session_hit ); ++batch.umes_prefetch_offset; continue; } if( loads >= max_meshes ) { return; } if( loads > 0 ) { auto const elapsed_ms { std::chrono::duration( std::chrono::steady_clock::now() - started ).count() }; if( elapsed_ms >= budget_ms ) { return; } } request_pack_mesh_load( model_file, section_manhattan_sections( batch.row, batch.column, g_stream.center_row, g_stream.center_column ) ); ++loads; pack_bench_inc( &Eu7PackBench::stream_prefetch_ready_umes_loads ); ++batch.umes_prefetch_offset; } } } [[nodiscard]] bool gameplay_stream_mode() { return g_loading_screen_dismissed && false == g_stream.bootstrap_active && false == g_stream.bootstrap_pending; } void replenish_bootstrap_ring( glm::dvec3 const &world_position ); void note_drain_idle() { if( ready_queue_size() > 0 || false == g_stream.in_flight_sections.empty() ) { pack_bench_inc( &Eu7PackBench::main_drain_wait_worker ); } else { pack_bench_inc( &Eu7PackBench::main_drain_idle ); } } void drain_apply_budget( double const budget_ms, std::size_t const max_instances, std::size_t const max_cold_meshes, double const cold_budget_ms, std::size_t const max_chunks ) { if( false == g_stream.active || g_stream.serializer == nullptr || budget_ms <= 0.0 ) { return; } pack_bench_inc( &Eu7PackBench::main_drain_calls ); pack_bench_note_ready_queue( ready_queue_size() ); pack_bench_note_in_flight( g_stream.in_flight_sections.size() ); if( false == apply_pending_chunk( budget_ms, max_instances, max_cold_meshes, cold_budget_ms, max_chunks ) ) { note_drain_idle(); } } void drain_sections( std::size_t const max_sections ) { if( false == g_stream.active || g_stream.serializer == nullptr || max_sections == 0 ) { return; } pack_bench_inc( &Eu7PackBench::main_drain_calls ); pack_bench_note_ready_queue( ready_queue_size() ); pack_bench_note_in_flight( g_stream.in_flight_sections.size() ); bool applied_work { false }; for( std::size_t i { 0 }; i < max_sections; ++i ) { if( false == apply_pending_chunk( 0.0, kLoaderSliceInstances, kLoaderSliceColdMeshes, 0.0, std::numeric_limits::max() ) ) { break; } applied_work = true; } if( false == applied_work ) { note_drain_idle(); } } void drain_frame_budget( double const budget_ms ) { if( budget_ms <= 0.0 ) { return; } auto const started { std::chrono::steady_clock::now() }; std::size_t chunks_done { 0 }; while( chunks_done < kFrameMaxChunksPerDrain ) { auto const elapsed_ms { std::chrono::duration( std::chrono::steady_clock::now() - started ).count() }; auto const remaining_ms { budget_ms - elapsed_ms }; if( remaining_ms <= 0.0 ) { break; } if( false == apply_pending_chunk( remaining_ms, kFrameSliceInstances, kFrameSliceColdMeshes, kFrameColdBudgetMs, 1 ) ) { break; } ++chunks_done; if( false == g_stream.pending_apply.has_value() && ready_queue_size() == 0 ) { break; } } } void drain_until_budget( double const budget_ms ) { if( budget_ms <= 0.0 ) { return; } auto const started { std::chrono::steady_clock::now() }; while( true ) { auto const elapsed_ms { std::chrono::duration( std::chrono::steady_clock::now() - started ).count() }; auto const remaining_ms { budget_ms - elapsed_ms }; if( remaining_ms <= 0.0 ) { break; } if( false == apply_pending_chunk( remaining_ms, kLoaderSliceInstances, kLoaderSliceColdMeshes, 0.0, std::numeric_limits::max() ) ) { break; } if( false == g_stream.pending_apply.has_value() && ready_queue_size() == 0 ) { break; } } } void sync_stream_limits( glm::dvec3 const &world_position ) { if( false == g_loading_screen_dismissed ) { g_stream_catchup = false; g_stream.radius_fill_active = false; g_stream_max_in_flight = 16; g_stream_max_ready = 8; return; } auto const stream_position { resolve_section_stream_position( world_position ) }; auto const cam_speed { camera_stream_speed_mps() }; g_stream.last_inner_ring = section_stream_ring_progress( stream_position, kSectionStreamGameplayRadiusKm ); g_stream.last_outer_ring = section_stream_ring_progress( stream_position, kSectionStreamTargetRadiusKm ); auto const was_radius_fill { g_stream.radius_fill_active }; g_stream.radius_fill_active = false == section_stream_ready_around( stream_position, kSectionStreamTargetRadiusKm ); if( false == was_radius_fill && g_stream.radius_fill_active ) { WriteLog( "EU7 PACK: faza radius_fill — docelowy promien " + std::to_string( kSectionStreamTargetRadiusKm ) + "km, ring=" + std::to_string( static_cast( g_stream.last_outer_ring * 100.f ) ) + "%" ); } else if( was_radius_fill && false == g_stream.radius_fill_active ) { WriteLog( "EU7 PACK: faza maintenance — promien " + std::to_string( kSectionStreamTargetRadiusKm ) + "km zapelniony, ring=" + std::to_string( static_cast( g_stream.last_outer_ring * 100.f ) ) + "%" ); } if( g_stream.radius_fill_active ) { g_stream_catchup = true; g_stream_max_in_flight = kRadiusFillMaxInFlight; g_stream_max_ready = kRadiusFillMaxReady; return; } g_stream_catchup = ( cam_speed > 80.0 || g_stream.last_inner_ring < 0.98f ); if( g_stream_catchup ) { g_stream_max_in_flight = kCatchupMaxInFlightSections; g_stream_max_ready = kCatchupMaxReadySections; if( cam_speed > kRingBoostInFlightSpeedMps && g_stream.last_inner_ring < kRingBoostOuterThreshold ) { g_stream_max_in_flight = std::max( g_stream_max_in_flight, kRingStrongBoostMaxInFlight ); g_stream_max_ready = std::max( g_stream_max_ready, kRingStrongBoostMaxReady ); } else if( cam_speed > kRingBoostInFlightSpeedMps && g_stream.last_inner_ring < kRingStrongBoostOuterThreshold ) { g_stream_max_in_flight = std::max( g_stream_max_in_flight, kRingBoostMaxInFlight ); g_stream_max_ready = std::max( g_stream_max_ready, kRingBoostMaxReady ); } } else { g_stream_max_in_flight = kMaxInFlightSections; g_stream_max_ready = kMaxReadySections; } } } // namespace void remember_stream_anchor( glm::dvec3 const &position ); [[nodiscard]] bool camera_far_from_stream_anchor( glm::dvec3 const &camera_position ); [[nodiscard]] glm::dvec3 resolve_section_stream_position( glm::dvec3 const &hint ); void init_section_stream( Eu7Module const &root_module, std::string const &resolved_path, simulation::state_serializer &serializer ) { stop_workers(); reset_stream_fields(); g_stream.module = &root_module; g_stream.path = resolved_path; g_stream.serializer = &serializer; g_stream.active = root_module.has_pack_chunk; g_stream.mesh_cache.reserve( 1024 ); g_stream.nodedata_cache.reserve( 1024 ); if( g_stream.active ) { g_loading_block_started = std::chrono::steady_clock::now(); g_stream.bootstrap_pending = true; WriteLog( "EU7 PACK: streaming wlaczony, plik=\"" + resolved_path + "\", sekcji w indeksie=" + std::to_string( root_module.pack_catalog.entries.size() ) + ", payload=" + std::to_string( root_module.pack_catalog.pack_payload_size ) + " B" ); start_workers(); } } void prime_section_stream( Eu7Module const &root_module ) { if( false == g_stream.active ) { return; } auto const initial { guess_initial_stream_position( root_module ) }; if( initial.x == 0.0 && initial.y == 0.0 && initial.z == 0.0 ) { WriteLog( "EU7 PACK: brak pozycji startowej — bootstrap po zaladowaniu scenariusza" ); return; } remember_stream_anchor( initial ); auto const [row, column] { section_row_column( initial ) }; g_stream.center_row = row; g_stream.center_column = column; WriteLog( "EU7 PACK: pre-warm sekcji " + std::to_string( row ) + "," + std::to_string( column ) + " (" + std::to_string( initial.x ) + "," + std::to_string( initial.y ) + "," + std::to_string( initial.z ) + ")" ); g_stream.bootstrap_active = true; enqueue_sections_around( row, column, kInitialBootstrapEnqueueRadius, initial ); auto const prime_started { std::chrono::steady_clock::now() }; while( true ) { drain_until_budget( kBootstrapDrainMs ); if( load_stats().pack_models > 0 ) { break; } auto const elapsed_ms { std::chrono::duration_cast( std::chrono::steady_clock::now() - prime_started ).count() }; if( elapsed_ms >= 4000 ) { break; } if( false == g_stream.pending_apply.has_value() && ready_queue_size() == 0 && g_stream.in_flight_sections.empty() ) { break; } } WriteLog( "EU7 PACK: prime drain modele=" + std::to_string( load_stats().pack_models ) + " ready_q=" + std::to_string( ready_queue_size() ) + " in_flight=" + std::to_string( g_stream.in_flight_sections.size() ) ); g_stream.bootstrap_active = false; g_stream.bootstrap_pending = false; g_stream.radius_fill_active = false; } [[nodiscard]] glm::dvec3 resolve_stream_position() { if( simulation::Train != nullptr && simulation::Train->Dynamic() != nullptr ) { return simulation::Train->Dynamic()->GetPosition(); } if( false == Global.local_start_vehicle.empty() && Global.local_start_vehicle != "ghostview" ) { if( auto *vehicle { simulation::Vehicles.find( Global.local_start_vehicle ) }; vehicle != nullptr ) { return vehicle->GetPosition(); } } auto const &saved_camera { Global.FreeCameraInit[ 0 ] }; if( saved_camera.x != 0.0 || saved_camera.y != 0.0 || saved_camera.z != 0.0 ) { return saved_camera; } for( auto *vehicle : simulation::Vehicles.sequence() ) { if( vehicle != nullptr ) { return vehicle->GetPosition(); } } return {}; } void bootstrap_section_stream( glm::dvec3 const &world_position ) { if( false == g_stream.active ) { return; } auto const [row, column] { section_row_column( world_position ) }; g_stream.center_row = row; g_stream.center_column = column; WriteLog( "EU7 PACK: bootstrap " + std::to_string( kInitialBootstrapRadius ) + "km, sekcja " + std::to_string( row ) + "," + std::to_string( column ) ); auto const started { std::chrono::steady_clock::now() }; g_stream.bootstrap_active = true; enqueue_sections_around( row, column, kInitialBootstrapRadius, world_position ); while( false == is_bootstrap_ring_loaded( row, column, kInitialBootstrapRadius ) ) { drain_until_budget( kBootstrapDrainMs ); if( false == is_bootstrap_ring_loaded( row, column, kInitialBootstrapRadius ) ) { enqueue_sections_around( row, column, kInitialBootstrapRadius, world_position ); } auto const elapsed_ms { std::chrono::duration_cast( std::chrono::steady_clock::now() - started ).count() }; if( static_cast( elapsed_ms ) >= kBootstrapTimeoutMs ) { ErrorLog( "EU7 PACK: bootstrap timeout po " + std::to_string( elapsed_ms ) + " ms" ); break; } std::this_thread::sleep_for( std::chrono::milliseconds( 8 ) ); } g_stream.bootstrap_active = false; g_stream.bootstrap_pending = false; g_stream.radius_fill_active = false; auto const elapsed_ms { std::chrono::duration_cast( std::chrono::steady_clock::now() - started ).count() }; WriteLog( "EU7 PACK: bootstrap zakonczony, sekcji=" + std::to_string( g_stream.loaded_sections.size() ) + ", czas=" + std::to_string( elapsed_ms ) + " ms" ); } void update_section_stream( glm::dvec3 const &world_position ) { if( false == g_stream.active ) { return; } auto const stream_position { resolve_section_stream_position( world_position ) }; auto const prev_center_row { g_stream.center_row }; auto const prev_center_column { g_stream.center_column }; auto const [center_row, center_column] { section_row_column( stream_position ) }; auto const center_moved { center_row != g_stream.center_row || center_column != g_stream.center_column }; auto const moved_since_enqueue { g_stream.has_last_enqueue_position ? glm::length( stream_position - g_stream.last_enqueue_position ) : std::numeric_limits::infinity() }; if( moved_since_enqueue > kTeleportReenqueueDistanceM ) { g_stream.has_last_enqueue_position = false; } g_stream.center_row = center_row; g_stream.center_column = center_column; update_unload_keep_radius_hysteresis(); if( center_moved ) { reprioritize_job_queue(); maybe_unload_distant_sections( prev_center_row, prev_center_column ); } sync_stream_limits( stream_position ); auto const radius_fill { in_radius_fill_phase() }; auto const inner_ring_ready { section_stream_ready_around( stream_position, kSectionStreamGameplayRadiusKm ) }; auto const ring_radius { radius_fill ? kSectionStreamTargetRadiusKm : kSectionStreamGameplayRadiusKm }; auto const cam_speed { camera_stream_speed_mps() }; auto const reenqueue_distance { radius_fill ? kCatchupReenqueueDistanceM : ( ( g_stream_catchup || cam_speed > 80.0 ) ? std::clamp( cam_speed * 0.25, kCatchupReenqueueDistanceM, kReenqueueDistanceM ) : kReenqueueDistanceM ) }; auto const current_section_unloaded { section_has_pack_models( center_row, center_column ) && false == g_stream.loaded_sections.contains( section_index( center_row, center_column ) ) }; auto const ring_deficit { stream_ring_deficit() }; auto const desperate { ring_deficit || current_section_unloaded }; auto max_lookahead { 0 }; if( radius_fill ) { max_lookahead = 0; } else if( inner_ring_ready || desperate ) { if( desperate && cam_speed > 200.0 ) { max_lookahead = cam_speed > 600.0 ? 10 : 8; } else { max_lookahead = ( g_stream_catchup || cam_speed > 200.0 ) ? kMaintenanceLookaheadMax : ( cam_speed > 80.0 ? 6 : 4 ); } max_lookahead = std::min( max_lookahead, kMaintenanceLookaheadMax ); } auto const travel_forward { guess_travel_forward() }; auto const ring_incomplete { radius_fill ? false == section_stream_ready_around( stream_position, kSectionStreamTargetRadiusKm ) : ( current_section_unloaded || false == inner_ring_ready ) }; auto const should_reenqueue { center_moved || false == g_stream.has_last_enqueue_position || moved_since_enqueue >= reenqueue_distance }; if( current_section_unloaded ) { enqueue_section_if_needed( center_row, center_column, 0 ); } if( should_reenqueue || ring_incomplete ) { enqueue_sections_around( center_row, center_column, ring_radius, stream_position ); g_stream.last_enqueue_position = stream_position; g_stream.has_last_enqueue_position = true; if( should_reenqueue || ring_incomplete ) { pack_bench_inc( &Eu7PackBench::stream_reenqueue ); } } if( false == radius_fill && ( inner_ring_ready || desperate ) && max_lookahead > 0 && cam_speed > 50.0 ) { enqueue_movement_lookahead( center_row, center_column, ring_radius, travel_forward, max_lookahead ); } } [[nodiscard]] bool section_stream_drain_idle( glm::dvec3 const &world_position ) { (void)world_position; return false; } void drain_section_stream( std::size_t const max_cold_meshes, std::size_t const max_instances ) { (void)max_cold_meshes; (void)max_instances; if( false == g_stream.active ) { return; } (void)drain_pack_mesh_loader_ready( g_stream.mesh_cache ); auto const stream_position { resolve_section_stream_position( g_loading_screen_dismissed ? Global.pCamera.Pos : stream_loading_position() ) }; auto const loading { false == g_loading_screen_dismissed }; if( false == loading ) { sync_stream_limits( stream_position ); } if( loading ) { if( section_stream_needs_bootstrap() ) { kick_section_stream_bootstrap(); } else if( stream_position.x != 0.0 || stream_position.y != 0.0 || stream_position.z != 0.0 ) { replenish_bootstrap_ring( stream_position ); } (void)pump_pack_mesh_loader( 8.0, 4 ); } else { if( stream_position.x != 0.0 || stream_position.y != 0.0 || stream_position.z != 0.0 ) { replenish_bootstrap_ring( stream_position ); } auto const radius_fill { in_radius_fill_phase() }; auto const gpu_init_budget { radius_fill ? 12.0 : ( g_stream_catchup ? 14.0 : 10.0 ) }; auto const gpu_init_loads { radius_fill ? std::size_t { 12 } : ( g_stream_catchup ? std::size_t { 14 } : std::size_t { 10 } ) }; (void)pump_pack_mesh_loader( gpu_init_budget, gpu_init_loads ); if( false == radius_fill ) { prefetch_ready_queue_umes_worker( kReadyUmesPrefetchBudgetMs, g_stream_catchup ? kReadyUmesPrefetchMaxMeshes + kReadyUmesPrefetchMaxMeshes / 2 : kReadyUmesPrefetchMaxMeshes ); prefetch_ready_queue_textures( ready_texture_prefetch_budget_ms() ); } } auto const radius_fill { false == loading && in_radius_fill_phase() }; drain_apply_budget( loading ? kLoaderDrainBudgetMs : frame_drain_budget_ms(), loading ? kLoaderSliceInstances : ( radius_fill ? kRadiusFillSliceInstances : 0 ), 0, 0, loading ? kLoaderSectionsPerDrain : gameplay_max_chunks_per_drain() ); if( loading ) { maybe_log_loading_stream_status(); } else { maybe_log_stream_status( stream_position ); } } bool section_stream_active() { return g_stream.active; } bool section_stream_needs_bootstrap() { return g_stream.active && g_stream.bootstrap_pending; } void kick_section_stream_bootstrap() { if( false == section_stream_needs_bootstrap() ) { return; } auto position { resolve_stream_position() }; if( position.x == 0.0 && position.y == 0.0 && position.z == 0.0 ) { position = guess_initial_stream_position( stream_module() ); } if( position.x == 0.0 && position.y == 0.0 && position.z == 0.0 ) { WriteLog( "EU7 PACK: bootstrap async — brak pozycji startowej" ); g_stream.bootstrap_pending = false; return; } remember_stream_anchor( position ); auto const [row, column] { section_row_column( position ) }; g_stream.center_row = row; g_stream.center_column = column; WriteLog( "EU7 PACK: bootstrap async " + std::to_string( kInitialBootstrapEnqueueRadius ) + "km, sekcja " + std::to_string( row ) + "," + std::to_string( column ) ); enqueue_sections_around( row, column, kInitialBootstrapEnqueueRadius, position ); g_stream.bootstrap_pending = false; } void try_bootstrap_section_stream() { if( false == section_stream_needs_bootstrap() ) { return; } auto position { resolve_stream_position() }; if( position.x == 0.0 && position.y == 0.0 && position.z == 0.0 ) { position = guess_initial_stream_position( stream_module() ); } if( position.x == 0.0 && position.y == 0.0 && position.z == 0.0 ) { return; } bootstrap_section_stream( position ); } void preload_section_stream( double const max_drain_ms ) { if( false == g_stream.active ) { return; } simulation::State.drain_deferred_eu7_trainsets( max_drain_ms > 0.0 ? 8.0 : 0.0 ); auto position { resolve_stream_position() }; if( position.x == 0.0 && position.y == 0.0 && position.z == 0.0 ) { position = guess_initial_stream_position( stream_module() ); } position = resolve_section_stream_position( position ); if( section_stream_needs_bootstrap() ) { if( position.x != 0.0 || position.y != 0.0 || position.z != 0.0 ) { kick_section_stream_bootstrap(); } } else if( position.x != 0.0 || position.y != 0.0 || position.z != 0.0 ) { update_section_stream( position ); } if( max_drain_ms <= 0.0 ) { WriteLog( "EU7 PACK: preload async kick, sekcji=" + std::to_string( load_stats().pack_sections_loaded ) + " modele=" + std::to_string( load_stats().pack_models ) ); return; } auto const started { std::chrono::steady_clock::now() }; while( true ) { drain_until_budget( kLoaderDrainBudgetMs ); if( position.x != 0.0 || position.y != 0.0 || position.z != 0.0 ) { update_section_stream( position ); } auto const elapsed_ms { std::chrono::duration_cast( std::chrono::steady_clock::now() - started ).count() }; if( static_cast( elapsed_ms ) >= max_drain_ms ) { break; } if( position.x != 0.0 || position.y != 0.0 || position.z != 0.0 ) { if( section_stream_ready_around( position, kSectionStreamLoadingDismissRadiusKm ) ) { break; } if( center_section_stream_satisfied( position ) && section_stream_ring_progress( position, kSectionStreamLoadingDismissRadiusKm ) >= kEarlyDismissRingProgress ) { break; } } if( g_stream.pending_apply.has_value() || ready_queue_size() > 0 || false == g_stream.in_flight_sections.empty() ) { continue; } if( load_stats().pack_models >= 400 ) { break; } std::this_thread::sleep_for( std::chrono::milliseconds( 4 ) ); } WriteLog( "EU7 PACK: preload sekcji=" + std::to_string( load_stats().pack_sections_loaded ) + " modele=" + std::to_string( load_stats().pack_models ) ); } [[nodiscard]] bool ring_section_in_radius( int const row, int const column, int const center_row, int const center_column, int const radius_km ) { return std::abs( row - center_row ) <= radius_km && std::abs( column - center_column ) <= radius_km; } [[nodiscard]] bool ring_has_pending_pack_work( int const center_row, int const center_column, int const radius_km ) { for( int row { center_row - radius_km }; row <= center_row + radius_km; ++row ) { if( row < 0 || row >= kRegionSideSectionCount ) { continue; } for( int column { center_column - radius_km }; column <= center_column + radius_km; ++column ) { if( column < 0 || column >= kRegionSideSectionCount ) { continue; } if( false == section_has_pack_models( row, column ) ) { continue; } auto const section_idx { section_index( row, column ) }; if( g_stream.in_flight_sections.contains( section_idx ) ) { return true; } if( g_stream.pending_apply.has_value() && g_stream.pending_apply->section_idx == section_idx ) { return true; } } } { std::lock_guard lock { g_stream.ready.mutex }; for( auto const &batch : g_stream.ready.data ) { if( ring_section_in_radius( batch.row, batch.column, center_row, center_column, radius_km ) && section_has_pack_models( batch.row, batch.column ) ) { return true; } } } return false; } bool section_stream_ready_around( glm::dvec3 const &world_position, int const radius_km ) { if( false == g_stream.active ) { return true; } auto const [center_row, center_column] { section_row_column( world_position ) }; if( false == is_bootstrap_ring_loaded( center_row, center_column, radius_km ) ) { return false; } if( g_stream.pending_apply.has_value() ) { auto const &batch { *g_stream.pending_apply }; if( ring_section_in_radius( batch.row, batch.column, center_row, center_column, radius_km ) && section_has_pack_models( batch.row, batch.column ) && batch.models != nullptr ) { return false; } } if( ring_has_pending_pack_work( center_row, center_column, radius_km ) ) { return false; } if( section_stream_needs_bootstrap() ) { return false; } return true; } bool section_stream_presentable_around( glm::dvec3 const &world_position, int const radius_km ) { if( false == section_stream_ready_around( world_position, radius_km ) ) { g_ring_ready_since.reset(); return false; } auto const now { std::chrono::steady_clock::now() }; if( false == g_ring_ready_since.has_value() ) { g_ring_ready_since = now; return false; } return ( now - *g_ring_ready_since ) >= kPresentableHoldMs; } bool loading_screen_dismissed() { return g_loading_screen_dismissed; } void dismiss_loading_screen() { pack_bench_begin_stream_phase(); g_loading_screen_dismissed = true; g_stream.radius_fill_active = true; g_ring_ready_since.reset(); simulation::is_ready = true; WriteLog( "EU7 PACK: loading screen dismissed — faza radius_fill do " + std::to_string( kSectionStreamTargetRadiusKm ) + "km" ); if( g_stream.has_anchor_position ) { auto const &saved_camera { Global.FreeCameraInit[ 0 ] }; auto const camera_unset { saved_camera.x == 0.0 && saved_camera.y == 0.0 && saved_camera.z == 0.0 }; auto const camera_off_scenery { false == camera_unset && camera_far_from_stream_anchor( saved_camera ) }; if( camera_unset || camera_off_scenery ) { Global.FreeCameraInit[ 0 ] = g_stream.anchor_position; Global.pCamera.Pos = g_stream.anchor_position; WriteLog( "EU7 PACK: ustawiam kamere startowa na " + std::to_string( g_stream.anchor_position.x ) + "," + std::to_string( g_stream.anchor_position.y ) + "," + std::to_string( g_stream.anchor_position.z ) ); } } } bool loading_screen_blocks_world( glm::dvec3 const &world_position, int const radius_km ) { (void)radius_km; if( false == g_stream.active || g_loading_screen_dismissed ) { return false; } if( load_stats().pack_models >= kBootstrapApplyModelThreshold ) { WriteLog( "EU7 PACK: loading screen off, pack_models=" + std::to_string( load_stats().pack_models ) ); dismiss_loading_screen(); return false; } auto const [center_row, center_column] { section_row_column( world_position ) }; if( section_has_pack_models( center_row, center_column ) ) { auto const section_idx { section_index( center_row, center_column ) }; if( g_stream.loaded_sections.contains( section_idx ) ) { WriteLog( "EU7 PACK: loading screen off, centrum zaladowane" ); dismiss_loading_screen(); return false; } } if( g_loading_block_started.has_value() ) { auto const blocked_for { std::chrono::steady_clock::now() - *g_loading_block_started }; if( blocked_for >= kLoadingScreenMaxBlockSec ) { ErrorLog( "EU7 PACK: loading screen timeout — wchodzę w świat (pack_models=" + std::to_string( load_stats().pack_models ) + ", ready_q=" + std::to_string( ready_queue_size() ) + ", in_flight=" + std::to_string( g_stream.in_flight_sections.size() ) + ")" ); dismiss_loading_screen(); return false; } } return true; } glm::dvec3 stream_loading_position() { if( g_loading_screen_dismissed ) { return Global.pCamera.Pos; } auto const &camera { Global.pCamera.Pos }; if( camera.x != 0.0 || camera.y != 0.0 || camera.z != 0.0 ) { return camera; } auto position { resolve_stream_position() }; if( position.x != 0.0 || position.y != 0.0 || position.z != 0.0 ) { return position; } if( g_stream.has_anchor_position ) { return g_stream.anchor_position; } return Global.FreeCameraInit[ 0 ]; } float section_stream_ring_progress( glm::dvec3 const &world_position, int const radius_km ) { if( false == g_stream.active ) { return 1.f; } auto const [center_row, center_column] { section_row_column( world_position ) }; std::size_t pack_sections { 0 }; float accumulated { 0.f }; for( int row { center_row - radius_km }; row <= center_row + radius_km; ++row ) { if( row < 0 || row >= kRegionSideSectionCount ) { continue; } for( int column { center_column - radius_km }; column <= center_column + radius_km; ++column ) { if( column < 0 || column >= kRegionSideSectionCount ) { continue; } if( false == section_has_pack_models( row, column ) ) { continue; } ++pack_sections; accumulated += section_ring_apply_fraction( row, column ); } } if( pack_sections == 0 ) { return 1.f; } return std::clamp( accumulated / static_cast( pack_sections ), 0.f, 1.f ); } void remember_stream_anchor( glm::dvec3 const &position ) { if( position.x == 0.0 && position.y == 0.0 && position.z == 0.0 ) { return; } g_stream.anchor_position = position; g_stream.has_anchor_position = true; } [[nodiscard]] glm::dvec3 resolve_section_stream_position( glm::dvec3 const &hint ) { if( hint.x == 0.0 && hint.y == 0.0 && hint.z == 0.0 && g_stream.has_anchor_position ) { return g_stream.anchor_position; } if( camera_far_from_stream_anchor( hint ) && g_stream.has_anchor_position ) { return g_stream.anchor_position; } return hint; } [[nodiscard]] bool camera_far_from_stream_anchor( glm::dvec3 const &camera_position ) { if( false == g_stream.has_anchor_position ) { return false; } auto const anchor_dist { glm::length( glm::dvec2 { camera_position.x - g_stream.anchor_position.x, camera_position.z - g_stream.anchor_position.z } ) }; auto const anchor_magnitude { glm::length( glm::dvec2 { g_stream.anchor_position.x, g_stream.anchor_position.z } ) }; auto const camera_magnitude { glm::length( glm::dvec2 { camera_position.x, camera_position.z } ) }; return anchor_dist > 30000.0 && camera_magnitude < 30000.0 && anchor_magnitude > 50000.0; } void reset_section_stream() { flush_pack_stream_bench(); stop_workers(); if( g_stream.serializer != nullptr ) { g_stream.serializer->reset_eu7_pack_section_instances(); } reset_stream_fields(); g_ring_ready_since.reset(); g_loading_block_started.reset(); g_loading_screen_dismissed = false; g_stream_catchup = false; g_stream_max_in_flight = kMaxInFlightSections; g_stream_max_ready = kMaxReadySections; reset_pack_bench(); } bool section_stream_frame_budget_pressure() { return g_stream_catchup || pending_apply_is_urgent() || in_radius_fill_phase(); } } // namespace scene::eu7