From db811bd0bac7b7a5a7abbf292c28764569314a36 Mon Sep 17 00:00:00 2001 From: salh Date: Wed, 22 Apr 2026 02:09:19 +0300 Subject: [PATCH] Support legacy DDS texture swaps and add Xenos format reference --- docs/TEXTURE_SWAPS.md | 58 -------------- docs/TEXTURE_SWAPS.txt | 137 ++++++++++++++++++++++++++++++++++ src/ac6_texture_overrides.cpp | 112 ++++++++++++++++++++------- 3 files changed, 223 insertions(+), 84 deletions(-) delete mode 100644 docs/TEXTURE_SWAPS.md create mode 100644 docs/TEXTURE_SWAPS.txt diff --git a/docs/TEXTURE_SWAPS.md b/docs/TEXTURE_SWAPS.md deleted file mode 100644 index 3d0946dc..00000000 --- a/docs/TEXTURE_SWAPS.md +++ /dev/null @@ -1,58 +0,0 @@ -# Texture Swaps - -The texture swap pipeline now lives in the authoritative RexGlue/Xenia D3D12 texture cache. It is dump-and-replace based, so you do not need to hand-author per-texture IDs before you can start modding. - -## Workflow - -1. Launch the game and let the texture cache see the textures you care about. -2. Dumped files appear under: - - `%USERPROFILE%\\Documents\\ac6recomp\\texture_dumps\\` - - or the directory set by the `user_data_root` runtime CVAR. -3. Each dumped texture produces: - - `.dds` - - `.json` -4. Edit the dumped DDS without changing its format, dimensions, array size, or mip count. -5. Place the replacement DDS at: - - `override/textures/.dds` - - or `mods//textures/.dds` -6. Restart or cause the texture to reload. - -`override/textures` wins over mod folders. Within `mods`, lexicographically later folder names win. - -## Stable Keys - -Dump filenames are generated from the texture cache key, not guessed game names. The filename includes: - -- a hash of the full cache key -- base and mip pages -- dimension -- size -- mip count -- guest format -- endian/tiled/packed/signed/scaled flags - -That makes the key stable enough for round-tripping replacements while still being readable. - -## Metadata Sidecars - -Each JSON sidecar records: - -- guest texture key fields -- chosen host DXGI format -- AC6 frame index -- latest AC6 backend signature ID -- active VS/PS hashes -- signature tags from the AC6 backend classifier - -This is meant for filtering and later tooling, not for the core replacement path. - -## Current Scope - -First pass limitations: - -- replacement files must be DX10-header DDS files -- replacement format, dimensions, depth/array size, and mip count must match exactly -- cube textures are skipped -- unsupported DXGI formats fall back to the original guest texture - -The fallback path is always the original guest texture load. A bad or missing replacement will not block rendering. diff --git a/docs/TEXTURE_SWAPS.txt b/docs/TEXTURE_SWAPS.txt new file mode 100644 index 00000000..97aef219 --- /dev/null +++ b/docs/TEXTURE_SWAPS.txt @@ -0,0 +1,137 @@ +Texture Swaps + +The texture swap pipeline lives in the authoritative RexGlue/Xenia D3D12 +texture cache. It is dump-and-replace based, so you do not need to hand-author +per-texture IDs before you can start modding. + +Workflow + +1. Launch the game and let the texture cache see the textures you care about. +2. Dumps appear under: + %USERPROFILE%\Documents\ac6recomp\texture_dumps\ + or the directory set by the user_data_root runtime CVAR. +3. Each dumped texture produces: + .dds + .json +4. Edit the dumped DDS without changing its format, dimensions, array size, or + mip count. +5. Place the replacement DDS at: + override/textures/.dds + or + mods//textures/.dds +6. Restart or cause the texture to reload. + +override/textures wins over mod folders. Within mods, lexicographically later +folder names win. + +Stable Keys + +Dump filenames are generated from the texture cache key, not guessed game +names. The filename includes: + - a hash of the full cache key + - base and mip pages + - dimension + - size + - mip count + - guest format + - endian/tiled/packed/signed/scaled flags + +That makes the key stable enough for round-tripping replacements while still +being readable. + +Metadata Sidecars + +Each JSON sidecar records: + - guest texture key fields + - chosen host DXGI format + - AC6 frame index + - latest AC6 backend signature ID + - active VS/PS hashes + - signature tags from the AC6 backend classifier + +This is meant for filtering and later tooling, not for the core replacement +path. + +Current Scope + +First pass limitations: + - replacement files must be DDS files + - DX10 DDS is supported + - legacy DXT1/DXT3/DXT5 DDS is supported for plain 2D textures + - replacement format, dimensions, depth/array size, and mip count must match + exactly + - cube textures are skipped + - unsupported DXGI formats fall back to the original guest texture + +The fallback path is always the original guest texture load. A bad or missing +replacement will not block rendering. + +Xenos Texture Formats + +Values below come from rex::graphics::xenos::TextureFormat in +thirdparty/rexglue-sdk/include/rex/graphics/xenos.h. + +0 k_1_REVERSE +1 k_1 +2 k_8 +3 k_1_5_5_5 +4 k_5_6_5 +5 k_6_5_5 +6 k_8_8_8_8 +7 k_2_10_10_10 +8 k_8_A +9 k_8_B +10 k_8_8 +11 k_Cr_Y1_Cb_Y0_REP +12 k_Y1_Cr_Y0_Cb_REP +13 k_16_16_EDRAM +14 k_8_8_8_8_A +15 k_4_4_4_4 +16 k_10_11_11 +17 k_11_11_10 +18 k_DXT1 +19 k_DXT2_3 +20 k_DXT4_5 +21 k_16_16_16_16_EDRAM +22 k_24_8 +23 k_24_8_FLOAT +24 k_16 +25 k_16_16 +26 k_16_16_16_16 +27 k_16_EXPAND +28 k_16_16_EXPAND +29 k_16_16_16_16_EXPAND +30 k_16_FLOAT +31 k_16_16_FLOAT +32 k_16_16_16_16_FLOAT +33 k_32 +34 k_32_32 +35 k_32_32_32_32 +36 k_32_FLOAT +37 k_32_32_FLOAT +38 k_32_32_32_32_FLOAT +39 k_32_AS_8 +40 k_32_AS_8_8 +41 k_16_MPEG +42 k_16_16_MPEG +43 k_8_INTERLACED +44 k_32_AS_8_INTERLACED +45 k_32_AS_8_8_INTERLACED +46 k_16_INTERLACED +47 k_16_MPEG_INTERLACED +48 k_16_16_MPEG_INTERLACED +49 k_DXN +50 k_8_8_8_8_AS_16_16_16_16 +51 k_DXT1_AS_16_16_16_16 +52 k_DXT2_3_AS_16_16_16_16 +53 k_DXT4_5_AS_16_16_16_16 +54 k_2_10_10_10_AS_16_16_16_16 +55 k_10_11_11_AS_16_16_16_16 +56 k_11_11_10_AS_16_16_16_16 +57 k_32_32_32_FLOAT +58 k_DXT3A +59 k_DXT5A +60 k_CTX1 +61 k_DXT3A_AS_1_1_1_1 +62 k_8_8_8_8_GAMMA_EDRAM +63 k_2_10_10_10_FLOAT_EDRAM diff --git a/src/ac6_texture_overrides.cpp b/src/ac6_texture_overrides.cpp index 0dd53cea..c4c6cc5a 100644 --- a/src/ac6_texture_overrides.cpp +++ b/src/ac6_texture_overrides.cpp @@ -31,6 +31,9 @@ namespace { constexpr uint32_t kDdsMagic = 0x20534444u; constexpr uint32_t kDdsFourCcDx10 = 0x30315844u; +constexpr uint32_t kDdsFourCcDxt1 = 0x31545844u; +constexpr uint32_t kDdsFourCcDxt3 = 0x33545844u; +constexpr uint32_t kDdsFourCcDxt5 = 0x35545844u; constexpr uint32_t kDdsHeaderFlagsTexture = 0x00001007u; constexpr uint32_t kDdsHeaderFlagsPitch = 0x00000008u; constexpr uint32_t kDdsHeaderFlagsLinearSize = 0x00080000u; @@ -40,6 +43,7 @@ constexpr uint32_t kDdsCapsTexture = 0x00001000u; constexpr uint32_t kDdsCapsComplex = 0x00000008u; constexpr uint32_t kDdsCapsMipmap = 0x00400000u; constexpr uint32_t kDdsCaps2Volume = 0x00200000u; +constexpr uint32_t kDdsCaps2Cubemap = 0x00000200u; constexpr uint32_t kDdsPixelFormatFlagsFourCc = 0x00000004u; constexpr uint32_t kDdsResourceDimensionTexture1D = 2u; constexpr uint32_t kDdsResourceDimensionTexture2D = 3u; @@ -300,6 +304,22 @@ bool MapDdsDimension(uint32_t dds_dimension, D3D12_RESOURCE_DIMENSION& out) { } } +bool MapLegacyDdsFourCc(uint32_t four_cc, DXGI_FORMAT& out) { + switch (four_cc) { + case kDdsFourCcDxt1: + out = DXGI_FORMAT_BC1_UNORM; + return true; + case kDdsFourCcDxt3: + out = DXGI_FORMAT_BC2_UNORM; + return true; + case kDdsFourCcDxt5: + out = DXGI_FORMAT_BC3_UNORM; + return true; + default: + return false; + } +} + uint32_t ToDdsDimension(D3D12_RESOURCE_DIMENSION dimension) { switch (dimension) { case D3D12_RESOURCE_DIMENSION_TEXTURE1D: @@ -437,50 +457,91 @@ bool LoadDdsFromFile(const std::filesystem::path& path, DdsImageData& out, std:: file.seekg(0, std::ios::end); const std::streamoff file_size = file.tellg(); file.seekg(0, std::ios::beg); - if (file_size < std::streamoff(sizeof(uint32_t) + sizeof(DdsHeader) + sizeof(DdsHeaderDx10))) { + if (file_size < std::streamoff(sizeof(uint32_t) + sizeof(DdsHeader))) { if (error_out) { - *error_out = "file is too small to be a DX10 DDS"; + *error_out = "file is too small to be a DDS"; } return false; } uint32_t magic = 0; DdsHeader header = {}; - DdsHeaderDx10 header_dx10 = {}; file.read(reinterpret_cast(&magic), sizeof(magic)); file.read(reinterpret_cast(&header), sizeof(header)); - file.read(reinterpret_cast(&header_dx10), sizeof(header_dx10)); if (!file || magic != kDdsMagic || header.size != sizeof(DdsHeader) || - header.pixel_format.size != sizeof(DdsPixelFormat) || - header.pixel_format.four_cc != kDdsFourCcDx10) { + header.pixel_format.size != sizeof(DdsPixelFormat)) { if (error_out) { - *error_out = "only DDS files with a DX10 header are supported"; - } - return false; - } - if ((header_dx10.misc_flag & kDdsResourceMiscTextureCube) != 0) { - if (error_out) { - *error_out = "cube DDS files are not supported by the first-pass texture swap loader"; + *error_out = "invalid DDS header"; } return false; } + DXGI_FORMAT dxgi_format = DXGI_FORMAT_UNKNOWN; D3D12_RESOURCE_DIMENSION dimension = D3D12_RESOURCE_DIMENSION_UNKNOWN; - if (!MapDdsDimension(header_dx10.resource_dimension, dimension)) { + uint32_t depth_or_array_size = 1; + size_t payload_offset = sizeof(uint32_t) + sizeof(DdsHeader); + + if (header.pixel_format.flags & kDdsPixelFormatFlagsFourCc) { + if (header.pixel_format.four_cc == kDdsFourCcDx10) { + if (file_size < std::streamoff(payload_offset + sizeof(DdsHeaderDx10))) { + if (error_out) { + *error_out = "file is too small to contain a DX10 DDS header"; + } + return false; + } + DdsHeaderDx10 header_dx10 = {}; + file.read(reinterpret_cast(&header_dx10), sizeof(header_dx10)); + if (!file) { + if (error_out) { + *error_out = "failed to read DX10 DDS header"; + } + return false; + } + payload_offset += sizeof(DdsHeaderDx10); + if ((header_dx10.misc_flag & kDdsResourceMiscTextureCube) != 0) { + if (error_out) { + *error_out = "cube DDS files are not supported by the first-pass texture swap loader"; + } + return false; + } + if (!MapDdsDimension(header_dx10.resource_dimension, dimension)) { + if (error_out) { + *error_out = "unsupported DDS resource dimension"; + } + return false; + } + dxgi_format = DXGI_FORMAT(header_dx10.dxgi_format); + depth_or_array_size = + dimension == D3D12_RESOURCE_DIMENSION_TEXTURE3D ? std::max(header.depth, 1u) + : std::max(header_dx10.array_size, 1u); + } else { + if (!MapLegacyDdsFourCc(header.pixel_format.four_cc, dxgi_format)) { + if (error_out) { + *error_out = "unsupported legacy DDS compression format"; + } + return false; + } + if (header.caps2 & (kDdsCaps2Cubemap | kDdsCaps2Volume)) { + if (error_out) { + *error_out = "legacy DDS support is limited to plain 2D textures"; + } + return false; + } + dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + depth_or_array_size = 1; + } + } else { if (error_out) { - *error_out = "unsupported DDS resource dimension"; + *error_out = "only FourCC DDS files are supported"; } return false; } const uint32_t mip_count = std::max(header.mip_map_count, 1u); - const uint32_t depth_or_array_size = - dimension == D3D12_RESOURCE_DIMENSION_TEXTURE3D ? std::max(header.depth, 1u) - : std::max(header_dx10.array_size, 1u); const uint32_t width = std::max(header.width, 1u); const uint32_t height = std::max(header.height, 1u); - if (!IsSupportedTextureSwapFormat(DXGI_FORMAT(header_dx10.dxgi_format))) { + if (!IsSupportedTextureSwapFormat(dxgi_format)) { if (error_out) { *error_out = "unsupported DXGI format in DDS file"; } @@ -488,7 +549,7 @@ bool LoadDdsFromFile(const std::filesystem::path& path, DdsImageData& out, std:: } DdsImageData image; - image.format = DXGI_FORMAT(header_dx10.dxgi_format); + image.format = dxgi_format; image.dimension = dimension; image.width = width; image.height = height; @@ -497,7 +558,7 @@ bool LoadDdsFromFile(const std::filesystem::path& path, DdsImageData& out, std:: image.is_cube = false; image.subresources.reserve(ComputeSubresourceCount(dimension, depth_or_array_size, mip_count)); - std::vector payload(size_t(file_size) - (sizeof(uint32_t) + sizeof(DdsHeader) + sizeof(DdsHeaderDx10))); + std::vector payload(size_t(file_size) - payload_offset); file.read(reinterpret_cast(payload.data()), std::streamsize(payload.size())); if (!file && !payload.empty()) { if (error_out) { @@ -505,8 +566,7 @@ bool LoadDdsFromFile(const std::filesystem::path& path, DdsImageData& out, std:: } return false; } - - size_t payload_offset = 0; + size_t payload_cursor = 0; const uint32_t subresource_count = ComputeSubresourceCount(dimension, depth_or_array_size, mip_count); for (uint32_t subresource_index = 0; subresource_index < subresource_count; ++subresource_index) { const uint32_t mip_index = @@ -527,7 +587,7 @@ bool LoadDdsFromFile(const std::filesystem::path& path, DdsImageData& out, std:: return false; } const size_t subresource_size = size_t(tight_layout.slice_pitch) * subresource.depth; - if (payload_offset + subresource_size > payload.size()) { + if (payload_cursor + subresource_size > payload.size()) { if (error_out) { *error_out = "DDS payload is truncated"; } @@ -536,8 +596,8 @@ bool LoadDdsFromFile(const std::filesystem::path& path, DdsImageData& out, std:: subresource.row_pitch = tight_layout.row_pitch; subresource.slice_pitch = tight_layout.slice_pitch; subresource.data.resize(subresource_size); - std::copy_n(payload.data() + payload_offset, subresource_size, subresource.data.data()); - payload_offset += subresource_size; + std::copy_n(payload.data() + payload_cursor, subresource_size, subresource.data.data()); + payload_cursor += subresource_size; image.subresources.push_back(std::move(subresource)); }