17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 03:59:19 +02:00

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

This commit is contained in:
2026-10-08 16:17:46 +02:00
parent 516aa19cb5
commit 28b4e57133
12 changed files with 683 additions and 23 deletions

View File

@@ -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<unsigned char> 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<texture_request> Request ) {
{
std::lock_guard<std::mutex> 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<unsigned int>( 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<texture_request> const &Request ) {
std::unique_lock<std::mutex> 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<std::mutex> lock( m_mutex );
m_stop = true;
}
m_queued.notify_all();
for( auto &worker : m_workers ) {
worker.join();
} }
private:
void work() {
std::unique_lock<std::mutex> 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<char *>( 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<std::shared_ptr<texture_request>> m_queue;
std::vector<std::thread> m_workers;
bool m_stop { false };
};
} // namespace
std::array<GLuint, gl::MAX_TEXTURES + gl::HELPER_TEXTURES> 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<texture_request>();
request->file = name + type;
request->offset = static_cast<std::size_t>( 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<texture_handle>( 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<GLuint>( -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() {