17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-08 08:27:17 +01:00
Files
maszyna/model/Texture.cpp
maj00r e230d7b902 fix: a deleted model is no longer the picked node, an orthophoto tile isn't swept as a grey stub, the ED brake arrays freed
Deleting a model in the editor left the renderer holding it as the picked node, the next pick used freed memory: the renderer forgets it now (Forget_Node). A texture filled straight from memory (internalsrc:) can't be read again, released by the garbage sweep it came back grey; it stays now, and the units bound with the old name are rebound. The arrays of the ED brake force sharing are vectors, they leaked once per update. The scene hierarchy and the editor menu items go through the translation. Python caches ignored.
2026-11-03 21:45:58 +01:00

1969 lines
68 KiB
C++

/*
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/.
*/
/*
MaSzyna EU07 locomotive simulator
Copyright (C) 2001-2004 Marcin Wozniak and others
*/
#include "stdafx.h"
#include "model/Texture.h"
#include "application/application.h"
#include "utilities/dictionary.h"
#include "utilities/Globals.h"
#include "utilities/Logs.h"
#include "utilities/utilities.h"
#include "scene/sn_utils.h"
#include "rendering/flip-s3tc.h"
#include "stb/stb_image.h"
//#include <png.h>
#include "dds-ktx/dds-ktx.h"
#include "winheaders.h"
#define EU07_DEFERRED_TEXTURE_UPLOAD
#ifndef FOURCC_DX10
#define FOURCC_DX10 (MAKEFOURCC('D','X','1','0'))
#endif
// BC7, core since GL 4.2 (ARB_texture_compression_bptc), the bundled glad is generated for 3.3
#ifndef GL_COMPRESSED_RGBA_BPTC_UNORM
#define GL_COMPRESSED_RGBA_BPTC_UNORM 0x8E8C
#endif
#ifndef GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM
#define GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM 0x8E8D
#endif
// 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 };
};
// dxgi formats and alpha modes of the dds dx10 extended header
constexpr std::uint32_t dxgi_format_bc7_typeless { 97 };
constexpr std::uint32_t dxgi_format_bc7_unorm { 98 };
constexpr std::uint32_t dxgi_format_bc7_unorm_srgb { 99 };
constexpr std::uint32_t dds_alpha_mode_unknown { 0 };
constexpr std::uint32_t dds_alpha_mode_opaque { 3 };
// returns Count bits starting at bit Offset of a 128-bit BC7 block
std::uint32_t bc7_bits( unsigned char const *Block, int const Offset, int const Count ) {
std::uint32_t result { 0 };
for( int bit = 0; bit < Count; ++bit ) {
auto const idx { Offset + bit };
result |= ( ( Block[ idx >> 3 ] >> ( idx & 7 ) ) & 1u ) << bit;
}
return result; }
// expands a BC7 endpoint of given precision to 8 bits
std::uint32_t bc7_expand( std::uint32_t const Value, int const Bits ) {
return ( Value << ( 8 - Bits ) ) | ( Value >> ( 2 * Bits - 8 ) ); }
// checks whether any texel of a BC7 block can come out noticeably translucent.
// it looks at the endpoints the alpha is interpolated between, so a translucent block is never reported as opaque
bool bc7_block_has_alpha( unsigned char const *Block ) {
// encoders sharing p-bits between colour and alpha (modes 6 and 7) can leave opaque alpha at 254 or 251
constexpr std::uint32_t opaque { 250 };
if( Block[ 0 ] == 0 ) {
// reserved mode, decodes to transparent black
return true;
}
int mode { 0 };
while( ( Block[ 0 ] & ( 1 << mode ) ) == 0 ) {
++mode;
}
switch( mode ) {
case 4:
case 5: {
// single subset with separate alpha endpoints. rotation swaps alpha with one of the colour channels
auto const rotation { static_cast<int>( bc7_bits( Block, mode + 1, 2 ) ) };
auto const colourbits { mode == 4 ? 5 : 7 };
auto const alphabits { mode == 4 ? 6 : 8 };
auto const bits { rotation == 0 ? alphabits : colourbits };
auto const start { rotation == 0 ? 8 + 6 * colourbits : 8 + 2 * colourbits * ( rotation - 1 ) };
for( int endpoint = 0; endpoint < 2; ++endpoint ) {
if( bc7_expand( bc7_bits( Block, start + endpoint * bits, bits ), bits ) < opaque ) {
return true;
}
}
return false;
}
case 6: {
// 7-bit alpha endpoints, each extended with its own p-bit
for( int endpoint = 0; endpoint < 2; ++endpoint ) {
auto const alpha { ( bc7_bits( Block, 49 + endpoint * 7, 7 ) << 1 ) | bc7_bits( Block, 63 + endpoint, 1 ) };
if( alpha < opaque ) {
return true;
}
}
return false;
}
case 7: {
// two subsets, 5-bit alpha endpoints extended with their p-bits to 6 bits
for( int endpoint = 0; endpoint < 4; ++endpoint ) {
auto const alpha { ( bc7_bits( Block, 74 + endpoint * 5, 5 ) << 1 ) | bc7_bits( Block, 94 + endpoint, 1 ) };
if( bc7_expand( alpha, 6 ) < opaque ) {
return true;
}
}
return false;
}
default: {
// modes 0-3 don't store alpha
return false;
}
} }
// picks data components of a BC7 texture. a BC7 file doesn't tell whether alpha is used the way DXT1/DXT5 do,
// so unless the dx10 header states the alpha mode, a small mipmap is checked for translucent texels.
// the mipmap is always read from the file, so the result doesn't depend on whether the texture data is streamed
GLint bc7_components( std::istream &File, std::streamoff const Start, std::size_t const Datasize, unsigned int Width, unsigned int Height, unsigned int const Mapcount, std::uint32_t const Alphamode ) {
if( Alphamode == dds_alpha_mode_opaque ) { return GL_RGB; }
if( Alphamode != dds_alpha_mode_unknown ) { return GL_RGBA; }
// up to 256x256, small enough to read quickly while translucent areas a few texels wide still show in it
auto const blocks = [ & ]() { return static_cast<std::size_t>( ( Width + 3 ) / 4 ) * ( ( Height + 3 ) / 4 ); };
std::size_t offset { 0 };
for( unsigned int level = 1; ( level < Mapcount ) && ( std::max( Width, Height ) > 256 ); ++level ) {
offset += blocks() * 16;
Width = std::max( Width / 2, 1u );
Height = std::max( Height / 2, 1u );
}
std::vector<unsigned char> probe( blocks() * 16 );
if( offset + probe.size() > Datasize ) {
// malformed file, err on the side of translucency like DXT5 does
return GL_RGBA;
}
auto const position { File.tellg() };
File.seekg( Start + static_cast<std::streamoff>( offset ) );
File.read( reinterpret_cast<char *>( probe.data() ), probe.size() );
auto const readok { File.good() };
File.clear();
File.seekg( position );
if( false == readok ) {
return GL_RGBA;
}
for( std::size_t block = 0; block < probe.size(); block += 16 ) {
if( bc7_block_has_alpha( &probe[ block ] ) ) {
return GL_RGBA;
}
}
return GL_RGB; }
// BC7 is core since GL 4.2, older contexts and GLES need the extension. requires current gl context
bool bptc_supported() {
static bool const supported = []() {
if( ( false == Global.gfx_usegles )
&& ( ( GLVersion.major > 4 ) || ( ( GLVersion.major == 4 ) && ( GLVersion.minor >= 2 ) ) ) ) {
return true;
}
GLint count { 0 };
::glGetIntegerv( GL_NUM_EXTENSIONS, &count );
for( GLint idx = 0; idx < count; ++idx ) {
auto const *extension { reinterpret_cast<char const *>( ::glGetStringi( GL_EXTENSIONS, idx ) ) };
if( extension == nullptr ) {
continue;
}
std::string_view const name { extension };
if( ( name == "GL_ARB_texture_compression_bptc" )
|| ( name == "GL_EXT_texture_compression_bptc" ) ) {
return true;
}
}
return false;
}();
return supported; }
} // namespace
std::array<GLuint, gl::MAX_TEXTURES + gl::HELPER_TEXTURES> opengl_texture::units = { 0 };
GLint opengl_texture::m_activeunit = -1;
std::unordered_map<GLint, int> opengl_texture::precompressed_formats =
{
{ GL_COMPRESSED_RGBA8_ETC2_EAC, 16 },
{ GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC, 16 },
{ GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT, 8 },
{ GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT, 16 },
{ GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT, 16 },
{ GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, 8 },
{ GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, 16 },
{ GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, 16 },
{ GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM, 16 },
{ GL_COMPRESSED_RGBA_BPTC_UNORM, 16 },
};
std::unordered_map<GLint, GLint> opengl_texture::drivercompressed_formats =
{
{ GL_SRGB8_ALPHA8, GL_COMPRESSED_SRGB_ALPHA },
{ GL_SRGB8, GL_COMPRESSED_SRGB },
{ GL_RGBA8, GL_COMPRESSED_RGBA },
{ GL_RGB8, GL_COMPRESSED_RGB },
{ GL_RG8, GL_COMPRESSED_RG },
{ GL_R8, GL_COMPRESSED_RED },
};
std::unordered_map<GLint, std::unordered_map<GLint, GLint>> opengl_texture::mapping =
{
// image have, material wants, gl internalformat
{ GL_COMPRESSED_RGBA8_ETC2_EAC , { { GL_SRGB_ALPHA, GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC },
{ GL_SRGB, GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC },
{ GL_RGBA, GL_COMPRESSED_RGBA8_ETC2_EAC },
{ GL_RGB, GL_COMPRESSED_RGBA8_ETC2_EAC },
{ GL_RG, GL_COMPRESSED_RGBA8_ETC2_EAC },
{ GL_RED, GL_COMPRESSED_RGBA8_ETC2_EAC } } },
{ GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, { { GL_SRGB_ALPHA, GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT },
{ GL_SRGB, GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT },
{ GL_RGBA, GL_COMPRESSED_RGBA_S3TC_DXT1_EXT },
{ GL_RGB, GL_COMPRESSED_RGBA_S3TC_DXT1_EXT },
{ GL_RG, GL_COMPRESSED_RGBA_S3TC_DXT1_EXT },
{ GL_RED, GL_COMPRESSED_RGBA_S3TC_DXT1_EXT } } },
{ GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, { { GL_SRGB_ALPHA, GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT },
{ GL_SRGB, GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT },
{ GL_RGBA, GL_COMPRESSED_RGBA_S3TC_DXT3_EXT },
{ GL_RGB, GL_COMPRESSED_RGBA_S3TC_DXT3_EXT },
{ GL_RG, GL_COMPRESSED_RGBA_S3TC_DXT3_EXT },
{ GL_RED, GL_COMPRESSED_RGBA_S3TC_DXT3_EXT } } },
{ GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, { { GL_SRGB_ALPHA, GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT },
{ GL_SRGB, GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT },
{ GL_RGBA, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT },
{ GL_RGB, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT },
{ GL_RG, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT },
{ GL_RED, GL_COMPRESSED_RGBA_S3TC_DXT5_EXT } } },
{ GL_COMPRESSED_RGBA_BPTC_UNORM, { { GL_SRGB_ALPHA, GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM },
{ GL_SRGB, GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM },
{ GL_RGBA, GL_COMPRESSED_RGBA_BPTC_UNORM },
{ GL_RGB, GL_COMPRESSED_RGBA_BPTC_UNORM },
{ GL_RG, GL_COMPRESSED_RGBA_BPTC_UNORM },
{ GL_RED, GL_COMPRESSED_RGBA_BPTC_UNORM } } },
{ GL_RGBA, { { GL_SRGB_ALPHA, GL_SRGB8_ALPHA8 },
{ GL_SRGB, GL_SRGB8 },
{ GL_RGBA, GL_RGBA8 },
{ GL_RGB, GL_RGB8 },
{ GL_RG, GL_RG8 },
{ GL_RED, GL_R8 } } },
{ GL_RGB, { { GL_SRGB_ALPHA, GL_SRGB8 }, // bad
{ GL_SRGB, GL_SRGB8 },
{ GL_RGBA, GL_RGB8 }, // bad
{ GL_RGB, GL_RGB8 },
{ GL_RG, GL_RG8 },
{ GL_RED, GL_R8 } } },
{ GL_RG, { { GL_SRGB_ALPHA, GL_SRGB8 }, // bad
{ GL_SRGB, GL_SRGB8 }, // bad
{ GL_RGBA, GL_RG8 }, // bad
{ GL_RGB, GL_RG8 }, // bad
{ GL_RG, GL_RG8 },
{ GL_RED, GL_R8 } } },
{ GL_RED, { { GL_SRGB_ALPHA, GL_SRGB8 }, // bad
{ GL_SRGB, GL_SRGB8 }, // bad
{ GL_RGBA, GL_R8 }, // bad
{ GL_RGB, GL_R8 }, // bad
{ GL_RG, GL_R8 }, // bad
{ GL_RED, GL_R8 } } },
};
texture_manager::texture_manager() {
// since index 0 is used to indicate no texture, we put a blank entry in the first texture slot
m_textures.emplace_back( new opengl_texture(), std::chrono::steady_clock::time_point() );
}
// convert image to format suitable for given internalformat
// required for GLES, on desktop GL it will be done by driver
void opengl_texture::gles_match_internalformat(GLuint internalformat)
{
// ignore compressed formats (and hope that GLES driver will support it)
if (precompressed_formats.find(internalformat) != precompressed_formats.end())
return;
// don't care about sRGB here
if (internalformat == GL_SRGB8)
internalformat = GL_RGB8;
if (internalformat == GL_SRGB8_ALPHA8)
internalformat = GL_RGBA8;
// we don't want BGR(A), reverse it
if (data_format == GL_BGR)
{
std::vector<unsigned char> reverse;
reverse.resize(data.size());
for (int y = 0; y < data_height; y++)
for (int x = 0; x < data_width; x++)
{
int offset = (y * data_width + x) * 3;
reverse[offset + 0] = data[offset + 2];
reverse[offset + 1] = data[offset + 1];
reverse[offset + 2] = data[offset + 0];
}
data_format = GL_RGB;
data = reverse;
}
else if (data_format == GL_BGRA)
{
std::vector<unsigned char> reverse;
reverse.resize(data.size());
for (int y = 0; y < data_height; y++)
for (int x = 0; x < data_width; x++)
{
int offset = (y * data_width + x) * 4;
reverse[offset + 0] = data[offset + 2];
reverse[offset + 1] = data[offset + 1];
reverse[offset + 2] = data[offset + 0];
reverse[offset + 3] = data[offset + 3];
}
data_format = GL_RGBA;
data = reverse;
}
// if format matches, we're done
if (data_format == GL_RGBA && internalformat == GL_RGBA8)
return;
if (data_format == GL_RGB && internalformat == GL_RGB8)
return;
if (data_format == GL_RG && internalformat == GL_RG8)
return;
if (data_format == GL_RED && internalformat == GL_R8)
return;
// do conversion
int in_c = 0;
if (data_format == GL_RGBA)
in_c = 4;
else if (data_format == GL_RGB)
in_c = 3;
else if (data_format == GL_RG)
in_c = 2;
else if (data_format == GL_RED)
in_c = 1;
int out_c = 0;
if (internalformat == GL_RGBA8)
out_c = 4;
else if (internalformat == GL_RGB8)
out_c = 3;
else if (internalformat == GL_RG8)
out_c = 2;
else if (internalformat == GL_R8)
out_c = 1;
if (!in_c || !out_c)
return; // conversion not supported
std::vector<unsigned char> out;
out.resize(data_width * data_height * out_c);
for (int y = 0; y < data_height; y++)
for (int x = 0; x < data_width; x++)
{
int pixel = y * data_width + x;
int in_off = pixel * in_c;
int out_off = pixel * out_c;
for (int i = 0; i < out_c; i++)
{
if (i < in_c)
out[out_off + i] = data[in_off + i];
else
out[out_off + i] = 0xFF;
}
}
if (out_c == 4)
data_format = GL_RGBA;
else if (out_c == 3)
data_format = GL_RGB;
else if (out_c == 2)
data_format = GL_RG;
else if (out_c == 1)
data_format = GL_RED;
data = out;
}
// loads texture data from specified file
// TODO: wrap it in a workitem class, for the job system deferred loading
void
opengl_texture::load() {
if (data_state == resource_state::good)
return;
if( type == "make:" || type == "internalsrc:" ) {
// for generated texture we delay data creation until texture is bound
// this ensures the script will receive all simulation data it might potentially want
// as any binding will happen after simulation is loaded, initialized and running
// until then we supply data for a tiny 2x2 grey stub
make_stub();
}
else {
WriteLog( "Loading texture data from \"" + name + "\"", logtype::texture );
data_state = resource_state::loading;
if( type == ".dds" ) { load_DDS(); }
else if( type == ".tga" ) { load_TGA(); }
else if( type == ".png" ) { load_STBI(); }
else if( type == ".ktx" ) { load_KTX(); }
else if( type == ".bmp" ) { load_STBI(); }
else if( type == ".jpg" ) { load_STBI(); }
else if( type == ".tex" ) { load_TEX(); }
else { goto fail; }
}
// data state will be set by called loader, so we're all done here
// (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 ) {
if( name != "logo" ) {
WriteLog( "Warning: dimensions of texture \"" + name + "\" aren't powers of 2", logtype::texture );
}
}
if( quantize(data_width, 4u) != data_width || quantize(data_height, 4u) != data_height ) {
WriteLog( "Warning: dimensions of texture \"" + name + "\" aren't multiples of 4", logtype::texture );
}
has_alpha = data_components == GL_RGBA ? true : false;
size = data.size() / 1024;
return;
}
fail:
data_state = resource_state::failed;
ErrorLog( "Bad texture: failed to load texture \"" + name + "\"" );
// NOTE: temporary workaround for texture assignment errors
id = 0;
return;
}
void opengl_texture::load_PNG()
{
//png_image png;
//memset(&png, 0, sizeof(png_image));
//png.version = PNG_IMAGE_VERSION;
//
//png_image_begin_read_from_file(&png, (name + type).c_str());
//if (png.warning_or_error)
//{
// data_state = resource_state::failed;
// ErrorLog(name + " error: " + std::string(png.message));
// return;
//}
//
//if (png.format & PNG_FORMAT_FLAG_ALPHA)
//{
// data_format = GL_RGBA;
// data_components = GL_RGBA;
// png.format = PNG_FORMAT_RGBA;
//}
//else
//{
// data_format = GL_RGB;
// data_components = GL_RGB;
// png.format = PNG_FORMAT_RGB;
//}
//data_width = png.width;
//data_height = png.height;
//
//data.resize(PNG_IMAGE_SIZE(png));
//
//png_image_finish_read(&png, nullptr,
// (void*)&data[0], -data_width * PNG_IMAGE_PIXEL_SIZE(png.format), nullptr);
//// we're storing texture data internally with bottom-left origin
//// so use negative stride
//
//if (png.warning_or_error)
//{
// data_state = resource_state::failed;
// ErrorLog(name + " error: " + std::string(png.message));
// return;
//}
//
//data_mapcount = 1;
//data_state = resource_state::good;
}
void opengl_texture::load_STBI()
{
int x, y, n;
stbi_set_flip_vertically_on_load(1);
uint8_t *image = stbi_load((name + type).c_str(), &x, &y, &n, 4);
if (!image) {
data_state = resource_state::failed;
ErrorLog(std::string(stbi_failure_reason()));
return;
}
data.resize(x * y * 4);
memcpy(&data[0], image, data.size());
free(image);
data_format = GL_RGBA;
data_components = n == 4 ? GL_RGBA : GL_RGB;
data_width = x;
data_height = y;
data_mapcount = 1;
data_state = resource_state::good;
}
void
opengl_texture::make_stub()
{
data_width = 2;
data_height = 2;
data.resize( data_width * data_height * 3 );
std::fill( std::begin( data ), std::end( data ), static_cast<char>( 0xc0 ) );
data_mapcount = 1;
data_format = GL_RGB;
data_components = GL_RGB;
data_state = resource_state::good;
is_texstub = true;
}
void
opengl_texture::make_from_memory(size_t width, size_t height, const uint8_t *raw)
{
release();
data_width = width;
data_height = height;
data.resize(data_width * data_height * 4);
memcpy(data.data(), raw, data.size());
data_format = GL_RGBA;
data_components = GL_RGBA;
data_mapcount = 1;
data_state = resource_state::good;
is_texstub = false;
}
void opengl_texture::update_from_memory(size_t width, size_t height, const uint8_t *raw)
{
if (id != invalid_id && (width != data_width || height != data_height || GL_SRGB8_ALPHA8 != data_format || GL_RGBA != data_components))
{
glDeleteTextures(1, &id);
id = invalid_id;
}
if (id == invalid_id) // NOSONAR
{
data_width = width;
data_height = height;
data_format = GL_SRGB8_ALPHA8;
data_components = GL_RGBA;
glGenTextures(1, &id);
glBindTexture(target, id);
set_filtering();
glTexParameteri(target, GL_TEXTURE_WRAP_S, wrap_mode_s);
glTexParameteri(target, GL_TEXTURE_WRAP_T, wrap_mode_t);
if (!glGenerateMipmap)
glTexParameteri(target, GL_GENERATE_MIPMAP, GL_TRUE);
}
else
{
glBindTexture(target, id);
}
glTexImage2D(target, 0, data_format, data_width, data_height, 0, data_components, GL_UNSIGNED_BYTE, raw);
if (glGenerateMipmap)
glGenerateMipmap(target);
glFlush();
// the content given straight to the gpu can't be read again: released by the garbage sweep, the texture would come back as
// the grey stub
if (type == "internalsrc:")
{
is_static = true;
is_ready = true;
}
// the name may be a new one, and the unit active took the texture past the unit cache
for (auto &unit : units)
unit = -1;
}
void
opengl_texture::make_request() {
auto const components { Split( name, '?' ) };
auto dictionary = std::make_shared<dictionary_source>( components.back() );
auto rt = std::make_shared<python_rt>();
rt->shared_tex = this;
Application.request( { ToLower( components.front() ), dictionary, rt } );
}
DDCOLORKEY opengl_texture::deserialize_ddck(std::istream &s)
{
DDCOLORKEY ddck;
ddck.dwColorSpaceLowValue = sn_utils::ld_uint32(s);
ddck.dwColorSpaceHighValue = sn_utils::ld_uint32(s);
return ddck;
}
DDPIXELFORMAT opengl_texture::deserialize_ddpf(std::istream &s)
{
DDPIXELFORMAT ddpf;
ddpf.dwSize = sn_utils::ld_uint32(s);
ddpf.dwFlags = sn_utils::ld_uint32(s);
ddpf.dwFourCC = sn_utils::ld_uint32(s);
ddpf.dwRGBBitCount = sn_utils::ld_uint32(s);
ddpf.dwRBitMask = sn_utils::ld_uint32(s);
ddpf.dwGBitMask = sn_utils::ld_uint32(s);
ddpf.dwBBitMask = sn_utils::ld_uint32(s);
ddpf.dwRGBAlphaBitMask = sn_utils::ld_uint32(s);
return ddpf;
}
DDSCAPS2 opengl_texture::deserialize_ddscaps(std::istream &s)
{
DDSCAPS2 ddsc;
ddsc.dwCaps = sn_utils::ld_uint32(s);
ddsc.dwCaps2 = sn_utils::ld_uint32(s);
ddsc.dwCaps3 = sn_utils::ld_uint32(s);
ddsc.dwCaps4 = sn_utils::ld_uint32(s);
return ddsc;
}
DDSURFACEDESC2 opengl_texture::deserialize_ddsd(std::istream &s)
{
DDSURFACEDESC2 ddsd;
ddsd.dwSize = sn_utils::ld_uint32(s);
ddsd.dwFlags = sn_utils::ld_uint32(s);
ddsd.dwHeight = sn_utils::ld_uint32(s);
ddsd.dwWidth = sn_utils::ld_uint32(s);
ddsd.lPitch = sn_utils::ld_uint32(s);
ddsd.dwBackBufferCount = sn_utils::ld_uint32(s);
ddsd.dwMipMapCount = sn_utils::ld_uint32(s);
ddsd.dwAlphaBitDepth = sn_utils::ld_uint32(s);
ddsd.dwReserved = sn_utils::ld_uint32(s);
sn_utils::ld_uint32(s);
ddsd.lpSurface = nullptr;
ddsd.ddckCKDestOverlay = deserialize_ddck(s);
ddsd.ddckCKDestBlt = deserialize_ddck(s);
ddsd.ddckCKSrcOverlay = deserialize_ddck(s);
ddsd.ddckCKSrcBlt = deserialize_ddck(s);
ddsd.ddpfPixelFormat = deserialize_ddpf(s);
ddsd.ddsCaps = deserialize_ddscaps(s);
ddsd.dwTextureStage = sn_utils::ld_uint32(s);
return ddsd;
}
void
opengl_texture::load_DDS() {
std::ifstream file( name + type, std::ios::binary | std::ios::ate ); file.unsetf( std::ios::skipws );
std::size_t filesize = static_cast<size_t>(file.tellg()); // ios::ate already positioned us at the end of the file
file.seekg( 0, std::ios::beg ); // rewind the caret afterwards
char filecode[5];
file.read(filecode, 4);
filesize -= 4;
filecode[4] = 0;
if( filecode != std::string( "DDS " ) )
{
data_state = resource_state::failed;
return;
}
DDSURFACEDESC2 ddsd = deserialize_ddsd(file);
filesize -= 124;
// formats without a legacy fourcc code (BC7) are described by the dx10 extended header
std::uint32_t dxgiformat { 0 };
std::uint32_t alphamode { dds_alpha_mode_unknown };
if( ddsd.ddpfPixelFormat.dwFourCC == FOURCC_DX10 ) {
dxgiformat = sn_utils::ld_uint32( file );
sn_utils::ld_uint32( file ); // resource dimension
sn_utils::ld_uint32( file ); // misc flags
auto const arraysize { sn_utils::ld_uint32( file ) };
alphamode = sn_utils::ld_uint32( file ) & 0x7; // misc flags 2, the lowest bits hold the alpha mode
filesize -= 20;
if( arraysize > 1 ) {
WriteLog( "Bad texture: texture arrays aren't supported, in file \"" + name + "\"", logtype::texture );
data_state = resource_state::failed;
return;
}
}
//
// This .dds loader supports the loading of compressed formats DXT1, DXT3,
// DXT5 and BC7.
//
switch (ddsd.ddpfPixelFormat.dwFourCC)
{
case FOURCC_DXT1:
// DXT1's compression ratio is 8:1
data_format = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
break;
case FOURCC_DXT3:
// DXT3's compression ratio is 4:1
data_format = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
break;
case FOURCC_DXT5:
// DXT5's compression ratio is 4:1
data_format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
break;
case FOURCC_DX10:
if( ( dxgiformat != dxgi_format_bc7_typeless )
&& ( dxgiformat != dxgi_format_bc7_unorm )
&& ( dxgiformat != dxgi_format_bc7_unorm_srgb ) ) {
data_state = resource_state::failed;
return;
}
// BC7's compression ratio is 4:1. srgb is picked by the material, same as for DXTn
data_format = GL_COMPRESSED_RGBA_BPTC_UNORM;
break;
default:
data_state = resource_state::failed;
return;
}
data_width = ddsd.dwWidth;
data_height = ddsd.dwHeight;
data_mapcount = ddsd.dwMipMapCount;
int blockSize = data_format == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT ? 8 : 16;
int offset = 0;
while( data_width > Global.CurrentMaxTextureSize || data_height > Global.CurrentMaxTextureSize ) {
// pomijanie zbyt dużych mipmap, jeśli wymagane jest ograniczenie rozmiaru
offset += (data_width + 3) / 4 * ( ( data_height + 3 ) / 4 ) * blockSize;
data_width /= 2;
data_height /= 2;
--data_mapcount;
WriteLog( "Texture pixelcount exceeds specified limits, skipping mipmap level" );
};
if( data_mapcount <= 0 ) {
// there's a chance we've discarded the provided mipmap(s) as too large
WriteLog( "Texture \"" + name + "\" has no mipmaps which can fit currently set texture pixelcount limits." );
data_state = resource_state::failed;
return;
}
size_t datasize = filesize - offset;
if( datasize == 0 ) {
// catch malformed .dds files
WriteLog( "Bad texture: file \"" + name + "\" is malformed and holds no texture data.", logtype::texture );
data_state = resource_state::failed;
return;
}
// dds data with top-left origin is flipped block by block. BC7 blocks can't be flipped that way,
// BC7 textures have to be stored with bottom-left origin regardless of the setting
bool const flip { Global.dds_upper_origin && ( data_format != GL_COMPRESSED_RGBA_BPTC_UNORM ) };
if( Global.dds_upper_origin && ( false == flip ) ) {
WriteLog( "Warning: BC7 texture \"" + name + "\" can't be flipped for ddsupperorigin, it's loaded as stored", logtype::texture );
}
GLint const components { (
data_format == GL_COMPRESSED_RGBA_BPTC_UNORM ?
bc7_components( file, static_cast<std::streamoff>( file.tellg() ) + offset, datasize, data_width, data_height, data_mapcount, alphamode ) :
ddsd.ddpfPixelFormat.dwFourCC == FOURCC_DXT1 ?
GL_RGB :
GL_RGBA ) };
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 = flip;
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 = components;
data_state = resource_state::loading;
return;
}
// reserve space and load texture data
data.resize( datasize );
if( offset != 0 ) {
// skip data for mipmaps we don't need
file.seekg( offset, std::ios_base::cur );
filesize -= offset;
}
file.read((char *)&data[0], datasize);
filesize -= datasize;
// we're storing texture data internally with bottom-left origin,
// while DDS stores it with top-left origin. we need to flip it.
if (flip)
{
char *mipmap = (char*)&data[0];
int mapcount = data_mapcount,
width = data_width,
height = data_height;
while (mapcount)
{
if (ddsd.ddpfPixelFormat.dwFourCC == FOURCC_DXT1)
flip_s3tc::flip_dxt1_image(mipmap, width, height);
else if (ddsd.ddpfPixelFormat.dwFourCC == FOURCC_DXT3)
flip_s3tc::flip_dxt23_image(mipmap, width, height);
else if (ddsd.ddpfPixelFormat.dwFourCC == FOURCC_DXT5)
flip_s3tc::flip_dxt45_image(mipmap, width, height);
mipmap += (width + 3) / 4 * ( ( height + 3 ) / 4 ) * blockSize;
width = std::max( width / 2, 4 );
height = std::max( height / 2, 4 );
--mapcount;
}
}
data_components = components;
data_state = resource_state::good;
return;
}
void
opengl_texture::load_KTX() {
std::ifstream file( name + type, std::ios::binary | std::ios::ate ); file.unsetf( std::ios::skipws );
std::size_t filesize = static_cast<size_t>(file.tellg()); // ios::ate already positioned us at the end of the file
file.seekg( 0, std::ios::beg ); // rewind the caret afterwards
std::vector<char> filecontent;
filecontent.resize(filesize);
file.read(filecontent.data(), filecontent.size());
ddsktx_texture_info info;
if (!ddsktx_parse(&info, filecontent.data(), filecontent.size(), nullptr)) {
ErrorLog("Bad texture: KTX parsing failed", logtype::texture);
data_state = resource_state::failed;
return;
}
if (info.format != DDSKTX_FORMAT_ETC2A) {
ErrorLog("Bad texture: currently unsupported KTX type", logtype::texture);
data_state = resource_state::failed;
return;
}
data_format = GL_COMPRESSED_RGBA8_ETC2_EAC;
data_components = GL_RGBA;
data_mapcount = info.num_mips;
bool started = false;
for (int level = 0; level < info.num_mips; level++) {
ddsktx_sub_data sub_data;
ddsktx_get_sub(&info, &sub_data, filecontent.data(), filecontent.size(), 0, 0, level);
if (!started) {
data_width = sub_data.width;
data_height = sub_data.height;
if( data_width > Global.CurrentMaxTextureSize || data_height > Global.CurrentMaxTextureSize ) {
data_mapcount--;
continue;
}
started = true;
}
size_t data_offset = data.size();
data.resize(data.size() + sub_data.size_bytes);
memcpy(data.data() + data_offset, sub_data.buff, sub_data.size_bytes);
}
if (!data_mapcount) {
// there's a chance we've discarded the provided mipmap(s) as too large
WriteLog( "Texture \"" + name + "\" has no mipmaps which can fit currently set texture pixelcount limits." );
data_state = resource_state::failed;
return;
}
data_state = resource_state::good;
}
void
opengl_texture::load_TEX() {
std::ifstream file( name + type, std::ios::binary ); file.unsetf( std::ios::skipws );
char head[ 5 ];
file.read( head, 4 );
head[ 4 ] = 0;
bool hasalpha;
if( std::string( "RGB " ) == head ) {
hasalpha = false;
}
else if( std::string( "RGBA" ) == head ) {
hasalpha = true;
}
else {
ErrorLog( "Bad texture: unrecognized TEX texture sub-format: " + std::string(head), logtype::texture );
data_state = resource_state::failed;
return;
};
file.read( (char *)&data_width, sizeof( int ) );
file.read( (char *)&data_height, sizeof( int ) );
std::size_t datasize = data_width * data_height * ( hasalpha ? 4 : 3 );
data.resize( datasize );
file.read( reinterpret_cast<char *>( &data[0] ), datasize );
// fill remaining data info
if( true == hasalpha ) {
data_format = GL_RGBA;
data_components = GL_RGBA;
}
else {
data_format = GL_RGB;
data_components = GL_RGB;
}
data_mapcount = 1;
data_state = resource_state::good;
return;
}
void
opengl_texture::load_TGA() {
std::ifstream file( name + type, std::ios::binary ); file.unsetf( std::ios::skipws );
// Read the header of the TGA, compare it with the known headers for compressed and uncompressed TGAs
unsigned char tgaheader[ 18 ];
file.read( (char *)tgaheader, sizeof( unsigned char ) * 18 );
while( tgaheader[ 0 ] > 0 ) {
--tgaheader[ 0 ];
unsigned char temp;
file.read( (char *)&temp, sizeof( unsigned char ) );
}
data_width = tgaheader[ 13 ] * 256 + tgaheader[ 12 ];
data_height = tgaheader[ 15 ] * 256 + tgaheader[ 14 ];
int const bytesperpixel = tgaheader[ 16 ] / 8;
// check whether width, height an BitsPerPixel are valid
if( data_width <= 0
|| data_height <= 0
|| ( bytesperpixel != 1 && bytesperpixel != 3 && bytesperpixel != 4 ) ) {
data_state = resource_state::failed;
return;
}
// allocate the data buffer
int const datasize = data_width * data_height * 4;
data.resize(datasize);
// call the appropriate loader-routine
if( tgaheader[ 2 ] == 2 ) {
// uncompressed TGA
if( bytesperpixel == 4 ) {
// read the data directly
file.read( reinterpret_cast<char*>( &data[0] ), datasize );
}
else {
// rgb or greyscale image, expand to bgra
unsigned char buffer[ 4 ] = { 255, 255, 255, 255 }; // alpha channel will be white
unsigned int *datapointer = (unsigned int*)&data[0];
unsigned int *bufferpointer = (unsigned int*)&buffer[ 0 ];
int const pixelcount = data_width * data_height;
for( int i = 0; i < pixelcount; ++i ) {
file.read( (char *)&buffer[ 0 ], sizeof( unsigned char ) * bytesperpixel );
if( bytesperpixel == 1 ) {
// expand greyscale data
buffer[ 1 ] = buffer[ 0 ];
buffer[ 2 ] = buffer[ 0 ];
}
// copy all four values in one operation
*datapointer = *bufferpointer;
++datapointer;
}
}
}
else if( tgaheader[ 2 ] == 10 ) {
// compressed TGA
int currentpixel = 0;
unsigned char buffer[ 4 ] = { 255, 255, 255, 255 };
const int pixelcount = data_width * data_height;
unsigned int *datapointer = (unsigned int *)&data[0];
unsigned int *bufferpointer = (unsigned int *)&buffer[ 0 ];
do {
unsigned char chunkheader = 0;
file.read( (char *)&chunkheader, sizeof( unsigned char ) );
if( (chunkheader & 0x80 ) == 0 ) {
// if the high bit is not set, it means it is the number of RAW color packets, plus 1
for( int i = 0; i <= chunkheader; ++i ) {
file.read( (char *)&buffer[ 0 ], bytesperpixel );
if( bytesperpixel == 1 ) {
// expand greyscale data
buffer[ 1 ] = buffer[ 0 ];
buffer[ 2 ] = buffer[ 0 ];
}
// copy all four values in one operation
*datapointer = *bufferpointer;
++datapointer;
++currentpixel;
}
}
else {
// rle chunk, the color supplied afterwards is reapeated header + 1 times (not including the highest bit)
chunkheader &= ~0x80;
// read the current color
file.read( (char *)&buffer[ 0 ], bytesperpixel );
if( bytesperpixel == 1 ) {
// expand greyscale data
buffer[ 1 ] = buffer[ 0 ];
buffer[ 2 ] = buffer[ 0 ];
}
// copy the color into the image data as many times as dictated
for( int i = 0; i <= chunkheader; ++i ) {
*datapointer = *bufferpointer;
++datapointer;
++currentpixel;
}
}
} while( currentpixel < pixelcount );
}
else {
// unrecognized TGA sub-type
data_state = resource_state::failed;
return;
}
if( ( tgaheader[ 17 ] & 0x20 ) != 0 ) {
// normally origin is bottom-left
// if byte 17 bit 5 is set, it is top-left and needs flip
flip_vertical();
}
downsize( GL_BGRA );
if( data_width > Global.CurrentMaxTextureSize || data_height > Global.CurrentMaxTextureSize ) {
// for non-square textures there's currently possibility the scaling routine will have to abort
// before it gets all work done
data_state = resource_state::failed;
return;
}
// TODO: add horizontal/vertical data flip, based on the descriptor (18th) header byte
// fill remaining data info
data_mapcount = 1;
data_format = GL_BGRA;
data_components =
bytesperpixel == 4 ? GL_RGBA : GL_RGB;
data_state = resource_state::good;
return;
}
bool
opengl_texture::bind(size_t unit) {
if( false == is_ready
&& false == create() ) {
return false;
}
if (units[unit] == id)
return true;
if (GLAD_GL_ARB_direct_state_access)
{
glBindTextureUnit(unit, id);
}
else
{
if (unit != m_activeunit)
{
glActiveTexture(GL_TEXTURE0 + unit);
m_activeunit = unit;
}
glBindTexture(target, id);
}
units[unit] = id;
return true;
}
void
opengl_texture::unbind(size_t unit)
{
if (GLAD_GL_ARB_direct_state_access)
{
glBindTextureUnit(unit, 0);
}
else
{
if (unit != m_activeunit)
{
glActiveTexture(GL_TEXTURE0 + unit);
m_activeunit = unit;
}
//todo: for other targets
glBindTexture(GL_TEXTURE_2D, 0);
}
units[unit] = 0;
}
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
return false;
}
if( ( data_format == GL_COMPRESSED_RGBA_BPTC_UNORM )
&& ( false == bptc_supported() ) ) {
ErrorLog( "Bad texture: \"" + name + "\" uses BC7 compression, which the graphics driver doesn't support", logtype::texture );
data = std::vector<unsigned char>();
data_state = resource_state::failed;
// NOTE: temporary workaround for texture assignment errors, same as in load()
id = 0;
return false;
}
// TODO: consider creating and storing low-res version of the texture if it's ever unloaded from the gfx card,
// as a placeholder until it can be loaded again
if( id == -1 ) {
::glGenTextures( 1, &id );
::glBindTexture( target, id );
// analyze specified texture traits
for( auto const &trait : traits ) {
switch( trait ) {
case 's': { wrap_mode_s = GL_CLAMP_TO_EDGE; break; }
case 't': { wrap_mode_t = GL_CLAMP_TO_EDGE; break; }
}
}
// upload texture data
int dataoffset = 0,
datasize = 0,
datawidth = data_width,
dataheight = data_height;
if (is_rendertarget)
{
if (data_components == GL_DEPTH_COMPONENT)
{
glTexParameteri(target, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
wrap_mode_s = GL_CLAMP_TO_BORDER;
wrap_mode_t = GL_CLAMP_TO_BORDER;
float borderColor[] = { 0.0f, 0.0f, 0.0f, 0.0f };
glTexParameterfv(target, GL_TEXTURE_BORDER_COLOR, borderColor);
}
glTexParameteri( target, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glTexParameteri( target, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( target, GL_TEXTURE_WRAP_S, wrap_mode_s );
glTexParameteri( target, GL_TEXTURE_WRAP_T, wrap_mode_t );
if (Global.gfx_usegles)
{
if( target == GL_TEXTURE_2D )
glTexStorage2D(target, std::countr_zero(static_cast<unsigned int>(std::max(data_width, data_height))) + 1, data_format, data_width, data_height);
else if( target == GL_TEXTURE_2D_MULTISAMPLE )
glTexStorage2DMultisample( target, samples, data_format, data_width, data_height, GL_FALSE );
else if( target == GL_TEXTURE_2D_ARRAY )
glTexStorage3D(target, std::countr_zero(static_cast<unsigned int>(std::max(data_width, data_height))) + 1, data_format, data_width, data_height, layers);
else if( target == GL_TEXTURE_2D_MULTISAMPLE_ARRAY )
glTexStorage3DMultisample( target, samples, data_format, data_width, data_height, layers, GL_FALSE );
}
else {
if( target == GL_TEXTURE_2D )
glTexImage2D( target, 0, data_format, data_width, data_height, 0, data_components, GL_UNSIGNED_SHORT, nullptr );
else if( target == GL_TEXTURE_2D_MULTISAMPLE )
glTexImage2DMultisample( target, samples, data_format, data_width, data_height, GL_FALSE );
else if( target == GL_TEXTURE_2D_ARRAY )
glTexImage3D( target, 0, data_format, data_width, data_height, layers, 0, data_components, GL_UNSIGNED_SHORT, nullptr );
else if( target == GL_TEXTURE_2D_MULTISAMPLE_ARRAY )
glTexImage3DMultisample( target, samples, data_format, data_width, data_height, layers, GL_FALSE );
}
}
else
{
::glTexParameteri(target, GL_TEXTURE_WRAP_S, wrap_mode_s);
::glTexParameteri(target, GL_TEXTURE_WRAP_T, wrap_mode_t);
set_filtering();
// data_format and data_type specifies how image is laid out in memory
// data_components specifies what useful channels image contains
// components_hint specifies what format we want to load
// now map that mess into opengl internal format
GLint components = data_components;
auto f_it = precompressed_formats.find(data_format);
if (f_it != precompressed_formats.end())
components = data_format;
if (!components_hint)
components_hint = GL_SRGB_ALPHA;
GLint internal_format = mapping[components][components_hint];
if (Global.gfx_usegles)
{
// GLES cannot generate mipmaps on SRGB8
if (internal_format == GL_SRGB8)
internal_format = GL_SRGB8_ALPHA8;
gles_match_internalformat(internal_format);
}
auto blocksize_it = precompressed_formats.find(internal_format);
if ( data_mapcount == 1 && !glGenerateMipmap ) {
glTexParameteri(target, GL_GENERATE_MIPMAP, GL_TRUE);
}
for( int maplevel = 0; maplevel < data_mapcount; ++maplevel ) {
if (blocksize_it != precompressed_formats.end())
{
// compressed dds formats
const int datablocksize = blocksize_it->second;
datasize = (std::max(datawidth, 4) + 3) / 4 * ( ( std::max( dataheight, 4 ) + 3 ) / 4 ) * datablocksize;
::glCompressedTexImage2D(
target, maplevel, internal_format,
datawidth, dataheight, 0,
datasize, (GLubyte *)&data[ dataoffset ] );
dataoffset += datasize;
datawidth = std::max( datawidth / 2, 1 );
dataheight = std::max( dataheight / 2, 1 );
}
else {
GLint compressed_format = drivercompressed_formats[internal_format];
// uncompressed texture data. have the gfx card do the compression as it sees fit
::glTexImage2D(
target, 0,
Global.compress_tex ? compressed_format : internal_format,
data_width, data_height, 0,
data_format, data_type, (GLubyte *)&data[ 0 ] );
}
}
if ( data_mapcount == 1 && glGenerateMipmap ) {
glGenerateMipmap(target);
}
if( true == Global.ResourceMove
|| false == Global.ResourceSweep ) {
// if garbage collection is disabled we don't expect having to upload the texture more than once
data = std::vector<unsigned char>();
data_state = resource_state::none;
}
}
if( type == "make:" ) {
// for generated textures send a request to have the actual content of the texture generated
make_request();
}
is_static = Static;
is_ready = true;
}
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() {
if( id == -1 ) { return; }
if( is_static ) { return; }
if( true == Global.ResourceMove && !is_rendertarget ) {
// if resource move is enabled we don't keep a cpu side copy after upload
// so need to re-acquire the data before release
// TBD, TODO: instead of vram-ram transfer fetch the data 'normally' from the disk using worker thread
if( type == "make:" ) {
// auto generated textures only store a stub
make_stub();
}
else {
::glBindTexture(target, id );
GLint datasize {};
GLint iscompressed {};
::glGetTexLevelParameteriv(target, 0, GL_TEXTURE_COMPRESSED, &iscompressed );
if( iscompressed == GL_TRUE ) {
// texture is compressed on the gpu side
// query texture details needed to perform the backup...
::glGetTexLevelParameteriv(target, 0, GL_TEXTURE_INTERNAL_FORMAT, &data_format );
::glGetTexLevelParameteriv(target, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &datasize );
data.resize( datasize );
// ...fetch the data...
::glGetCompressedTexImage(target, 0, &data[ 0 ] );
}
else {
// for whatever reason texture didn't get compressed during upload
// fallback on plain rgba storage...
data_format = GL_RGBA;
data_type = GL_UNSIGNED_BYTE;
data.resize( data_width * data_height * 4 );
// ...fetch the data...
::glGetTexImage(target, 0, data_format, GL_UNSIGNED_BYTE, &data[ 0 ] );
}
// ...and update texture object state
data_mapcount = 1; // we keep copy of only top mipmap level
data_state = resource_state::good;
}
}
// release opengl resources
::glDeleteTextures( 1, &id );
id = -1;
is_ready = false;
return;
}
void
opengl_texture::alloc_rendertarget( GLint format, GLint components, int width, int height, int l, int s, GLint wrap ) {
data_width = width;
data_height = height;
data_format = format;
data_components = components;
data_mapcount = 1;
is_rendertarget = true;
wrap_mode_s = wrap;
wrap_mode_t = wrap;
samples = s;
if( Global.gfx_usegles && !glTexStorage2DMultisample ) {
samples = 1;
}
layers = l;
if( layers > 1 ) {
target = samples > 1 ? GL_TEXTURE_2D_MULTISAMPLE_ARRAY : GL_TEXTURE_2D_ARRAY;
}
else {
target = samples > 1 ? GL_TEXTURE_2D_MULTISAMPLE : GL_TEXTURE_2D;
}
create();
}
void
opengl_texture::set_components_hint( GLint hint ) {
components_hint = hint;
}
void
opengl_texture::reset_unit_cache() {
for( auto &unit : units ) {
unit = 0;
}
m_activeunit = -1;
}
void
opengl_texture::set_filtering() const
{
// default texture mode
::glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
::glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR );
if (Global.AnisotropicFiltering >= 0
&& ( GLAD_GL_EXT_texture_filter_anisotropic || GLAD_GL_ARB_texture_filter_anisotropic) ) {
// anisotropic filtering
::glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, Global.AnisotropicFiltering );
}
if( Global.LegacyRenderer ) {
bool sharpen{ false };
for( auto const &trait : traits ) {
switch( trait ) {
case '#': { sharpen = true; break; }
default: { break; }
}
}
if( true == sharpen ) {
// #: sharpen more
::glTexEnvf( GL_TEXTURE_FILTER_CONTROL, GL_TEXTURE_LOD_BIAS, -2.0f );
}
else {
// regular texture sharpening
::glTexEnvf( GL_TEXTURE_FILTER_CONTROL, GL_TEXTURE_LOD_BIAS, -1.0f );
}
}
}
void
opengl_texture::downsize( GLuint const Format ) {
while( data_width > Global.CurrentMaxTextureSize || data_height > Global.CurrentMaxTextureSize ) {
// scale down the base texture, if it's larger than allowed maximum
// NOTE: scaling is uniform along both axes, meaning non-square textures can drop below the maximum
// TODO: replace with proper scaling function once we have image middleware in place
if( data_width < 2 || data_height < 2 ) {
// can't go any smaller
break;
}
WriteLog( "Texture pixelcount exceeds specified limits, downsampling data" );
// trim potential odd texture sizes
data_width -= data_width % 2;
data_height -= data_height % 2;
switch( Format ) {
case GL_RGB: { downsample< glm::tvec3<std::uint8_t> >( data_width, data_height, data.data() ); break; }
case GL_BGRA:
case GL_RGBA: { downsample< glm::tvec4<std::uint8_t> >( data_width, data_height, data.data() ); break; }
default: { break; }
}
data_width /= 2;
data_height /= 2;
data.resize( data.size() / 4 ); // not strictly needed, but, eh
};
}
void
opengl_texture::flip_vertical() {
auto const swapsize { data_width * 4 };
auto destination { data.begin() + ( data_height - 1 ) * swapsize };
auto sampler { data.begin() };
for( auto row = 0; row < data_height / 2; ++row ) {
std::swap_ranges( sampler, sampler + swapsize, destination );
sampler += swapsize;
destination -= swapsize;
}
}
void
texture_manager::unit( GLint const Textureunit ) {
if( opengl_texture::m_activeunit == Textureunit ) { return; }
opengl_texture::m_activeunit = Textureunit;
::glActiveTexture( GL_TEXTURE0 + Textureunit );
}
// ustalenie numeru tekstury, wczytanie jeśli jeszcze takiej nie było
texture_handle
texture_manager::create( std::string Filename, bool const Loadnow, GLint Formathint ) {
if( contains( Filename, '|' ) ) {
Filename.erase( Filename.find( '|' ) ); // po | może być nazwa kolejnej tekstury
}
std::pair<std::string, std::string> locator; // resource name, resource type
std::string traits;
// discern textures generated by a script
// TBD: support file: for file resources?
auto const isgenerated { Filename.find( "make:" ) == 0 };
auto const isinternalsrc { Filename.find( "internal_src:" ) == 0 };
// process supplied resource name
if( isgenerated || isinternalsrc ) {
// generated resource
// scheme:(user@)path?query
// remove scheme indicator
Filename.erase( 0, Filename.find( ':' ) + 1 );
// TODO: extract traits specification from the query
// clean up slashes
erase_leading_slashes( Filename );
}
else {
// regular file resource
// (filepath/)(#)filename.extension(:traits)
// extract trait specifications
auto const traitpos = Filename.rfind( ':' );
if( traitpos != std::string::npos ) {
// po dwukropku mogą być podane dodatkowe informacje niebędące nazwą tekstury
if( Filename.size() > traitpos + 1 ) {
traits = Filename.substr( traitpos + 1 );
}
Filename.erase( traitpos );
}
// potentially trim file type indicator since we check for multiple types
erase_extension( Filename );
// clean up slashes
erase_leading_slashes( Filename );
Filename = ToLower( Filename );
// temporary code for legacy assets -- textures with names beginning with # are to be sharpened
if( Filename.starts_with("#") || contains( Filename, "/#" ) ) {
traits += '#';
}
}
// try to locate requested texture in the databank
// TBD, TODO: include trait specification in the resource id in the databank?
auto const lookup { find_in_databank( Filename ) };
if( lookup != npos ) {
return lookup;
}
// if the lookup fails...
if( isgenerated ) {
// TODO: verify presence of the generator script
locator.first = Filename;
locator.second = "make:";
}
else if ( isinternalsrc ) {
locator.first = Filename;
locator.second = "internalsrc:";
}
else {
// ...for file resources check if it's on disk
locator = find_on_disk( Filename );
if( true == locator.first.empty() ) {
// there's nothing matching in the databank nor on the disk, report failure
ErrorLog( "Bad file: failed to locate texture file \"" + Filename + "\"", logtype::file );
return npos;
}
}
auto texture = new opengl_texture();
texture->name = locator.first;
texture->type = locator.second;
texture->traits = traits;
texture->components_hint = Formathint;
auto const textureindex = (texture_handle)m_textures.size();
m_textures.emplace_back( texture, std::chrono::steady_clock::time_point() );
m_texturemappings.emplace( locator.first, textureindex );
WriteLog( "Created texture object for \"" + locator.first + "\"", logtype::texture );
if( true == Loadnow ) {
texture_manager::texture( textureindex ).load();
#ifndef EU07_DEFERRED_TEXTURE_UPLOAD
texture_manager::texture( textureindex ).create();
// texture creation binds a different texture, force a re-bind on next use
m_activetexture = -1;
#endif
}
return textureindex;
};
void
texture_manager::bind( std::size_t const Unit, texture_handle const Texture ) {
if( Unit == -1 ) { return; } // no texture unit, nothing to bind the texture to
if (Texture != null_handle)
mark_as_used(Texture).bind(Unit);
else
opengl_texture::unbind(Unit);
}
opengl_texture &
texture_manager::mark_as_used(const texture_handle Texture) {
auto &pair = m_textures[ Texture ];
pair.second = m_garbagecollector.timestamp();
return *pair.first;
}
void
texture_manager::delete_textures() {
for( auto const &texture : m_textures ) {
// usunięcie wszyskich tekstur (bez usuwania struktury)
if( texture.first->id > 0
&& texture.first->id != -1 ) {
::glDeleteTextures( 1, &texture.first->id );
}
delete texture.first;
}
}
// 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() {
if( m_garbagecollector.sweep() > 0 ) {
for( auto &unit : opengl_texture::units ) {
unit = -1;
}
}
}
// debug performance string
std::string
texture_manager::info() const {
// TODO: cache this data and update only during resource sweep
std::size_t totaltexturecount{ m_textures.size() - 1 };
std::size_t totaltexturesize{ 0 };
#ifdef EU07_DEFERRED_TEXTURE_UPLOAD
std::size_t readytexturecount{ 0 };
std::size_t readytexturesize{ 0 };
#endif
for( auto const& texture : m_textures ) {
totaltexturesize += texture.first->size;
#ifdef EU07_DEFERRED_TEXTURE_UPLOAD
if( texture.first->is_ready ) {
++readytexturecount;
readytexturesize += texture.first->size;
}
#endif
}
return
"textures: "
#ifdef EU07_DEFERRED_TEXTURE_UPLOAD
+ std::to_string( readytexturecount )
+ " ("
+ to_string( readytexturesize / 1024.0f, 2 ) + " mb)"
+ " in vram, "
#endif
+ std::to_string( totaltexturecount )
+ " ("
+ to_string( totaltexturesize / 1024.0f, 2 ) + " mb)"
+ " total";
}
// checks whether specified texture is in the texture bank. returns texture id, or npos.
texture_handle
texture_manager::find_in_databank( std::string const &Texturename ) const {
std::vector<std::string> const filenames {
Global.asCurrentTexturePath + Texturename,
Texturename,
paths::textures + Texturename };
for( auto const &filename : filenames ) {
auto const lookup { m_texturemappings.find( filename ) };
if( lookup != m_texturemappings.end() ) {
return lookup->second;
}
}
// all lookups failed
return npos;
}
// checks whether specified file exists.
std::pair<std::string, std::string>
texture_manager::find_on_disk( std::string const &Texturename ) {
std::vector<std::string> const filenames {
Global.asCurrentTexturePath + Texturename,
Texturename,
paths::textures + Texturename };
auto lookup =
FileExists(
filenames,
{ Global.szDefaultExt } );
if( false == lookup.first.empty() ) {
return lookup;
}
// if the first attempt fails, try entire extension list
// NOTE: slightly wasteful as it means preferred extension is tested twice, but, eh
return FileExists(filenames, {".dds", ".tga", ".ktx", ".png", ".bmp", ".jpg", ".tex"});
}
//---------------------------------------------------------------------------