diff --git a/codxe.vcxproj b/codxe.vcxproj index 0c208ef1..d99351f7 100644 --- a/codxe.vcxproj +++ b/codxe.vcxproj @@ -79,6 +79,11 @@ + + + + + @@ -134,6 +139,7 @@ + @@ -203,6 +209,12 @@ + + + + + + @@ -262,6 +274,7 @@ + diff --git a/resources/iw3/_codxe/mods/codjumper/highmip/~weapon_rpg7_spec-rgb&weapon_~06163c5b.dds b/resources/iw3/_codxe/mods/codjumper/highmip/~weapon_rpg7_spec-rgb&weapon_~06163c5b.dds deleted file mode 100644 index d1f2e9e2..00000000 Binary files a/resources/iw3/_codxe/mods/codjumper/highmip/~weapon_rpg7_spec-rgb&weapon_~06163c5b.dds and /dev/null differ diff --git a/resources/iw3/_codxe/mods/codjumper/images/~weapon_rpg7_spec-rgb&weapon_~06163c5b.dds b/resources/iw3/_codxe/mods/codjumper/images/~weapon_rpg7_spec-rgb&weapon_~06163c5b.dds index 905ca9b9..d64b27d3 100644 Binary files a/resources/iw3/_codxe/mods/codjumper/images/~weapon_rpg7_spec-rgb&weapon_~06163c5b.dds and b/resources/iw3/_codxe/mods/codjumper/images/~weapon_rpg7_spec-rgb&weapon_~06163c5b.dds differ diff --git a/src/common/config.cpp b/src/common/config.cpp index d3754b9a..9d94b97b 100644 --- a/src/common/config.cpp +++ b/src/common/config.cpp @@ -11,12 +11,12 @@ const char *CONFIG_PATH = "game:\\_codxe\\codxe.json"; const char *MOD_DIR = "game:\\_codxe\\mods"; +const char *USERRAW_DIR = "game:\\_codxe\\userraw"; const char *DUMP_DIR = "game:\\_codxe\\dump"; // Default values std::string Config::active_mod = ""; -bool Config::dump_rawfile = false; -bool Config::dump_map_ents = false; +bool Config::dump_assets = false; std::string Config::mod_base_path = ""; namespace @@ -203,8 +203,7 @@ Config::~Config() // Reset to defaults on cleanup active_mod = ""; mod_base_path = ""; - dump_rawfile = false; - dump_map_ents = false; + dump_assets = false; DbgPrint("[codxe][Config] Configuration unloaded\n"); } @@ -243,13 +242,9 @@ bool Config::LoadFromJson(const char *jsonBuffer, DWORD bufferSize) wcstombs(narrowValue, valueBuffer, sizeof(narrowValue)); active_mod = narrowValue; } - else if (wcscmp(propertyName, L"dump_rawfile") == 0) + else if (wcscmp(propertyName, L"dump_assets") == 0) { - dump_rawfile = (jsonTokenType == Json_True); - } - else if (wcscmp(propertyName, L"dump_map_ents") == 0) - { - dump_map_ents = (jsonTokenType == Json_True); + dump_assets = (jsonTokenType == Json_True); } else { @@ -262,8 +257,7 @@ bool Config::LoadFromJson(const char *jsonBuffer, DWORD bufferSize) DbgPrint("[codxe][Config] Configuration loaded:\n"); DbgPrint(" Active Mod: %s\n", active_mod.c_str()); - DbgPrint(" Dump Raw Scripts: %s\n", dump_rawfile ? "true" : "false"); - DbgPrint(" Dump Map Entities: %s\n", dump_map_ents ? "true" : "false"); + DbgPrint(" Dump Assets: %s\n", dump_assets ? "true" : "false"); if (!active_mod.empty()) { diff --git a/src/common/config.h b/src/common/config.h index 7fdc37a9..15c06f0e 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -4,6 +4,7 @@ extern const char *CONFIG_PATH; extern const char *MOD_DIR; +extern const char *USERRAW_DIR; extern const char *DUMP_DIR; bool DirectoryExists(const char *path); @@ -23,8 +24,7 @@ class Config : public Module static std::string active_mod; static std::string mod_base_path; - static bool dump_rawfile; - static bool dump_map_ents; + static bool dump_assets; static std::string GetModBasePath(); static const char *GetModBasePathCStr(); diff --git a/src/game/iw2/mp/components/scr_parser.cpp b/src/game/iw2/mp/components/scr_parser.cpp index 5a53821f..d6fa3cfd 100644 --- a/src/game/iw2/mp/components/scr_parser.cpp +++ b/src/game/iw2/mp/components/scr_parser.cpp @@ -15,7 +15,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename, co archive); }; - if (Config::dump_rawfile) + if (Config::dump_assets) { DbgPrint("GSCLoader: Dumping script %s\n", extFilename); auto contents = callOriginal(); diff --git a/src/game/iw2/sp/components/scr_parser.cpp b/src/game/iw2/sp/components/scr_parser.cpp index 8f49c3ab..c18d144e 100644 --- a/src/game/iw2/sp/components/scr_parser.cpp +++ b/src/game/iw2/sp/components/scr_parser.cpp @@ -15,7 +15,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename, co archive); }; - if (Config::dump_rawfile) + if (Config::dump_assets) { DbgPrint("GSCLoader: Dumping script %s\n", extFilename); auto contents = callOriginal(); diff --git a/src/game/iw3/mp/components/image_loader.cpp b/src/game/iw3/mp/components/image_loader.cpp index dc157c14..cfc99b54 100644 --- a/src/game/iw3/mp/components/image_loader.cpp +++ b/src/game/iw3/mp/components/image_loader.cpp @@ -2,606 +2,390 @@ #include "common/config.h" #include "command.h" #include "image_loader.h" - -// Forgive me for this dreadful code. It was hacked together until semi working and not touched since. -// TODO: refactor and generalise for the other games. +#include "image/dds_loader.h" +#include "image/dds_writer.h" +#include "image/texture_layout.h" +#include "image/xenos_texture.h" namespace { +std::set g_streamedImageReplacements; -// TODO: MAKEFOURCC('D', 'X', 'T', '1'); -// DDS Constants -const uint32_t DDS_MAGIC = MAKEFOURCC('D', 'D', 'S', ' '); -const uint32_t DDS_HEADER_SIZE = 124; -const uint32_t DDS_PIXEL_FORMAT_SIZE = 32; -const uint32_t DDSD_CAPS = 0x1; -const uint32_t DDSD_HEIGHT = 0x2; -const uint32_t DDSD_WIDTH = 0x4; -const uint32_t DDSD_PIXELFORMAT = 0x1000; -const uint32_t DDSD_LINEARSIZE = 0x80000; -const uint32_t DDPF_FOURCC = 0x4; -const uint32_t DDPF_RGB = 0x40; -const uint32_t DDPF_ALPHAPIXELS = 0x1; -const uint32_t DDSCAPS_TEXTURE = 0x1000; -const uint32_t DDSCAPS_MIPMAP = 0x400000; -const uint32_t DDPF_LUMINANCE = 0x20000; - -// DDS Pixel Formats (FourCC Codes) -const uint32_t DXT1_FOURCC = MAKEFOURCC('D', 'X', 'T', '1'); -const uint32_t DXT3_FOURCC = MAKEFOURCC('D', 'X', 'T', '3'); -const uint32_t DXT5_FOURCC = MAKEFOURCC('D', 'X', 'T', '5'); -const uint32_t DXN_FOURCC = MAKEFOURCC('A', 'T', 'I', '2'); // (DXN / BC5) - -// Additional DDS Cubemap Flags -const uint32_t DDSCAPS2_CUBEMAP = 0x200; -const uint32_t DDSCAPS2_CUBEMAP_POSITIVEX = 0x400; -const uint32_t DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800; -const uint32_t DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000; -const uint32_t DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000; -const uint32_t DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000; -const uint32_t DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000; - -// DDS Header Structure (with inline endian swapping) -struct DDSHeader -{ - uint32_t magic; - uint32_t size; - uint32_t flags; - uint32_t height; - uint32_t width; - uint32_t pitchOrLinearSize; - uint32_t depth; - uint32_t mipMapCount; - uint32_t reserved1[11]; - struct - { - uint32_t size; - uint32_t flags; - uint32_t fourCC; - uint32_t rgbBitCount; - uint32_t rBitMask; - uint32_t gBitMask; - uint32_t bBitMask; - uint32_t aBitMask; - } pixelFormat; - uint32_t caps; - uint32_t caps2; - uint32_t caps3; - uint32_t caps4; - uint32_t reserved2; -}; - -static_assert(sizeof(DDSHeader) == 128, ""); - -struct DDSImage +std::string extract_filename(const char *filename) { - DDSHeader header; - std::vector data; -}; + std::string path(filename); -struct StaticDDSImage -{ - DDSHeader header; - uint8_t *data; - DWORD dataSize; -}; + // Find last backslash '\' or forward slash '/' + size_t lastSlash = path.find_last_of("\\/"); + size_t start = (lastSlash == std::string::npos) ? 0 : lastSlash + 1; -uint8_t g_staticDDSData[1024 * 1024]; + // Find last dot '.' (extension separator) + size_t lastDot = path.find_last_of('.'); + size_t end = (lastDot == std::string::npos || lastDot < start) ? path.length() : lastDot; -// Function to swap all necessary fields from little-endian to big-endian -void SwapDDSHeaderEndian(DDSHeader &header) -{ - header.magic = _byteswap_ulong(header.magic); - header.size = _byteswap_ulong(header.size); - header.flags = _byteswap_ulong(header.flags); - header.height = _byteswap_ulong(header.height); - header.width = _byteswap_ulong(header.width); - header.pitchOrLinearSize = _byteswap_ulong(header.pitchOrLinearSize); - header.depth = _byteswap_ulong(header.depth); - header.mipMapCount = _byteswap_ulong(header.mipMapCount); - - for (int i = 0; i < 11; i++) - header.reserved1[i] = _byteswap_ulong(header.reserved1[i]); - - header.pixelFormat.size = _byteswap_ulong(header.pixelFormat.size); - header.pixelFormat.flags = _byteswap_ulong(header.pixelFormat.flags); - header.pixelFormat.fourCC = _byteswap_ulong(header.pixelFormat.fourCC); - header.pixelFormat.rgbBitCount = _byteswap_ulong(header.pixelFormat.rgbBitCount); - header.pixelFormat.rBitMask = _byteswap_ulong(header.pixelFormat.rBitMask); - header.pixelFormat.gBitMask = _byteswap_ulong(header.pixelFormat.gBitMask); - header.pixelFormat.bBitMask = _byteswap_ulong(header.pixelFormat.bBitMask); - header.pixelFormat.aBitMask = _byteswap_ulong(header.pixelFormat.aBitMask); - - header.caps = _byteswap_ulong(header.caps); - header.caps2 = _byteswap_ulong(header.caps2); - header.caps3 = _byteswap_ulong(header.caps3); - header.caps4 = _byteswap_ulong(header.caps4); - header.reserved2 = _byteswap_ulong(header.reserved2); + return path.substr(start, end - start); } -DDSImage ReadDDSFile(const std::string &filepath) +bool Validate2DReplacementData(const iw3::mp::GfxImage *image, const image::DdsImage &ddsImage, GPUTEXTUREFORMAT format, + uint32_t ddsFirstMipLevel, uint32_t replacementLevelCount, size_t *requiredDDSSize, + size_t *requiredTextureBytes) { - DDSImage ddsImage; - std::ifstream file(filepath, std::ios::binary); - - if (!file.is_open()) - { - DbgPrint("ERROR: Unable to open file: %s\n", filepath.c_str()); - return ddsImage; // Return empty DDSImage - } - - // Read DDS header (raw, little-endian) - file.read(reinterpret_cast(&ddsImage.header), sizeof(DDSHeader)); + const size_t ddsMipOffset = + image::CalculateDdsMipOffset(ddsImage.header.dwWidth, ddsImage.header.dwHeight, format, ddsFirstMipLevel); + const size_t requiredLinearSize = image::CalculateRequiredLinearDataSize( + ddsImage.header.dwWidth, ddsImage.header.dwHeight, format, ddsFirstMipLevel, replacementLevelCount, 1u); + *requiredDDSSize = ddsMipOffset + requiredLinearSize; + if (requiredLinearSize == 0 || (ddsFirstMipLevel > 0 && ddsMipOffset == 0)) + return false; - // Swap only the magic number to big-endian for proper validation - uint32_t magicSwapped = _byteswap_ulong(ddsImage.header.magic); + if (ddsImage.data.size() < *requiredDDSSize) + return false; - if (magicSwapped != 0x20534444) // 'DDS ' in big-endian - { - DbgPrint("ERROR: Invalid DDS file: %s\n", filepath.c_str()); - file.close(); - return ddsImage; - } + const uint32_t baseSize = + image::xenos_texture::CalculateBaseSize(image->texture.basemap, image->width, image->height, 1u); + const size_t mipBytes = + image::CalculateRequiredMipTextureBytes(image->width, image->height, format, 1u, replacementLevelCount, 1u); + *requiredTextureBytes = static_cast(baseSize) + mipBytes; + const int cardMemory = image->cardMemory.platform[0]; + if (cardMemory > 0 && *requiredTextureBytes > static_cast(cardMemory)) + return false; - // Swap header fields to big-endian for Xbox 360 - SwapDDSHeaderEndian(ddsImage.header); + return true; +} - // Move to end of file to get total file size - file.seekg(0, std::ios::end); - std::streampos fileSize = file.tellg(); +bool ValidateCubeReplacementData(const iw3::mp::GfxImage *image, const image::DdsImage &ddsImage, + GPUTEXTUREFORMAT format, uint32_t faceSize, uint32_t tiledBaseSize, + size_t *requiredDDSSize) +{ + *requiredDDSSize = static_cast(faceSize) * 6u; + if (faceSize == 0 || *requiredDDSSize == 0) + return false; - // Ensure fileSize is valid before proceeding - if (fileSize == std::streampos(-1)) - { - DbgPrint("ERROR: Failed to determine file size.\n"); - file.close(); - return ddsImage; - } + if (ddsImage.data.size() < *requiredDDSSize) + return false; - // Move back to after the header - file.seekg(sizeof(DDSHeader), std::ios::beg); + const int cardMemory = image->cardMemory.platform[0]; + if (cardMemory > 0 && static_cast(tiledBaseSize) > static_cast(cardMemory)) + return false; - // Compute data size safely - size_t dataSize = static_cast(fileSize) - sizeof(DDSHeader); + return true; +} - // Read image data - ddsImage.data.resize(dataSize); - file.read(reinterpret_cast(ddsImage.data.data()), dataSize); +} // namespace - file.close(); +namespace iw3 +{ +namespace mp +{ +namespace +{ +const char *HIGHMIP_DIR = "D:\\highmip"; - // Debug output - DbgPrint("INFO: DDS file '%s' loaded successfully.\n", filepath.c_str()); - DbgPrint(" Resolution: %ux%u\n", ddsImage.header.width, ddsImage.header.height); - DbgPrint(" MipMaps: %u\n", ddsImage.header.mipMapCount); - DbgPrint(" Data Size: %u bytes\n", static_cast(dataSize)); +std::string GetSanitizedImageName(const char *imageName) +{ + if (imageName == NULL) + return std::string(); - return ddsImage; + std::string sanitizedName = imageName; + sanitizedName.erase(std::remove_if(sanitizedName.begin(), sanitizedName.end(), [](char c) { return c == '*'; }), + sanitizedName.end()); + return sanitizedName; } -StaticDDSImage ReadDDSFileStatic(const char *filepath) +std::string GetImageDumpPath(const char *imageName) { - StaticDDSImage ddsImage = {}; + return std::string(DUMP_DIR) + "\\images\\" + GetSanitizedImageName(imageName) + ".dds"; +} - HANDLE file = CreateFileA(filepath, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL, nullptr); - if (file == INVALID_HANDLE_VALUE) - { - DbgPrint("ERROR: Unable to open file: %s\n", filepath); - return ddsImage; - } +std::string GetImageReplacementPath(const char *imageName) +{ + const std::string userPath = std::string(USERRAW_DIR) + "\\images\\" + imageName + ".dds"; + if (filesystem::file_exists(userPath)) + return userPath; - DWORD bytesRead = 0; - if (!ReadFile(file, &ddsImage.header, sizeof(DDSHeader), &bytesRead, nullptr) || bytesRead != sizeof(DDSHeader)) - { - DbgPrint("ERROR: Failed to read DDS header: %s\n", filepath); - CloseHandle(file); - return ddsImage; - } + return Config::GetModBasePath() + "\\images\\" + imageName + ".dds"; +} - const uint32_t magicSwapped = _byteswap_ulong(ddsImage.header.magic); - if (magicSwapped != 0x20534444) - { - DbgPrint("ERROR: Invalid DDS file: %s\n", filepath); - CloseHandle(file); - return ddsImage; - } +bool ReadBinaryFile(const std::string &path, std::vector *buffer) +{ + std::ifstream file(path.c_str(), std::ios::binary | std::ios::ate); + if (!file) + return false; - SwapDDSHeaderEndian(ddsImage.header); + const std::streamsize size = file.tellg(); + if (size < 0) + return false; - const DWORD fileSize = GetFileSize(file, nullptr); - if (fileSize == INVALID_FILE_SIZE || fileSize <= sizeof(DDSHeader)) - { - DbgPrint("ERROR: Failed to determine DDS file size: %s\n", filepath); - CloseHandle(file); - return ddsImage; - } + file.seekg(0, std::ios::beg); + buffer->resize(static_cast(size)); + if (size == 0) + return true; - const DWORD dataSize = fileSize - sizeof(DDSHeader); - if (dataSize > sizeof(g_staticDDSData)) - { - DbgPrint("ERROR: DDS file too large for static buffer: %s size=%u max=%u\n", filepath, dataSize, - static_cast(sizeof(g_staticDDSData))); - CloseHandle(file); - return ddsImage; - } + return file.read(reinterpret_cast(buffer->data()), size) != 0; +} - if (!ReadFile(file, g_staticDDSData, dataSize, &bytesRead, nullptr) || bytesRead != dataSize) +std::map CollectHighMipFiles() +{ + std::map highMipFiles; + const std::vector files = filesystem::list_files_in_directory(HIGHMIP_DIR); + for (size_t i = 0; i < files.size(); ++i) { - DbgPrint("ERROR: Failed to read DDS data: %s\n", filepath); - CloseHandle(file); - return ddsImage; + const std::string assetName = extract_filename(files[i].c_str()); + highMipFiles[assetName] = std::string(HIGHMIP_DIR) + "\\" + files[i]; } - CloseHandle(file); - - ddsImage.data = g_staticDDSData; - ddsImage.dataSize = dataSize; - - DbgPrint("INFO: DDS file '%s' loaded successfully.\n", filepath); - DbgPrint(" Resolution: %ux%u\n", ddsImage.header.width, ddsImage.header.height); - DbgPrint(" MipMaps: %u\n", ddsImage.header.mipMapCount); - DbgPrint(" Data Size: %u bytes\n", ddsImage.dataSize); - - return ddsImage; + return highMipFiles; } -std::string extract_filename(const char *filename) +std::string FindHighMipPathForImage(const std::map &highMipFiles, const char *imageName) { - std::string path(filename); + if (imageName == NULL) + return std::string(); - // Find last backslash '\' or forward slash '/' - size_t lastSlash = path.find_last_of("\\/"); - size_t start = (lastSlash == std::string::npos) ? 0 : lastSlash + 1; + std::map::const_iterator highMip = highMipFiles.find(imageName); + if (highMip != highMipFiles.end()) + return highMip->second; - // Find last dot '.' (extension separator) - size_t lastDot = path.find_last_of('.'); - size_t end = (lastDot == std::string::npos || lastDot < start) ? path.length() : lastDot; + const std::string sanitizedName = GetSanitizedImageName(imageName); + highMip = highMipFiles.find(sanitizedName); + if (highMip != highMipFiles.end()) + return highMip->second; - return path.substr(start, end - start); + return std::string(); } -void GPUEndianSwapTexture(std::vector &pixelData, GPUENDIAN endianType) +bool Image_DumpHighMip(const GfxImage *image, const std::string &highMipPath) { - switch (endianType) + if (image->mapType != MAPTYPE_2D) + return false; + + std::vector highMipData; + if (!ReadBinaryFile(highMipPath, &highMipData)) { - case GPUENDIAN_8IN16: - XGEndianSwapMemory(pixelData.data(), pixelData.data(), XGENDIAN_8IN16, 2, pixelData.size() / 2); - break; - case GPUENDIAN_8IN32: - XGEndianSwapMemory(pixelData.data(), pixelData.data(), XGENDIAN_8IN32, 4, pixelData.size() / 4); - break; - case GPUENDIAN_16IN32: - XGEndianSwapMemory(pixelData.data(), pixelData.data(), XGENDIAN_16IN32, 4, pixelData.size() / 4); - break; + Com_PrintError(CON_CHANNEL_ERROR, "Could not read highmip for image '%s': %s\n", image->name, + highMipPath.c_str()); + return false; } -} - -UINT CalculateMipLevelSize(UINT width, UINT height, UINT mipLevel, GPUTEXTUREFORMAT format) -{ - // Calculate dimensions for the requested mip level - UINT mipWidth = max(1, width >> mipLevel); - UINT mipHeight = max(1, height >> mipLevel); - // Calculate size based on format - UINT blockSize; - switch (format) + const uint32_t width = static_cast(image->width) * 2u; + const uint32_t height = static_cast(image->height) * 2u; + const uint32_t format = image->texture.basemap->Format.DataFormat; + const uint32_t linearLevelSize = image::xenos_texture::CalculateLinearLevelSize(width, height, 0u, format); + const uint32_t tiledLevelSize = image::xenos_texture::CalculateTiledLevelSize(width, height, 0u, format, 0u); + const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(width, 0u, format); + if (linearLevelSize == 0 || tiledLevelSize == 0 || rowPitch == 0) { - case GPUTEXTUREFORMAT_DXT1: - blockSize = 8; // 8 bytes per 4x4 block - break; - case GPUTEXTUREFORMAT_DXT2_3: - blockSize = 16; // 16 bytes per 4x4 block - break; - case GPUTEXTUREFORMAT_DXT4_5: - blockSize = 16; // 16 bytes per 4x4 block - break; - case GPUTEXTUREFORMAT_DXN: - blockSize = 16; // 16 bytes per 4x4 block (two 8-byte channels) - break; - default: - DbgPrint("CalculateMipLevelSize: Unsupported format %d\n", format); - return 0; + Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': unsupported highmip format %u\n", image->name, format); + return false; } - // For block-compressed formats, calculate number of blocks - // Each block is 4x4 pixels, so we need to round up to nearest block - UINT blocksWide = (mipWidth + 3) / 4; - UINT blocksHigh = (mipHeight + 3) / 4; - - // Calculate total size in bytes - UINT sizeInBytes = blocksWide * blocksHigh * blockSize; - - return sizeInBytes; -} - -} // namespace + if (highMipData.size() < tiledLevelSize) + { + Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': highmip data is too small (have %u, need %u)\n", + image->name, static_cast(highMipData.size()), tiledLevelSize); + return false; + } -namespace iw3 -{ -namespace mp -{ -void Image_DbgPrint(const GfxImage *image) -{ - const int format = image->texture.basemap->Format.DataFormat; - char *format_str; - switch (format) + image::DDS_HEADER header; + if (!image::CreateDdsHeader(header, width, height, image->depth, 1u, linearLevelSize, image::DDSCAPS_TEXTURE, 0u, + format)) { - case GPUTEXTUREFORMAT_DXT1: - format_str = "DXT1"; - break; - case GPUTEXTUREFORMAT_DXT2_3: - format_str = "DXT2_3"; - break; - case GPUTEXTUREFORMAT_DXT4_5: - format_str = "DXT4_5"; - break; - case GPUTEXTUREFORMAT_DXN: - format_str = "DXN"; - break; - case GPUTEXTUREFORMAT_8: - format_str = "8"; - break; - case GPUTEXTUREFORMAT_8_8: - format_str = "8_8"; - break; - case GPUTEXTUREFORMAT_8_8_8_8: - format_str = "8_8_8_8"; - break; - default: - format_str = "UNKNOWN"; - break; + Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': unsupported highmip format %u\n", image->name, format); + return false; } - XGTEXTURE_DESC SourceDesc; - XGGetTextureDesc(image->texture.basemap, 0, &SourceDesc); - BOOL IsBorderTexture = XGIsBorderTexture(image->texture.basemap); - UINT MipTailBaseLevel = XGGetMipTailBaseLevel(SourceDesc.Width, SourceDesc.Height, IsBorderTexture); + const std::string outputPath = GetImageDumpPath(image->name); + std::ofstream outputFile(outputPath.c_str(), std::ios::binary); + if (!outputFile) + { + Com_PrintError(CON_CHANNEL_ERROR, "Could not create DDS for image '%s': %s\n", image->name, outputPath.c_str()); + return false; + } - // SourceDesc.BitsPerPixel; - // SourceDesc.BytesPerBlock; + image::WriteDdsHeader(outputFile, header); - UINT MipLevelCount = image->texture.basemap->GetLevelCount(); + std::vector tiledData(highMipData.begin(), highMipData.begin() + tiledLevelSize); + image::xenos_texture::ApplyGpuEndian(tiledData.data(), tiledData.size(), + static_cast(image->texture.basemap->Format.Endian)); - UINT BaseSize; - XGGetTextureLayout(image->texture.basemap, 0, &BaseSize, 0, 0, 0, 0, 0, 0, 0, 0); + std::vector linearData(linearLevelSize); + if (!image::xenos_texture::UntileTextureLevel(width, height, 0u, format, 0u, linearData.data(), linearData.size(), + rowPitch, tiledData.data(), tiledData.size())) + { + Com_PrintError(CON_CHANNEL_ERROR, "Could not decode highmip for image '%s'\n", image->name); + return false; + } - Com_Printf(CON_CHANNEL_CONSOLEONLY, - "Image_DbgPrint: Dumping image Name='%s', Type=%d, Dimensions=%dx%d, MipLevels=%d, MipTailBaseLevel=%d, " - "Format=%s, BitsPerPixel=%d, BytesPerBlock=%d, Endian=%d, BaseSize=%d\n", - image->name, image->mapType, image->width, image->height, MipLevelCount, MipTailBaseLevel, format_str, - SourceDesc.BitsPerPixel, SourceDesc.BytesPerBlock, image->texture.basemap->Format.Endian, BaseSize); + outputFile.write(reinterpret_cast(linearData.data()), linearData.size()); + Com_Printf(CON_CHANNEL_CONSOLEONLY, "Dumped image '%s' (highmip)\n", image->name); + return true; } +} // namespace -void Image_Dump(const GfxImage *image) +void Image_Dump(const GfxImage *image, const std::string &highMipPath) { // TODO: cleanup empty files if failed - Com_Printf(CON_CHANNEL_CONSOLEONLY, "Image_Dump: Dumping image '%s'\n", image->name); - if (!image) { - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Null GfxImage!\n"); + Com_PrintError(CON_CHANNEL_ERROR, "Skipping image: null asset\n"); + return; + } + + if (image->name == NULL) + { + Com_PrintError(CON_CHANNEL_ERROR, "Skipping image: missing name\n"); return; } if (!image->pixels || image->baseSize == 0) { - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Image '%s' has no valid pixel data!\n", image->name); + Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': no pixel data\n", image->name); return; } if (image->mapType != MAPTYPE_2D && image->mapType != MAPTYPE_CUBE) { - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported map type %d!\n", image->mapType); + Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': unsupported map type %d\n", image->name, + image->mapType); return; } - UINT BaseSize; - XGGetTextureLayout(image->texture.basemap, 0, &BaseSize, 0, 0, 0, 0, 0, 0, 0, 0); - - DDSHeader header; - memset(&header, 0, sizeof(DDSHeader)); + if (!highMipPath.empty() && Image_DumpHighMip(image, highMipPath)) + return; - header.magic = _byteswap_ulong(DDS_MAGIC); - header.size = _byteswap_ulong(DDS_HEADER_SIZE); - header.flags = _byteswap_ulong(DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_LINEARSIZE); - header.height = _byteswap_ulong(image->height); - header.width = _byteswap_ulong(image->width); - header.depth = _byteswap_ulong(image->depth); - header.mipMapCount = _byteswap_ulong(image->texture.basemap->GetLevelCount()); + const uint32_t faceCount = image->mapType == MAPTYPE_CUBE ? 6u : 1u; + uint32_t BaseSize = + image::xenos_texture::CalculateBaseSize(image->texture.basemap, image->width, image->height, faceCount); auto format = image->texture.basemap->Format.DataFormat; - switch (format) + uint32_t caps2 = 0u; + if (image->mapType == mp::MAPTYPE_CUBE) { - case GPUTEXTUREFORMAT_DXT1: - header.pixelFormat.fourCC = _byteswap_ulong(DXT1_FOURCC); - header.pitchOrLinearSize = BaseSize; - break; - case GPUTEXTUREFORMAT_DXT2_3: - header.pixelFormat.fourCC = _byteswap_ulong(DXT3_FOURCC); - header.pitchOrLinearSize = BaseSize; - break; - case GPUTEXTUREFORMAT_DXT4_5: - header.pixelFormat.fourCC = _byteswap_ulong(DXT5_FOURCC); - header.pitchOrLinearSize = BaseSize; - break; - case GPUTEXTUREFORMAT_DXN: - header.pixelFormat.fourCC = _byteswap_ulong(DXN_FOURCC); - header.pitchOrLinearSize = BaseSize; - break; - case GPUTEXTUREFORMAT_8: - header.pixelFormat.flags = _byteswap_ulong(DDPF_LUMINANCE); - header.pixelFormat.rgbBitCount = _byteswap_ulong(8); - header.pixelFormat.rBitMask = _byteswap_ulong(0x000000FF); - header.pixelFormat.gBitMask = 0; - header.pixelFormat.bBitMask = 0; - header.pixelFormat.aBitMask = 0; - header.pitchOrLinearSize = BaseSize; - break; - case GPUTEXTUREFORMAT_8_8: - header.pixelFormat.flags = _byteswap_ulong(DDPF_LUMINANCE | DDPF_ALPHAPIXELS); - header.pixelFormat.rgbBitCount = _byteswap_ulong(16); - header.pixelFormat.rBitMask = _byteswap_ulong(0x000000FF); - header.pixelFormat.gBitMask = _byteswap_ulong(0x0000FF00); - header.pixelFormat.bBitMask = 0; - header.pixelFormat.aBitMask = 0; - header.pitchOrLinearSize = BaseSize; - break; - case GPUTEXTUREFORMAT_8_8_8_8: - header.pixelFormat.flags = _byteswap_ulong(DDPF_RGB | DDPF_ALPHAPIXELS); - header.pixelFormat.rgbBitCount = _byteswap_ulong(32); - header.pixelFormat.rBitMask = _byteswap_ulong(0x00FF0000); - header.pixelFormat.gBitMask = _byteswap_ulong(0x0000FF00); - header.pixelFormat.bBitMask = _byteswap_ulong(0x000000FF); - header.pixelFormat.aBitMask = _byteswap_ulong(0xFF000000); - header.pitchOrLinearSize = BaseSize; - break; - default: - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported texture format %d!\n", format); - return; + caps2 = image::DDSCAPS2_CUBEMAP | image::DDSCAPS2_CUBEMAP_POSITIVEX | image::DDSCAPS2_CUBEMAP_NEGATIVEX | + image::DDSCAPS2_CUBEMAP_POSITIVEY | image::DDSCAPS2_CUBEMAP_NEGATIVEY | + image::DDSCAPS2_CUBEMAP_POSITIVEZ | image::DDSCAPS2_CUBEMAP_NEGATIVEZ; } - // Set texture capabilities - header.caps = _byteswap_ulong(DDSCAPS_TEXTURE | DDSCAPS_MIPMAP); - - // Handle Cubemaps - if (image->mapType == mp::MAPTYPE_CUBE) + image::DDS_HEADER header; + if (!image::CreateDdsHeader(header, image->width, image->height, image->depth, + image::xenos_texture::GetTextureLevelCount(image->texture.basemap), BaseSize, + image::DDSCAPS_TEXTURE | image::DDSCAPS_MIPMAP, caps2, format)) { - header.caps2 = _byteswap_ulong(DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX | DDSCAPS2_CUBEMAP_NEGATIVEX | - DDSCAPS2_CUBEMAP_POSITIVEY | DDSCAPS2_CUBEMAP_NEGATIVEY | - DDSCAPS2_CUBEMAP_POSITIVEZ | DDSCAPS2_CUBEMAP_NEGATIVEZ); + Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': unsupported texture format %d\n", image->name, format); + return; } - std::string filename = std::string(DUMP_DIR) + "\\" + "images"; - std::string sanitized_name = image->name; - - // Remove invalid characters - sanitized_name.erase(std::remove_if(sanitized_name.begin(), sanitized_name.end(), [](char c) { return c == '*'; }), - sanitized_name.end()); - - filename += "\\" + sanitized_name + ".dds"; + const std::string filename = GetImageDumpPath(image->name); - std::ofstream file(filename, std::ios::binary); + std::ofstream file(filename.c_str(), std::ios::binary); if (!file) { - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Failed to open file: %s\n", filename.c_str()); + Com_PrintError(CON_CHANNEL_ERROR, "Could not create DDS for image '%s': %s\n", image->name, filename.c_str()); return; } if (image->mapType == MAPTYPE_CUBE) { - file.write(reinterpret_cast(&header), sizeof(DDSHeader)); + image::WriteDdsHeader(file, header); - unsigned int face_size = 0; - unsigned int rowPitch = 0; const GPUTEXTUREFORMAT format = static_cast(image->texture.basemap->Format.DataFormat); - - switch (format) + const uint32_t linearFaceSize = + image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0u, format); + const uint32_t tiledFaceSize = image::xenos_texture::CalculateTiledLevelSize( + image->width, image->height, 0u, format, image->texture.basemap->Format.Pitch); + const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(image->width, 0u, format); + if (linearFaceSize == 0 || tiledFaceSize == 0 || rowPitch == 0) { - case GPUTEXTUREFORMAT_DXT1: - face_size = (image->width / 4) * (image->height / 4) * 8; - rowPitch = (image->width / 4) * 8; // 8 bytes per 4x4 block - break; - case GPUTEXTUREFORMAT_8_8_8_8: - face_size = image->width * image->height * 4; - rowPitch = image->width * 4; // 4 bytes per pixel - break; - default: - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported cube map format %d!\n", format); + Com_PrintError(CON_CHANNEL_ERROR, "Skipping cubemap '%s': unsupported texture format %d\n", image->name, + format); return; } // TODO: handle mip levels per face for cubemaps for (int i = 0; i < 6; i++) { - unsigned char *face_pixels = image->pixels + (i * face_size); // Offset for each face + const size_t faceOffset = static_cast(i) * tiledFaceSize; + if (faceOffset + tiledFaceSize > image->baseSize) + { + Com_PrintError(CON_CHANNEL_ERROR, "Skipping cubemap '%s': pixel data is too small (have %u, need %u)\n", + image->name, image->baseSize, static_cast(faceOffset + tiledFaceSize)); + return; + } + + unsigned char *face_pixels = image->pixels + faceOffset; // Offset for each face - std::vector swappedFace(face_pixels, face_pixels + face_size); - GPUEndianSwapTexture(swappedFace, static_cast(image->texture.basemap->Format.Endian)); + std::vector swappedFace(face_pixels, face_pixels + tiledFaceSize); + image::xenos_texture::ApplyGpuEndian(swappedFace.data(), swappedFace.size(), + static_cast(image->texture.basemap->Format.Endian)); // Create buffer for linear texture data - std::vector linearFace(face_size); - - // Convert tiled texture to linear layout using XGUntileTextureLevel - XGUntileTextureLevel(image->width, // Width - image->height, // Height - 0, // Level (base level) - static_cast(format), // GpuFormat - 0, // Flags (no special flags) - linearFace.data(), // pDestination (linear output) - rowPitch, // RowPitch - nullptr, // pPoint (no offset) - swappedFace.data(), // pSource (tiled input) - nullptr // pRect (entire texture) - ); + std::vector linearFace(linearFaceSize); + + if (!image::xenos_texture::UntileTextureLevel( + image->width, image->height, 0, static_cast(format), image->texture.basemap->Format.Pitch, + linearFace.data(), linearFace.size(), rowPitch, swappedFace.data(), swappedFace.size())) + { + Com_PrintError(CON_CHANNEL_ERROR, "Could not decode cubemap '%s' face %d\n", image->name, i); + return; + } file.write(reinterpret_cast(linearFace.data()), linearFace.size()); } file.close(); + Com_Printf(CON_CHANNEL_CONSOLEONLY, "Dumped image '%s'\n", image->name); } else if (image->mapType == MAPTYPE_2D) { // TODO: write mip levels - file.write(reinterpret_cast(&header), sizeof(DDSHeader)); + image::WriteDdsHeader(file, header); - std::vector pixelData(image->pixels, image->pixels + image->baseSize); + auto format = image->texture.basemap->Format.DataFormat; + const uint32_t linearLevelSize = + image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0u, format); + const uint32_t tiledLevelSize = image::xenos_texture::CalculateTiledLevelSize( + image->width, image->height, 0u, format, image->texture.basemap->Format.Pitch); + const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(image->width, 0u, format); + if (linearLevelSize == 0 || tiledLevelSize == 0 || rowPitch == 0) + { + Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': unsupported texture format %d\n", image->name, + format); + return; + } - GPUEndianSwapTexture(pixelData, static_cast(image->texture.basemap->Format.Endian)); + if (image->baseSize < tiledLevelSize) + { + Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': pixel data is too small (have %u, need %u)\n", + image->name, image->baseSize, tiledLevelSize); + return; + } - // Create a linear data buffer to hold the untiled texture - std::vector linearData(image->baseSize); + std::vector pixelData(image->pixels, image->pixels + tiledLevelSize); - // Calculate row pitch based on format - UINT rowPitch; - auto format = image->texture.basemap->Format.DataFormat; + image::xenos_texture::ApplyGpuEndian(pixelData.data(), pixelData.size(), + static_cast(image->texture.basemap->Format.Endian)); + + // Create a linear data buffer to hold the untiled texture + std::vector linearData(linearLevelSize); - switch (format) + if (!image::xenos_texture::UntileTextureLevel(image->width, image->height, 0, static_cast(format), + image->texture.basemap->Format.Pitch, linearData.data(), + linearData.size(), rowPitch, pixelData.data(), pixelData.size())) { - case GPUTEXTUREFORMAT_DXT1: - case GPUTEXTUREFORMAT_DXT2_3: - case GPUTEXTUREFORMAT_DXT4_5: - case GPUTEXTUREFORMAT_DXN: - // Block compressed formats use 4x4 blocks - rowPitch = ((image->width + 3) / 4) * (format == GPUTEXTUREFORMAT_DXT1 ? 8 : 16); - break; - case GPUTEXTUREFORMAT_8: - rowPitch = image->width; - break; - case GPUTEXTUREFORMAT_8_8: - rowPitch = image->width * 2; - break; - case GPUTEXTUREFORMAT_8_8_8_8: - rowPitch = image->width * 4; - break; - default: - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported texture format %d!\n", format); + Com_PrintError(CON_CHANNEL_ERROR, "Could not decode image '%s'\n", image->name); return; } - DbgPrint("Image_Dump: rowPitch=%d\n", rowPitch); - - // Call XGUntileTextureLevel to convert the tiled texture to linear format - XGUntileTextureLevel(image->width, // Width - image->height, // Height - 0, // Level (base level 0) - static_cast(format), // GpuFormat - XGTILE_NONPACKED, // Flags (no special flags) - linearData.data(), // pDestination - rowPitch, // RowPitch (calculated based on format) - nullptr, // pPoint (no offset) - pixelData.data(), // pSource - nullptr // pRect (entire texture) - ); - file.write(reinterpret_cast(linearData.data()), linearData.size()); file.close(); + Com_Printf(CON_CHANNEL_CONSOLEONLY, "Dumped image '%s'\n", image->name); } else { - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported map type %d!\n", image->mapType); + Com_PrintError(CON_CHANNEL_ERROR, "Skipping image '%s': unsupported map type %d\n", image->name, + image->mapType); return; } } @@ -620,379 +404,250 @@ void Cmd_imagedump() CreateDirectoryA(DUMP_DIR, 0); CreateDirectoryA((std::string(DUMP_DIR) + "\\images").c_str(), 0); - CreateDirectoryA((std::string(DUMP_DIR) + "\\highmip").c_str(), 0); + const std::map highMipFiles = CollectHighMipFiles(); for (unsigned int i = 0; i < imageList.count; i++) { auto image = imageList.image[i]; - Image_DbgPrint(image); + if (image == NULL) + continue; - Image_Dump(image); + Image_Dump(image, FindHighMipPathForImage(highMipFiles, image->name)); } +} - auto highmips = filesystem::list_files_in_directory("D:\\highmip"); - for (size_t i = 0; i < highmips.size(); ++i) +bool Image_Replace_2D(GfxImage *image, const image::DdsImage &ddsImage, uint32_t ddsFirstMipLevel) +{ + if (image->mapType != MAPTYPE_2D) { - const std::string &filepath = "D:\\highmip\\" + highmips[i]; - Com_Printf(CON_CHANNEL_CONSOLEONLY, "Dumping highmip file: %s\n", filepath.c_str()); - std::string assetName = extract_filename(filepath.c_str()); - auto asset = DB_FindXAssetEntry(ASSET_TYPE_IMAGE, assetName.c_str()); - if (!asset) - { - Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' not found in asset list!\n", assetName.c_str()); - continue; - } + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a 2D map!\n", image->name); + return false; + } - auto image = asset->entry.asset.header.image; + const GPUTEXTUREFORMAT format = static_cast(image->texture.basemap->Format.DataFormat); + const uint32_t levelCount = image::xenos_texture::GetTextureLevelCount(image->texture.basemap); + const uint32_t mipTailBaseLevel = image->texture.basemap->Format.PackedMips != 0 + ? image::xenos_texture::GetMipTailBaseLevel(image->width, image->height) + : levelCount; + const uint32_t ddsMipCount = ddsImage.GetMipCount(); + if (ddsFirstMipLevel >= ddsMipCount) + { + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' replacement DDS has no mip %u: mipCount=%u\n", image->name, + ddsFirstMipLevel, ddsMipCount); + return false; + } - std::ifstream input_file(filepath, - std::ios::binary | std::ios::ate); // Open file in binary mode and seek to end - if (!input_file) - { - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Failed to open file: %s\n", filepath.c_str()); - continue; - } + const uint32_t replaceLevelCount = min(levelCount, ddsMipCount - ddsFirstMipLevel); + const uint32_t nonPackedLevelCount = max(1u, min(replaceLevelCount, mipTailBaseLevel)); + unsigned char *baseData = image::xenos_texture::GetTextureBase(image->texture.basemap, image->pixels); + unsigned char *mipData = image::xenos_texture::GetTextureMipBase(image->texture.basemap, baseData, image->width, + image->height, format, 1u); - std::streamsize size = input_file.tellg(); - if (size < 0) + size_t requiredDDSSize = 0; + size_t requiredTextureBytes = 0; + if (!Validate2DReplacementData(image, ddsImage, format, ddsFirstMipLevel, nonPackedLevelCount, &requiredDDSSize, + &requiredTextureBytes)) + { + if (requiredDDSSize == 0) { - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Failed to determine file size: %s\n", filepath.c_str()); - continue; + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has unsupported replacement format %d!\n", image->name, + format); } - - input_file.seekg(0, std::ios::beg); - std::vector buffer(static_cast(size)); - - if (input_file.read(reinterpret_cast(buffer.data()), size)) + else if (ddsImage.data.size() < requiredDDSSize) { - Com_Printf(CON_CHANNEL_CONSOLEONLY, "Read %d bytes from file.\n", size); + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' DDS data is too small: have=%u need=%u for %u mip levels\n", + image->name, static_cast(ddsImage.data.size()), + static_cast(requiredDDSSize), nonPackedLevelCount); } else { - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Error reading file: %s\n", filepath.c_str()); - continue; + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' replacement exceeds texture memory: have=%u need=%u\n", + image->name, static_cast(image->cardMemory.platform[0]), + static_cast(requiredTextureBytes)); } - auto width = image->width * 2; - auto height = image->height * 2; - auto baseSize = width * height * 4; - - DDSHeader header; - memset(&header, 0, sizeof(DDSHeader)); - - header.magic = _byteswap_ulong(DDS_MAGIC); - header.size = _byteswap_ulong(DDS_HEADER_SIZE); - header.flags = _byteswap_ulong(DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_LINEARSIZE); - header.width = _byteswap_ulong(width); - header.height = _byteswap_ulong(height); - header.depth = _byteswap_ulong(image->depth); - header.mipMapCount = _byteswap_ulong(1); - header.caps = _byteswap_ulong(DDSCAPS_TEXTURE); - header.pitchOrLinearSize = baseSize; - - auto format = image->texture.basemap->Format.DataFormat; - switch (format) - { - case GPUTEXTUREFORMAT_DXT1: - header.pixelFormat.fourCC = _byteswap_ulong(DXT1_FOURCC); - - break; - case GPUTEXTUREFORMAT_DXT2_3: - header.pixelFormat.fourCC = _byteswap_ulong(DXT3_FOURCC); - - break; - case GPUTEXTUREFORMAT_DXT4_5: - header.pixelFormat.fourCC = _byteswap_ulong(DXT5_FOURCC); - - break; - case GPUTEXTUREFORMAT_DXN: - header.pixelFormat.fourCC = _byteswap_ulong(DXN_FOURCC); - - break; - case GPUTEXTUREFORMAT_8: - header.pixelFormat.flags = _byteswap_ulong(DDPF_LUMINANCE); - header.pixelFormat.rgbBitCount = _byteswap_ulong(8); - header.pixelFormat.rBitMask = _byteswap_ulong(0x000000FF); - header.pixelFormat.gBitMask = 0; - header.pixelFormat.bBitMask = 0; - header.pixelFormat.aBitMask = 0; - - break; - case GPUTEXTUREFORMAT_8_8: - header.pixelFormat.flags = _byteswap_ulong(DDPF_LUMINANCE | DDPF_ALPHAPIXELS); - header.pixelFormat.rgbBitCount = _byteswap_ulong(16); - header.pixelFormat.rBitMask = _byteswap_ulong(0x000000FF); - header.pixelFormat.gBitMask = _byteswap_ulong(0x0000FF00); - header.pixelFormat.bBitMask = 0; - header.pixelFormat.aBitMask = 0; - - break; - case GPUTEXTUREFORMAT_8_8_8_8: - header.pixelFormat.flags = _byteswap_ulong(DDPF_RGB | DDPF_ALPHAPIXELS); - header.pixelFormat.rgbBitCount = _byteswap_ulong(32); - header.pixelFormat.rBitMask = _byteswap_ulong(0x00FF0000); - header.pixelFormat.gBitMask = _byteswap_ulong(0x0000FF00); - header.pixelFormat.bBitMask = _byteswap_ulong(0x000000FF); - header.pixelFormat.aBitMask = _byteswap_ulong(0xFF000000); - - break; - default: - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported texture format %d!\n", format); - return; - } - - // TODO: add sanity checks for format, size, etc. - // TODO: handle filenames with unsupported characters for Windows - - auto output_filepath = std::string(DUMP_DIR) + "\\highmip\\" + assetName + ".dds"; - - std::ofstream output_file(output_filepath, std::ios::binary); - if (!output_file) - { - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Failed to open file: %s\n", output_filepath.c_str()); - return; - } - - output_file.write(reinterpret_cast(&header), sizeof(DDSHeader)); - - GPUEndianSwapTexture(buffer, static_cast(image->texture.basemap->Format.Endian)); - - // Calculate row pitch based on format - UINT rowPitch; - - switch (format) - { - case GPUTEXTUREFORMAT_DXT1: - rowPitch = (width / 4) * 8; // 8 bytes per 4x4 block - break; - case GPUTEXTUREFORMAT_DXT2_3: - case GPUTEXTUREFORMAT_DXT4_5: - case GPUTEXTUREFORMAT_DXN: - rowPitch = (width / 4) * 16; // 16 bytes per 4x4 block - break; - case GPUTEXTUREFORMAT_8: - rowPitch = width; // 1 byte per pixel - break; - case GPUTEXTUREFORMAT_8_8: - rowPitch = width * 2; // 2 bytes per pixel - break; - case GPUTEXTUREFORMAT_8_8_8_8: - rowPitch = width * 4; // 4 bytes per pixel - break; - default: - rowPitch = width * 4; // Default to 4 bytes per pixel - break; - } - - // Create a buffer for linear texture data - std::vector linearData(buffer.size()); - std::vector bufferAsUint8(buffer.begin(), buffer.end()); - - // Convert tiled texture to linear layout - XGUntileTextureLevel(width, // Width - height, // Height - 0, // Level (base level) - static_cast(format), // GpuFormat - 0, // Flags (no special flags) - linearData.data(), // pDestination (linear output) - rowPitch, // RowPitch - nullptr, // pPoint (no offset) - bufferAsUint8.data(), // pSource (tiled input) - nullptr // pRect (entire texture) - ); - - output_file.write(reinterpret_cast(linearData.data()), linearData.size()); - output_file.close(); - - Com_Printf(CON_CHANNEL_CONSOLEONLY, "Dumped highmip file: %s\n", output_filepath.c_str()); + return false; } -} -void Image_Replace_2D(GfxImage *image, const DDSImage &ddsImage) -{ - if (image->mapType != MAPTYPE_2D) + if (baseData == NULL || mipData == NULL) { - Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a 2D map!\n", image->name); - return; + Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_2D: Image '%s' has no valid texture memory!\n", image->name); + return false; } - // Get base texture layout - UINT baseAddress, baseSize, mipAddress, mipSize; - - XGGetTextureLayout(image->texture.basemap, &baseAddress, &baseSize, 0, 0, 0, &mipAddress, &mipSize, 0, 0, 0); - - XGTEXTURE_DESC TextureDesc; - XGGetTextureDesc(image->texture.basemap, 0, &TextureDesc); - - UINT mipTailBaseLevel = - XGGetMipTailBaseLevel(TextureDesc.Width, TextureDesc.Height, XGIsBorderTexture(image->texture.basemap)); + uint32_t ddsOffset = + image::CalculateDdsMipOffset(ddsImage.header.dwWidth, ddsImage.header.dwHeight, format, ddsFirstMipLevel); - UINT ddsOffset = 0; - - for (UINT mipLevel = 0; mipLevel < mipTailBaseLevel; mipLevel++) + for (uint32_t localMipLevel = 0; localMipLevel < nonPackedLevelCount; localMipLevel++) { - UINT widthInBlocks = max(1, TextureDesc.WidthInBlocks >> mipLevel); - UINT rowPitch = widthInBlocks * TextureDesc.BytesPerBlock; - // UINT levelSize = rowPitch * heightInBlocks; - UINT ddsMipLevelSize = - CalculateMipLevelSize(image->width, image->height, mipLevel, - static_cast(image->texture.basemap->Format.DataFormat)); - - if (ddsMipLevelSize == 0) + const uint32_t ddsMipLevel = ddsFirstMipLevel + localMipLevel; + uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(ddsImage.header.dwWidth, ddsMipLevel, format); + uint32_t ddsMipLevelSize = image::xenos_texture::CalculateLinearLevelSize( + ddsImage.header.dwWidth, ddsImage.header.dwHeight, ddsMipLevel, format); + uint32_t tiledMipLevelSize = image::xenos_texture::CalculateTiledLevelSize( + image->width, image->height, localMipLevel, format, image->texture.basemap->Format.Pitch); + + if (ddsMipLevelSize == 0 || tiledMipLevelSize == 0 || rowPitch == 0) { - DbgPrint(" [ERROR] Unsupported format %d for mip level %u! Skipping...\n", - image->texture.basemap->Format.DataFormat, mipLevel); - break; + Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_2D: Unsupported format %d for image '%s' mip level %u\n", + image->texture.basemap->Format.DataFormat, image->name, localMipLevel); + return false; } // Ensure we're not reading out of bounds if (ddsOffset + ddsMipLevelSize > ddsImage.data.size()) { - DbgPrint(" [ERROR] Mip Level %u exceeds DDS data size! Skipping...\n", mipLevel); - break; + Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_2D: Image '%s' mip level %u exceeds DDS data size\n", + image->name, ddsMipLevel); + return false; } std::vector levelData(ddsImage.data.begin() + ddsOffset, ddsImage.data.begin() + ddsOffset + ddsMipLevelSize); - GPUEndianSwapTexture(levelData, static_cast(image->texture.basemap->Format.Endian)); - - DbgPrint("Image_Replace_2D: Mip Level %d - Row Pitch=%u\n", mipLevel, rowPitch); + image::xenos_texture::ApplyGpuEndian(levelData.data(), levelData.size(), + static_cast(image->texture.basemap->Format.Endian)); - UINT address = baseAddress; - if (mipLevel > 0) + unsigned char *destination = baseData; + if (localMipLevel > 0) { - UINT mipLevelOffset = XGGetMipLevelOffset(image->texture.basemap, 0, mipLevel); - address = mipAddress + mipLevelOffset; + destination = mipData + image::xenos_texture::CalculateMipLevelOffset(image->width, image->height, + localMipLevel, format, 1u); } - DbgPrint("Image_Replace_2D: Writing mip level %d to address 0x%08X - levelSize=%u\n", mipLevel, address, - ddsMipLevelSize); + std::vector tiledData(tiledMipLevelSize); + + if (!image::xenos_texture::TileTextureLevel(image->width, image->height, localMipLevel, format, + image->texture.basemap->Format.Pitch, tiledData.data(), + tiledData.size(), levelData.data(), levelData.size(), rowPitch)) + { + Com_PrintError( + CON_CHANNEL_ERROR, + "Image_Replace_2D: Failed to tile mip level %d for image '%s' rowPitch=%u sourceSize=%u destSize=%u\n", + localMipLevel, image->name, rowPitch, static_cast(levelData.size()), tiledMipLevelSize); + return false; + } - // // Write the base level - XGTileTextureLevel(TextureDesc.Width, TextureDesc.Height, mipLevel, image->texture.basemap->Format.DataFormat, - XGTILE_NONPACKED, // Use non-packed mode (likely required for this texture) - reinterpret_cast(address), // Destination (tiled GPU memory for Base) - nullptr, // No offset (tile the whole image) - levelData.data(), // Source mip level data - rowPitch, // Row pitch of source image (should match DDS format) - nullptr // No subrectangle (tile the full image) - ); + memcpy(destination, tiledData.data(), tiledMipLevelSize); ddsOffset += ddsMipLevelSize; } + + return true; } -void Image_Replace_Cube(GfxImage *image, const DDSImage &ddsImage) +bool Image_Replace_Cube(GfxImage *image, const image::DdsImage &ddsImage) { if (image->mapType != MAPTYPE_CUBE) { Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a cube map!\n", image->name); - return; + return false; } const GPUTEXTUREFORMAT format = static_cast(image->texture.basemap->Format.DataFormat); - // Check can we get the base size here and /6 for face size - unsigned int face_size = 0; - unsigned int rowPitch = 0; + unsigned int face_size = image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0, format); + unsigned int rowPitch = image::xenos_texture::CalculateLinearRowPitch(image->width, 0, format); + unsigned int tiledFaceSize = image::xenos_texture::CalculateTiledLevelSize(image->width, image->height, 0, format, + image->texture.basemap->Format.Pitch); + unsigned int tiledBaseSize = + image::xenos_texture::CalculateBaseSize(image->texture.basemap, image->width, image->height, 6u); + unsigned char *baseData = image::xenos_texture::GetTextureBase(image->texture.basemap, image->pixels); - switch (format) + if (face_size == 0 || rowPitch == 0 || tiledFaceSize == 0 || tiledBaseSize < tiledFaceSize * 6u) { - case GPUTEXTUREFORMAT_DXT1: - face_size = (image->width / 4) * (image->height / 4) * 8; - rowPitch = (image->width / 4) * 8; // 8 bytes per 4x4 block - break; - case GPUTEXTUREFORMAT_8_8_8_8: - face_size = image->width * image->height * 4; - rowPitch = image->width * 4; // 4 bytes per pixel - break; - default: Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has unsupported format %d!\n", image->name, format); - return; + return false; + } + + if (baseData == NULL) + { + Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_Cube: Image '%s' has no valid texture memory!\n", image->name); + return false; + } + + size_t requiredDDSSize = 0; + if (!ValidateCubeReplacementData(image, ddsImage, format, face_size, tiledBaseSize, &requiredDDSSize)) + { + if (requiredDDSSize == 0) + { + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has unsupported cube replacement format %d!\n", image->name, + format); + } + else if (ddsImage.data.size() < requiredDDSSize) + { + Com_PrintError(CON_CHANNEL_ERROR, + "Image_Replace_Cube: Image '%s' DDS is too small for 6 faces: have=%u need=%u\n", + image->name, static_cast(ddsImage.data.size()), + static_cast(requiredDDSSize)); + } + else + { + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' cube replacement exceeds texture memory: have=%u need=%u\n", + image->name, static_cast(image->cardMemory.platform[0]), tiledBaseSize); + } + + return false; } for (int i = 0; i < 6; i++) { const unsigned char *face_pixels = ddsImage.data.data() + (i * face_size); + unsigned char *face_destination = baseData + (i * tiledFaceSize); // Create a buffer for the tiled texture data - std::vector tiledData(face_size); - - // Convert the linear texture to tiled format using XGTileTextureLevel - XGTileTextureLevel(image->width, // Width - image->height, // Height - 0, // Level (base level) - static_cast(format), // GpuFormat - 0, // Flags (no special flags) - tiledData.data(), // pDestination (tiled output) - nullptr, // pPoint (no offset) - face_pixels, // pSource (linear input) - rowPitch, // RowPitch - nullptr // pRect (entire texture) - ); - - GPUEndianSwapTexture(tiledData, static_cast(image->texture.basemap->Format.Endian)); + std::vector tiledData(tiledFaceSize); + + if (!image::xenos_texture::TileTextureLevel(image->width, image->height, 0, static_cast(format), + image->texture.basemap->Format.Pitch, tiledData.data(), + tiledData.size(), face_pixels, face_size, rowPitch)) + { + Com_PrintError( + CON_CHANNEL_ERROR, + "Image_Replace_Cube: Failed to tile image '%s' face %d rowPitch=%u sourceSize=%u destSize=%u\n", + image->name, i, rowPitch, face_size, tiledFaceSize); + return false; + } + + image::xenos_texture::ApplyGpuEndian(tiledData.data(), tiledData.size(), + static_cast(image->texture.basemap->Format.Endian)); // Copy the data to the image - memcpy(image->pixels + (i * face_size), tiledData.data(), face_size); + memcpy(face_destination, tiledData.data(), tiledFaceSize); } + + return true; } void Image_Replace(GfxImage *image) { - const std::string replacement_base_dir = Config::GetModBasePath() + "\\images"; - const std::string replacement_path = replacement_base_dir + "\\" + image->name + ".dds"; + const std::string replacement_path = GetImageReplacementPath(image->name); if (!filesystem::file_exists(replacement_path)) { - Com_PrintError(CON_CHANNEL_ERROR, "File does not exist: %s\n", replacement_path.c_str()); return; } - StaticDDSImage staticDDSImage = ReadDDSFileStatic(replacement_path.c_str()); - if (staticDDSImage.data == nullptr) - { - Com_PrintError(CON_CHANNEL_ERROR, "Failed to load DDS file: %s\n", replacement_path.c_str()); - return; - } - - DbgPrint("[codxe][image_loader] DDS loaded for isolation: %s bytes=%u\n", replacement_path.c_str(), - static_cast(staticDDSImage.dataSize)); - return; - - DDSImage ddsImage = ReadDDSFile(replacement_path.c_str()); + image::DdsImage ddsImage = image::LoadDdsFromFile(replacement_path.c_str()); if (ddsImage.data.empty()) { Com_PrintError(CON_CHANNEL_ERROR, "Failed to load DDS file: %s\n", replacement_path.c_str()); return; } - if (image->width != ddsImage.header.width || image->height != ddsImage.header.height) + if (ddsImage.header.dwSize != image::DDS_HEADER_SIZE || + ddsImage.header.ddspf.dwSize != image::DDS_PIXEL_FORMAT_SIZE) { - Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' dimensions do not match DDS file: %s\n", image->name, - replacement_path.c_str()); + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has an invalid DDS header: size=%u pixelFormatSize=%u\n", + image->name, ddsImage.header.dwSize, ddsImage.header.ddspf.dwSize); return; } GPUTEXTUREFORMAT ddsFormat; - switch (ddsImage.header.pixelFormat.fourCC) + if (!ddsImage.GetGpuFormat(&ddsFormat)) { - case DXT1_FOURCC: - ddsFormat = GPUTEXTUREFORMAT_DXT1; - break; - case DXT3_FOURCC: - ddsFormat = GPUTEXTUREFORMAT_DXT2_3; - break; - case DXT5_FOURCC: - ddsFormat = GPUTEXTUREFORMAT_DXT4_5; - break; - case DXN_FOURCC: - ddsFormat = GPUTEXTUREFORMAT_DXN; - break; - default: - Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has an unsupported DDS format: 0x%X\n", image->name, - ddsImage.header.pixelFormat.fourCC); + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has an unsupported DDS format: flags=0x%X fourCC=0x%X\n", + image->name, ddsImage.header.ddspf.dwFlags, ddsImage.header.ddspf.dwFourCC); return; } @@ -1004,23 +659,100 @@ void Image_Replace(GfxImage *image) return; } + const bool ddsIsCubemap = ddsImage.IsCubemap(); + const bool ddsMatchesImageDimensions = + image->width == ddsImage.header.dwWidth && image->height == ddsImage.header.dwHeight; + const bool ddsMatchesStreamDimensions = image->streaming && image->mapType == MAPTYPE_2D && !ddsIsCubemap && + ddsImage.header.dwWidth == static_cast(image->width) * 2u && + ddsImage.header.dwHeight == static_cast(image->height) * 2u; + uint32_t ddsFirstMipLevel = 0; + + if (image->mapType == MAPTYPE_2D && ddsIsCubemap) + { + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is 2D but replacement DDS is a cubemap!\n", image->name); + return; + } + + if (image->streaming && image->mapType == MAPTYPE_2D) + { + if (!ddsMatchesStreamDimensions) + { + Com_PrintError(CON_CHANNEL_ERROR, + "Streamed image '%s' replacement must include the streamed mip: expected=%ux%u got=%ux%u " + "%s\n", + image->name, static_cast(image->width) * 2u, + static_cast(image->height) * 2u, ddsImage.header.dwWidth, ddsImage.header.dwHeight, + replacement_path.c_str()); + return; + } + + const uint32_t ddsMipCount = ddsImage.GetMipCount(); + if (ddsMipCount < 2u) + { + Com_PrintError(CON_CHANNEL_ERROR, + "Image '%s' replacement DDS starts at the streamed mip but has no resident mip: " + "%ux%u mipCount=%u\n", + image->name, ddsImage.header.dwWidth, ddsImage.header.dwHeight, ddsMipCount); + return; + } + + ddsFirstMipLevel = 1u; + } + else if (!ddsMatchesImageDimensions) + { + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' dimensions do not match DDS file: image=%ux%u dds=%ux%u %s\n", + image->name, image->width, image->height, ddsImage.header.dwWidth, ddsImage.header.dwHeight, + replacement_path.c_str()); + return; + } + + if (image->mapType == MAPTYPE_CUBE && !ddsIsCubemap) + { + const uint32_t faceSize = + image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0, ddsFormat); + if (faceSize == 0 || ddsImage.data.size() < static_cast(faceSize) * 6u) + { + Com_PrintError(CON_CHANNEL_ERROR, + "Image '%s' is a cubemap but replacement DDS is not a valid 6-face cubemap!\n", image->name); + return; + } + + Com_Printf(CON_CHANNEL_CONSOLEONLY, + "Image '%s' replacement DDS has no cubemap caps but contains enough data for 6 sequential faces; " + "accepting it.\n", + image->name); + } + + bool replaced = false; if (image->mapType == MAPTYPE_2D) { - Image_Replace_2D(image, ddsImage); + replaced = Image_Replace_2D(image, ddsImage, ddsFirstMipLevel); } else if (image->mapType == MAPTYPE_CUBE) { - Image_Replace_Cube(image, ddsImage); + replaced = Image_Replace_Cube(image, ddsImage); } else { Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a 2D or cube map!\n", image->name); return; } + + if (replaced) + { + if (ddsFirstMipLevel > 0) + { + g_streamedImageReplacements.insert(image->name); + } + + Com_Printf(CON_CHANNEL_CONSOLEONLY, "Replaced image '%s'\n", image->name); + } } void Load_images() { + g_streamedImageReplacements.clear(); + const int MAX_IMAGES = 2048; XAssetHeader assets[MAX_IMAGES]; const auto count = DB_GetAllXAssetOfType_FastFile(ASSET_TYPE_IMAGE, assets, MAX_IMAGES); @@ -1051,50 +783,142 @@ bool R_StreamLoadHighMipReplacement(const char *filename, unsigned int bytesToRe auto image = asset->entry.asset.header.image; - // Use new ConfigModule API - std::string replacement_path = Config::GetModBasePath() + "\\highmip" + "\\" + asset_name + ".dds"; - std::ifstream file(replacement_path, std::ios::binary | std::ios::ate); - if (!file) + if (image == NULL || image->texture.basemap == NULL || image->mapType != MAPTYPE_2D) { return false; } - std::streamsize file_size = file.tellg(); - file.seekg(0, std::ios::beg); + const uint32_t highMipWidth = static_cast(image->width) * 2u; + const uint32_t highMipHeight = static_cast(image->height) * 2u; + const bool blockStockStream = g_streamedImageReplacements.find(asset_name) != g_streamedImageReplacements.end(); - if (file_size - 0x80 != bytesToRead) // 0x80 is the size of the DDS header + if (!blockStockStream) { - Com_PrintError(CON_CHANNEL_ERROR, "R_StreamLoadHighMipReplacement: File size mismatch: %s\n", replacement_path); return false; } - std::vector ddsHeader(0x80); - file.read(reinterpret_cast(ddsHeader.data()), 0x80); + const auto tryReplaceHighMipDDS = [&](const std::string &replacement_path, bool quietDimensionMismatch) -> bool + { + image::DdsImage ddsImage = image::LoadDdsFromFile(replacement_path.c_str()); + if (ddsImage.data.empty()) + { + Com_PrintError(CON_CHANNEL_ERROR, "R_StreamLoadHighMipReplacement: Failed to load DDS file: %s\n", + replacement_path.c_str()); + return false; + } + + if (ddsImage.header.dwSize != image::DDS_HEADER_SIZE || + ddsImage.header.ddspf.dwSize != image::DDS_PIXEL_FORMAT_SIZE) + { + Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadHighMipReplacement: Image '%s' has an invalid DDS header: size=%u " + "pixelFormatSize=%u\n", + asset_name.c_str(), ddsImage.header.dwSize, ddsImage.header.ddspf.dwSize); + return false; + } + + GPUTEXTUREFORMAT ddsFormat; + if (!ddsImage.GetGpuFormat(&ddsFormat)) + { + Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadHighMipReplacement: Image '%s' has an unsupported DDS format: flags=0x%X " + "fourCC=0x%X\n", + asset_name.c_str(), ddsImage.header.ddspf.dwFlags, ddsImage.header.ddspf.dwFourCC); + return false; + } + + if (static_cast(image->texture.basemap->Format.DataFormat) != static_cast(ddsFormat)) + { + Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadHighMipReplacement: Image '%s' format does not match DDS: expected=%u " + "got=%u\n", + asset_name.c_str(), static_cast(image->texture.basemap->Format.DataFormat), + static_cast(ddsFormat)); + return false; + } + + if (ddsImage.header.dwWidth != highMipWidth || ddsImage.header.dwHeight != highMipHeight) + { + if (!quietDimensionMismatch) + { + Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadHighMipReplacement: Image '%s' dimensions do not match streamed mip: " + "expected=%ux%u got=%ux%u\n", + asset_name.c_str(), highMipWidth, highMipHeight, ddsImage.header.dwWidth, + ddsImage.header.dwHeight); + } + + return false; + } + + const uint32_t ddsMipCount = ddsImage.GetMipCount(); + if (ddsMipCount < 2u) + { + Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadHighMipReplacement: Image '%s' replacement DDS must include stream and " + "resident mips: mipCount=%u\n", + asset_name.c_str(), ddsMipCount); + return false; + } + + const uint32_t sourceSize = image::xenos_texture::CalculateLinearLevelSize( + ddsImage.header.dwWidth, ddsImage.header.dwHeight, 0u, ddsFormat); + const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(ddsImage.header.dwWidth, 0u, ddsFormat); + const uint32_t tiledSize = image::xenos_texture::CalculateTiledLevelSize( + ddsImage.header.dwWidth, ddsImage.header.dwHeight, 0u, ddsFormat, 0u); + + if (sourceSize == 0 || rowPitch == 0 || tiledSize == 0) + { + Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadHighMipReplacement: Image '%s' has unsupported replacement format %u\n", + asset_name.c_str(), static_cast(ddsFormat)); + return false; + } + + if (ddsImage.data.size() < sourceSize) + { + Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadHighMipReplacement: Image '%s' DDS data is too small: have=%u need=%u\n", + asset_name.c_str(), static_cast(ddsImage.data.size()), sourceSize); + return false; + } - // TODO: check if file is DDS and has correct format and dimensions + if (tiledSize != bytesToRead) + { + Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadHighMipReplacement: Image '%s' stream size mismatch: expected=%u got=%u\n", + asset_name.c_str(), bytesToRead, tiledSize); + return false; + } - std::vector buffer; - buffer.resize(static_cast(bytesToRead)); - file.read(reinterpret_cast(buffer.data()), bytesToRead); + std::vector buffer(ddsImage.data.begin(), ddsImage.data.begin() + sourceSize); + image::xenos_texture::ApplyGpuEndian(buffer.data(), buffer.size(), + static_cast(image->texture.basemap->Format.Endian)); - GPUEndianSwapTexture(buffer, static_cast(image->texture.basemap->Format.Endian)); + if (!image::xenos_texture::TileTextureLevel(ddsImage.header.dwWidth, ddsImage.header.dwHeight, 0u, ddsFormat, + 0u, outData, bytesToRead, buffer.data(), buffer.size(), rowPitch)) + { + Com_PrintError(CON_CHANNEL_ERROR, "R_StreamLoadHighMipReplacement: Failed to tile image '%s'\n", + asset_name.c_str()); + return false; + } - XGTEXTURE_DESC textureDesc; - XGGetTextureDesc(image->texture.basemap, 0, &textureDesc); + Com_Printf(CON_CHANNEL_CONSOLEONLY, "Replaced streamed image '%s'\n", asset_name.c_str()); + return true; + }; - // High mip are 2x the size of the original image - auto width = image->width * 2; - auto height = image->height * 2; - auto rowPitch = textureDesc.RowPitch * 2; + const std::string combined_path = GetImageReplacementPath(asset_name.c_str()); + if (filesystem::file_exists(combined_path) && tryReplaceHighMipDDS(combined_path, true)) + { + return true; + } - XGTileTextureLevel(width, height, 0, image->texture.basemap->Format.DataFormat, - XGTILE_NONPACKED, // Use non-packed mode (likely required for this texture) - outData, // Destination (tiled GPU memory for Base) - nullptr, // No offset (tile the whole image) - buffer.data(), // Source mip level data - rowPitch, // Row pitch of source image (should match DDS format) - nullptr // No subrectangle (tile the full image) - ); + Com_PrintError(CON_CHANNEL_ERROR, "R_StreamLoadHighMipReplacement: Blocking stock stream for replaced image '%s'\n", + asset_name.c_str()); + if (outData != NULL && bytesToRead > 0) + { + memset(outData, 0, bytesToRead); + } return true; } @@ -1108,7 +932,7 @@ int R_StreamLoadFileSynchronously_Hook(const char *filename, unsigned int bytesT return 1; } - // Fallback to original path if modified path failed + // Let the game handle stock streams only when we did not accept a streamed replacement for this image. return R_StreamLoadFileSynchronously_Detour.GetOriginal()( filename, bytesToRead, outData); } @@ -1119,7 +943,7 @@ image_loader::image_loader() CG_RegisterGraphics_Detour = Detour(CG_RegisterGraphics, CG_RegisterGraphics_Hook); CG_RegisterGraphics_Detour.Install(); - // Load highmip texture replacements from active mod folder + // Load streamed mip texture replacements from active mod folder R_StreamLoadFileSynchronously_Detour = Detour(R_StreamLoadFileSynchronously, R_StreamLoadFileSynchronously_Hook); R_StreamLoadFileSynchronously_Detour.Install(); diff --git a/src/game/iw3/mp/components/scr_parser.cpp b/src/game/iw3/mp/components/scr_parser.cpp index e7d6dbc0..f8bcf7c4 100644 --- a/src/game/iw3/mp/components/scr_parser.cpp +++ b/src/game/iw3/mp/components/scr_parser.cpp @@ -113,7 +113,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename, co archive); }; - if (Config::dump_rawfile) + if (Config::dump_assets) { char *contents = callOriginal(); if (contents != nullptr) diff --git a/src/game/iw3/sp/components/scr_parser.cpp b/src/game/iw3/sp/components/scr_parser.cpp index 47e12fb5..85810d59 100644 --- a/src/game/iw3/sp/components/scr_parser.cpp +++ b/src/game/iw3/sp/components/scr_parser.cpp @@ -15,7 +15,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename, co archive); }; - if (Config::dump_rawfile) + if (Config::dump_assets) { DbgPrint("GSCLoader: Dumping script %s\n", extFilename); auto contents = callOriginal(); diff --git a/src/game/iw4/mp_tu6/components/clipmap.cpp b/src/game/iw4/mp_tu6/components/clipmap.cpp index cf7a517a..99b3e468 100644 --- a/src/game/iw4/mp_tu6/components/clipmap.cpp +++ b/src/game/iw4/mp_tu6/components/clipmap.cpp @@ -6,10 +6,7 @@ iw4::mp_tu6::dvar_t *noclip_brushes = nullptr; std::vector original_brush_contents; -Detour DB_LinkXAssetEntry1_Detour; - -iw4::mp_tu6::XAssetEntryPoolEntry *DB_LinkXAssetEntry1_Hook(iw4::mp_tu6::XAssetType type, - iw4::mp_tu6::XAssetHeader *header) +void OnDBLinkXAssetPre(iw4::mp_tu6::XAssetType &type, iw4::mp_tu6::XAssetHeader *header) { // Register once // TODO: Move to dvar init event @@ -19,9 +16,6 @@ iw4::mp_tu6::XAssetEntryPoolEntry *DB_LinkXAssetEntry1_Hook(iw4::mp_tu6::XAssetT "noclip_brushes", "", 0x10, "Brush indices to disable playerclip. Use '*' for all, '' to restore"); } - iw4::mp_tu6::XAssetEntryPoolEntry *entry = - DB_LinkXAssetEntry1_Detour.GetOriginal()(type, header); - if (type == iw4::mp_tu6::ASSET_TYPE_CLIPMAP_MP) { // Resize the vector to match the number of brushes @@ -34,8 +28,6 @@ iw4::mp_tu6::XAssetEntryPoolEntry *DB_LinkXAssetEntry1_Hook(iw4::mp_tu6::XAssetT header->clipMap->brushContents[i]; // Assuming this is the field you want to save } } - - return entry; } void RestoreBrushContents() @@ -133,8 +125,7 @@ void DisablePlayerClipOnIntersectingBrushes(iw4::mp_tu6::scr_entref_t entref) clipmap::clipmap() { - DB_LinkXAssetEntry1_Detour = Detour(iw4::mp_tu6::DB_LinkXAssetEntry1, DB_LinkXAssetEntry1_Hook); - DB_LinkXAssetEntry1_Detour.Install(); + Events::OnDBLinkXAssetPre(OnDBLinkXAssetPre); iw4::mp_tu6::Scr_AddMethod("disableplayercliponintersectingbrushes", DisablePlayerClipOnIntersectingBrushes, iw4::mp_tu6::BUILTIN_ANY); @@ -179,5 +170,4 @@ clipmap::clipmap() clipmap::~clipmap() { - DB_LinkXAssetEntry1_Detour.Remove(); } diff --git a/src/game/iw4/mp_tu6/components/events.cpp b/src/game/iw4/mp_tu6/components/events.cpp index 70177444..3ea6eedb 100644 --- a/src/game/iw4/mp_tu6/components/events.cpp +++ b/src/game/iw4/mp_tu6/components/events.cpp @@ -63,6 +63,41 @@ void Events::OnCmdInit(const std::function &callback) Detour Events::Cmd_Init_Detour; +std::vector> + Events::db_linkxasset_pre_callbacks; +std::vector> Events::db_linkxasset_post_callbacks; +Detour Events::DB_LinkXAssetEntry1_Detour; + +iw4::mp_tu6::XAssetEntryPoolEntry *Events::DB_LinkXAssetEntry1_Hook(iw4::mp_tu6::XAssetType type, + iw4::mp_tu6::XAssetHeader *header) +{ + for (auto it = db_linkxasset_pre_callbacks.begin(); it != db_linkxasset_pre_callbacks.end(); ++it) + { + (*it)(type, header); + } + + iw4::mp_tu6::XAssetEntryPoolEntry *entry = + DB_LinkXAssetEntry1_Detour.GetOriginal()(type, header); + + for (auto it = db_linkxasset_post_callbacks.begin(); it != db_linkxasset_post_callbacks.end(); ++it) + { + (*it)(entry); + } + + return entry; +} + +void Events::OnDBLinkXAssetPre( + const std::function &callback) +{ + db_linkxasset_pre_callbacks.emplace_back(callback); +} + +void Events::OnDBLinkXAssetPost(const std::function &callback) +{ + db_linkxasset_post_callbacks.emplace_back(callback); +} + std::vector> Events::vmshutdown_callbacks; void Events::Scr_ShutdownSystem_Hook(unsigned __int8 sys) @@ -93,6 +128,9 @@ Events::Events() Cmd_Init_Detour = Detour(iw4::mp_tu6::Cmd_Init, Cmd_Init_Hook); Cmd_Init_Detour.Install(); + DB_LinkXAssetEntry1_Detour = Detour(iw4::mp_tu6::DB_LinkXAssetEntry1, DB_LinkXAssetEntry1_Hook); + DB_LinkXAssetEntry1_Detour.Install(); + Scr_ShutdownSystem_Detour = Detour(iw4::mp_tu6::Scr_ShutdownSystem, Scr_ShutdownSystem_Hook); Scr_ShutdownSystem_Detour.Install(); } @@ -102,10 +140,13 @@ Events::~Events() Com_InitDvars_Detour.Remove(); CG_DrawActive_Detour.Remove(); Cmd_Init_Detour.Remove(); + DB_LinkXAssetEntry1_Detour.Remove(); Scr_ShutdownSystem_Detour.Remove(); com_initdvars_callbacks.clear(); cg_drawactive_callbacks.clear(); cmdinit_callbacks.clear(); + db_linkxasset_pre_callbacks.clear(); + db_linkxasset_post_callbacks.clear(); vmshutdown_callbacks.clear(); } diff --git a/src/game/iw4/mp_tu6/components/events.h b/src/game/iw4/mp_tu6/components/events.h index aa3fff09..65c212c9 100644 --- a/src/game/iw4/mp_tu6/components/events.h +++ b/src/game/iw4/mp_tu6/components/events.h @@ -16,6 +16,9 @@ class Events : public Module static void OnDvarInit(const std::function &callback); static void OnCG_DrawActive(const std::function &callback); static void OnCmdInit(const std::function &callback); + static void OnDBLinkXAssetPre( + const std::function &callback); + static void OnDBLinkXAssetPost(const std::function &callback); static void OnVMShutdown(const std::function &callback); private: @@ -31,6 +34,13 @@ class Events : public Module static Detour Cmd_Init_Detour; static void Cmd_Init_Hook(); + static std::vector> + db_linkxasset_pre_callbacks; + static std::vector> db_linkxasset_post_callbacks; + static Detour DB_LinkXAssetEntry1_Detour; + static iw4::mp_tu6::XAssetEntryPoolEntry *DB_LinkXAssetEntry1_Hook(iw4::mp_tu6::XAssetType type, + iw4::mp_tu6::XAssetHeader *header); + static std::vector> vmshutdown_callbacks; static Detour Scr_ShutdownSystem_Detour; static void Scr_ShutdownSystem_Hook(unsigned __int8 sys); diff --git a/src/game/iw4/mp_tu6/components/image_loader.cpp b/src/game/iw4/mp_tu6/components/image_loader.cpp new file mode 100644 index 00000000..020c5065 --- /dev/null +++ b/src/game/iw4/mp_tu6/components/image_loader.cpp @@ -0,0 +1,1413 @@ +#include "pch.h" +#include "common/config.h" +#include "events.h" +#include "image_loader.h" +#include "image/dds_loader.h" +#include "image/dds_writer.h" +#include "image/texture_layout.h" +#include "image/xenos_texture.h" + +#include + +#ifndef INVALID_FILE_ATTRIBUTES +#define INVALID_FILE_ATTRIBUTES ((DWORD) - 1) +#endif + +#ifdef PtrToUint +#undef PtrToUint +#endif + +namespace +{ +namespace game = iw4::mp_tu6; + +const uint32_t STREAM_PIXEL_SIZE_MASK = 0x3FFFFFF; +const uint32_t MAX_STREAM_COMPRESSED_SIZE = 64u * 1024u * 1024u; +const uint32_t DDS_FILE_HEADER_SIZE = sizeof(uint32_t) + sizeof(image::DDS_HEADER); +const uint32_t REPLACEMENT_ROW_SCRATCH_SIZE = 64u * 1024u; + +typedef image::DdsImage DDSImage; + +struct ZlibStream +{ + unsigned __int8 *next_in; + unsigned int avail_in; + unsigned int total_in; + unsigned __int8 *next_out; + unsigned int avail_out; + unsigned int total_out; + char *msg; + void *state; + unsigned __int8 *(__fastcall *zalloc)(unsigned __int8 *opaque, unsigned int items, unsigned int size); + void(__fastcall *zfree)(unsigned __int8 *opaque, unsigned __int8 *ptr); + unsigned __int8 *opaque; + int data_type; +}; +static_assert(sizeof(ZlibStream) == 48, ""); + +game::dvar_t *dump_assets = nullptr; +unsigned char g_replacementRowScratch[REPLACEMENT_ROW_SCRATCH_SIZE]; + +struct DdsDataFile +{ + HANDLE handle; + std::string path; + uint32_t dataSize; + + DdsDataFile(const std::string &filePath, uint32_t fileDataSize) + : handle(CreateFileA(filePath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)), + path(filePath), dataSize(fileDataSize) + { + } + + ~DdsDataFile() + { + if (handle != INVALID_HANDLE_VALUE) + CloseHandle(handle); + } + + bool IsValid() const + { + return handle != INVALID_HANDLE_VALUE; + } + + bool SeekData(uint32_t dataOffset, uint32_t size) const + { + if (dataOffset > dataSize || size > dataSize - dataOffset) + { + game::Com_Printf(0, "DDS range outside file '%s': dataOffset=%u size=%u dataSize=%u\n", path.c_str(), + dataOffset, size, dataSize); + return false; + } + + const DWORD fileOffset = static_cast(DDS_FILE_HEADER_SIZE) + dataOffset; + SetLastError(NO_ERROR); + const DWORD seekResult = SetFilePointer(handle, fileOffset, nullptr, FILE_BEGIN); + if (seekResult == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR) + { + game::Com_Printf(0, "DDS seek failed '%s': offset=%u error=0x%08X\n", path.c_str(), fileOffset, + GetLastError()); + return false; + } + + return true; + } +}; + +struct DdsRowReadState +{ + DdsDataFile *file; + GPUENDIAN endian; +}; + +uint32_t PtrToUint(const void *ptr) +{ + return static_cast(reinterpret_cast(ptr)); +} + +void PrintImageError(const char *format, ...) +{ + char buffer[1024]; + va_list args; + va_start(args, format); + _vsnprintf(buffer, sizeof(buffer), format, args); + va_end(args); + buffer[sizeof(buffer) - 1] = '\0'; + + game::Com_Printf(0, "%s", buffer); +} + +void PrintImageInfo(const char *format, ...) +{ + char buffer[1024]; + va_list args; + va_start(args, format); + _vsnprintf(buffer, sizeof(buffer), format, args); + va_end(args); + buffer[sizeof(buffer) - 1] = '\0'; + + game::Com_Printf(0, "%s", buffer); +} + +bool ReadDdsTileRow(uint32_t rowIndex, unsigned char *rowBuffer, uint32_t rowPitch, void *userData) +{ + DdsRowReadState *state = static_cast(userData); + if (state == nullptr || state->file == nullptr || rowBuffer == nullptr || rowPitch == 0) + return false; + + DWORD bytesRead = 0; + if (!ReadFile(state->file->handle, rowBuffer, rowPitch, &bytesRead, nullptr) || bytesRead != rowPitch) + { + PrintImageError("DDS row read failed '%s': row=%u size=%u actual=%u error=0x%08X\n", + state->file->path.c_str(), rowIndex, rowPitch, bytesRead, GetLastError()); + return false; + } + + image::xenos_texture::ApplyGpuEndian(rowBuffer, rowPitch, state->endian); + return true; +} + +bool TileDdsLevelToTexture(DdsDataFile &ddsFile, uint32_t dataOffset, uint32_t expectedDataSize, + uint32_t sourceWidth, uint32_t sourceHeight, uint32_t sourceMipLevel, + uint32_t destinationWidth, uint32_t destinationHeight, uint32_t destinationMipLevel, + GPUTEXTUREFORMAT format, uint32_t basePitch, unsigned char *destination, + uint32_t destinationSize, GPUENDIAN endian) +{ + if (destination == nullptr || destinationSize == 0) + return false; + + const uint32_t sourceRowPitch = + image::xenos_texture::CalculateLinearRowPitch(sourceWidth, sourceMipLevel, format); + const uint32_t sourceLevelSize = + image::xenos_texture::CalculateLinearLevelSize(sourceWidth, sourceHeight, sourceMipLevel, format); + if (sourceRowPitch == 0 || sourceLevelSize == 0 || sourceLevelSize != expectedDataSize) + return false; + + if (sourceRowPitch > sizeof(g_replacementRowScratch)) + { + PrintImageError("DDS row scratch too small '%s': rowPitch=%u scratch=%u\n", ddsFile.path.c_str(), + sourceRowPitch, static_cast(sizeof(g_replacementRowScratch))); + return false; + } + + if (!ddsFile.SeekData(dataOffset, expectedDataSize)) + return false; + + DdsRowReadState rowState = {&ddsFile, endian}; + memset(destination, 0, destinationSize); + return image::xenos_texture::TileTextureLevelFromRows( + destinationWidth, destinationHeight, destinationMipLevel, format, basePitch, destination, destinationSize, + sourceRowPitch, g_replacementRowScratch, sizeof(g_replacementRowScratch), ReadDdsTileRow, &rowState); +} + +bool ImageFileExists(const std::string &path) +{ + const DWORD attributes = GetFileAttributesA(path.c_str()); + return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0; +} + +std::string GetReplacementDirectory() +{ + return Config::GetModBasePath() + "\\images"; +} + +std::string GetUserReplacementDirectory() +{ + return std::string(USERRAW_DIR) + "\\images"; +} + +bool ReadDDSHeader(const std::string &filepath, DDSImage *out, uint32_t *dataSize) +{ + if (out == nullptr) + return false; + + *out = DDSImage(); + return image::LoadDdsHeaderFromFile(filepath, &out->header, dataSize); +} + +std::string GetReplacementPath(const char *imageName) +{ + const std::string userPath = GetUserReplacementDirectory() + "\\" + imageName + ".dds"; + if (ImageFileExists(userPath)) + return userPath; + + return GetReplacementDirectory() + "\\" + imageName + ".dds"; +} + +bool ValidateDDSHeaderFields(const game::GfxImage *image, const DDSImage &ddsImage, GPUTEXTUREFORMAT *ddsFormat) +{ + if (ddsImage.header.dwSize != image::DDS_HEADER_SIZE || + ddsImage.header.ddspf.dwSize != image::DDS_PIXEL_FORMAT_SIZE) + { + PrintImageError("image '%s' has an invalid DDS header: size=%u pixelFormatSize=%u\n", image->name, + ddsImage.header.dwSize, ddsImage.header.ddspf.dwSize); + return false; + } + + if (!ddsImage.GetGpuFormat(ddsFormat)) + { + PrintImageError("image '%s' has an unsupported DDS format: flags=0x%X fourCC=0x%X bitCount=%u\n", image->name, + ddsImage.header.ddspf.dwFlags, ddsImage.header.ddspf.dwFourCC, + ddsImage.header.ddspf.dwRGBBitCount); + return false; + } + + const uint32_t imageFormat = image->texture.basemap.Format.DataFormat; + if (imageFormat != static_cast(*ddsFormat)) + { + PrintImageError("image '%s' format does not match DDS: expected=%u got=%u\n", image->name, imageFormat, + static_cast(*ddsFormat)); + return false; + } + + return true; +} + +bool Validate2DReplacementData(const game::GfxImage *image, GPUTEXTUREFORMAT format, size_t ddsDataSize, + uint32_t replacementLevelCount, size_t *requiredDDSSize, size_t *requiredTextureBytes) +{ + *requiredDDSSize = + image::CalculateRequiredLinearDataSize(image->width, image->height, format, 0u, replacementLevelCount, 1u); + if (*requiredDDSSize == 0) + return false; + + if (ddsDataSize < *requiredDDSSize) + return false; + + const D3DBaseTexture *texture = &image->texture.basemap; + const uint32_t baseSize = image::xenos_texture::CalculateBaseSize(texture, image->width, image->height, 1u); + const size_t mipBytes = + image::CalculateRequiredMipTextureBytes(image->width, image->height, format, 1u, replacementLevelCount, 1u); + + *requiredTextureBytes = static_cast(baseSize) + mipBytes; + const int cardMemory = image->cardMemory.platform[0]; + if (cardMemory > 0 && *requiredTextureBytes > static_cast(cardMemory)) + return false; + + return true; +} + +bool ValidateResidentMipCount(const game::GfxImage *image, const DDSImage &ddsImage, uint32_t textureLevelCount) +{ + const uint32_t ddsMipCount = ddsImage.GetMipCount(); + if (ddsMipCount != textureLevelCount) + { + PrintImageError("image '%s' mip count does not match DDS: image=%u dds=%u\n", image->name, textureLevelCount, + ddsMipCount); + return false; + } + + return true; +} + +bool ValidateDDSDataSize(const game::GfxImage *image, const image::DDS_HEADER &header, size_t dataSize, + GPUTEXTUREFORMAT format, uint32_t mipCount, uint32_t faceCount) +{ + const size_t expectedSize = image::CalculateRequiredLinearDataSize( + header.dwWidth, header.dwHeight, format, 0u, mipCount, faceCount); + if (expectedSize == 0) + { + PrintImageError("image '%s' has unsupported DDS dimensions or format\n", image->name); + return false; + } + + if (dataSize != expectedSize) + { + PrintImageError("image '%s' DDS data size does not match shape: have=%u expected=%u mips=%u faces=%u\n", + image->name, static_cast(dataSize), static_cast(expectedSize), + mipCount, faceCount); + return false; + } + + return true; +} + +bool ValidateCubeReplacementData(const game::GfxImage *image, size_t ddsDataSize, GPUTEXTUREFORMAT format, + uint32_t faceSize, uint32_t tiledBaseSize, size_t *requiredDDSSize) +{ + *requiredDDSSize = static_cast(faceSize) * 6u; + if (faceSize == 0 || *requiredDDSSize == 0) + return false; + + if (ddsDataSize != *requiredDDSSize) + return false; + + const int cardMemory = image->cardMemory.platform[0]; + if (cardMemory > 0 && static_cast(tiledBaseSize) > static_cast(cardMemory)) + return false; + + return true; +} + +bool ImageHasStreamedParts(const game::GfxImage *image) +{ + if (image->streaming) + return true; + + for (uint32_t imagePartIndex = 0; imagePartIndex < 4u; ++imagePartIndex) + { + if ((image->streams[imagePartIndex].pixelSize & STREAM_PIXEL_SIZE_MASK) != 0) + return true; + } + + return false; +} + +GPUTEXTUREFORMAT GetImageGpuFormat(const game::GfxImage *image) +{ + return static_cast(image->texture.basemap.Format.DataFormat); +} + +uint32_t GetImageBasePitch(const game::GfxImage *image, bool streamed) +{ + if (streamed) + return 0u; + + return image->texture.basemap.Format.Pitch; +} + +GPUENDIAN GetImageEndian(const game::GfxImage *image) +{ + return static_cast(image->texture.basemap.Format.Endian); +} + +uint32_t GetImageLevelCount(const game::GfxImage *image, bool streamed) +{ + uint32_t levelCount = max(1u, static_cast(image->levelCount)); + + if (!streamed && image->texture.basemap.Format.PackedMips != 0) + { + const uint32_t mipTailBaseLevel = image::xenos_texture::GetMipTailBaseLevel(image->width, image->height); + levelCount = max(1u, min(levelCount, mipTailBaseLevel)); + } + + return levelCount; +} + +std::string GetSanitizedImageName(const char *imageName) +{ + if (imageName == nullptr) + return std::string(); + + std::string sanitizedName; + for (const char *current = imageName; *current != '\0'; ++current) + { + const char c = *current; + if (c == '*') + continue; + + if (c == '/' || c == '\\' || c == ':' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|') + sanitizedName.push_back('_'); + else + sanitizedName.push_back(c); + } + + return sanitizedName; +} + +std::string GetDumpZoneName(const char *zoneName) +{ + const std::string sanitizedZoneName = GetSanitizedImageName(zoneName); + return sanitizedZoneName.empty() ? "unknown" : sanitizedZoneName; +} + +std::string GetImageDumpPath(const char *imageName, const char *zoneName) +{ + return std::string(DUMP_DIR) + "\\" + GetDumpZoneName(zoneName) + "\\images\\" + GetSanitizedImageName(imageName) + + ".dds"; +} + +void EnsureImageDumpDirectory(const char *zoneName) +{ + CreateDirectoryA(DUMP_DIR, nullptr); + + const std::string zoneDirectory = std::string(DUMP_DIR) + "\\" + GetDumpZoneName(zoneName); + CreateDirectoryA(zoneDirectory.c_str(), nullptr); + CreateDirectoryA((zoneDirectory + "\\images").c_str(), nullptr); +} + +const char *GetZoneName(uint32_t zoneIndex) +{ + if (zoneIndex >= game::g_zoneCount) + return nullptr; + + const char *zoneName = game::g_zones[zoneIndex].file.name; + if (zoneName[0] == '\0') + return nullptr; + + return zoneName; +} + +bool ReadFileRange(const std::string &path, uint32_t offset, uint32_t size, std::vector *buffer) +{ + if (buffer == nullptr || size == 0) + return false; + + HANDLE file = CreateFileA(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) + return false; + + SetLastError(NO_ERROR); + const DWORD seekResult = SetFilePointer(file, offset, nullptr, FILE_BEGIN); + if (seekResult == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR) + { + CloseHandle(file); + return false; + } + + buffer->assign(size, 0); + + DWORD bytesRead = 0; + const BOOL readOk = ReadFile(file, &(*buffer)[0], size, &bytesRead, nullptr); + CloseHandle(file); + + if (!readOk || bytesRead != size) + { + buffer->clear(); + return false; + } + + return true; +} + +bool GetImagePoolIndex(const game::GfxImage *image, uint32_t *imageIndex) +{ + if (image == nullptr || imageIndex == nullptr) + return false; + + const uint32_t imageAddress = PtrToUint(image); + const uint32_t poolAddress = PtrToUint(game::g_gfxImagePool); + const uint32_t poolSize = sizeof(game::GfxImage) * game::g_gfxImagePoolSize; + if (imageAddress < poolAddress || imageAddress >= poolAddress + poolSize) + return false; + + const uint32_t imageOffset = imageAddress - poolAddress; + if ((imageOffset % sizeof(game::GfxImage)) != 0) + return false; + + *imageIndex = imageOffset / sizeof(game::GfxImage); + return true; +} + +const game::GfxSubImageStream *GetImageStreamSources(const game::GfxImage *image) +{ + uint32_t imageIndex = 0; + if (!GetImagePoolIndex(image, &imageIndex)) + return nullptr; + + return game::g_imageStreams[imageIndex].part; +} + +bool GetImageFilePath(const game::GfxSubImageStream &source, std::string *path) +{ + if (path == nullptr || source.file == nullptr || source.file->name[0] == '\0') + return false; + + char filePath[MAX_PATH]; + _snprintf(filePath, sizeof(filePath), "game:\\%s.pak", source.file->name); + filePath[sizeof(filePath) - 1] = '\0'; + *path = filePath; + return true; +} + +unsigned __int8 *__fastcall ImageZlibAlloc(unsigned __int8 *opaque, unsigned int items, unsigned int size) +{ + (void)opaque; + + if (items == 0 || size == 0 || items > 0xFFFFFFFFu / size) + return nullptr; + + return static_cast(malloc(items * size)); +} + +void __fastcall ImageZlibFree(unsigned __int8 *opaque, unsigned __int8 *ptr) +{ + (void)opaque; + free(ptr); +} + +bool InflateImageStream(const std::vector &compressedData, uint32_t expectedSize, + std::vector *inflatedData) +{ + if (inflatedData == nullptr || compressedData.empty() || expectedSize == 0) + return false; + + inflatedData->assign(expectedSize, 0); + ZlibStream stream; + memset(&stream, 0, sizeof(stream)); + + stream.next_in = const_cast(&compressedData[0]); + stream.avail_in = static_cast(compressedData.size()); + stream.next_out = &(*inflatedData)[0]; + stream.avail_out = expectedSize; + stream.zalloc = ImageZlibAlloc; + stream.zfree = ImageZlibFree; + + int result = game::Zlib_InflateInit(&stream, "1.1.4", sizeof(stream)); + if (result == 0) + { + const int inflateResult = game::Zlib_Inflate(&stream, 4); + if (inflateResult == 1) + { + result = game::Zlib_InflateEnd(&stream); + } + else + { + game::Zlib_InflateEnd(&stream); + result = inflateResult != 0 ? inflateResult : -5; + } + } + + if (result != 0 || stream.total_out == 0 || stream.total_out > expectedSize) + { + inflatedData->clear(); + return false; + } + + inflatedData->resize(stream.total_out); + return true; +} + +bool WriteUntiledLevel(std::ofstream &file, const game::GfxImage *image, uint32_t width, uint32_t height, + uint32_t mipLevel, GPUTEXTUREFORMAT format, uint32_t basePitch, const unsigned char *tiledPixels, + uint32_t tiledSize, GPUENDIAN endian) +{ + const uint32_t linearLevelSize = image::xenos_texture::CalculateLinearLevelSize(width, height, mipLevel, format); + const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(width, mipLevel, format); + if (linearLevelSize == 0 || rowPitch == 0 || tiledSize == 0) + return false; + + std::vector tiledData(tiledPixels, tiledPixels + tiledSize); + image::xenos_texture::ApplyGpuEndian(&tiledData[0], tiledData.size(), endian); + + std::vector linearData(linearLevelSize); + if (!image::xenos_texture::UntileTextureLevel(width, height, mipLevel, format, basePitch, &linearData[0], + linearData.size(), rowPitch, &tiledData[0], tiledData.size())) + return false; + + file.write(reinterpret_cast(&linearData[0]), linearData.size()); + return true; +} + +bool Dump2DImage(const game::GfxImage *image, bool streamed, const char *zoneName) +{ + if (image->pixels == nullptr || image->cardMemory.platform[0] <= 0) + return false; + + const GPUTEXTUREFORMAT format = GetImageGpuFormat(image); + const uint32_t levelCount = GetImageLevelCount(image, streamed); + const uint32_t basePitch = GetImageBasePitch(image, streamed); + const GPUENDIAN endian = GetImageEndian(image); + const uint32_t linearBaseSize = + image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0u, format); + const uint32_t tiledBaseSize = + image::xenos_texture::CalculateTiledLevelSize(image->width, image->height, 0u, format, basePitch); + if (linearBaseSize == 0 || tiledBaseSize == 0) + return false; + + const size_t requiredBytes = + static_cast(tiledBaseSize) + + image::CalculateRequiredMipTextureBytes(image->width, image->height, format, 1u, levelCount, 1u); + if (requiredBytes > static_cast(image->cardMemory.platform[0])) + return false; + + image::DDS_HEADER header; + const uint32_t caps = + image::DDSCAPS_TEXTURE | (levelCount > 1u ? image::DDSCAPS_COMPLEX | image::DDSCAPS_MIPMAP : 0u); + if (!image::CreateDdsHeader(header, image->width, image->height, image->depth, levelCount, linearBaseSize, caps, 0u, + format)) + return false; + + if (levelCount > 1u) + header.dwFlags |= image::DDSD_MIPMAPCOUNT; + + EnsureImageDumpDirectory(zoneName); + const std::string filename = GetImageDumpPath(image->name, zoneName); + std::ofstream file(filename.c_str(), std::ios::binary); + if (!file) + { + PrintImageError("Could not create DDS for image '%s': %s\n", image->name, filename.c_str()); + return false; + } + + image::WriteDdsHeader(file, header); + + const unsigned char *baseData = image->pixels; + const unsigned char *mipData = baseData + tiledBaseSize; + for (uint32_t mipLevel = 0; mipLevel < levelCount; ++mipLevel) + { + const uint32_t tiledLevelSize = + image::xenos_texture::CalculateTiledLevelSize(image->width, image->height, mipLevel, format, basePitch); + const unsigned char *source = baseData; + if (mipLevel > 0) + { + source = mipData + + image::xenos_texture::CalculateMipLevelOffset(image->width, image->height, mipLevel, format, 1u); + } + + if (!WriteUntiledLevel(file, image, image->width, image->height, mipLevel, format, basePitch, source, + tiledLevelSize, endian)) + return false; + } + + PrintImageInfo("Dumped image '%s'%s\n", image->name, streamed ? " (streamed)" : ""); + return true; +} + +bool DumpCubeImage(const game::GfxImage *image, bool streamed, const char *zoneName) +{ + if (image->pixels == nullptr || image->cardMemory.platform[0] <= 0) + return false; + + const GPUTEXTUREFORMAT format = GetImageGpuFormat(image); + const uint32_t basePitch = GetImageBasePitch(image, streamed); + const GPUENDIAN endian = GetImageEndian(image); + const uint32_t linearFaceSize = + image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0u, format); + const uint32_t tiledFaceSize = + image::xenos_texture::CalculateTiledLevelSize(image->width, image->height, 0u, format, basePitch); + if (linearFaceSize == 0 || tiledFaceSize == 0) + return false; + + const size_t requiredBytes = static_cast(tiledFaceSize) * 6u; + if (requiredBytes > static_cast(image->cardMemory.platform[0])) + return false; + + const uint32_t caps2 = image::DDSCAPS2_CUBEMAP | image::DDSCAPS2_CUBEMAP_POSITIVEX | + image::DDSCAPS2_CUBEMAP_NEGATIVEX | image::DDSCAPS2_CUBEMAP_POSITIVEY | + image::DDSCAPS2_CUBEMAP_NEGATIVEY | image::DDSCAPS2_CUBEMAP_POSITIVEZ | + image::DDSCAPS2_CUBEMAP_NEGATIVEZ; + + image::DDS_HEADER header; + if (!image::CreateDdsHeader(header, image->width, image->height, image->depth, 1u, linearFaceSize, + image::DDSCAPS_TEXTURE | image::DDSCAPS_COMPLEX, caps2, format)) + return false; + + EnsureImageDumpDirectory(zoneName); + const std::string filename = GetImageDumpPath(image->name, zoneName); + std::ofstream file(filename.c_str(), std::ios::binary); + if (!file) + { + PrintImageError("Could not create DDS for image '%s': %s\n", image->name, filename.c_str()); + return false; + } + + image::WriteDdsHeader(file, header); + + for (uint32_t faceIndex = 0; faceIndex < 6u; ++faceIndex) + { + const unsigned char *source = image->pixels + static_cast(faceIndex) * tiledFaceSize; + if (!WriteUntiledLevel(file, image, image->width, image->height, 0u, format, basePitch, source, tiledFaceSize, + endian)) + return false; + } + + PrintImageInfo("Dumped image '%s'%s\n", image->name, streamed ? " (streamed)" : ""); + return true; +} + +uint32_t GetStreamPartLevelCount(const game::GfxImage *image, uint32_t imagePartIndex) +{ + const uint32_t streamLevelCount = image->streams[imagePartIndex].pixelSize >> 26; + if (streamLevelCount != 0) + return streamLevelCount; + + return max(1u, static_cast(image->levelCount)); +} + +bool DumpStreamPartFromData(const game::GfxImage *image, uint32_t imagePartIndex, const char *zoneName, + const std::vector &pixelData) +{ + if (imagePartIndex >= 4u || pixelData.empty()) + return false; + + const game::GfxImageStreamData &streamData = image->streams[imagePartIndex]; + if (streamData.width == 0 || streamData.height == 0) + return false; + + game::GfxImage streamImage = *image; + streamImage.width = streamData.width; + streamImage.height = streamData.height; + streamImage.levelCount = static_cast(GetStreamPartLevelCount(image, imagePartIndex)); + streamImage.cardMemory.platform[0] = static_cast(pixelData.size()); + streamImage.pixels = const_cast(&pixelData[0]); + + if (streamImage.mapType == game::MAPTYPE_2D) + return Dump2DImage(&streamImage, true, zoneName); + + if (streamImage.mapType == game::MAPTYPE_CUBE) + return DumpCubeImage(&streamImage, true, zoneName); + + return false; +} + +bool TryReadStreamPartPixels(const game::GfxImage *image, uint32_t imagePartIndex, std::vector *pixelData) +{ + if (imagePartIndex >= 4u || pixelData == nullptr) + return false; + + const game::GfxSubImageStream *sources = GetImageStreamSources(image); + if (sources == nullptr) + return false; + + const game::GfxImageStreamData &streamData = image->streams[imagePartIndex]; + const uint32_t expectedSize = streamData.pixelSize & STREAM_PIXEL_SIZE_MASK; + if (expectedSize == 0) + return false; + + const game::GfxSubImageStream &source = sources[imagePartIndex]; + if (source.fileOffsetEnd <= source.fileOffset) + return false; + + const uint32_t compressedSize = source.fileOffsetEnd - source.fileOffset; + if (compressedSize > MAX_STREAM_COMPRESSED_SIZE) + return false; + + std::string imageFilePath; + if (!GetImageFilePath(source, &imageFilePath)) + return false; + + std::vector compressedData; + if (!ReadFileRange(imageFilePath, source.fileOffset, compressedSize, &compressedData)) + return false; + + return InflateImageStream(compressedData, expectedSize, pixelData); +} + +bool TryDumpStreamPartEager(const game::GfxImage *image, uint32_t imagePartIndex, const char *zoneName) +{ + if (imagePartIndex >= 4u) + return false; + + const game::GfxImageStreamData &streamData = image->streams[imagePartIndex]; + if ((streamData.pixelSize & STREAM_PIXEL_SIZE_MASK) == 0) + return false; + + std::vector pixelData; + if (!TryReadStreamPartPixels(image, imagePartIndex, &pixelData)) + return false; + + return DumpStreamPartFromData(image, imagePartIndex, zoneName, pixelData); +} + +bool TryDumpStreamedImageEager(const game::GfxImage *image, const char *zoneName) +{ + bool usedParts[4] = {false, false, false, false}; + + for (uint32_t attempt = 0; attempt < 4u; ++attempt) + { + uint32_t bestPart = 4u; + uint32_t bestArea = 0; + + for (uint32_t imagePartIndex = 0; imagePartIndex < 4u; ++imagePartIndex) + { + if (usedParts[imagePartIndex]) + continue; + + const game::GfxImageStreamData &streamData = image->streams[imagePartIndex]; + if ((streamData.pixelSize & STREAM_PIXEL_SIZE_MASK) == 0) + continue; + + const uint32_t imageArea = + static_cast(streamData.width) * static_cast(streamData.height); + if (bestPart == 4u || imageArea > bestArea) + { + bestPart = imagePartIndex; + bestArea = imageArea; + } + } + + if (bestPart == 4u) + break; + + usedParts[bestPart] = true; + if (TryDumpStreamPartEager(image, bestPart, zoneName)) + return true; + } + + return false; +} + +bool Image_Dump(game::GfxImage *image, const char *zoneName) +{ + if (ImageHasStreamedParts(image)) + return TryDumpStreamedImageEager(image, zoneName); + + if (image->mapType == game::MAPTYPE_2D) + return Dump2DImage(image, false, zoneName); + + if (image->mapType == game::MAPTYPE_CUBE) + return DumpCubeImage(image, false, zoneName); + + return false; +} + +void RegisterDvars() +{ + dump_assets = game::Dvar_RegisterBool("dump_assets", Config::dump_assets, 0, "Dump assets as they are loaded."); +} + +bool Image_Replace_2D(game::GfxImage *image, const DDSImage &ddsImage, DdsDataFile &ddsFile, uint32_t ddsDataSize) +{ + if (image->mapType != game::MAPTYPE_2D) + { + PrintImageError("image '%s' is not a 2D map\n", image->name); + return false; + } + + const D3DBaseTexture *texture = &image->texture.basemap; + const GPUTEXTUREFORMAT format = static_cast(texture->Format.DataFormat); + const uint32_t levelCount = image::xenos_texture::GetTextureLevelCount(texture); + const uint32_t mipTailBaseLevel = texture->Format.PackedMips != 0 + ? image::xenos_texture::GetMipTailBaseLevel(image->width, image->height) + : levelCount; + if (!ValidateResidentMipCount(image, ddsImage, levelCount)) + return false; + if (!ValidateDDSDataSize(image, ddsImage.header, ddsDataSize, format, levelCount, 1u)) + return false; + + const uint32_t nonPackedLevelCount = max(1u, min(levelCount, mipTailBaseLevel)); + unsigned char *baseData = image::xenos_texture::GetTextureBase(texture, image->pixels); + unsigned char *mipData = + image::xenos_texture::GetTextureMipBase(texture, baseData, image->width, image->height, format, 1u); + + size_t requiredDDSSize = 0; + size_t requiredTextureBytes = 0; + if (!Validate2DReplacementData(image, format, ddsDataSize, nonPackedLevelCount, &requiredDDSSize, + &requiredTextureBytes)) + { + if (requiredDDSSize == 0) + { + PrintImageError("image '%s' has unsupported replacement format %u\n", image->name, + static_cast(format)); + } + else if (ddsDataSize < requiredDDSSize) + { + PrintImageError("image '%s' DDS data is too small: have=%u need=%u for %u mip levels\n", image->name, + static_cast(ddsDataSize), static_cast(requiredDDSSize), + nonPackedLevelCount); + } + else + { + PrintImageError("image '%s' replacement exceeds texture memory: have=%u need=%u\n", image->name, + static_cast(image->cardMemory.platform[0]), + static_cast(requiredTextureBytes)); + } + + return false; + } + + if (baseData == nullptr || mipData == nullptr) + { + PrintImageError("image '%s' has no valid texture memory\n", image->name); + return false; + } + + uint32_t ddsOffset = 0; + + for (uint32_t mipLevel = 0; mipLevel < nonPackedLevelCount; ++mipLevel) + { + const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(image->width, mipLevel, format); + const uint32_t ddsMipLevelSize = + image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, mipLevel, format); + const uint32_t tiledMipLevelSize = image::xenos_texture::CalculateTiledLevelSize( + image->width, image->height, mipLevel, format, texture->Format.Pitch); + + if (ddsMipLevelSize == 0 || tiledMipLevelSize == 0 || rowPitch == 0) + { + PrintImageError("unsupported format %u for image '%s' mip level %u\n", texture->Format.DataFormat, + image->name, mipLevel); + return false; + } + + if (static_cast(ddsOffset) + ddsMipLevelSize > ddsDataSize) + { + PrintImageError("image '%s' mip level %u exceeds DDS data size\n", image->name, mipLevel); + return false; + } + + unsigned char *destination = baseData; + if (mipLevel > 0) + { + destination = mipData + image::xenos_texture::CalculateMipLevelOffset(image->width, image->height, mipLevel, + format, 1u); + } + + if (!TileDdsLevelToTexture(ddsFile, ddsOffset, ddsMipLevelSize, image->width, image->height, mipLevel, + image->width, image->height, mipLevel, format, texture->Format.Pitch, destination, + tiledMipLevelSize, static_cast(texture->Format.Endian))) + { + PrintImageError("failed to tile image '%s' mip level %u\n", image->name, mipLevel); + return false; + } + + ddsOffset += ddsMipLevelSize; + } + + return true; +} + +bool Image_Replace_Cube(game::GfxImage *image, const DDSImage &ddsImage, DdsDataFile &ddsFile, uint32_t ddsDataSize) +{ + if (image->mapType != game::MAPTYPE_CUBE) + { + PrintImageError("image '%s' is not a cube map\n", image->name); + return false; + } + + const D3DBaseTexture *texture = &image->texture.basemap; + const GPUTEXTUREFORMAT format = static_cast(texture->Format.DataFormat); + const uint32_t levelCount = image::xenos_texture::GetTextureLevelCount(texture); + const uint32_t faceSize = image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0u, format); + const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(image->width, 0u, format); + const uint32_t tiledFaceSize = + image::xenos_texture::CalculateTiledLevelSize(image->width, image->height, 0u, format, texture->Format.Pitch); + const uint32_t tiledBaseSize = image::xenos_texture::CalculateBaseSize(texture, image->width, image->height, 6u); + unsigned char *baseData = image::xenos_texture::GetTextureBase(texture, image->pixels); + + if (faceSize == 0 || rowPitch == 0 || tiledFaceSize == 0 || tiledBaseSize < tiledFaceSize * 6u) + { + PrintImageError("image '%s' has unsupported cube format %u\n", image->name, static_cast(format)); + return false; + } + + if (baseData == nullptr) + { + PrintImageError("image '%s' has no valid cube texture memory\n", image->name); + return false; + } + + if (levelCount != 1u || ddsImage.GetMipCount() != 1u) + { + PrintImageError("image '%s' cube replacement must be base-level only: imageMips=%u ddsMips=%u\n", image->name, + levelCount, ddsImage.GetMipCount()); + return false; + } + + if (!ValidateDDSDataSize(image, ddsImage.header, ddsDataSize, format, 1u, 6u)) + return false; + + size_t requiredDDSSize = 0; + if (!ValidateCubeReplacementData(image, ddsDataSize, format, faceSize, tiledBaseSize, &requiredDDSSize)) + { + if (ddsDataSize < requiredDDSSize) + { + PrintImageError("image '%s' DDS is too small for 6 cube faces: have=%u need=%u\n", image->name, + static_cast(ddsDataSize), static_cast(requiredDDSSize)); + } + else + { + PrintImageError("image '%s' cube replacement exceeds texture memory: have=%u need=%u\n", image->name, + static_cast(image->cardMemory.platform[0]), tiledBaseSize); + } + + return false; + } + + for (uint32_t faceIndex = 0; faceIndex < 6u; ++faceIndex) + { + const uint32_t faceOffset = faceIndex * faceSize; + unsigned char *faceDestination = baseData + (faceIndex * tiledFaceSize); + + if (!TileDdsLevelToTexture(ddsFile, faceOffset, faceSize, image->width, image->height, 0u, image->width, + image->height, 0u, format, texture->Format.Pitch, faceDestination, tiledFaceSize, + static_cast(texture->Format.Endian))) + { + PrintImageError("failed to tile cube image '%s' face %u\n", image->name, faceIndex); + return false; + } + } + + return true; +} + +bool ValidateReplacementShape(const game::GfxImage *image, const DDSImage &ddsImage, uint32_t ddsDataSize) +{ + const bool ddsIsCubemap = ddsImage.IsCubemap(); + + if (image->mapType == game::MAPTYPE_2D && ddsIsCubemap) + { + PrintImageError("image '%s' is 2D but replacement DDS is a cubemap\n", image->name); + return false; + } + + if (image->mapType == game::MAPTYPE_CUBE && !ddsIsCubemap) + { + GPUTEXTUREFORMAT ddsFormat; + if (!ddsImage.GetGpuFormat(&ddsFormat)) + return false; + + const uint32_t faceSize = image::xenos_texture::CalculateLinearLevelSize( + ddsImage.header.dwWidth, ddsImage.header.dwHeight, 0u, ddsFormat); + if (faceSize == 0 || ddsDataSize < static_cast(faceSize) * 6u) + { + PrintImageError("image '%s' is a cubemap but replacement DDS is not a valid 6-face cubemap\n", image->name); + return false; + } + } + + return true; +} + +void Image_Replace(game::GfxImage *image) +{ + if (image == nullptr || image->name == nullptr) + return; + + const std::string replacementPath = GetReplacementPath(image->name); + if (!ImageFileExists(replacementPath)) + return; + + if (ImageHasStreamedParts(image)) + return; + + if (image->pixels == nullptr || image->cardMemory.platform[0] <= 0) + { + PrintImageError("image '%s' replacement exists but resident texture memory is not available\n", image->name); + return; + } + + DDSImage ddsImage; + uint32_t ddsDataSize = 0; + if (!ReadDDSHeader(replacementPath, &ddsImage, &ddsDataSize)) + { + PrintImageError("failed to load DDS header for image '%s': %s\n", image->name, replacementPath.c_str()); + return; + } + + GPUTEXTUREFORMAT ddsFormat; + if (!ValidateDDSHeaderFields(image, ddsImage, &ddsFormat)) + return; + + if (image->width != ddsImage.header.dwWidth || image->height != ddsImage.header.dwHeight) + { + PrintImageError("image '%s' dimensions do not match DDS: image=%ux%u dds=%ux%u\n", image->name, image->width, + image->height, ddsImage.header.dwWidth, ddsImage.header.dwHeight); + return; + } + + if (!ValidateReplacementShape(image, ddsImage, ddsDataSize)) + return; + + DdsDataFile ddsFile(replacementPath, ddsDataSize); + if (!ddsFile.IsValid()) + { + PrintImageError("failed to open DDS data for image '%s': %s error=0x%08X\n", image->name, + replacementPath.c_str(), GetLastError()); + return; + } + + bool replaced = false; + if (image->mapType == game::MAPTYPE_2D) + replaced = Image_Replace_2D(image, ddsImage, ddsFile, ddsDataSize); + else if (image->mapType == game::MAPTYPE_CUBE) + replaced = Image_Replace_Cube(image, ddsImage, ddsFile, ddsDataSize); + else + PrintImageError("image '%s' is not a 2D or cube map\n", image->name); + + if (replaced) + PrintImageInfo("replaced image '%s' (resident)\n", image->name); +} + +bool FindMipLevelForDimensions(const DDSImage &ddsImage, uint32_t width, uint32_t height, uint32_t *mipLevel) +{ + const uint32_t ddsMipCount = ddsImage.GetMipCount(); + + for (uint32_t currentMip = 0; currentMip < ddsMipCount; ++currentMip) + { + if (image::GetMipDimension(ddsImage.header.dwWidth, currentMip) == width && + image::GetMipDimension(ddsImage.header.dwHeight, currentMip) == height) + { + *mipLevel = currentMip; + return true; + } + } + + return false; +} + +bool ValidateStreamReplacementData(const game::GfxImage *image, const DDSImage &ddsImage, size_t ddsDataSize, + GPUTEXTUREFORMAT format, uint32_t startMipLevel, uint32_t levelCount, + uint32_t basePitch, size_t *requiredDDSSize, size_t *requiredTextureBytes) +{ + *requiredDDSSize = image::CalculateRequiredLinearDataSize(ddsImage.header.dwWidth, ddsImage.header.dwHeight, format, + startMipLevel, levelCount, 1u); + if (*requiredDDSSize == 0) + return false; + + const uint32_t ddsOffset = + image::CalculateDdsMipOffset(ddsImage.header.dwWidth, ddsImage.header.dwHeight, format, startMipLevel); + if (static_cast(ddsOffset) + *requiredDDSSize > ddsDataSize) + return false; + + *requiredTextureBytes = 0; + for (uint32_t localMipLevel = 0; localMipLevel < levelCount; ++localMipLevel) + { + const uint32_t levelSize = image::xenos_texture::CalculateTiledLevelSize(image->width, image->height, + localMipLevel, format, basePitch); + if (levelSize == 0) + return false; + + *requiredTextureBytes += levelSize; + } + + const int cardMemory = image->cardMemory.platform[0]; + if (cardMemory <= 0 || *requiredTextureBytes > static_cast(cardMemory)) + return false; + + return true; +} + +bool Image_Replace_StreamCubePart(game::GfxImage *image, DdsDataFile &ddsFile, const DDSImage &ddsImage, + size_t ddsDataSize, GPUTEXTUREFORMAT ddsFormat, uint32_t imagePartIndex) +{ + if (imagePartIndex != 0u) + { + PrintImageError("streamed cube image '%s' has unsupported part %u\n", image->name, imagePartIndex); + return false; + } + + if (image->width != ddsImage.header.dwWidth || image->height != ddsImage.header.dwHeight) + { + PrintImageError("streamed cube image '%s' dimensions do not match DDS: image=%ux%u dds=%ux%u\n", image->name, + image->width, image->height, ddsImage.header.dwWidth, ddsImage.header.dwHeight); + return false; + } + + const uint32_t ddsMipCount = ddsImage.GetMipCount(); + if (image->levelCount != 1u || ddsMipCount != 1u) + { + PrintImageError("streamed cube image '%s' must be base-level only: imageMips=%u ddsMips=%u\n", image->name, + static_cast(image->levelCount), ddsMipCount); + return false; + } + + if (!ValidateDDSDataSize(image, ddsImage.header, ddsDataSize, ddsFormat, 1u, 6u)) + return false; + + const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(image->width, 0u, ddsFormat); + const uint32_t faceSize = + image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0u, ddsFormat); + const uint32_t tiledFaceSize = + image::xenos_texture::CalculateTiledLevelSize(image->width, image->height, 0u, ddsFormat, 0u); + const size_t requiredTextureBytes = static_cast(tiledFaceSize) * 6u; + + if (rowPitch == 0 || faceSize == 0 || tiledFaceSize == 0) + { + PrintImageError("streamed cube image '%s' has unsupported format %u\n", image->name, + static_cast(ddsFormat)); + return false; + } + + if (image->cardMemory.platform[0] <= 0 || requiredTextureBytes > static_cast(image->cardMemory.platform[0])) + { + PrintImageError("streamed cube image '%s' replacement exceeds texture memory: have=%u need=%u\n", image->name, + static_cast(image->cardMemory.platform[0]), + static_cast(requiredTextureBytes)); + return false; + } + + for (uint32_t faceIndex = 0; faceIndex < 6u; ++faceIndex) + { + const uint32_t faceOffset = faceIndex * faceSize; + unsigned char *faceDestination = image->pixels + static_cast(faceIndex) * tiledFaceSize; + + if (!TileDdsLevelToTexture(ddsFile, faceOffset, faceSize, image->width, image->height, 0u, image->width, + image->height, 0u, ddsFormat, 0u, faceDestination, tiledFaceSize, + static_cast(image->texture.basemap.Format.Endian))) + { + PrintImageError("failed to tile streamed cube image '%s' face %u\n", image->name, faceIndex); + return false; + } + } + + return true; +} + +bool Image_Replace_StreamPart(game::GfxImage *image, DdsDataFile &ddsFile, const DDSImage &ddsImage, + size_t ddsDataSize, uint32_t imagePartIndex) +{ + if (image == nullptr || image->name == nullptr || imagePartIndex >= 4u) + return false; + + if (image->pixels == nullptr) + { + PrintImageError("streamed image '%s' part %u replacement exists but pixel memory is not available\n", + image->name, imagePartIndex); + return false; + } + + GPUTEXTUREFORMAT ddsFormat; + if (!ValidateDDSHeaderFields(image, ddsImage, &ddsFormat)) + return false; + + if (image->mapType == game::MAPTYPE_CUBE) + return Image_Replace_StreamCubePart(image, ddsFile, ddsImage, ddsDataSize, ddsFormat, imagePartIndex); + + if (image->mapType != game::MAPTYPE_2D) + { + PrintImageError("streamed image '%s' is not a supported 2D or cube map\n", image->name); + return false; + } + + if (ddsImage.IsCubemap()) + { + PrintImageError("streamed image '%s' is 2D but replacement DDS is a cubemap\n", image->name); + return false; + } + + const uint32_t ddsMipCount = ddsImage.GetMipCount(); + if (!ValidateDDSDataSize(image, ddsImage.header, ddsDataSize, ddsFormat, ddsMipCount, 1u)) + return false; + + uint32_t startMipLevel = 0; + if (!FindMipLevelForDimensions(ddsImage, image->width, image->height, &startMipLevel)) + { + PrintImageError("streamed image '%s' part %u dimensions do not exist in DDS: part=%ux%u dds=%ux%u\n", + image->name, imagePartIndex, image->width, image->height, ddsImage.header.dwWidth, + ddsImage.header.dwHeight); + return false; + } + + const game::GfxImageStreamData &streamData = image->streams[imagePartIndex]; + uint32_t levelCount = streamData.pixelSize >> 26; + if (levelCount == 0) + levelCount = max(1u, static_cast(image->levelCount)); + + const D3DBaseTexture *texture = &image->texture.basemap; + const uint32_t textureLevelCount = image::xenos_texture::GetTextureLevelCount(texture); + levelCount = max(1u, min(levelCount, textureLevelCount)); + + const uint32_t mipTailBaseLevel = texture->Format.PackedMips != 0 + ? image::xenos_texture::GetMipTailBaseLevel(image->width, image->height) + : levelCount; + const uint32_t replaceLevelCount = max(1u, min(levelCount, mipTailBaseLevel)); + + if (startMipLevel + replaceLevelCount > ddsMipCount) + { + PrintImageError("streamed image '%s' part %u needs %u DDS mips from mip %u, but DDS has %u\n", image->name, + imagePartIndex, replaceLevelCount, startMipLevel, ddsMipCount); + return false; + } + + size_t requiredDDSSize = 0; + size_t requiredTextureBytes = 0; + const uint32_t streamBasePitch = 0u; + if (!ValidateStreamReplacementData(image, ddsImage, ddsDataSize, ddsFormat, startMipLevel, replaceLevelCount, + streamBasePitch, &requiredDDSSize, &requiredTextureBytes)) + { + PrintImageError("streamed image '%s' part %u replacement size is invalid: ddsNeed=%u textureNeed=%u card=%u\n", + image->name, imagePartIndex, static_cast(requiredDDSSize), + static_cast(requiredTextureBytes), + static_cast(image->cardMemory.platform[0])); + return false; + } + + uint32_t ddsOffset = + image::CalculateDdsMipOffset(ddsImage.header.dwWidth, ddsImage.header.dwHeight, ddsFormat, startMipLevel); + size_t destinationOffset = 0; + + for (uint32_t localMipLevel = 0; localMipLevel < replaceLevelCount; ++localMipLevel) + { + const uint32_t globalMipLevel = startMipLevel + localMipLevel; + const uint32_t rowPitch = + image::xenos_texture::CalculateLinearRowPitch(ddsImage.header.dwWidth, globalMipLevel, ddsFormat); + const uint32_t ddsMipLevelSize = image::xenos_texture::CalculateLinearLevelSize( + ddsImage.header.dwWidth, ddsImage.header.dwHeight, globalMipLevel, ddsFormat); + const uint32_t tiledMipLevelSize = image::xenos_texture::CalculateTiledLevelSize( + image->width, image->height, localMipLevel, ddsFormat, streamBasePitch); + + if (rowPitch == 0 || ddsMipLevelSize == 0 || tiledMipLevelSize == 0) + { + PrintImageError("streamed image '%s' part %u has unsupported format %u at mip %u\n", image->name, + imagePartIndex, static_cast(ddsFormat), localMipLevel); + return false; + } + + if (static_cast(ddsOffset) + ddsMipLevelSize > ddsDataSize || + destinationOffset + tiledMipLevelSize > static_cast(image->cardMemory.platform[0])) + { + PrintImageError("streamed image '%s' part %u mip %u exceeds source or destination bounds\n", image->name, + imagePartIndex, localMipLevel); + return false; + } + + unsigned char *destination = image->pixels + destinationOffset; + + if (!TileDdsLevelToTexture(ddsFile, ddsOffset, ddsMipLevelSize, ddsImage.header.dwWidth, + ddsImage.header.dwHeight, globalMipLevel, image->width, image->height, + localMipLevel, ddsFormat, streamBasePitch, destination, tiledMipLevelSize, + static_cast(image->texture.basemap.Format.Endian))) + { + PrintImageError("failed to tile streamed image '%s' part %u mip %u\n", image->name, imagePartIndex, + localMipLevel); + return false; + } + + ddsOffset += ddsMipLevelSize; + destinationOffset += tiledMipLevelSize; + } + + return true; +} + +void TryReplaceStreamPart(game::GfxImage *image, uint32_t imagePartIndex) +{ + if (image == nullptr || image->name == nullptr) + return; + + const std::string replacementPath = GetReplacementPath(image->name); + const bool replacementExists = ImageFileExists(replacementPath); + + if (imagePartIndex >= 4u) + return; + + if (!replacementExists) + return; + + DDSImage ddsImage; + uint32_t ddsDataSize = 0; + if (!ReadDDSHeader(replacementPath, &ddsImage, &ddsDataSize)) + { + PrintImageError("failed to load DDS header for image '%s': %s\n", image->name, replacementPath.c_str()); + return; + } + + DdsDataFile ddsFile(replacementPath, ddsDataSize); + if (!ddsFile.IsValid()) + { + PrintImageError("failed to open DDS data for streamed image '%s': %s error=0x%08X\n", image->name, + replacementPath.c_str(), GetLastError()); + return; + } + + if (Image_Replace_StreamPart(image, ddsFile, ddsImage, ddsDataSize, imagePartIndex)) + PrintImageInfo("replaced image '%s' (streamed part %u)\n", image->name, imagePartIndex); +} + +void OnDBLinkXAssetPre(game::XAssetType &type, game::XAssetHeader *header) +{ + if (type == game::ASSET_TYPE_IMAGE && header != nullptr) + Image_Replace(header->image); +} + +void OnDBLinkXAssetPost(game::XAssetEntryPoolEntry *poolEntry) +{ + if (dump_assets == nullptr || !dump_assets->current.enabled || poolEntry == nullptr) + return; + + const game::XAssetEntry &entry = poolEntry->entry; + if (entry.asset.type != game::ASSET_TYPE_IMAGE) + return; + + game::GfxImage *image = entry.asset.header.image; + Image_Dump(image, GetZoneName(entry.zoneIndex)); +} + +Detour ImageCache_InitImage_Detour; + +void ImageCache_InitImage_Hook(game::GfxImage *image, game::GfxImage *remoteImage, unsigned __int8 *pixels, + unsigned int imagePartIndex) +{ + ImageCache_InitImage_Detour.GetOriginal()(image, remoteImage, pixels, imagePartIndex); + TryReplaceStreamPart(image, imagePartIndex); +} +} // namespace + +image_loader::image_loader() +{ + Events::OnDvarInit(RegisterDvars); + Events::OnDBLinkXAssetPre(OnDBLinkXAssetPre); + Events::OnDBLinkXAssetPost(OnDBLinkXAssetPost); + + ImageCache_InitImage_Detour = Detour(iw4::mp_tu6::ImageCache_InitImage, ImageCache_InitImage_Hook); + ImageCache_InitImage_Detour.Install(); +} + +image_loader::~image_loader() +{ + ImageCache_InitImage_Detour.Remove(); + dump_assets = nullptr; +} diff --git a/src/game/iw4/mp_tu6/components/image_loader.h b/src/game/iw4/mp_tu6/components/image_loader.h new file mode 100644 index 00000000..b78ef026 --- /dev/null +++ b/src/game/iw4/mp_tu6/components/image_loader.h @@ -0,0 +1,15 @@ +#pragma once + +#include "pch.h" + +class image_loader : public Module +{ + public: + image_loader(); + ~image_loader(); + + const char *get_name() override + { + return "image_loader"; + }; +}; diff --git a/src/game/iw4/mp_tu6/components/mpsp.cpp b/src/game/iw4/mp_tu6/components/mpsp.cpp index bca57864..b507bd43 100644 --- a/src/game/iw4/mp_tu6/components/mpsp.cpp +++ b/src/game/iw4/mp_tu6/components/mpsp.cpp @@ -2,6 +2,7 @@ #include "pch.h" #include "mpsp.h" +#include "events.h" #include "unordered_map" namespace iw4 @@ -30,37 +31,6 @@ bool is_mp_fastfile(const char *name) struct internal_state; -struct Sys_File -{ - void *handle; - int startOffset; -}; - -struct DBFile -{ - Sys_File handle; - char name[64]; -}; - -struct XBlock -{ - unsigned __int8 *data; - unsigned int size; -}; - -struct XZoneMemory -{ - XBlock blocks[6]; -}; - -struct XZone -{ - DBFile file; - int flags; - int allocType; - XZoneMemory mem; -}; - struct _OVERLAPPED { unsigned int Internal; @@ -112,7 +82,6 @@ const char **g_assetNames = reinterpret_cast(0x82442298); int *g_poolSize = reinterpret_cast(0x82442588); const DB_LoadData *g_load = reinterpret_cast(0x82678600); const unsigned int *g_zoneIndex = reinterpret_cast(0x827ADAE4); -const XZone *g_zones = reinterpret_cast(0x829D8048); GameWorldMp *gameWorldMp = reinterpret_cast(0x82DFD010); typedef int (*Com_sprintf_t)(char *dest, unsigned int size, const char *fmt, ...); @@ -263,6 +232,11 @@ void dump(MapEnts *asset) { std::string buffer; + if (!Config::dump_assets) + { + return; + } + if (!asset || !asset->name || asset->name[0] == '\0') { return; @@ -384,8 +358,7 @@ void override_(RawFile *asset) } // namespace Asset -Detour DB_LinkXAssetEntry1_Detour; -XAssetEntryPoolEntry *DB_LinkXAssetEntry1_Hook(XAssetType type, XAssetHeader *header) +void OnDBLinkXAssetPre(XAssetType &type, XAssetHeader *header) { XAsset xasset; xasset.type = type; @@ -453,10 +426,6 @@ XAssetEntryPoolEntry *DB_LinkXAssetEntry1_Hook(XAssetType type, XAssetHeader *he DB_SetXAssetName(&xasset, ",CGAME_UNKNOWN"); } } - - XAssetEntryPoolEntry *entry = DB_LinkXAssetEntry1_Detour.GetOriginal()(type, header); - - return entry; } void DB_ReallocXAssetPool(XAssetType type, unsigned int newSize) @@ -535,8 +504,7 @@ mpsp::mpsp() #endif // Modify some assets before linking - DB_LinkXAssetEntry1_Detour = Detour(DB_LinkXAssetEntry1, DB_LinkXAssetEntry1_Hook); - DB_LinkXAssetEntry1_Detour.Install(); + ::Events::OnDBLinkXAssetPre(OnDBLinkXAssetPre); // Rewrite some strings on the fly Com_sprintf_Detour = Detour(Com_sprintf, Com_sprintf_Hook); @@ -550,8 +518,6 @@ mpsp::~mpsp() CL_ConsolePrint_Detour.Remove(); #endif - DB_LinkXAssetEntry1_Detour.Remove(); - Com_sprintf_Detour.Remove(); } diff --git a/src/game/iw4/mp_tu6/components/scr_parser.cpp b/src/game/iw4/mp_tu6/components/scr_parser.cpp index 3e8f9710..6b7931b6 100644 --- a/src/game/iw4/mp_tu6/components/scr_parser.cpp +++ b/src/game/iw4/mp_tu6/components/scr_parser.cpp @@ -12,7 +12,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename) auto callOriginal = [&]() { return Scr_AddSourceBuffer_Detour.GetOriginal()(filename, extFilename); }; - if (Config::dump_rawfile) + if (Config::dump_assets) { auto contents = callOriginal(); // Dump the script to a file diff --git a/src/game/iw4/mp_tu6/main.cpp b/src/game/iw4/mp_tu6/main.cpp index 5cca6963..d33a4ca9 100644 --- a/src/game/iw4/mp_tu6/main.cpp +++ b/src/game/iw4/mp_tu6/main.cpp @@ -6,6 +6,7 @@ #include "components/events.h" #include "components/g_client_fields.h" #include "components/g_scr_main.h" +#include "components/image_loader.h" #include "components/mpsp.h" #include "components/mr.h" #include "components/patches.h" @@ -30,6 +31,7 @@ IW4_MP_TU6_Plugin::IW4_MP_TU6_Plugin() RegisterModule(new console()); RegisterModule(new g_client_fields()); RegisterModule(new g_scr_main()); + RegisterModule(new image_loader()); RegisterModule(new mpsp()); RegisterModule(new MovementRecorder()); RegisterModule(new patches()); diff --git a/src/game/iw4/mp_tu6/structs.h b/src/game/iw4/mp_tu6/structs.h index 244b66c8..f3a83e37 100644 --- a/src/game/iw4/mp_tu6/structs.h +++ b/src/game/iw4/mp_tu6/structs.h @@ -1807,6 +1807,99 @@ enum XAssetType : __int32 ASSET_TYPE_ASSETLIST = 0x2A, }; +enum MapType : unsigned __int8 +{ + MAPTYPE_NONE = 0x0, + MAPTYPE_INVALID1 = 0x1, + MAPTYPE_1D = 0x2, + MAPTYPE_2D = 0x3, + MAPTYPE_3D = 0x4, + MAPTYPE_CUBE = 0x5, + MAPTYPE_COUNT = 0x6, +}; + +struct Picmip +{ + unsigned __int8 platform[2]; +}; + +struct CardMemory +{ + int platform[1]; +}; + +struct GfxTexture +{ + D3DBaseTexture basemap; +}; +static_assert(sizeof(GfxTexture) == 0x34, ""); + +struct GfxImageStreamData +{ + unsigned __int16 width; + unsigned __int16 height; + unsigned int pixelSize; +}; +static_assert(sizeof(GfxImageStreamData) == 0x8, ""); + +struct GfxImage +{ + GfxTexture texture; + unsigned __int8 semantic; + unsigned __int8 category; + bool cached; + unsigned __int8 flags; + MapType mapType; + Picmip picmip; + bool noPicmip; + CardMemory cardMemory; + unsigned __int16 width; + unsigned __int16 height; + unsigned __int16 depth; + unsigned __int8 levelCount; + bool streaming; + unsigned __int8 *pixels; + GfxImageStreamData streams[4]; + const char *name; +}; +static_assert(sizeof(GfxImage) == 0x70, ""); +static_assert(offsetof(GfxImage, texture) == 0x0, ""); +static_assert(offsetof(GfxImage, mapType) == 0x38, ""); +static_assert(offsetof(GfxImage, cardMemory) == 0x3C, ""); +static_assert(offsetof(GfxImage, width) == 0x40, ""); +static_assert(offsetof(GfxImage, pixels) == 0x48, ""); +static_assert(offsetof(GfxImage, streams) == 0x4C, ""); +static_assert(offsetof(GfxImage, name) == 0x6C, ""); + +struct Sys_File +{ + void *handle; + int startOffset; +}; +static_assert(sizeof(Sys_File) == 0x8, ""); + +struct DBFile +{ + Sys_File handle; + char name[64]; +}; +static_assert(sizeof(DBFile) == 0x48, ""); +static_assert(offsetof(DBFile, name) == 0x8, ""); + +struct GfxSubImageStream +{ + unsigned int fileOffset; + unsigned int fileOffsetEnd; + DBFile *file; +}; +static_assert(sizeof(GfxSubImageStream) == 0xC, ""); + +struct GfxImageStream +{ + GfxSubImageStream part[4]; +}; +static_assert(sizeof(GfxImageStream) == 0x30, ""); + struct cplane_s; struct cStaticModel_s; struct ClipMaterial @@ -2007,6 +2100,7 @@ static_assert(offsetof(StringTable, values) == 0xC, ""); union XAssetHeader { + GfxImage *image; clipMap_t *clipMap; GameWorldSp *gameWorldSp; GameWorldMp *gameWorldMp; @@ -2022,6 +2116,30 @@ struct XAsset XAssetHeader header; }; +struct XBlock +{ + unsigned __int8 *data; + unsigned int size; +}; +static_assert(sizeof(XBlock) == 0x8, ""); + +struct XZoneMemory +{ + XBlock blocks[6]; +}; +static_assert(sizeof(XZoneMemory) == 0x30, ""); + +struct XZone +{ + DBFile file; + int flags; + int allocType; + XZoneMemory mem; +}; +static_assert(sizeof(XZone) == 0x80, ""); +static_assert(offsetof(XZone, file) == 0x0, ""); +static_assert(offsetof(XZone, mem) == 0x50, ""); + struct __declspec(align(4)) XAssetEntry { XAsset asset; @@ -2030,12 +2148,15 @@ struct __declspec(align(4)) XAssetEntry unsigned __int16 nextHash; unsigned __int16 nextOverride; }; +static_assert(sizeof(XAssetEntry) == 0x10, ""); +static_assert(offsetof(XAssetEntry, zoneIndex) == 0x8, ""); union XAssetEntryPoolEntry { XAssetEntry entry; XAssetEntryPoolEntry *next; }; +static_assert(sizeof(XAssetEntryPoolEntry) == 0x10, ""); } // namespace mp_tu6 } // namespace iw4 diff --git a/src/game/iw4/mp_tu6/symbols.h b/src/game/iw4/mp_tu6/symbols.h index ee6e7c7d..3cb31764 100644 --- a/src/game/iw4/mp_tu6/symbols.h +++ b/src/game/iw4/mp_tu6/symbols.h @@ -146,6 +146,19 @@ static Key_SetCatcher_t Key_SetCatcher = reinterpret_cast(0x82 typedef Material *(*Material_RegisterHandle_t)(const char *name); static Material_RegisterHandle_t Material_RegisterHandle = reinterpret_cast(0x823C2FF8); +typedef void (*ImageCache_InitImage_t)(GfxImage *image, GfxImage *remoteImage, unsigned __int8 *pixels, + unsigned int imagePartIndex); +static ImageCache_InitImage_t ImageCache_InitImage = reinterpret_cast(0x823DE448); + +typedef int (*Zlib_InflateInit_t)(void *stream, const char *version, int streamSize); +static Zlib_InflateInit_t Zlib_InflateInit = reinterpret_cast(0x82344E90); + +typedef int (*Zlib_Inflate_t)(void *stream, int flush); +static Zlib_Inflate_t Zlib_Inflate = reinterpret_cast(0x82344EA0); + +typedef int (*Zlib_InflateEnd_t)(void *stream); +static Zlib_InflateEnd_t Zlib_InflateEnd = reinterpret_cast(0x82344CB8); + static auto R_CheckDvarModified = reinterpret_cast(0x823DDD78); typedef void (*R_AddCmdDrawStretchPic_t)(float x, float y, float w, float h, float s0, float t0, float s1, float t1, @@ -290,6 +303,13 @@ static auto CL_CreateNewCommands = reinterpret_cast(0x83052680); static auto fields = reinterpret_cast(0x82027518); +static auto g_assetEntryPool = reinterpret_cast(0x82839700); +static const unsigned int g_assetEntryPoolSize = 34000; +static auto g_gfxImagePool = reinterpret_cast(0x828C058C); +static const unsigned int g_gfxImagePoolSize = 0xE00; +static auto g_imageStreams = reinterpret_cast(0x82C91600); +static auto g_zones = reinterpret_cast(0x829D8048); +static const unsigned int g_zoneCount = 33; static auto g_entities = reinterpret_cast(0x82E2A580); static auto level = reinterpret_cast(0x82FF2F08); static auto sharedUiInfo = reinterpret_cast(0x836A3AC0); diff --git a/src/game/iw4/sp/components/clipmap.cpp b/src/game/iw4/sp/components/clipmap.cpp index f155db9c..77f6e6d3 100644 --- a/src/game/iw4/sp/components/clipmap.cpp +++ b/src/game/iw4/sp/components/clipmap.cpp @@ -18,7 +18,7 @@ void Load_clipMap_t_Hook(bool atStreamStart) auto mapEnts = (*varclipMap_t)->mapEnts; // Dump map entities if enabled - if (Config::dump_map_ents) + if (Config::dump_assets) { std::string dumpPath = va("%s\\%s.ents", DUMP_DIR, mapEnts->name); // IW4x naming convention std::replace(dumpPath.begin(), dumpPath.end(), '/', '\\'); diff --git a/src/game/iw4/sp/components/scr_parser.cpp b/src/game/iw4/sp/components/scr_parser.cpp index fca12990..2b44d4dd 100644 --- a/src/game/iw4/sp/components/scr_parser.cpp +++ b/src/game/iw4/sp/components/scr_parser.cpp @@ -12,7 +12,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename) auto callOriginal = [&]() { return Scr_AddSourceBuffer_Detour.GetOriginal()(filename, extFilename); }; - if (Config::dump_rawfile) + if (Config::dump_assets) { auto contents = callOriginal(); // Dump the script to a file diff --git a/src/game/qos/mp/components/scr_parser.cpp b/src/game/qos/mp/components/scr_parser.cpp index 119a3bb1..5d2ce8b0 100644 --- a/src/game/qos/mp/components/scr_parser.cpp +++ b/src/game/qos/mp/components/scr_parser.cpp @@ -15,7 +15,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename, co archive); }; - if (Config::dump_rawfile) + if (Config::dump_assets) { DbgPrint("GSCLoader: Dumping script %s\n", extFilename); auto contents = callOriginal(); diff --git a/src/game/qos/sp/components/scr_parser.cpp b/src/game/qos/sp/components/scr_parser.cpp index 891e4d4f..4a9a67fc 100644 --- a/src/game/qos/sp/components/scr_parser.cpp +++ b/src/game/qos/sp/components/scr_parser.cpp @@ -15,7 +15,7 @@ char *Scr_AddSourceBuffer_Hook(const char *filename, const char *extFilename, co archive); }; - if (Config::dump_rawfile) + if (Config::dump_assets) { DbgPrint("GSCLoader: Dumping script %s\n", extFilename); auto contents = callOriginal(); diff --git a/src/game/t4/mp/components/gsc_loader.cpp b/src/game/t4/mp/components/gsc_loader.cpp index 49120d54..ea3ee487 100644 --- a/src/game/t4/mp/components/gsc_loader.cpp +++ b/src/game/t4/mp/components/gsc_loader.cpp @@ -16,7 +16,7 @@ char *GSCLoader::Scr_AddSourceBuffer_Hook(scriptInstance_t a1, const char *filen codePos, archive); }; - if (Config::dump_rawfile) + if (Config::dump_assets) { auto contents = callOriginal(); // Dump the script to a file diff --git a/src/game/t4/mp/components/image_loader.cpp b/src/game/t4/mp/components/image_loader.cpp index a81e9fbc..29ea765b 100644 --- a/src/game/t4/mp/components/image_loader.cpp +++ b/src/game/t4/mp/components/image_loader.cpp @@ -1,558 +1,615 @@ #include "pch.h" +#include "common/config.h" #include "image_loader.h" +#include "image/dds_loader.h" +#include "image/texture_layout.h" +#include "image/xenos_texture.h" namespace { -void GPUEndianSwapTexture(std::vector &pixelData, GPUENDIAN endianType) +namespace game = t4::mp; + +const int CON_CHANNEL_ERROR = 1; +const int CON_CHANNEL_CONSOLEONLY = 7; + +std::set g_streamedImageReplacements; + +typedef image::DdsImage DDSImage; + +DDSImage ReadDDSFile(const std::string &filepath) { - switch (endianType) - { - case GPUENDIAN_8IN16: - XGEndianSwapMemory(pixelData.data(), pixelData.data(), XGENDIAN_8IN16, 2, pixelData.size() / 2); - break; - case GPUENDIAN_8IN32: - XGEndianSwapMemory(pixelData.data(), pixelData.data(), XGENDIAN_8IN32, 4, pixelData.size() / 4); - break; - case GPUENDIAN_16IN32: - XGEndianSwapMemory(pixelData.data(), pixelData.data(), XGENDIAN_16IN32, 4, pixelData.size() / 4); - break; - } + return image::LoadDdsFromFile(filepath); } -// Function to swap all necessary fields from little-endian to big-endian -void SwapDDSHeaderEndian(DDSHeader &header) +std::string GetReplacementPath(const char *imageName) { - header.magic = _byteswap_ulong(header.magic); - header.size = _byteswap_ulong(header.size); - header.flags = _byteswap_ulong(header.flags); - header.height = _byteswap_ulong(header.height); - header.width = _byteswap_ulong(header.width); - header.pitchOrLinearSize = _byteswap_ulong(header.pitchOrLinearSize); - header.depth = _byteswap_ulong(header.depth); - header.mipMapCount = _byteswap_ulong(header.mipMapCount); - - for (int i = 0; i < 11; i++) - header.reserved1[i] = _byteswap_ulong(header.reserved1[i]); - - header.pixelFormat.size = _byteswap_ulong(header.pixelFormat.size); - header.pixelFormat.flags = _byteswap_ulong(header.pixelFormat.flags); - header.pixelFormat.fourCC = _byteswap_ulong(header.pixelFormat.fourCC); - header.pixelFormat.rgbBitCount = _byteswap_ulong(header.pixelFormat.rgbBitCount); - header.pixelFormat.rBitMask = _byteswap_ulong(header.pixelFormat.rBitMask); - header.pixelFormat.gBitMask = _byteswap_ulong(header.pixelFormat.gBitMask); - header.pixelFormat.bBitMask = _byteswap_ulong(header.pixelFormat.bBitMask); - header.pixelFormat.aBitMask = _byteswap_ulong(header.pixelFormat.aBitMask); - - header.caps = _byteswap_ulong(header.caps); - header.caps2 = _byteswap_ulong(header.caps2); - header.caps3 = _byteswap_ulong(header.caps3); - header.caps4 = _byteswap_ulong(header.caps4); - header.reserved2 = _byteswap_ulong(header.reserved2); + const std::string userPath = std::string(USERRAW_DIR) + "\\images\\" + imageName + ".dds"; + if (filesystem::file_exists(userPath)) + return userPath; + + return Config::GetModBasePath() + "\\images\\" + imageName + ".dds"; } -DDSImage ReadDDSFile(const std::string &filepath) +bool ValidateDDSHeader(const game::GfxImage *image, const DDSImage &ddsImage, const std::string &path, + GPUTEXTUREFORMAT *ddsFormat) { - DDSImage ddsImage; - std::ifstream file(filepath, std::ios::binary); - - if (!file.is_open()) + if (ddsImage.data.empty()) { - DbgPrint("ERROR: Unable to open file: %s\n", filepath.c_str()); - return ddsImage; // Return empty DDSImage + game::Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace: Failed to load DDS file for image '%s': %s\n", + image->name, path.c_str()); + return false; } - // Read DDS header (raw, little-endian) - file.read(reinterpret_cast(&ddsImage.header), sizeof(DDSHeader)); - - // Swap only the magic number to big-endian for proper validation - uint32_t magicSwapped = _byteswap_ulong(ddsImage.header.magic); - - if (magicSwapped != 0x20534444) // 'DDS ' in big-endian + if (ddsImage.header.dwSize != image::DDS_HEADER_SIZE || + ddsImage.header.ddspf.dwSize != image::DDS_PIXEL_FORMAT_SIZE) { - DbgPrint("ERROR: Invalid DDS file: %s\n", filepath.c_str()); - file.close(); - return ddsImage; + game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has an invalid DDS header: size=%u pixelFormatSize=%u\n", + image->name, ddsImage.header.dwSize, ddsImage.header.ddspf.dwSize); + return false; } - // Swap header fields to big-endian for Xbox 360 - SwapDDSHeaderEndian(ddsImage.header); - - // Move to end of file to get total file size - file.seekg(0, std::ios::end); - std::streampos fileSize = file.tellg(); + if (!ddsImage.GetGpuFormat(ddsFormat)) + { + game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has an unsupported DDS format: flags=0x%X fourCC=0x%X\n", + image->name, ddsImage.header.ddspf.dwFlags, ddsImage.header.ddspf.dwFourCC); + return false; + } - // Ensure fileSize is valid before proceeding - if (fileSize == std::streampos(-1)) + if (static_cast(image->texture.basemap->Format.DataFormat) != static_cast(*ddsFormat)) { - DbgPrint("ERROR: Failed to determine file size.\n"); - file.close(); - return ddsImage; + game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' format does not match DDS file: Expected %u, Got %u\n", + image->name, static_cast(image->texture.basemap->Format.DataFormat), + static_cast(*ddsFormat)); + return false; } - // Move back to after the header - file.seekg(sizeof(DDSHeader), std::ios::beg); + return true; +} - // Compute data size safely - size_t dataSize = static_cast(fileSize) - sizeof(DDSHeader); +bool Validate2DReplacementData(const game::GfxImage *image, const DDSImage &ddsImage, GPUTEXTUREFORMAT format, + uint32_t ddsFirstMipLevel, uint32_t replacementLevelCount, size_t *requiredDDSSize, + size_t *requiredTextureBytes) +{ + const size_t ddsMipOffset = + image::CalculateDdsMipOffset(ddsImage.header.dwWidth, ddsImage.header.dwHeight, format, ddsFirstMipLevel); + const size_t requiredLinearSize = image::CalculateRequiredLinearDataSize( + ddsImage.header.dwWidth, ddsImage.header.dwHeight, format, ddsFirstMipLevel, replacementLevelCount, 1u); + *requiredDDSSize = ddsMipOffset + requiredLinearSize; + if (requiredLinearSize == 0 || (ddsFirstMipLevel > 0 && ddsMipOffset == 0)) + return false; + + if (ddsImage.data.size() < *requiredDDSSize) + return false; + + const uint32_t baseSize = + image::xenos_texture::CalculateBaseSize(image->texture.basemap, image->width, image->height, 1u); + const size_t mipBytes = + image::CalculateRequiredMipTextureBytes(image->width, image->height, format, 1u, replacementLevelCount, 1u); + *requiredTextureBytes = static_cast(baseSize) + mipBytes; + const int cardMemory = image->cardMemory.platform[0]; + if (cardMemory > 0 && *requiredTextureBytes > static_cast(cardMemory)) + return false; + + return true; +} - // Read image data - ddsImage.data.resize(dataSize); - file.read(reinterpret_cast(ddsImage.data.data()), dataSize); +bool ValidateCubeReplacementData(const game::GfxImage *image, const DDSImage &ddsImage, GPUTEXTUREFORMAT format, + uint32_t faceSize, uint32_t tiledBaseSize, size_t *requiredDDSSize) +{ + *requiredDDSSize = static_cast(faceSize) * 6u; + if (faceSize == 0 || *requiredDDSSize == 0) + return false; - file.close(); + if (ddsImage.data.size() < *requiredDDSSize) + return false; - // Debug output - DbgPrint("INFO: DDS file '%s' loaded successfully.\n", filepath.c_str()); - DbgPrint(" Resolution: %ux%u\n", ddsImage.header.width, ddsImage.header.height); - DbgPrint(" MipMaps: %u\n", ddsImage.header.mipMapCount); - DbgPrint(" Data Size: %u bytes\n", static_cast(dataSize)); + const int cardMemory = image->cardMemory.platform[0]; + if (cardMemory > 0 && static_cast(tiledBaseSize) > static_cast(cardMemory)) + return false; - return ddsImage; + return true; } -} // namespace -namespace t4 +bool Image_Replace_2D(game::GfxImage *image, const DDSImage &ddsImage, uint32_t ddsFirstMipLevel) { -namespace mp -{ -void ImageLoader::DumpGfxImage(const GfxImage *image) -{ - DbgPrint("Image_Dump: Dumping image '%s'\n", image->name); - - if (!image) + if (image->mapType != game::MAPTYPE_2D) { - DbgPrint("Image_Dump: Null GfxImage!\n"); - return; + game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a 2D map!\n", image->name); + return false; } - if (!image->pixels || image->baseSize == 0) + const GPUTEXTUREFORMAT format = static_cast(image->texture.basemap->Format.DataFormat); + const uint32_t levelCount = image::xenos_texture::GetTextureLevelCount(image->texture.basemap); + const uint32_t mipTailBaseLevel = image->texture.basemap->Format.PackedMips != 0 + ? image::xenos_texture::GetMipTailBaseLevel(image->width, image->height) + : levelCount; + const uint32_t ddsMipCount = ddsImage.GetMipCount(); + if (ddsFirstMipLevel >= ddsMipCount) { - DbgPrint("Image_Dump: Image '%s' has no valid pixel data!\n", image->name); - return; + game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' replacement DDS has no mip %u: mipCount=%u\n", image->name, + ddsFirstMipLevel, ddsMipCount); + return false; } - if (image->mapType != MAPTYPE_2D && image->mapType != MAPTYPE_CUBE) + const uint32_t replaceLevelCount = min(levelCount, ddsMipCount - ddsFirstMipLevel); + const uint32_t nonPackedLevelCount = max(1u, min(replaceLevelCount, mipTailBaseLevel)); + unsigned char *baseData = image::xenos_texture::GetTextureBase(image->texture.basemap, image->pixels); + unsigned char *mipData = image::xenos_texture::GetTextureMipBase(image->texture.basemap, baseData, image->width, + image->height, format, 1u); + + size_t requiredDDSSize = 0; + size_t requiredTextureBytes = 0; + if (!Validate2DReplacementData(image, ddsImage, format, ddsFirstMipLevel, nonPackedLevelCount, &requiredDDSSize, + &requiredTextureBytes)) { - DbgPrint("Image_Dump: Unsupported map type %d!\n", image->mapType); - return; + if (requiredDDSSize == 0) + { + game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has unsupported replacement format %u!\n", image->name, + static_cast(format)); + } + else if (ddsImage.data.size() < requiredDDSSize) + { + game::Com_PrintError(CON_CHANNEL_ERROR, + "Image '%s' DDS data is too small: have=%u need=%u for %u mip levels\n", image->name, + static_cast(ddsImage.data.size()), + static_cast(requiredDDSSize), nonPackedLevelCount); + } + else + { + game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' replacement exceeds texture memory: have=%u need=%u\n", + image->name, static_cast(image->cardMemory.platform[0]), + static_cast(requiredTextureBytes)); + } + + return false; } - UINT BaseSize; - XGGetTextureLayout(image->texture.basemap, 0, &BaseSize, 0, 0, 0, 0, 0, 0, 0, 0); - - DDSHeader header; - memset(&header, 0, sizeof(DDSHeader)); - - header.magic = _byteswap_ulong(DDS_MAGIC); - header.size = _byteswap_ulong(DDS_HEADER_SIZE); - header.flags = _byteswap_ulong(DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_LINEARSIZE); - header.height = _byteswap_ulong(image->height); - header.width = _byteswap_ulong(image->width); - header.depth = _byteswap_ulong(image->depth); - header.mipMapCount = _byteswap_ulong(image->texture.basemap->GetLevelCount()); - - auto format = image->texture.basemap->Format.DataFormat; - switch (format) - { - case GPUTEXTUREFORMAT_DXT1: - header.pixelFormat.fourCC = _byteswap_ulong(DXT1_FOURCC); - header.pitchOrLinearSize = BaseSize; - break; - case GPUTEXTUREFORMAT_DXT2_3: - header.pixelFormat.fourCC = _byteswap_ulong(DXT3_FOURCC); - header.pitchOrLinearSize = BaseSize; - break; - case GPUTEXTUREFORMAT_DXT4_5: - header.pixelFormat.fourCC = _byteswap_ulong(DXT5_FOURCC); - header.pitchOrLinearSize = BaseSize; - break; - case GPUTEXTUREFORMAT_DXN: - header.pixelFormat.fourCC = _byteswap_ulong(DXN_FOURCC); - header.pitchOrLinearSize = BaseSize; - break; - case GPUTEXTUREFORMAT_8: - header.pixelFormat.flags = _byteswap_ulong(DDPF_LUMINANCE); - header.pixelFormat.rgbBitCount = _byteswap_ulong(8); - header.pixelFormat.rBitMask = _byteswap_ulong(0x000000FF); - header.pixelFormat.gBitMask = 0; - header.pixelFormat.bBitMask = 0; - header.pixelFormat.aBitMask = 0; - header.pitchOrLinearSize = BaseSize; - break; - case GPUTEXTUREFORMAT_8_8: - header.pixelFormat.flags = _byteswap_ulong(DDPF_LUMINANCE | DDPF_ALPHAPIXELS); - header.pixelFormat.rgbBitCount = _byteswap_ulong(16); - header.pixelFormat.rBitMask = _byteswap_ulong(0x000000FF); - header.pixelFormat.gBitMask = _byteswap_ulong(0x0000FF00); - header.pixelFormat.bBitMask = 0; - header.pixelFormat.aBitMask = 0; - header.pitchOrLinearSize = BaseSize; - break; - case GPUTEXTUREFORMAT_8_8_8_8: - header.pixelFormat.flags = _byteswap_ulong(DDPF_RGB | DDPF_ALPHAPIXELS); - header.pixelFormat.rgbBitCount = _byteswap_ulong(32); - header.pixelFormat.rBitMask = _byteswap_ulong(0x00FF0000); - header.pixelFormat.gBitMask = _byteswap_ulong(0x0000FF00); - header.pixelFormat.bBitMask = _byteswap_ulong(0x000000FF); - header.pixelFormat.aBitMask = _byteswap_ulong(0xFF000000); - header.pitchOrLinearSize = BaseSize; - break; - default: - DbgPrint("Image_Dump: Unsupported texture format %d!\n", format); - return; + if (baseData == NULL || mipData == NULL) + { + game::Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_2D: Image '%s' has no valid texture memory!\n", + image->name); + return false; } - // Set texture capabilities - header.caps = _byteswap_ulong(DDSCAPS_TEXTURE | DDSCAPS_MIPMAP); + uint32_t ddsOffset = + image::CalculateDdsMipOffset(ddsImage.header.dwWidth, ddsImage.header.dwHeight, format, ddsFirstMipLevel); - // Handle Cubemaps - if (image->mapType == mp::MAPTYPE_CUBE) + for (uint32_t localMipLevel = 0; localMipLevel < nonPackedLevelCount; localMipLevel++) { - header.caps2 = _byteswap_ulong(DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX | DDSCAPS2_CUBEMAP_NEGATIVEX | - DDSCAPS2_CUBEMAP_POSITIVEY | DDSCAPS2_CUBEMAP_NEGATIVEY | - DDSCAPS2_CUBEMAP_POSITIVEZ | DDSCAPS2_CUBEMAP_NEGATIVEZ); - } + const uint32_t ddsMipLevel = ddsFirstMipLevel + localMipLevel; + const uint32_t rowPitch = + image::xenos_texture::CalculateLinearRowPitch(ddsImage.header.dwWidth, ddsMipLevel, format); + const uint32_t ddsMipLevelSize = image::xenos_texture::CalculateLinearLevelSize( + ddsImage.header.dwWidth, ddsImage.header.dwHeight, ddsMipLevel, format); + const uint32_t tiledMipLevelSize = image::xenos_texture::CalculateTiledLevelSize( + image->width, image->height, localMipLevel, format, image->texture.basemap->Format.Pitch); + + if (ddsMipLevelSize == 0 || tiledMipLevelSize == 0 || rowPitch == 0) + { + game::Com_PrintError(CON_CHANNEL_ERROR, + "Image_Replace_2D: Unsupported format %u for image '%s' mip level %u\n", + image->texture.basemap->Format.DataFormat, image->name, localMipLevel); + return false; + } - std::string filename = "game:\\_codxe\\dump\\images\\"; - std::string sanitized_name = image->name; + if (static_cast(ddsOffset) + ddsMipLevelSize > ddsImage.data.size()) + { + game::Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_2D: Image '%s' mip level %u exceeds DDS data size\n", + image->name, ddsMipLevel); + return false; + } + + std::vector levelData(ddsImage.data.begin() + ddsOffset, + ddsImage.data.begin() + ddsOffset + ddsMipLevelSize); + image::xenos_texture::ApplyGpuEndian(&levelData[0], levelData.size(), + static_cast(image->texture.basemap->Format.Endian)); - // Remove invalid characters - sanitized_name.erase(std::remove_if(sanitized_name.begin(), sanitized_name.end(), [](char c) { return c == '*'; }), - sanitized_name.end()); + unsigned char *destination = baseData; + if (localMipLevel > 0) + { + destination = mipData + image::xenos_texture::CalculateMipLevelOffset(image->width, image->height, + localMipLevel, format, 1u); + } - filename += sanitized_name + ".dds"; + std::vector tiledData(tiledMipLevelSize); + if (!image::xenos_texture::TileTextureLevel(image->width, image->height, localMipLevel, format, + image->texture.basemap->Format.Pitch, &tiledData[0], + tiledData.size(), &levelData[0], levelData.size(), rowPitch)) + { + game::Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_2D: Failed to tile mip level %u for image '%s'\n", + localMipLevel, image->name); + return false; + } + + memcpy(destination, &tiledData[0], tiledMipLevelSize); + ddsOffset += ddsMipLevelSize; + } - std::ofstream file(filename, std::ios::binary); - if (!file) + return true; +} + +bool Image_Replace_Cube(game::GfxImage *image, const DDSImage &ddsImage) +{ + if (image->mapType != game::MAPTYPE_CUBE) { - DbgPrint("Image_Dump: Failed to open file: %s\n", filename.c_str()); - return; + game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a cube map!\n", image->name); + return false; } - if (image->mapType == MAPTYPE_CUBE) + const GPUTEXTUREFORMAT format = static_cast(image->texture.basemap->Format.DataFormat); + const uint32_t faceSize = image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0u, format); + const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(image->width, 0u, format); + const uint32_t tiledFaceSize = image::xenos_texture::CalculateTiledLevelSize( + image->width, image->height, 0u, format, image->texture.basemap->Format.Pitch); + const uint32_t tiledBaseSize = + image::xenos_texture::CalculateBaseSize(image->texture.basemap, image->width, image->height, 6u); + unsigned char *baseData = image::xenos_texture::GetTextureBase(image->texture.basemap, image->pixels); + + if (faceSize == 0 || rowPitch == 0 || tiledFaceSize == 0 || tiledBaseSize < tiledFaceSize * 6u) { - file.write(reinterpret_cast(&header), sizeof(DDSHeader)); + game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has unsupported format %u!\n", image->name, + static_cast(format)); + return false; + } - unsigned int face_size = 0; - unsigned int rowPitch = 0; - const GPUTEXTUREFORMAT format = static_cast(image->texture.basemap->Format.DataFormat); + if (baseData == NULL) + { + game::Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_Cube: Image '%s' has no valid texture memory!\n", + image->name); + return false; + } - switch (format) + size_t requiredDDSSize = 0; + if (!ValidateCubeReplacementData(image, ddsImage, format, faceSize, tiledBaseSize, &requiredDDSSize)) + { + if (requiredDDSSize == 0) { - case GPUTEXTUREFORMAT_DXT1: - face_size = (image->width / 4) * (image->height / 4) * 8; - rowPitch = (image->width / 4) * 8; // 8 bytes per 4x4 block - break; - case GPUTEXTUREFORMAT_8_8_8_8: - face_size = image->width * image->height * 4; - rowPitch = image->width * 4; // 4 bytes per pixel - break; - default: - DbgPrint("Image_Dump: Unsupported cube map format %d!\n", format); - return; + game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has unsupported cube replacement format %u!\n", + image->name, static_cast(format)); } - - // TODO: handle mip levels per face for cubemaps - for (int i = 0; i < 6; i++) + else if (ddsImage.data.size() < requiredDDSSize) + { + game::Com_PrintError(CON_CHANNEL_ERROR, + "Image_Replace_Cube: Image '%s' DDS is too small for 6 faces: have=%u need=%u\n", + image->name, static_cast(ddsImage.data.size()), + static_cast(requiredDDSSize)); + } + else { - unsigned char *face_pixels = image->pixels + (i * face_size); // Offset for each face - - std::vector swappedFace(face_pixels, face_pixels + face_size); - GPUEndianSwapTexture(swappedFace, static_cast(image->texture.basemap->Format.Endian)); - - // Create buffer for linear texture data - std::vector linearFace(face_size); - - // Convert tiled texture to linear layout using XGUntileTextureLevel - XGUntileTextureLevel(image->width, // Width - image->height, // Height - 0, // Level (base level) - static_cast(format), // GpuFormat - 0, // Flags (no special flags) - linearFace.data(), // pDestination (linear output) - rowPitch, // RowPitch - nullptr, // pPoint (no offset) - swappedFace.data(), // pSource (tiled input) - nullptr // pRect (entire texture) - ); - - file.write(reinterpret_cast(linearFace.data()), linearFace.size()); + game::Com_PrintError(CON_CHANNEL_ERROR, + "Image '%s' cube replacement exceeds texture memory: have=%u " + "need=%u\n", + image->name, static_cast(image->cardMemory.platform[0]), tiledBaseSize); } - file.close(); + return false; } - else if (image->mapType == MAPTYPE_2D) + + for (uint32_t faceIndex = 0; faceIndex < 6u; faceIndex++) { - // TODO: write mip levels - file.write(reinterpret_cast(&header), sizeof(DDSHeader)); + const unsigned char *facePixels = &ddsImage.data[faceIndex * faceSize]; + unsigned char *faceDestination = baseData + (faceIndex * tiledFaceSize); + std::vector tiledData(tiledFaceSize); + + if (!image::xenos_texture::TileTextureLevel(image->width, image->height, 0u, static_cast(format), + image->texture.basemap->Format.Pitch, &tiledData[0], + tiledData.size(), facePixels, faceSize, rowPitch)) + { + game::Com_PrintError(CON_CHANNEL_ERROR, "Image_Replace_Cube: Failed to tile image '%s' face %u\n", + image->name, faceIndex); + return false; + } + + image::xenos_texture::ApplyGpuEndian(&tiledData[0], tiledData.size(), + static_cast(image->texture.basemap->Format.Endian)); + memcpy(faceDestination, &tiledData[0], tiledFaceSize); + } + + return true; +} - std::vector pixelData(image->pixels, image->pixels + image->baseSize); +void Image_Replace(game::GfxImage *image) +{ + if (image == NULL || image->name == NULL || image->texture.basemap == NULL) + return; + + const std::string replacementPath = GetReplacementPath(image->name); + if (!filesystem::file_exists(replacementPath)) + return; - GPUEndianSwapTexture(pixelData, static_cast(image->texture.basemap->Format.Endian)); + DDSImage ddsImage = ReadDDSFile(replacementPath); + GPUTEXTUREFORMAT ddsFormat; + if (!ValidateDDSHeader(image, ddsImage, replacementPath, &ddsFormat)) + return; - // Create a linear data buffer to hold the untiled texture - std::vector linearData(image->baseSize); + const bool ddsIsCubemap = ddsImage.IsCubemap(); + const bool ddsMatchesImageDimensions = + image->width == ddsImage.header.dwWidth && image->height == ddsImage.header.dwHeight; + const bool ddsMatchesStreamDimensions = image->streaming && image->mapType == game::MAPTYPE_2D && !ddsIsCubemap && + ddsImage.header.dwWidth == static_cast(image->width) * 2u && + ddsImage.header.dwHeight == static_cast(image->height) * 2u; + uint32_t ddsFirstMipLevel = 0; - // Calculate row pitch based on format - UINT rowPitch; - auto format = image->texture.basemap->Format.DataFormat; + if (image->mapType == game::MAPTYPE_2D && ddsIsCubemap) + { + game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is 2D but replacement DDS is a cubemap!\n", image->name); + return; + } - switch (format) + if (image->streaming && image->mapType == game::MAPTYPE_2D) + { + if (!ddsMatchesStreamDimensions) { - case GPUTEXTUREFORMAT_DXT1: - case GPUTEXTUREFORMAT_DXT2_3: - case GPUTEXTUREFORMAT_DXT4_5: - case GPUTEXTUREFORMAT_DXN: - // Block compressed formats use 4x4 blocks - rowPitch = ((image->width + 3) / 4) * (format == GPUTEXTUREFORMAT_DXT1 ? 8 : 16); - break; - case GPUTEXTUREFORMAT_8: - rowPitch = image->width; - break; - case GPUTEXTUREFORMAT_8_8: - rowPitch = image->width * 2; - break; - case GPUTEXTUREFORMAT_8_8_8_8: - rowPitch = image->width * 4; - break; - default: - DbgPrint("Image_Dump: Unsupported texture format %d!\n", format); + game::Com_PrintError(CON_CHANNEL_ERROR, + "Streamed image '%s' replacement must include the streamed mip: expected=%ux%u " + "got=%ux%u %s\n", + image->name, static_cast(image->width) * 2u, + static_cast(image->height) * 2u, ddsImage.header.dwWidth, + ddsImage.header.dwHeight, replacementPath.c_str()); return; } - // Call XGUntileTextureLevel to convert the tiled texture to linear format - XGUntileTextureLevel(image->width, // Width - image->height, // Height - 0, // Level (base level 0) - static_cast(format), // GpuFormat - XGTILE_NONPACKED, // Flags (no special flags) - linearData.data(), // pDestination - rowPitch, // RowPitch (calculated based on format) - nullptr, // pPoint (no offset) - pixelData.data(), // pSource - nullptr // pRect (entire texture) - ); + const uint32_t ddsMipCount = ddsImage.GetMipCount(); + if (ddsMipCount < 2u) + { + game::Com_PrintError(CON_CHANNEL_ERROR, + "Image '%s' replacement DDS starts at the streamed mip but has no resident mip: " + "%ux%u mipCount=%u\n", + image->name, ddsImage.header.dwWidth, ddsImage.header.dwHeight, ddsMipCount); + return; + } - file.write(reinterpret_cast(linearData.data()), linearData.size()); + ddsFirstMipLevel = 1u; + } + else if (!ddsMatchesImageDimensions) + { + game::Com_PrintError(CON_CHANNEL_ERROR, + "Image '%s' dimensions do not match DDS file: image=%ux%u dds=%ux%u " + "%s\n", + image->name, image->width, image->height, ddsImage.header.dwWidth, + ddsImage.header.dwHeight, replacementPath.c_str()); + return; + } - file.close(); + if (image->mapType == game::MAPTYPE_CUBE && !ddsIsCubemap) + { + const uint32_t faceSize = + image::xenos_texture::CalculateLinearLevelSize(image->width, image->height, 0u, ddsFormat); + if (faceSize == 0 || ddsImage.data.size() < static_cast(faceSize) * 6u) + { + game::Com_PrintError(CON_CHANNEL_ERROR, + "Image '%s' is a cubemap but replacement DDS is not a valid 6-face cubemap!\n", + image->name); + return; + } + + game::Com_Printf(CON_CHANNEL_CONSOLEONLY, + "Image '%s' replacement DDS has no cubemap caps but contains enough data for 6 sequential " + "faces; accepting it.\n", + image->name); } + + bool replaced = false; + if (image->mapType == game::MAPTYPE_2D) + replaced = Image_Replace_2D(image, ddsImage, ddsFirstMipLevel); + else if (image->mapType == game::MAPTYPE_CUBE) + replaced = Image_Replace_Cube(image, ddsImage); else + game::Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a 2D or cube map!\n", image->name); + + if (replaced) { - DbgPrint("Image_Dump: Unsupported map type %d!\n", image->mapType); - return; + if (ddsFirstMipLevel > 0) + g_streamedImageReplacements.insert(image->name); + + game::Com_Printf(CON_CHANNEL_CONSOLEONLY, "Replaced image '%s'\n", image->name); } } -unsigned int ImageLoader::CalculateMipLevelSize(unsigned int width, unsigned int height, unsigned int mipLevel, - GPUTEXTUREFORMAT format) +void Load_images() { - // Calculate dimensions for the requested mip level - unsigned int mipWidth = max(1, width >> mipLevel); - unsigned int mipHeight = max(1, height >> mipLevel); - - // Calculate size based on format - unsigned int blockSize; - switch (format) - { - case GPUTEXTUREFORMAT_DXT1: - blockSize = 8; // 8 bytes per 4x4 block - break; - case GPUTEXTUREFORMAT_DXT2_3: - blockSize = 16; // 16 bytes per 4x4 block - break; - case GPUTEXTUREFORMAT_DXT4_5: - blockSize = 16; // 16 bytes per 4x4 block - break; - case GPUTEXTUREFORMAT_DXN: - blockSize = 16; // 16 bytes per 4x4 block (two 8-byte channels) - break; - default: - DbgPrint("CalculateMipLevelSize: Unsupported format %d\n", format); - return 0; - } - - // For block-compressed formats, calculate number of blocks - // Each block is 4x4 pixels, so we need to round up to nearest block - unsigned int blocksWide = (mipWidth + 3) / 4; - unsigned int blocksHigh = (mipHeight + 3) / 4; - - // Calculate total size in bytes - unsigned int sizeInBytes = blocksWide * blocksHigh * blockSize; - - return sizeInBytes; -} + g_streamedImageReplacements.clear(); -void ImageLoader::ReplaceGfxImage2d(GfxImage *image, const DDSImage &ddsImage) -{ - if (image->mapType != MAPTYPE_2D) + const int MAX_IMAGES = 2048; + game::XAssetHeader assets[MAX_IMAGES]; + const int count = game::DB_GetAllXAssetOfType_FastFile(game::ASSET_TYPE_IMAGE, assets, MAX_IMAGES); + for (int i = 0; i < count; i++) { - DbgPrint("Image '%s' is not a 2D map!\n", image->name); - return; + game::GfxImage *image = assets[i].image; + Image_Replace(image); } +} - // Get base texture layout - UINT baseAddress, baseSize, mipAddress, mipSize; - - XGGetTextureLayout(image->texture.basemap, &baseAddress, &baseSize, 0, 0, 0, &mipAddress, &mipSize, 0, 0, 0); - - XGTEXTURE_DESC TextureDesc; - XGGetTextureDesc(image->texture.basemap, 0, &TextureDesc); +uint32_t GetStreamPriority(double imageDistSq) +{ + if (imageDistSq > 0.0) + { + if (imageDistSq > 10000.0) + return imageDistSq > 90000.0 ? 1u : 2u; - UINT mipTailBaseLevel = - XGGetMipTailBaseLevel(TextureDesc.Width, TextureDesc.Height, XGIsBorderTexture(image->texture.basemap)); + return 3u; + } - UINT ddsOffset = 0; + return 5u; +} - for (UINT mipLevel = 0; mipLevel < mipTailBaseLevel; mipLevel++) +bool FinalizeStreamReplacement(game::StreamAllocBlockInfo *block, uint32_t streamSlot, game::GfxImage *image, + unsigned char *destination, uint32_t streamPriority) +{ + if (streamPriority == 5u) { - UINT widthInBlocks = max(1, TextureDesc.WidthInBlocks >> mipLevel); - UINT rowPitch = widthInBlocks * TextureDesc.BytesPerBlock; - // UINT levelSize = rowPitch * heightInBlocks; - UINT ddsMipLevelSize = - CalculateMipLevelSize(image->width, image->height, mipLevel, - static_cast(image->texture.basemap->Format.DataFormat)); - - if (ddsMipLevelSize == 0) + while (!game::RB_StreamQueueCommandSetHighMip(image, destination)) { - DbgPrint(" [ERROR] Unsupported format %d for mip level %u! Skipping...\n", - image->texture.basemap->Format.DataFormat, mipLevel); - break; } + } - // Ensure we're not reading out of bounds - if (ddsOffset + ddsMipLevelSize > ddsImage.data.size()) - { - DbgPrint(" [ERROR] Mip Level %u exceeds DDS data size! Skipping...\n", mipLevel); - break; - } + game::R_StreamAlloc_SetImage(block, static_cast<__int16>(streamSlot), image); + return true; +} - std::vector levelData(ddsImage.data.begin() + ddsOffset, - ddsImage.data.begin() + ddsOffset + ddsMipLevelSize); +bool R_StreamLoadImageReplacement(game::GfxImage *image, double imageDistSq) +{ + if (image == NULL || image->name == NULL || image->texture.basemap == NULL || image->mapType != game::MAPTYPE_2D || + !image->streaming) + { + return false; + } - GPUEndianSwapTexture(levelData, static_cast(image->texture.basemap->Format.Endian)); + if (g_streamedImageReplacements.find(image->name) == g_streamedImageReplacements.end()) + return false; - DbgPrint("Image_Replace_2D: Mip Level %d - Row Pitch=%u\n", mipLevel, rowPitch); + const std::string replacementPath = GetReplacementPath(image->name); + if (!filesystem::file_exists(replacementPath)) + { + game::Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadImageReplacement: Blocking stock stream for replaced " + "image '%s' because DDS is missing\n", + image->name); + return true; + } - UINT address = baseAddress; - if (mipLevel > 0) - { - UINT mipLevelOffset = XGGetMipLevelOffset(image->texture.basemap, 0, mipLevel); - address = mipAddress + mipLevelOffset; - } + DDSImage ddsImage = ReadDDSFile(replacementPath); + GPUTEXTUREFORMAT ddsFormat; + if (!ValidateDDSHeader(image, ddsImage, replacementPath, &ddsFormat)) + { + game::Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadImageReplacement: Blocking stock stream for replaced image '%s'\n", + image->name); + return true; + } - DbgPrint("Image_Replace_2D: Writing mip level %d to address 0x%08X - levelSize=%u\n", mipLevel, address, - ddsMipLevelSize); + const uint32_t streamWidth = static_cast(image->width) * 2u; + const uint32_t streamHeight = static_cast(image->height) * 2u; + if (ddsImage.IsCubemap() || ddsImage.header.dwWidth != streamWidth || ddsImage.header.dwHeight != streamHeight) + { + game::Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadImageReplacement: Image '%s' dimensions do not match streamed mip: " + "expected=%ux%u got=%ux%u\n", + image->name, streamWidth, streamHeight, ddsImage.header.dwWidth, ddsImage.header.dwHeight); + return true; + } - // // Write the base level - XGTileTextureLevel(TextureDesc.Width, TextureDesc.Height, mipLevel, image->texture.basemap->Format.DataFormat, - XGTILE_NONPACKED, // Use non-packed mode (likely required for this texture) - reinterpret_cast(address), // Destination (tiled GPU memory for Base) - nullptr, // No offset (tile the whole image) - levelData.data(), // Source mip level data - rowPitch, // Row pitch of source image (should match DDS format) - nullptr // No subrectangle (tile the full image) - ); + if (ddsImage.GetMipCount() < 2u) + { + game::Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadImageReplacement: Image '%s' replacement DDS must include stream and " + "resident mips: mipCount=%u\n", + image->name, ddsImage.GetMipCount()); + return true; + } - ddsOffset += ddsMipLevelSize; + if (image->baseSize == 0 || image->baseSize > 0x3FFFFFFFu) + { + game::Com_PrintError(CON_CHANNEL_ERROR, "R_StreamLoadImageReplacement: Image '%s' has invalid stream size %u\n", + image->name, image->baseSize); + return true; } -} -void ImageLoader::ReplaceGfxImage(GfxImage *image) -{ - const std::string replacement_base_dir = "game:\\_codxe\\raw\\images"; - const std::string replacement_path = replacement_base_dir + "\\" + image->name + ".dds"; + const uint32_t streamSize = image->baseSize * 4u; + const uint32_t sourceSize = + image::xenos_texture::CalculateLinearLevelSize(streamWidth, streamHeight, 0u, ddsFormat); + const uint32_t rowPitch = image::xenos_texture::CalculateLinearRowPitch(streamWidth, 0u, ddsFormat); + const uint32_t tiledSize = + image::xenos_texture::CalculateTiledLevelSize(streamWidth, streamHeight, 0u, ddsFormat, 0u); - if (!filesystem::file_exists(replacement_path)) + if (sourceSize == 0 || rowPitch == 0 || tiledSize == 0) { - DbgPrint("File does not exist: %s\n", replacement_path.c_str()); - return; + game::Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadImageReplacement: Image '%s' has unsupported replacement format %u\n", + image->name, static_cast(ddsFormat)); + return true; } - DDSImage ddsImage = ReadDDSFile(replacement_path.c_str()); - if (ddsImage.data.empty()) + if (ddsImage.data.size() < sourceSize) { - DbgPrint("Failed to load DDS file: %s\n", replacement_path.c_str()); - return; + game::Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadImageReplacement: Image '%s' DDS data is too small: have=%u need=%u\n", + image->name, static_cast(ddsImage.data.size()), sourceSize); + return true; } - if (image->width != ddsImage.header.width || image->height != ddsImage.header.height) + if (tiledSize != streamSize) { - DbgPrint("Image '%s' dimensions do not match DDS file: %s\n", image->name, replacement_path.c_str()); - return; + game::Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadImageReplacement: Image '%s' stream size mismatch: expected=%u got=%u\n", + image->name, streamSize, tiledSize); + return true; } - GPUTEXTUREFORMAT ddsFormat; - switch (ddsImage.header.pixelFormat.fourCC) - { - case DXT1_FOURCC: - ddsFormat = GPUTEXTUREFORMAT_DXT1; - break; - case DXT3_FOURCC: - ddsFormat = GPUTEXTUREFORMAT_DXT2_3; - break; - case DXT5_FOURCC: - ddsFormat = GPUTEXTUREFORMAT_DXT4_5; - break; - case DXN_FOURCC: - ddsFormat = GPUTEXTUREFORMAT_DXN; - break; - default: - DbgPrint("Image '%s' has an unsupported DDS format: 0x%X\n", image->name, ddsImage.header.pixelFormat.fourCC); - return; + game::StreamAllocBlockInfo *streamBlock = NULL; + uint32_t streamSlot = 0; + const uint32_t streamPriority = GetStreamPriority(imageDistSq); + if (!game::R_StreamAlloc_Alloc(streamSize, streamPriority, &streamBlock, &streamSlot)) + { + game::Com_PrintError(CON_CHANNEL_ERROR, + "R_StreamLoadImageReplacement: Failed to allocate stream memory for " + "image '%s' size=%u\n", + image->name, streamSize); + return true; } - if (static_cast(image->texture.basemap->Format.DataFormat) != static_cast(ddsFormat)) + unsigned char *destination = reinterpret_cast(*game::r_streamBufferBase + (streamSlot << 17u)); + if (destination == NULL) { - DbgPrint("Image '%s' format does not match DDS file: Expected %d, Got %d\n", image->name, - static_cast(image->texture.basemap->Format.DataFormat), static_cast(ddsFormat)); - return; + game::Com_PrintError(CON_CHANNEL_ERROR, "R_StreamLoadImageReplacement: Image '%s' stream destination is null\n", + image->name); + return true; } - if (image->mapType == MAPTYPE_2D) + std::vector buffer(ddsImage.data.begin(), ddsImage.data.begin() + sourceSize); + image::xenos_texture::ApplyGpuEndian(&buffer[0], buffer.size(), + static_cast(image->texture.basemap->Format.Endian)); + + if (!image::xenos_texture::TileTextureLevel(streamWidth, streamHeight, 0u, ddsFormat, 0u, destination, streamSize, + &buffer[0], buffer.size(), rowPitch)) { - ReplaceGfxImage2d(image, ddsImage); + memset(destination, 0, streamSize); + game::Com_PrintError(CON_CHANNEL_ERROR, "R_StreamLoadImageReplacement: Failed to tile streamed image '%s'\n", + image->name); + FinalizeStreamReplacement(streamBlock, streamSlot, image, destination, streamPriority); + return true; } - // else if (image->mapType == MAPTYPE_CUBE) - // { - // Image_Replace_Cube(image, ddsImage); - // } - else - { - DbgPrint("Image '%s' is not a 2D or cube map!\n", image->name); + + FinalizeStreamReplacement(streamBlock, streamSlot, image, destination, streamPriority); + game::Com_Printf(CON_CHANNEL_CONSOLEONLY, "Replaced streamed image '%s'\n", image->name); + return true; +} + +Detour CG_RegisterGraphics_Detour; +Detour R_StreamLoadImage_Detour; + +void CG_RegisterGraphics_Hook(int localClientNum, const char *mapname) +{ + CG_RegisterGraphics_Detour.GetOriginal()(localClientNum, mapname); + Load_images(); +} + +void R_StreamLoadImage_Hook(game::GfxImage *image, double imageDistSq) +{ + if (R_StreamLoadImageReplacement(image, imageDistSq)) return; - } + + R_StreamLoadImage_Detour.GetOriginal()(image, imageDistSq); } +} // namespace -ImageLoader::ImageLoader() +namespace t4 { - // Create directories for dumping images - CreateDirectoryA("game:\\_codxe", nullptr); - CreateDirectoryA("game:\\_codxe\\dump", nullptr); - CreateDirectoryA("game:\\_codxe\\dump\\images", nullptr); - CreateDirectoryA("game:\\_codxe\\dump\\highmip", nullptr); - - // // Dump all images from the database - // const auto MAX_IMAGES = 2048; - // XAssetHeader assets[MAX_IMAGES]; - // auto count = DB_GetAllXAssetOfType_FastFile(ASSET_TYPE_IMAGE, assets, MAX_IMAGES); - // DbgPrint("ImageLoader: Found %d images\n", count); - // for (int i = 0; i < count; i++) - // { - // DumpGfxImage(assets[i].image); - // } - - // Replace images - const auto MAX_IMAGES = 2048; - XAssetHeader assets[MAX_IMAGES]; - auto count = DB_GetAllXAssetOfType_FastFile(ASSET_TYPE_IMAGE, assets, MAX_IMAGES); - DbgPrint("Cmd_imageload2_f: Found %d images\n", count); - for (int i = 0; i < count; i++) - ReplaceGfxImage(assets[i].image); +namespace mp +{ +image_loader::image_loader() +{ + CG_RegisterGraphics_Detour = Detour(CG_RegisterGraphics, CG_RegisterGraphics_Hook); + CG_RegisterGraphics_Detour.Install(); + + R_StreamLoadImage_Detour = Detour(R_StreamLoadImage, R_StreamLoadImage_Hook); + R_StreamLoadImage_Detour.Install(); } -ImageLoader::~ImageLoader() +image_loader::~image_loader() { + CG_RegisterGraphics_Detour.Remove(); + R_StreamLoadImage_Detour.Remove(); } } // namespace mp } // namespace t4 diff --git a/src/game/t4/mp/components/image_loader.h b/src/game/t4/mp/components/image_loader.h index 7d0f52ae..e69c4627 100644 --- a/src/game/t4/mp/components/image_loader.h +++ b/src/game/t4/mp/components/image_loader.h @@ -2,95 +2,20 @@ #include "pch.h" -// DDS Constants -const uint32_t DDS_MAGIC = MAKEFOURCC('D', 'D', 'S', ' '); -const uint32_t DDS_HEADER_SIZE = 124; -const uint32_t DDS_PIXEL_FORMAT_SIZE = 32; -const uint32_t DDSD_CAPS = 0x1; -const uint32_t DDSD_HEIGHT = 0x2; -const uint32_t DDSD_WIDTH = 0x4; -const uint32_t DDSD_PIXELFORMAT = 0x1000; -const uint32_t DDSD_LINEARSIZE = 0x80000; -const uint32_t DDPF_FOURCC = 0x4; -const uint32_t DDPF_RGB = 0x40; -const uint32_t DDPF_ALPHAPIXELS = 0x1; -const uint32_t DDSCAPS_TEXTURE = 0x1000; -const uint32_t DDSCAPS_MIPMAP = 0x400000; -const uint32_t DDPF_LUMINANCE = 0x20000; - -// DDS Pixel Formats (FourCC Codes) -const uint32_t DXT1_FOURCC = MAKEFOURCC('D', 'X', 'T', '1'); -const uint32_t DXT3_FOURCC = MAKEFOURCC('D', 'X', 'T', '3'); -const uint32_t DXT5_FOURCC = MAKEFOURCC('D', 'X', 'T', '5'); -const uint32_t DXN_FOURCC = MAKEFOURCC('A', 'T', 'I', '2'); // (DXN / BC5) - -// Additional DDS Cubemap Flags -const uint32_t DDSCAPS2_CUBEMAP = 0x200; -const uint32_t DDSCAPS2_CUBEMAP_POSITIVEX = 0x400; -const uint32_t DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800; -const uint32_t DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000; -const uint32_t DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000; -const uint32_t DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000; -const uint32_t DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000; - -struct DDSHeader -{ - uint32_t magic; - uint32_t size; - uint32_t flags; - uint32_t height; - uint32_t width; - uint32_t pitchOrLinearSize; - uint32_t depth; - uint32_t mipMapCount; - uint32_t reserved1[11]; - struct - { - uint32_t size; - uint32_t flags; - uint32_t fourCC; - uint32_t rgbBitCount; - uint32_t rBitMask; - uint32_t gBitMask; - uint32_t bBitMask; - uint32_t aBitMask; - } pixelFormat; - uint32_t caps; - uint32_t caps2; - uint32_t caps3; - uint32_t caps4; - uint32_t reserved2; -}; -static_assert(sizeof(DDSHeader) == 128, ""); - -struct DDSImage -{ - DDSHeader header; - std::vector data; -}; - namespace t4 { namespace mp { - -class ImageLoader : public Module +class image_loader : public Module { - public: - ImageLoader(); - ~ImageLoader(); + image_loader(); + ~image_loader(); + const char *get_name() override { - return "ImageLoader"; + return "image_loader"; }; - - private: - void DumpGfxImage(const GfxImage *image); - void ImageLoader::ReplaceGfxImage(GfxImage *image); - void ImageLoader::ReplaceGfxImage2d(GfxImage *image, const DDSImage &ddsImage); - unsigned int ImageLoader::CalculateMipLevelSize(unsigned int width, unsigned int height, unsigned int mipLevel, - GPUTEXTUREFORMAT format); }; } // namespace mp } // namespace t4 diff --git a/src/game/t4/mp/components/map.cpp b/src/game/t4/mp/components/map.cpp index 9f1f88a9..22242f17 100644 --- a/src/game/t4/mp/components/map.cpp +++ b/src/game/t4/mp/components/map.cpp @@ -17,7 +17,7 @@ void Load_clipMap_t_Hook(bool atStreamStart) auto mapEnts = (*varclipMap_t)->mapEnts; // Dump map entities if enabled - if (Config::dump_map_ents) + if (Config::dump_assets) { std::string dumpPath = va("%s\\%s.ents", DUMP_DIR, mapEnts->name); // IW4x naming convention std::replace(dumpPath.begin(), dumpPath.end(), '/', '\\'); diff --git a/src/game/t4/mp/main.cpp b/src/game/t4/mp/main.cpp index 39a0ca13..cff372b6 100644 --- a/src/game/t4/mp/main.cpp +++ b/src/game/t4/mp/main.cpp @@ -32,7 +32,7 @@ T4_MP_Plugin::T4_MP_Plugin() RegisterModule(new GSCFunctions()); RegisterModule(new SVBots()); RegisterModule(new GSCLoader()); - // RegisterModule(new ImageLoader()); + RegisterModule(new image_loader()); RegisterModule(new Map()); RegisterModule(new Patches()); RegisterModule(new stats()); diff --git a/src/game/t4/mp/symbols.h b/src/game/t4/mp/symbols.h index 605df41f..5abf34e9 100644 --- a/src/game/t4/mp/symbols.h +++ b/src/game/t4/mp/symbols.h @@ -44,9 +44,13 @@ const int NUM_BSP_OR_DYNAMIC_SPAWNS = 6; static auto s_bspOrDynamicSpawns = reinterpret_cast(0x82035D30); // Functions +struct StreamAllocBlockInfo; + static auto CG_BoldGameMessage = reinterpret_cast(0x8216EC88); static auto CG_DrawActive = reinterpret_cast(0x82159560); static auto CG_GameMessage = reinterpret_cast(0x8216EC68); +typedef void (*CG_RegisterGraphics_t)(int localClientNum, const char *mapname); +static CG_RegisterGraphics_t CG_RegisterGraphics = reinterpret_cast(0x8216F338); static auto CG_Init = reinterpret_cast( 0x82171A30); @@ -72,6 +76,8 @@ static auto CL_WritePacket = reinterpret_cast(0x82 typedef void (*Com_Printf_t)(int channel, const char *fmt, ...); static Com_Printf_t Com_Printf = reinterpret_cast(0x82271BE0); +typedef void (*Com_PrintError_t)(int channel, const char *fmt, ...); +static Com_PrintError_t Com_PrintError = reinterpret_cast(0x82271D00); static auto Com_InitDvars = reinterpret_cast(0x82272BF8); @@ -112,6 +118,18 @@ static auto R_AddCmdDrawText = reinterpret_cast(0x82401C30); static auto R_CheckDvarModified = reinterpret_cast(0x8240D860); +typedef void (*R_StreamLoadImage_t)(GfxImage *image, double imageDistSq); +static R_StreamLoadImage_t R_StreamLoadImage = reinterpret_cast(0x82410190); +typedef int (*RB_StreamQueueCommandSetHighMip_t)(GfxImage *image, unsigned __int8 *pixels); +static RB_StreamQueueCommandSetHighMip_t RB_StreamQueueCommandSetHighMip = + reinterpret_cast(0x82428320); +typedef int (*R_StreamAlloc_Alloc_t)(unsigned int size, int priority, StreamAllocBlockInfo **block, + unsigned int *streamSlot); +static R_StreamAlloc_Alloc_t R_StreamAlloc_Alloc = reinterpret_cast(0x82436728); +typedef void (*R_StreamAlloc_SetImage_t)(StreamAllocBlockInfo *block, __int16 streamSlot, GfxImage *image); +static R_StreamAlloc_SetImage_t R_StreamAlloc_SetImage = reinterpret_cast(0x82436A08); +typedef unsigned int *r_streamBufferBase_t; +static r_streamBufferBase_t r_streamBufferBase = reinterpret_cast(0x85F03DCC); static auto ScriptEnt_GetMethod = reinterpret_cast(0x82244B50); static auto Scr_AddArray = reinterpret_cast(0x82345C80); diff --git a/src/game/t4/sp/components/clipmap.cpp b/src/game/t4/sp/components/clipmap.cpp index 42c59c49..190ec1b8 100644 --- a/src/game/t4/sp/components/clipmap.cpp +++ b/src/game/t4/sp/components/clipmap.cpp @@ -18,7 +18,7 @@ void Load_clipMap_t_Hook(bool atStreamStart) auto mapEnts = (*varclipMap_t)->mapEnts; // Dump map entities if enabled - if (Config::dump_map_ents) + if (Config::dump_assets) { std::string dumpPath = va("%s\\%s.ents", DUMP_DIR, mapEnts->name); // IW4x naming convention std::replace(dumpPath.begin(), dumpPath.end(), '/', '\\'); diff --git a/src/game/t4/sp/components/scr_parser.cpp b/src/game/t4/sp/components/scr_parser.cpp index 1006609e..2ed37342 100644 --- a/src/game/t4/sp/components/scr_parser.cpp +++ b/src/game/t4/sp/components/scr_parser.cpp @@ -16,7 +16,7 @@ char *Scr_AddSourceBuffer_Hook(scriptInstance_t inst, const char *filename, cons codePos, archive); }; - if (Config::dump_rawfile) + if (Config::dump_assets) { DbgPrint("GSCLoader: Dumping script %s\n", extFilename); auto contents = callOriginal(); diff --git a/src/game/t5/mp/components/scr_parser.cpp b/src/game/t5/mp/components/scr_parser.cpp index 6da80236..060dbcf9 100644 --- a/src/game/t5/mp/components/scr_parser.cpp +++ b/src/game/t5/mp/components/scr_parser.cpp @@ -16,7 +16,7 @@ char *Scr_AddSourceBuffer_Hook(scriptInstance_t inst, const char *filename, cons codePos, archive); }; - if (Config::dump_rawfile) + if (Config::dump_assets) { DbgPrint("GSCLoader: Dumping script %s\n", extFilename); auto contents = callOriginal(); diff --git a/src/game/t5/sp/components/scr_parser.cpp b/src/game/t5/sp/components/scr_parser.cpp index c20933df..1b345173 100644 --- a/src/game/t5/sp/components/scr_parser.cpp +++ b/src/game/t5/sp/components/scr_parser.cpp @@ -16,7 +16,7 @@ char *Scr_AddSourceBuffer_Hook(scriptInstance_t inst, const char *filename, cons codePos, archive); }; - if (Config::dump_rawfile) + if (Config::dump_assets) { DbgPrint("GSCLoader: Dumping script %s\n", extFilename); auto contents = callOriginal(); diff --git a/src/image/dds_loader.cpp b/src/image/dds_loader.cpp new file mode 100644 index 00000000..21279c59 --- /dev/null +++ b/src/image/dds_loader.cpp @@ -0,0 +1,362 @@ +#include "pch.h" +#include "image/dds_loader.h" +#include "utils/endian.h" + +#ifndef INVALID_FILE_SIZE +#define INVALID_FILE_SIZE ((DWORD)-1) +#endif + +namespace image +{ +namespace +{ +const size_t DDS_FILE_HEADER_SIZE = sizeof(uint32_t) + sizeof(DDS_HEADER); +const DWORD MAX_DDS_FILE_SIZE = 64u * 1024u * 1024u; + +struct ScopedFileHandle +{ + HANDLE handle; + + explicit ScopedFileHandle(HANDLE file) : handle(file) + { + } + + ~ScopedFileHandle() + { + if (handle != INVALID_HANDLE_VALUE) + CloseHandle(handle); + } + + bool IsValid() const + { + return handle != INVALID_HANDLE_VALUE; + } +}; + +bool ReadExact(HANDLE file, const std::string &path, void *buffer, DWORD size, const char *label) +{ + DWORD bytesRead = 0; + if (!ReadFile(file, buffer, size, &bytesRead, nullptr) || bytesRead != size) + { + DbgPrint("[codxe][DDS] failed to read %s from '%s': expected=%u actual=%u error=0x%08X\n", label, + path.c_str(), size, bytesRead, GetLastError()); + return false; + } + + return true; +} + +void SwapDDSHeaderEndian(DDS_HEADER &header) +{ + utils::endian::ByteSwap(header.dwSize); + utils::endian::ByteSwap(header.dwFlags); + utils::endian::ByteSwap(header.dwHeight); + utils::endian::ByteSwap(header.dwWidth); + utils::endian::ByteSwap(header.dwPitchOrLinearSize); + utils::endian::ByteSwap(header.dwDepth); + utils::endian::ByteSwap(header.dwMipMapCount); + + for (int i = 0; i < 11; i++) + utils::endian::ByteSwap(header.dwReserved1[i]); + + utils::endian::ByteSwap(header.ddspf.dwSize); + utils::endian::ByteSwap(header.ddspf.dwFlags); + utils::endian::ByteSwap(header.ddspf.dwFourCC); + utils::endian::ByteSwap(header.ddspf.dwRGBBitCount); + utils::endian::ByteSwap(header.ddspf.dwRBitMask); + utils::endian::ByteSwap(header.ddspf.dwGBitMask); + utils::endian::ByteSwap(header.ddspf.dwBBitMask); + utils::endian::ByteSwap(header.ddspf.dwABitMask); + + utils::endian::ByteSwap(header.dwCaps); + utils::endian::ByteSwap(header.dwCaps2); + utils::endian::ByteSwap(header.dwCaps3); + utils::endian::ByteSwap(header.dwCaps4); + utils::endian::ByteSwap(header.dwReserved2); +} + +bool IsValidDdsImage(const DdsImage &image) +{ + return image.header.dwSize == DDS_HEADER_SIZE && image.header.ddspf.dwSize == DDS_PIXEL_FORMAT_SIZE && + !image.data.empty(); +} + +void ResetDdsImage(DdsImage *image) +{ + if (image == nullptr) + return; + + ZeroMemory(&image->header, sizeof(image->header)); + DdsByteVector().swap(image->data); +} +} // namespace + +bool DdsImage::IsCubemap() const +{ + const uint32_t cubemapFaces = DDSCAPS2_CUBEMAP_POSITIVEX | DDSCAPS2_CUBEMAP_NEGATIVEX | DDSCAPS2_CUBEMAP_POSITIVEY | + DDSCAPS2_CUBEMAP_NEGATIVEY | DDSCAPS2_CUBEMAP_POSITIVEZ | DDSCAPS2_CUBEMAP_NEGATIVEZ; + + return (header.dwCaps2 & DDSCAPS2_CUBEMAP) != 0 || (header.dwCaps2 & cubemapFaces) == cubemapFaces; +} + +uint32_t DdsImage::GetMipCount() const +{ + return max(1u, static_cast(header.dwMipMapCount)); +} + +bool DdsImage::GetGpuFormat(GPUTEXTUREFORMAT *format) const +{ + if (format == nullptr) + return false; + + if ((header.ddspf.dwFlags & DDPF_FOURCC) != 0) + { + switch (header.ddspf.dwFourCC) + { + case DXT1_FOURCC: + *format = GPUTEXTUREFORMAT_DXT1; + return true; + case DXT3_FOURCC: + *format = GPUTEXTUREFORMAT_DXT2_3; + return true; + case DXT5_FOURCC: + *format = GPUTEXTUREFORMAT_DXT4_5; + return true; + case DXN_FOURCC: + *format = GPUTEXTUREFORMAT_DXN; + return true; + default: + return false; + } + } + + if ((header.ddspf.dwFlags & DDPF_LUMINANCE) != 0) + { + if (header.ddspf.dwRGBBitCount == 8 && header.ddspf.dwRBitMask == 0x000000FF) + { + *format = GPUTEXTUREFORMAT_8; + return true; + } + + if ((header.ddspf.dwFlags & DDPF_ALPHAPIXELS) != 0 && header.ddspf.dwRGBBitCount == 16 && + header.ddspf.dwRBitMask == 0x000000FF && header.ddspf.dwABitMask == 0x0000FF00) + { + *format = GPUTEXTUREFORMAT_8_8; + return true; + } + } + + if ((header.ddspf.dwFlags & DDPF_RGB) != 0 && (header.ddspf.dwFlags & DDPF_ALPHAPIXELS) != 0 && + header.ddspf.dwRGBBitCount == 32 && header.ddspf.dwRBitMask == 0x00FF0000 && + header.ddspf.dwGBitMask == 0x0000FF00 && header.ddspf.dwBBitMask == 0x000000FF && + header.ddspf.dwABitMask == 0xFF000000) + { + *format = GPUTEXTUREFORMAT_8_8_8_8; + return true; + } + + return false; +} + +bool LoadDdsHeaderFromFile(const std::string &path, DDS_HEADER *outHeader, uint32_t *outDataSize) +{ + if (outHeader == nullptr) + return false; + + ZeroMemory(outHeader, sizeof(*outHeader)); + if (outDataSize != nullptr) + *outDataSize = 0; + + ScopedFileHandle file(CreateFileA(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + if (!file.IsValid()) + { + DbgPrint("[codxe][DDS] failed to open '%s': error=0x%08X\n", path.c_str(), GetLastError()); + return false; + } + + const DWORD fileSize = GetFileSize(file.handle, nullptr); + if (fileSize == INVALID_FILE_SIZE) + { + DbgPrint("[codxe][DDS] failed to get size for '%s': error=0x%08X\n", path.c_str(), GetLastError()); + return false; + } + + if (fileSize < DDS_FILE_HEADER_SIZE) + { + DbgPrint("[codxe][DDS] file too small '%s': size=%u header=%u\n", path.c_str(), fileSize, + static_cast(DDS_FILE_HEADER_SIZE)); + return false; + } + + if (fileSize > MAX_DDS_FILE_SIZE) + { + DbgPrint("[codxe][DDS] file too large '%s': size=%u max=%u\n", path.c_str(), fileSize, MAX_DDS_FILE_SIZE); + return false; + } + + uint32_t magic = 0; + if (!ReadExact(file.handle, path, &magic, sizeof(magic), "magic")) + return false; + + utils::endian::ByteSwap(magic); + if (magic != DDS_MAGIC) + { + DbgPrint("[codxe][DDS] invalid magic '%s': 0x%08X\n", path.c_str(), magic); + return false; + } + + if (!ReadExact(file.handle, path, outHeader, sizeof(DDS_HEADER), "header")) + return false; + + SwapDDSHeaderEndian(*outHeader); + + if (outDataSize != nullptr) + *outDataSize = fileSize - static_cast(DDS_FILE_HEADER_SIZE); + + return true; +} + +bool LoadDdsDataRangeFromFile(const std::string &path, uint32_t dataOffset, void *buffer, uint32_t size) +{ + if (buffer == nullptr || size == 0) + return false; + + ScopedFileHandle file(CreateFileA(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + if (!file.IsValid()) + { + DbgPrint("[codxe][DDS] failed to open '%s': error=0x%08X\n", path.c_str(), GetLastError()); + return false; + } + + const DWORD fileSize = GetFileSize(file.handle, nullptr); + if (fileSize == INVALID_FILE_SIZE) + { + DbgPrint("[codxe][DDS] failed to get size for '%s': error=0x%08X\n", path.c_str(), GetLastError()); + return false; + } + + if (fileSize < DDS_FILE_HEADER_SIZE || dataOffset > fileSize - DDS_FILE_HEADER_SIZE || + size > fileSize - DDS_FILE_HEADER_SIZE - dataOffset) + { + DbgPrint("[codxe][DDS] range outside file '%s': dataOffset=%u size=%u fileSize=%u\n", path.c_str(), + dataOffset, size, fileSize); + return false; + } + + const DWORD fileOffset = static_cast(DDS_FILE_HEADER_SIZE) + dataOffset; + SetLastError(NO_ERROR); + const DWORD seekResult = SetFilePointer(file.handle, fileOffset, nullptr, FILE_BEGIN); + if (seekResult == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR) + { + DbgPrint("[codxe][DDS] failed to seek '%s': offset=%u error=0x%08X\n", path.c_str(), fileOffset, + GetLastError()); + return false; + } + + return ReadExact(file.handle, path, buffer, size, "data range"); +} + +bool LoadDdsFromFile(const std::string &path, DdsImage *out) +{ + if (out == nullptr) + return false; + + const char *stage = "init"; + DWORD dataSize = 0; + + try + { + ResetDdsImage(out); + + stage = "open"; + ScopedFileHandle file(CreateFileA(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + if (!file.IsValid()) + { + DbgPrint("[codxe][DDS] failed to open '%s': error=0x%08X\n", path.c_str(), GetLastError()); + return false; + } + + stage = "get size"; + const DWORD fileSize = GetFileSize(file.handle, nullptr); + if (fileSize == INVALID_FILE_SIZE) + { + DbgPrint("[codxe][DDS] failed to get size for '%s': error=0x%08X\n", path.c_str(), GetLastError()); + return false; + } + + if (fileSize < DDS_FILE_HEADER_SIZE) + { + DbgPrint("[codxe][DDS] file too small '%s': size=%u header=%u\n", path.c_str(), fileSize, + static_cast(DDS_FILE_HEADER_SIZE)); + return false; + } + + if (fileSize > MAX_DDS_FILE_SIZE) + { + DbgPrint("[codxe][DDS] file too large '%s': size=%u max=%u\n", path.c_str(), fileSize, + MAX_DDS_FILE_SIZE); + return false; + } + + stage = "read magic"; + uint32_t magic = 0; + if (!ReadExact(file.handle, path, &magic, sizeof(magic), "magic")) + return false; + + utils::endian::ByteSwap(magic); + if (magic != DDS_MAGIC) + { + DbgPrint("[codxe][DDS] invalid magic '%s': 0x%08X\n", path.c_str(), magic); + return false; + } + + stage = "read header"; + if (!ReadExact(file.handle, path, &out->header, sizeof(DDS_HEADER), "header")) + return false; + + SwapDDSHeaderEndian(out->header); + + dataSize = fileSize - static_cast(DDS_FILE_HEADER_SIZE); + stage = "allocate data"; + out->data.resize(dataSize); + + stage = "read data"; + if (dataSize > 0 && !ReadExact(file.handle, path, &out->data[0], dataSize, "data")) + { + ResetDdsImage(out); + return false; + } + + return IsValidDdsImage(*out); + } + catch (const std::bad_alloc &) + { + DbgPrint("[codxe][DDS] allocation failed while loading '%s': stage=%s dataSize=%u error=0x%08X\n", + path.c_str(), stage, dataSize, GetLastError()); + ResetDdsImage(out); + return false; + } + catch (...) + { + DbgPrint("[codxe][DDS] exception while loading '%s': stage=%s dataSize=%u error=0x%08X\n", path.c_str(), + stage, dataSize, GetLastError()); + ResetDdsImage(out); + return false; + } +} + +DdsImage LoadDdsFromFile(const std::string &path) +{ + DdsImage image; + LoadDdsFromFile(path, &image); + return image; +} + +uint32_t GetMipDimension(uint32_t dimension, uint32_t mipLevel) +{ + return max(1u, dimension >> mipLevel); +} +} // namespace image diff --git a/src/image/dds_loader.h b/src/image/dds_loader.h new file mode 100644 index 00000000..fb7eb0dd --- /dev/null +++ b/src/image/dds_loader.h @@ -0,0 +1,14 @@ +#pragma once + +#include "image/dds_types.h" + +#include + +namespace image +{ +DdsImage LoadDdsFromFile(const std::string &path); +bool LoadDdsFromFile(const std::string &path, DdsImage *out); +bool LoadDdsHeaderFromFile(const std::string &path, DDS_HEADER *outHeader, uint32_t *outDataSize); +bool LoadDdsDataRangeFromFile(const std::string &path, uint32_t dataOffset, void *buffer, uint32_t size); +uint32_t GetMipDimension(uint32_t dimension, uint32_t mipLevel); +} // namespace image diff --git a/src/image/dds_types.h b/src/image/dds_types.h new file mode 100644 index 00000000..36ae8696 --- /dev/null +++ b/src/image/dds_types.h @@ -0,0 +1,184 @@ +#pragma once + +#include +#include +#include +#include + +namespace image +{ +template class VirtualAllocAllocator +{ + public: + typedef T value_type; + typedef T *pointer; + typedef const T *const_pointer; + typedef T &reference; + typedef const T &const_reference; + typedef size_t size_type; + typedef ptrdiff_t difference_type; + + template struct rebind + { + typedef VirtualAllocAllocator other; + }; + + VirtualAllocAllocator() + { + } + + template VirtualAllocAllocator(const VirtualAllocAllocator &) + { + } + + pointer allocate(size_type count, const void * = nullptr) + { + if (count == 0) + return nullptr; + + if (count > max_size()) + throw std::bad_alloc(); + + void *memory = VirtualAlloc(nullptr, count * sizeof(T), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); + if (memory == nullptr) + throw std::bad_alloc(); + + return static_cast(memory); + } + + void deallocate(pointer memory, size_type) + { + if (memory != nullptr) + VirtualFree(memory, 0, MEM_RELEASE); + } + + void construct(pointer memory, const_reference value) + { + new (memory) T(value); + } + + void destroy(pointer memory) + { + memory->~T(); + } + + size_type max_size() const + { + return static_cast(-1) / sizeof(T); + } +}; + +template +bool operator==(const VirtualAllocAllocator &, const VirtualAllocAllocator &) +{ + return true; +} + +template +bool operator!=(const VirtualAllocAllocator &, const VirtualAllocAllocator &) +{ + return false; +} + +typedef std::vector> DdsByteVector; + +enum DDS_CONSTANTS +{ + DDS_MAGIC = MAKEFOURCC('D', 'D', 'S', ' '), + DDS_HEADER_SIZE = 124u, + DDS_PIXEL_FORMAT_SIZE = 32u, +}; + +enum DDS_FOURCC +{ + DXT1_FOURCC = MAKEFOURCC('D', 'X', 'T', '1'), + DXT3_FOURCC = MAKEFOURCC('D', 'X', 'T', '3'), + DXT5_FOURCC = MAKEFOURCC('D', 'X', 'T', '5'), + DXN_FOURCC = MAKEFOURCC('A', 'T', 'I', '2'), +}; + +enum DDP_FLAGS +{ + DDPF_ALPHAPIXELS = 0x1, + DDPF_ALPHA = 0x2, + DDPF_FOURCC = 0x4, + DDPF_RGB = 0x40, + DDPF_YUV = 0x200, + DDPF_LUMINANCE = 0x20000, +}; + +enum DDS_HEADER_FLAGS +{ + DDSD_CAPS = 0x1, + DDSD_HEIGHT = 0x2, + DDSD_WIDTH = 0x4, + DDSD_PITCH = 0x8, + DDSD_PIXELFORMAT = 0x1000, + DDSD_MIPMAPCOUNT = 0x20000, + DDSD_LINEARSIZE = 0x80000, + DDSD_DEPTH = 0x800000, +}; + +enum DDS_HEADER_CAPS +{ + DDSCAPS_COMPLEX = 0x8, + DDSCAPS_TEXTURE = 0x1000, + DDSCAPS_MIPMAP = 0x400000, +}; + +enum DDS_HEADER_CAPS2 +{ + DDSCAPS2_CUBEMAP = 0x200, + DDSCAPS2_CUBEMAP_POSITIVEX = 0x400, + DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800, + DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000, + DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000, + DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000, + DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000, + DDSCAPS2_VOLUME = 0x200000, +}; + +struct DDS_PIXELFORMAT +{ + uint32_t dwSize; + uint32_t dwFlags; + uint32_t dwFourCC; + uint32_t dwRGBBitCount; + uint32_t dwRBitMask; + uint32_t dwGBitMask; + uint32_t dwBBitMask; + uint32_t dwABitMask; +}; + +static_assert(sizeof(DDS_PIXELFORMAT) == 32, ""); + +struct DDS_HEADER +{ + uint32_t dwSize; + uint32_t dwFlags; + uint32_t dwHeight; + uint32_t dwWidth; + uint32_t dwPitchOrLinearSize; + uint32_t dwDepth; + uint32_t dwMipMapCount; + uint32_t dwReserved1[11]; + DDS_PIXELFORMAT ddspf; + uint32_t dwCaps; + uint32_t dwCaps2; + uint32_t dwCaps3; + uint32_t dwCaps4; + uint32_t dwReserved2; +}; + +static_assert(sizeof(DDS_HEADER) == DDS_HEADER_SIZE, ""); + +struct DdsImage +{ + DDS_HEADER header; + DdsByteVector data; + + bool IsCubemap() const; + uint32_t GetMipCount() const; + bool GetGpuFormat(GPUTEXTUREFORMAT *format) const; +}; +} // namespace image diff --git a/src/image/dds_writer.cpp b/src/image/dds_writer.cpp new file mode 100644 index 00000000..3d684cd8 --- /dev/null +++ b/src/image/dds_writer.cpp @@ -0,0 +1,129 @@ +#include "pch.h" +#include "image/dds_writer.h" +#include "utils/endian.h" + +namespace image +{ +namespace +{ +void ByteSwapDDSPixelFormat(DDS_PIXELFORMAT &pixelFormat) +{ + utils::endian::ByteSwap(pixelFormat.dwSize); + utils::endian::ByteSwap(pixelFormat.dwFlags); + utils::endian::ByteSwap(pixelFormat.dwFourCC); + utils::endian::ByteSwap(pixelFormat.dwRGBBitCount); + utils::endian::ByteSwap(pixelFormat.dwRBitMask); + utils::endian::ByteSwap(pixelFormat.dwGBitMask); + utils::endian::ByteSwap(pixelFormat.dwBBitMask); + utils::endian::ByteSwap(pixelFormat.dwABitMask); +} + +void ByteSwapDDSHeader(DDS_HEADER &header) +{ + utils::endian::ByteSwap(header.dwSize); + utils::endian::ByteSwap(header.dwFlags); + utils::endian::ByteSwap(header.dwHeight); + utils::endian::ByteSwap(header.dwWidth); + utils::endian::ByteSwap(header.dwPitchOrLinearSize); + utils::endian::ByteSwap(header.dwDepth); + utils::endian::ByteSwap(header.dwMipMapCount); + + for (int i = 0; i < 11; i++) + utils::endian::ByteSwap(header.dwReserved1[i]); + + ByteSwapDDSPixelFormat(header.ddspf); + utils::endian::ByteSwap(header.dwCaps); + utils::endian::ByteSwap(header.dwCaps2); + utils::endian::ByteSwap(header.dwCaps3); + utils::endian::ByteSwap(header.dwCaps4); + utils::endian::ByteSwap(header.dwReserved2); +} + +bool PopulateDdsPixelFormat(DDS_PIXELFORMAT &pixelFormat, uint32_t gpuFormat) +{ + memset(&pixelFormat, 0, sizeof(pixelFormat)); + pixelFormat.dwSize = DDS_PIXEL_FORMAT_SIZE; + + switch (gpuFormat) + { + case GPUTEXTUREFORMAT_DXT1: + pixelFormat.dwFlags = DDPF_FOURCC; + pixelFormat.dwFourCC = DXT1_FOURCC; + return true; + case GPUTEXTUREFORMAT_DXT2_3: + pixelFormat.dwFlags = DDPF_FOURCC; + pixelFormat.dwFourCC = DXT3_FOURCC; + return true; + case GPUTEXTUREFORMAT_DXT4_5: + pixelFormat.dwFlags = DDPF_FOURCC; + pixelFormat.dwFourCC = DXT5_FOURCC; + return true; + case GPUTEXTUREFORMAT_DXN: + pixelFormat.dwFlags = DDPF_FOURCC; + pixelFormat.dwFourCC = DXN_FOURCC; + return true; + case GPUTEXTUREFORMAT_8: + pixelFormat.dwFlags = DDPF_LUMINANCE; + pixelFormat.dwRGBBitCount = 8; + pixelFormat.dwRBitMask = 0x000000FF; + return true; + case GPUTEXTUREFORMAT_8_8: + pixelFormat.dwFlags = DDPF_LUMINANCE | DDPF_ALPHAPIXELS; + pixelFormat.dwRGBBitCount = 16; + pixelFormat.dwRBitMask = 0x000000FF; + pixelFormat.dwABitMask = 0x0000FF00; + return true; + case GPUTEXTUREFORMAT_8_8_8_8: + pixelFormat.dwFlags = DDPF_RGB | DDPF_ALPHAPIXELS; + pixelFormat.dwRGBBitCount = 32; + pixelFormat.dwRBitMask = 0x00FF0000; + pixelFormat.dwGBitMask = 0x0000FF00; + pixelFormat.dwBBitMask = 0x000000FF; + pixelFormat.dwABitMask = 0xFF000000; + return true; + default: + return false; + } +} +} // namespace + +bool CreateDdsHeader(DDS_HEADER &header, uint32_t width, uint32_t height, uint32_t depth, uint32_t mipMapCount, + uint32_t pitchOrLinearSize, uint32_t caps, uint32_t caps2, uint32_t gpuFormat) +{ + memset(&header, 0, sizeof(header)); + header.dwSize = DDS_HEADER_SIZE; + header.dwHeight = height; + header.dwWidth = width; + header.dwDepth = depth; + header.dwMipMapCount = mipMapCount; + header.dwCaps = caps; + header.dwCaps2 = caps2; + + if (!PopulateDdsPixelFormat(header.ddspf, gpuFormat)) + return false; + + header.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT; + if ((header.ddspf.dwFlags & DDPF_FOURCC) != 0) + { + header.dwFlags |= DDSD_LINEARSIZE; + header.dwPitchOrLinearSize = pitchOrLinearSize; + } + else + { + header.dwFlags |= DDSD_PITCH; + header.dwPitchOrLinearSize = (width * header.ddspf.dwRGBBitCount + 7u) / 8u; + } + + return true; +} + +void WriteDdsHeader(std::ofstream &file, DDS_HEADER header) +{ + uint32_t magic = DDS_MAGIC; + utils::endian::ByteSwap(magic); + ByteSwapDDSHeader(header); + + file.write(reinterpret_cast(&magic), sizeof(magic)); + file.write(reinterpret_cast(&header), sizeof(header)); +} +} // namespace image diff --git a/src/image/dds_writer.h b/src/image/dds_writer.h new file mode 100644 index 00000000..c9d5a998 --- /dev/null +++ b/src/image/dds_writer.h @@ -0,0 +1,12 @@ +#pragma once + +#include "image/dds_types.h" + +#include + +namespace image +{ +bool CreateDdsHeader(DDS_HEADER &header, uint32_t width, uint32_t height, uint32_t depth, uint32_t mipMapCount, + uint32_t pitchOrLinearSize, uint32_t caps, uint32_t caps2, uint32_t gpuFormat); +void WriteDdsHeader(std::ofstream &file, DDS_HEADER header); +} // namespace image diff --git a/src/image/texture_layout.cpp b/src/image/texture_layout.cpp new file mode 100644 index 00000000..8729b4dc --- /dev/null +++ b/src/image/texture_layout.cpp @@ -0,0 +1,57 @@ +#include "pch.h" +#include "image/texture_layout.h" +#include "image/xenos_texture.h" + +namespace image +{ +size_t CalculateRequiredLinearDataSize(uint32_t width, uint32_t height, GPUTEXTUREFORMAT format, uint32_t firstMipLevel, + uint32_t levelCount, uint32_t faceCount) +{ + size_t requiredSize = 0; + + for (uint32_t localMipLevel = 0; localMipLevel < levelCount; ++localMipLevel) + { + const uint32_t mipLevel = firstMipLevel + localMipLevel; + const uint32_t levelSize = xenos_texture::CalculateLinearLevelSize(width, height, mipLevel, format); + if (levelSize == 0) + return 0; + + requiredSize += static_cast(levelSize) * faceCount; + } + + return requiredSize; +} + +uint32_t CalculateDdsMipOffset(uint32_t width, uint32_t height, GPUTEXTUREFORMAT format, uint32_t mipLevel) +{ + uint32_t offset = 0; + + for (uint32_t currentMip = 0; currentMip < mipLevel; ++currentMip) + { + const uint32_t levelSize = xenos_texture::CalculateLinearLevelSize(width, height, currentMip, format); + if (levelSize == 0) + return 0; + + offset += levelSize; + } + + return offset; +} + +size_t CalculateRequiredMipTextureBytes(uint32_t width, uint32_t height, GPUTEXTUREFORMAT format, + uint32_t firstMipLevel, uint32_t levelCount, uint32_t faceCount) +{ + size_t requiredSize = 0; + + for (uint32_t mipLevel = firstMipLevel; mipLevel < levelCount; ++mipLevel) + { + const uint32_t levelSize = xenos_texture::CalculateTiledLevelSize(width, height, mipLevel, format, 0u); + if (levelSize == 0) + return 0; + + requiredSize += static_cast(levelSize) * faceCount; + } + + return requiredSize; +} +} // namespace image diff --git a/src/image/texture_layout.h b/src/image/texture_layout.h new file mode 100644 index 00000000..92ab97be --- /dev/null +++ b/src/image/texture_layout.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +namespace image +{ +size_t CalculateRequiredLinearDataSize(uint32_t width, uint32_t height, GPUTEXTUREFORMAT format, uint32_t firstMipLevel, + uint32_t levelCount, uint32_t faceCount); +uint32_t CalculateDdsMipOffset(uint32_t width, uint32_t height, GPUTEXTUREFORMAT format, uint32_t mipLevel); +size_t CalculateRequiredMipTextureBytes(uint32_t width, uint32_t height, GPUTEXTUREFORMAT format, + uint32_t firstMipLevel, uint32_t levelCount, uint32_t faceCount); +} // namespace image diff --git a/src/image/xenos_texture.cpp b/src/image/xenos_texture.cpp new file mode 100644 index 00000000..3e4d8f6d --- /dev/null +++ b/src/image/xenos_texture.cpp @@ -0,0 +1,419 @@ +#include "pch.h" +#include "image/xenos_texture.h" + +namespace image +{ +namespace +{ +struct TextureLevelLayout +{ + uint32_t widthBlocks; + uint32_t heightBlocks; + uint32_t rowPitchBytes; + uint32_t storedWidthBlocks; + uint32_t storedHeightBlocks; + uint32_t arraySliceStrideBytes; +}; + +uint32_t DivideRoundUp(uint32_t value, uint32_t divisor) +{ + return (value + divisor - 1) / divisor; +} + +uint32_t AlignTo(uint32_t value, uint32_t alignment) +{ + return DivideRoundUp(value, alignment) * alignment; +} + +uint32_t NextPow2(uint32_t value) +{ + if (value <= 1) + return 1; + + uint32_t result = 1; + while (result < value) + result <<= 1; + return result; +} + +uint32_t Log2Ceil(uint32_t value) +{ + uint32_t result = 0; + uint32_t current = value - 1; + while (current != 0) + { + current >>= 1; + ++result; + } + return result; +} + +uint32_t CalculatePitch(uint32_t width, const xenos_texture::TextureFormatInfo &formatInfo) +{ + if (formatInfo.blockWidth > 1) + { + const uint32_t widthInBlocks = max(1u, DivideRoundUp(width, formatInfo.blockWidth)); + return AlignTo(widthInBlocks, 32u) / 8u; + } + + return AlignTo(width, 32u) / 32u; +} + +TextureLevelLayout CalculateLevelLayout(uint32_t width, uint32_t height, uint32_t mipLevel, + const xenos_texture::TextureFormatInfo &formatInfo, uint32_t basePitch) +{ + const uint32_t mipWidth = max(width >> mipLevel, 1u); + const uint32_t mipHeight = max(height >> mipLevel, 1u); + + TextureLevelLayout layout = {}; + layout.widthBlocks = max(1u, DivideRoundUp(mipWidth, formatInfo.blockWidth)); + layout.heightBlocks = max(1u, DivideRoundUp(mipHeight, formatInfo.blockHeight)); + + if (mipLevel == 0) + { + const uint32_t pitch = basePitch != 0 ? basePitch : CalculatePitch(width, formatInfo); + const uint32_t rowPitchTexels = pitch << 5u; + layout.rowPitchBytes = max(1u, DivideRoundUp(rowPitchTexels, formatInfo.blockWidth)) * formatInfo.bytesPerBlock; + layout.storedWidthBlocks = layout.rowPitchBytes / formatInfo.bytesPerBlock; + layout.storedHeightBlocks = AlignTo(layout.heightBlocks, 32u); + } + else + { + const uint32_t mipWidthTexels = max(NextPow2(width) >> mipLevel, 1u); + const uint32_t mipHeightTexels = max(NextPow2(height) >> mipLevel, 1u); + layout.storedWidthBlocks = AlignTo(DivideRoundUp(mipWidthTexels, formatInfo.blockWidth), 32u); + layout.storedHeightBlocks = AlignTo(DivideRoundUp(mipHeightTexels, formatInfo.blockHeight), 32u); + layout.rowPitchBytes = layout.storedWidthBlocks * formatInfo.bytesPerBlock; + } + + layout.arraySliceStrideBytes = AlignTo(layout.rowPitchBytes * layout.storedHeightBlocks, 4096u); + return layout; +} + +uint32_t CalculateLog2BytesPerBlock(uint32_t bytesPerBlock) +{ + return (bytesPerBlock / 4u) + ((bytesPerBlock / 2u) >> (bytesPerBlock / 4u)); +} + +uint32_t TiledOffset2DRow(uint32_t y, uint32_t width, uint32_t log2BytesPerBlock) +{ + const uint32_t macro = ((y / 32u) * (width / 32u)) << (log2BytesPerBlock + 7u); + const uint32_t micro = ((y & 6u) << 2u) << log2BytesPerBlock; + return macro + ((micro & ~0xFu) << 1u) + (micro & 0xFu) + ((y & 8u) << (3u + log2BytesPerBlock)) + ((y & 1u) << 4u); +} + +uint32_t TiledOffset2DColumn(uint32_t x, uint32_t y, uint32_t log2BytesPerBlock, uint32_t baseOffset) +{ + const uint32_t macro = (x / 32u) << (log2BytesPerBlock + 7u); + const uint32_t micro = (x & 7u) << log2BytesPerBlock; + const uint32_t offset = baseOffset + macro + ((micro & ~0xFu) << 1u) + (micro & 0xFu); + return ((offset & ~0x1FFu) << 3u) + ((offset & 0x1C0u) << 2u) + (offset & 0x3Fu) + ((y & 16u) << 7u) + + (((((y & 8u) >> 2u) + (x >> 3u)) & 3u) << 6u); +} + +void EndianSwap8In16(unsigned char *data, size_t size) +{ + for (size_t i = 0; i + 1 < size; i += 2) + std::swap(data[i], data[i + 1]); +} + +void EndianSwap8In32(unsigned char *data, size_t size) +{ + for (size_t i = 0; i + 3 < size; i += 4) + { + std::swap(data[i], data[i + 3]); + std::swap(data[i + 1], data[i + 2]); + } +} + +void EndianSwap16In32(unsigned char *data, size_t size) +{ + for (size_t i = 0; i + 3 < size; i += 4) + { + std::swap(data[i], data[i + 2]); + std::swap(data[i + 1], data[i + 3]); + } +} +} // namespace + +namespace xenos_texture +{ +const TextureFormatInfo *GetTextureFormatInfo(uint32_t gpuFormat) +{ + static const TextureFormatInfo formats[] = { + {GPUTEXTUREFORMAT_8, 1u, 1u, 1u, 8u}, {GPUTEXTUREFORMAT_8_8, 1u, 1u, 2u, 16u}, + {GPUTEXTUREFORMAT_8_8_8_8, 1u, 1u, 4u, 32u}, {GPUTEXTUREFORMAT_DXT1, 4u, 4u, 8u, 4u}, + {GPUTEXTUREFORMAT_DXT2_3, 4u, 4u, 16u, 8u}, {GPUTEXTUREFORMAT_DXT4_5, 4u, 4u, 16u, 8u}, + {GPUTEXTUREFORMAT_DXN, 4u, 4u, 16u, 8u}, {GPUTEXTUREFORMAT_DXT3A, 4u, 4u, 8u, 4u}, + {GPUTEXTUREFORMAT_DXT5A, 4u, 4u, 8u, 4u}, + }; + + for (size_t i = 0; i < sizeof(formats) / sizeof(formats[0]); ++i) + { + if (formats[i].gpuFormat == gpuFormat) + return &formats[i]; + } + + return NULL; +} + +void ApplyGpuEndian(void *data, size_t size, GPUENDIAN endianType) +{ + unsigned char *bytes = static_cast(data); + switch (endianType) + { + case GPUENDIAN_8IN16: + EndianSwap8In16(bytes, size); + break; + case GPUENDIAN_8IN32: + EndianSwap8In32(bytes, size); + break; + case GPUENDIAN_16IN32: + EndianSwap16In32(bytes, size); + break; + default: + break; + } +} + +uint32_t GetTextureLevelCount(const D3DBaseTexture *texture) +{ + if (texture == NULL) + return 1; + return max(1u, static_cast(texture->Format.MaxMipLevel) + 1u); +} + +uint32_t GetMipTailBaseLevel(uint32_t width, uint32_t height) +{ + const uint32_t log2Size = Log2Ceil(min(width, height)); + return log2Size > 4u ? log2Size - 4u : 0u; +} + +uint32_t CalculateLinearRowPitch(uint32_t width, uint32_t mipLevel, uint32_t gpuFormat) +{ + const TextureFormatInfo *formatInfo = GetTextureFormatInfo(gpuFormat); + if (formatInfo == NULL) + return 0; + + const uint32_t mipWidth = max(width >> mipLevel, 1u); + const uint32_t widthBlocks = max(1u, DivideRoundUp(mipWidth, formatInfo->blockWidth)); + return widthBlocks * formatInfo->bytesPerBlock; +} + +uint32_t CalculateLinearLevelSize(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat) +{ + const TextureFormatInfo *formatInfo = GetTextureFormatInfo(gpuFormat); + if (formatInfo == NULL) + return 0; + + const uint32_t mipWidth = max(width >> mipLevel, 1u); + const uint32_t mipHeight = max(height >> mipLevel, 1u); + const uint32_t widthBlocks = max(1u, DivideRoundUp(mipWidth, formatInfo->blockWidth)); + const uint32_t heightBlocks = max(1u, DivideRoundUp(mipHeight, formatInfo->blockHeight)); + return widthBlocks * heightBlocks * formatInfo->bytesPerBlock; +} + +uint32_t CalculateTiledLevelSize(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat, + uint32_t basePitch) +{ + const TextureFormatInfo *formatInfo = GetTextureFormatInfo(gpuFormat); + if (formatInfo == NULL) + return 0; + + const TextureLevelLayout layout = CalculateLevelLayout(width, height, mipLevel, *formatInfo, basePitch); + return layout.arraySliceStrideBytes; +} + +uint32_t CalculateBaseSize(const D3DBaseTexture *texture, uint32_t width, uint32_t height, uint32_t faceCount) +{ + if (texture == NULL) + return 0; + + const TextureFormatInfo *formatInfo = GetTextureFormatInfo(texture->Format.DataFormat); + if (formatInfo == NULL) + return 0; + + const TextureLevelLayout layout = CalculateLevelLayout(width, height, 0u, *formatInfo, texture->Format.Pitch); + return layout.arraySliceStrideBytes * faceCount; +} + +uint32_t CalculateMipLevelOffset(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat, + uint32_t faceCount) +{ + const TextureFormatInfo *formatInfo = GetTextureFormatInfo(gpuFormat); + if (formatInfo == NULL || mipLevel <= 1u) + return 0; + + uint32_t offset = 0; + for (uint32_t level = 1u; level < mipLevel; ++level) + { + const TextureLevelLayout layout = CalculateLevelLayout(width, height, level, *formatInfo, 0u); + offset += layout.arraySliceStrideBytes * faceCount; + } + + return offset; +} + +unsigned char *GetTextureBase(const D3DBaseTexture *texture, unsigned char *fallbackBaseData) +{ + if (fallbackBaseData != NULL) + return fallbackBaseData; + + if (texture != NULL && texture->Format.BaseAddress != 0) + return reinterpret_cast(texture->Format.BaseAddress << 12u); + + return NULL; +} + +unsigned char *GetTextureMipBase(const D3DBaseTexture *texture, unsigned char *baseData, uint32_t width, + uint32_t height, uint32_t gpuFormat, uint32_t faceCount) +{ + const uint32_t baseSize = CalculateBaseSize(texture, width, height, faceCount); + if (baseData != NULL) + return baseData + baseSize; + + if (texture != NULL && texture->Format.MipAddress != 0) + return reinterpret_cast(texture->Format.MipAddress << 12u); + + return NULL; +} + +bool TileTextureLevel(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat, uint32_t basePitch, + void *destination, const void *source, uint32_t sourceRowPitch) +{ + return TileTextureLevel(width, height, mipLevel, gpuFormat, basePitch, destination, static_cast(-1), source, + static_cast(-1), sourceRowPitch); +} + +bool TileTextureLevel(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat, uint32_t basePitch, + void *destination, size_t destinationSize, const void *source, size_t sourceSize, + uint32_t sourceRowPitch) +{ + const TextureFormatInfo *formatInfo = GetTextureFormatInfo(gpuFormat); + if (formatInfo == NULL || destination == NULL || source == NULL || sourceRowPitch == 0) + { + return false; + } + + const TextureLevelLayout layout = CalculateLevelLayout(width, height, mipLevel, *formatInfo, basePitch); + if (sourceRowPitch < layout.widthBlocks * formatInfo->bytesPerBlock) + { + return false; + } + + const uint32_t log2BytesPerBlock = CalculateLog2BytesPerBlock(formatInfo->bytesPerBlock); + const unsigned char *sourceBytes = static_cast(source); + unsigned char *destinationBytes = static_cast(destination); + + for (uint32_t y = 0; y < layout.heightBlocks; ++y) + { + const uint32_t destinationRowOffset = TiledOffset2DRow(y, layout.storedWidthBlocks, log2BytesPerBlock); + + for (uint32_t x = 0; x < layout.widthBlocks; ++x) + { + uint32_t tiledOffset = TiledOffset2DColumn(x, y, log2BytesPerBlock, destinationRowOffset); + tiledOffset >>= log2BytesPerBlock; + + const size_t sourceOffset = + static_cast(y) * sourceRowPitch + static_cast(x) * formatInfo->bytesPerBlock; + const size_t destinationOffset = static_cast(tiledOffset) * formatInfo->bytesPerBlock; + if (sourceOffset + formatInfo->bytesPerBlock > sourceSize || + destinationOffset + formatInfo->bytesPerBlock > destinationSize) + { + return false; + } + + memcpy(destinationBytes + destinationOffset, sourceBytes + sourceOffset, formatInfo->bytesPerBlock); + } + } + + return true; +} + +bool TileTextureLevelFromRows(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat, + uint32_t basePitch, void *destination, size_t destinationSize, + uint32_t sourceRowPitch, unsigned char *rowBuffer, size_t rowBufferSize, + TileTextureRowReader rowReader, void *userData) +{ + const TextureFormatInfo *formatInfo = GetTextureFormatInfo(gpuFormat); + if (formatInfo == NULL || destination == NULL || rowBuffer == NULL || rowReader == NULL || sourceRowPitch == 0) + return false; + + const TextureLevelLayout layout = CalculateLevelLayout(width, height, mipLevel, *formatInfo, basePitch); + if (sourceRowPitch < layout.widthBlocks * formatInfo->bytesPerBlock || rowBufferSize < sourceRowPitch) + return false; + + const uint32_t log2BytesPerBlock = CalculateLog2BytesPerBlock(formatInfo->bytesPerBlock); + unsigned char *destinationBytes = static_cast(destination); + + for (uint32_t y = 0; y < layout.heightBlocks; ++y) + { + if (!rowReader(y, rowBuffer, sourceRowPitch, userData)) + return false; + + const uint32_t destinationRowOffset = TiledOffset2DRow(y, layout.storedWidthBlocks, log2BytesPerBlock); + + for (uint32_t x = 0; x < layout.widthBlocks; ++x) + { + uint32_t tiledOffset = TiledOffset2DColumn(x, y, log2BytesPerBlock, destinationRowOffset); + tiledOffset >>= log2BytesPerBlock; + + const size_t sourceOffset = static_cast(x) * formatInfo->bytesPerBlock; + const size_t destinationOffset = static_cast(tiledOffset) * formatInfo->bytesPerBlock; + if (sourceOffset + formatInfo->bytesPerBlock > rowBufferSize || + destinationOffset + formatInfo->bytesPerBlock > destinationSize) + return false; + + memcpy(destinationBytes + destinationOffset, rowBuffer + sourceOffset, formatInfo->bytesPerBlock); + } + } + + return true; +} + +bool UntileTextureLevel(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat, uint32_t basePitch, + void *destination, uint32_t destinationRowPitch, const void *source) +{ + return UntileTextureLevel(width, height, mipLevel, gpuFormat, basePitch, destination, static_cast(-1), + destinationRowPitch, source, static_cast(-1)); +} + +bool UntileTextureLevel(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat, uint32_t basePitch, + void *destination, size_t destinationSize, uint32_t destinationRowPitch, const void *source, + size_t sourceSize) +{ + const TextureFormatInfo *formatInfo = GetTextureFormatInfo(gpuFormat); + if (formatInfo == NULL || destination == NULL || source == NULL || destinationRowPitch == 0) + return false; + + const TextureLevelLayout layout = CalculateLevelLayout(width, height, mipLevel, *formatInfo, basePitch); + if (destinationRowPitch < layout.widthBlocks * formatInfo->bytesPerBlock) + return false; + + const uint32_t log2BytesPerBlock = CalculateLog2BytesPerBlock(formatInfo->bytesPerBlock); + unsigned char *destinationBytes = static_cast(destination); + const unsigned char *sourceBytes = static_cast(source); + + for (uint32_t y = 0; y < layout.heightBlocks; ++y) + { + const uint32_t sourceRowOffset = TiledOffset2DRow(y, layout.storedWidthBlocks, log2BytesPerBlock); + for (uint32_t x = 0; x < layout.widthBlocks; ++x) + { + uint32_t tiledOffset = TiledOffset2DColumn(x, y, log2BytesPerBlock, sourceRowOffset); + tiledOffset >>= log2BytesPerBlock; + + const size_t destinationOffset = + static_cast(y) * destinationRowPitch + static_cast(x) * formatInfo->bytesPerBlock; + const size_t sourceOffset = static_cast(tiledOffset) * formatInfo->bytesPerBlock; + if (destinationOffset + formatInfo->bytesPerBlock > destinationSize || + sourceOffset + formatInfo->bytesPerBlock > sourceSize) + return false; + + memcpy(destinationBytes + destinationOffset, sourceBytes + sourceOffset, formatInfo->bytesPerBlock); + } + } + + return true; +} +} // namespace xenos_texture +} // namespace image diff --git a/src/image/xenos_texture.h b/src/image/xenos_texture.h new file mode 100644 index 00000000..f27eb228 --- /dev/null +++ b/src/image/xenos_texture.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include + +namespace image +{ +namespace xenos_texture +{ +struct TextureFormatInfo +{ + uint32_t gpuFormat; + uint32_t blockWidth; + uint32_t blockHeight; + uint32_t bytesPerBlock; + uint32_t bitsPerPixel; +}; + +typedef bool (*TileTextureRowReader)(uint32_t rowIndex, unsigned char *rowBuffer, uint32_t rowPitch, void *userData); + +const TextureFormatInfo *GetTextureFormatInfo(uint32_t gpuFormat); +void ApplyGpuEndian(void *data, size_t size, GPUENDIAN endianType); +uint32_t GetTextureLevelCount(const D3DBaseTexture *texture); +uint32_t GetMipTailBaseLevel(uint32_t width, uint32_t height); +uint32_t CalculateLinearRowPitch(uint32_t width, uint32_t mipLevel, uint32_t gpuFormat); +uint32_t CalculateLinearLevelSize(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat); +uint32_t CalculateTiledLevelSize(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat, + uint32_t basePitch); +uint32_t CalculateBaseSize(const D3DBaseTexture *texture, uint32_t width, uint32_t height, uint32_t faceCount); +uint32_t CalculateMipLevelOffset(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat, + uint32_t faceCount); +unsigned char *GetTextureBase(const D3DBaseTexture *texture, unsigned char *fallbackBaseData); +unsigned char *GetTextureMipBase(const D3DBaseTexture *texture, unsigned char *baseData, uint32_t width, + uint32_t height, uint32_t gpuFormat, uint32_t faceCount); +bool TileTextureLevel(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat, uint32_t basePitch, + void *destination, const void *source, uint32_t sourceRowPitch); +bool TileTextureLevel(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat, uint32_t basePitch, + void *destination, size_t destinationSize, const void *source, size_t sourceSize, + uint32_t sourceRowPitch); +bool TileTextureLevelFromRows(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat, + uint32_t basePitch, void *destination, size_t destinationSize, + uint32_t sourceRowPitch, unsigned char *rowBuffer, size_t rowBufferSize, + TileTextureRowReader rowReader, void *userData); +bool UntileTextureLevel(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat, uint32_t basePitch, + void *destination, uint32_t destinationRowPitch, const void *source); +bool UntileTextureLevel(uint32_t width, uint32_t height, uint32_t mipLevel, uint32_t gpuFormat, uint32_t basePitch, + void *destination, size_t destinationSize, uint32_t destinationRowPitch, const void *source, + size_t sourceSize); +} // namespace xenos_texture +} // namespace image diff --git a/src/pch.h b/src/pch.h index 5a2a9dd9..b84c5005 100644 --- a/src/pch.h +++ b/src/pch.h @@ -23,13 +23,10 @@ #include #include #include -#include #include #include #include -#pragma comment(lib, "d3d9ltcg.lib") -#pragma comment(lib, "xgraphics.lib") #pragma comment(lib, "xjson.lib") // Project-specific includes diff --git a/src/utils/endian.cpp b/src/utils/endian.cpp new file mode 100644 index 00000000..0c980856 --- /dev/null +++ b/src/utils/endian.cpp @@ -0,0 +1,74 @@ +#include "pch.h" +#include "utils/endian.h" + +#include + +namespace utils +{ +namespace endian +{ +namespace +{ +uint16_t ByteSwapped(uint16_t value) +{ + return static_cast((value >> 8) | (value << 8)); +} + +int16_t ByteSwapped(int16_t value) +{ + return static_cast(ByteSwapped(static_cast(value))); +} + +uint32_t ByteSwapped(uint32_t value) +{ + return _byteswap_ulong(value); +} + +int32_t ByteSwapped(int32_t value) +{ + return static_cast(ByteSwapped(static_cast(value))); +} + +uint64_t ByteSwapped(uint64_t value) +{ + return (static_cast(ByteSwapped(static_cast(value))) << 32) | + ByteSwapped(static_cast(value >> 32)); +} + +int64_t ByteSwapped(int64_t value) +{ + return static_cast(ByteSwapped(static_cast(value))); +} +} // namespace + +void ByteSwap(uint16_t &value) +{ + value = ByteSwapped(value); +} + +void ByteSwap(int16_t &value) +{ + value = ByteSwapped(value); +} + +void ByteSwap(uint32_t &value) +{ + value = ByteSwapped(value); +} + +void ByteSwap(int32_t &value) +{ + value = ByteSwapped(value); +} + +void ByteSwap(uint64_t &value) +{ + value = ByteSwapped(value); +} + +void ByteSwap(int64_t &value) +{ + value = ByteSwapped(value); +} +} // namespace endian +} // namespace utils diff --git a/src/utils/endian.h b/src/utils/endian.h new file mode 100644 index 00000000..a9d59097 --- /dev/null +++ b/src/utils/endian.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace utils +{ +namespace endian +{ +void ByteSwap(uint16_t &value); +void ByteSwap(int16_t &value); +void ByteSwap(uint32_t &value); +void ByteSwap(int32_t &value); +void ByteSwap(uint64_t &value); +void ByteSwap(int64_t &value); +} // namespace endian +} // namespace utils