mirror of
https://github.com/sal063/AC6_recomp
synced 2026-07-11 23:30:18 -04:00
Improve texture swap modding workflow
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user