From 794ac578de5c46d9b353f3fa9f1fce1128d34629 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Fri, 23 Oct 2026 23:35:02 +0200 Subject: [PATCH] perf(textures): with texture streaming read the data ahead of the camera instead of the whole scenery at load, drop read-ahead data unused for 5 min --- model/AnimModel.h | 1 + model/Texture.cpp | 85 +++++++++++- model/Texture.h | 14 ++ rendering/opengl46/opengl46renderer.cpp | 176 ++++++++++++++++++++++++ rendering/opengl46/opengl46renderer.h | 6 + 5 files changed, 281 insertions(+), 1 deletion(-) diff --git a/model/AnimModel.h b/model/AnimModel.h index 263c752c..5e975b4d 100644 --- a/model/AnimModel.h +++ b/model/AnimModel.h @@ -202,6 +202,7 @@ public: glm::vec3 m_rotationscaleangles { 0.0f, 0.0f, 0.0f }; glm::vec3 m_rotationscalescale { 1.0f, 1.0f, 1.0f }; bool m_rotationscalevalid { false }; + bool m_texturesprefetched { false }; // the renderer had the data of its textures read ahead (gfx.textures.streaming) material_data m_materialdata; std::string asText; // tekst dla wyƛwietlacza znakowego diff --git a/model/Texture.cpp b/model/Texture.cpp index d4fc449b..f8def211 100644 --- a/model/Texture.cpp +++ b/model/Texture.cpp @@ -43,6 +43,9 @@ struct texture_request { int height { 0 }; int mapcount { 0 }; int blocksize { 0 }; + // handed to the workers (opengl_texture::prefetch()), and when: the main thread only + bool submitted { false }; + std::chrono::steady_clock::time_point submittime; // results, written by the thread reading the data std::vector data; bool failed { false }; @@ -90,6 +93,22 @@ public: } m_read.wait( lock, [ &Request ]() { return Request->done; } ); } + // bytes of the data read for a request, 0 while it isn't there yet + std::size_t read_size( texture_request const &Request ) { + std::lock_guard lock( m_mutex ); + return ( Request.done ? Request.data.capacity() : 0 ); } + + // gives back the data read for a request, once it's there. the request can be submitted again then + bool drop( texture_request &Request ) { + std::lock_guard lock( m_mutex ); + if( false == Request.done ) { + return false; + } + Request.data = std::vector(); + Request.done = false; + Request.failed = false; + return true; } + ~texture_loader() { { std::lock_guard lock( m_mutex ); @@ -613,7 +632,8 @@ opengl_texture::load_DDS() { request->height = data_height; request->mapcount = data_mapcount; request->blocksize = blockSize; - texture_loader::instance().submit( request ); + // the data is read when it's going to be needed (prefetch()), or the first time the texture is made: the data of textures + // nothing draws yet would take the memory for nothing data_components = ddsd.ddpfPixelFormat.dwFourCC == FOURCC_DXT1 ? GL_RGB : GL_RGBA; @@ -1096,6 +1116,8 @@ opengl_texture::complete_data() { if( request == nullptr ) { return; } + // read right away if nothing asked for the data before + prefetch(); texture_loader::instance().wait( request ); auto const completed { std::move( request ) }; @@ -1111,6 +1133,17 @@ opengl_texture::complete_data() { size = data.size() / 1024; } +// has a worker thread read the data of the texture, if it's waiting for that +void +opengl_texture::prefetch() { + + if( ( request == nullptr ) || ( true == request->submitted ) ) { return; } + + request->submitted = true; + request->submittime = std::chrono::steady_clock::now(); + texture_loader::instance().submit( request ); +} + // releases resources allocated on the opengl end, storing local copy if requested void opengl_texture::release() { @@ -1424,6 +1457,7 @@ texture_manager::mark_as_needed( texture_handle const Texture, bool const Near ) // the vehicle came back, the texture is read again before it's drawn (in the background with gfx.textures.streaming) texture.reload_on_use = false; texture.load(); + texture.prefetch(); } } else if( needed == resource_timestamp() ) { @@ -1499,10 +1533,59 @@ texture_manager::memory() const { ++result.gpu.count; result.gpu.bytes += static_cast( texture.first->size ) * 1024; } + if( texture.first->request != nullptr ) { + auto const &request { *texture.first->request }; + if( false == request.submitted ) { + ++result.waiting; + } + else if( auto const size { texture_loader::instance().read_size( request ) }; size > 0 ) { + ++result.readahead.count; + result.readahead.bytes += size; + } + else { + ++result.reading; + } + } } return result; } +// has the data of a texture read, if it's waiting for that +void +texture_manager::prefetch( texture_handle const Texture ) { + + if( ( Texture <= 0 ) + || ( Texture >= static_cast( m_textures.size() ) ) ) { + return; + } + m_textures[ Texture ].first->prefetch(); +} + +// gives back the data read for textures which nothing drew within the specified time since it was asked for. it's read again when +// a texture is needed +std::size_t +texture_manager::drop_read_ahead( std::chrono::steady_clock::duration const Age ) { + + auto const now { std::chrono::steady_clock::now() }; + std::size_t count { 0 }; + for( auto const &texture : m_textures ) { + if( ( texture.first == nullptr ) + || ( texture.first->request == nullptr ) ) { + continue; + } + auto &request { *texture.first->request }; + if( ( false == request.submitted ) + || ( now - request.submittime < Age ) ) { + continue; + } + if( true == texture_loader::instance().drop( request ) ) { + request.submitted = false; + ++count; + } + } + return count; +} + // debug performance string std::string texture_manager::info() const { diff --git a/model/Texture.h b/model/Texture.h index d4f15071..87a9e046 100644 --- a/model/Texture.h +++ b/model/Texture.h @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #pragma once #include +#include #include #include "winheaders.h" #include "utilities/memorystats.h" @@ -117,6 +118,8 @@ public: // takes over the data read by a worker thread, waiting for it if needed void complete_data(); + // has a worker thread read the data of the texture, if it's waiting for that (gfx.textures.streaming) + void prefetch(); // members std::shared_ptr request; // data being read by a worker thread @@ -183,12 +186,23 @@ public: info() const; // what the textures keep in memory, for the debug panel: copies of the data kept in the process (count, bytes), and the // textures made on the gpu with the size of their data (count, bytes) + // with gfx.textures.streaming also the data read for textures not made yet (count, bytes), the textures which wait for a + // worker to read it, and the ones which wait to be asked for it struct memory_data { memory_count cpu; memory_count gpu; + memory_count readahead; + std::uint64_t reading { 0 }; + std::uint64_t waiting { 0 }; }; memory_data memory() const; + // has the data of a texture read, if it's waiting for that (gfx.textures.streaming) + void + prefetch( texture_handle const Texture ); + // gives back the data read for textures which nothing drew within the specified time since it was asked for + std::size_t + drop_read_ahead( std::chrono::steady_clock::duration const Age ); private: // types: diff --git a/rendering/opengl46/opengl46renderer.cpp b/rendering/opengl46/opengl46renderer.cpp index 99c498af..eddd180c 100644 --- a/rendering/opengl46/opengl46renderer.cpp +++ b/rendering/opengl46/opengl46renderer.cpp @@ -1254,8 +1254,13 @@ void opengl46_renderer::Update_Memory_Info() // textures: the copies of the data kept in the process after the upload auto const textures { m_textures.memory() }; accounted += textures.cpu.bytes; + accounted += textures.readahead.bytes; text += "textures: " + std::to_string( textures.cpu.count ) + " keep their data in ram, " + mb( textures.cpu.bytes ) + "; " + std::to_string( textures.gpu.count ) + " on the card, ~" + mb( textures.gpu.bytes ) + "\n"; + if( textures.readahead.count + textures.reading + textures.waiting > 0 ) { + text += " streamed: " + std::to_string( textures.readahead.count ) + " read ahead, not drawn yet, " + mb( textures.readahead.bytes ) + "; " + + std::to_string( textures.reading ) + " being read; " + std::to_string( textures.waiting ) + " not read until they're needed\n"; + } // geometry: the copies of the chunks sent to the card auto const geometry { m_geometry.memory() }; accounted += geometry.vertices.bytes + geometry.indices.bytes + geometry.userdata.bytes; @@ -6118,6 +6123,172 @@ void opengl46_renderer::Update_Texture_Release() m_textures.release_unneeded(); } +// with gfx.textures.streaming the data of a texture is read when it's going to be drawn, not with the scenery: the textures of the +// whole scenery don't fit in memory. the cells of the region within the draw range and a margin around the camera are gone through +// a few in a frame, and the textures of what they hold which will be seen from there are read ahead, as are the ones of the vehicles +// nearby. what is drawn without that waits for its data to be read +void opengl46_renderer::Update_Texture_Prefetch() +{ + if (m_colorpass.draw_range <= 0.f) + { + return; + } + double const margin{500.0}; // m: the way the data has to arrive + std::size_t const cellsperframe{32}; + auto const camera{m_colorpass.pass_camera.position()}; + auto const range{static_cast(m_colorpass.draw_range) * Global.fDistanceFactor + margin}; + + auto const prefetch_material = [this](material_handle const Material) { + if (Material <= 0) + { + return; + } + auto const &material{m_materials.material(Material)}; + for (auto const texture : material.textures) + { + m_textures.prefetch(texture); + } + for (auto const &variant : material.texture_variants) + { + for (auto const texture : variant) + { + m_textures.prefetch(texture); + } + } + }; + auto const prefetch_model = [this](TModel3d *Model) { + if (Model == nullptr) + { + return; + } + for (auto const texture : Model_Textures(Model)) + { + m_textures.prefetch(texture); + } + }; + + // the sections around the camera, the same way Render(scene::basic_region *) picks them + int constexpr cellspersection{(scene::EU07_SECTIONSIZE / scene::EU07_CELLSIZE) * (scene::EU07_SECTIONSIZE / scene::EU07_CELLSIZE)}; + auto const camerax{static_cast(std::floor(camera.x / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2))}; + auto const cameraz{static_cast(std::floor(camera.z / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2))}; + int const segmentcount{2 * static_cast(std::ceil(range / scene::EU07_SECTIONSIZE))}; + int const side{segmentcount + 1}; + int const originx{camerax - segmentcount / 2}; + int const originz{cameraz - segmentcount / 2}; + auto const cellcount{static_cast(side) * side * cellspersection}; + auto *region{simulation::Region}; + + // the first time the whole range at once: the textures around the start are read by the workers, not one by one by the first frame + auto const steps{m_prefetchcursor == 0 ? cellcount : cellsperframe}; + for (std::size_t step = 0; step < steps; ++step, ++m_prefetchcursor) + { + auto const index{m_prefetchcursor % cellcount}; + auto const sectionindex{static_cast(index / cellspersection)}; + auto const cellindex{index % cellspersection}; + auto const row{originz + sectionindex / side}; + auto const column{originx + sectionindex % side}; + if ((row < 0) || (row >= scene::EU07_REGIONSIDESECTIONCOUNT) || (column < 0) || (column >= scene::EU07_REGIONSIDESECTIONCOUNT)) + { + continue; + } + auto *section{region->m_sections[row * scene::EU07_REGIONSIDESECTIONCOUNT + column]}; + if (section == nullptr) + { + continue; + } + if ((cellindex == 0) && (glm::length(section->m_area.center - camera) - section->m_area.radius <= range)) + { + for (auto const &shape : section->m_shapes) + { + prefetch_material(shape.data().material); + } + } + auto const &cell{section->m_cells[cellindex]}; + if ((false == cell.m_active) || (glm::length(cell.m_area.center - camera) - cell.m_area.radius > range)) + { + continue; + } + for (auto const &shape : cell.m_shapesopaque) + { + prefetch_material(shape.data().material); + } + for (auto const &shape : cell.m_shapestranslucent) + { + prefetch_material(shape.data().material); + } + for (auto const *track : cell.m_paths) + { + prefetch_material(track->m_material1); + prefetch_material(track->m_material2); + prefetch_material(track->m_sleeper_skin); + if (track->SwitchExtension != nullptr) + { + prefetch_material(track->SwitchExtension->m_material3); + } + } + // the instances which will be seen from within the margin. the ones asked for once aren't gone through again until they're + // out of reach, it's when their data could have been dropped + auto const prefetch_instances = [&](scene::basic_cell::instance_sequence const &Instances) { + for (auto *instance : Instances) + { + auto const distance{glm::length(instance->location() - camera)}; + auto const reach{std::sqrt(instance->m_rangesquaredmax * Global.fDistanceFactor) + margin}; + if ((distance > reach) || (false == instance->visible())) + { + if (distance > reach + margin) + { + instance->m_texturesprefetched = false; + } + continue; + } + if (true == instance->m_texturesprefetched) + { + continue; + } + instance->m_texturesprefetched = true; + prefetch_model(instance->Model()); + for (auto const skin : instance->Material()->replacable_skins) + { + prefetch_material(skin); + } + } + }; + prefetch_instances(cell.m_instancesopaque); + prefetch_instances(cell.m_instancetranslucent); + } + + auto const now{std::chrono::steady_clock::now()}; + if (now - m_prefetchvehicletime >= std::chrono::seconds(1)) + { + m_prefetchvehicletime = now; + for (auto *vehicle : simulation::Vehicles.sequence()) + { + if ((vehicle == nullptr) || (glm::length(vehicle->vPosition - camera) - vehicle->radius() > range)) + { + continue; + } + prefetch_model(vehicle->mdModel); + prefetch_model(vehicle->mdLowPolyInt); + prefetch_model(vehicle->mdLoad); + for (auto *attachment : vehicle->mdAttachments) + { + prefetch_model(attachment); + } + for (auto const skin : vehicle->m_materialdata.replacable_skins) + { + prefetch_material(skin); + } + } + } + // the data read for what wasn't drawn after all (the details of models seen only from close, or a turn not taken) goes after a + // while + if (now - m_prefetchdroptime >= std::chrono::seconds(10)) + { + m_prefetchdroptime = now; + m_textures.drop_read_ahead(std::chrono::minutes(5)); + } +} + // textures used by materials of the sub-models of specified model, replaceable skins excluded std::vector const &opengl46_renderer::Model_Textures(TModel3d *Model) { @@ -8506,6 +8677,11 @@ void opengl46_renderer::Update(double const Deltatime) Update_Texture_Release(); } + if ((true == Global.gfx_texture_streaming) && (true == simulation::is_ready) && (simulation::Region != nullptr)) + { + Update_Texture_Prefetch(); + } + if ((true == Global.ControlPicking) && (false == FreeFlyModeFlag)) Pick_Control_Callback([](const TSubModel *, const glm::vec2) {}); // temporary conditions for testing. eventually will be coupled with editor mode diff --git a/rendering/opengl46/opengl46renderer.h b/rendering/opengl46/opengl46renderer.h index a6ed0318..488a546b 100644 --- a/rendering/opengl46/opengl46renderer.h +++ b/rendering/opengl46/opengl46renderer.h @@ -507,6 +507,8 @@ class opengl46_renderer : public gfx_renderer { void Update_Texture_Release(); // textures used by materials of the sub-models of specified model, replaceable skins excluded std::vector const &Model_Textures( TModel3d *Model ); + // has the data of the textures read ahead of the camera, with gfx.textures.streaming + void Update_Texture_Prefetch(); void Render(TMemCell *Memcell); void Render_particles(); void Render_precipitation(); @@ -647,6 +649,10 @@ class opengl46_renderer : public gfx_renderer { std::unordered_map, scene::basic_cell::instance_bucket_key_hash> m_frame_instance_buckets; // textures used by vehicle models, for the release of far textures std::unordered_map> m_modeltextures; + // the read ahead of textures (Update_Texture_Prefetch()): the cell it goes on with + std::size_t m_prefetchcursor { 0 }; + std::chrono::steady_clock::time_point m_prefetchvehicletime; + std::chrono::steady_clock::time_point m_prefetchdroptime; // geometry of neighbouring tracks of a cell which share the material, gathered to be drawn together. reused by each Render(path_sequence) std::vector m_pathbatch; // instance records kept by the buckets of scene cells are good as long as they carry this number.