From 28b4e57133c563629afafd029c60de9317e1efca Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Thu, 8 Oct 2026 16:17:46 +0200 Subject: [PATCH] feat(loading): optional background loading: sound files decoded by worker threads (sound.asyncload), data of dds textures read by worker threads (gfx.textures.streaming), gl textures of vehicles farther than a multiple of the draw range released (gfx.textures.releasedistance); all off by default --- application/scenarioloadermode.cpp | 3 + audio/audio.cpp | 242 +++++++++++++++++++++++--- audio/audio.h | 14 +- audio/audiorenderer.cpp | 10 ++ audio/audiorenderer.h | 2 + model/ResourceManager.h | 4 + model/Texture.cpp | 264 ++++++++++++++++++++++++++++- model/Texture.h | 19 +++ rendering/opengl33renderer.cpp | 118 +++++++++++++ rendering/opengl33renderer.h | 6 + utilities/Globals.cpp | 21 +++ utilities/Globals.h | 3 + 12 files changed, 683 insertions(+), 23 deletions(-) diff --git a/application/scenarioloadermode.cpp b/application/scenarioloadermode.cpp index a2293fe5..dbdce606 100644 --- a/application/scenarioloadermode.cpp +++ b/application/scenarioloadermode.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/renderer.h" #include "utilities/Logs.h" #include "utilities/translation.h" +#include "audio/audiorenderer.h" scenarioloader_mode::scenarioloader_mode() { m_userinterface = std::make_shared(); @@ -58,6 +59,8 @@ bool scenarioloader_mode::update() { // the loading state holds the scenario file open, which on some systems keeps the file from being replaced // when the scenery is saved in the editor. it'd also get in the way of another load state.reset(); + // sound files decoded in the background are all ready before the simulation starts + audio::renderer.complete_buffers(); WriteLog( "Scenario loading time: " + std::to_string( std::chrono::duration_cast( std::chrono::system_clock::now() - timestart ).count() ) + " seconds" ); // TODO: implement and use next mode cue diff --git a/audio/audio.cpp b/audio/audio.cpp index 1c72d1bc..3be2810d 100644 --- a/audio/audio.cpp +++ b/audio/audio.cpp @@ -19,35 +19,42 @@ http://mozilla.org/MPL/2.0/. namespace audio { -openal_buffer::openal_buffer( std::string const &Filename ) : - name( Filename ) { +namespace { + +// content of a sound file, mixed down to 16 bit mono +struct decoded_sound { + std::vector samples; + unsigned int rate { 0 }; + bool mixed { false }; // the file had more than one channel +}; + +// reads and decodes whole sound file. throws std::runtime_error on failure +// NOTE: uses no simulator state, it's also run by the background decoder threads +decoded_sound decode_sound( std::string const &File ) { SF_INFO si; si.format = 0; - std::string file = Filename; - - WriteLog("sound: loading file: " + file); - - SNDFILE *sf = sf_open(file.c_str(), SFM_READ, &si); + SNDFILE *sf = sf_open(File.c_str(), SFM_READ, &si); if (sf == nullptr) throw std::runtime_error("sound: sf_open failed"); sf_command(sf, SFC_SET_NORM_FLOAT, nullptr, SF_TRUE); - float *fbuf = new float[si.frames * si.channels]; - if (sf_readf_float(sf, fbuf, si.frames) != si.frames) + std::unique_ptr fbuf { new float[si.frames * si.channels] }; + if (sf_readf_float(sf, fbuf.get(), si.frames) != si.frames) { + sf_close(sf); throw std::runtime_error("sound: incomplete file"); + } sf_close(sf); - rate = si.samplerate; + decoded_sound sound; + sound.rate = si.samplerate; + sound.mixed = ( si.channels != 1 ); + sound.samples.resize( si.frames ); - if (si.channels != 1) - WriteLog("sound: warning: mixing multichannel file to mono"); - - int16_t *buf = new int16_t[si.frames]; for (size_t i = 0; i < si.frames; i++) { float accum = 0; @@ -59,26 +66,149 @@ openal_buffer::openal_buffer( std::string const &Filename ) : val = 32767; if (val < -32767) val = -32767; - buf[i] = val; + sound.samples[i] = val; } + return sound; +} + +// creates AL buffer holding provided sound. returns: id of the buffer, or null_resource on failure +ALuint upload_sound( decoded_sound const &Sound ) { + + ALuint id { null_resource }; alGenBuffers(1, &id); if (id != null_resource && alIsBuffer(id)) { alGetError(); - alBufferData(id, AL_FORMAT_MONO16, buf, si.frames * 2, rate); + alBufferData(id, AL_FORMAT_MONO16, Sound.samples.data(), static_cast(Sound.samples.size() * 2), Sound.rate); } else { id = null_resource; const char *str = alGetString(alGetError()); ErrorLog("sound: failed to create AL buffer: " + (str != nullptr ? std::string(str) : "")); } + return id; +} - delete[] buf; - delete[] fbuf; +} // namespace + +openal_buffer::openal_buffer( std::string const &Filename ) : + name( Filename ) { + + WriteLog("sound: loading file: " + Filename); + + auto const sound { decode_sound( Filename ) }; + + rate = sound.rate; + + if (sound.mixed) + WriteLog("sound: warning: mixing multichannel file to mono"); + + id = upload_sound( sound ); fetch_caption(); } +// sound files decoded by worker threads. AL buffers for them are made on the main thread +struct buffer_manager::decoder { + + struct job { + audio::buffer_handle handle; + std::string file; + decoded_sound sound; // written by the thread decoding the file + std::string error; + bool done { false }; // guarded by the mutex + }; + + std::mutex mutex; + std::condition_variable queued; // wakes workers when files are added + std::condition_variable decoded; // wakes the main thread waiting for a file + std::deque> queue; // files no worker has taken yet + std::vector> pending; // files without AL buffer yet, in order of request. main thread only + bool stop { false }; + std::vector workers; + + decoder() { + auto const count { std::clamp( std::thread::hardware_concurrency(), 2, 9 ) - 1 }; + for( unsigned int idx = 0; idx < count; ++idx ) { + workers.emplace_back( [ this ]() { work(); } ); + } + } + ~decoder() { + { + std::lock_guard lock( mutex ); + stop = true; + } + queued.notify_all(); + for( auto &worker : workers ) { + worker.join(); + } + } + + static void decode( job &Item ) { + try { + Item.sound = decode_sound( Item.file ); + } + catch( std::exception const &Error ) { + Item.error = Error.what(); + } + } + + void work() { + std::unique_lock lock( mutex ); + while( true ) { + queued.wait( lock, [ this ]() { return stop || ( false == queue.empty() ); } ); + if( stop ) { + return; + } + auto item { queue.front() }; + queue.pop_front(); + lock.unlock(); + decode( *item ); + lock.lock(); + item->done = true; + decoded.notify_all(); + } + } + + // waits until the specified file is decoded. a file no worker has taken yet is decoded right away + void wait( std::shared_ptr const &Item ) { + std::unique_lock lock( mutex ); + if( Item->done ) { + return; + } + auto const lookup { std::find( std::begin( queue ), std::end( queue ), Item ) }; + if( lookup != std::end( queue ) ) { + queue.erase( lookup ); + lock.unlock(); + decode( *Item ); + lock.lock(); + Item->done = true; + return; + } + decoded.wait( lock, [ &Item ]() { return Item->done; } ); + } + + bool is_done( job const &Item ) { + std::lock_guard lock( mutex ); + return Item.done; + } + + // makes the AL buffer out of the decoded file + static void finish( job &Item, openal_buffer &Buffer ) { + if( false == Item.error.empty() ) { + ErrorLog( "Bad file: failed to load audio file \"" + Item.file + "\" (" + Item.error + ")", logtype::file ); + return; + } + Buffer.rate = Item.sound.rate; + if( Item.sound.mixed ) { + WriteLog( "sound: warning: mixing multichannel file \"" + Item.file + "\" to mono" ); + } + Buffer.id = upload_sound( Item.sound ); + Item.sound = {}; + Buffer.fetch_caption(); + } +}; + // retrieves sound caption in currently set language void openal_buffer::fetch_caption() { @@ -93,8 +223,16 @@ openal_buffer::fetch_caption() { } } +buffer_manager::buffer_manager() { + + m_buffers.emplace_back( openal_buffer() ); // empty bindings for null buffer +} + buffer_manager::~buffer_manager() { + // workers are stopped before the buffers go + m_decoder.reset(); + for( auto &buffer : m_buffers ) { if( buffer.id != null_resource ) { ::alDeleteBuffers( 1, &buffer.id ); @@ -152,15 +290,81 @@ buffer_manager::create( std::string const &Filename ) { audio::openal_buffer const & buffer_manager::buffer( audio::buffer_handle const Buffer ) const { + if( ( m_decoder != nullptr ) + && ( false == m_decoder->pending.empty() ) + && ( m_buffers[ Buffer ].id == null_resource ) ) { + complete( Buffer ); + } return m_buffers[ Buffer ]; } +// creates AL buffers for files decoded in the background so far, or for all of them if requested +void +buffer_manager::update( bool const Wait ) { + + if( ( m_decoder == nullptr ) + || ( m_decoder->pending.empty() ) ) { + return; + } + std::vector> undone; + for( auto &item : m_decoder->pending ) { + if( Wait ) { + m_decoder->wait( item ); + } + else if( false == m_decoder->is_done( *item ) ) { + undone.emplace_back( item ); + continue; + } + decoder::finish( *item, m_buffers[ item->handle ] ); + } + m_decoder->pending.swap( undone ); +} + +// creates AL buffer for a file decoded in the background, waiting for the decoder if needed +void +buffer_manager::complete( audio::buffer_handle const Buffer ) const { + + auto &pending { m_decoder->pending }; + auto const lookup { + std::find_if( + std::begin( pending ), std::end( pending ), + [ Buffer ]( std::shared_ptr const &Item ) { return Item->handle == Buffer; } ) }; + if( lookup == std::end( pending ) ) { + return; + } + auto const item { *lookup }; + pending.erase( lookup ); + m_decoder->wait( item ); + decoder::finish( *item, m_buffers[ Buffer ] ); +} + // places in the bank a buffer containing data stored in specified file. returns: handle to the buffer audio::buffer_handle buffer_manager::emplace( std::string Filename ) { buffer_handle const handle { m_buffers.size() }; - m_buffers.emplace_back( Filename ); + if( true == Global.AudioAsyncLoad ) { + // the file is decoded in the background, the AL buffer is made once the data is ready or when the buffer is needed + WriteLog( "sound: loading file: " + Filename ); + openal_buffer buffer; + buffer.name = Filename; + m_buffers.emplace_back( std::move( buffer ) ); + if( m_decoder == nullptr ) { + m_decoder = std::make_unique(); + } + auto item { std::make_shared() }; + item->handle = handle; + item->file = Filename; + m_decoder->pending.emplace_back( item ); + { + std::lock_guard lock( m_decoder->mutex ); + m_decoder->queue.emplace_back( item ); + } + m_decoder->queued.notify_one(); + } + else { + m_buffers.emplace_back( Filename ); + } // NOTE: we store mapping without file type extension, to simplify lookups erase_extension( Filename ); diff --git a/audio/audio.h b/audio/audio.h index 5e306d25..391bf1eb 100644 --- a/audio/audio.h +++ b/audio/audio.h @@ -47,21 +47,25 @@ class buffer_manager { public: // constructors - buffer_manager() { m_buffers.emplace_back( openal_buffer() ); } // empty bindings for null buffer + buffer_manager(); // makes empty bindings for null buffer // destructor ~buffer_manager(); // methods // creates buffer object out of data stored in specified file. returns: handle to the buffer or null_handle if creation failed buffer_handle create( std::string const &Filename ); - // provides direct access to a specified buffer + // provides direct access to a specified buffer. a buffer still decoded in the background is completed first audio::openal_buffer const & buffer( audio::buffer_handle const Buffer ) const; + // creates AL buffers for files decoded in the background so far, or for all of them if requested + void + update( bool const Wait = false ); private: // types using buffer_sequence = std::vector; using index_map = std::unordered_map; + struct decoder; // background decoding of sound files, used with sound.asyncload // methods // places in the bank a buffer containing data stored in specified file. returns: handle to the buffer buffer_handle @@ -72,9 +76,13 @@ private: // checks whether specified file exists. returns: name of the located file, or empty string. std::string find_file( std::string const &Filename ) const; + // creates AL buffer for a file decoded in the background, waiting for the decoder if needed + void + complete( audio::buffer_handle const Buffer ) const; // members - buffer_sequence m_buffers; + mutable buffer_sequence m_buffers; // buffers decoded in the background are completed on first access index_map m_buffermappings; + mutable std::unique_ptr m_decoder; }; } // audio diff --git a/audio/audiorenderer.cpp b/audio/audiorenderer.cpp index 81f81570..d0e1ce55 100644 --- a/audio/audiorenderer.cpp +++ b/audio/audiorenderer.cpp @@ -305,6 +305,13 @@ openal_renderer::buffer( audio::buffer_handle const Buffer ) const { return m_buffers.buffer( Buffer ); } +// makes buffers of all sound files still decoded in the background, waiting for them +void +openal_renderer::complete_buffers() { + + m_buffers.update( true ); +} + // initializes the service bool openal_renderer::init() { @@ -350,6 +357,9 @@ openal_renderer::erase( sound_source const *Controller ) { void openal_renderer::update( double const Deltatime ) { + // buffers of sound files decoded in the background since the last update + m_buffers.update(); + ALenum err = alGetError(); if (err != AL_NO_ERROR) { diff --git a/audio/audiorenderer.h b/audio/audiorenderer.h index ec26ec82..3f462eb4 100644 --- a/audio/audiorenderer.h +++ b/audio/audiorenderer.h @@ -109,6 +109,8 @@ public: audio::buffer_handle fetch_buffer( std::string const &Filename ); audio::openal_buffer const &buffer( audio::buffer_handle const Buffer ) const; + // makes buffers of all sound files still decoded in the background (sound.asyncload), waiting for them + void complete_buffers(); bool init(); diff --git a/model/ResourceManager.h b/model/ResourceManager.h index c9f3cb0b..c9a1036d 100644 --- a/model/ResourceManager.h +++ b/model/ResourceManager.h @@ -67,6 +67,10 @@ public: std::chrono::steady_clock::time_point timestamp() const { return m_resourcetimestamp; } + // updates the time given to resources marked as used, without a sweep + void + refresh() { + m_resourcetimestamp = std::chrono::steady_clock::now(); } private: // members: diff --git a/model/Texture.cpp b/model/Texture.cpp index 5c1d852f..a3508168 100644 --- a/model/Texture.cpp +++ b/model/Texture.cpp @@ -30,6 +30,134 @@ http://mozilla.org/MPL/2.0/. #define EU07_DEFERRED_TEXTURE_UPLOAD +// texture data read by a worker thread. the texture itself is only touched by the main thread, +// which takes the data over when the texture is created on the gl side +struct texture_request { + std::string file; + std::size_t offset { 0 }; // start of the texture data in the file + std::size_t size { 0 }; // size of the texture data + // dds data with top-left origin is flipped by the worker + bool flip { false }; + std::uint32_t fourcc { 0 }; + int width { 0 }; + int height { 0 }; + int mapcount { 0 }; + int blocksize { 0 }; + // results, written by the thread reading the data + std::vector data; + bool failed { false }; + bool done { false }; // guarded by the mutex of the loader +}; + +namespace { + +// worker threads reading texture data from disk +class texture_loader { + +public: + static texture_loader &instance() { + static texture_loader loader; + return loader; } + + void submit( std::shared_ptr Request ) { + { + std::lock_guard lock( m_mutex ); + if( m_workers.empty() ) { + // reading the data is mostly waiting for the disk, a few threads are enough + auto const count { std::clamp( std::thread::hardware_concurrency(), 2, 5 ) - 1 }; + for( unsigned int idx = 0; idx < count; ++idx ) { + m_workers.emplace_back( [ this ]() { work(); } ); + } + } + m_queue.emplace_back( std::move( Request ) ); + } + m_queued.notify_one(); } + + // waits until data of the specified request is read. a request no worker has taken yet is carried out right away + void wait( std::shared_ptr const &Request ) { + std::unique_lock lock( m_mutex ); + if( Request->done ) { + return; + } + auto const lookup { std::find( std::begin( m_queue ), std::end( m_queue ), Request ) }; + if( lookup != std::end( m_queue ) ) { + m_queue.erase( lookup ); + lock.unlock(); + read( *Request ); + lock.lock(); + Request->done = true; + return; + } + m_read.wait( lock, [ &Request ]() { return Request->done; } ); } + + ~texture_loader() { + { + std::lock_guard lock( m_mutex ); + m_stop = true; + } + m_queued.notify_all(); + for( auto &worker : m_workers ) { + worker.join(); + } } + +private: + void work() { + std::unique_lock lock( m_mutex ); + while( true ) { + m_queued.wait( lock, [ this ]() { return m_stop || ( false == m_queue.empty() ); } ); + if( m_stop ) { + return; + } + auto request { m_queue.front() }; + m_queue.pop_front(); + lock.unlock(); + read( *request ); + lock.lock(); + request->done = true; + m_read.notify_all(); + } } + + // same as the reading part of opengl_texture::load_DDS() + static void read( texture_request &Request ) { + std::ifstream file( Request.file, std::ios::binary ); + if( false == file.is_open() ) { + Request.failed = true; + return; + } + file.seekg( Request.offset ); + Request.data.resize( Request.size ); + file.read( reinterpret_cast( Request.data.data() ), Request.size ); + + if( Request.flip ) { + char *mipmap = (char *)&Request.data[ 0 ]; + int mapcount = Request.mapcount, + width = Request.width, + height = Request.height; + while( mapcount ) { + if( Request.fourcc == FOURCC_DXT1 ) + flip_s3tc::flip_dxt1_image( mipmap, width, height ); + else if( Request.fourcc == FOURCC_DXT3 ) + flip_s3tc::flip_dxt23_image( mipmap, width, height ); + else if( Request.fourcc == FOURCC_DXT5 ) + flip_s3tc::flip_dxt45_image( mipmap, width, height ); + + mipmap += ( width + 3 ) / 4 * ( ( height + 3 ) / 4 ) * Request.blocksize; + width = std::max( width / 2, 4 ); + height = std::max( height / 2, 4 ); + --mapcount; + } + } } + + std::mutex m_mutex; + std::condition_variable m_queued; // wakes workers when requests are added + std::condition_variable m_read; // wakes the main thread waiting for a request + std::deque> m_queue; + std::vector m_workers; + bool m_stop { false }; +}; + +} // namespace + std::array opengl_texture::units = { 0 }; GLint opengl_texture::m_activeunit = -1; @@ -261,7 +389,9 @@ opengl_texture::load() { } // data state will be set by called loader, so we're all done here - if( data_state == resource_state::good ) { + // (a streamed texture has its description ready while the data is still being read) + if( ( data_state == resource_state::good ) + || ( ( data_state == resource_state::loading ) && ( request != nullptr ) ) ) { // verify texture size if( clamp_power_of_two(data_width) != data_width || clamp_power_of_two(data_height) != data_height ) { @@ -579,6 +709,28 @@ opengl_texture::load_DDS() { data_state = resource_state::failed; return; } + if( true == Global.gfx_texture_streaming ) { + // the data is read by a worker thread, everything else about the texture is known already + request = std::make_shared(); + request->file = name + type; + request->offset = static_cast( file.tellg() ) + offset; + request->size = datasize; + request->flip = Global.dds_upper_origin; + request->fourcc = ddsd.ddpfPixelFormat.dwFourCC; + request->width = data_width; + request->height = data_height; + request->mapcount = data_mapcount; + request->blocksize = blockSize; + texture_loader::instance().submit( request ); + + data_components = + ddsd.ddpfPixelFormat.dwFourCC == FOURCC_DXT1 ? GL_RGB : GL_RGBA; + + data_state = resource_state::loading; + + return; + } + // reserve space and load texture data data.resize( datasize ); @@ -930,6 +1082,17 @@ opengl_texture::unbind(size_t unit) bool opengl_texture::create( bool const Static ) { + if( ( data_state == resource_state::none ) + && ( true == reload_on_use ) ) { + // the data was dropped along with the gl texture when the texture was far away + reload_on_use = false; + load(); + } + if( ( data_state == resource_state::loading ) + && ( request != nullptr ) ) { + complete_data(); + } + if( data_state != resource_state::good && !is_rendertarget ) { // don't bother until we have useful texture data // and it isn't rendertarget texture without loaded data @@ -1087,6 +1250,27 @@ opengl_texture::create( bool const Static ) { return true; } +// takes over the data read by a worker thread, waiting for it if needed +void +opengl_texture::complete_data() { + + if( request == nullptr ) { return; } + + texture_loader::instance().wait( request ); + auto const completed { std::move( request ) }; + + if( true == completed->failed ) { + data_state = resource_state::failed; + ErrorLog( "Bad texture: failed to load texture \"" + name + "\"" ); + // NOTE: temporary workaround for texture assignment errors, same as in load() + id = 0; + return; + } + data.swap( completed->data ); + data_state = resource_state::good; + size = data.size() / 1024; +} + // releases resources allocated on the opengl end, storing local copy if requested void opengl_texture::release() { @@ -1397,6 +1581,84 @@ texture_manager::delete_textures() { } } +// starts a scan for gl textures used only by vehicles far away +void +texture_manager::begin_release_scan() { + + m_releasescantime = std::chrono::steady_clock::now(); + // textures bound from now on are stamped with the time of this scan, the sweep may not be running + m_garbagecollector.refresh(); + if( m_neededtimes.size() < m_textures.size() ) { + m_neededtimes.resize( m_textures.size() ); + } +} + +// marks texture of a vehicle as needed if the vehicle is near, otherwise only as one used by vehicles +void +texture_manager::mark_as_needed( texture_handle const Texture, bool const Near ) { + + if( ( Texture <= 0 ) + || ( Texture >= static_cast( m_neededtimes.size() ) ) ) { + return; + } + auto &needed { m_neededtimes[ Texture ] }; + if( true == Near ) { + needed = m_releasescantime; + auto &texture { *m_textures[ Texture ].first }; + if( ( true == texture.reload_on_use ) + && ( texture.data_state == resource_state::none ) ) { + // 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(); + } + } + else if( needed == resource_timestamp() ) { + // from now on the texture can be released, if no vehicle near enough needs it for a while + needed = m_releasescantime; + } +} + +// releases gl textures of vehicles not needed nor drawn for a while +void +texture_manager::release_unneeded() { + + // the delay keeps textures of a vehicle moving back and forth around the release distance from going in and out + auto const delay { std::chrono::seconds{ 10 } }; + + auto releasecount { 0 }; + for( std::size_t index = 1; index < m_neededtimes.size(); ++index ) { + auto const needed { m_neededtimes[ index ] }; + if( ( needed == resource_timestamp() ) + || ( m_releasescantime - needed < delay ) ) { + continue; + } + auto &record { m_textures[ index ] }; + auto &texture { *record.first }; + if( ( texture.id == static_cast( -1 ) ) + || ( texture.id == 0 ) + || ( true == texture.is_static ) + || ( true == texture.is_rendertarget ) + || ( texture.type == "make:" ) + || ( texture.type == "internalsrc:" ) ) { + continue; + } + if( m_releasescantime - record.second < delay ) { + // drawn lately, the texture is also used by something else than far vehicles + continue; + } + texture.release(); + // without the resource sweep the data isn't kept after upload, it'll have to be read again + texture.reload_on_use = ( texture.data_state == resource_state::none ); + ++releasecount; + } + if( releasecount > 0 ) { + // released gl names can be given to new textures, the unit cache can't trust them + for( auto &unit : opengl_texture::units ) { + unit = -1; + } + } +} + // performs a resource sweep void texture_manager::update() { diff --git a/model/Texture.h b/model/Texture.h index 1f582640..3df04fef 100644 --- a/model/Texture.h +++ b/model/Texture.h @@ -16,6 +16,8 @@ http://mozilla.org/MPL/2.0/. #include "gl/ubo.h" #include "interfaces/ITexture.h" +struct texture_request; // texture data read by a worker thread, with gfx.textures.streaming + struct opengl_texture : public ITexture { static DDSURFACEDESC2 deserialize_ddsd(std::istream&); static DDCOLORKEY deserialize_ddck(std::istream&); @@ -111,7 +113,12 @@ public: void flip_vertical(); void gles_match_internalformat(GLuint format); + // takes over the data read by a worker thread, waiting for it if needed + void complete_data(); + // members + std::shared_ptr request; // data being read by a worker thread + bool reload_on_use = false; // the data was dropped along with the gl texture of a far texture, it's read again when the texture is needed bool is_static = false; // is excluded from garbage collection bool is_rendertarget = false; // is used as postfx rendertarget, without loaded data int samples = 1; @@ -159,6 +166,16 @@ public: // performs a resource sweep void update(); + // release of gl textures used only far away (gfx.textures.releasedistance). a scan is made of + // begin_release_scan(), mark_as_needed() for textures of each vehicle and release_unneeded() + void + begin_release_scan(); + // marks texture of a vehicle as needed if the vehicle is near, otherwise only as one used by vehicles + void + mark_as_needed( texture_handle const Texture, bool const Near ); + // releases gl textures of vehicles not needed nor drawn for a while + void + release_unneeded(); // debug performance string std::string info() const; @@ -191,6 +208,8 @@ private: texture_handle const npos { 0 }; // should be -1, but the rest of the code uses -1 for something else texturetimepointpair_sequence m_textures; index_map m_texturemappings; + std::vector m_neededtimes; // last time a vehicle near enough needed the texture, blank for textures not used by vehicles + resource_timestamp m_releasescantime; // time of the current release scan garbage_collector m_garbagecollector { m_textures, 600, 60, "texture" }; }; diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 752ba842..392404a1 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -3351,6 +3351,119 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector(Global.gfx_texture_releasedistance) * m_colorpass.draw_range}; + auto const camera{m_colorpass.pass_camera.position()}; + + m_textures.begin_release_scan(); + for (auto *vehicle : simulation::Vehicles.sequence()) + { + if (vehicle == nullptr) + { + continue; + } + bool const isnear{glm::length(vehicle->vPosition - camera) - vehicle->radius() <= range}; + auto const mark_model = [&](TModel3d *Model) { + if (Model == nullptr) + { + return; + } + for (auto const texture : Model_Textures(Model)) + { + m_textures.mark_as_needed(texture, isnear); + } + }; + mark_model(vehicle->mdModel); + mark_model(vehicle->mdLowPolyInt); + mark_model(vehicle->mdLoad); + for (auto *attachment : vehicle->mdAttachments) + { + mark_model(attachment); + } + for (auto const skin : vehicle->m_materialdata.replacable_skins) + { + if (skin <= 0) + { + continue; + } + auto const &material{m_materials.material(skin)}; + for (auto const texture : material.textures) + { + m_textures.mark_as_needed(texture, isnear); + } + for (auto const &variant : material.texture_variants) + { + for (auto const texture : variant) + { + m_textures.mark_as_needed(texture, isnear); + } + } + } + } + m_textures.release_unneeded(); +} + +// textures used by materials of the sub-models of specified model, replaceable skins excluded +std::vector const &opengl33_renderer::Model_Textures(TModel3d *Model) +{ + auto const lookup{m_modeltextures.find(Model)}; + if (lookup != m_modeltextures.end()) + { + return lookup->second; + } + auto &textures{m_modeltextures[Model]}; + std::vector submodels; + if (Model->GetSMRoot() != nullptr) + { + submodels.emplace_back(Model->GetSMRoot()); + } + while (false == submodels.empty()) + { + auto *submodel{submodels.back()}; + submodels.pop_back(); + if (submodel->NextGet() != nullptr) + { + submodels.emplace_back(submodel->NextGet()); + } + if (submodel->ChildGet() != nullptr) + { + submodels.emplace_back(submodel->ChildGet()); + } + // negative material is one of replaceable skins of the vehicle, these are handled with the vehicle + if (submodel->GetMaterial() <= 0) + { + continue; + } + auto const &material{m_materials.material(submodel->GetMaterial())}; + for (auto const texture : material.textures) + { + if (texture > 0) + { + textures.emplace_back(texture); + } + } + for (auto const &variant : material.texture_variants) + { + for (auto const texture : variant) + { + if (texture > 0) + { + textures.emplace_back(texture); + } + } + } + } + std::sort(std::begin(textures), std::end(textures)); + textures.erase(std::unique(std::begin(textures), std::end(textures)), std::end(textures)); + return textures; +} + void opengl33_renderer::Render_Sleepers( TTrack *Track ) { if( Track == nullptr ) { return; } @@ -5379,6 +5492,11 @@ void opengl33_renderer::Update(double const Deltatime) m_textures.update(); } + if ((Global.gfx_texture_releasedistance > 0.f) && (true == simulation::is_ready)) + { + Update_Texture_Release(); + } + 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/opengl33renderer.h b/rendering/opengl33renderer.h index b028f929..1820abe4 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -304,6 +304,10 @@ class opengl33_renderer : public gfx_renderer { bool Render_interior( bool const Alpha = false ); bool Render_lowpoly( TDynamicObject *Dynamic, float const Squaredistance, bool const Setup, bool const Alpha = false ); bool Render_coupler_adapter( TDynamicObject *Dynamic, float const Squaredistance, int const End, bool const Alpha = false ); + // releases gl textures used only by vehicles farther than gfx.textures.releasedistance times the draw range + 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 ); void Render(TMemCell *Memcell); void Render_particles(); void Render_precipitation(); @@ -388,6 +392,8 @@ class opengl33_renderer : public gfx_renderer { // of once per cell -- collapsing many tiny instanced draws into a few large // batches. Reused every pass; the vectors are emptied (not freed) at the top of Render(scene::basic_region*). scene::basic_cell::instance_bucket_map m_frame_instance_buckets; + // textures used by vehicle models, for the release of far textures + std::unordered_map> m_modeltextures; // 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; // copies of instance bounds kept by the buckets of scene cells are good as long as they carry this number. diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 7ce2446a..2cf5235c 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -187,6 +187,12 @@ bool global_settings::ConfigParseAudio(cParser& Parser, const std::string& token return true; } + if (token == "sound.asyncload") + { + ParseOne(Parser, AudioAsyncLoad); + return true; + } + if (token == "sound.volume.vehicle") { ParseOneClamped(Parser, VehicleVolume, 0.f, 1.f); @@ -1347,6 +1353,18 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token Parser.getTokens(); Parser >> ResourceMove; } + else if (Token == "gfx.textures.streaming") + { + Parser.getTokens(); + Parser >> gfx_texture_streaming; + } + else if (Token == "gfx.textures.releasedistance") + { + // multiplier of the draw range, 0 turns the release off. textures within the draw range are never released + Parser.getTokens(); + Parser >> gfx_texture_releasedistance; + gfx_texture_releasedistance = (gfx_texture_releasedistance > 0.f ? std::max(gfx_texture_releasedistance, 1.f) : 0.f); + } else if (Token == "gfx.reflections.framerate") { auto const updatespersecond{std::abs(Parser.getToken())}; @@ -1530,6 +1548,7 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "sound.volume.vehicle", VehicleVolume ); export_as_text( Output, "sound.volume.positional", EnvironmentPositionalVolume ); export_as_text( Output, "sound.volume.ambient", EnvironmentAmbientVolume ); + export_as_text( Output, "sound.asyncload", AudioAsyncLoad ); export_as_text( Output, "physicslog", WriteLogFlag ); export_as_text( Output, "fullphysics", FullPhysics ); export_as_text( Output, "debuglog", iWriteLogEnabled ); @@ -1584,6 +1603,8 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "createswitchtrackbeds", CreateSwitchTrackbeds ); export_as_text( Output, "gfx.resource.sweep", ResourceSweep ); export_as_text( Output, "gfx.resource.move", ResourceMove ); + export_as_text( Output, "gfx.textures.streaming", gfx_texture_streaming ); + export_as_text( Output, "gfx.textures.releasedistance", gfx_texture_releasedistance ); export_as_text( Output, "gfx.reflections.framerate", 1.0 / reflectiontune.update_interval ); export_as_text( Output, "gfx.reflections.fidelity", reflectiontune.fidelity ); export_as_text( Output, "timespeed", fTimeSpeed ); diff --git a/utilities/Globals.h b/utilities/Globals.h index 36574ab1..f65fbc2c 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -182,6 +182,8 @@ struct global_settings { float SmokeFidelity{ 1.f }; // determines amount of generated smoke particles bool ResourceSweep{ true }; // gfx resource garbage collection bool ResourceMove{ false }; // gfx resources are moved between cpu and gpu side instead of sending a copy + bool gfx_texture_streaming{ false }; // data of dds textures is read by worker threads, the rest of the loading doesn't wait for it + float gfx_texture_releasedistance{ 0.f }; // gl textures of vehicles farther than this many draw ranges are released. 0 = off bool compress_tex{ true }; // all textures are compressed on gpu side std::string asSky{ "1" }; float fFpsAverage{ 0.f }; // oczekiwana wartosć FPS @@ -197,6 +199,7 @@ struct global_settings { float EnvironmentAmbientVolume{ 1.0f }; int audio_max_sources = 30; std::string AudioRenderer; + bool AudioAsyncLoad{ false }; // sound files are decoded by worker threads // input float fMouseXScale{ 1.5f }; float fMouseYScale{ 0.2f };