From 4624760d477ddeee96cb1687e3e7c5f31794d103 Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Wed, 24 Dec 2025 11:24:15 +0000 Subject: [PATCH] refactor(IW3): move image loader code to module --- codxe.vcxproj | 1 + src/game/iw3/mp/components/image_loader.cpp | 1050 +++++++++++++++++++ src/game/iw3/mp/components/image_loader.h | 22 + src/game/iw3/mp/main.cpp | 1040 +----------------- 4 files changed, 1075 insertions(+), 1038 deletions(-) create mode 100644 src/game/iw3/mp/components/image_loader.cpp create mode 100644 src/game/iw3/mp/components/image_loader.h diff --git a/codxe.vcxproj b/codxe.vcxproj index 3aedab1..b0c7e4e 100644 --- a/codxe.vcxproj +++ b/codxe.vcxproj @@ -95,6 +95,7 @@ + diff --git a/src/game/iw3/mp/components/image_loader.cpp b/src/game/iw3/mp/components/image_loader.cpp new file mode 100644 index 0000000..d066855 --- /dev/null +++ b/src/game/iw3/mp/components/image_loader.cpp @@ -0,0 +1,1050 @@ +#include "pch.h" +#include "common/config.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. + +namespace +{ + +// 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 +{ + DDSHeader header; + std::vector data; +}; + +// 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); +} + +DDSImage ReadDDSFile(const std::string &filepath) +{ + 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)); + + // 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 + { + DbgPrint("ERROR: Invalid DDS file: %s\n", filepath.c_str()); + file.close(); + return ddsImage; + } + + // 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(); + + // Ensure fileSize is valid before proceeding + if (fileSize == std::streampos(-1)) + { + DbgPrint("ERROR: Failed to determine file size.\n"); + file.close(); + return ddsImage; + } + + // Move back to after the header + file.seekg(sizeof(DDSHeader), std::ios::beg); + + // Compute data size safely + size_t dataSize = static_cast(fileSize) - sizeof(DDSHeader); + + // Read image data + ddsImage.data.resize(dataSize); + file.read(reinterpret_cast(ddsImage.data.data()), dataSize); + + file.close(); + + // 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)); + + return ddsImage; +} + +std::string extract_filename(const char *filename) +{ + std::string path(filename); + + // Find last backslash '\' or forward slash '/' + size_t lastSlash = path.find_last_of("\\/"); + size_t start = (lastSlash == std::string::npos) ? 0 : lastSlash + 1; + + // Find last dot '.' (extension separator) + size_t lastDot = path.find_last_of('.'); + size_t end = (lastDot == std::string::npos || lastDot < start) ? path.length() : lastDot; + + return path.substr(start, end - start); +} + +void GPUEndianSwapTexture(std::vector &pixelData, GPUENDIAN endianType) +{ + 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; + } +} + +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) + { + 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 + UINT blocksWide = (mipWidth + 3) / 4; + UINT blocksHigh = (mipHeight + 3) / 4; + + // Calculate total size in bytes + UINT sizeInBytes = blocksWide * blocksHigh * blockSize; + + return sizeInBytes; +} + +} // namespace + +namespace iw3 +{ +namespace mp +{ + +void Image_DbgPrint(const GfxImage *image) +{ + const int format = image->texture.basemap->Format.DataFormat; + char *format_str; + switch (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; + } + + XGTEXTURE_DESC SourceDesc; + XGGetTextureDesc(image->texture.basemap, 0, &SourceDesc); + BOOL IsBorderTexture = XGIsBorderTexture(image->texture.basemap); + UINT MipTailBaseLevel = XGGetMipTailBaseLevel(SourceDesc.Width, SourceDesc.Height, IsBorderTexture); + + // SourceDesc.BitsPerPixel; + // SourceDesc.BytesPerBlock; + + UINT MipLevelCount = image->texture.basemap->GetLevelCount(); + + UINT BaseSize; + XGGetTextureLayout(image->texture.basemap, 0, &BaseSize, 0, 0, 0, 0, 0, 0, 0, 0); + + 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); +} + +void Image_Dump(const GfxImage *image) +{ + // 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"); + return; + } + + if (!image->pixels || image->baseSize == 0) + { + Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Image '%s' has no valid 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); + return; + } + + 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: + Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported texture format %d!\n", format); + return; + } + + // Set texture capabilities + header.caps = _byteswap_ulong(DDSCAPS_TEXTURE | DDSCAPS_MIPMAP); + + // Handle Cubemaps + if (image->mapType == mp::MAPTYPE_CUBE) + { + header.caps2 = _byteswap_ulong(DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX | DDSCAPS2_CUBEMAP_NEGATIVEX | + DDSCAPS2_CUBEMAP_POSITIVEY | DDSCAPS2_CUBEMAP_NEGATIVEY | + DDSCAPS2_CUBEMAP_POSITIVEZ | DDSCAPS2_CUBEMAP_NEGATIVEZ); + } + + 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"; + + std::ofstream file(filename, std::ios::binary); + if (!file) + { + Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Failed to open file: %s\n", filename.c_str()); + return; + } + + if (image->mapType == MAPTYPE_CUBE) + { + file.write(reinterpret_cast(&header), sizeof(DDSHeader)); + + unsigned int face_size = 0; + unsigned int rowPitch = 0; + const GPUTEXTUREFORMAT format = static_cast(image->texture.basemap->Format.DataFormat); + + switch (format) + { + 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); + 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 + + 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()); + } + + file.close(); + } + else if (image->mapType == MAPTYPE_2D) + { + // TODO: write mip levels + file.write(reinterpret_cast(&header), sizeof(DDSHeader)); + + std::vector pixelData(image->pixels, image->pixels + image->baseSize); + + GPUEndianSwapTexture(pixelData, static_cast(image->texture.basemap->Format.Endian)); + + // Create a linear data buffer to hold the untiled texture + std::vector linearData(image->baseSize); + + // Calculate row pitch based on format + UINT rowPitch; + auto format = image->texture.basemap->Format.DataFormat; + + switch (format) + { + 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); + 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(); + } + else + { + Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported map type %d!\n", image->mapType); + return; + } +} + +void Cmd_imagedump() +{ + ImageList imageList; + R_GetImageList(&imageList); + + // images bundled in xex + // auto g_imageProgs = reinterpret_cast(0x84FEA6D0); + // for (unsigned int i = 0; i < 10; i++) + // { + // imageList.image[imageList.count++] = &g_imageProgs[i]; + // } + + CreateDirectoryA(DUMP_DIR, 0); + CreateDirectoryA((std::string(DUMP_DIR) + "\\images").c_str(), 0); + CreateDirectoryA((std::string(DUMP_DIR) + "\\highmip").c_str(), 0); + + for (unsigned int i = 0; i < imageList.count; i++) + { + auto image = imageList.image[i]; + Image_DbgPrint(image); + + Image_Dump(image); + } + + auto highmips = filesystem::list_files_in_directory("D:\\highmip"); + for (size_t i = 0; i < highmips.size(); ++i) + { + 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; + } + + auto image = asset->entry.asset.header.image; + + 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; + } + + std::streamsize size = input_file.tellg(); + if (size < 0) + { + Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Failed to determine file size: %s\n", filepath.c_str()); + continue; + } + + input_file.seekg(0, std::ios::beg); + std::vector buffer(static_cast(size)); + + if (input_file.read(reinterpret_cast(buffer.data()), size)) + { + Com_Printf(CON_CHANNEL_CONSOLEONLY, "Read %d bytes from file.\n", size); + } + else + { + Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Error reading file: %s\n", filepath.c_str()); + continue; + } + + 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()); + } +} + +void Image_Replace_2D(GfxImage *image, const DDSImage &ddsImage) +{ + if (image->mapType != MAPTYPE_2D) + { + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a 2D map!\n", image->name); + return; + } + + // 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)); + + UINT ddsOffset = 0; + + for (UINT mipLevel = 0; mipLevel < mipTailBaseLevel; mipLevel++) + { + 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) + { + 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; + } + + 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); + + UINT address = baseAddress; + if (mipLevel > 0) + { + UINT mipLevelOffset = XGGetMipLevelOffset(image->texture.basemap, 0, mipLevel); + address = mipAddress + mipLevelOffset; + } + + DbgPrint("Image_Replace_2D: Writing mip level %d to address 0x%08X - levelSize=%u\n", mipLevel, address, + ddsMipLevelSize); + + // // 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) + ); + + ddsOffset += ddsMipLevelSize; + } +} + +void Image_Replace_Cube(GfxImage *image, const DDSImage &ddsImage) +{ + if (image->mapType != MAPTYPE_CUBE) + { + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a cube map!\n", image->name); + return; + } + + 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; + + switch (format) + { + 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; + } + + for (int i = 0; i < 6; i++) + { + const unsigned char *face_pixels = ddsImage.data.data() + (i * face_size); + + // 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)); + + // Copy the data to the image + memcpy(image->pixels + (i * face_size), tiledData.data(), face_size); + } +} + +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"; + + if (!filesystem::file_exists(replacement_path)) + { + Com_PrintError(CON_CHANNEL_ERROR, "File does not exist: %s\n", replacement_path.c_str()); + return; + } + + DDSImage ddsImage = ReadDDSFile(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) + { + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' dimensions do not match DDS file: %s\n", image->name, + replacement_path.c_str()); + return; + } + + 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: + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has an unsupported DDS format: 0x%X\n", image->name, + ddsImage.header.pixelFormat.fourCC); + return; + } + + if (static_cast(image->texture.basemap->Format.DataFormat) != static_cast(ddsFormat)) + { + Com_PrintError(CON_CHANNEL_ERROR, "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; + } + + if (image->mapType == MAPTYPE_2D) + { + Image_Replace_2D(image, ddsImage); + } + else if (image->mapType == MAPTYPE_CUBE) + { + Image_Replace_Cube(image, ddsImage); + } + else + { + Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a 2D or cube map!\n", image->name); + return; + } +} + +void Load_images() +{ + const int MAX_IMAGES = 2048; + XAssetHeader assets[MAX_IMAGES]; + const auto count = DB_GetAllXAssetOfType_FastFile(ASSET_TYPE_IMAGE, assets, MAX_IMAGES); + for (int i = 0; i < count; i++) + { + GfxImage *image = assets[i].image; + Image_Replace(image); + } +} + +Detour CG_RegisterGraphics_Detour; + +void CG_RegisterGraphics_Hook(int localClientNum, const char *mapname) +{ + CG_RegisterGraphics_Detour.GetOriginal()(localClientNum, mapname); + Load_images(); +} + +bool R_StreamLoadHighMipReplacement(const char *filename, unsigned int bytesToRead, unsigned __int8 *outData) +{ + std::string asset_name = extract_filename(filename); + auto asset = DB_FindXAssetEntry(ASSET_TYPE_IMAGE, asset_name.c_str()); + + if (!asset) + { + return false; + } + + 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) + { + return false; + } + + std::streamsize file_size = file.tellg(); + file.seekg(0, std::ios::beg); + + if (file_size - 0x80 != bytesToRead) // 0x80 is the size of the DDS header + { + 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); + + // TODO: check if file is DDS and has correct format and dimensions + + std::vector buffer; + buffer.resize(static_cast(bytesToRead)); + file.read(reinterpret_cast(buffer.data()), bytesToRead); + + GPUEndianSwapTexture(buffer, static_cast(image->texture.basemap->Format.Endian)); + + XGTEXTURE_DESC textureDesc; + XGGetTextureDesc(image->texture.basemap, 0, &textureDesc); + + // 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; + + 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) + ); + + return true; +} + +Detour R_StreamLoadFileSynchronously_Detour; + +int R_StreamLoadFileSynchronously_Hook(const char *filename, unsigned int bytesToRead, unsigned __int8 *outData) +{ + if (R_StreamLoadHighMipReplacement(filename, bytesToRead, outData)) + { + return 1; + } + + // Fallback to original path if modified path failed + return R_StreamLoadFileSynchronously_Detour.GetOriginal()( + filename, bytesToRead, outData); +} + +image_loader::image_loader() +{ + // Load raw texture replacements from active mod folder + CG_RegisterGraphics_Detour = Detour(CG_RegisterGraphics, CG_RegisterGraphics_Hook); + CG_RegisterGraphics_Detour.Install(); + + // Load highmip texture replacements from active mod folder + R_StreamLoadFileSynchronously_Detour = Detour(R_StreamLoadFileSynchronously, R_StreamLoadFileSynchronously_Hook); + R_StreamLoadFileSynchronously_Detour.Install(); + + cmd_function_s *imagedump_VAR = new cmd_function_s; + Cmd_AddCommandInternal("imagedump", Cmd_imagedump, imagedump_VAR); +} + +image_loader::~image_loader() +{ + CG_RegisterGraphics_Detour.Remove(); + + R_StreamLoadFileSynchronously_Detour.Remove(); +} +} // namespace mp +} // namespace iw3 diff --git a/src/game/iw3/mp/components/image_loader.h b/src/game/iw3/mp/components/image_loader.h new file mode 100644 index 0000000..e905a2e --- /dev/null +++ b/src/game/iw3/mp/components/image_loader.h @@ -0,0 +1,22 @@ +#pragma once + +#include "pch.h" + +namespace iw3 +{ +namespace mp +{ +class image_loader : public Module +{ + + public: + image_loader(); + ~image_loader(); + + const char *get_name() override + { + return "image_loader"; + }; +}; +} // namespace mp +} // namespace iw3 diff --git a/src/game/iw3/mp/main.cpp b/src/game/iw3/mp/main.cpp index f1e8d2f..e0d05ba 100644 --- a/src/game/iw3/mp/main.cpp +++ b/src/game/iw3/mp/main.cpp @@ -8,6 +8,7 @@ #include "components/gsc_client_fields.h" #include "components/gsc_functions.h" #include "components/gsc_methods.h" +#include "components/image_loader.h" #include "components/mpsp.h" #include "components/pm.h" #include "components/scr_parser.h" @@ -132,183 +133,6 @@ void CheckKeyboardCompletion() // If not completed, we'll check again next frame } -void GPUEndianSwapTexture(std::vector &pixelData, GPUENDIAN endianType) -{ - 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; - } -} - -// 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 -{ - DDSHeader header; - std::vector data; -}; - -// 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); -} - -DDSImage ReadDDSFile(const std::string &filepath) -{ - 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)); - - // 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 - { - DbgPrint("ERROR: Invalid DDS file: %s\n", filepath.c_str()); - file.close(); - return ddsImage; - } - - // 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(); - - // Ensure fileSize is valid before proceeding - if (fileSize == std::streampos(-1)) - { - DbgPrint("ERROR: Failed to determine file size.\n"); - file.close(); - return ddsImage; - } - - // Move back to after the header - file.seekg(sizeof(DDSHeader), std::ios::beg); - - // Compute data size safely - size_t dataSize = static_cast(fileSize) - sizeof(DDSHeader); - - // Read image data - ddsImage.data.resize(dataSize); - file.read(reinterpret_cast(ddsImage.data.data()), dataSize); - - file.close(); - - // 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)); - - return ddsImage; -} - namespace iw3 { namespace mp @@ -440,857 +264,6 @@ void Load_MapEntsPtr_Hook() } } -std::string extractFilename(const char *filename) -{ - std::string path(filename); - - // Find last backslash '\' or forward slash '/' - size_t lastSlash = path.find_last_of("\\/"); - size_t start = (lastSlash == std::string::npos) ? 0 : lastSlash + 1; - - // Find last dot '.' (extension separator) - size_t lastDot = path.find_last_of('.'); - size_t end = (lastDot == std::string::npos || lastDot < start) ? path.length() : lastDot; - - return path.substr(start, end - start); -} - -bool R_StreamLoadHighMipReplacement(const char *filename, unsigned int bytesToRead, unsigned __int8 *outData) -{ - std::string asset_name = extractFilename(filename); - auto asset = DB_FindXAssetEntry(ASSET_TYPE_IMAGE, asset_name.c_str()); - - if (!asset) - { - return false; - } - - 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) - { - return false; - } - - std::streamsize file_size = file.tellg(); - file.seekg(0, std::ios::beg); - - if (file_size - 0x80 != bytesToRead) // 0x80 is the size of the DDS header - { - 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); - - // TODO: check if file is DDS and has correct format and dimensions - - std::vector buffer; - buffer.resize(static_cast(bytesToRead)); - file.read(reinterpret_cast(buffer.data()), bytesToRead); - - GPUEndianSwapTexture(buffer, static_cast(image->texture.basemap->Format.Endian)); - - XGTEXTURE_DESC textureDesc; - XGGetTextureDesc(image->texture.basemap, 0, &textureDesc); - - // 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; - - 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) - ); - - return true; -} - -Detour R_StreamLoadFileSynchronously_Detour; - -int R_StreamLoadFileSynchronously_Hook(const char *filename, unsigned int bytesToRead, unsigned __int8 *outData) -{ - if (R_StreamLoadHighMipReplacement(filename, bytesToRead, outData)) - { - return 1; - } - - // Fallback to original path if modified path failed - return R_StreamLoadFileSynchronously_Detour.GetOriginal()( - filename, bytesToRead, outData); -} - -void Image_DbgPrint(const GfxImage *image) -{ - const int format = image->texture.basemap->Format.DataFormat; - char *format_str; - switch (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; - } - - XGTEXTURE_DESC SourceDesc; - XGGetTextureDesc(image->texture.basemap, 0, &SourceDesc); - BOOL IsBorderTexture = XGIsBorderTexture(image->texture.basemap); - UINT MipTailBaseLevel = XGGetMipTailBaseLevel(SourceDesc.Width, SourceDesc.Height, IsBorderTexture); - - // SourceDesc.BitsPerPixel; - // SourceDesc.BytesPerBlock; - - UINT MipLevelCount = image->texture.basemap->GetLevelCount(); - - UINT BaseSize; - XGGetTextureLayout(image->texture.basemap, 0, &BaseSize, 0, 0, 0, 0, 0, 0, 0, 0); - - 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); -} - -void Image_Dump(const GfxImage *image) -{ - // 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"); - return; - } - - if (!image->pixels || image->baseSize == 0) - { - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Image '%s' has no valid 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); - return; - } - - 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: - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported texture format %d!\n", format); - return; - } - - // Set texture capabilities - header.caps = _byteswap_ulong(DDSCAPS_TEXTURE | DDSCAPS_MIPMAP); - - // Handle Cubemaps - if (image->mapType == mp::MAPTYPE_CUBE) - { - header.caps2 = _byteswap_ulong(DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX | DDSCAPS2_CUBEMAP_NEGATIVEX | - DDSCAPS2_CUBEMAP_POSITIVEY | DDSCAPS2_CUBEMAP_NEGATIVEY | - DDSCAPS2_CUBEMAP_POSITIVEZ | DDSCAPS2_CUBEMAP_NEGATIVEZ); - } - - 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"; - - std::ofstream file(filename, std::ios::binary); - if (!file) - { - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Failed to open file: %s\n", filename.c_str()); - return; - } - - if (image->mapType == MAPTYPE_CUBE) - { - file.write(reinterpret_cast(&header), sizeof(DDSHeader)); - - unsigned int face_size = 0; - unsigned int rowPitch = 0; - const GPUTEXTUREFORMAT format = static_cast(image->texture.basemap->Format.DataFormat); - - switch (format) - { - 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); - 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 - - 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()); - } - - file.close(); - } - else if (image->mapType == MAPTYPE_2D) - { - // TODO: write mip levels - file.write(reinterpret_cast(&header), sizeof(DDSHeader)); - - std::vector pixelData(image->pixels, image->pixels + image->baseSize); - - GPUEndianSwapTexture(pixelData, static_cast(image->texture.basemap->Format.Endian)); - - // Create a linear data buffer to hold the untiled texture - std::vector linearData(image->baseSize); - - // Calculate row pitch based on format - UINT rowPitch; - auto format = image->texture.basemap->Format.DataFormat; - - switch (format) - { - 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); - 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(); - } - else - { - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Unsupported map type %d!\n", image->mapType); - return; - } -} - -void Cmd_imagedump() -{ - ImageList imageList; - R_GetImageList(&imageList); - - // images bundled in xex - // auto g_imageProgs = reinterpret_cast(0x84FEA6D0); - // for (unsigned int i = 0; i < 10; i++) - // { - // imageList.image[imageList.count++] = &g_imageProgs[i]; - // } - - CreateDirectoryA(DUMP_DIR, 0); - CreateDirectoryA((std::string(DUMP_DIR) + "\\images").c_str(), 0); - CreateDirectoryA((std::string(DUMP_DIR) + "\\highmip").c_str(), 0); - - for (unsigned int i = 0; i < imageList.count; i++) - { - auto image = imageList.image[i]; - // DbgPrint( - // "Image %d: Name='%s', Type=%d, Dimensions=%dx%dx%d, MipLevels=%d, Format=%d, CardMemory=%d bytes, - // BaseSize=%d, Streaming=%d, DelayLoad=%d, Semantic=%d, StreamSlot=%d\n", i, image->name, image->mapType, - // image->width, - // image->height, - // image->depth, // Added depth for 3D textures - // image->texture.basemap->Format.MaxMipLevel + 1, - // image->texture.basemap->Format.DataFormat, - // image->cardMemory.platform[1], // Total memory allocation for the texture - // image->baseSize, // Base memory size (for non-streaming textures) - // image->streaming, // Whether the image is streamed - // image->delayLoadPixels, // Whether pixel data is delayed in loading - // image->semantic, // Purpose of the texture in the engine - // image->streamSlot // Streaming slot ID - // ); - Image_DbgPrint(image); - - Image_Dump(image); - } - - auto highmips = filesystem::list_files_in_directory("D:\\highmip"); - for (size_t i = 0; i < highmips.size(); ++i) - { - const std::string &filepath = "D:\\highmip\\" + highmips[i]; - Com_Printf(CON_CHANNEL_CONSOLEONLY, "Dumping highmip file: %s\n", filepath.c_str()); - std::string assetName = extractFilename(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; - } - - auto image = asset->entry.asset.header.image; - - 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; - } - - std::streamsize size = input_file.tellg(); - if (size < 0) - { - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Failed to determine file size: %s\n", filepath.c_str()); - continue; - } - - input_file.seekg(0, std::ios::beg); - std::vector buffer(static_cast(size)); - - if (input_file.read(reinterpret_cast(buffer.data()), size)) - { - Com_Printf(CON_CHANNEL_CONSOLEONLY, "Read %d bytes from file.\n", size); - } - else - { - Com_PrintError(CON_CHANNEL_ERROR, "Image_Dump: Error reading file: %s\n", filepath.c_str()); - continue; - } - - 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()); - } -} - -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) - { - 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 - UINT blocksWide = (mipWidth + 3) / 4; - UINT blocksHigh = (mipHeight + 3) / 4; - - // Calculate total size in bytes - UINT sizeInBytes = blocksWide * blocksHigh * blockSize; - - return sizeInBytes; -} - -void Image_Replace_2D(GfxImage *image, const DDSImage &ddsImage) -{ - if (image->mapType != MAPTYPE_2D) - { - Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a 2D map!\n", image->name); - return; - } - - // 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)); - - UINT ddsOffset = 0; - - for (UINT mipLevel = 0; mipLevel < mipTailBaseLevel; mipLevel++) - { - 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) - { - 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; - } - - 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); - - UINT address = baseAddress; - if (mipLevel > 0) - { - UINT mipLevelOffset = XGGetMipLevelOffset(image->texture.basemap, 0, mipLevel); - address = mipAddress + mipLevelOffset; - } - - DbgPrint("Image_Replace_2D: Writing mip level %d to address 0x%08X - levelSize=%u\n", mipLevel, address, - ddsMipLevelSize); - - // // 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) - ); - - ddsOffset += ddsMipLevelSize; - } -} - -void Image_Replace_Cube(GfxImage *image, const DDSImage &ddsImage) -{ - if (image->mapType != MAPTYPE_CUBE) - { - Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a cube map!\n", image->name); - return; - } - - 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; - - switch (format) - { - 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; - } - - for (int i = 0; i < 6; i++) - { - const unsigned char *face_pixels = ddsImage.data.data() + (i * face_size); - - // 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)); - - // Copy the data to the image - memcpy(image->pixels + (i * face_size), tiledData.data(), face_size); - } -} - -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"; - - if (!filesystem::file_exists(replacement_path)) - { - Com_PrintError(CON_CHANNEL_ERROR, "File does not exist: %s\n", replacement_path.c_str()); - return; - } - - DDSImage ddsImage = ReadDDSFile(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) - { - Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' dimensions do not match DDS file: %s\n", image->name, - replacement_path.c_str()); - return; - } - - 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: - Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' has an unsupported DDS format: 0x%X\n", image->name, - ddsImage.header.pixelFormat.fourCC); - return; - } - - if (static_cast(image->texture.basemap->Format.DataFormat) != static_cast(ddsFormat)) - { - Com_PrintError(CON_CHANNEL_ERROR, "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; - } - - if (image->mapType == MAPTYPE_2D) - { - Image_Replace_2D(image, ddsImage); - } - else if (image->mapType == MAPTYPE_CUBE) - { - Image_Replace_Cube(image, ddsImage); - } - else - { - Com_PrintError(CON_CHANNEL_ERROR, "Image '%s' is not a 2D or cube map!\n", image->name); - return; - } -} - -void Load_images() -{ - const int MAX_IMAGES = 2048; - XAssetHeader assets[MAX_IMAGES]; - const auto count = DB_GetAllXAssetOfType_FastFile(ASSET_TYPE_IMAGE, assets, MAX_IMAGES); - for (int i = 0; i < count; i++) - { - GfxImage *image = assets[i].image; - // debug image metadata print out all - Image_Replace(image); - } -} - -Detour CG_RegisterGraphics_Detour; - -void CG_RegisterGraphics_Hook(int localClientNum, const char *mapname) -{ - CG_RegisterGraphics_Detour.GetOriginal()(localClientNum, mapname); - DbgPrint("CG_RegisterGraphics mapname=%s \n", mapname); - Load_images(); -} - const float colorWhiteRGBA[4] = {1.0f, 1.0f, 1.0f, 1.0f}; void DrawFixedFPS() @@ -1445,6 +418,7 @@ IW3_MP_Plugin::IW3_MP_Plugin() RegisterModule(new gsc_client_fields()); RegisterModule(new gsc_functions()); RegisterModule(new gsc_methods()); + RegisterModule(new image_loader()); RegisterModule(new pm()); RegisterModule(new mpsp()); RegisterModule(new scr_parser()); @@ -1462,15 +436,6 @@ IW3_MP_Plugin::IW3_MP_Plugin() Load_MapEntsPtr_Detour = Detour(Load_MapEntsPtr, Load_MapEntsPtr_Hook); Load_MapEntsPtr_Detour.Install(); - R_StreamLoadFileSynchronously_Detour = Detour(R_StreamLoadFileSynchronously, R_StreamLoadFileSynchronously_Hook); - R_StreamLoadFileSynchronously_Detour.Install(); - - cmd_function_s *imagedump_VAR = new cmd_function_s; - Cmd_AddCommandInternal("imagedump", Cmd_imagedump, imagedump_VAR); - - CG_RegisterGraphics_Detour = Detour(CG_RegisterGraphics, CG_RegisterGraphics_Hook); - CG_RegisterGraphics_Detour.Install(); - cmd_function_s *cmdinput_VAR = new cmd_function_s; Cmd_AddCommandInternal("cmdinput", Cmd_cmdinput_f, cmdinput_VAR); @@ -1490,7 +455,6 @@ IW3_MP_Plugin::~IW3_MP_Plugin() CG_DrawActive_Detour.Remove(); CL_GamepadButtonEvent_Detour.Remove(); Load_MapEntsPtr_Detour.Remove(); - R_StreamLoadFileSynchronously_Detour.Remove(); Pmove_Detour.Remove(); }