mirror of
https://github.com/sal063/AC6_recomp
synced 2026-07-11 15:29:02 -04:00
Support legacy DDS texture swaps and add Xenos format reference
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user