Improve texture swap modding workflow

This commit is contained in:
salh
2026-04-22 16:22:41 +03:00
parent d3bb76be61
commit 34d8c0185e
6 changed files with 697 additions and 11 deletions
+5
View File
@@ -40,6 +40,11 @@ cmake --build --preset win-amd64-relwithdebinfo
The executable is placed at `out/build/win-amd64-relwithdebinfo/ac6recomp.exe`.
## Modding Docs
- [Texture Swap Modding Guide](docs/TEXTURE_SWAP_MODDING_GUIDE.txt)
- [Texture Swap Reference](docs/TEXTURE_SWAPS.txt)
## Runtime Defaults
The default AC6 graphics configuration after this pivot is:
+47 -1
View File
@@ -10,6 +10,10 @@ Workflow
2. Dumps appear under:
%USERPROFILE%\Documents\ac6recomp\texture_dumps\
or the directory set by the user_data_root runtime CVAR.
New dumps are also mirrored into:
%USERPROFILE%\Documents\ac6recomp\texture_dumps\sessions\<session_id>\
and the current session path is written to:
%USERPROFILE%\Documents\ac6recomp\texture_dumps\current_session.txt
3. Each dumped texture produces:
<stable_key>.dds
<stable_key>.json
@@ -24,6 +28,40 @@ Workflow
override/textures wins over mod folders. Within mods, lexicographically later
folder names win.
Shared Replacements
Exact <stable_key>.dds matches still work, but texture override folders now
also support an optional manifest:
override/textures/manifest.toml
mods/<mod_name>/textures/manifest.toml
That lets one DDS serve multiple dumped textures, which is useful when the game
has several menu-specific versions of the same logical art.
Example:
[[swap]]
source = "shared/playstation_buttons.dds"
stable_keys = [
"tex_1111111111111111_bp00000000_mp00000000_2d_512x256x1_m1_fmt18_e0_t1_p0_s0_r0",
"tex_2222222222222222_bp00000000_mp00000000_2d_512x256x1_m1_fmt18_e0_t1_p0_s0_r0",
]
You can also use wildcards:
[[swap]]
source = "shared/common_ui_256.dds"
stable_key_globs = [
"tex_*_2d_256x256x1_m1_fmt6_*",
]
Manifest notes:
- source paths are relative to the texture override folder that owns the manifest
- exact <stable_key>.dds files still win over manifest rules in the same folder
- if multiple manifest rules match, the later rule wins
- later mod folders still override earlier mod folders
Stable Keys
Dump filenames are generated from the texture cache key, not guessed game
@@ -50,7 +88,8 @@ Each JSON sidecar records:
- signature tags from the AC6 backend classifier
This is meant for filtering and later tooling, not for the core replacement
path.
path, but the stable_key and layout fields are handy when authoring manifest
rules.
Current Scope
@@ -66,6 +105,13 @@ First pass limitations:
The fallback path is always the original guest texture load. A bad or missing
replacement will not block rendering.
Practical Workflow Tip
If you are targeting one menu or one HUD state, start the game fresh, open just
that screen, then inspect current_session.txt or the matching sessions folder.
That gives you a much smaller set of fresh dumps instead of the entire dump
history.
Xenos Texture Formats
Values below come from rex::graphics::xenos::TextureFormat in
+205
View File
@@ -0,0 +1,205 @@
Texture Swap Modding Guide
This guide is for modders who want to replace in-game textures without digging
through the renderer code.
What the system does
The game can dump textures it sees at runtime as DDS files. You can edit those
DDS files and place replacements in the override folders. The game will load
your replacements the next time those textures are used.
The texture swap system now supports:
- session-based dump browsing, so you can work from a small fresh folder
- exact one-file replacements with <stable_key>.dds
- manifest rules, so one DDS can replace multiple dumped textures
Where dumps go
Global dump history:
%USERPROFILE%\Documents\ac6recomp\texture_dumps\
Current session pointer:
%USERPROFILE%\Documents\ac6recomp\texture_dumps\current_session.txt
Current session folder:
%USERPROFILE%\Documents\ac6recomp\texture_dumps\sessions\<session_id>\
The session folder is the main quality-of-life feature for modding. If you boot
the game fresh and only visit one menu, that session folder should stay much
smaller than the full dump history.
Basic workflow
1. Start with a clean target
Launch the game fresh if possible.
Go straight to the menu, HUD, or screen you want to mod.
2. Open the current session folder
Read current_session.txt.
Open the session_path listed there.
3. Identify the texture you want
Look through the DDS files in the current session folder.
Each DDS has a matching JSON file with the same stable key.
4. Edit the DDS
Keep the same:
- format
- width and height
- array size or depth
- mip count
5. Install the replacement
Put the edited DDS in one of these locations:
override/textures/<stable_key>.dds
or
mods/<mod_name>/textures/<stable_key>.dds
6. Reload the texture
Restart the game or otherwise make the texture reload.
Recommended folder layout
For a loose override:
Documents\ac6recomp\
override\
textures\
tex_....dds
For a mod:
Documents\ac6recomp\
mods\
my_ui_mod\
textures\
tex_....dds
Override priority
Priority is:
- override/textures first
- then mods/<mod_name>/textures
- among mods, later folder names win
That means a loose override is best for testing, while a mod folder is better
for packaging and sharing.
Using one DDS for many textures
Some UI assets exist in several menu-specific variants. Instead of copying the
same replacement to many stable keys, you can use a manifest.
Create:
override/textures/manifest.toml
or:
mods/<mod_name>/textures/manifest.toml
Example with exact keys:
[[swap]]
source = "shared/playstation_buttons.dds"
stable_keys = [
"tex_1111111111111111_bp00000000_mp00000000_2d_512x256x1_m1_fmt18_e0_t1_p0_s0_r0",
"tex_2222222222222222_bp00000000_mp00000000_2d_512x256x1_m1_fmt18_e0_t1_p0_s0_r0",
]
Example with wildcard matching:
[[swap]]
source = "shared/common_ui_256.dds"
stable_key_globs = [
"tex_*_2d_256x256x1_m1_fmt6_*",
]
Manifest behavior
- source is relative to the textures folder that contains manifest.toml
- exact <stable_key>.dds files still beat manifest rules in the same folder
- if more than one manifest rule matches, the later rule wins
- later mod folders still beat earlier mod folders
Example mod layout with a manifest
Documents\ac6recomp\
mods\
playstation_buttons\
textures\
manifest.toml
shared\
playstation_buttons.dds
How to use the JSON sidecars
Each dumped texture has a matching JSON file. The JSON is useful when the DDS
preview alone is not enough.
Useful fields include:
- stable_key
- width
- height
- guest_format
- dxgi_format
- signature_tags
- active_vertex_shader_hash
- active_pixel_shader_hash
You do not need to edit the JSON. It is there to help you identify and group
textures.
Good workflow for menu mods
If you are replacing UI prompts, button art, or menu icons:
1. Start a fresh session.
2. Open only the target menu.
3. Inspect the current session folder.
4. Find the relevant DDS files.
5. If several files are really the same logical art, create one shared DDS and
bind them together with manifest.toml.
This is much faster than browsing the entire dump history every time.
Troubleshooting
Problem: The session folder exists but is empty.
Cause: The textures you hit may already exist in the global dump history, or
the relevant texture was never encountered.
What to do: Trigger the texture again. Existing global dumps should now also be
mirrored into the current session folder when they are encountered.
Problem: My replacement does not load.
Cause: The DDS does not match the original texture layout, or the stable key is
wrong, or a higher-priority override is winning.
What to do:
- confirm the filename or manifest rule matches the dumped stable key
- keep the same format, size, array/depth, and mip count
- test with override/textures first
Problem: The wrong texture is replaced.
Cause: A wildcard manifest rule is too broad.
What to do: Narrow the stable_key_globs rule or switch to exact stable_keys.
Practical advice
- Use the current session folder first, not the full dump folder.
- Use loose overrides for quick testing.
- Use mod folders plus a manifest for cleaner releases.
- Prefer one shared DDS plus manifest rules when the game has several
duplicates of the same art.
Reference
For the lower-level technical reference, see:
docs/TEXTURE_SWAPS.txt
+417 -10
View File
@@ -2,14 +2,20 @@
#include <algorithm>
#include <array>
#include <chrono>
#include <ctime>
#include <fstream>
#include <iomanip>
#include <mutex>
#include <sstream>
#include <system_error>
#include <unordered_map>
#include <utility>
#include <rex/cvar.h>
#include <rex/filesystem.h>
#include <rex/logging.h>
#include <toml++/toml.hpp>
REXCVAR_DECLARE(std::string, user_data_root);
@@ -95,6 +101,24 @@ struct DxgiLayoutInfo {
const char* name;
};
struct TextureSwapRule {
std::filesystem::path source_path;
std::vector<std::string> stable_keys;
std::vector<std::string> stable_key_globs;
};
struct TextureSwapManifestCacheEntry {
bool present = false;
bool parse_failed = false;
std::filesystem::file_time_type last_write_time{};
std::vector<TextureSwapRule> rules;
};
std::mutex g_texture_swap_manifest_mutex;
std::unordered_map<std::string, TextureSwapManifestCacheEntry> g_texture_swap_manifest_cache;
bool EnsureParentExists(const std::filesystem::path& path, std::string* error_out);
std::filesystem::path GetUserDataRoot() {
const std::string user_root = REXCVAR_GET(user_data_root);
if (!user_root.empty()) {
@@ -103,6 +127,60 @@ std::filesystem::path GetUserDataRoot() {
return rex::filesystem::GetUserFolder() / "ac6recomp";
}
std::filesystem::path GetTextureDumpRoot() {
return GetUserDataRoot() / REXCVAR_GET(ac6_texture_swaps_dump_dir);
}
std::string BuildTextureDumpSessionId() {
const auto now = std::chrono::system_clock::now();
const auto time_value = std::chrono::system_clock::to_time_t(now);
std::tm local_time{};
#if defined(_WIN32)
localtime_s(&local_time, &time_value);
#else
localtime_r(&time_value, &local_time);
#endif
const auto milliseconds =
std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) %
std::chrono::seconds(1);
std::ostringstream stream;
stream << std::put_time(&local_time, "%Y%m%d_%H%M%S") << "_" << std::setw(3)
<< std::setfill('0') << milliseconds.count();
return stream.str();
}
const std::string& GetTextureDumpSessionId() {
static const std::string session_id = BuildTextureDumpSessionId();
return session_id;
}
std::filesystem::path GetTextureDumpSessionsRoot() {
return GetTextureDumpRoot() / "sessions";
}
void PublishCurrentTextureDumpSessionInfo() {
static std::once_flag once;
std::call_once(once, []() {
const std::filesystem::path session_root =
GetTextureDumpSessionsRoot() / GetTextureDumpSessionId();
std::string error;
if (!EnsureParentExists(session_root / "placeholder", &error)) {
REXLOG_WARN("Texture swap dump session: failed to create session root ({})", error);
return;
}
std::ofstream file(GetTextureDumpRoot() / "current_session.txt", std::ios::out | std::ios::trunc);
if (!file) {
REXLOG_WARN("Texture swap dump session: failed to write current_session.txt");
return;
}
file << "session_id=" << GetTextureDumpSessionId() << "\n";
file << "session_path=" << session_root.string() << "\n";
});
}
bool EnsureParentExists(const std::filesystem::path& path, std::string* error_out) {
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
@@ -143,6 +221,266 @@ std::string EscapeJson(std::string_view value) {
return escaped;
}
char AsciiToLower(char value) {
return (value >= 'A' && value <= 'Z') ? char(value - 'A' + 'a') : value;
}
bool EqualsIgnoreAsciiCase(std::string_view lhs, std::string_view rhs) {
if (lhs.size() != rhs.size()) {
return false;
}
for (size_t i = 0; i < lhs.size(); ++i) {
if (AsciiToLower(lhs[i]) != AsciiToLower(rhs[i])) {
return false;
}
}
return true;
}
bool WildcardMatchRecursive(std::string_view pattern, std::string_view value, size_t pattern_index,
size_t value_index) {
while (pattern_index < pattern.size()) {
const char pattern_char = pattern[pattern_index];
if (pattern_char == '*') {
while (pattern_index + 1 < pattern.size() && pattern[pattern_index + 1] == '*') {
++pattern_index;
}
if (pattern_index + 1 == pattern.size()) {
return true;
}
for (size_t candidate = value_index; candidate <= value.size(); ++candidate) {
if (WildcardMatchRecursive(pattern, value, pattern_index + 1, candidate)) {
return true;
}
}
return false;
}
if (value_index >= value.size()) {
return false;
}
if (pattern_char != '?' && AsciiToLower(pattern_char) != AsciiToLower(value[value_index])) {
return false;
}
++pattern_index;
++value_index;
}
return value_index == value.size();
}
bool WildcardMatch(std::string_view pattern, std::string_view value) {
return WildcardMatchRecursive(pattern, value, 0, 0);
}
std::vector<std::string> ParseStringList(const toml::node_view<const toml::node>& node) {
std::vector<std::string> values;
if (const auto value = node.value<std::string>()) {
values.push_back(*value);
return values;
}
const toml::array* array = node.as_array();
if (!array) {
return values;
}
values.reserve(array->size());
for (const toml::node& item : *array) {
if (const auto string_value = item.value<std::string>()) {
values.push_back(*string_value);
}
}
return values;
}
bool PathStartsWith(const std::filesystem::path& path, const std::filesystem::path& prefix) {
auto path_it = path.begin();
auto prefix_it = prefix.begin();
while (prefix_it != prefix.end()) {
if (path_it == path.end()) {
return false;
}
if (!EqualsIgnoreAsciiCase(path_it->string(), prefix_it->string())) {
return false;
}
++path_it;
++prefix_it;
}
return true;
}
std::optional<std::filesystem::path> ResolveManifestSourcePath(const std::filesystem::path& root,
std::string_view source_value) {
if (source_value.empty()) {
return std::nullopt;
}
const std::filesystem::path source_path = std::filesystem::path(source_value);
if (source_path.is_absolute()) {
return std::nullopt;
}
const std::filesystem::path normalized_root = root.lexically_normal();
const std::filesystem::path resolved = (normalized_root / source_path).lexically_normal();
if (!PathStartsWith(resolved, normalized_root)) {
return std::nullopt;
}
return resolved;
}
std::filesystem::path GetTextureSwapManifestPath(const std::filesystem::path& root) {
return root / "manifest.toml";
}
TextureSwapManifestCacheEntry LoadTextureSwapManifestCacheEntry(const std::filesystem::path& root) {
TextureSwapManifestCacheEntry entry;
const std::filesystem::path manifest_path = GetTextureSwapManifestPath(root);
std::error_code ec;
if (!std::filesystem::exists(manifest_path, ec) || ec) {
return entry;
}
entry.present = true;
entry.last_write_time = std::filesystem::last_write_time(manifest_path, ec);
if (ec) {
entry.last_write_time = {};
}
toml::table manifest;
try {
manifest = toml::parse_file(manifest_path.string());
} catch (const toml::parse_error& err) {
entry.parse_failed = true;
REXLOG_WARN("Texture swap manifest {}: parse error: {}", manifest_path.string(), err.description());
return entry;
}
const toml::array* swaps = manifest["swap"].as_array();
if (!swaps) {
return entry;
}
for (const toml::node& swap_node : *swaps) {
const toml::table* table = swap_node.as_table();
if (!table) {
continue;
}
const auto source_value = (*table)["source"].value<std::string>();
if (!source_value) {
continue;
}
std::optional<std::filesystem::path> source_path = ResolveManifestSourcePath(root, *source_value);
if (!source_path) {
REXLOG_WARN("Texture swap manifest {}: ignoring rule with invalid source '{}'",
manifest_path.string(), *source_value);
continue;
}
TextureSwapRule rule;
rule.source_path = *source_path;
auto append_values = [](std::vector<std::string>& out, std::vector<std::string>&& values) {
out.insert(out.end(), std::make_move_iterator(values.begin()), std::make_move_iterator(values.end()));
};
append_values(rule.stable_keys, ParseStringList((*table)["stable_key"]));
append_values(rule.stable_keys, ParseStringList((*table)["stable_keys"]));
append_values(rule.stable_keys, ParseStringList((*table)["key"]));
append_values(rule.stable_keys, ParseStringList((*table)["keys"]));
append_values(rule.stable_key_globs, ParseStringList((*table)["stable_key_glob"]));
append_values(rule.stable_key_globs, ParseStringList((*table)["stable_key_globs"]));
append_values(rule.stable_key_globs, ParseStringList((*table)["pattern"]));
append_values(rule.stable_key_globs, ParseStringList((*table)["patterns"]));
if (rule.stable_keys.empty() && rule.stable_key_globs.empty()) {
REXLOG_WARN("Texture swap manifest {}: ignoring rule for {} with no keys or patterns",
manifest_path.string(), rule.source_path.string());
continue;
}
entry.rules.push_back(std::move(rule));
}
return entry;
}
const TextureSwapManifestCacheEntry& GetTextureSwapManifestCacheEntry(const std::filesystem::path& root) {
const std::filesystem::path normalized_root = root.lexically_normal();
const std::string cache_key = normalized_root.string();
const std::filesystem::path manifest_path = GetTextureSwapManifestPath(normalized_root);
std::error_code ec;
const bool present = std::filesystem::exists(manifest_path, ec) && !ec;
std::filesystem::file_time_type last_write_time{};
if (present) {
last_write_time = std::filesystem::last_write_time(manifest_path, ec);
if (ec) {
last_write_time = {};
}
}
std::lock_guard<std::mutex> lock(g_texture_swap_manifest_mutex);
auto it = g_texture_swap_manifest_cache.find(cache_key);
if (it == g_texture_swap_manifest_cache.end() || it->second.present != present ||
(present && it->second.last_write_time != last_write_time)) {
it = g_texture_swap_manifest_cache
.insert_or_assign(cache_key, LoadTextureSwapManifestCacheEntry(normalized_root))
.first;
}
return it->second;
}
bool TextureSwapRuleMatches(const TextureSwapRule& rule, std::string_view stable_key) {
for (const std::string& exact_key : rule.stable_keys) {
if (EqualsIgnoreAsciiCase(exact_key, stable_key)) {
return true;
}
}
for (const std::string& pattern : rule.stable_key_globs) {
if (WildcardMatch(pattern, stable_key)) {
return true;
}
}
return false;
}
std::optional<std::filesystem::path> ResolveManifestReplacementDdsPath(
const std::filesystem::path& root, std::string_view stable_key) {
const TextureSwapManifestCacheEntry& entry = GetTextureSwapManifestCacheEntry(root);
if (!entry.present || entry.parse_failed || entry.rules.empty()) {
return std::nullopt;
}
std::optional<std::filesystem::path> resolved;
std::error_code ec;
for (const TextureSwapRule& rule : entry.rules) {
if (!TextureSwapRuleMatches(rule, stable_key)) {
continue;
}
if (std::filesystem::exists(rule.source_path, ec)) {
resolved = rule.source_path;
}
}
return resolved;
}
std::optional<std::filesystem::path> ResolveReplacementDdsPathInRoot(
const std::filesystem::path& root, std::string_view stable_key) {
const std::filesystem::path file_name = std::string(stable_key) + ".dds";
std::error_code ec;
const std::filesystem::path exact_path = root / file_name;
if (std::filesystem::exists(exact_path, ec)) {
return exact_path;
}
return ResolveManifestReplacementDdsPath(root, stable_key);
}
std::string HexU32(uint32_t value) {
std::ostringstream stream;
stream << std::uppercase << std::hex << std::setw(8) << std::setfill('0') << value;
@@ -390,13 +728,31 @@ std::string BuildTextureStableKey(uint64_t texture_key_hash, uint32_t base_page,
}
std::filesystem::path GetTextureDumpDdsPath(std::string_view stable_key) {
return GetUserDataRoot() / REXCVAR_GET(ac6_texture_swaps_dump_dir) /
(std::string(stable_key) + ".dds");
PublishCurrentTextureDumpSessionInfo();
return GetTextureDumpRoot() / (std::string(stable_key) + ".dds");
}
std::filesystem::path GetTextureDumpMetadataPath(std::string_view stable_key) {
return GetUserDataRoot() / REXCVAR_GET(ac6_texture_swaps_dump_dir) /
(std::string(stable_key) + ".json");
PublishCurrentTextureDumpSessionInfo();
return GetTextureDumpRoot() / (std::string(stable_key) + ".json");
}
std::filesystem::path GetTextureDumpCurrentSessionRoot() {
PublishCurrentTextureDumpSessionInfo();
return GetTextureDumpSessionsRoot() / GetTextureDumpSessionId();
}
std::filesystem::path GetTextureDumpCurrentSessionDdsPath(std::string_view stable_key) {
return GetTextureDumpCurrentSessionRoot() / (std::string(stable_key) + ".dds");
}
std::filesystem::path GetTextureDumpCurrentSessionMetadataPath(std::string_view stable_key) {
return GetTextureDumpCurrentSessionRoot() / (std::string(stable_key) + ".json");
}
std::filesystem::path GetTextureDumpCurrentSessionInfoPath() {
PublishCurrentTextureDumpSessionInfo();
return GetTextureDumpRoot() / "current_session.txt";
}
bool DumpExists(std::string_view stable_key) {
@@ -404,18 +760,69 @@ bool DumpExists(std::string_view stable_key) {
return std::filesystem::exists(GetTextureDumpDdsPath(stable_key), ec);
}
bool MirrorDumpToCurrentSession(std::string_view stable_key, std::string* error_out) {
PublishCurrentTextureDumpSessionInfo();
const std::filesystem::path source_dds = GetTextureDumpDdsPath(stable_key);
const std::filesystem::path source_json = GetTextureDumpMetadataPath(stable_key);
const std::filesystem::path dest_dds = GetTextureDumpCurrentSessionDdsPath(stable_key);
const std::filesystem::path dest_json = GetTextureDumpCurrentSessionMetadataPath(stable_key);
std::error_code ec;
if (!std::filesystem::exists(source_dds, ec) || ec) {
if (error_out) {
*error_out = "source DDS dump does not exist";
}
return false;
}
ec.clear();
if (!std::filesystem::exists(source_json, ec) || ec) {
if (error_out) {
*error_out = "source metadata dump does not exist";
}
return false;
}
std::string ensure_error;
if (!EnsureParentExists(dest_dds, &ensure_error) || !EnsureParentExists(dest_json, &ensure_error)) {
if (error_out) {
*error_out = ensure_error;
}
return false;
}
auto copy_if_needed = [&](const std::filesystem::path& source, const std::filesystem::path& dest,
const char* label) -> bool {
std::error_code local_ec;
if (std::filesystem::exists(dest, local_ec) && !local_ec) {
return true;
}
local_ec.clear();
if (!std::filesystem::copy_file(source, dest, std::filesystem::copy_options::overwrite_existing,
local_ec)) {
if (error_out) {
*error_out = std::string("failed to mirror ") + label + ": " + local_ec.message();
}
return false;
}
return true;
};
return copy_if_needed(source_dds, dest_dds, "DDS") &&
copy_if_needed(source_json, dest_json, "metadata");
}
std::optional<std::filesystem::path> ResolveReplacementDdsPath(std::string_view stable_key) {
if (!TextureReplacementEnabled()) {
return std::nullopt;
}
const std::filesystem::path user_root = GetUserDataRoot();
const std::filesystem::path file_name = std::string(stable_key) + ".dds";
std::error_code ec;
const std::filesystem::path loose_path =
user_root / REXCVAR_GET(ac6_texture_swaps_override_dir) / file_name;
if (std::filesystem::exists(loose_path, ec)) {
const std::filesystem::path loose_root = user_root / REXCVAR_GET(ac6_texture_swaps_override_dir);
if (const std::optional<std::filesystem::path> loose_path =
ResolveReplacementDdsPathInRoot(loose_root, stable_key)) {
return loose_path;
}
@@ -437,8 +844,8 @@ std::optional<std::filesystem::path> ResolveReplacementDdsPath(std::string_view
std::optional<std::filesystem::path> resolved;
for (const std::filesystem::path& mod_root : mod_roots) {
const std::filesystem::path candidate = mod_root / "textures" / file_name;
if (std::filesystem::exists(candidate, ec)) {
if (const std::optional<std::filesystem::path> candidate =
ResolveReplacementDdsPathInRoot(mod_root / "textures", stable_key)) {
resolved = candidate;
}
}
+5
View File
@@ -78,7 +78,12 @@ std::string BuildTextureStableKey(uint64_t texture_key_hash, uint32_t base_page,
std::filesystem::path GetTextureDumpDdsPath(std::string_view stable_key);
std::filesystem::path GetTextureDumpMetadataPath(std::string_view stable_key);
std::filesystem::path GetTextureDumpCurrentSessionRoot();
std::filesystem::path GetTextureDumpCurrentSessionDdsPath(std::string_view stable_key);
std::filesystem::path GetTextureDumpCurrentSessionMetadataPath(std::string_view stable_key);
std::filesystem::path GetTextureDumpCurrentSessionInfoPath();
bool DumpExists(std::string_view stable_key);
bool MirrorDumpToCurrentSession(std::string_view stable_key, std::string* error_out = nullptr);
std::optional<std::filesystem::path> ResolveReplacementDdsPath(std::string_view stable_key);
@@ -2343,6 +2343,19 @@ void D3D12TextureCache::ProcessCompletedTextureTransfers() {
} else if (!ac6::textures::WriteDumpMetadata(
ac6::textures::GetTextureDumpMetadataPath(it->stable_key), metadata, &error)) {
REXGPU_WARN("Texture swap dump {}: failed to write metadata ({})", it->stable_key, error);
} else {
std::string session_error;
if (!ac6::textures::WriteDdsToFile(
ac6::textures::GetTextureDumpCurrentSessionDdsPath(it->stable_key), dds_image,
&session_error)) {
REXGPU_WARN("Texture swap dump {}: failed to write session DDS ({})", it->stable_key,
session_error);
} else if (!ac6::textures::WriteDumpMetadata(
ac6::textures::GetTextureDumpCurrentSessionMetadataPath(it->stable_key),
metadata, &session_error)) {
REXGPU_WARN("Texture swap dump {}: failed to write session metadata ({})",
it->stable_key, session_error);
}
}
it = pending_texture_dumps_.erase(it);
@@ -2363,6 +2376,11 @@ bool D3D12TextureCache::ScheduleTextureDump(D3D12Texture& texture, DXGI_FORMAT d
key.scaled_resolve != 0);
if (dumped_texture_keys_.contains(stable_key) || ac6::textures::DumpExists(stable_key)) {
std::string mirror_error;
if (!ac6::textures::MirrorDumpToCurrentSession(stable_key, &mirror_error)) {
REXGPU_WARN("Texture swap dump {}: failed to mirror existing dump into session ({})",
stable_key, mirror_error);
}
dumped_texture_keys_.insert(stable_key);
return false;
}