Fix graphics diagnostics and restore default runtime settings

This commit is contained in:
salh
2026-04-20 23:13:53 +03:00
parent d6a1733648
commit 701eb202eb
10 changed files with 840 additions and 22 deletions
+171 -2
View File
@@ -1,10 +1,13 @@
#include "ac6_native_graphics.h"
#include <algorithm>
#include <atomic>
#include <cstdint>
#include <string_view>
#include <native/audio/audio_system.h>
#include <rex/cvar.h>
#include <rex/graphics/flags.h>
#include <rex/graphics/graphics_system.h>
#include <rex/logging.h>
#include <rex/system/kernel_state.h>
@@ -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<rex::memory::Memory*> 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<uint32_t>(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<int32_t>(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<int32_t>(shared_scale)
: REXCVAR_GET(draw_resolution_scale_y);
g_runtime_status.draw_resolution_scale_x =
static_cast<uint32_t>(std::max(INT32_C(1), configured_scale_x));
g_runtime_status.draw_resolution_scale_y =
static_cast<uint32_t>(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<uint32_t>(std::min<uint64_t>(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<ac6::renderer::ObservedPassDesc>& 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<RankedPass> 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<uint32_t>(static_cast<uint32_t>(ranked.size()),
static_cast<uint32_t>(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<uint32_t>(frame_capture.resolves.size());
const uint32_t count =
std::min<uint32_t>(total_resolves,
static_cast<uint32_t>(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
+59
View File
@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <cstdint>
#include <string_view>
@@ -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<uint32_t, 4> slots{};
std::array<uint32_t, 4> 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<uint32_t, 7> 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<PassDiagnosticsEntry, 6> pass_diagnostics{};
uint32_t resolve_diagnostics_count = 0;
std::array<ResolveDiagnosticsEntry, 8> 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
+488
View File
@@ -1,10 +1,359 @@
#include "ac6_native_graphics_overlay.h"
#include <algorithm>
#include <cstdio>
#include <string>
#include <imgui.h>
#include <rex/graphics/pipeline/texture/info.h>
#include <rex/graphics/xenos.h>
#include <rex/memory.h>
#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 <size_t N>
struct DecodedSampleSet {
uint32_t count = 0;
std::array<DecodedTextureFetch, N> 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<const uint8_t*>(guest_ptr);
if (fetch_base == nullptr) {
return false;
}
fetch.dword_0 = rex::memory::load_and_swap<uint32_t>(fetch_base + 0);
fetch.dword_1 = rex::memory::load_and_swap<uint32_t>(fetch_base + 4);
fetch.dword_2 = rex::memory::load_and_swap<uint32_t>(fetch_base + 8);
fetch.dword_3 = rex::memory::load_and_swap<uint32_t>(fetch_base + 12);
fetch.dword_4 = rex::memory::load_and_swap<uint32_t>(fetch_base + 16);
fetch.dword_5 = rex::memory::load_and_swap<uint32_t>(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<uint32_t>(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 <typename Sample>
DecodedSampleSet<4> DecodeSampleSet(const Sample& sample) {
DecodedSampleSet<4> decoded_set;
const uint32_t shown =
std::min<uint32_t>(sample.count, static_cast<uint32_t>(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 <size_t N>
bool SampleUsesBaseAddress(const DecodedSampleSet<N>& 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 <size_t N>
std::string FormatPassSources(const DecodedSampleSet<N>& first_fetch,
const DecodedSampleSet<N>& 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 <size_t N>
std::string FormatResolveSources(const DecodedSampleSet<N>& first_fetch,
const DecodedSampleSet<N>& 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 <typename Sample>
std::string FormatBoundResourceSample(const Sample& sample) {
if (sample.count == 0) {
return "none";
}
std::string formatted;
const uint32_t shown = std::min<uint32_t>(sample.count, static_cast<uint32_t>(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 <typename Sample>
std::string FormatFetchConstantSample(const Sample& sample) {
if (sample.count == 0) {
return "none";
}
std::string formatted;
const uint32_t shown = std::min<uint32_t>(sample.count, static_cast<uint32_t>(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<unsigned long long>(pass.pass_signature),
static_cast<unsigned long long>(pass.first_texture_fetch_layout_signature),
static_cast<unsigned long long>(pass.last_texture_fetch_layout_signature),
static_cast<unsigned long long>(pass.first_resource_binding_signature),
static_cast<unsigned long long>(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<unsigned long long>(diagnostics.active_vertex_shader_hash),
static_cast<unsigned long long>(diagnostics.active_pixel_shader_hash));
@@ -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 <typename Container>
uint32_t CountNonZeroEntries(const Container& values) {
uint32_t count = 0;
@@ -16,6 +27,23 @@ uint32_t CountNonZeroEntries(const Container& values) {
return count;
}
template <size_t N>
ObservedPassDesc::BoundResourceSample SampleBoundEntries(
const std::array<uint32_t, N>& 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<ac6::d3d::StreamBinding, ac6::d3d::kMaxStreams>& 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<uint32_t>(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;
@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <cstdint>
#include <vector>
@@ -47,7 +48,14 @@ struct ObservedCommandDesc {
};
struct ObservedPassDesc {
struct BoundResourceSample {
uint32_t count = 0;
std::array<uint32_t, 4> slots{};
std::array<uint32_t, 4> 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<ObservedCommandDesc> commands;
+12 -2
View File
@@ -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<float>(ctx.f1.f64);
record.shadow_state = SnapshotShadowState(device);
std::unique_lock<std::shared_mutex> 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");
+2
View File
@@ -149,6 +149,8 @@ struct ClearRecord {
struct ResolveRecord {
uint32_t sequence{0};
std::array<uint32_t, 7> args{};
float depth_or_scale{0.0f};
ShadowState shadow_state{};
};
+1 -6
View File
@@ -45,12 +45,7 @@ std::unique_ptr<rex::ui::WindowedApp> 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),
@@ -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<std::recursive_mutex>& global_lock,
void* context, void* data, uint64_t argument, bool invalidated_by_gpu);
@@ -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();