/* 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 "Texture.h" #include "application.h" #include "dictionary.h" #include "Globals.h" #include "Logs.h" #include "utilities.h" #include "sn_utils.h" #include "utilities.h" #include "flip-s3tc.h" #include "stb/stb_image.h" #include #include "dds-ktx/dds-ktx.h" #define EU07_DEFERRED_TEXTURE_UPLOAD std::array opengl_texture::units = { 0 }; GLint opengl_texture::m_activeunit = -1; std::unordered_map 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 }, }; std::unordered_map 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> 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_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 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 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 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_PNG(); } 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 if( data_state == resource_state::good ) { // 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, 4 ) != data_width ) || ( quantize( data_height, 4 ) != 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( 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 = width; 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::make_request() { auto const components { Split( name, '?' ) }; auto *dictionary { new dictionary_source( components.back() ) }; auto rt = std::make_shared(); rt->shared_tex = id; 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(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; // // This .dds loader supports the loading of compressed formats DXT1, DXT3 // and DXT5. // 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; 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; } // 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 (Global.dds_upper_origin) { 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 = ( ddsd.ddpfPixelFormat.dwFourCC == FOURCC_DXT1 ? GL_RGB : GL_RGBA ); 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(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 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( &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( &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::good && !is_rendertarget ) { // don't bother until we have useful texture data // and it isn't rendertarget texture without loaded data 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, count_trailing_zeros(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, count_trailing_zeros( 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(); 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; } // 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 >( data_width, data_height, data.data() ); break; } case GL_BGRA: case GL_RGBA: { downsample< glm::tvec4 >( 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( Filename.find( '|' ) != std::string::npos ) Filename.erase( Filename.find( '|' ) ); // po | może być nazwa kolejnej tekstury std::pair 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 ); // temporary code for legacy assets -- textures with names beginning with # are to be sharpened if( ( Filename.front() == '#' ) || ( Filename.find( "/#" ) != std::string::npos ) ) { 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; } } // 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 const filenames { Global.asCurrentTexturePath + Texturename, Texturename, szTexturePath + 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 texture_manager::find_on_disk( std::string const &Texturename ) const { std::vector const filenames { Global.asCurrentTexturePath + Texturename, Texturename, szTexturePath + 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" } ) ); } //---------------------------------------------------------------------------