Files
AC6_recomp/src/ac6_native_graphics.cpp
T

510 lines
20 KiB
C++

#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>
#include <rex/system/thread_state.h>
#include <rex/ui/d3d12/d3d12_provider.h>
#include "ac6_native_renderer/backends/d3d12_backend.h"
#include "ac6_native_renderer/native_renderer.h"
#include "d3d_hooks.h"
#include "render_hooks.h"
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_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_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",
"Legacy experimental replay backend preference");
REXCVAR_DEFINE_STRING(ac6_native_graphics_feature_level, "scene_submission", "AC6/NativeGraphics",
"Legacy experimental replay feature level (shipping is a legacy scaffold label)")
.allowed({"bootstrap", "scene_submission", "parity_validation", "shipping"});
REXCVAR_DEFINE_INT32(ac6_native_graphics_frames_in_flight, 2, "AC6/NativeGraphics",
"Legacy experimental replay max frames in flight")
.range(1, 4);
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") {
return GraphicsRuntimeMode::kDisabled;
}
if (value == "analysis_only") {
return GraphicsRuntimeMode::kAnalysisOnly;
}
if (value == "legacy_replay_experimental") {
return GraphicsRuntimeMode::kLegacyReplayExperimental;
}
return GraphicsRuntimeMode::kHybridBackendFixes;
}
ac6::renderer::FeatureLevel ParseFeatureLevel(std::string_view value) {
using ac6::renderer::FeatureLevel;
if (value == "scene_submission") {
return FeatureLevel::kSceneSubmission;
}
if (value == "parity_validation") {
return FeatureLevel::kParityValidation;
}
if (value == "shipping") {
return FeatureLevel::kShipping;
}
return FeatureLevel::kBootstrap;
}
bool IsReplayMode(const GraphicsRuntimeMode mode) {
return mode == GraphicsRuntimeMode::kLegacyReplayExperimental;
}
void ResetReplayStatus() {
g_runtime_status.initialized = false;
g_runtime_status.replay_frames_built = 0;
g_runtime_status.active_backend = ac6::renderer::BackendType::kUnknown;
g_runtime_status.renderer_stats = {};
g_runtime_status.replay_summary = {};
g_runtime_status.execution_summary = {};
g_runtime_status.executor_summary = {};
g_runtime_status.backend_executor_status = {};
g_runtime_status.latest_renderer_frame_index = 0;
g_runtime_status.last_meaningful_renderer_frame_index = 0;
g_runtime_status.showing_latched_snapshot = false;
}
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;
g_runtime_status.experimental_replay_present =
g_runtime_status.enabled &&
IsReplayMode(g_runtime_status.mode) &&
REXCVAR_GET(ac6_experimental_replay_present);
}
void RefreshRuntimeStatusFromRenderer() {
g_runtime_status.active_backend = g_native_renderer.GetStats().active_backend;
g_runtime_status.feature_level = g_native_renderer.feature_level();
g_runtime_status.renderer_stats = g_native_renderer.GetStats();
g_runtime_status.frontend_summary = g_native_renderer.frontend_summary();
g_runtime_status.replay_summary = g_native_renderer.replay_summary();
g_runtime_status.execution_summary = g_native_renderer.execution_summary();
g_runtime_status.executor_summary = g_native_renderer.executor_summary();
g_runtime_status.backend_executor_status = g_native_renderer.backend_executor_status();
g_runtime_status.frame_plan = g_native_renderer.frame_plan();
}
bool IsMeaningfulRendererSnapshot(
const ac6::d3d::FrameCaptureSummary& capture_summary,
const ac6::renderer::FrontendFrameSummary& frontend_summary,
const ac6::renderer::ReplayFrameSummary& replay_summary,
const ac6::renderer::ExecutionFrameSummary& execution_summary,
const ac6::renderer::ReplayExecutorFrameSummary& executor_summary,
const ac6::renderer::BackendExecutorStatus& backend_status) {
return capture_summary.draw_count != 0 || capture_summary.clear_count != 0 ||
capture_summary.resolve_count != 0 ||
frontend_summary.total_command_count != 0 ||
replay_summary.command_count != 0 ||
execution_summary.command_count != 0 ||
executor_summary.command_count != 0 ||
backend_status.draw_attempt_count != 0 ||
backend_status.clear_command_count != 0 ||
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) {
return;
}
g_native_renderer.Shutdown();
ResetReplayStatus();
}
bool EnsureExperimentalReplayInitialized(rex::memory::Memory* memory) {
if (!g_runtime_status.enabled || !IsReplayMode(g_runtime_status.mode)) {
return false;
}
if (g_runtime_status.initialized) {
return true;
}
++g_runtime_status.init_attempts;
auto* ts = rex::runtime::ThreadState::Get();
if (!ts || !ts->context() || !ts->context()->kernel_state) {
return false;
}
auto* graphics_system = ts->context()->kernel_state->graphics_system();
if (!graphics_system || !graphics_system->provider()) {
return false;
}
auto* d3d_provider =
dynamic_cast<rex::ui::d3d12::D3D12Provider*>(graphics_system->provider());
if (!d3d_provider) {
g_runtime_status.had_init_failure = true;
return false;
}
ID3D12Device* device = d3d_provider->GetDevice();
ID3D12CommandQueue* queue = d3d_provider->GetDirectQueue();
if (!device || !queue) {
return false;
}
ac6::renderer::NativeRendererConfig config;
config.preferred_backend = ac6::renderer::BackendType::kD3D12;
config.feature_level =
ParseFeatureLevel(REXCVAR_GET(ac6_native_graphics_feature_level));
config.max_frames_in_flight = static_cast<uint32_t>(
std::clamp(REXCVAR_GET(ac6_native_graphics_frames_in_flight), 1, 4));
config.enable_debug_markers = true;
config.enable_validation = false;
if (!g_native_renderer.InitializeShared(config, memory, device, queue)) {
g_runtime_status.had_init_failure = true;
return false;
}
g_d3d12_backend = g_native_renderer.GetD3D12Backend();
++g_runtime_status.init_successes;
g_runtime_status.initialized = true;
RefreshRuntimeStatusFromRenderer();
REXLOG_INFO("AC6 graphics: legacy experimental replay renderer initialized");
return true;
}
} // namespace
std::string_view ToString(const GraphicsRuntimeMode mode) {
switch (mode) {
case GraphicsRuntimeMode::kDisabled:
return "disabled";
case GraphicsRuntimeMode::kAnalysisOnly:
return "analysis_only";
case GraphicsRuntimeMode::kHybridBackendFixes:
return "hybrid_backend_fixes";
case GraphicsRuntimeMode::kLegacyReplayExperimental:
return "legacy_replay_experimental";
default:
return "unknown";
}
}
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();
ac6::backend::ShutdownDiagnostics();
return;
}
if (REXCVAR_GET(ac6_native_graphics_require_capture)) {
REXCVAR_SET(ac6_render_capture, true);
g_runtime_status.capture_enabled = true;
}
if (!IsReplayMode(g_runtime_status.mode)) {
ShutdownReplayRenderer();
}
ac6::d3d::OnFrameBoundary();
const ac6::d3d::FrameCaptureSnapshot frame_capture = ac6::d3d::GetFrameCapture();
const ac6::d3d::FrameCaptureSummary capture_summary = ac6::d3d::GetFrameCaptureSummary();
const ac6::d3d::ShadowState shadow_state = ac6::d3d::GetShadowState();
++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;
}
rex::system::GraphicsSwapSubmission swap_submission{};
uint64_t swap_sequence = 0;
uint64_t guest_vblank_interval_ticks = 0;
uint64_t last_guest_vblank_tick = 0;
rex::audio::AudioTelemetrySnapshot audio_telemetry{};
rex::audio::AudioClientTimingSnapshot audio_timing{};
const rex::audio::AudioTelemetrySnapshot* audio_telemetry_ptr = nullptr;
const rex::audio::AudioClientTimingSnapshot* audio_timing_ptr = nullptr;
auto* ts = rex::runtime::ThreadState::Get();
if (ts && ts->context() && ts->context()->kernel_state) {
auto* kernel_state = ts->context()->kernel_state;
if (auto* concrete_graphics =
dynamic_cast<rex::graphics::GraphicsSystem*>(kernel_state->graphics_system())) {
concrete_graphics->GetLastSwapSubmission(&swap_submission, &swap_sequence);
guest_vblank_interval_ticks = concrete_graphics->guest_vblank_interval_ticks();
last_guest_vblank_tick = concrete_graphics->last_vblank_interrupt_guest_tick();
}
if (auto* native_audio = kernel_state->native_audio_system()) {
audio_telemetry = native_audio->GetTelemetrySnapshot();
audio_telemetry_ptr = &audio_telemetry;
if (audio_telemetry.active_clients != 0) {
audio_timing = native_audio->GetClientTimingSnapshot(0);
audio_timing_ptr = &audio_timing;
}
}
}
ac6::backend::AnalyzeFrameBoundary(
frame_capture, capture_summary, shadow_state,
swap_sequence ? &swap_submission : nullptr, swap_sequence,
guest_vblank_interval_ticks, last_guest_vblank_tick, ac6::GetFrameStats(),
audio_telemetry_ptr, audio_timing_ptr);
g_runtime_status.backend_diagnostics = ac6::backend::GetDiagnosticsSnapshot();
if (!IsReplayMode(g_runtime_status.mode)) {
return;
}
if (!EnsureExperimentalReplayInitialized(memory)) {
return;
}
g_native_renderer.BeginFrame();
g_native_renderer.BuildCapturedFrame(frame_capture);
g_runtime_status.replay_frames_built = g_native_renderer.GetStats().frame_count;
g_runtime_status.latest_renderer_frame_index =
g_native_renderer.GetStats().frame_count;
const ac6::renderer::FrontendFrameSummary frontend_summary =
g_native_renderer.frontend_summary();
const ac6::renderer::ReplayFrameSummary replay_summary =
g_native_renderer.replay_summary();
const ac6::renderer::ExecutionFrameSummary execution_summary =
g_native_renderer.execution_summary();
const ac6::renderer::ReplayExecutorFrameSummary executor_summary =
g_native_renderer.executor_summary();
const ac6::renderer::BackendExecutorStatus backend_status =
g_native_renderer.backend_executor_status();
if (IsMeaningfulRendererSnapshot(capture_summary, frontend_summary, replay_summary,
execution_summary, executor_summary, backend_status) ||
g_runtime_status.last_meaningful_renderer_frame_index == 0) {
RefreshRuntimeStatusFromRenderer();
g_runtime_status.showing_latched_snapshot = false;
g_runtime_status.last_meaningful_renderer_frame_index =
g_runtime_status.latest_renderer_frame_index;
} else {
g_runtime_status.active_backend = g_native_renderer.GetStats().active_backend;
g_runtime_status.feature_level = g_native_renderer.feature_level();
g_runtime_status.renderer_stats = g_native_renderer.GetStats();
g_runtime_status.showing_latched_snapshot = true;
}
}
void Shutdown() {
g_captured_memory.store(nullptr, std::memory_order_release);
ShutdownReplayRenderer();
}
NativeGraphicsRuntimeStatus GetRuntimeStatus() {
SyncRuntimeFlags();
g_runtime_status.backend_diagnostics = ac6::backend::GetDiagnosticsSnapshot();
return g_runtime_status;
}
ID3D12Resource* GetNativeOutputTexture() {
SyncRuntimeFlags();
if (!g_runtime_status.enabled || !g_runtime_status.initialized ||
!IsReplayMode(g_runtime_status.mode) ||
!REXCVAR_GET(ac6_experimental_replay_present)) {
return nullptr;
}
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