From 3d3579d290adfd16534b4389e343debaaad6ba45 Mon Sep 17 00:00:00 2001 From: salh Date: Wed, 22 Apr 2026 01:06:29 +0300 Subject: [PATCH] Implement texture swap dump and replace pipeline --- CMakeLists.txt | 1 + docs/TEXTURE_SWAPS.md | 58 ++ .../win-amd64-relwithdebinfo/ac6recomp.toml | 2 +- src/ac6_native_graphics.cpp | 29 +- src/ac6_texture_overrides.cpp | 699 ++++++++++++++++++ src/ac6_texture_overrides.h | 92 +++ src/d3d_hooks.cpp | 16 +- src/d3d_hooks.h | 3 +- src/main.cpp | 39 + .../rex/graphics/d3d12/command_processor.h | 4 +- .../rex/graphics/d3d12/texture_cache.h | 44 ++ .../src/graphics/d3d12/texture_cache.cpp | 382 ++++++++++ 12 files changed, 1355 insertions(+), 14 deletions(-) create mode 100644 docs/TEXTURE_SWAPS.md create mode 100644 src/ac6_texture_overrides.cpp create mode 100644 src/ac6_texture_overrides.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3448460b..3e96e867 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ set(AC6RECOMP_SOURCES src/main.cpp src/d3d_hooks.cpp src/render_hooks.cpp + src/ac6_texture_overrides.cpp src/ac6_native_graphics.cpp src/ac6_native_graphics_overlay.cpp src/ac6_backend_fixes/ac6_backend_capture_bridge.cpp diff --git a/docs/TEXTURE_SWAPS.md b/docs/TEXTURE_SWAPS.md new file mode 100644 index 00000000..3d0946dc --- /dev/null +++ b/docs/TEXTURE_SWAPS.md @@ -0,0 +1,58 @@ +# 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/out/build/win-amd64-relwithdebinfo/ac6recomp.toml b/out/build/win-amd64-relwithdebinfo/ac6recomp.toml index 4e116dd4..47cf6377 100644 --- a/out/build/win-amd64-relwithdebinfo/ac6recomp.toml +++ b/out/build/win-amd64-relwithdebinfo/ac6recomp.toml @@ -6,9 +6,9 @@ log_file = "ac6recomp.log" video_mode_width = 1920 video_mode_height = 1080 resolution = "1080p" -resolution_scale = 2 direct_host_resolve = false guest_vblank_sync_to_refresh = true window_width = 1920 window_height = 1080 +ac6_effect_log_point_sampler_override = true vfetch_index_rounding_bias = true diff --git a/src/ac6_native_graphics.cpp b/src/ac6_native_graphics.cpp index 5c44553b..b012274e 100644 --- a/src/ac6_native_graphics.cpp +++ b/src/ac6_native_graphics.cpp @@ -23,9 +23,15 @@ REXCVAR_DEFINE_BOOL(ac6_native_graphics_enabled, true, "AC6/NativeGraphics", "Enable AC6 graphics capture analysis, overlay reporting, and backend fixes"); REXCVAR_DEFINE_BOOL(ac6_native_graphics_require_capture, true, "AC6/NativeGraphics", "Keep render capture enabled while AC6 graphics analysis is active"); +REXCVAR_DEFINE_BOOL(ac6_force_safe_render_capture, true, "AC6/NativeGraphics", + "Force AC6 hybrid backend fixes mode to keep per-draw render capture disabled until the capture path is stabilized"); REXCVAR_DEFINE_STRING(ac6_graphics_mode, "hybrid_backend_fixes", "AC6/NativeGraphics", "AC6 graphics runtime mode: disabled, analysis_only, hybrid_backend_fixes, legacy_replay_experimental") .allowed({"disabled", "analysis_only", "hybrid_backend_fixes", "legacy_replay_experimental"}); +REXCVAR_DEFINE_BOOL(ac6_force_safe_draw_resolution_scale, true, "AC6/NativeGraphics", + "Force AC6 hybrid backend fixes mode to use 1x draw resolution scaling until the scaled path is fixed"); +REXCVAR_DEFINE_BOOL(ac6_force_safe_direct_host_resolve, true, "AC6/NativeGraphics", + "Force AC6 hybrid backend fixes mode to keep direct_host_resolve disabled until the AC6 crash is fixed"); REXCVAR_DEFINE_BOOL(ac6_experimental_replay_present, false, "AC6/NativeGraphics", "Allow the legacy AC6 replay renderer to override the RexGlue swap source"); REXCVAR_DEFINE_STRING(ac6_native_graphics_backend, "auto", "AC6/NativeGraphics", @@ -46,6 +52,8 @@ ac6::renderer::FramePlanner g_capture_frame_planner; NativeGraphicsRuntimeStatus g_runtime_status{}; ac6::renderer::D3D12Backend* g_d3d12_backend = nullptr; std::atomic g_captured_memory{nullptr}; +std::atomic_flag g_frame_boundary_active = ATOMIC_FLAG_INIT; +std::atomic g_frame_boundary_reentry_count{0}; GraphicsRuntimeMode ParseGraphicsMode(std::string_view value) { if (value == "disabled") { @@ -370,6 +378,22 @@ std::string_view ToString(const GraphicsRuntimeMode mode) { } void OnFrameBoundary(rex::memory::Memory* memory) { + if (g_frame_boundary_active.test_and_set(std::memory_order_acquire)) { + const uint32_t reentry_count = + g_frame_boundary_reentry_count.fetch_add(1, std::memory_order_relaxed) + 1; + if (reentry_count == 1 || (reentry_count % 64) == 0) { + REXLOG_WARN("AC6 graphics: dropping re-entrant frame boundary callback (count={})", + reentry_count); + } + return; + } + + struct FrameBoundaryScope { + ~FrameBoundaryScope() { + g_frame_boundary_active.clear(std::memory_order_release); + } + } frame_boundary_scope; + SyncRuntimeFlags(); g_captured_memory.store(memory, std::memory_order_release); @@ -390,8 +414,9 @@ void OnFrameBoundary(rex::memory::Memory* memory) { ac6::d3d::OnFrameBoundary(); - const ac6::d3d::FrameCaptureSnapshot frame_capture = ac6::d3d::GetFrameCapture(); - const ac6::d3d::FrameCaptureSummary capture_summary = ac6::d3d::GetFrameCaptureSummary(); + ac6::d3d::FrameCaptureSummary capture_summary; + const ac6::d3d::FrameCaptureSnapshot frame_capture = + ac6::d3d::TakeFrameCapture(&capture_summary); const ac6::d3d::ShadowState shadow_state = ac6::d3d::GetShadowState(); ++g_runtime_status.analysis_frames_observed; diff --git a/src/ac6_texture_overrides.cpp b/src/ac6_texture_overrides.cpp new file mode 100644 index 00000000..0dd53cea --- /dev/null +++ b/src/ac6_texture_overrides.cpp @@ -0,0 +1,699 @@ +#include "ac6_texture_overrides.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +REXCVAR_DECLARE(std::string, user_data_root); + +REXCVAR_DEFINE_BOOL(ac6_texture_swaps_enabled, true, "AC6/TextureSwaps", + "Enable AC6 texture dump and replacement support"); +REXCVAR_DEFINE_BOOL(ac6_texture_swaps_dump_enabled, true, "AC6/TextureSwaps", + "Dump host-ready textures to the user-data texture dump folder"); +REXCVAR_DEFINE_BOOL(ac6_texture_swaps_replace_enabled, true, "AC6/TextureSwaps", + "Load matching replacement DDS files from the user-data texture override folders"); +REXCVAR_DEFINE_STRING(ac6_texture_swaps_dump_dir, "texture_dumps", "AC6/TextureSwaps", + "User-data subdirectory that stores dumped texture DDS files and metadata"); +REXCVAR_DEFINE_STRING(ac6_texture_swaps_override_dir, "override/textures", "AC6/TextureSwaps", + "User-data subdirectory that stores loose replacement texture DDS files"); +REXCVAR_DEFINE_STRING(ac6_texture_swaps_mods_dir, "mods", "AC6/TextureSwaps", + "User-data subdirectory containing mod folders with texture overrides"); + +namespace ac6::textures { +namespace { + +constexpr uint32_t kDdsMagic = 0x20534444u; +constexpr uint32_t kDdsFourCcDx10 = 0x30315844u; +constexpr uint32_t kDdsHeaderFlagsTexture = 0x00001007u; +constexpr uint32_t kDdsHeaderFlagsPitch = 0x00000008u; +constexpr uint32_t kDdsHeaderFlagsLinearSize = 0x00080000u; +constexpr uint32_t kDdsHeaderFlagsMipmap = 0x00020000u; +constexpr uint32_t kDdsHeaderFlagsDepth = 0x00800000u; +constexpr uint32_t kDdsCapsTexture = 0x00001000u; +constexpr uint32_t kDdsCapsComplex = 0x00000008u; +constexpr uint32_t kDdsCapsMipmap = 0x00400000u; +constexpr uint32_t kDdsCaps2Volume = 0x00200000u; +constexpr uint32_t kDdsPixelFormatFlagsFourCc = 0x00000004u; +constexpr uint32_t kDdsResourceDimensionTexture1D = 2u; +constexpr uint32_t kDdsResourceDimensionTexture2D = 3u; +constexpr uint32_t kDdsResourceDimensionTexture3D = 4u; +constexpr uint32_t kDdsResourceMiscTextureCube = 0x4u; + +#pragma pack(push, 1) +struct DdsPixelFormat { + uint32_t size; + uint32_t flags; + uint32_t four_cc; + uint32_t rgb_bit_count; + uint32_t r_bit_mask; + uint32_t g_bit_mask; + uint32_t b_bit_mask; + uint32_t a_bit_mask; +}; + +struct DdsHeader { + uint32_t size; + uint32_t flags; + uint32_t height; + uint32_t width; + uint32_t pitch_or_linear_size; + uint32_t depth; + uint32_t mip_map_count; + uint32_t reserved1[11]; + DdsPixelFormat pixel_format; + uint32_t caps; + uint32_t caps2; + uint32_t caps3; + uint32_t caps4; + uint32_t reserved2; +}; + +struct DdsHeaderDx10 { + uint32_t dxgi_format; + uint32_t resource_dimension; + uint32_t misc_flag; + uint32_t array_size; + uint32_t misc_flags2; +}; +#pragma pack(pop) + +struct DxgiLayoutInfo { + uint32_t block_width; + uint32_t block_height; + uint32_t bytes_per_block; + const char* name; +}; + +std::filesystem::path GetUserDataRoot() { + const std::string user_root = REXCVAR_GET(user_data_root); + if (!user_root.empty()) { + return std::filesystem::path(user_root); + } + return rex::filesystem::GetUserFolder() / "ac6recomp"; +} + +bool EnsureParentExists(const std::filesystem::path& path, std::string* error_out) { + std::error_code ec; + std::filesystem::create_directories(path.parent_path(), ec); + if (ec) { + if (error_out) { + *error_out = "failed to create parent directory: " + ec.message(); + } + return false; + } + return true; +} + +std::string EscapeJson(std::string_view value) { + std::string escaped; + escaped.reserve(value.size()); + for (char c : value) { + switch (c) { + case '\\': + escaped += "\\\\"; + break; + case '"': + escaped += "\\\""; + break; + case '\n': + escaped += "\\n"; + break; + case '\r': + escaped += "\\r"; + break; + case '\t': + escaped += "\\t"; + break; + default: + escaped.push_back(c); + break; + } + } + return escaped; +} + +std::string HexU32(uint32_t value) { + std::ostringstream stream; + stream << std::uppercase << std::hex << std::setw(8) << std::setfill('0') << value; + return stream.str(); +} + +std::string HexU64(uint64_t value) { + std::ostringstream stream; + stream << std::uppercase << std::hex << std::setw(16) << std::setfill('0') << value; + return stream.str(); +} + +const char* DimensionTag(uint32_t dimension) { + if (dimension == 0 || dimension == uint32_t(D3D12_RESOURCE_DIMENSION_TEXTURE1D)) { + return "1d"; + } + if (dimension == 1 || dimension == uint32_t(D3D12_RESOURCE_DIMENSION_TEXTURE2D)) { + return "2d"; + } + if (dimension == 2 || dimension == uint32_t(D3D12_RESOURCE_DIMENSION_TEXTURE3D)) { + return "3d"; + } + if (dimension == 3) { + return "cube"; + } + return "unknown"; +} + +bool GetDxgiLayoutInfo(DXGI_FORMAT format, DxgiLayoutInfo& out) { + switch (format) { + case DXGI_FORMAT_R8_TYPELESS: + case DXGI_FORMAT_R8_UNORM: + case DXGI_FORMAT_R8_SNORM: + case DXGI_FORMAT_R8_UINT: + case DXGI_FORMAT_R8_SINT: + out = {1, 1, 1, "DXGI_FORMAT_R8"}; + return true; + case DXGI_FORMAT_R8G8_TYPELESS: + case DXGI_FORMAT_R8G8_UNORM: + case DXGI_FORMAT_R8G8_SNORM: + case DXGI_FORMAT_R8G8_UINT: + case DXGI_FORMAT_R8G8_SINT: + out = {1, 1, 2, "DXGI_FORMAT_R8G8"}; + return true; + case DXGI_FORMAT_R8G8B8A8_TYPELESS: + case DXGI_FORMAT_R8G8B8A8_UNORM: + case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: + case DXGI_FORMAT_R8G8B8A8_SNORM: + case DXGI_FORMAT_R8G8B8A8_UINT: + case DXGI_FORMAT_R8G8B8A8_SINT: + case DXGI_FORMAT_B8G8R8A8_UNORM: + case DXGI_FORMAT_B8G8R8X8_UNORM: + out = {1, 1, 4, "DXGI_FORMAT_R8G8B8A8"}; + return true; + case DXGI_FORMAT_R10G10B10A2_TYPELESS: + case DXGI_FORMAT_R10G10B10A2_UNORM: + case DXGI_FORMAT_R10G10B10A2_UINT: + out = {1, 1, 4, "DXGI_FORMAT_R10G10B10A2"}; + return true; + case DXGI_FORMAT_B5G6R5_UNORM: + out = {1, 1, 2, "DXGI_FORMAT_B5G6R5_UNORM"}; + return true; + case DXGI_FORMAT_B5G5R5A1_UNORM: + out = {1, 1, 2, "DXGI_FORMAT_B5G5R5A1_UNORM"}; + return true; + case DXGI_FORMAT_B4G4R4A4_UNORM: + out = {1, 1, 2, "DXGI_FORMAT_B4G4R4A4_UNORM"}; + return true; + case DXGI_FORMAT_R16_TYPELESS: + case DXGI_FORMAT_R16_UNORM: + case DXGI_FORMAT_R16_SNORM: + case DXGI_FORMAT_R16_UINT: + case DXGI_FORMAT_R16_SINT: + case DXGI_FORMAT_R16_FLOAT: + out = {1, 1, 2, "DXGI_FORMAT_R16"}; + return true; + case DXGI_FORMAT_R16G16_TYPELESS: + case DXGI_FORMAT_R16G16_UNORM: + case DXGI_FORMAT_R16G16_SNORM: + case DXGI_FORMAT_R16G16_UINT: + case DXGI_FORMAT_R16G16_SINT: + case DXGI_FORMAT_R16G16_FLOAT: + out = {1, 1, 4, "DXGI_FORMAT_R16G16"}; + return true; + case DXGI_FORMAT_R16G16B16A16_TYPELESS: + case DXGI_FORMAT_R16G16B16A16_UNORM: + case DXGI_FORMAT_R16G16B16A16_SNORM: + case DXGI_FORMAT_R16G16B16A16_UINT: + case DXGI_FORMAT_R16G16B16A16_SINT: + case DXGI_FORMAT_R16G16B16A16_FLOAT: + out = {1, 1, 8, "DXGI_FORMAT_R16G16B16A16"}; + return true; + case DXGI_FORMAT_R32_TYPELESS: + case DXGI_FORMAT_R32_FLOAT: + case DXGI_FORMAT_R32_UINT: + case DXGI_FORMAT_R32_SINT: + out = {1, 1, 4, "DXGI_FORMAT_R32"}; + return true; + case DXGI_FORMAT_R32G32_TYPELESS: + case DXGI_FORMAT_R32G32_FLOAT: + case DXGI_FORMAT_R32G32_UINT: + case DXGI_FORMAT_R32G32_SINT: + out = {1, 1, 8, "DXGI_FORMAT_R32G32"}; + return true; + case DXGI_FORMAT_R32G32B32A32_FLOAT: + case DXGI_FORMAT_R32G32B32A32_UINT: + case DXGI_FORMAT_R32G32B32A32_SINT: + out = {1, 1, 16, "DXGI_FORMAT_R32G32B32A32"}; + return true; + case DXGI_FORMAT_BC1_TYPELESS: + case DXGI_FORMAT_BC1_UNORM: + case DXGI_FORMAT_BC1_UNORM_SRGB: + out = {4, 4, 8, "DXGI_FORMAT_BC1"}; + return true; + case DXGI_FORMAT_BC2_TYPELESS: + case DXGI_FORMAT_BC2_UNORM: + case DXGI_FORMAT_BC2_UNORM_SRGB: + out = {4, 4, 16, "DXGI_FORMAT_BC2"}; + return true; + case DXGI_FORMAT_BC3_TYPELESS: + case DXGI_FORMAT_BC3_UNORM: + case DXGI_FORMAT_BC3_UNORM_SRGB: + out = {4, 4, 16, "DXGI_FORMAT_BC3"}; + return true; + case DXGI_FORMAT_BC4_TYPELESS: + case DXGI_FORMAT_BC4_UNORM: + case DXGI_FORMAT_BC4_SNORM: + out = {4, 4, 8, "DXGI_FORMAT_BC4"}; + return true; + case DXGI_FORMAT_BC5_TYPELESS: + case DXGI_FORMAT_BC5_UNORM: + case DXGI_FORMAT_BC5_SNORM: + out = {4, 4, 16, "DXGI_FORMAT_BC5"}; + return true; + default: + return false; + } +} + +uint32_t ComputeSubresourceCount(D3D12_RESOURCE_DIMENSION dimension, uint32_t depth_or_array_size, + uint32_t mip_count) { + return dimension == D3D12_RESOURCE_DIMENSION_TEXTURE3D ? mip_count + : depth_or_array_size * mip_count; +} + +bool MapDdsDimension(uint32_t dds_dimension, D3D12_RESOURCE_DIMENSION& out) { + switch (dds_dimension) { + case kDdsResourceDimensionTexture1D: + out = D3D12_RESOURCE_DIMENSION_TEXTURE1D; + return true; + case kDdsResourceDimensionTexture2D: + out = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + return true; + case kDdsResourceDimensionTexture3D: + out = D3D12_RESOURCE_DIMENSION_TEXTURE3D; + return true; + default: + return false; + } +} + +uint32_t ToDdsDimension(D3D12_RESOURCE_DIMENSION dimension) { + switch (dimension) { + case D3D12_RESOURCE_DIMENSION_TEXTURE1D: + return kDdsResourceDimensionTexture1D; + case D3D12_RESOURCE_DIMENSION_TEXTURE3D: + return kDdsResourceDimensionTexture3D; + case D3D12_RESOURCE_DIMENSION_TEXTURE2D: + default: + return kDdsResourceDimensionTexture2D; + } +} + +} // namespace + +bool TextureSwapsEnabled() { + return REXCVAR_GET(ac6_texture_swaps_enabled); +} + +bool TextureDumpEnabled() { + return TextureSwapsEnabled() && REXCVAR_GET(ac6_texture_swaps_dump_enabled); +} + +bool TextureReplacementEnabled() { + return TextureSwapsEnabled() && REXCVAR_GET(ac6_texture_swaps_replace_enabled); +} + +bool IsSupportedTextureSwapFormat(DXGI_FORMAT format) { + DxgiLayoutInfo layout = {}; + return GetDxgiLayoutInfo(format, layout); +} + +bool GetTightTextureSubresourceLayout(DXGI_FORMAT format, uint32_t width, uint32_t height, + TextureSubresourceLayout& out) { + DxgiLayoutInfo info = {}; + if (!GetDxgiLayoutInfo(format, info)) { + return false; + } + const uint32_t width_blocks = std::max((width + info.block_width - 1) / info.block_width, 1u); + const uint32_t height_blocks = std::max((height + info.block_height - 1) / info.block_height, 1u); + out.row_pitch = width_blocks * info.bytes_per_block; + out.row_count = height_blocks; + out.slice_pitch = out.row_pitch * out.row_count; + return true; +} + +std::string DescribeDxgiFormat(DXGI_FORMAT format) { + DxgiLayoutInfo layout = {}; + if (GetDxgiLayoutInfo(format, layout)) { + return layout.name; + } + std::ostringstream stream; + stream << "DXGI_FORMAT_" << uint32_t(format); + return stream.str(); +} + +std::string BuildTextureStableKey(uint64_t texture_key_hash, uint32_t base_page, uint32_t mip_page, + uint32_t dimension, uint32_t width, uint32_t height, + uint32_t depth_or_array_size, uint32_t mip_count, + uint32_t guest_format, uint32_t endianness, bool tiled, + bool packed_mips, bool signed_separate, bool scaled_resolve) { + std::ostringstream stream; + stream << "tex_" << HexU64(texture_key_hash) << "_bp" << HexU32(base_page) << "_mp" + << HexU32(mip_page) << "_" << DimensionTag(dimension) << "_" << width << "x" << height + << "x" << depth_or_array_size << "_m" << mip_count << "_fmt" << guest_format << "_e" + << endianness << "_t" << (tiled ? 1 : 0) << "_p" << (packed_mips ? 1 : 0) << "_s" + << (signed_separate ? 1 : 0) << "_r" << (scaled_resolve ? 1 : 0); + return stream.str(); +} + +std::filesystem::path GetTextureDumpDdsPath(std::string_view stable_key) { + return GetUserDataRoot() / REXCVAR_GET(ac6_texture_swaps_dump_dir) / + (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"); +} + +bool DumpExists(std::string_view stable_key) { + std::error_code ec; + return std::filesystem::exists(GetTextureDumpDdsPath(stable_key), ec); +} + +std::optional 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)) { + return loose_path; + } + + const std::filesystem::path mods_root = user_root / REXCVAR_GET(ac6_texture_swaps_mods_dir); + if (!std::filesystem::exists(mods_root, ec) || ec) { + return std::nullopt; + } + + std::vector mod_roots; + for (const std::filesystem::directory_entry& entry : std::filesystem::directory_iterator(mods_root, ec)) { + if (ec) { + break; + } + if (entry.is_directory()) { + mod_roots.push_back(entry.path()); + } + } + std::sort(mod_roots.begin(), mod_roots.end()); + + std::optional 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)) { + resolved = candidate; + } + } + return resolved; +} + +bool LoadDdsFromFile(const std::filesystem::path& path, DdsImageData& out, std::string* error_out) { + std::ifstream file(path, std::ios::binary); + if (!file) { + if (error_out) { + *error_out = "failed to open file"; + } + return false; + } + + 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 (error_out) { + *error_out = "file is too small to be a DX10 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) { + 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"; + } + return false; + } + + D3D12_RESOURCE_DIMENSION dimension = D3D12_RESOURCE_DIMENSION_UNKNOWN; + if (!MapDdsDimension(header_dx10.resource_dimension, dimension)) { + if (error_out) { + *error_out = "unsupported DDS resource dimension"; + } + 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 (error_out) { + *error_out = "unsupported DXGI format in DDS file"; + } + return false; + } + + DdsImageData image; + image.format = DXGI_FORMAT(header_dx10.dxgi_format); + image.dimension = dimension; + image.width = width; + image.height = height; + image.depth_or_array_size = depth_or_array_size; + image.mip_count = mip_count; + 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))); + file.read(reinterpret_cast(payload.data()), std::streamsize(payload.size())); + if (!file && !payload.empty()) { + if (error_out) { + *error_out = "failed to read DDS payload"; + } + return false; + } + + size_t payload_offset = 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 = + dimension == D3D12_RESOURCE_DIMENSION_TEXTURE3D ? subresource_index + : (subresource_index % mip_count); + DdsSubresource subresource; + subresource.width = std::max(width >> mip_index, 1u); + subresource.height = std::max(height >> mip_index, 1u); + subresource.depth = dimension == D3D12_RESOURCE_DIMENSION_TEXTURE3D + ? std::max(depth_or_array_size >> mip_index, 1u) + : 1u; + TextureSubresourceLayout tight_layout = {}; + if (!GetTightTextureSubresourceLayout(image.format, subresource.width, subresource.height, + tight_layout)) { + if (error_out) { + *error_out = "unsupported tight layout for DDS subresource"; + } + return false; + } + const size_t subresource_size = size_t(tight_layout.slice_pitch) * subresource.depth; + if (payload_offset + subresource_size > payload.size()) { + if (error_out) { + *error_out = "DDS payload is truncated"; + } + return false; + } + 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; + image.subresources.push_back(std::move(subresource)); + } + + out = std::move(image); + return true; +} + +bool WriteDdsToFile(const std::filesystem::path& path, const DdsImageData& data, + std::string* error_out) { + if (data.is_cube) { + if (error_out) { + *error_out = "cube textures are not supported for DDS dumping"; + } + return false; + } + + const uint32_t expected_subresource_count = + ComputeSubresourceCount(data.dimension, data.depth_or_array_size, data.mip_count); + if (data.subresources.size() != expected_subresource_count) { + if (error_out) { + *error_out = "DDS subresource count does not match the texture description"; + } + return false; + } + + if (!EnsureParentExists(path, error_out)) { + return false; + } + + TextureSubresourceLayout base_layout = {}; + if (!GetTightTextureSubresourceLayout(data.format, data.width, data.height, base_layout)) { + if (error_out) { + *error_out = "unsupported DXGI format for DDS output"; + } + return false; + } + + DdsHeader header = {}; + header.size = sizeof(DdsHeader); + header.flags = kDdsHeaderFlagsTexture; + header.height = std::max(data.height, 1u); + header.width = std::max(data.width, 1u); + header.pitch_or_linear_size = base_layout.row_pitch; + header.depth = data.dimension == D3D12_RESOURCE_DIMENSION_TEXTURE3D ? data.depth_or_array_size : 0; + header.mip_map_count = std::max(data.mip_count, 1u); + header.pixel_format = {sizeof(DdsPixelFormat), kDdsPixelFormatFlagsFourCc, kDdsFourCcDx10, 0, 0, 0, 0, 0}; + header.caps = kDdsCapsTexture; + header.caps2 = 0; + if (data.mip_count > 1) { + header.flags |= kDdsHeaderFlagsMipmap; + header.caps |= kDdsCapsComplex | kDdsCapsMipmap; + } + if (data.dimension == D3D12_RESOURCE_DIMENSION_TEXTURE3D) { + header.flags |= kDdsHeaderFlagsDepth; + header.caps |= kDdsCapsComplex; + header.caps2 |= kDdsCaps2Volume; + } else if (data.depth_or_array_size > 1) { + header.caps |= kDdsCapsComplex; + } + + DxgiLayoutInfo format_info = {}; + GetDxgiLayoutInfo(data.format, format_info); + if (format_info.block_width != 1 || format_info.block_height != 1) { + header.flags |= kDdsHeaderFlagsLinearSize; + header.pitch_or_linear_size = base_layout.slice_pitch; + } else { + header.flags |= kDdsHeaderFlagsPitch; + } + + DdsHeaderDx10 header_dx10 = {}; + header_dx10.dxgi_format = uint32_t(data.format); + header_dx10.resource_dimension = ToDdsDimension(data.dimension); + header_dx10.array_size = + data.dimension == D3D12_RESOURCE_DIMENSION_TEXTURE3D ? 1u : std::max(data.depth_or_array_size, 1u); + + std::ofstream file(path, std::ios::binary | std::ios::trunc); + if (!file) { + if (error_out) { + *error_out = "failed to create DDS file"; + } + return false; + } + + file.write(reinterpret_cast(&kDdsMagic), sizeof(kDdsMagic)); + file.write(reinterpret_cast(&header), sizeof(header)); + file.write(reinterpret_cast(&header_dx10), sizeof(header_dx10)); + for (const DdsSubresource& subresource : data.subresources) { + const size_t expected_size = size_t(subresource.slice_pitch) * subresource.depth; + if (subresource.data.size() != expected_size) { + if (error_out) { + *error_out = "DDS subresource payload has an unexpected size"; + } + return false; + } + file.write(reinterpret_cast(subresource.data.data()), + std::streamsize(subresource.data.size())); + } + if (!file) { + if (error_out) { + *error_out = "failed while writing DDS file"; + } + return false; + } + + return true; +} + +bool WriteDumpMetadata(const std::filesystem::path& path, const TextureDumpMetadata& metadata, + std::string* error_out) { + if (!EnsureParentExists(path, error_out)) { + return false; + } + + std::ofstream file(path, std::ios::binary | std::ios::trunc); + if (!file) { + if (error_out) { + *error_out = "failed to create metadata file"; + } + return false; + } + + file << "{\n"; + file << " \"stable_key\": \"" << EscapeJson(metadata.stable_key) << "\",\n"; + file << " \"texture_key_hash\": \"0x" << HexU64(metadata.texture_key_hash) << "\",\n"; + file << " \"base_page\": \"0x" << HexU32(metadata.base_page) << "\",\n"; + file << " \"mip_page\": \"0x" << HexU32(metadata.mip_page) << "\",\n"; + file << " \"dimension\": " << metadata.dimension << ",\n"; + file << " \"width\": " << metadata.width << ",\n"; + file << " \"height\": " << metadata.height << ",\n"; + file << " \"depth_or_array_size\": " << metadata.depth_or_array_size << ",\n"; + file << " \"mip_count\": " << metadata.mip_count << ",\n"; + file << " \"guest_format\": " << metadata.guest_format << ",\n"; + file << " \"endianness\": " << metadata.endianness << ",\n"; + file << " \"dxgi_format\": " << metadata.dxgi_format << ",\n"; + file << " \"dxgi_format_name\": \"" << EscapeJson(DescribeDxgiFormat(DXGI_FORMAT(metadata.dxgi_format))) + << "\",\n"; + file << " \"tiled\": " << (metadata.tiled ? "true" : "false") << ",\n"; + file << " \"packed_mips\": " << (metadata.packed_mips ? "true" : "false") << ",\n"; + file << " \"signed_separate\": " << (metadata.signed_separate ? "true" : "false") << ",\n"; + file << " \"scaled_resolve\": " << (metadata.scaled_resolve ? "true" : "false") << ",\n"; + file << " \"frame_index\": " << metadata.frame_index << ",\n"; + file << " \"signature_stable_id\": \"0x" << HexU64(metadata.signature_stable_id) << "\",\n"; + file << " \"active_vertex_shader_hash\": \"0x" << HexU64(metadata.active_vertex_shader_hash) + << "\",\n"; + file << " \"active_pixel_shader_hash\": \"0x" << HexU64(metadata.active_pixel_shader_hash) + << "\",\n"; + file << " \"signature_tags\": \"" << EscapeJson(metadata.signature_tags) << "\"\n"; + file << "}\n"; + + if (!file) { + if (error_out) { + *error_out = "failed while writing metadata file"; + } + return false; + } + return true; +} + +} // namespace ac6::textures diff --git a/src/ac6_texture_overrides.h b/src/ac6_texture_overrides.h new file mode 100644 index 00000000..9c7ecb91 --- /dev/null +++ b/src/ac6_texture_overrides.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace ac6::textures { + +struct TextureSubresourceLayout { + uint32_t row_pitch = 0; + uint32_t slice_pitch = 0; + uint32_t row_count = 0; +}; + +struct DdsSubresource { + uint32_t width = 0; + uint32_t height = 0; + uint32_t depth = 1; + uint32_t row_pitch = 0; + uint32_t slice_pitch = 0; + std::vector data; +}; + +struct DdsImageData { + DXGI_FORMAT format = DXGI_FORMAT_UNKNOWN; + D3D12_RESOURCE_DIMENSION dimension = D3D12_RESOURCE_DIMENSION_UNKNOWN; + uint32_t width = 0; + uint32_t height = 0; + uint32_t depth_or_array_size = 1; + uint32_t mip_count = 1; + bool is_cube = false; + std::vector subresources; +}; + +struct TextureDumpMetadata { + std::string stable_key; + uint64_t texture_key_hash = 0; + uint32_t base_page = 0; + uint32_t mip_page = 0; + uint32_t dimension = 0; + uint32_t width = 0; + uint32_t height = 0; + uint32_t depth_or_array_size = 1; + uint32_t mip_count = 1; + uint32_t guest_format = 0; + uint32_t endianness = 0; + uint32_t dxgi_format = 0; + bool tiled = false; + bool packed_mips = false; + bool signed_separate = false; + bool scaled_resolve = false; + uint64_t frame_index = 0; + uint64_t signature_stable_id = 0; + uint64_t active_vertex_shader_hash = 0; + uint64_t active_pixel_shader_hash = 0; + std::string signature_tags; +}; + +bool TextureSwapsEnabled(); +bool TextureDumpEnabled(); +bool TextureReplacementEnabled(); + +bool IsSupportedTextureSwapFormat(DXGI_FORMAT format); +bool GetTightTextureSubresourceLayout(DXGI_FORMAT format, uint32_t width, uint32_t height, + TextureSubresourceLayout& out); +std::string DescribeDxgiFormat(DXGI_FORMAT format); + +std::string BuildTextureStableKey(uint64_t texture_key_hash, uint32_t base_page, uint32_t mip_page, + uint32_t dimension, uint32_t width, uint32_t height, + uint32_t depth_or_array_size, uint32_t mip_count, + uint32_t guest_format, uint32_t endianness, bool tiled, + bool packed_mips, bool signed_separate, bool scaled_resolve); + +std::filesystem::path GetTextureDumpDdsPath(std::string_view stable_key); +std::filesystem::path GetTextureDumpMetadataPath(std::string_view stable_key); +bool DumpExists(std::string_view stable_key); + +std::optional ResolveReplacementDdsPath(std::string_view stable_key); + +bool LoadDdsFromFile(const std::filesystem::path& path, DdsImageData& out, + std::string* error_out = nullptr); +bool WriteDdsToFile(const std::filesystem::path& path, const DdsImageData& data, + std::string* error_out = nullptr); +bool WriteDumpMetadata(const std::filesystem::path& path, const TextureDumpMetadata& metadata, + std::string* error_out = nullptr); + +} // namespace ac6::textures diff --git a/src/d3d_hooks.cpp b/src/d3d_hooks.cpp index 22198684..c0b6e060 100644 --- a/src/d3d_hooks.cpp +++ b/src/d3d_hooks.cpp @@ -815,14 +815,14 @@ DrawStatsSnapshot GetDrawStats() { return g_snapshot; } -FrameCaptureSnapshot GetFrameCapture() { - std::shared_lock lock(g_capture_mutex); - return g_capture_snapshot; -} - -FrameCaptureSummary GetFrameCaptureSummary() { - std::shared_lock lock(g_capture_mutex); - return g_capture_summary; +FrameCaptureSnapshot TakeFrameCapture(FrameCaptureSummary* summary_out) { + std::unique_lock lock(g_capture_mutex); + if (summary_out) { + *summary_out = g_capture_summary; + } + FrameCaptureSnapshot snapshot = std::move(g_capture_snapshot); + g_capture_snapshot = {}; + return snapshot; } ShadowState GetShadowState() { diff --git a/src/d3d_hooks.h b/src/d3d_hooks.h index cd4a540a..0d739af1 100644 --- a/src/d3d_hooks.h +++ b/src/d3d_hooks.h @@ -11,8 +11,7 @@ namespace ac6::d3d { void OnFrameBoundary(); DrawStatsSnapshot GetDrawStats(); -FrameCaptureSnapshot GetFrameCapture(); -FrameCaptureSummary GetFrameCaptureSummary(); +FrameCaptureSnapshot TakeFrameCapture(FrameCaptureSummary* summary_out = nullptr); ShadowState GetShadowState(); } // namespace ac6::d3d diff --git a/src/main.cpp b/src/main.cpp index e73b7fe5..54d71c61 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,8 +10,16 @@ REXCVAR_DECLARE(bool, ac6_render_capture); REXCVAR_DECLARE(bool, ac6_timing_hooks_enabled); REXCVAR_DECLARE(bool, ac6_unlock_fps); REXCVAR_DECLARE(bool, ac6_native_graphics_enabled); +REXCVAR_DECLARE(bool, ac6_native_graphics_require_capture); REXCVAR_DECLARE(bool, ac6_experimental_replay_present); +REXCVAR_DECLARE(bool, ac6_force_safe_render_capture); +REXCVAR_DECLARE(bool, ac6_force_safe_draw_resolution_scale); +REXCVAR_DECLARE(bool, ac6_force_safe_direct_host_resolve); REXCVAR_DECLARE(std::string, ac6_graphics_mode); +REXCVAR_DECLARE(bool, direct_host_resolve); +REXCVAR_DECLARE(int32_t, resolution_scale); +REXCVAR_DECLARE(int32_t, draw_resolution_scale_x); +REXCVAR_DECLARE(int32_t, draw_resolution_scale_y); REXCVAR_DECLARE(std::string, log_file); REXCVAR_DECLARE(std::string, log_level); @@ -26,6 +34,36 @@ REXCVAR_DECLARE(std::string, log_level); // Early boot log to catch crashes before the SDK logger is ready std::ofstream g_boot_log; +namespace { + +bool ShouldApplyAc6HybridStartupSafetyOverrides() { + return REXCVAR_GET(ac6_native_graphics_enabled) && + REXCVAR_GET(ac6_graphics_mode) == "hybrid_backend_fixes"; +} + +void ApplyAc6HybridStartupSafetyOverrides() { + if (!ShouldApplyAc6HybridStartupSafetyOverrides()) { + return; + } + + if (REXCVAR_GET(ac6_force_safe_draw_resolution_scale)) { + REXCVAR_SET(resolution_scale, 1); + REXCVAR_SET(draw_resolution_scale_x, 1); + REXCVAR_SET(draw_resolution_scale_y, 1); + } + + if (REXCVAR_GET(ac6_force_safe_direct_host_resolve)) { + REXCVAR_SET(direct_host_resolve, false); + } + + if (REXCVAR_GET(ac6_force_safe_render_capture)) { + REXCVAR_SET(ac6_native_graphics_require_capture, false); + REXCVAR_SET(ac6_render_capture, false); + } +} + +} // namespace + void InitEarlyLog() { g_boot_log.open("boot.log", std::ios::out | std::ios::trunc); if (g_boot_log.is_open()) { @@ -46,6 +84,7 @@ std::unique_ptr Ac6recompAppCreate(rex::ui::WindowedAppCon REXCVAR_SET(log_file, "ac6recomp.log"); REXCVAR_SET(log_level, "info"); REXCVAR_SET(ac6_unlock_fps, false); + ApplyAc6HybridStartupSafetyOverrides(); REXLOG_INFO("Ac6recompAppCreate: graphics mode={} replay_present={} capture={}", REXCVAR_GET(ac6_graphics_mode), diff --git a/thirdparty/rexglue-sdk/include/rex/graphics/d3d12/command_processor.h b/thirdparty/rexglue-sdk/include/rex/graphics/d3d12/command_processor.h index ec54e7b6..bc1f550c 100644 --- a/thirdparty/rexglue-sdk/include/rex/graphics/d3d12/command_processor.h +++ b/thirdparty/rexglue-sdk/include/rex/graphics/d3d12/command_processor.h @@ -226,9 +226,11 @@ class D3D12CommandProcessor : public CommandProcessor { IndexBufferInfo* index_buffer_info, bool major_mode_explicit) override; bool IssueCopy() override; - void InitializeTrace() override; + void InitializeTrace() override; private: + friend class D3D12TextureCache; + static constexpr uint32_t kQueueFrames = 3; enum RootParameter : UINT { diff --git a/thirdparty/rexglue-sdk/include/rex/graphics/d3d12/texture_cache.h b/thirdparty/rexglue-sdk/include/rex/graphics/d3d12/texture_cache.h index 6f25399d..be4c3460 100644 --- a/thirdparty/rexglue-sdk/include/rex/graphics/d3d12/texture_cache.h +++ b/thirdparty/rexglue-sdk/include/rex/graphics/d3d12/texture_cache.h @@ -14,7 +14,9 @@ #include #include #include +#include #include +#include #include #include @@ -404,6 +406,44 @@ class D3D12TextureCache final : public TextureCache { bool is_signed, uint32_t host_swizzle); void ReleaseTextureDescriptor(uint32_t descriptor_index); D3D12_CPU_DESCRIPTOR_HANDLE GetTextureDescriptorCPUHandle(uint32_t descriptor_index) const; + void ProcessCompletedTextureTransfers(); + bool ScheduleTextureDump(D3D12Texture& texture, DXGI_FORMAT dump_format); + bool ApplyTextureReplacement(D3D12Texture& texture, DXGI_FORMAT replacement_format); + + struct PendingTextureDump { + uint64_t submission_index = 0; + uint64_t total_size = 0; + uint64_t texture_key_hash = 0; + uint64_t frame_index = 0; + uint64_t signature_stable_id = 0; + uint64_t active_vertex_shader_hash = 0; + uint64_t active_pixel_shader_hash = 0; + uint32_t base_page = 0; + uint32_t mip_page = 0; + uint32_t guest_dimension = 0; + uint32_t width = 0; + uint32_t height = 0; + uint32_t depth_or_array_size = 1; + uint32_t mip_count = 1; + uint32_t guest_format = 0; + uint32_t endianness = 0; + DXGI_FORMAT dxgi_format = DXGI_FORMAT_UNKNOWN; + D3D12_RESOURCE_DIMENSION resource_dimension = D3D12_RESOURCE_DIMENSION_UNKNOWN; + bool tiled = false; + bool packed_mips = false; + bool signed_separate = false; + bool scaled_resolve = false; + std::string stable_key; + std::string signature_tags; + Microsoft::WRL::ComPtr readback_buffer; + std::vector footprints; + std::vector row_counts; + }; + + struct PendingUploadResource { + uint64_t submission_index = 0; + Microsoft::WRL::ComPtr resource; + }; size_t GetScaledResolveBufferCount() const { assert_true(IsDrawResolutionScaled()); @@ -490,6 +530,10 @@ class D3D12TextureCache final : public TextureCache { kUnsupportedSnormBit = kUnsupportedUnormBit << 1, }; uint8_t unsupported_format_features_used_[64]; + std::unordered_set dumped_texture_keys_; + std::unordered_set replacement_warning_keys_; + std::vector pending_texture_dumps_; + std::vector pending_upload_resources_; // The tiled buffer for resolved data with resolution scaling. // Because on Direct3D 12 (at least on Windows 10 2004) typed SRV or UAV diff --git a/thirdparty/rexglue-sdk/src/graphics/d3d12/texture_cache.cpp b/thirdparty/rexglue-sdk/src/graphics/d3d12/texture_cache.cpp index df52fe2f..2da7f50a 100644 --- a/thirdparty/rexglue-sdk/src/graphics/d3d12/texture_cache.cpp +++ b/thirdparty/rexglue-sdk/src/graphics/d3d12/texture_cache.cpp @@ -30,6 +30,10 @@ #include #include #include +#include + +#include "../../../../../src/ac6_backend_fixes/ac6_backend_hooks.h" +#include "../../../../../src/ac6_texture_overrides.h" namespace rex::graphics::d3d12 { @@ -807,6 +811,7 @@ void D3D12TextureCache::BeginSubmission(uint64_t new_submission_index) { void D3D12TextureCache::BeginFrame() { TextureCache::BeginFrame(); + ProcessCompletedTextureTransfers(); std::memset(unsupported_format_features_used_, 0, sizeof(unsupported_format_features_used_)); } @@ -2212,6 +2217,383 @@ bool D3D12TextureCache::LoadTextureDataFromResidentMemoryImpl(Texture& texture, command_processor_.ReleaseScratchGPUBuffer(copy_buffer_copy_source, copy_buffer_copy_source_state); + DXGI_FORMAT swap_format = host_format_is_signed + ? host_formats_[uint32_t(texture_key.format)].dxgi_format_signed + : GetDXGIUnormFormat(texture_key); + if (swap_format != DXGI_FORMAT_UNKNOWN && texture_key.dimension != xenos::DataDimension::kCube) { + ScheduleTextureDump(d3d12_texture, swap_format); + ApplyTextureReplacement(d3d12_texture, swap_format); + } + + return true; +} + +void D3D12TextureCache::ProcessCompletedTextureTransfers() { + const uint64_t completed_submission = command_processor_.GetCompletedSubmission(); + + for (auto it = pending_upload_resources_.begin(); it != pending_upload_resources_.end();) { + if (it->submission_index > completed_submission) { + ++it; + continue; + } + it = pending_upload_resources_.erase(it); + } + + for (auto it = pending_texture_dumps_.begin(); it != pending_texture_dumps_.end();) { + if (it->submission_index > completed_submission) { + ++it; + continue; + } + + D3D12_RANGE read_range; + read_range.Begin = 0; + read_range.End = SIZE_T(it->total_size); + void* mapped = nullptr; + if (FAILED(it->readback_buffer->Map(0, &read_range, &mapped))) { + REXGPU_WARN("Texture swap dump {}: failed to map readback buffer", it->stable_key); + it = pending_texture_dumps_.erase(it); + continue; + } + + ac6::textures::DdsImageData dds_image; + dds_image.format = it->dxgi_format; + dds_image.dimension = it->resource_dimension; + dds_image.width = it->width; + dds_image.height = it->height; + dds_image.depth_or_array_size = it->depth_or_array_size; + dds_image.mip_count = it->mip_count; + dds_image.is_cube = false; + dds_image.subresources.reserve(it->footprints.size()); + + bool build_failed = false; + for (size_t subresource_index = 0; subresource_index < it->footprints.size(); ++subresource_index) { + const uint32_t mip_index = + it->resource_dimension == D3D12_RESOURCE_DIMENSION_TEXTURE3D + ? uint32_t(subresource_index) + : (uint32_t(subresource_index) % it->mip_count); + ac6::textures::DdsSubresource subresource; + subresource.width = std::max(it->width >> mip_index, 1u); + subresource.height = std::max(it->height >> mip_index, 1u); + subresource.depth = it->resource_dimension == D3D12_RESOURCE_DIMENSION_TEXTURE3D + ? std::max(it->depth_or_array_size >> mip_index, 1u) + : 1u; + + ac6::textures::TextureSubresourceLayout tight_layout = {}; + if (!ac6::textures::GetTightTextureSubresourceLayout( + it->dxgi_format, subresource.width, subresource.height, tight_layout)) { + REXGPU_WARN("Texture swap dump {}: unsupported dump format {}", + it->stable_key, uint32_t(it->dxgi_format)); + build_failed = true; + break; + } + + subresource.row_pitch = tight_layout.row_pitch; + subresource.slice_pitch = tight_layout.slice_pitch; + subresource.data.resize(size_t(subresource.slice_pitch) * subresource.depth); + + const uint8_t* source_base = + reinterpret_cast(mapped) + it->footprints[subresource_index].Offset; + const uint32_t source_row_pitch = it->footprints[subresource_index].Footprint.RowPitch; + const uint32_t source_row_count = it->row_counts[subresource_index]; + for (uint32_t z = 0; z < subresource.depth; ++z) { + const uint8_t* source_slice = source_base + size_t(z) * source_row_pitch * source_row_count; + uint8_t* dest_slice = subresource.data.data() + size_t(z) * subresource.slice_pitch; + for (uint32_t row = 0; row < tight_layout.row_count; ++row) { + std::memcpy(dest_slice + size_t(row) * subresource.row_pitch, + source_slice + size_t(row) * source_row_pitch, subresource.row_pitch); + } + } + + dds_image.subresources.push_back(std::move(subresource)); + } + + it->readback_buffer->Unmap(0, nullptr); + if (build_failed) { + it = pending_texture_dumps_.erase(it); + continue; + } + + ac6::textures::TextureDumpMetadata metadata; + metadata.stable_key = it->stable_key; + metadata.texture_key_hash = it->texture_key_hash; + metadata.base_page = it->base_page; + metadata.mip_page = it->mip_page; + metadata.dimension = it->guest_dimension; + metadata.width = it->width; + metadata.height = it->height; + metadata.depth_or_array_size = it->depth_or_array_size; + metadata.mip_count = it->mip_count; + metadata.guest_format = it->guest_format; + metadata.endianness = it->endianness; + metadata.dxgi_format = uint32_t(it->dxgi_format); + metadata.tiled = it->tiled; + metadata.packed_mips = it->packed_mips; + metadata.signed_separate = it->signed_separate; + metadata.scaled_resolve = it->scaled_resolve; + metadata.frame_index = it->frame_index; + metadata.signature_stable_id = it->signature_stable_id; + metadata.active_vertex_shader_hash = it->active_vertex_shader_hash; + metadata.active_pixel_shader_hash = it->active_pixel_shader_hash; + metadata.signature_tags = it->signature_tags; + + std::string error; + if (!ac6::textures::WriteDdsToFile(ac6::textures::GetTextureDumpDdsPath(it->stable_key), + dds_image, &error)) { + REXGPU_WARN("Texture swap dump {}: failed to write DDS ({})", it->stable_key, error); + } 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); + } + + it = pending_texture_dumps_.erase(it); + } +} + +bool D3D12TextureCache::ScheduleTextureDump(D3D12Texture& texture, DXGI_FORMAT dump_format) { + if (!ac6::textures::TextureDumpEnabled() || !ac6::textures::IsSupportedTextureSwapFormat(dump_format)) { + return false; + } + + const TextureKey& key = texture.key(); + const uint64_t texture_key_hash = XXH3_64bits(&key, sizeof(key)); + const std::string stable_key = ac6::textures::BuildTextureStableKey( + texture_key_hash, key.base_page, key.mip_page, uint32_t(key.dimension), key.GetWidth(), + key.GetHeight(), key.GetDepthOrArraySize(), key.mip_max_level + 1, uint32_t(key.format), + uint32_t(key.endianness), key.tiled != 0, key.packed_mips != 0, key.signed_separate != 0, + key.scaled_resolve != 0); + + if (dumped_texture_keys_.contains(stable_key) || ac6::textures::DumpExists(stable_key)) { + dumped_texture_keys_.insert(stable_key); + return false; + } + + ID3D12Resource* texture_resource = texture.resource(); + D3D12_RESOURCE_DESC resource_desc = texture_resource->GetDesc(); + if (resource_desc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE2D && + resource_desc.Dimension != D3D12_RESOURCE_DIMENSION_TEXTURE3D) { + return false; + } + + const uint32_t subresource_count = + resource_desc.Dimension == D3D12_RESOURCE_DIMENSION_TEXTURE3D + ? resource_desc.MipLevels + : resource_desc.MipLevels * resource_desc.DepthOrArraySize; + std::vector footprints(subresource_count); + std::vector row_counts(subresource_count); + std::vector row_sizes(subresource_count); + UINT64 total_size = 0; + ID3D12Device* device = command_processor_.GetD3D12Provider().GetDevice(); + device->GetCopyableFootprints(&resource_desc, 0, subresource_count, 0, footprints.data(), + row_counts.data(), row_sizes.data(), &total_size); + + ID3D12Resource* readback_resource = command_processor_.RequestReadbackBuffer(uint32_t(total_size)); + if (!readback_resource) { + return false; + } + + const D3D12_RESOURCE_STATES previous_state = texture.SetResourceState(D3D12_RESOURCE_STATE_COPY_SOURCE); + if (previous_state != D3D12_RESOURCE_STATE_COPY_SOURCE) { + command_processor_.PushTransitionBarrier(texture_resource, previous_state, + D3D12_RESOURCE_STATE_COPY_SOURCE); + command_processor_.SubmitBarriers(); + } + + DeferredCommandList& command_list = command_processor_.GetDeferredCommandList(); + for (uint32_t subresource_index = 0; subresource_index < subresource_count; ++subresource_index) { + D3D12_TEXTURE_COPY_LOCATION source = {}; + source.pResource = texture_resource; + source.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + source.SubresourceIndex = subresource_index; + + D3D12_TEXTURE_COPY_LOCATION dest = {}; + dest.pResource = readback_resource; + dest.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + dest.PlacedFootprint = footprints[subresource_index]; + + command_list.D3DCopyTextureRegion(&dest, 0, 0, 0, &source, nullptr); + } + + const ac6::backend::BackendDiagnosticsSnapshot diagnostics = ac6::backend::GetDiagnosticsSnapshot(); + PendingTextureDump pending_dump; + pending_dump.submission_index = command_processor_.GetCurrentSubmission(); + pending_dump.total_size = total_size; + pending_dump.texture_key_hash = texture_key_hash; + pending_dump.base_page = key.base_page; + pending_dump.mip_page = key.mip_page; + pending_dump.guest_dimension = uint32_t(key.dimension); + pending_dump.width = key.GetWidth(); + pending_dump.height = key.GetHeight(); + pending_dump.depth_or_array_size = key.GetDepthOrArraySize(); + pending_dump.mip_count = key.mip_max_level + 1; + pending_dump.guest_format = uint32_t(key.format); + pending_dump.endianness = uint32_t(key.endianness); + pending_dump.dxgi_format = dump_format; + pending_dump.resource_dimension = resource_desc.Dimension; + pending_dump.tiled = key.tiled != 0; + pending_dump.packed_mips = key.packed_mips != 0; + pending_dump.signed_separate = key.signed_separate != 0; + pending_dump.scaled_resolve = key.scaled_resolve != 0; + pending_dump.frame_index = diagnostics.frame_index; + pending_dump.signature_stable_id = diagnostics.latest_signature.stable_id; + pending_dump.active_vertex_shader_hash = diagnostics.active_vertex_shader_hash; + pending_dump.active_pixel_shader_hash = diagnostics.active_pixel_shader_hash; + pending_dump.stable_key = stable_key; + pending_dump.signature_tags = diagnostics.latest_signature_tags; + pending_dump.readback_buffer = readback_resource; + pending_dump.footprints = std::move(footprints); + pending_dump.row_counts.reserve(row_counts.size()); + for (UINT row_count : row_counts) { + pending_dump.row_counts.push_back(uint32_t(row_count)); + } + pending_texture_dumps_.push_back(std::move(pending_dump)); + dumped_texture_keys_.insert(stable_key); + return true; +} + +bool D3D12TextureCache::ApplyTextureReplacement(D3D12Texture& texture, DXGI_FORMAT replacement_format) { + if (!ac6::textures::TextureReplacementEnabled() || + !ac6::textures::IsSupportedTextureSwapFormat(replacement_format)) { + return false; + } + + const TextureKey& key = texture.key(); + const uint64_t texture_key_hash = XXH3_64bits(&key, sizeof(key)); + const std::string stable_key = ac6::textures::BuildTextureStableKey( + texture_key_hash, key.base_page, key.mip_page, uint32_t(key.dimension), key.GetWidth(), + key.GetHeight(), key.GetDepthOrArraySize(), key.mip_max_level + 1, uint32_t(key.format), + uint32_t(key.endianness), key.tiled != 0, key.packed_mips != 0, key.signed_separate != 0, + key.scaled_resolve != 0); + + const std::optional replacement_path = + ac6::textures::ResolveReplacementDdsPath(stable_key); + if (!replacement_path) { + return false; + } + + ac6::textures::DdsImageData replacement; + std::string error; + if (!ac6::textures::LoadDdsFromFile(*replacement_path, replacement, &error)) { + if (replacement_warning_keys_.insert(stable_key).second) { + REXGPU_WARN("Texture swap {}: failed to load replacement {} ({})", stable_key, + replacement_path->string(), error); + } + return false; + } + + ID3D12Resource* texture_resource = texture.resource(); + const D3D12_RESOURCE_DESC resource_desc = texture_resource->GetDesc(); + if (replacement.is_cube || replacement.format != replacement_format || + replacement.dimension != resource_desc.Dimension || replacement.width != resource_desc.Width || + replacement.height != resource_desc.Height || + replacement.depth_or_array_size != resource_desc.DepthOrArraySize || + replacement.mip_count != resource_desc.MipLevels) { + if (replacement_warning_keys_.insert(stable_key).second) { + REXGPU_WARN( + "Texture swap {}: replacement {} does not match expected format/layout (expected {} {}x{}x{} mips={})", + stable_key, replacement_path->string(), ac6::textures::DescribeDxgiFormat(replacement_format), + uint32_t(resource_desc.Width), resource_desc.Height, resource_desc.DepthOrArraySize, + resource_desc.MipLevels); + } + return false; + } + + const uint32_t subresource_count = + resource_desc.Dimension == D3D12_RESOURCE_DIMENSION_TEXTURE3D + ? resource_desc.MipLevels + : resource_desc.MipLevels * resource_desc.DepthOrArraySize; + if (replacement.subresources.size() != subresource_count) { + if (replacement_warning_keys_.insert(stable_key).second) { + REXGPU_WARN("Texture swap {}: replacement {} has {} subresources, expected {}", stable_key, + replacement_path->string(), replacement.subresources.size(), subresource_count); + } + return false; + } + + ID3D12Device* device = command_processor_.GetD3D12Provider().GetDevice(); + std::vector footprints(subresource_count); + std::vector row_counts(subresource_count); + std::vector row_sizes(subresource_count); + UINT64 upload_size = 0; + device->GetCopyableFootprints(&resource_desc, 0, subresource_count, 0, footprints.data(), + row_counts.data(), row_sizes.data(), &upload_size); + + D3D12_RESOURCE_DESC upload_desc = {}; + upload_desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; + upload_desc.Alignment = 0; + upload_desc.Width = upload_size; + upload_desc.Height = 1; + upload_desc.DepthOrArraySize = 1; + upload_desc.MipLevels = 1; + upload_desc.Format = DXGI_FORMAT_UNKNOWN; + upload_desc.SampleDesc.Count = 1; + upload_desc.SampleDesc.Quality = 0; + upload_desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; + upload_desc.Flags = D3D12_RESOURCE_FLAG_NONE; + + Microsoft::WRL::ComPtr upload_buffer; + if (FAILED(device->CreateCommittedResource(&ui::d3d12::util::kHeapPropertiesUpload, + command_processor_.GetD3D12Provider().GetHeapFlagCreateNotZeroed(), + &upload_desc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, + IID_PPV_ARGS(&upload_buffer)))) { + if (replacement_warning_keys_.insert(stable_key).second) { + REXGPU_WARN("Texture swap {}: failed to create upload buffer for {}", stable_key, + replacement_path->string()); + } + return false; + } + + D3D12_RANGE no_read_range = {}; + void* mapped_upload = nullptr; + if (FAILED(upload_buffer->Map(0, &no_read_range, &mapped_upload))) { + if (replacement_warning_keys_.insert(stable_key).second) { + REXGPU_WARN("Texture swap {}: failed to map upload buffer for {}", stable_key, + replacement_path->string()); + } + return false; + } + + for (uint32_t subresource_index = 0; subresource_index < subresource_count; ++subresource_index) { + const ac6::textures::DdsSubresource& subresource = replacement.subresources[subresource_index]; + const uint8_t* source_base = subresource.data.data(); + uint8_t* dest_base = reinterpret_cast(mapped_upload) + footprints[subresource_index].Offset; + const uint32_t dest_row_pitch = footprints[subresource_index].Footprint.RowPitch; + + for (uint32_t z = 0; z < subresource.depth; ++z) { + const uint8_t* source_slice = source_base + size_t(z) * subresource.slice_pitch; + uint8_t* dest_slice = dest_base + size_t(z) * dest_row_pitch * row_counts[subresource_index]; + for (uint32_t row = 0; row < row_counts[subresource_index]; ++row) { + std::memcpy(dest_slice + size_t(row) * dest_row_pitch, + source_slice + size_t(row) * subresource.row_pitch, subresource.row_pitch); + } + } + } + upload_buffer->Unmap(0, nullptr); + + const D3D12_RESOURCE_STATES previous_state = texture.SetResourceState(D3D12_RESOURCE_STATE_COPY_DEST); + if (previous_state != D3D12_RESOURCE_STATE_COPY_DEST) { + command_processor_.PushTransitionBarrier(texture_resource, previous_state, + D3D12_RESOURCE_STATE_COPY_DEST); + command_processor_.SubmitBarriers(); + } + + DeferredCommandList& command_list = command_processor_.GetDeferredCommandList(); + for (uint32_t subresource_index = 0; subresource_index < subresource_count; ++subresource_index) { + D3D12_TEXTURE_COPY_LOCATION source = {}; + source.pResource = upload_buffer.Get(); + source.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + source.PlacedFootprint = footprints[subresource_index]; + + D3D12_TEXTURE_COPY_LOCATION dest = {}; + dest.pResource = texture_resource; + dest.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + dest.SubresourceIndex = subresource_index; + + command_list.D3DCopyTextureRegion(&dest, 0, 0, 0, &source, nullptr); + } + + pending_upload_resources_.push_back( + PendingUploadResource{command_processor_.GetCurrentSubmission(), upload_buffer}); + replacement_warning_keys_.erase(stable_key); return true; }