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:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user