From 701eb202eb6bc444a9d6a8207677e97634a6e5d9 Mon Sep 17 00:00:00 2001 From: salh Date: Mon, 20 Apr 2026 23:13:53 +0300 Subject: [PATCH] Fix graphics diagnostics and restore default runtime settings --- src/ac6_native_graphics.cpp | 173 ++++++- src/ac6_native_graphics.h | 59 +++ src/ac6_native_graphics_overlay.cpp | 488 ++++++++++++++++++ .../ac6_render_frontend.cpp | 63 +++ src/ac6_native_renderer/ac6_render_frontend.h | 19 + src/d3d_hooks.cpp | 14 +- src/d3d_state.h | 2 + src/main.cpp | 7 +- .../rex/graphics/pipeline/texture/cache.h | 4 + .../src/graphics/pipeline/texture/cache.cpp | 33 +- 10 files changed, 840 insertions(+), 22 deletions(-) diff --git a/src/ac6_native_graphics.cpp b/src/ac6_native_graphics.cpp index 9d4c7e00..5c44553b 100644 --- a/src/ac6_native_graphics.cpp +++ b/src/ac6_native_graphics.cpp @@ -1,10 +1,13 @@ #include "ac6_native_graphics.h" #include +#include +#include #include #include #include +#include #include #include #include @@ -38,8 +41,11 @@ namespace ac6::graphics { namespace { ac6::renderer::NativeRenderer g_native_renderer; +ac6::renderer::Ac6RenderFrontend g_capture_frontend; +ac6::renderer::FramePlanner g_capture_frame_planner; NativeGraphicsRuntimeStatus g_runtime_status{}; ac6::renderer::D3D12Backend* g_d3d12_backend = nullptr; +std::atomic g_captured_memory{nullptr}; GraphicsRuntimeMode ParseGraphicsMode(std::string_view value) { if (value == "disabled") { @@ -77,12 +83,10 @@ void ResetReplayStatus() { g_runtime_status.replay_frames_built = 0; g_runtime_status.active_backend = ac6::renderer::BackendType::kUnknown; g_runtime_status.renderer_stats = {}; - g_runtime_status.frontend_summary = {}; g_runtime_status.replay_summary = {}; g_runtime_status.execution_summary = {}; g_runtime_status.executor_summary = {}; g_runtime_status.backend_executor_status = {}; - g_runtime_status.frame_plan = {}; g_runtime_status.latest_renderer_frame_index = 0; g_runtime_status.last_meaningful_renderer_frame_index = 0; g_runtime_status.showing_latched_snapshot = false; @@ -92,6 +96,24 @@ void SyncRuntimeFlags() { g_runtime_status.enabled = REXCVAR_GET(ac6_native_graphics_enabled); g_runtime_status.mode = ParseGraphicsMode(REXCVAR_GET(ac6_graphics_mode)); g_runtime_status.capture_enabled = REXCVAR_GET(ac6_render_capture); + const uint32_t shared_scale = + static_cast(std::max(INT32_C(1), REXCVAR_GET(resolution_scale))); + const bool use_shared_scale = rex::cvar::HasNonDefaultValue("resolution_scale"); + const int32_t configured_scale_x = + use_shared_scale && !rex::cvar::HasNonDefaultValue("draw_resolution_scale_x") + ? static_cast(shared_scale) + : REXCVAR_GET(draw_resolution_scale_x); + const int32_t configured_scale_y = + use_shared_scale && !rex::cvar::HasNonDefaultValue("draw_resolution_scale_y") + ? static_cast(shared_scale) + : REXCVAR_GET(draw_resolution_scale_y); + g_runtime_status.draw_resolution_scale_x = + static_cast(std::max(INT32_C(1), configured_scale_x)); + g_runtime_status.draw_resolution_scale_y = + static_cast(std::max(INT32_C(1), configured_scale_y)); + g_runtime_status.direct_host_resolve = REXCVAR_GET(direct_host_resolve); + g_runtime_status.draw_resolution_scaled_texture_offsets = + REXCVAR_GET(draw_resolution_scaled_texture_offsets); g_runtime_status.authoritative_renderer_active = g_runtime_status.enabled && g_runtime_status.mode != GraphicsRuntimeMode::kDisabled; @@ -131,6 +153,142 @@ bool IsMeaningfulRendererSnapshot( backend_status.resolve_command_count != 0; } +uint32_t ScoreObservedPassForDiagnostics(const ac6::renderer::ObservedPassDesc& pass) { + const uint64_t viewport_area = + uint64_t(pass.viewport_width) * uint64_t(pass.viewport_height); + uint32_t score = 0; + score += pass.selected_for_present ? 160u : 0u; + score += pass.matches_frame_end_viewport ? 120u : 0u; + score += pass.resolve_count * 80u; + score += pass.draw_count * 4u; + score += pass.clear_count * 6u; + score += pass.max_texture_count * 3u; + score += pass.max_stream_count * 2u; + score += pass.max_sampler_count * 2u; + score += static_cast(std::min(viewport_area / 32768u, 120u)); + switch (pass.kind) { + case ac6::renderer::ObservedPassKind::kScene: + score += 40u; + break; + case ac6::renderer::ObservedPassKind::kPostProcess: + score += 50u; + break; + case ac6::renderer::ObservedPassKind::kUiComposite: + score += 20u; + break; + case ac6::renderer::ObservedPassKind::kUnknown: + default: + break; + } + return score; +} + +void RefreshPassDiagnostics(const std::vector& passes) { + g_runtime_status.pass_diagnostics_count = 0; + g_runtime_status.pass_diagnostics = {}; + if (passes.empty()) { + return; + } + + struct RankedPass { + uint32_t pass_index = 0; + uint32_t score = 0; + }; + + std::vector ranked; + ranked.reserve(passes.size()); + for (uint32_t i = 0; i < passes.size(); ++i) { + const auto& pass = passes[i]; + ranked.push_back({i, ScoreObservedPassForDiagnostics(pass)}); + } + + std::sort(ranked.begin(), ranked.end(), [&](const RankedPass& left, const RankedPass& right) { + if (left.score != right.score) { + return left.score > right.score; + } + return left.pass_index < right.pass_index; + }); + + const uint32_t count = std::min(static_cast(ranked.size()), + static_cast(g_runtime_status.pass_diagnostics.size())); + g_runtime_status.pass_diagnostics_count = count; + for (uint32_t i = 0; i < count; ++i) { + const auto& ranked_pass = ranked[i]; + const auto& pass = passes[ranked_pass.pass_index]; + g_runtime_status.pass_diagnostics[i] = { + .valid = true, + .pass_index = ranked_pass.pass_index, + .kind = pass.kind, + .score = ranked_pass.score, + .render_target_0 = pass.render_target_0, + .depth_stencil = pass.depth_stencil, + .viewport_x = pass.viewport_x, + .viewport_y = pass.viewport_y, + .viewport_width = pass.viewport_width, + .viewport_height = pass.viewport_height, + .draw_count = pass.draw_count, + .clear_count = pass.clear_count, + .resolve_count = pass.resolve_count, + .max_texture_count = pass.max_texture_count, + .max_stream_count = pass.max_stream_count, + .max_sampler_count = pass.max_sampler_count, + .max_fetch_constant_count = pass.max_fetch_constant_count, + .max_shader_gpr_alloc = pass.max_shader_gpr_alloc, + .pass_signature = pass.pass_signature, + .first_texture_fetch_layout_signature = + pass.first_texture_fetch_layout_signature, + .last_texture_fetch_layout_signature = + pass.last_texture_fetch_layout_signature, + .first_resource_binding_signature = + pass.first_resource_binding_signature, + .last_resource_binding_signature = + pass.last_resource_binding_signature, + .first_textures = {.count = pass.first_textures.count, + .slots = pass.first_textures.slots, + .values = pass.first_textures.values}, + .last_textures = {.count = pass.last_textures.count, + .slots = pass.last_textures.slots, + .values = pass.last_textures.values}, + .first_fetch_constants = {.count = pass.first_fetch_constants.count, + .slots = pass.first_fetch_constants.slots, + .values = pass.first_fetch_constants.values}, + .last_fetch_constants = {.count = pass.last_fetch_constants.count, + .slots = pass.last_fetch_constants.slots, + .values = pass.last_fetch_constants.values}, + .selected_for_present = pass.selected_for_present, + .matches_frame_end_viewport = pass.matches_frame_end_viewport, + }; + } +} + +void RefreshResolveDiagnostics(const ac6::d3d::FrameCaptureSnapshot& frame_capture) { + g_runtime_status.resolve_diagnostics_count = 0; + g_runtime_status.resolve_diagnostics = {}; + if (frame_capture.resolves.empty()) { + return; + } + + const uint32_t total_resolves = static_cast(frame_capture.resolves.size()); + const uint32_t count = + std::min(total_resolves, + static_cast(g_runtime_status.resolve_diagnostics.size())); + const uint32_t start_index = total_resolves - count; + g_runtime_status.resolve_diagnostics_count = count; + for (uint32_t i = 0; i < count; ++i) { + const auto& resolve = frame_capture.resolves[start_index + i]; + g_runtime_status.resolve_diagnostics[i] = { + .valid = true, + .sequence = resolve.sequence, + .render_target_0 = resolve.shadow_state.render_targets[0], + .depth_stencil = resolve.shadow_state.depth_stencil, + .viewport_width = resolve.shadow_state.viewport.width, + .viewport_height = resolve.shadow_state.viewport.height, + .args = resolve.args, + .depth_or_scale = resolve.depth_or_scale, + }; + } +} + void ShutdownReplayRenderer() { g_d3d12_backend = nullptr; if (!g_runtime_status.initialized) { @@ -213,6 +371,7 @@ std::string_view ToString(const GraphicsRuntimeMode mode) { void OnFrameBoundary(rex::memory::Memory* memory) { SyncRuntimeFlags(); + g_captured_memory.store(memory, std::memory_order_release); if (!g_runtime_status.enabled || g_runtime_status.mode == GraphicsRuntimeMode::kDisabled) { ShutdownReplayRenderer(); @@ -238,6 +397,11 @@ void OnFrameBoundary(rex::memory::Memory* memory) { ++g_runtime_status.analysis_frames_observed; g_runtime_status.capture_summary = capture_summary; g_runtime_status.latest_capture_frame_index = capture_summary.frame_index; + g_runtime_status.frontend_summary = g_capture_frontend.BuildFromCapture(frame_capture); + RefreshPassDiagnostics(g_capture_frontend.passes()); + RefreshResolveDiagnostics(frame_capture); + g_runtime_status.frame_plan = g_capture_frame_planner.Build( + g_runtime_status.frontend_summary, g_capture_frontend.passes()); if (capture_summary.draw_count || capture_summary.clear_count || capture_summary.resolve_count) { g_runtime_status.last_meaningful_capture_frame_index = capture_summary.frame_index; @@ -318,6 +482,7 @@ void OnFrameBoundary(rex::memory::Memory* memory) { } void Shutdown() { + g_captured_memory.store(nullptr, std::memory_order_release); ShutdownReplayRenderer(); } @@ -337,4 +502,8 @@ ID3D12Resource* GetNativeOutputTexture() { return g_d3d12_backend ? g_d3d12_backend->GetOutputTexture() : nullptr; } +rex::memory::Memory* GetCapturedMemory() { + return g_captured_memory.load(std::memory_order_acquire); +} + } // namespace ac6::graphics diff --git a/src/ac6_native_graphics.h b/src/ac6_native_graphics.h index e0f117f6..d673f1f9 100644 --- a/src/ac6_native_graphics.h +++ b/src/ac6_native_graphics.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -28,11 +29,64 @@ enum class GraphicsRuntimeMode : uint8_t { std::string_view ToString(GraphicsRuntimeMode mode); struct NativeGraphicsRuntimeStatus { + struct PassDiagnosticsEntry { + struct BoundResourceSample { + uint32_t count = 0; + std::array slots{}; + std::array values{}; + }; + + bool valid = false; + uint32_t pass_index = 0; + ac6::renderer::ObservedPassKind kind = ac6::renderer::ObservedPassKind::kUnknown; + uint32_t score = 0; + uint32_t render_target_0 = 0; + uint32_t depth_stencil = 0; + uint32_t viewport_x = 0; + uint32_t viewport_y = 0; + uint32_t viewport_width = 0; + uint32_t viewport_height = 0; + uint32_t draw_count = 0; + uint32_t clear_count = 0; + uint32_t resolve_count = 0; + uint32_t max_texture_count = 0; + uint32_t max_stream_count = 0; + uint32_t max_sampler_count = 0; + uint32_t max_fetch_constant_count = 0; + uint32_t max_shader_gpr_alloc = 0; + uint64_t pass_signature = 0; + uint64_t first_texture_fetch_layout_signature = 0; + uint64_t last_texture_fetch_layout_signature = 0; + uint64_t first_resource_binding_signature = 0; + uint64_t last_resource_binding_signature = 0; + BoundResourceSample first_textures{}; + BoundResourceSample last_textures{}; + BoundResourceSample first_fetch_constants{}; + BoundResourceSample last_fetch_constants{}; + bool selected_for_present = false; + bool matches_frame_end_viewport = false; + }; + + struct ResolveDiagnosticsEntry { + bool valid = false; + uint32_t sequence = 0; + uint32_t render_target_0 = 0; + uint32_t depth_stencil = 0; + uint32_t viewport_width = 0; + uint32_t viewport_height = 0; + std::array args{}; + float depth_or_scale = 0.0f; + }; + bool enabled = false; GraphicsRuntimeMode mode = GraphicsRuntimeMode::kHybridBackendFixes; bool capture_enabled = false; bool authoritative_renderer_active = false; bool experimental_replay_present = false; + uint32_t draw_resolution_scale_x = 1; + uint32_t draw_resolution_scale_y = 1; + bool direct_host_resolve = true; + bool draw_resolution_scaled_texture_offsets = true; bool initialized = false; bool had_init_failure = false; bool showing_latched_snapshot = false; @@ -56,6 +110,10 @@ struct NativeGraphicsRuntimeStatus { ac6::d3d::FrameCaptureSummary capture_summary{}; ac6::backend::BackendDiagnosticsSnapshot backend_diagnostics{}; ac6::renderer::NativeFramePlan frame_plan{}; + uint32_t pass_diagnostics_count = 0; + std::array pass_diagnostics{}; + uint32_t resolve_diagnostics_count = 0; + std::array resolve_diagnostics{}; }; void OnFrameBoundary(rex::memory::Memory* memory); @@ -63,5 +121,6 @@ void Shutdown(); NativeGraphicsRuntimeStatus GetRuntimeStatus(); ID3D12Resource* GetNativeOutputTexture(); +rex::memory::Memory* GetCapturedMemory(); } // namespace ac6::graphics diff --git a/src/ac6_native_graphics_overlay.cpp b/src/ac6_native_graphics_overlay.cpp index 0ddd8377..e0c41abb 100644 --- a/src/ac6_native_graphics_overlay.cpp +++ b/src/ac6_native_graphics_overlay.cpp @@ -1,10 +1,359 @@ #include "ac6_native_graphics_overlay.h" +#include +#include +#include + #include +#include +#include +#include #include "ac6_native_graphics.h" namespace ac6::graphics { +namespace { + +struct DecodedTextureFetch { + bool valid = false; + uint32_t header_offset = 0; + uint32_t width = 0; + uint32_t height = 0; + uint32_t depth = 0; + uint32_t base_address = 0; + const char* format_name = nullptr; + rex::graphics::xenos::DataDimension dimension = + rex::graphics::xenos::DataDimension::k2DOrStacked; + bool tiled = false; +}; + +template +struct DecodedSampleSet { + uint32_t count = 0; + std::array entries{}; +}; + +bool IsReadableGuestRange(rex::memory::Memory* memory, uint32_t guest_ptr, uint32_t size) { + if (memory == nullptr || guest_ptr == 0 || size == 0) { + return false; + } + const uint32_t guest_end = guest_ptr + size - 1; + if (guest_end < guest_ptr) { + return false; + } + auto* start_heap = memory->LookupHeap(guest_ptr); + if (start_heap == nullptr) { + return false; + } + auto* end_heap = memory->LookupHeap(guest_end); + if (end_heap != start_heap) { + return false; + } + const auto access = start_heap->QueryRangeAccess(guest_ptr, guest_end); + return access == rex::memory::PageAccess::kReadOnly || + access == rex::memory::PageAccess::kReadWrite || + access == rex::memory::PageAccess::kExecuteReadOnly || + access == rex::memory::PageAccess::kExecuteReadWrite; +} + +bool LoadTextureFetchAt(rex::memory::Memory* memory, uint32_t guest_ptr, + rex::graphics::xenos::xe_gpu_texture_fetch_t& fetch) { + if (!IsReadableGuestRange(memory, guest_ptr, sizeof(fetch))) { + return false; + } + const uint8_t* fetch_base = memory->TranslateVirtual(guest_ptr); + if (fetch_base == nullptr) { + return false; + } + fetch.dword_0 = rex::memory::load_and_swap(fetch_base + 0); + fetch.dword_1 = rex::memory::load_and_swap(fetch_base + 4); + fetch.dword_2 = rex::memory::load_and_swap(fetch_base + 8); + fetch.dword_3 = rex::memory::load_and_swap(fetch_base + 12); + fetch.dword_4 = rex::memory::load_and_swap(fetch_base + 16); + fetch.dword_5 = rex::memory::load_and_swap(fetch_base + 20); + return true; +} + +bool IsPlausibleTextureFetchHeader(const rex::graphics::xenos::xe_gpu_texture_fetch_t& fetch) { + using rex::graphics::xenos::DataDimension; + + if (static_cast(fetch.format) >= 64) { + return false; + } + + switch (fetch.dimension) { + case DataDimension::k1D: + case DataDimension::k3D: + return !fetch.stacked; + case DataDimension::k2DOrStacked: + return true; + case DataDimension::kCube: + return !fetch.stacked && fetch.size_2d.stack_depth == 5; + default: + return false; + } +} + +bool IsPlausibleTextureInfo(const rex::graphics::TextureInfo& info) { + const uint32_t width = info.width + 1; + const uint32_t height = info.height + 1; + const uint32_t depth = info.depth + 1; + return width >= 1 && width <= 8192 && height >= 1 && height <= 8192 && depth >= 1 && + depth <= 2048 && info.memory.base_address != 0 && info.format_info() != nullptr; +} + +bool DecodeTextureFetch(uint32_t guest_ptr, DecodedTextureFetch& out) { + rex::memory::Memory* memory = GetCapturedMemory(); + if (!IsReadableGuestRange(memory, guest_ptr, sizeof(rex::graphics::xenos::xe_gpu_texture_fetch_t))) { + return false; + } + + constexpr uint32_t kProbeLimit = 0x80; + for (uint32_t offset = 0; offset + sizeof(rex::graphics::xenos::xe_gpu_texture_fetch_t) <= + kProbeLimit; + offset += 4) { + if (!IsReadableGuestRange(memory, guest_ptr + offset, + sizeof(rex::graphics::xenos::xe_gpu_texture_fetch_t))) { + continue; + } + rex::graphics::xenos::xe_gpu_texture_fetch_t fetch{}; + if (!LoadTextureFetchAt(memory, guest_ptr + offset, fetch)) { + continue; + } + if (fetch.type != rex::graphics::xenos::FetchConstantType::kTexture) { + continue; + } + if (!IsPlausibleTextureFetchHeader(fetch)) { + continue; + } + + rex::graphics::TextureInfo texture_info; + if (!rex::graphics::TextureInfo::Prepare(fetch, &texture_info) || + !IsPlausibleTextureInfo(texture_info)) { + continue; + } + + out.valid = true; + out.header_offset = offset; + out.dimension = texture_info.dimension; + out.tiled = texture_info.is_tiled; + out.width = texture_info.width + 1; + out.height = texture_info.height + 1; + out.depth = texture_info.depth + 1; + out.base_address = texture_info.memory.base_address; + out.format_name = texture_info.format_info()->name; + return true; + } + return false; +} + +std::string FormatDecodedTextureObject(uint32_t guest_ptr) { + if (guest_ptr == 0) { + return "none"; + } + DecodedTextureFetch decoded; + if (!DecodeTextureFetch(guest_ptr, decoded) || !decoded.valid) { + char buffer[32]; + std::snprintf(buffer, sizeof(buffer), "%08X", guest_ptr); + return buffer; + } + + char buffer[128]; + const char* tiled_tag = decoded.tiled ? "t" : "l"; + if (decoded.dimension == rex::graphics::xenos::DataDimension::k3D || decoded.depth > 1) { + std::snprintf(buffer, sizeof(buffer), "%08X+%02X[%ux%ux%u %s @%08X %s]", guest_ptr, + decoded.header_offset, decoded.width, decoded.height, decoded.depth, tiled_tag, + decoded.base_address, decoded.format_name ? decoded.format_name : "?"); + } else { + std::snprintf(buffer, sizeof(buffer), "%08X+%02X[%ux%u %s @%08X %s]", guest_ptr, + decoded.header_offset, decoded.width, decoded.height, tiled_tag, + decoded.base_address, decoded.format_name ? decoded.format_name : "?"); + } + return buffer; +} + +template +DecodedSampleSet<4> DecodeSampleSet(const Sample& sample) { + DecodedSampleSet<4> decoded_set; + const uint32_t shown = + std::min(sample.count, static_cast(sample.values.size())); + for (uint32_t i = 0; i < shown; ++i) { + DecodedTextureFetch decoded; + if (!DecodeTextureFetch(sample.values[i], decoded) || !decoded.valid) { + continue; + } + decoded_set.entries[decoded_set.count++] = decoded; + } + return decoded_set; +} + +template +bool SampleUsesBaseAddress(const DecodedSampleSet& sample_set, uint32_t base_address) { + if (base_address == 0) { + return false; + } + for (uint32_t i = 0; i < sample_set.count; ++i) { + if (sample_set.entries[i].base_address == base_address) { + return true; + } + } + return false; +} + +std::string FormatResolveArgCandidates( + const NativeGraphicsRuntimeStatus::ResolveDiagnosticsEntry& resolve) { + std::string formatted; + for (uint32_t i = 0; i < resolve.args.size(); ++i) { + if (resolve.args[i] == 0) { + continue; + } + DecodedTextureFetch decoded; + if (!DecodeTextureFetch(resolve.args[i], decoded) || !decoded.valid) { + continue; + } + char buffer[96]; + std::snprintf(buffer, sizeof(buffer), "r%u=%08X+%02X[%ux%u @%08X %s]", 4u + i, + resolve.args[i], decoded.header_offset, decoded.width, decoded.height, + decoded.base_address, decoded.format_name ? decoded.format_name : "?"); + if (!formatted.empty()) { + formatted.append(", "); + } + formatted.append(buffer); + } + return formatted.empty() ? "none" : formatted; +} + +template +std::string FormatPassSources(const DecodedSampleSet& first_fetch, + const DecodedSampleSet& last_fetch, + const NativeGraphicsRuntimeStatus& status, uint32_t self_index) { + std::string formatted; + for (uint32_t other_index = 0; other_index < status.pass_diagnostics_count; ++other_index) { + if (other_index == self_index) { + continue; + } + const auto& other_pass = status.pass_diagnostics[other_index]; + if (!other_pass.valid || other_pass.render_target_0 == 0) { + continue; + } + DecodedTextureFetch decoded_rt0; + if (!DecodeTextureFetch(other_pass.render_target_0, decoded_rt0) || !decoded_rt0.valid || + decoded_rt0.base_address == 0) { + continue; + } + if (!SampleUsesBaseAddress(first_fetch, decoded_rt0.base_address) && + !SampleUsesBaseAddress(last_fetch, decoded_rt0.base_address)) { + continue; + } + char buffer[48]; + std::snprintf(buffer, sizeof(buffer), "#%u(rt0 #%u %ux%u @%08X)", other_index, + other_pass.pass_index, decoded_rt0.width, decoded_rt0.height, + decoded_rt0.base_address); + if (!formatted.empty()) { + formatted.append(", "); + } + formatted.append(buffer); + } + return formatted.empty() ? "none" : formatted; +} + +template +std::string FormatResolveSources(const DecodedSampleSet& first_fetch, + const DecodedSampleSet& last_fetch, + const NativeGraphicsRuntimeStatus& status) { + std::string formatted; + for (uint32_t resolve_index = 0; resolve_index < status.resolve_diagnostics_count; + ++resolve_index) { + const auto& resolve = status.resolve_diagnostics[resolve_index]; + if (!resolve.valid) { + continue; + } + for (uint32_t arg_index = 0; arg_index < resolve.args.size(); ++arg_index) { + if (resolve.args[arg_index] == 0) { + continue; + } + DecodedTextureFetch decoded; + if (!DecodeTextureFetch(resolve.args[arg_index], decoded) || !decoded.valid || + decoded.base_address == 0) { + continue; + } + if (!SampleUsesBaseAddress(first_fetch, decoded.base_address) && + !SampleUsesBaseAddress(last_fetch, decoded.base_address)) { + continue; + } + char buffer[72]; + std::snprintf(buffer, sizeof(buffer), "#%u:r%u[%ux%u @%08X]", resolve_index, 4u + arg_index, + decoded.width, decoded.height, decoded.base_address); + if (!formatted.empty()) { + formatted.append(", "); + } + formatted.append(buffer); + } + } + return formatted.empty() ? "none" : formatted; +} + +template +std::string FormatBoundResourceSample(const Sample& sample) { + if (sample.count == 0) { + return "none"; + } + std::string formatted; + const uint32_t shown = std::min(sample.count, static_cast(sample.values.size())); + for (uint32_t i = 0; i < shown; ++i) { + if (!formatted.empty()) { + formatted.append(", "); + } + char buffer[32]; + std::snprintf(buffer, sizeof(buffer), "%u:%08X", sample.slots[i], sample.values[i]); + formatted.append(buffer); + } + if (sample.count > shown) { + formatted.append(", ..."); + } + return formatted; +} + +template +std::string FormatFetchConstantSample(const Sample& sample) { + if (sample.count == 0) { + return "none"; + } + std::string formatted; + const uint32_t shown = std::min(sample.count, static_cast(sample.values.size())); + for (uint32_t i = 0; i < shown; ++i) { + if (!formatted.empty()) { + formatted.append(", "); + } + char buffer[96]; + DecodedTextureFetch decoded; + if (DecodeTextureFetch(sample.values[i], decoded) && decoded.valid) { + const char* tiled_tag = decoded.tiled ? "t" : "l"; + if (decoded.dimension == rex::graphics::xenos::DataDimension::k3D || + decoded.depth > 1) { + std::snprintf(buffer, sizeof(buffer), + "%u:%08X+%02X[%ux%ux%u %s @%08X %s]", sample.slots[i], sample.values[i], + decoded.header_offset, decoded.width, decoded.height, decoded.depth, + tiled_tag, decoded.base_address, + decoded.format_name ? decoded.format_name : "?"); + } else { + std::snprintf(buffer, sizeof(buffer), "%u:%08X+%02X[%ux%u %s @%08X %s]", + sample.slots[i], sample.values[i], decoded.header_offset, decoded.width, + decoded.height, tiled_tag, decoded.base_address, + decoded.format_name ? decoded.format_name : "?"); + } + } else { + std::snprintf(buffer, sizeof(buffer), "%u:%08X", sample.slots[i], sample.values[i]); + } + formatted.append(buffer); + } + if (sample.count > shown) { + formatted.append(", ..."); + } + return formatted; +} + +} // namespace NativeGraphicsStatusDialog::NativeGraphicsStatusDialog(rex::ui::ImGuiDrawer* imgui_drawer) : ImGuiDialog(imgui_drawer) {} @@ -33,6 +382,11 @@ void NativeGraphicsStatusDialog::OnDraw(ImGuiIO& io) { status.authoritative_renderer_active ? "RexGlue/Xenia D3D12 backend" : "disabled"); ImGui::Text("capture active: %s", status.capture_enabled ? "yes" : "no"); + ImGui::Text("draw resolution scale: %ux%u", status.draw_resolution_scale_x, + status.draw_resolution_scale_y); + ImGui::Text("scaled tex offsets / direct host resolve: %s / %s", + status.draw_resolution_scaled_texture_offsets ? "on" : "off", + status.direct_host_resolve ? "on" : "off"); ImGui::Text("experimental replay present override: %s", status.experimental_replay_present ? "enabled" : "disabled"); ImGui::Text("analysis frames / replay frames: %llu / %llu", @@ -55,6 +409,109 @@ void NativeGraphicsStatusDialog::OnDraw(ImGuiIO& io) { ImGui::Text("frame-end viewport: %ux%u", status.capture_summary.frame_end_viewport_width, status.capture_summary.frame_end_viewport_height); + ImGui::Text("frontend passes scene/post/ui: %u / %u / %u / %u", + status.frontend_summary.pass_count, status.frontend_summary.scene_pass_count, + status.frontend_summary.post_process_pass_count, + status.frontend_summary.ui_pass_count); + if (status.frame_plan.present_stage.valid) { + ImGui::Text( + "present stage: #%u %s vp=%ux%u rt0=%08X depth=%08X draws/resolves=%u/%u", + status.frame_plan.present_stage.pass_index, + ac6::renderer::ToString(status.frame_plan.present_stage.kind), + status.frame_plan.present_stage.viewport_width, + status.frame_plan.present_stage.viewport_height, + status.frame_plan.present_stage.render_target_0, + status.frame_plan.present_stage.depth_stencil, + status.frame_plan.present_stage.draw_count, + status.frame_plan.present_stage.resolve_count); + } + if (status.frame_plan.scene_stage.valid) { + ImGui::Text( + "scene stage: #%u %s vp=%ux%u rt0=%08X depth=%08X draws/resolves=%u/%u", + status.frame_plan.scene_stage.pass_index, + ac6::renderer::ToString(status.frame_plan.scene_stage.kind), + status.frame_plan.scene_stage.viewport_width, + status.frame_plan.scene_stage.viewport_height, + status.frame_plan.scene_stage.render_target_0, + status.frame_plan.scene_stage.depth_stencil, + status.frame_plan.scene_stage.draw_count, + status.frame_plan.scene_stage.resolve_count); + } + if (status.frame_plan.post_process_stage.valid) { + ImGui::Text( + "post stage: #%u %s vp=%ux%u rt0=%08X depth=%08X draws/resolves=%u/%u", + status.frame_plan.post_process_stage.pass_index, + ac6::renderer::ToString(status.frame_plan.post_process_stage.kind), + status.frame_plan.post_process_stage.viewport_width, + status.frame_plan.post_process_stage.viewport_height, + status.frame_plan.post_process_stage.render_target_0, + status.frame_plan.post_process_stage.depth_stencil, + status.frame_plan.post_process_stage.draw_count, + status.frame_plan.post_process_stage.resolve_count); + } + if (status.frame_plan.ui_stage.valid) { + ImGui::Text( + "ui stage: #%u %s vp=%ux%u rt0=%08X depth=%08X draws/resolves=%u/%u", + status.frame_plan.ui_stage.pass_index, + ac6::renderer::ToString(status.frame_plan.ui_stage.kind), + status.frame_plan.ui_stage.viewport_width, + status.frame_plan.ui_stage.viewport_height, + status.frame_plan.ui_stage.render_target_0, + status.frame_plan.ui_stage.depth_stencil, + status.frame_plan.ui_stage.draw_count, + status.frame_plan.ui_stage.resolve_count); + } + for (uint32_t i = 0; i < status.pass_diagnostics_count; ++i) { + const auto& pass = status.pass_diagnostics[i]; + if (!pass.valid) { + continue; + } + ImGui::Text( + "pass[%u]: #%u %s score=%u vp=%ux%u+%u+%u rt0=%08X depth=%08X d/c/r=%u/%u/%u tex/str/samp/fetch=%u/%u/%u/%u gpr=%u %s%s", + i, pass.pass_index, ac6::renderer::ToString(pass.kind), pass.score, + pass.viewport_width, pass.viewport_height, pass.viewport_x, pass.viewport_y, + pass.render_target_0, pass.depth_stencil, pass.draw_count, pass.clear_count, + pass.resolve_count, pass.max_texture_count, pass.max_stream_count, + pass.max_sampler_count, pass.max_fetch_constant_count, pass.max_shader_gpr_alloc, + pass.selected_for_present ? "present " : "", + pass.matches_frame_end_viewport ? "frame_end" : ""); + ImGui::Text( + " sig=%016llX tfetch=%016llX->%016llX bind=%016llX->%016llX", + static_cast(pass.pass_signature), + static_cast(pass.first_texture_fetch_layout_signature), + static_cast(pass.last_texture_fetch_layout_signature), + static_cast(pass.first_resource_binding_signature), + static_cast(pass.last_resource_binding_signature)); + const std::string first_textures = FormatBoundResourceSample(pass.first_textures); + const std::string last_textures = FormatBoundResourceSample(pass.last_textures); + const std::string first_fetch = FormatFetchConstantSample(pass.first_fetch_constants); + const std::string last_fetch = FormatFetchConstantSample(pass.last_fetch_constants); + const std::string rt0_object = FormatDecodedTextureObject(pass.render_target_0); + const std::string depth_object = FormatDecodedTextureObject(pass.depth_stencil); + const DecodedSampleSet<4> decoded_first_fetch = DecodeSampleSet(pass.first_fetch_constants); + const DecodedSampleSet<4> decoded_last_fetch = DecodeSampleSet(pass.last_fetch_constants); + const std::string source_passes = + FormatPassSources(decoded_first_fetch, decoded_last_fetch, status, i); + const std::string resolve_sources = + FormatResolveSources(decoded_first_fetch, decoded_last_fetch, status); + ImGui::Text(" tex %s -> %s", first_textures.c_str(), last_textures.c_str()); + ImGui::Text(" fet %s -> %s", first_fetch.c_str(), last_fetch.c_str()); + ImGui::Text(" rt0 %s depth %s", rt0_object.c_str(), depth_object.c_str()); + ImGui::Text(" src %s", source_passes.c_str()); + ImGui::Text(" rslv %s", resolve_sources.c_str()); + } + for (uint32_t i = 0; i < status.resolve_diagnostics_count; ++i) { + const auto& resolve = status.resolve_diagnostics[i]; + if (!resolve.valid) { + continue; + } + ImGui::Text( + "resolve[%u]: seq=%u vp=%ux%u rt0=%08X depth=%08X f1=%.3f", i, resolve.sequence, + resolve.viewport_width, resolve.viewport_height, resolve.render_target_0, + resolve.depth_stencil, resolve.depth_or_scale); + const std::string candidates = FormatResolveArgCandidates(resolve); + ImGui::Text(" %s", candidates.c_str()); + } ImGui::Separator(); ImGui::Text("swap source: %s", ac6::backend::ToString(diagnostics.swap_source)); @@ -90,10 +547,41 @@ void NativeGraphicsStatusDialog::OnDraw(ImGuiIO& io) { diagnostics.latest_signature.quarter_res_like ? "yes" : "no", diagnostics.latest_signature.point_sprite_like ? "yes" : "no", diagnostics.latest_signature.additive_like ? "yes" : "no"); + ImGui::Text("sampler hints: point min/mip=%u/%u linear min/mip=%u/%u mip clamp=%u max mip=%u", + diagnostics.latest_signature.point_min_sampler_count, + diagnostics.latest_signature.point_mip_sampler_count, + diagnostics.latest_signature.linear_min_sampler_count, + diagnostics.latest_signature.linear_mip_sampler_count, + diagnostics.latest_signature.mip_clamp_sampler_count, + diagnostics.latest_signature.max_sampler_mip_level); ImGui::TextWrapped("signature tags: %s", diagnostics.latest_signature_tags.empty() ? "none" : diagnostics.latest_signature_tags.c_str()); + const bool draw_scale_is_native = + status.draw_resolution_scale_x <= 1 && status.draw_resolution_scale_y <= 1; + const bool draw_scale_is_scaled = + status.draw_resolution_scale_x > 1 || status.draw_resolution_scale_y > 1; + const bool likely_point_filtered = + diagnostics.latest_signature.point_min_sampler_count != 0 || + diagnostics.latest_signature.point_mip_sampler_count != 0; + if (draw_scale_is_native) { + ImGui::TextWrapped( + "likely cause: guest output is still native 720p/1x, so particles and low-res effect " + "passes will look blocky on higher-resolution displays."); + } else if (!diagnostics.swap_source_scaled) { + ImGui::TextWrapped( + "likely cause: draw scaling is enabled, but the presented swap source is still " + "unscaled. This points to an unscaled resolve / swap-texture path."); + } else if (status.direct_host_resolve) { + ImGui::TextWrapped( + "likely cause: draw scaling is active and the swap source is scaled, so the failure is " + "likely in the scaled resolve path rather than scaling being ignored."); + } else if (diagnostics.latest_signature.point_sprite_like && likely_point_filtered) { + ImGui::TextWrapped( + "likely cause: the active effect pass looks like point-sprite rendering with point " + "sampling, so close sprites can stay pixelated even when they are near the camera."); + } ImGui::Text("authoritative VS / PS: %016llX / %016llX", static_cast(diagnostics.active_vertex_shader_hash), static_cast(diagnostics.active_pixel_shader_hash)); diff --git a/src/ac6_native_renderer/ac6_render_frontend.cpp b/src/ac6_native_renderer/ac6_render_frontend.cpp index 5bc4de8a..54ad2f74 100644 --- a/src/ac6_native_renderer/ac6_render_frontend.cpp +++ b/src/ac6_native_renderer/ac6_render_frontend.cpp @@ -5,6 +5,17 @@ namespace ac6::renderer { namespace { +void HashU32(uint64_t& hash, uint32_t value) { + constexpr uint64_t kFnvPrime = 1099511628211ull; + hash ^= value; + hash *= kFnvPrime; +} + +void HashU64(uint64_t& hash, uint64_t value) { + HashU32(hash, uint32_t(value & 0xFFFFFFFFull)); + HashU32(hash, uint32_t(value >> 32)); +} + template uint32_t CountNonZeroEntries(const Container& values) { uint32_t count = 0; @@ -16,6 +27,23 @@ uint32_t CountNonZeroEntries(const Container& values) { return count; } +template +ObservedPassDesc::BoundResourceSample SampleBoundEntries( + const std::array& values) { + ObservedPassDesc::BoundResourceSample sample; + for (uint32_t slot = 0; slot < values.size(); ++slot) { + if (!values[slot]) { + continue; + } + if (sample.count < sample.values.size()) { + sample.slots[sample.count] = slot; + sample.values[sample.count] = values[slot]; + } + ++sample.count; + } + return sample; +} + uint32_t CountBoundStreams( const std::array& streams) { uint32_t count = 0; @@ -273,9 +301,44 @@ FrontendFrameSummary Ac6RenderFrontend::BuildFromCapture( current_pass.viewport_y = event.shadow_state->viewport.y; current_pass.viewport_width = event.shadow_state->viewport.width; current_pass.viewport_height = event.shadow_state->viewport.height; + current_pass.first_vertex_fetch_layout_signature = + event.shadow_state->vertex_fetch_layout_signature; + current_pass.last_vertex_fetch_layout_signature = + event.shadow_state->vertex_fetch_layout_signature; + current_pass.first_texture_fetch_layout_signature = + event.shadow_state->texture_fetch_layout_signature; + current_pass.last_texture_fetch_layout_signature = + event.shadow_state->texture_fetch_layout_signature; + current_pass.first_resource_binding_signature = + event.shadow_state->resource_binding_signature; + current_pass.last_resource_binding_signature = + event.shadow_state->resource_binding_signature; + current_pass.first_textures = SampleBoundEntries(event.shadow_state->textures); + current_pass.last_textures = current_pass.first_textures; + current_pass.first_fetch_constants = + SampleBoundEntries(event.shadow_state->texture_fetch_ptrs); + current_pass.last_fetch_constants = current_pass.first_fetch_constants; + constexpr uint64_t kFnvOffsetBasis = 1469598103934665603ull; + current_pass.pass_signature = kFnvOffsetBasis; } current_pass.end_sequence = event.sequence; + HashU32(current_pass.pass_signature, static_cast(event.type)); + HashU32(current_pass.pass_signature, event.sequence); + HashU64(current_pass.pass_signature, event.shadow_state->vertex_fetch_layout_signature); + HashU64(current_pass.pass_signature, event.shadow_state->texture_fetch_layout_signature); + HashU64(current_pass.pass_signature, event.shadow_state->resource_binding_signature); + current_pass.last_vertex_fetch_layout_signature = + event.shadow_state->vertex_fetch_layout_signature; + current_pass.last_texture_fetch_layout_signature = + event.shadow_state->texture_fetch_layout_signature; + current_pass.last_resource_binding_signature = + event.shadow_state->resource_binding_signature; + current_pass.last_textures = SampleBoundEntries(event.shadow_state->textures); + current_pass.last_fetch_constants = + SampleBoundEntries(event.shadow_state->texture_fetch_ptrs); + current_pass.max_shader_gpr_alloc = + std::max(current_pass.max_shader_gpr_alloc, event.shadow_state->shader_gpr_alloc); switch (event.type) { case CapturedFrameEventType::kDraw: ++current_pass.draw_count; diff --git a/src/ac6_native_renderer/ac6_render_frontend.h b/src/ac6_native_renderer/ac6_render_frontend.h index d0243793..e9e798c9 100644 --- a/src/ac6_native_renderer/ac6_render_frontend.h +++ b/src/ac6_native_renderer/ac6_render_frontend.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -47,7 +48,14 @@ struct ObservedCommandDesc { }; struct ObservedPassDesc { + struct BoundResourceSample { + uint32_t count = 0; + std::array slots{}; + std::array values{}; + }; + ObservedPassKind kind = ObservedPassKind::kUnknown; + uint64_t pass_signature = 0; uint32_t start_sequence = 0; uint32_t end_sequence = 0; uint32_t draw_count = 0; @@ -61,12 +69,23 @@ struct ObservedPassDesc { uint32_t max_sampler_count = 0; uint32_t max_fetch_constant_count = 0; uint32_t max_draw_call_count = 0; + uint32_t max_shader_gpr_alloc = 0; uint32_t render_target_0 = 0; uint32_t depth_stencil = 0; uint32_t viewport_x = 0; uint32_t viewport_y = 0; uint32_t viewport_width = 0; uint32_t viewport_height = 0; + uint64_t first_vertex_fetch_layout_signature = 0; + uint64_t last_vertex_fetch_layout_signature = 0; + uint64_t first_texture_fetch_layout_signature = 0; + uint64_t last_texture_fetch_layout_signature = 0; + uint64_t first_resource_binding_signature = 0; + uint64_t last_resource_binding_signature = 0; + BoundResourceSample first_textures{}; + BoundResourceSample last_textures{}; + BoundResourceSample first_fetch_constants{}; + BoundResourceSample last_fetch_constants{}; bool selected_for_present = false; bool matches_frame_end_viewport = false; std::vector commands; diff --git a/src/d3d_hooks.cpp b/src/d3d_hooks.cpp index 9d422436..22198684 100644 --- a/src/d3d_hooks.cpp +++ b/src/d3d_hooks.cpp @@ -315,12 +315,22 @@ void CaptureClear(uint8_t* base, uint32_t device, uint32_t rect_count, uint32_t g_live_clears.push_back(std::move(record)); } -void CaptureResolve(uint32_t device) { +void CaptureResolve(uint32_t device, const PPCContext& ctx) { if (!REXCVAR_GET(ac6_render_capture)) { return; } ac6::d3d::ResolveRecord record; + record.args = { + ctx.r4.u32, + ctx.r5.u32, + ctx.r6.u32, + ctx.r7.u32, + ctx.r8.u32, + ctx.r9.u32, + ctx.r10.u32, + }; + record.depth_or_scale = static_cast(ctx.f1.f64); record.shadow_state = SnapshotShadowState(device); std::unique_lock lock(g_capture_mutex); @@ -530,7 +540,7 @@ PPC_FUNC_IMPL(rex_sub_821E2BB8) { RememberDevice(ctx.r3.u32); g_live_stats.resolve_calls.fetch_add(1, std::memory_order_relaxed); - CaptureResolve(ctx.r3.u32); + CaptureResolve(ctx.r3.u32, ctx); if (REXCVAR_GET(ac6_d3d_trace)) { REXLOG_CAT_TRACE(kLogGPU, "Resolve"); diff --git a/src/d3d_state.h b/src/d3d_state.h index 0ccfe3e4..00efeb53 100644 --- a/src/d3d_state.h +++ b/src/d3d_state.h @@ -149,6 +149,8 @@ struct ClearRecord { struct ResolveRecord { uint32_t sequence{0}; + std::array args{}; + float depth_or_scale{0.0f}; ShadowState shadow_state{}; }; diff --git a/src/main.cpp b/src/main.cpp index 96328d98..e73b7fe5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -45,12 +45,7 @@ std::unique_ptr Ac6recompAppCreate(rex::ui::WindowedAppCon // Force SDK logging to a file as well REXCVAR_SET(log_file, "ac6recomp.log"); REXCVAR_SET(log_level, "info"); - REXCVAR_SET(ac6_native_graphics_enabled, true); - REXCVAR_SET(ac6_graphics_mode, "hybrid_backend_fixes"); - REXCVAR_SET(ac6_experimental_replay_present, false); - REXCVAR_SET(ac6_render_capture, true); - REXCVAR_SET(ac6_timing_hooks_enabled, true); - REXCVAR_SET(ac6_unlock_fps, true); + REXCVAR_SET(ac6_unlock_fps, false); REXLOG_INFO("Ac6recompAppCreate: graphics mode={} replay_present={} capture={}", REXCVAR_GET(ac6_graphics_mode), diff --git a/thirdparty/rexglue-sdk/include/rex/graphics/pipeline/texture/cache.h b/thirdparty/rexglue-sdk/include/rex/graphics/pipeline/texture/cache.h index 537c7c0e..ebd6f9f8 100644 --- a/thirdparty/rexglue-sdk/include/rex/graphics/pipeline/texture/cache.h +++ b/thirdparty/rexglue-sdk/include/rex/graphics/pipeline/texture/cache.h @@ -561,6 +561,10 @@ class TextureCache { void UpdateTexturesTotalHostMemoryUsage(uint64_t add, uint64_t subtract); + // Updates the scaled-resolve bit in the key based on the current resolved + // guest-memory page tracking. + void ApplyScaledResolveState(TextureKey& key); + // Shared memory callback for texture data invalidation. static void WatchCallback(const std::unique_lock& global_lock, void* context, void* data, uint64_t argument, bool invalidated_by_gpu); diff --git a/thirdparty/rexglue-sdk/src/graphics/pipeline/texture/cache.cpp b/thirdparty/rexglue-sdk/src/graphics/pipeline/texture/cache.cpp index 50f25495..3c48b3e7 100644 --- a/thirdparty/rexglue-sdk/src/graphics/pipeline/texture/cache.cpp +++ b/thirdparty/rexglue-sdk/src/graphics/pipeline/texture/cache.cpp @@ -487,6 +487,9 @@ void TextureCache::RequestTextures(uint32_t used_texture_mask) { TextureKey old_key = binding.key; uint8_t old_swizzled_signs = binding.swizzled_signs; BindingInfoFromFetchConstant(fetch, binding.key, &binding.swizzled_signs); + if (binding.key.is_valid) { + ApplyScaledResolveState(binding.key); + } texture_bindings_in_sync_ |= index_bit; if (!binding.key.is_valid) { if (old_key.is_valid) { @@ -754,20 +757,26 @@ void TextureCache::DestroyAllTextures(bool from_destructor) { COUNT_profile_set("gpu/texture_cache/textures", 0); } -TextureCache::Texture* TextureCache::FindOrCreateTexture(TextureKey key) { - // Check if the texture is a scaled resolve texture. - if (IsDrawResolutionScaled() && key.tiled && IsScaledResolveSupportedForFormat(key)) { - texture_util::TextureGuestLayout scaled_resolve_guest_layout = key.GetGuestLayout(); - if ((scaled_resolve_guest_layout.base.level_data_extent_bytes && - IsRangeScaledResolved(key.base_page << 12, - scaled_resolve_guest_layout.base.level_data_extent_bytes)) || - (scaled_resolve_guest_layout.mips_total_extent_bytes && - IsRangeScaledResolved(key.mip_page << 12, - scaled_resolve_guest_layout.mips_total_extent_bytes))) { - key.scaled_resolve = 1; - } +void TextureCache::ApplyScaledResolveState(TextureKey& key) { + key.scaled_resolve = 0; + if (!IsDrawResolutionScaled() || !key.tiled || !IsScaledResolveSupportedForFormat(key)) { + return; } + texture_util::TextureGuestLayout scaled_resolve_guest_layout = key.GetGuestLayout(); + if ((scaled_resolve_guest_layout.base.level_data_extent_bytes && + IsRangeScaledResolved(key.base_page << 12, + scaled_resolve_guest_layout.base.level_data_extent_bytes)) || + (scaled_resolve_guest_layout.mips_total_extent_bytes && + IsRangeScaledResolved(key.mip_page << 12, + scaled_resolve_guest_layout.mips_total_extent_bytes))) { + key.scaled_resolve = 1; + } +} + +TextureCache::Texture* TextureCache::FindOrCreateTexture(TextureKey key) { + ApplyScaledResolveState(key); + auto get_host_extent = [this, &key](uint32_t& host_width_out, uint32_t& host_height_out, uint32_t& host_depth_or_array_size_out) { host_width_out = key.GetWidth();