mirror of
https://github.com/sal063/AC6_recomp
synced 2026-06-14 22:29:27 -04:00
Implement texture swap dump and replace pipeline
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
- `<stable_key>.dds`
|
||||
- `<stable_key>.json`
|
||||
4. Edit the dumped DDS without changing its format, dimensions, array size, or mip count.
|
||||
5. Place the replacement DDS at:
|
||||
- `override/textures/<stable_key>.dds`
|
||||
- or `mods/<mod_name>/textures/<stable_key>.dds`
|
||||
6. Restart or cause the texture to reload.
|
||||
|
||||
`override/textures` wins over mod folders. Within `mods`, lexicographically later folder names win.
|
||||
|
||||
## Stable Keys
|
||||
|
||||
Dump filenames are generated from the texture cache key, not guessed game names. The filename includes:
|
||||
|
||||
- a hash of the full cache key
|
||||
- base and mip pages
|
||||
- dimension
|
||||
- size
|
||||
- mip count
|
||||
- guest format
|
||||
- endian/tiled/packed/signed/scaled flags
|
||||
|
||||
That makes the key stable enough for round-tripping replacements while still being readable.
|
||||
|
||||
## Metadata Sidecars
|
||||
|
||||
Each JSON sidecar records:
|
||||
|
||||
- guest texture key fields
|
||||
- chosen host DXGI format
|
||||
- AC6 frame index
|
||||
- latest AC6 backend signature ID
|
||||
- active VS/PS hashes
|
||||
- signature tags from the AC6 backend classifier
|
||||
|
||||
This is meant for filtering and later tooling, not for the core replacement path.
|
||||
|
||||
## Current Scope
|
||||
|
||||
First pass limitations:
|
||||
|
||||
- replacement files must be DX10-header DDS files
|
||||
- replacement format, dimensions, depth/array size, and mip count must match exactly
|
||||
- cube textures are skipped
|
||||
- unsupported DXGI formats fall back to the original guest texture
|
||||
|
||||
The fallback path is always the original guest texture load. A bad or missing replacement will not block rendering.
|
||||
@@ -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
|
||||
|
||||
@@ -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<rex::memory::Memory*> g_captured_memory{nullptr};
|
||||
std::atomic_flag g_frame_boundary_active = ATOMIC_FLAG_INIT;
|
||||
std::atomic<uint32_t> 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;
|
||||
|
||||
@@ -0,0 +1,699 @@
|
||||
#include "ac6_texture_overrides.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/filesystem.h>
|
||||
|
||||
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<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)) {
|
||||
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<std::filesystem::path> 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<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)) {
|
||||
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<char*>(&magic), sizeof(magic));
|
||||
file.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||
file.read(reinterpret_cast<char*>(&header_dx10), sizeof(header_dx10));
|
||||
if (!file || magic != kDdsMagic || header.size != sizeof(DdsHeader) ||
|
||||
header.pixel_format.size != sizeof(DdsPixelFormat) ||
|
||||
header.pixel_format.four_cc != kDdsFourCcDx10) {
|
||||
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<uint8_t> payload(size_t(file_size) - (sizeof(uint32_t) + sizeof(DdsHeader) + sizeof(DdsHeaderDx10)));
|
||||
file.read(reinterpret_cast<char*>(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<const char*>(&kDdsMagic), sizeof(kDdsMagic));
|
||||
file.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
file.write(reinterpret_cast<const char*>(&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<const char*>(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
|
||||
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <rex/ui/d3d12/d3d12_api.h>
|
||||
|
||||
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<uint8_t> 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<DdsSubresource> 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<std::filesystem::path> 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
|
||||
+8
-8
@@ -815,14 +815,14 @@ DrawStatsSnapshot GetDrawStats() {
|
||||
return g_snapshot;
|
||||
}
|
||||
|
||||
FrameCaptureSnapshot GetFrameCapture() {
|
||||
std::shared_lock<std::shared_mutex> lock(g_capture_mutex);
|
||||
return g_capture_snapshot;
|
||||
}
|
||||
|
||||
FrameCaptureSummary GetFrameCaptureSummary() {
|
||||
std::shared_lock<std::shared_mutex> lock(g_capture_mutex);
|
||||
return g_capture_summary;
|
||||
FrameCaptureSnapshot TakeFrameCapture(FrameCaptureSummary* summary_out) {
|
||||
std::unique_lock<std::shared_mutex> 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() {
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
@@ -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<rex::ui::WindowedApp> 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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -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<ID3D12Resource> readback_buffer;
|
||||
std::vector<D3D12_PLACED_SUBRESOURCE_FOOTPRINT> footprints;
|
||||
std::vector<uint32_t> row_counts;
|
||||
};
|
||||
|
||||
struct PendingUploadResource {
|
||||
uint64_t submission_index = 0;
|
||||
Microsoft::WRL::ComPtr<ID3D12Resource> 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<std::string> dumped_texture_keys_;
|
||||
std::unordered_set<std::string> replacement_warning_keys_;
|
||||
std::vector<PendingTextureDump> pending_texture_dumps_;
|
||||
std::vector<PendingUploadResource> 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
|
||||
|
||||
@@ -30,6 +30,10 @@
|
||||
#include <rex/math.h>
|
||||
#include <rex/ui/d3d12/d3d12_upload_buffer_pool.h>
|
||||
#include <rex/ui/d3d12/d3d12_util.h>
|
||||
#include <rex/hash.h>
|
||||
|
||||
#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<const uint8_t*>(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<D3D12_PLACED_SUBRESOURCE_FOOTPRINT> footprints(subresource_count);
|
||||
std::vector<UINT> row_counts(subresource_count);
|
||||
std::vector<UINT64> 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<std::filesystem::path> 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<D3D12_PLACED_SUBRESOURCE_FOOTPRINT> footprints(subresource_count);
|
||||
std::vector<UINT> row_counts(subresource_count);
|
||||
std::vector<UINT64> 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<ID3D12Resource> 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<uint8_t*>(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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user