Support legacy DDS texture swaps and add Xenos format reference

This commit is contained in:
salh
2026-04-22 02:09:19 +03:00
parent 3d3579d290
commit db811bd0ba
3 changed files with 223 additions and 84 deletions
-58
View File
@@ -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:
- `<stable_key>.dds`
- `<stable_key>.json`
4. Edit the dumped DDS without changing its format, dimensions, array size, or mip count.
5. Place the replacement DDS at:
- `override/textures/<stable_key>.dds`
- or `mods/<mod_name>/textures/<stable_key>.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.
+137
View File
@@ -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:
<stable_key>.dds
<stable_key>.json
4. Edit the dumped DDS without changing its format, dimensions, array size, or
mip count.
5. Place the replacement DDS at:
override/textures/<stable_key>.dds
or
mods/<mod_name>/textures/<stable_key>.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
+86 -26
View File
@@ -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<char*>(&magic), sizeof(magic));
file.read(reinterpret_cast<char*>(&header), sizeof(header));
file.read(reinterpret_cast<char*>(&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<char*>(&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<uint8_t> payload(size_t(file_size) - (sizeof(uint32_t) + sizeof(DdsHeader) + sizeof(DdsHeaderDx10)));
std::vector<uint8_t> payload(size_t(file_size) - payload_offset);
file.read(reinterpret_cast<char*>(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));
}