mirror of
https://github.com/sal063/AC6_recomp
synced 2026-08-14 20:44:00 -04:00
Refactor native graphics backend and fix build errors
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
#include "ac6_backend_capture_bridge.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
namespace ac6::backend {
|
||||
namespace {
|
||||
|
||||
template <typename Container>
|
||||
uint32_t CountNonZero(const Container& values) {
|
||||
uint32_t count = 0;
|
||||
for (const auto& value : values) {
|
||||
if (value) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
uint32_t CountBoundStreams(
|
||||
const std::array<ac6::d3d::StreamBinding, ac6::d3d::kMaxStreams>& streams) {
|
||||
uint32_t count = 0;
|
||||
for (const auto& stream : streams) {
|
||||
if (stream.buffer) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
uint32_t CountBoundSamplers(
|
||||
const std::array<ac6::d3d::SamplerBinding, ac6::d3d::kMaxSamplers>& samplers) {
|
||||
uint32_t count = 0;
|
||||
for (const auto& sampler : samplers) {
|
||||
if (sampler.mag_filter || sampler.min_filter || sampler.mip_filter ||
|
||||
sampler.mip_level || sampler.border_color) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
bool IsHalfResLike(const ac6::d3d::ShadowState& shadow_state,
|
||||
const rex::system::GraphicsSwapSubmission* swap_submission) {
|
||||
if (!swap_submission || !swap_submission->frontbuffer_width ||
|
||||
!swap_submission->frontbuffer_height || !shadow_state.viewport.width ||
|
||||
!shadow_state.viewport.height) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t swap_width = swap_submission->frontbuffer_width;
|
||||
const uint32_t swap_height = swap_submission->frontbuffer_height;
|
||||
return shadow_state.viewport.width * 4 <= swap_width * 3 ||
|
||||
shadow_state.viewport.height * 4 <= swap_height * 3;
|
||||
}
|
||||
|
||||
bool IsLikelyUiPass(const ac6::d3d::FrameCaptureSummary& capture_summary,
|
||||
const ac6::d3d::ShadowState& shadow_state,
|
||||
const rex::system::GraphicsSwapSubmission* swap_submission) {
|
||||
if (shadow_state.depth_stencil != 0 || !swap_submission ||
|
||||
!swap_submission->frontbuffer_width || !swap_submission->frontbuffer_height) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool viewport_matches_swap =
|
||||
shadow_state.viewport.width == swap_submission->frontbuffer_width &&
|
||||
shadow_state.viewport.height == swap_submission->frontbuffer_height;
|
||||
const bool low_complexity =
|
||||
capture_summary.draw_count > 0 && capture_summary.draw_count <= 96 &&
|
||||
capture_summary.resolve_count == 0;
|
||||
return viewport_matches_swap && low_complexity;
|
||||
}
|
||||
|
||||
bool IsLikelyParticlePass(const ac6::d3d::FrameCaptureSummary& capture_summary,
|
||||
const ac6::d3d::ShadowState& shadow_state) {
|
||||
const bool mostly_primitive =
|
||||
capture_summary.primitive_draw_count > capture_summary.indexed_draw_count &&
|
||||
capture_summary.primitive_draw_count > 0;
|
||||
const bool light_bindings =
|
||||
CountBoundSamplers(shadow_state.samplers) <= 4 &&
|
||||
CountBoundStreams(shadow_state.streams) <= 4;
|
||||
return mostly_primitive || (capture_summary.resolve_count > 0 && light_bindings &&
|
||||
shadow_state.depth_stencil == 0);
|
||||
}
|
||||
|
||||
bool IsLikelyAdditive(const ac6::d3d::FrameCaptureSummary& capture_summary,
|
||||
const ac6::d3d::ShadowState& shadow_state) {
|
||||
if (shadow_state.depth_stencil != 0) {
|
||||
return false;
|
||||
}
|
||||
return capture_summary.clear_count == 0 &&
|
||||
CountNonZero(shadow_state.textures) > 0 &&
|
||||
CountBoundSamplers(shadow_state.samplers) > 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint64_t HashSwapTextureFetch(const rex::system::GraphicsSwapSubmission& submission) {
|
||||
constexpr uint64_t kFnvOffsetBasis = 1469598103934665603ull;
|
||||
uint64_t hash = kFnvOffsetBasis;
|
||||
for (uint32_t word : submission.texture_fetch) {
|
||||
HashU32(hash, word);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
RenderEventSignature BuildRenderEventSignature(
|
||||
const ac6::d3d::FrameCaptureSnapshot& frame_capture,
|
||||
const ac6::d3d::FrameCaptureSummary& capture_summary,
|
||||
const ac6::d3d::ShadowState& shadow_state,
|
||||
const rex::system::GraphicsSwapSubmission* swap_submission,
|
||||
const uint64_t active_vertex_shader_hash,
|
||||
const uint64_t active_pixel_shader_hash) {
|
||||
constexpr uint64_t kFnvOffsetBasis = 1469598103934665603ull;
|
||||
(void)frame_capture;
|
||||
|
||||
RenderEventSignature signature;
|
||||
signature.capture_record_signature = capture_summary.record_signature;
|
||||
signature.swap_texture_fetch_signature =
|
||||
swap_submission ? HashSwapTextureFetch(*swap_submission) : 0;
|
||||
signature.render_target_0 = shadow_state.render_targets[0];
|
||||
signature.depth_stencil = shadow_state.depth_stencil;
|
||||
signature.viewport_width = shadow_state.viewport.width;
|
||||
signature.viewport_height = shadow_state.viewport.height;
|
||||
signature.draw_count = capture_summary.draw_count;
|
||||
signature.clear_count = capture_summary.clear_count;
|
||||
signature.resolve_count = capture_summary.resolve_count;
|
||||
signature.indexed_draw_count = capture_summary.indexed_draw_count;
|
||||
signature.primitive_draw_count = capture_summary.primitive_draw_count;
|
||||
signature.texture_count = CountNonZero(shadow_state.textures);
|
||||
signature.sampler_count = CountBoundSamplers(shadow_state.samplers);
|
||||
signature.stream_count = CountBoundStreams(shadow_state.streams);
|
||||
signature.fetch_constant_count = CountNonZero(shadow_state.texture_fetch_ptrs);
|
||||
signature.shader_gpr_alloc = shadow_state.shader_gpr_alloc;
|
||||
signature.active_vertex_shader_hash = active_vertex_shader_hash;
|
||||
signature.active_pixel_shader_hash = active_pixel_shader_hash;
|
||||
signature.has_depth_stencil = shadow_state.depth_stencil != 0;
|
||||
signature.has_resolve = capture_summary.resolve_count != 0;
|
||||
signature.half_res_like = IsHalfResLike(shadow_state, swap_submission);
|
||||
signature.post_process_like =
|
||||
signature.has_resolve && !signature.has_depth_stencil;
|
||||
signature.ui_like =
|
||||
IsLikelyUiPass(capture_summary, shadow_state, swap_submission);
|
||||
signature.particle_like =
|
||||
IsLikelyParticlePass(capture_summary, shadow_state);
|
||||
signature.additive_like =
|
||||
IsLikelyAdditive(capture_summary, shadow_state);
|
||||
|
||||
uint64_t hash = kFnvOffsetBasis;
|
||||
HashU64(hash, signature.capture_record_signature);
|
||||
HashU64(hash, signature.swap_texture_fetch_signature);
|
||||
HashU32(hash, signature.render_target_0);
|
||||
HashU32(hash, signature.depth_stencil);
|
||||
HashU32(hash, signature.viewport_width);
|
||||
HashU32(hash, signature.viewport_height);
|
||||
HashU32(hash, signature.draw_count);
|
||||
HashU32(hash, signature.resolve_count);
|
||||
HashU32(hash, signature.texture_count);
|
||||
HashU32(hash, signature.sampler_count);
|
||||
HashU32(hash, signature.stream_count);
|
||||
HashU32(hash, signature.fetch_constant_count);
|
||||
HashU32(hash, signature.shader_gpr_alloc);
|
||||
HashU64(hash, shadow_state.vertex_fetch_layout_signature);
|
||||
HashU64(hash, shadow_state.texture_fetch_layout_signature);
|
||||
HashU64(hash, shadow_state.resource_binding_signature);
|
||||
signature.stable_id = hash;
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
std::string BuildSignatureTags(const RenderEventSignature& signature) {
|
||||
std::string tags;
|
||||
auto append = [&tags](const char* token) {
|
||||
if (!tags.empty()) {
|
||||
tags.append(", ");
|
||||
}
|
||||
tags.append(token);
|
||||
};
|
||||
|
||||
if (signature.has_depth_stencil) {
|
||||
append("depth");
|
||||
}
|
||||
if (signature.has_resolve) {
|
||||
append("resolve");
|
||||
}
|
||||
if (signature.half_res_like) {
|
||||
append("half_res");
|
||||
}
|
||||
if (signature.post_process_like) {
|
||||
append("post");
|
||||
}
|
||||
if (signature.ui_like) {
|
||||
append("ui");
|
||||
}
|
||||
if (signature.particle_like) {
|
||||
append("particles");
|
||||
}
|
||||
if (signature.additive_like) {
|
||||
append("additive");
|
||||
}
|
||||
if (tags.empty()) {
|
||||
tags = "unclassified";
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
} // namespace ac6::backend
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include <rex/system/interfaces/graphics.h>
|
||||
|
||||
#include "../d3d_state.h"
|
||||
|
||||
namespace ac6::backend {
|
||||
|
||||
enum class SignatureClass : uint8_t {
|
||||
kUnknown,
|
||||
kScene,
|
||||
kPostProcess,
|
||||
kUiComposite,
|
||||
kParticles,
|
||||
kClouds,
|
||||
kSmoke,
|
||||
kExplosions,
|
||||
kMissileTrails,
|
||||
};
|
||||
|
||||
struct RenderEventSignature {
|
||||
uint64_t stable_id = 0;
|
||||
uint64_t capture_record_signature = 0;
|
||||
uint64_t swap_texture_fetch_signature = 0;
|
||||
uint32_t render_target_0 = 0;
|
||||
uint32_t depth_stencil = 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 indexed_draw_count = 0;
|
||||
uint32_t primitive_draw_count = 0;
|
||||
uint32_t texture_count = 0;
|
||||
uint32_t sampler_count = 0;
|
||||
uint32_t stream_count = 0;
|
||||
uint32_t fetch_constant_count = 0;
|
||||
uint32_t shader_gpr_alloc = 0;
|
||||
uint64_t active_vertex_shader_hash = 0;
|
||||
uint64_t active_pixel_shader_hash = 0;
|
||||
bool has_depth_stencil = false;
|
||||
bool has_resolve = false;
|
||||
bool half_res_like = false;
|
||||
bool post_process_like = false;
|
||||
bool ui_like = false;
|
||||
bool particle_like = false;
|
||||
bool additive_like = false;
|
||||
SignatureClass classification = SignatureClass::kUnknown;
|
||||
};
|
||||
|
||||
uint64_t HashSwapTextureFetch(const rex::system::GraphicsSwapSubmission& submission);
|
||||
|
||||
RenderEventSignature BuildRenderEventSignature(
|
||||
const ac6::d3d::FrameCaptureSnapshot& frame_capture,
|
||||
const ac6::d3d::FrameCaptureSummary& capture_summary,
|
||||
const ac6::d3d::ShadowState& shadow_state,
|
||||
const rex::system::GraphicsSwapSubmission* swap_submission,
|
||||
uint64_t active_vertex_shader_hash,
|
||||
uint64_t active_pixel_shader_hash);
|
||||
|
||||
std::string BuildSignatureTags(const RenderEventSignature& signature);
|
||||
|
||||
} // namespace ac6::backend
|
||||
@@ -0,0 +1,197 @@
|
||||
#include "ac6_backend_hooks.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <rex/logging.h>
|
||||
|
||||
#include "ac6_backend_pass_classifier.h"
|
||||
#include "render_hooks.h"
|
||||
|
||||
REXCVAR_DEFINE_BOOL(ac6_backend_debug_swap, false, "AC6/Backend",
|
||||
"Log AC6 swap-path diagnostics from the authoritative backend");
|
||||
REXCVAR_DEFINE_BOOL(ac6_backend_log_signatures, false, "AC6/Backend",
|
||||
"Log AC6 capture signatures used for backend-fix routing");
|
||||
|
||||
namespace ac6::backend {
|
||||
namespace {
|
||||
|
||||
std::mutex g_snapshot_mutex;
|
||||
BackendDiagnosticsSnapshot g_snapshot{};
|
||||
std::unordered_map<uint64_t, uint32_t> g_signature_hits;
|
||||
|
||||
bool ShouldLogSignature(const BackendDiagnosticsSnapshot& snapshot) {
|
||||
if (!REXCVAR_GET(ac6_backend_log_signatures) || snapshot.latest_signature.stable_id == 0) {
|
||||
return false;
|
||||
}
|
||||
return snapshot.repeated_signature_count == 1 ||
|
||||
(snapshot.repeated_signature_count % 32) == 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AnalyzeFrameBoundary(
|
||||
const ac6::d3d::FrameCaptureSnapshot& frame_capture,
|
||||
const ac6::d3d::FrameCaptureSummary& capture_summary,
|
||||
const ac6::d3d::ShadowState& shadow_state,
|
||||
const rex::system::GraphicsSwapSubmission* swap_submission,
|
||||
const uint64_t swap_submission_sequence,
|
||||
const uint64_t guest_vblank_interval_ticks,
|
||||
const uint64_t last_guest_vblank_tick,
|
||||
const ac6::FrameStats& frame_stats,
|
||||
const rex::audio::AudioTelemetrySnapshot* audio_telemetry,
|
||||
const rex::audio::AudioClientTimingSnapshot* audio_timing) {
|
||||
std::lock_guard<std::mutex> lock(g_snapshot_mutex);
|
||||
|
||||
const uint64_t previous_swap_sequence = g_snapshot.swap_submission_sequence;
|
||||
g_snapshot.valid = true;
|
||||
g_snapshot.frame_index = capture_summary.frame_index;
|
||||
g_snapshot.swap_submission_sequence = swap_submission_sequence;
|
||||
g_snapshot.swap_submission_valid = swap_submission != nullptr;
|
||||
g_snapshot.guest_vblank_interval_ticks = guest_vblank_interval_ticks;
|
||||
g_snapshot.last_guest_vblank_tick = last_guest_vblank_tick;
|
||||
g_snapshot.host_frame_time_ms = frame_stats.frame_time_ms;
|
||||
g_snapshot.host_fps = frame_stats.fps;
|
||||
g_snapshot.host_frame_count = frame_stats.frame_count;
|
||||
g_snapshot.capture_draw_count = capture_summary.draw_count;
|
||||
g_snapshot.capture_clear_count = capture_summary.clear_count;
|
||||
g_snapshot.capture_resolve_count = capture_summary.resolve_count;
|
||||
|
||||
if (swap_submission) {
|
||||
g_snapshot.frontbuffer_width = swap_submission->frontbuffer_width;
|
||||
g_snapshot.frontbuffer_height = swap_submission->frontbuffer_height;
|
||||
g_snapshot.texture_format = swap_submission->texture_format;
|
||||
g_snapshot.color_space = swap_submission->color_space;
|
||||
} else {
|
||||
g_snapshot.frontbuffer_width = 0;
|
||||
g_snapshot.frontbuffer_height = 0;
|
||||
g_snapshot.texture_format = 0;
|
||||
g_snapshot.color_space = 0;
|
||||
}
|
||||
|
||||
if (swap_submission_sequence != previous_swap_sequence) {
|
||||
g_snapshot.swap_source = SwapSourceType::kUnknown;
|
||||
g_snapshot.active_vertex_shader_hash = 0;
|
||||
g_snapshot.active_pixel_shader_hash = 0;
|
||||
}
|
||||
|
||||
if (audio_telemetry) {
|
||||
g_snapshot.audio_active_clients = audio_telemetry->active_clients;
|
||||
g_snapshot.audio_queued_frames = audio_telemetry->queued_frames;
|
||||
g_snapshot.audio_peak_queued_frames = audio_telemetry->peak_queued_frames;
|
||||
g_snapshot.audio_dropped_frames = audio_telemetry->dropped_frames;
|
||||
g_snapshot.audio_underruns = audio_telemetry->underruns;
|
||||
g_snapshot.audio_silence_injections = audio_telemetry->silence_injections;
|
||||
g_snapshot.audio_backend_name = audio_telemetry->backend_name;
|
||||
} else {
|
||||
g_snapshot.audio_active_clients = 0;
|
||||
g_snapshot.audio_queued_frames = 0;
|
||||
g_snapshot.audio_peak_queued_frames = 0;
|
||||
g_snapshot.audio_dropped_frames = 0;
|
||||
g_snapshot.audio_underruns = 0;
|
||||
g_snapshot.audio_silence_injections = 0;
|
||||
g_snapshot.audio_backend_name.clear();
|
||||
}
|
||||
|
||||
if (audio_timing) {
|
||||
g_snapshot.audio_timing_valid = true;
|
||||
g_snapshot.audio_consumed_frames = audio_timing->consumed_frames;
|
||||
g_snapshot.audio_submitted_tic = audio_timing->submitted_tic;
|
||||
g_snapshot.audio_host_elapsed_tic = audio_timing->host_elapsed_tic;
|
||||
g_snapshot.audio_startup_inflight_frames = audio_timing->startup_inflight_frames;
|
||||
g_snapshot.audio_callback_dispatch_count = audio_timing->callback_dispatch_count;
|
||||
g_snapshot.audio_callback_throttle_count = audio_timing->callback_throttle_count;
|
||||
} else {
|
||||
g_snapshot.audio_timing_valid = false;
|
||||
g_snapshot.audio_consumed_frames = 0;
|
||||
g_snapshot.audio_submitted_tic = 0;
|
||||
g_snapshot.audio_host_elapsed_tic = 0;
|
||||
g_snapshot.audio_startup_inflight_frames = 0;
|
||||
g_snapshot.audio_callback_dispatch_count = 0;
|
||||
g_snapshot.audio_callback_throttle_count = 0;
|
||||
}
|
||||
|
||||
g_snapshot.latest_signature = BuildRenderEventSignature(
|
||||
frame_capture, capture_summary, shadow_state, swap_submission,
|
||||
g_snapshot.active_vertex_shader_hash, g_snapshot.active_pixel_shader_hash);
|
||||
g_snapshot.latest_signature.classification =
|
||||
ClassifySignature(g_snapshot.latest_signature);
|
||||
g_snapshot.latest_signature_tags = BuildSignatureTags(g_snapshot.latest_signature);
|
||||
g_snapshot.repeated_signature_count =
|
||||
++g_signature_hits[g_snapshot.latest_signature.stable_id];
|
||||
|
||||
if (ShouldLogSignature(g_snapshot)) {
|
||||
REXLOG_INFO(
|
||||
"AC6 backend signature frame={} class={} id={:016X} hits={} tags={} draws={} resolves={}",
|
||||
g_snapshot.frame_index, ToString(g_snapshot.latest_signature.classification),
|
||||
g_snapshot.latest_signature.stable_id, g_snapshot.repeated_signature_count,
|
||||
g_snapshot.latest_signature_tags, g_snapshot.capture_draw_count,
|
||||
g_snapshot.capture_resolve_count);
|
||||
}
|
||||
}
|
||||
|
||||
void ReportSwapDecision(const rex::system::GraphicsSwapSubmission& submission,
|
||||
const uint64_t submission_sequence,
|
||||
const SwapSourceType swap_source,
|
||||
const bool swap_source_scaled,
|
||||
const uint32_t guest_output_width,
|
||||
const uint32_t guest_output_height,
|
||||
const uint32_t source_width,
|
||||
const uint32_t source_height,
|
||||
const uint64_t active_vertex_shader_hash,
|
||||
const uint64_t active_pixel_shader_hash) {
|
||||
std::lock_guard<std::mutex> lock(g_snapshot_mutex);
|
||||
|
||||
g_snapshot.valid = true;
|
||||
g_snapshot.swap_submission_valid = true;
|
||||
g_snapshot.swap_submission_sequence = submission_sequence;
|
||||
g_snapshot.swap_source = swap_source;
|
||||
g_snapshot.swap_source_scaled = swap_source_scaled;
|
||||
g_snapshot.guest_output_width = guest_output_width;
|
||||
g_snapshot.guest_output_height = guest_output_height;
|
||||
g_snapshot.source_width = source_width;
|
||||
g_snapshot.source_height = source_height;
|
||||
g_snapshot.frontbuffer_width = submission.frontbuffer_width;
|
||||
g_snapshot.frontbuffer_height = submission.frontbuffer_height;
|
||||
g_snapshot.texture_format = submission.texture_format;
|
||||
g_snapshot.color_space = submission.color_space;
|
||||
g_snapshot.active_vertex_shader_hash = active_vertex_shader_hash;
|
||||
g_snapshot.active_pixel_shader_hash = active_pixel_shader_hash;
|
||||
g_snapshot.latest_signature.active_vertex_shader_hash = active_vertex_shader_hash;
|
||||
g_snapshot.latest_signature.active_pixel_shader_hash = active_pixel_shader_hash;
|
||||
|
||||
if (REXCVAR_GET(ac6_backend_debug_swap)) {
|
||||
REXLOG_INFO(
|
||||
"AC6 swap source={} guest={}x{} source={}x{} scaled={} vs={:016X} ps={:016X}",
|
||||
ToString(swap_source), guest_output_width, guest_output_height, source_width,
|
||||
source_height, swap_source_scaled ? "yes" : "no", active_vertex_shader_hash,
|
||||
active_pixel_shader_hash);
|
||||
}
|
||||
}
|
||||
|
||||
BackendDiagnosticsSnapshot GetDiagnosticsSnapshot() {
|
||||
std::lock_guard<std::mutex> lock(g_snapshot_mutex);
|
||||
return g_snapshot;
|
||||
}
|
||||
|
||||
void ShutdownDiagnostics() {
|
||||
std::lock_guard<std::mutex> lock(g_snapshot_mutex);
|
||||
g_snapshot = {};
|
||||
g_signature_hits.clear();
|
||||
}
|
||||
|
||||
const char* ToString(const SwapSourceType swap_source) {
|
||||
switch (swap_source) {
|
||||
case SwapSourceType::kGuestSwapTexture:
|
||||
return "guest_swap_texture";
|
||||
case SwapSourceType::kDirectDisplayFallback:
|
||||
return "direct_display_fallback";
|
||||
case SwapSourceType::kExperimentalReplayOverride:
|
||||
return "experimental_replay_override";
|
||||
case SwapSourceType::kUnknown:
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ac6::backend
|
||||
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include <native/audio/audio_runtime.h>
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/system/interfaces/graphics.h>
|
||||
|
||||
#include "ac6_backend_capture_bridge.h"
|
||||
#include "ac6_backend_pass_classifier.h"
|
||||
#include "../d3d_state.h"
|
||||
|
||||
REXCVAR_DECLARE(bool, ac6_backend_debug_swap);
|
||||
REXCVAR_DECLARE(bool, ac6_backend_log_signatures);
|
||||
|
||||
namespace ac6 {
|
||||
struct FrameStats;
|
||||
}
|
||||
|
||||
namespace ac6::backend {
|
||||
|
||||
enum class SwapSourceType : uint8_t {
|
||||
kUnknown,
|
||||
kGuestSwapTexture,
|
||||
kDirectDisplayFallback,
|
||||
kExperimentalReplayOverride,
|
||||
};
|
||||
|
||||
struct BackendDiagnosticsSnapshot {
|
||||
bool valid = false;
|
||||
uint64_t frame_index = 0;
|
||||
uint64_t swap_submission_sequence = 0;
|
||||
bool swap_submission_valid = false;
|
||||
SwapSourceType swap_source = SwapSourceType::kUnknown;
|
||||
bool swap_source_scaled = false;
|
||||
uint32_t source_width = 0;
|
||||
uint32_t source_height = 0;
|
||||
uint32_t guest_output_width = 0;
|
||||
uint32_t guest_output_height = 0;
|
||||
uint32_t frontbuffer_width = 0;
|
||||
uint32_t frontbuffer_height = 0;
|
||||
uint32_t texture_format = 0;
|
||||
uint32_t color_space = 0;
|
||||
uint32_t audio_active_clients = 0;
|
||||
uint32_t audio_queued_frames = 0;
|
||||
uint32_t audio_peak_queued_frames = 0;
|
||||
uint32_t audio_dropped_frames = 0;
|
||||
uint32_t audio_underruns = 0;
|
||||
uint32_t audio_silence_injections = 0;
|
||||
uint32_t audio_startup_inflight_frames = 0;
|
||||
uint32_t audio_callback_dispatch_count = 0;
|
||||
uint32_t audio_callback_throttle_count = 0;
|
||||
uint64_t active_vertex_shader_hash = 0;
|
||||
uint64_t active_pixel_shader_hash = 0;
|
||||
uint64_t guest_vblank_interval_ticks = 0;
|
||||
uint64_t last_guest_vblank_tick = 0;
|
||||
uint64_t audio_consumed_frames = 0;
|
||||
uint64_t audio_submitted_tic = 0;
|
||||
uint64_t audio_host_elapsed_tic = 0;
|
||||
double host_frame_time_ms = 0.0;
|
||||
double host_fps = 0.0;
|
||||
uint64_t host_frame_count = 0;
|
||||
uint32_t capture_draw_count = 0;
|
||||
uint32_t capture_clear_count = 0;
|
||||
uint32_t capture_resolve_count = 0;
|
||||
uint32_t repeated_signature_count = 0;
|
||||
bool audio_timing_valid = false;
|
||||
std::string audio_backend_name;
|
||||
std::string latest_signature_tags;
|
||||
RenderEventSignature latest_signature{};
|
||||
};
|
||||
|
||||
void AnalyzeFrameBoundary(
|
||||
const ac6::d3d::FrameCaptureSnapshot& frame_capture,
|
||||
const ac6::d3d::FrameCaptureSummary& capture_summary,
|
||||
const ac6::d3d::ShadowState& shadow_state,
|
||||
const rex::system::GraphicsSwapSubmission* swap_submission,
|
||||
uint64_t swap_submission_sequence,
|
||||
uint64_t guest_vblank_interval_ticks,
|
||||
uint64_t last_guest_vblank_tick,
|
||||
const ac6::FrameStats& frame_stats,
|
||||
const rex::audio::AudioTelemetrySnapshot* audio_telemetry,
|
||||
const rex::audio::AudioClientTimingSnapshot* audio_timing);
|
||||
|
||||
void ReportSwapDecision(const rex::system::GraphicsSwapSubmission& submission,
|
||||
uint64_t submission_sequence,
|
||||
SwapSourceType swap_source,
|
||||
bool swap_source_scaled,
|
||||
uint32_t guest_output_width,
|
||||
uint32_t guest_output_height,
|
||||
uint32_t source_width,
|
||||
uint32_t source_height,
|
||||
uint64_t active_vertex_shader_hash,
|
||||
uint64_t active_pixel_shader_hash);
|
||||
|
||||
BackendDiagnosticsSnapshot GetDiagnosticsSnapshot();
|
||||
void ShutdownDiagnostics();
|
||||
|
||||
const char* ToString(SwapSourceType swap_source);
|
||||
|
||||
} // namespace ac6::backend
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "ac6_backend_pass_classifier.h"
|
||||
|
||||
namespace ac6::backend {
|
||||
|
||||
SignatureClass ClassifySignature(const RenderEventSignature& signature) {
|
||||
if (signature.ui_like) {
|
||||
return SignatureClass::kUiComposite;
|
||||
}
|
||||
if (signature.particle_like && signature.additive_like &&
|
||||
signature.viewport_width && signature.viewport_height &&
|
||||
signature.viewport_width >= signature.viewport_height * 2) {
|
||||
return SignatureClass::kMissileTrails;
|
||||
}
|
||||
if (signature.half_res_like && signature.post_process_like &&
|
||||
signature.sampler_count >= 4 && signature.fetch_constant_count >= 2) {
|
||||
return SignatureClass::kClouds;
|
||||
}
|
||||
if (signature.half_res_like && signature.particle_like && signature.additive_like) {
|
||||
return SignatureClass::kExplosions;
|
||||
}
|
||||
if (signature.half_res_like && signature.post_process_like) {
|
||||
return SignatureClass::kSmoke;
|
||||
}
|
||||
if (signature.particle_like) {
|
||||
return SignatureClass::kParticles;
|
||||
}
|
||||
if (signature.post_process_like) {
|
||||
return SignatureClass::kPostProcess;
|
||||
}
|
||||
if (signature.has_depth_stencil) {
|
||||
return SignatureClass::kScene;
|
||||
}
|
||||
return SignatureClass::kUnknown;
|
||||
}
|
||||
|
||||
const char* ToString(const SignatureClass signature_class) {
|
||||
switch (signature_class) {
|
||||
case SignatureClass::kScene:
|
||||
return "scene";
|
||||
case SignatureClass::kPostProcess:
|
||||
return "post_process";
|
||||
case SignatureClass::kUiComposite:
|
||||
return "ui_composite";
|
||||
case SignatureClass::kParticles:
|
||||
return "particles";
|
||||
case SignatureClass::kClouds:
|
||||
return "clouds";
|
||||
case SignatureClass::kSmoke:
|
||||
return "smoke";
|
||||
case SignatureClass::kExplosions:
|
||||
return "explosions";
|
||||
case SignatureClass::kMissileTrails:
|
||||
return "missile_trails";
|
||||
case SignatureClass::kUnknown:
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ac6::backend
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "ac6_backend_capture_bridge.h"
|
||||
|
||||
namespace ac6::backend {
|
||||
|
||||
SignatureClass ClassifySignature(const RenderEventSignature& signature);
|
||||
const char* ToString(SignatureClass signature_class);
|
||||
|
||||
} // namespace ac6::backend
|
||||
+265
-73
@@ -1,148 +1,340 @@
|
||||
#include "ac6_native_graphics.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <algorithm>
|
||||
#include <string_view>
|
||||
|
||||
#include <native/audio/audio_system.h>
|
||||
#include <rex/cvar.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 native renderer frame-plan execution from captured D3D state");
|
||||
"Enable AC6 graphics capture analysis, overlay reporting, and backend fixes");
|
||||
REXCVAR_DEFINE_BOOL(ac6_native_graphics_require_capture, true, "AC6/NativeGraphics",
|
||||
"Force render-capture on while native graphics execution is enabled");
|
||||
"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",
|
||||
"Preferred native backend: auto, d3d12, vulkan, metal")
|
||||
.allowed({"auto", "d3d12", "vulkan", "metal"});
|
||||
"Legacy experimental replay backend preference");
|
||||
REXCVAR_DEFINE_STRING(ac6_native_graphics_feature_level, "scene_submission", "AC6/NativeGraphics",
|
||||
"Native renderer feature level: bootstrap, scene_submission, parity_validation, shipping")
|
||||
"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",
|
||||
"Native renderer max frames in flight")
|
||||
"Legacy experimental replay max frames in flight")
|
||||
.range(1, 4);
|
||||
|
||||
namespace ac6::graphics {
|
||||
namespace {
|
||||
|
||||
std::mutex g_native_graphics_mutex;
|
||||
ac6::renderer::NativeRenderer g_native_renderer;
|
||||
NativeGraphicsRuntimeStatus g_runtime_status{};
|
||||
ac6::renderer::D3D12Backend* g_d3d12_backend = nullptr;
|
||||
|
||||
ac6::renderer::BackendType ParseBackend(std::string_view value) {
|
||||
if (value == "d3d12") {
|
||||
return ac6::renderer::BackendType::kD3D12;
|
||||
GraphicsRuntimeMode ParseGraphicsMode(std::string_view value) {
|
||||
if (value == "disabled") {
|
||||
return GraphicsRuntimeMode::kDisabled;
|
||||
}
|
||||
if (value == "vulkan") {
|
||||
return ac6::renderer::BackendType::kVulkan;
|
||||
if (value == "analysis_only") {
|
||||
return GraphicsRuntimeMode::kAnalysisOnly;
|
||||
}
|
||||
if (value == "metal") {
|
||||
return ac6::renderer::BackendType::kMetal;
|
||||
if (value == "legacy_replay_experimental") {
|
||||
return GraphicsRuntimeMode::kLegacyReplayExperimental;
|
||||
}
|
||||
return ac6::renderer::BackendType::kUnknown;
|
||||
return GraphicsRuntimeMode::kHybridBackendFixes;
|
||||
}
|
||||
|
||||
ac6::renderer::FeatureLevel ParseFeatureLevel(std::string_view value) {
|
||||
if (value == "bootstrap") {
|
||||
return ac6::renderer::FeatureLevel::kBootstrap;
|
||||
using ac6::renderer::FeatureLevel;
|
||||
if (value == "scene_submission") {
|
||||
return FeatureLevel::kSceneSubmission;
|
||||
}
|
||||
if (value == "parity_validation") {
|
||||
return ac6::renderer::FeatureLevel::kParityValidation;
|
||||
return FeatureLevel::kParityValidation;
|
||||
}
|
||||
if (value == "shipping") {
|
||||
return ac6::renderer::FeatureLevel::kShipping;
|
||||
return FeatureLevel::kShipping;
|
||||
}
|
||||
return ac6::renderer::FeatureLevel::kSceneSubmission;
|
||||
return FeatureLevel::kBootstrap;
|
||||
}
|
||||
|
||||
ac6::renderer::NativeRendererConfig BuildRendererConfig() {
|
||||
ac6::renderer::NativeRendererConfig config;
|
||||
config.preferred_backend = ParseBackend(REXCVAR_GET(ac6_native_graphics_backend));
|
||||
config.feature_level = ParseFeatureLevel(REXCVAR_GET(ac6_native_graphics_feature_level));
|
||||
config.max_frames_in_flight = static_cast<uint32_t>(REXCVAR_GET(ac6_native_graphics_frames_in_flight));
|
||||
config.enable_debug_markers = true;
|
||||
config.enable_validation = true;
|
||||
return config;
|
||||
bool IsReplayMode(const GraphicsRuntimeMode mode) {
|
||||
return mode == GraphicsRuntimeMode::kLegacyReplayExperimental;
|
||||
}
|
||||
|
||||
bool EnsureInitialized() {
|
||||
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.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;
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
const ac6::renderer::NativeRendererConfig config = BuildRendererConfig();
|
||||
if (!g_native_renderer.Initialize(config)) {
|
||||
g_runtime_status.had_init_failure = true;
|
||||
REXLOG_ERROR("AC6 native graphics failed to initialize backend={}",
|
||||
ac6::renderer::ToString(ac6::renderer::ResolveBackend(config.preferred_backend)));
|
||||
auto* ts = rex::runtime::ThreadState::Get();
|
||||
if (!ts || !ts->context() || !ts->context()->kernel_state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
g_runtime_status.initialized = true;
|
||||
g_runtime_status.had_init_failure = false;
|
||||
++g_runtime_status.init_successes;
|
||||
g_runtime_status.feature_level = config.feature_level;
|
||||
return true;
|
||||
}
|
||||
auto* graphics_system = ts->context()->kernel_state->graphics_system();
|
||||
if (!graphics_system || !graphics_system->provider()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void UpdateStatusFromRendererUnlocked() {
|
||||
g_runtime_status.renderer_stats = g_native_renderer.GetStats();
|
||||
g_runtime_status.active_backend = g_runtime_status.renderer_stats.active_backend;
|
||||
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();
|
||||
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
|
||||
|
||||
void OnFrameBoundary() {
|
||||
std::scoped_lock<std::mutex> lock(g_native_graphics_mutex);
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
g_runtime_status.enabled = REXCVAR_GET(ac6_native_graphics_enabled);
|
||||
if (!g_runtime_status.enabled) {
|
||||
if (g_runtime_status.initialized) {
|
||||
g_native_renderer.Shutdown();
|
||||
g_runtime_status.initialized = false;
|
||||
}
|
||||
void OnFrameBoundary(rex::memory::Memory* memory) {
|
||||
SyncRuntimeFlags();
|
||||
|
||||
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_GET(ac6_render_capture)) {
|
||||
if (REXCVAR_GET(ac6_native_graphics_require_capture)) {
|
||||
REXCVAR_SET(ac6_render_capture, true);
|
||||
g_runtime_status.capture_enabled = true;
|
||||
}
|
||||
|
||||
if (!EnsureInitialized()) {
|
||||
return;
|
||||
if (!IsReplayMode(g_runtime_status.mode)) {
|
||||
ShutdownReplayRenderer();
|
||||
}
|
||||
|
||||
ac6::d3d::OnFrameBoundary();
|
||||
|
||||
const ac6::d3d::FrameCaptureSnapshot frame_capture = ac6::d3d::GetFrameCapture();
|
||||
g_runtime_status.capture_summary = ac6::d3d::GetFrameCaptureSummary();
|
||||
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;
|
||||
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.frames_built;
|
||||
UpdateStatusFromRendererUnlocked();
|
||||
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() {
|
||||
std::scoped_lock<std::mutex> lock(g_native_graphics_mutex);
|
||||
if (!g_runtime_status.initialized) {
|
||||
return;
|
||||
}
|
||||
g_native_renderer.Shutdown();
|
||||
g_runtime_status.initialized = false;
|
||||
ShutdownReplayRenderer();
|
||||
}
|
||||
|
||||
NativeGraphicsRuntimeStatus GetRuntimeStatus() {
|
||||
std::scoped_lock<std::mutex> lock(g_native_graphics_mutex);
|
||||
SyncRuntimeFlags();
|
||||
g_runtime_status.backend_diagnostics = ac6::backend::GetDiagnosticsSnapshot();
|
||||
return g_runtime_status;
|
||||
}
|
||||
|
||||
} // namespace ac6::graphics
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace ac6::graphics
|
||||
|
||||
@@ -1,24 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
#include <rex/memory.h>
|
||||
|
||||
#include "ac6_backend_fixes/ac6_backend_hooks.h"
|
||||
#include "ac6_native_renderer/ac6_render_frontend.h"
|
||||
#include "ac6_native_renderer/execution_plan.h"
|
||||
#include "ac6_native_renderer/frame_plan.h"
|
||||
#include "ac6_native_renderer/replay_ir.h"
|
||||
#include "ac6_native_renderer/replay_executor.h"
|
||||
#include "ac6_native_renderer/replay_ir.h"
|
||||
#include "ac6_native_renderer/types.h"
|
||||
#include "d3d_state.h"
|
||||
|
||||
struct ID3D12Resource;
|
||||
|
||||
namespace ac6::graphics {
|
||||
|
||||
enum class GraphicsRuntimeMode : uint8_t {
|
||||
kDisabled,
|
||||
kAnalysisOnly,
|
||||
kHybridBackendFixes,
|
||||
kLegacyReplayExperimental,
|
||||
};
|
||||
|
||||
std::string_view ToString(GraphicsRuntimeMode mode);
|
||||
|
||||
struct NativeGraphicsRuntimeStatus {
|
||||
bool enabled = false;
|
||||
GraphicsRuntimeMode mode = GraphicsRuntimeMode::kHybridBackendFixes;
|
||||
bool capture_enabled = false;
|
||||
bool authoritative_renderer_active = false;
|
||||
bool experimental_replay_present = false;
|
||||
bool initialized = false;
|
||||
bool had_init_failure = false;
|
||||
bool showing_latched_snapshot = false;
|
||||
uint64_t init_attempts = 0;
|
||||
uint64_t init_successes = 0;
|
||||
uint64_t frames_built = 0;
|
||||
uint64_t analysis_frames_observed = 0;
|
||||
uint64_t replay_frames_built = 0;
|
||||
uint64_t latest_capture_frame_index = 0;
|
||||
uint64_t latest_renderer_frame_index = 0;
|
||||
uint64_t last_meaningful_capture_frame_index = 0;
|
||||
uint64_t last_meaningful_renderer_frame_index = 0;
|
||||
|
||||
ac6::renderer::BackendType active_backend = ac6::renderer::BackendType::kUnknown;
|
||||
ac6::renderer::FeatureLevel feature_level = ac6::renderer::FeatureLevel::kBootstrap;
|
||||
@@ -29,13 +54,14 @@ struct NativeGraphicsRuntimeStatus {
|
||||
ac6::renderer::ReplayExecutorFrameSummary executor_summary{};
|
||||
ac6::renderer::BackendExecutorStatus backend_executor_status{};
|
||||
ac6::d3d::FrameCaptureSummary capture_summary{};
|
||||
ac6::backend::BackendDiagnosticsSnapshot backend_diagnostics{};
|
||||
ac6::renderer::NativeFramePlan frame_plan{};
|
||||
};
|
||||
|
||||
void OnFrameBoundary();
|
||||
void OnFrameBoundary(rex::memory::Memory* memory);
|
||||
void Shutdown();
|
||||
|
||||
NativeGraphicsRuntimeStatus GetRuntimeStatus();
|
||||
ID3D12Resource* GetNativeOutputTexture();
|
||||
|
||||
} // namespace ac6::graphics
|
||||
|
||||
|
||||
@@ -17,91 +17,112 @@ void NativeGraphicsStatusDialog::OnDraw(ImGuiIO& io) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ImGui::Begin("AC6 Native Graphics##status", &visible_, ImGuiWindowFlags_NoCollapse)) {
|
||||
if (!ImGui::Begin("AC6 Graphics Diagnostics##status", &visible_,
|
||||
ImGuiWindowFlags_NoCollapse)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
const NativeGraphicsRuntimeStatus status = GetRuntimeStatus();
|
||||
ImGui::Text("enabled: %s", status.enabled ? "true" : "false");
|
||||
ImGui::Text("initialized: %s", status.initialized ? "true" : "false");
|
||||
ImGui::Text("init failures seen: %s", status.had_init_failure ? "true" : "false");
|
||||
ImGui::Text("init attempts/successes: %llu / %llu",
|
||||
static_cast<unsigned long long>(status.init_attempts),
|
||||
static_cast<unsigned long long>(status.init_successes));
|
||||
ImGui::Text("frames built: %llu", static_cast<unsigned long long>(status.frames_built));
|
||||
ImGui::Separator();
|
||||
ImGui::Text("backend: %s", ac6::renderer::ToString(status.active_backend).data());
|
||||
ImGui::Text("feature level: %s", ac6::renderer::ToString(status.feature_level).data());
|
||||
ImGui::Text("renderer frames: %llu",
|
||||
static_cast<unsigned long long>(status.renderer_stats.frame_count));
|
||||
ImGui::Text("render passes built: %llu",
|
||||
static_cast<unsigned long long>(status.renderer_stats.built_pass_count));
|
||||
ImGui::Text("backend submits: %llu",
|
||||
static_cast<unsigned long long>(status.renderer_stats.backend_submit_count));
|
||||
ImGui::Text("frontend passes/commands: %u / %u", status.frontend_summary.pass_count,
|
||||
status.frontend_summary.total_command_count);
|
||||
ImGui::Text("replay passes/commands: %u / %u", status.replay_summary.pass_count,
|
||||
status.replay_summary.command_count);
|
||||
ImGui::Text("execution passes/commands: %u / %u",
|
||||
status.execution_summary.pass_count, status.execution_summary.command_count);
|
||||
ImGui::Text("executor passes/commands: %u / %u",
|
||||
status.executor_summary.pass_count, status.executor_summary.command_count);
|
||||
const auto& diagnostics = status.backend_diagnostics;
|
||||
|
||||
ImGui::Text("module: %s", status.enabled ? "enabled" : "disabled");
|
||||
ImGui::Text("mode: %.*s", static_cast<int>(ToString(status.mode).size()),
|
||||
ToString(status.mode).data());
|
||||
ImGui::Text("authoritative renderer: %s",
|
||||
status.authoritative_renderer_active ? "RexGlue/Xenia D3D12 backend"
|
||||
: "disabled");
|
||||
ImGui::Text("capture active: %s", status.capture_enabled ? "yes" : "no");
|
||||
ImGui::Text("experimental replay present override: %s",
|
||||
status.experimental_replay_present ? "enabled" : "disabled");
|
||||
ImGui::Text("analysis frames / replay frames: %llu / %llu",
|
||||
static_cast<unsigned long long>(status.analysis_frames_observed),
|
||||
static_cast<unsigned long long>(status.replay_frames_built));
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("capture frame: %llu",
|
||||
static_cast<unsigned long long>(status.capture_summary.frame_index));
|
||||
ImGui::Text("capture draws/clears/resolves: %u / %u / %u",
|
||||
ImGui::Text("capture draws / clears / resolves: %u / %u / %u",
|
||||
status.capture_summary.draw_count, status.capture_summary.clear_count,
|
||||
status.capture_summary.resolve_count);
|
||||
ImGui::Text("capture indexed / shared / primitive: %u / %u / %u",
|
||||
status.capture_summary.indexed_draw_count,
|
||||
status.capture_summary.indexed_shared_draw_count,
|
||||
status.capture_summary.primitive_draw_count);
|
||||
ImGui::Text("capture rt0 switches / unique rt0: %u / %u",
|
||||
status.capture_summary.rt0_switch_count,
|
||||
status.capture_summary.unique_rt0_count);
|
||||
ImGui::Text("frame-end viewport: %ux%u",
|
||||
status.capture_summary.frame_end_viewport_width,
|
||||
status.capture_summary.frame_end_viewport_height);
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("guest draw counts (this frame, pre-reset):");
|
||||
ImGui::Text(" indexed / shared / primitive: %u / %u / %u",
|
||||
status.capture_summary.frame_stats.draw_calls_indexed,
|
||||
status.capture_summary.frame_stats.draw_calls_indexed_shared,
|
||||
status.capture_summary.frame_stats.draw_calls_primitive);
|
||||
ImGui::Text(" set_sampler / set_texture_fetch: %u / %u",
|
||||
status.capture_summary.frame_stats.set_sampler_state_calls,
|
||||
status.capture_summary.frame_stats.set_texture_fetch_calls);
|
||||
ImGui::TextUnformatted("primitive topology (D3D9 type, all draws):");
|
||||
ImGui::Text(" point %u line %u strip %u tri %u triStrip %u fan %u other %u",
|
||||
status.capture_summary.topology_pointlist, status.capture_summary.topology_linelist,
|
||||
status.capture_summary.topology_linestrip, status.capture_summary.topology_trianglelist,
|
||||
status.capture_summary.topology_trianglestrip, status.capture_summary.topology_trianglefan,
|
||||
status.capture_summary.topology_other);
|
||||
ImGui::Text("last draw: prim_type=%u count=%u flags=0x%X",
|
||||
status.capture_summary.last_draw_primitive_type, status.capture_summary.last_draw_count,
|
||||
status.capture_summary.last_draw_flags);
|
||||
ImGui::Text("swap source: %s", ac6::backend::ToString(diagnostics.swap_source));
|
||||
ImGui::Text("frontbuffer / guest output: %ux%u / %ux%u",
|
||||
diagnostics.frontbuffer_width, diagnostics.frontbuffer_height,
|
||||
diagnostics.guest_output_width, diagnostics.guest_output_height);
|
||||
ImGui::Text("swap source extent: %ux%u (%s)",
|
||||
diagnostics.source_width, diagnostics.source_height,
|
||||
diagnostics.swap_source_scaled ? "scaled" : "unscaled");
|
||||
ImGui::Text("present classification: %s",
|
||||
ac6::backend::ToString(diagnostics.latest_signature.classification));
|
||||
ImGui::Text("signature: %016llX hits=%u",
|
||||
static_cast<unsigned long long>(diagnostics.latest_signature.stable_id),
|
||||
diagnostics.repeated_signature_count);
|
||||
ImGui::TextWrapped("signature tags: %s",
|
||||
diagnostics.latest_signature_tags.empty()
|
||||
? "none"
|
||||
: diagnostics.latest_signature_tags.c_str());
|
||||
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));
|
||||
ImGui::Text("vblank interval / last tick: %llu / %llu",
|
||||
static_cast<unsigned long long>(diagnostics.guest_vblank_interval_ticks),
|
||||
static_cast<unsigned long long>(diagnostics.last_guest_vblank_tick));
|
||||
ImGui::Text("host frame time / fps: %.2f ms / %.2f",
|
||||
diagnostics.host_frame_time_ms, diagnostics.host_fps);
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("planned output: %ux%u", status.frame_plan.output_width,
|
||||
status.frame_plan.output_height);
|
||||
ImGui::Text("replay output/present: %ux%u / %s", status.replay_summary.output_width,
|
||||
status.replay_summary.output_height,
|
||||
status.replay_summary.has_present_pass ? "yes" : "no");
|
||||
ImGui::Text("execution output/present: %ux%u / %s",
|
||||
status.execution_summary.output_width, status.execution_summary.output_height,
|
||||
status.execution_summary.has_present_pass ? "yes" : "no");
|
||||
ImGui::Text("executor output/present: %ux%u / %s",
|
||||
status.executor_summary.output_width, status.executor_summary.output_height,
|
||||
status.executor_summary.has_present_pass ? "yes" : "no");
|
||||
ImGui::Text("executor graphics/present/resource: %u / %u / %u",
|
||||
status.executor_summary.graphics_pass_count,
|
||||
status.executor_summary.present_pass_count,
|
||||
status.executor_summary.resource_translation_pass_count);
|
||||
ImGui::Text("backend consumed frame/passes/cmds: %s / %u / %u",
|
||||
status.backend_executor_status.frame_valid ? "yes" : "no",
|
||||
status.backend_executor_status.submitted_pass_count,
|
||||
status.backend_executor_status.submitted_command_count);
|
||||
ImGui::Text("backend resource/pso/descriptors: %u / %u / %u",
|
||||
status.backend_executor_status.resource_translation_pass_count,
|
||||
status.backend_executor_status.pipeline_state_pass_count,
|
||||
status.backend_executor_status.descriptor_setup_pass_count);
|
||||
ImGui::Text("stages scene/post/ui: %s / %s / %s",
|
||||
status.frame_plan.has_scene_stage ? "yes" : "no",
|
||||
status.frame_plan.has_post_process_stage ? "yes" : "no",
|
||||
status.frame_plan.has_ui_stage ? "yes" : "no");
|
||||
ImGui::Text("audio backend: %s",
|
||||
diagnostics.audio_backend_name.empty()
|
||||
? "unavailable"
|
||||
: diagnostics.audio_backend_name.c_str());
|
||||
ImGui::Text("audio clients / queued / peak: %u / %u / %u",
|
||||
diagnostics.audio_active_clients, diagnostics.audio_queued_frames,
|
||||
diagnostics.audio_peak_queued_frames);
|
||||
ImGui::Text("audio underruns / dropped / silence inject: %u / %u / %u",
|
||||
diagnostics.audio_underruns, diagnostics.audio_dropped_frames,
|
||||
diagnostics.audio_silence_injections);
|
||||
ImGui::Text("audio consumed frames / submitted tic / host tic: %llu / %llu / %llu",
|
||||
static_cast<unsigned long long>(diagnostics.audio_consumed_frames),
|
||||
static_cast<unsigned long long>(diagnostics.audio_submitted_tic),
|
||||
static_cast<unsigned long long>(diagnostics.audio_host_elapsed_tic));
|
||||
ImGui::Text("audio startup inflight / callback dispatch / throttle: %u / %u / %u",
|
||||
diagnostics.audio_startup_inflight_frames,
|
||||
diagnostics.audio_callback_dispatch_count,
|
||||
diagnostics.audio_callback_throttle_count);
|
||||
|
||||
if (status.mode == GraphicsRuntimeMode::kLegacyReplayExperimental) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("legacy replay diagnostics (experimental):");
|
||||
ImGui::Text("initialized: %s", status.initialized ? "true" : "false");
|
||||
ImGui::Text("init failures seen: %s", status.had_init_failure ? "true" : "false");
|
||||
ImGui::Text("replay backend: %s",
|
||||
ac6::renderer::ToString(status.active_backend).data());
|
||||
ImGui::Text("replay feature level: %s",
|
||||
ac6::renderer::ToString(status.feature_level).data());
|
||||
ImGui::Text("frontend / replay / execution commands: %u / %u / %u",
|
||||
status.frontend_summary.total_command_count,
|
||||
status.replay_summary.command_count,
|
||||
status.execution_summary.command_count);
|
||||
ImGui::Text("backend draw attempts / success: %u / %u",
|
||||
status.backend_executor_status.draw_attempt_count,
|
||||
status.backend_executor_status.draw_success_count);
|
||||
ImGui::Text("planned output: %ux%u", status.frame_plan.output_width,
|
||||
status.frame_plan.output_height);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace ac6::graphics
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
namespace ac6::renderer {
|
||||
namespace {
|
||||
|
||||
template <typename T, size_t N>
|
||||
uint32_t CountNonZeroEntries(const std::array<T, N>& values) {
|
||||
template <typename Container>
|
||||
uint32_t CountNonZeroEntries(const Container& values) {
|
||||
uint32_t count = 0;
|
||||
for (const T& value : values) {
|
||||
for (const auto& value : values) {
|
||||
if (value) {
|
||||
++count;
|
||||
}
|
||||
@@ -73,6 +73,7 @@ ObservedCommandDesc MakeObservedCommand(const ac6::d3d::DrawCallRecord& draw) {
|
||||
.viewport_y = draw.shadow_state.viewport.y,
|
||||
.viewport_width = draw.shadow_state.viewport.width,
|
||||
.viewport_height = draw.shadow_state.viewport.height,
|
||||
.shadow_state = draw.shadow_state,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -96,6 +97,7 @@ ObservedCommandDesc MakeObservedCommand(const ac6::d3d::ClearRecord& clear) {
|
||||
.viewport_y = clear.shadow_state.viewport.y,
|
||||
.viewport_width = clear.shadow_state.viewport.width,
|
||||
.viewport_height = clear.shadow_state.viewport.height,
|
||||
.shadow_state = clear.shadow_state,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,6 +115,7 @@ ObservedCommandDesc MakeObservedCommand(const ac6::d3d::ResolveRecord& resolve)
|
||||
.viewport_y = resolve.shadow_state.viewport.y,
|
||||
.viewport_width = resolve.shadow_state.viewport.width,
|
||||
.viewport_height = resolve.shadow_state.viewport.height,
|
||||
.shadow_state = resolve.shadow_state,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ struct ObservedCommandDesc {
|
||||
uint32_t viewport_y = 0;
|
||||
uint32_t viewport_width = 0;
|
||||
uint32_t viewport_height = 0;
|
||||
ac6::d3d::ShadowState shadow_state{};
|
||||
};
|
||||
|
||||
struct ObservedPassDesc {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,10 @@
|
||||
#include "../render_device.h"
|
||||
#include "../frame_scheduler.h"
|
||||
|
||||
#include "d3d12_resource_manager.h"
|
||||
#include "d3d12_resource_tracker.h"
|
||||
#include "d3d12_shader_manager.h"
|
||||
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -14,49 +18,120 @@
|
||||
|
||||
namespace ac6::renderer {
|
||||
|
||||
// Experimental replay backend retained for research and targeted override work.
|
||||
// The authoritative default presentation path remains the RexGlue backend.
|
||||
class D3D12Backend final : public RenderDeviceBackend {
|
||||
public:
|
||||
BackendType GetType() const override { return BackendType::kD3D12; }
|
||||
std::string_view GetName() const override { return "d3d12"; }
|
||||
bool IsSupported() const override;
|
||||
bool Initialize(const NativeRendererConfig& config) override;
|
||||
bool Initialize(const NativeRendererConfig& config, rex::memory::Memory* memory) override;
|
||||
bool InitializeShared(const NativeRendererConfig& config, rex::memory::Memory* memory,
|
||||
ID3D12Device* device, ID3D12CommandQueue* queue);
|
||||
bool SubmitExecutorFrame(const ReplayExecutorFrame& frame) override;
|
||||
BackendExecutorStatus GetExecutorStatus() const override { return executor_status_; }
|
||||
void Shutdown() override;
|
||||
|
||||
// Phase 4: Returns the native output texture for swapchain blit.
|
||||
// nullptr until a frame has been rendered.
|
||||
ID3D12Resource* GetOutputTexture() const { return output_texture_.Get(); }
|
||||
|
||||
private:
|
||||
BackendExecutorStatus executor_status_{};
|
||||
bool initialized_ = false;
|
||||
rex::memory::Memory* memory_ = nullptr;
|
||||
|
||||
#if defined(_WIN32)
|
||||
struct FrameContext {
|
||||
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> command_allocator;
|
||||
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> command_allocator;
|
||||
Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> command_list;
|
||||
uint64_t fence_value = 0;
|
||||
};
|
||||
|
||||
Microsoft::WRL::ComPtr<IDXGIFactory4> dxgi_factory_;
|
||||
Microsoft::WRL::ComPtr<ID3D12Device> device_;
|
||||
Microsoft::WRL::ComPtr<ID3D12CommandQueue> graphics_queue_;
|
||||
Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> command_list_;
|
||||
struct DrawResources {
|
||||
bool valid = false;
|
||||
bool indexed = false;
|
||||
uint32_t draw_count = 0;
|
||||
uint32_t draw_start = 0;
|
||||
uint32_t vertex_base_offset = 0;
|
||||
uint32_t vertex_stride = 0;
|
||||
uint32_t vertex_buffer_size = 0;
|
||||
uint32_t color_offset = 0xFFFFFFFFu;
|
||||
D3D12_GPU_DESCRIPTOR_HANDLE vertex_buffer_gpu{};
|
||||
D3D12_PRIMITIVE_TOPOLOGY topology = D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST;
|
||||
D3D12_PRIMITIVE_TOPOLOGY_TYPE topology_type = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
|
||||
};
|
||||
|
||||
Microsoft::WRL::ComPtr<ID3D12Fence> fence_;
|
||||
void* fence_event_ = nullptr; // HANDLE
|
||||
struct SubmissionDebugStats {
|
||||
uint32_t draw_attempt_count = 0;
|
||||
uint32_t draw_success_count = 0;
|
||||
uint32_t draw_prepare_failure_count = 0;
|
||||
uint32_t draw_pso_failure_count = 0;
|
||||
uint32_t indexed_draw_count = 0;
|
||||
uint32_t non_indexed_draw_count = 0;
|
||||
uint32_t clear_command_count = 0;
|
||||
uint32_t resolve_command_count = 0;
|
||||
uint32_t invalid_stream_binding_count = 0;
|
||||
uint32_t invalid_index_buffer_count = 0;
|
||||
uint32_t index_count_overflow_count = 0;
|
||||
uint32_t index_data_unavailable_count = 0;
|
||||
uint32_t index_buffer_create_failure_count = 0;
|
||||
uint32_t index_upload_failure_count = 0;
|
||||
uint32_t zero_vertex_count = 0;
|
||||
uint32_t invalid_vertex_range_count = 0;
|
||||
uint32_t vertex_buffer_size_invalid_count = 0;
|
||||
uint32_t vertex_buffer_create_failure_count = 0;
|
||||
uint32_t vertex_data_unavailable_count = 0;
|
||||
uint32_t vertex_upload_failure_count = 0;
|
||||
};
|
||||
|
||||
Microsoft::WRL::ComPtr<IDXGIFactory4> dxgi_factory_;
|
||||
Microsoft::WRL::ComPtr<ID3D12Device> device_;
|
||||
Microsoft::WRL::ComPtr<ID3D12CommandQueue> graphics_queue_;
|
||||
|
||||
Microsoft::WRL::ComPtr<ID3D12Fence> fence_;
|
||||
void* fence_event_ = nullptr;
|
||||
uint64_t current_fence_value_ = 0;
|
||||
|
||||
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> rtv_heap_;
|
||||
Microsoft::WRL::ComPtr<ID3D12Resource> dummy_output_resource_;
|
||||
uint32_t rtv_descriptor_size_ = 0;
|
||||
Microsoft::WRL::ComPtr<ID3D12RootSignature> root_signature_;
|
||||
|
||||
FrameScheduler frame_scheduler_;
|
||||
// Phase 4: output render target (native renderer draws into this)
|
||||
Microsoft::WRL::ComPtr<ID3D12Resource> output_texture_;
|
||||
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> output_rtv_heap_;
|
||||
D3D12_CPU_DESCRIPTOR_HANDLE output_rtv_{};
|
||||
uint32_t output_width_ = 0;
|
||||
uint32_t output_height_ = 0;
|
||||
static constexpr DXGI_FORMAT kOutputFormat = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
|
||||
FrameScheduler frame_scheduler_;
|
||||
std::vector<FrameContext> frame_contexts_;
|
||||
SubmissionDebugStats submission_debug_stats_{};
|
||||
|
||||
std::unordered_map<uint64_t, Microsoft::WRL::ComPtr<ID3D12Resource>> resource_cache_;
|
||||
std::unordered_map<uint64_t, Microsoft::WRL::ComPtr<ID3D12PipelineState>> pso_cache_;
|
||||
D3D12ResourceManager resource_manager_;
|
||||
D3D12ResourceTracker resource_tracker_;
|
||||
D3D12ShaderManager shader_manager_;
|
||||
|
||||
// PSO state hash helper
|
||||
uint64_t MakePSOHash(DXGI_FORMAT rt_fmt, DXGI_FORMAT ds_fmt,
|
||||
D3D12_PRIMITIVE_TOPOLOGY_TYPE topo, bool soft_particle) const;
|
||||
|
||||
// Ensure output texture is created at the right size
|
||||
bool EnsureOutputTexture(uint32_t width, uint32_t height);
|
||||
|
||||
bool CreateDevice();
|
||||
bool CreateCommandObjects(uint32_t num_frames);
|
||||
bool CreateRootSignature();
|
||||
void WaitForGpu();
|
||||
|
||||
// Phase 3 helpers
|
||||
void DispatchPassCommands(ID3D12GraphicsCommandList* cmd,
|
||||
const ReplayExecutorPassPacket& pass,
|
||||
uint32_t slot);
|
||||
bool PrepareDrawResources(ID3D12GraphicsCommandList* cmd,
|
||||
const ReplayExecutorCommandPacket& command,
|
||||
uint32_t slot,
|
||||
DrawResources& out_resources);
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace ac6::renderer
|
||||
} // namespace ac6::renderer
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include "d3d12_resource_manager.h"
|
||||
|
||||
#include <rex/logging.h>
|
||||
#include <rex/graphics/xenos.h>
|
||||
|
||||
namespace ac6::renderer {
|
||||
|
||||
bool D3D12ResourceManager::Initialize(ID3D12Device* device, uint32_t max_frames) {
|
||||
device_ = device;
|
||||
max_frames_ = max_frames;
|
||||
|
||||
D3D12_DESCRIPTOR_HEAP_DESC rtv_desc = {};
|
||||
rtv_desc.NumDescriptors = kMaxRtvDescriptors;
|
||||
rtv_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
|
||||
if (FAILED(device_->CreateDescriptorHeap(&rtv_desc, IID_PPV_ARGS(&rtv_heap_)))) return false;
|
||||
rtv_size_ = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
|
||||
|
||||
D3D12_DESCRIPTOR_HEAP_DESC dsv_desc = {};
|
||||
dsv_desc.NumDescriptors = kMaxDsvDescriptors;
|
||||
dsv_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
|
||||
if (FAILED(device_->CreateDescriptorHeap(&dsv_desc, IID_PPV_ARGS(&dsv_heap_)))) return false;
|
||||
dsv_size_ = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
|
||||
|
||||
D3D12_DESCRIPTOR_HEAP_DESC srv_desc = {};
|
||||
srv_desc.NumDescriptors = kMaxSrvDescriptors;
|
||||
srv_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
|
||||
srv_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
|
||||
if (FAILED(device_->CreateDescriptorHeap(&srv_desc, IID_PPV_ARGS(&srv_heap_)))) return false;
|
||||
srv_size_ = device_->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
|
||||
|
||||
frame_contexts_.resize(max_frames);
|
||||
for (uint32_t i = 0; i < max_frames; ++i) {
|
||||
D3D12_HEAP_PROPERTIES upload_props = {};
|
||||
upload_props.Type = D3D12_HEAP_TYPE_UPLOAD;
|
||||
|
||||
D3D12_RESOURCE_DESC buffer_desc = {};
|
||||
buffer_desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
|
||||
buffer_desc.Width = kUploadBufferSize;
|
||||
buffer_desc.Height = 1;
|
||||
buffer_desc.DepthOrArraySize = 1;
|
||||
buffer_desc.MipLevels = 1;
|
||||
buffer_desc.Format = DXGI_FORMAT_UNKNOWN;
|
||||
buffer_desc.SampleDesc.Count = 1;
|
||||
buffer_desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
|
||||
|
||||
if (FAILED(device_->CreateCommittedResource(&upload_props, D3D12_HEAP_FLAG_NONE, &buffer_desc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&frame_contexts_[i].upload_buffer)))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
D3D12_RANGE read_range = {0, 0};
|
||||
if (FAILED(frame_contexts_[i].upload_buffer->Map(0, &read_range, reinterpret_cast<void**>(&frame_contexts_[i].upload_ptr)))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void D3D12ResourceManager::Shutdown() {
|
||||
for (auto& ctx : frame_contexts_) {
|
||||
if (ctx.upload_buffer) {
|
||||
ctx.upload_buffer->Unmap(0, nullptr);
|
||||
}
|
||||
}
|
||||
frame_contexts_.clear();
|
||||
resource_cache_.clear();
|
||||
rtv_heap_.Reset();
|
||||
dsv_heap_.Reset();
|
||||
srv_heap_.Reset();
|
||||
}
|
||||
|
||||
void D3D12ResourceManager::BeginFrame(uint32_t frame_index) {
|
||||
current_frame_index_ = frame_index;
|
||||
// Reset transient descriptors
|
||||
rtv_ptr_ = 0;
|
||||
dsv_ptr_ = 0;
|
||||
srv_ptr_ = 0;
|
||||
|
||||
FrameContext& ctx = frame_contexts_[current_frame_index_ % max_frames_];
|
||||
ctx.upload_offset = 0;
|
||||
|
||||
// Simple LRU cleanup could go here
|
||||
}
|
||||
|
||||
ID3D12Resource* D3D12ResourceManager::GetOrCreateBuffer(uint32_t guest_address, uint32_t size, D3D12_RESOURCE_FLAGS flags) {
|
||||
if (guest_address == 0) return nullptr;
|
||||
|
||||
auto it = resource_cache_.find(guest_address);
|
||||
if (it != resource_cache_.end()) {
|
||||
if (it->second.size_bytes >= size) {
|
||||
it->second.last_used_frame = current_frame_index_;
|
||||
return it->second.resource.Get();
|
||||
}
|
||||
REXLOG_INFO("Growing cached D3D12 buffer for guest address 0x{:08X} from {} to {} bytes",
|
||||
guest_address, it->second.size_bytes, size);
|
||||
resource_cache_.erase(it);
|
||||
}
|
||||
|
||||
if (size == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
D3D12_HEAP_PROPERTIES heap_props = {};
|
||||
heap_props.Type = D3D12_HEAP_TYPE_DEFAULT;
|
||||
|
||||
D3D12_RESOURCE_DESC desc = {};
|
||||
desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
|
||||
desc.Width = size;
|
||||
desc.Height = 1;
|
||||
desc.DepthOrArraySize = 1;
|
||||
desc.MipLevels = 1;
|
||||
desc.Format = DXGI_FORMAT_UNKNOWN;
|
||||
desc.SampleDesc.Count = 1;
|
||||
desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
|
||||
desc.Flags = flags;
|
||||
|
||||
Microsoft::WRL::ComPtr<ID3D12Resource> resource;
|
||||
if (FAILED(device_->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(&resource)))) {
|
||||
REXLOG_ERROR("Failed to create D3D12 buffer for guest address 0x{:08X}", guest_address);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
resource_cache_[guest_address] = {resource, size, current_frame_index_};
|
||||
return resource.Get();
|
||||
}
|
||||
|
||||
ID3D12Resource* D3D12ResourceManager::GetOrCreateTexture(uint32_t guest_address, const d3d::ShadowState& state) {
|
||||
// In a real implementation, we would extract width, height, format from the fetch constant at guest_address
|
||||
// For this scaffold realization, we use guest_address as the key.
|
||||
(void)state;
|
||||
|
||||
if (guest_address == 0) return nullptr;
|
||||
|
||||
auto it = resource_cache_.find(guest_address);
|
||||
if (it != resource_cache_.end()) {
|
||||
it->second.last_used_frame = current_frame_index_;
|
||||
return it->second.resource.Get();
|
||||
}
|
||||
|
||||
// Placeholder texture creation
|
||||
D3D12_RESOURCE_DESC desc = {};
|
||||
desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
|
||||
desc.Width = 1024; // Mock
|
||||
desc.Height = 1024; // Mock
|
||||
desc.DepthOrArraySize = 1;
|
||||
desc.MipLevels = 1;
|
||||
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
desc.SampleDesc.Count = 1;
|
||||
desc.Flags = D3D12_RESOURCE_FLAG_NONE;
|
||||
|
||||
D3D12_HEAP_PROPERTIES heap_props = {};
|
||||
heap_props.Type = D3D12_HEAP_TYPE_DEFAULT;
|
||||
|
||||
Microsoft::WRL::ComPtr<ID3D12Resource> resource;
|
||||
if (FAILED(device_->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(&resource)))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
resource_cache_[guest_address] = {resource, 0, current_frame_index_};
|
||||
return resource.Get();
|
||||
}
|
||||
|
||||
D3D12ResourceManager::ResourceView D3D12ResourceManager::AllocateRTV() {
|
||||
uint32_t index = rtv_ptr_++;
|
||||
D3D12_CPU_DESCRIPTOR_HANDLE cpu = rtv_heap_->GetCPUDescriptorHandleForHeapStart();
|
||||
cpu.ptr += index * rtv_size_;
|
||||
return {cpu, {0}, index};
|
||||
}
|
||||
|
||||
D3D12ResourceManager::ResourceView D3D12ResourceManager::AllocateDSV() {
|
||||
uint32_t index = dsv_ptr_++;
|
||||
D3D12_CPU_DESCRIPTOR_HANDLE cpu = dsv_heap_->GetCPUDescriptorHandleForHeapStart();
|
||||
cpu.ptr += index * dsv_size_;
|
||||
return {cpu, {0}, index};
|
||||
}
|
||||
|
||||
D3D12ResourceManager::ResourceView D3D12ResourceManager::AllocateSRV() {
|
||||
uint32_t index = srv_ptr_++;
|
||||
D3D12_CPU_DESCRIPTOR_HANDLE cpu = srv_heap_->GetCPUDescriptorHandleForHeapStart();
|
||||
D3D12_GPU_DESCRIPTOR_HANDLE gpu = srv_heap_->GetGPUDescriptorHandleForHeapStart();
|
||||
cpu.ptr += index * srv_size_;
|
||||
gpu.ptr += index * srv_size_;
|
||||
return {cpu, gpu, index};
|
||||
}
|
||||
|
||||
bool D3D12ResourceManager::UploadData(ID3D12GraphicsCommandList* command_list, ID3D12Resource* destination, const void* data, uint64_t size, uint64_t destination_offset) {
|
||||
if (!destination || !data || size == 0) return false;
|
||||
|
||||
FrameContext& ctx = frame_contexts_[current_frame_index_ % max_frames_];
|
||||
|
||||
// Align offset to 256 bytes for good practice, though not strictly required for all buffer copies
|
||||
uint32_t aligned_offset = (ctx.upload_offset + 255) & ~255;
|
||||
if (aligned_offset + size > kUploadBufferSize) {
|
||||
REXLOG_ERROR("Upload buffer overflow in frame {}", current_frame_index_);
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(ctx.upload_ptr + aligned_offset, data, size);
|
||||
ctx.upload_offset = aligned_offset + static_cast<uint32_t>(size);
|
||||
|
||||
command_list->CopyBufferRegion(destination, destination_offset, ctx.upload_buffer.Get(), aligned_offset, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
DXGI_FORMAT D3D12ResourceManager::TranslateColorFormat(uint32_t guest_format) {
|
||||
using namespace rex::graphics::xenos;
|
||||
switch (static_cast<ColorRenderTargetFormat>(guest_format)) {
|
||||
case ColorRenderTargetFormat::k_8_8_8_8: return DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
case ColorRenderTargetFormat::k_8_8_8_8_GAMMA: return DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
|
||||
case ColorRenderTargetFormat::k_2_10_10_10: return DXGI_FORMAT_R10G10B10A2_UNORM;
|
||||
case ColorRenderTargetFormat::k_16_16_FLOAT: return DXGI_FORMAT_R16G16_FLOAT;
|
||||
case ColorRenderTargetFormat::k_16_16_16_16_FLOAT: return DXGI_FORMAT_R16G16B16A16_FLOAT;
|
||||
case ColorRenderTargetFormat::k_32_FLOAT: return DXGI_FORMAT_R32_FLOAT;
|
||||
case ColorRenderTargetFormat::k_32_32_FLOAT: return DXGI_FORMAT_R32G32_FLOAT;
|
||||
default: return DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
}
|
||||
}
|
||||
|
||||
DXGI_FORMAT D3D12ResourceManager::TranslateDepthFormat(uint32_t guest_format) {
|
||||
using namespace rex::graphics::xenos;
|
||||
switch (static_cast<DepthRenderTargetFormat>(guest_format)) {
|
||||
case DepthRenderTargetFormat::kD24S8: return DXGI_FORMAT_D24_UNORM_S8_UINT;
|
||||
case DepthRenderTargetFormat::kD24FS8: return DXGI_FORMAT_D32_FLOAT_S8X24_UINT; // Nearest
|
||||
default: return DXGI_FORMAT_D24_UNORM_S8_UINT;
|
||||
}
|
||||
}
|
||||
|
||||
DXGI_FORMAT D3D12ResourceManager::TranslateTextureFormat(uint32_t guest_format) {
|
||||
using namespace rex::graphics::xenos;
|
||||
switch (static_cast<TextureFormat>(guest_format)) {
|
||||
case TextureFormat::k_8_8_8_8: return DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
case TextureFormat::k_DXT1: return DXGI_FORMAT_BC1_UNORM;
|
||||
case TextureFormat::k_DXT2_3: return DXGI_FORMAT_BC2_UNORM;
|
||||
case TextureFormat::k_DXT4_5: return DXGI_FORMAT_BC3_UNORM;
|
||||
case TextureFormat::k_16_16_16_16_FLOAT: return DXGI_FORMAT_R16G16B16A16_FLOAT;
|
||||
case TextureFormat::k_32_FLOAT: return DXGI_FORMAT_R32_FLOAT;
|
||||
default: return DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
}
|
||||
}
|
||||
|
||||
DXGI_FORMAT D3D12ResourceManager::TranslateVertexFormat(uint32_t guest_format) {
|
||||
using namespace rex::graphics::xenos;
|
||||
switch (static_cast<VertexFormat>(guest_format)) {
|
||||
case VertexFormat::k_32_32_32_32_FLOAT: return DXGI_FORMAT_R32G32B32A32_FLOAT;
|
||||
case VertexFormat::k_32_32_32_FLOAT: return DXGI_FORMAT_R32G32B32_FLOAT;
|
||||
case VertexFormat::k_32_32_FLOAT: return DXGI_FORMAT_R32G32_FLOAT;
|
||||
case VertexFormat::k_32_FLOAT: return DXGI_FORMAT_R32_FLOAT;
|
||||
case VertexFormat::k_16_16_16_16_FLOAT: return DXGI_FORMAT_R16G16B16A16_FLOAT;
|
||||
case VertexFormat::k_16_16_FLOAT: return DXGI_FORMAT_R16G16_FLOAT;
|
||||
case VertexFormat::k_8_8_8_8: return DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
default: return DXGI_FORMAT_R32G32B32A32_FLOAT;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ac6::renderer
|
||||
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <wrl/client.h>
|
||||
#include <d3d12.h>
|
||||
|
||||
#include "../types.h"
|
||||
#include "../../d3d_state.h"
|
||||
|
||||
namespace ac6::renderer {
|
||||
|
||||
class D3D12ResourceManager {
|
||||
public:
|
||||
struct ResourceView {
|
||||
D3D12_CPU_DESCRIPTOR_HANDLE cpu_handle;
|
||||
D3D12_GPU_DESCRIPTOR_HANDLE gpu_handle;
|
||||
uint32_t heap_index;
|
||||
};
|
||||
|
||||
bool Initialize(ID3D12Device* device, uint32_t max_frames);
|
||||
void Shutdown();
|
||||
|
||||
void BeginFrame(uint32_t frame_index);
|
||||
|
||||
// Translation
|
||||
ID3D12Resource* GetOrCreateBuffer(uint32_t guest_address, uint32_t size, D3D12_RESOURCE_FLAGS flags = D3D12_RESOURCE_FLAG_NONE);
|
||||
ID3D12Resource* GetOrCreateTexture(uint32_t guest_address, const d3d::ShadowState& state);
|
||||
|
||||
// Descriptor management
|
||||
ResourceView AllocateRTV();
|
||||
ResourceView AllocateDSV();
|
||||
ResourceView AllocateSRV();
|
||||
ID3D12DescriptorHeap* GetSrvHeap() const { return srv_heap_.Get(); }
|
||||
|
||||
// Data sync
|
||||
bool UploadData(ID3D12GraphicsCommandList* command_list, ID3D12Resource* destination, const void* data, uint64_t size, uint64_t destination_offset = 0);
|
||||
|
||||
// Format translation
|
||||
DXGI_FORMAT TranslateColorFormat(uint32_t guest_format);
|
||||
DXGI_FORMAT TranslateDepthFormat(uint32_t guest_format);
|
||||
DXGI_FORMAT TranslateTextureFormat(uint32_t guest_format);
|
||||
DXGI_FORMAT TranslateVertexFormat(uint32_t guest_format);
|
||||
|
||||
private:
|
||||
ID3D12Device* device_ = nullptr;
|
||||
uint32_t max_frames_ = 0;
|
||||
uint32_t current_frame_index_ = 0;
|
||||
|
||||
struct CachedResource {
|
||||
Microsoft::WRL::ComPtr<ID3D12Resource> resource;
|
||||
uint64_t size_bytes = 0;
|
||||
uint64_t last_used_frame = 0;
|
||||
};
|
||||
|
||||
std::unordered_map<uint32_t, CachedResource> resource_cache_;
|
||||
|
||||
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> rtv_heap_;
|
||||
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> dsv_heap_;
|
||||
Microsoft::WRL::ComPtr<ID3D12DescriptorHeap> srv_heap_;
|
||||
|
||||
uint32_t rtv_ptr_ = 0;
|
||||
uint32_t dsv_ptr_ = 0;
|
||||
uint32_t srv_ptr_ = 0;
|
||||
|
||||
uint32_t rtv_size_ = 0;
|
||||
uint32_t dsv_size_ = 0;
|
||||
uint32_t srv_size_ = 0;
|
||||
|
||||
struct FrameContext {
|
||||
Microsoft::WRL::ComPtr<ID3D12Resource> upload_buffer;
|
||||
uint8_t* upload_ptr = nullptr;
|
||||
uint32_t upload_offset = 0;
|
||||
};
|
||||
std::vector<FrameContext> frame_contexts_;
|
||||
|
||||
static constexpr uint32_t kMaxRtvDescriptors = 1024;
|
||||
static constexpr uint32_t kMaxDsvDescriptors = 256;
|
||||
static constexpr uint32_t kMaxSrvDescriptors = 4096;
|
||||
static constexpr uint32_t kUploadBufferSize = 16 * 1024 * 1024; // 16 MB per frame
|
||||
};
|
||||
|
||||
} // namespace ac6::renderer
|
||||
@@ -0,0 +1,52 @@
|
||||
#include "d3d12_resource_tracker.h"
|
||||
|
||||
namespace ac6::renderer {
|
||||
|
||||
void D3D12ResourceTracker::TrackResource(ID3D12Resource* resource,
|
||||
D3D12_RESOURCE_STATES initial_state) {
|
||||
if (!resource) return;
|
||||
tracked_[resource] = {initial_state};
|
||||
}
|
||||
|
||||
bool D3D12ResourceTracker::TransitionBarrier(ID3D12GraphicsCommandList* cmd_list,
|
||||
ID3D12Resource* resource,
|
||||
D3D12_RESOURCE_STATES target_state) {
|
||||
if (!resource || !cmd_list) return false;
|
||||
|
||||
auto it = tracked_.find(resource);
|
||||
if (it == tracked_.end()) {
|
||||
// First time seeing this resource — assume COMMON
|
||||
tracked_[resource] = {D3D12_RESOURCE_STATE_COMMON};
|
||||
it = tracked_.find(resource);
|
||||
}
|
||||
|
||||
if (it->second.current_state == target_state) {
|
||||
return false; // No transition needed
|
||||
}
|
||||
|
||||
D3D12_RESOURCE_BARRIER barrier = {};
|
||||
barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
|
||||
barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
|
||||
barrier.Transition.pResource = resource;
|
||||
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
|
||||
barrier.Transition.StateBefore = it->second.current_state;
|
||||
barrier.Transition.StateAfter = target_state;
|
||||
|
||||
cmd_list->ResourceBarrier(1, &barrier);
|
||||
it->second.current_state = target_state;
|
||||
return true;
|
||||
}
|
||||
|
||||
void D3D12ResourceTracker::FlushBarriers(ID3D12GraphicsCommandList* cmd_list) {
|
||||
if (pending_barriers_.empty() || !cmd_list) return;
|
||||
cmd_list->ResourceBarrier(static_cast<UINT>(pending_barriers_.size()),
|
||||
pending_barriers_.data());
|
||||
pending_barriers_.clear();
|
||||
}
|
||||
|
||||
void D3D12ResourceTracker::Reset() {
|
||||
tracked_.clear();
|
||||
pending_barriers_.clear();
|
||||
}
|
||||
|
||||
} // namespace ac6::renderer
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <d3d12.h>
|
||||
|
||||
namespace ac6::renderer {
|
||||
|
||||
// Tracks D3D12 resource states for automatic barrier generation.
|
||||
class D3D12ResourceTracker {
|
||||
public:
|
||||
void TrackResource(ID3D12Resource* resource, D3D12_RESOURCE_STATES initial_state);
|
||||
|
||||
// Returns true if a barrier was needed and appended.
|
||||
bool TransitionBarrier(ID3D12GraphicsCommandList* cmd_list,
|
||||
ID3D12Resource* resource,
|
||||
D3D12_RESOURCE_STATES target_state);
|
||||
|
||||
// Flush all pending barriers at once (batch mode).
|
||||
void FlushBarriers(ID3D12GraphicsCommandList* cmd_list);
|
||||
|
||||
void Reset();
|
||||
|
||||
private:
|
||||
struct TrackedResource {
|
||||
D3D12_RESOURCE_STATES current_state = D3D12_RESOURCE_STATE_COMMON;
|
||||
};
|
||||
|
||||
std::unordered_map<ID3D12Resource*, TrackedResource> tracked_;
|
||||
std::vector<D3D12_RESOURCE_BARRIER> pending_barriers_;
|
||||
};
|
||||
|
||||
} // namespace ac6::renderer
|
||||
@@ -0,0 +1,283 @@
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include "d3d12_shader_manager.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <rex/logging.h>
|
||||
|
||||
#pragma comment(lib, "d3dcompiler.lib")
|
||||
|
||||
namespace ac6::renderer {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Embedded HLSL shaders (compiled inline at runtime, no external files)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Generic vertex-pulling shader. It reads stream 0 directly as a raw byte
|
||||
// buffer, byte-swaps guest big-endian dwords, and interprets the first float3
|
||||
// plus an optional fourth component as position data. Pre-transformed menu / UI vertices are projected from the
|
||||
// captured viewport into clip space; already-transformed clip-space vertices
|
||||
// are passed through.
|
||||
static constexpr const char kPassthroughVS[] = R"HLSL(
|
||||
cbuffer DrawConstants : register(b0) {
|
||||
uint vertex_base_offset;
|
||||
uint vertex_stride;
|
||||
uint vertex_buffer_size;
|
||||
uint viewport_x;
|
||||
uint viewport_y;
|
||||
uint viewport_width;
|
||||
uint viewport_height;
|
||||
uint color_offset;
|
||||
uint flags;
|
||||
};
|
||||
|
||||
ByteAddressBuffer g_vertex_buffer : register(t0);
|
||||
|
||||
struct VSOut {
|
||||
float4 pos : SV_Position;
|
||||
float4 color : COLOR0;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
|
||||
uint ByteSwap32(uint v) {
|
||||
return (v << 24) | ((v << 8) & 0x00FF0000u) | ((v >> 8) & 0x0000FF00u) | (v >> 24);
|
||||
}
|
||||
|
||||
bool CanLoadDword(uint byte_offset) {
|
||||
return (byte_offset & 3u) == 0u && (byte_offset + 4u) <= vertex_buffer_size;
|
||||
}
|
||||
|
||||
float LoadGuestFloat(uint byte_offset, float fallback_value) {
|
||||
if (!CanLoadDword(byte_offset)) {
|
||||
return fallback_value;
|
||||
}
|
||||
return asfloat(ByteSwap32(g_vertex_buffer.Load(byte_offset)));
|
||||
}
|
||||
|
||||
float4 LoadGuestColor(uint byte_offset, float4 fallback_value) {
|
||||
if (!CanLoadDword(byte_offset)) {
|
||||
return fallback_value;
|
||||
}
|
||||
uint packed = ByteSwap32(g_vertex_buffer.Load(byte_offset));
|
||||
return float4(
|
||||
float((packed >> 16) & 0xFFu) / 255.0f,
|
||||
float((packed >> 8) & 0xFFu) / 255.0f,
|
||||
float((packed >> 0) & 0xFFu) / 255.0f,
|
||||
max(float((packed >> 24) & 0xFFu) / 255.0f, 1.0f / 255.0f));
|
||||
}
|
||||
|
||||
VSOut main(uint vid : SV_VertexID) {
|
||||
uint stride = vertex_stride;
|
||||
uint byte_offset = vertex_base_offset + vid * stride;
|
||||
float4 default_color = float4(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
|
||||
// Unsupported stream layouts should draw nothing rather than issue invalid
|
||||
// raw-buffer reads that can remove the device.
|
||||
if (stride == 0u || byte_offset >= vertex_buffer_size) {
|
||||
VSOut empty;
|
||||
empty.pos = float4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
empty.color = default_color;
|
||||
empty.uv = float2(0.0f, 0.0f);
|
||||
return empty;
|
||||
}
|
||||
|
||||
float4 raw_pos = float4(
|
||||
LoadGuestFloat(byte_offset + 0, 0.0f),
|
||||
LoadGuestFloat(byte_offset + 4, 0.0f),
|
||||
LoadGuestFloat(byte_offset + 8, 0.0f),
|
||||
1.0f);
|
||||
if (stride >= 16u && CanLoadDword(byte_offset + 12u)) {
|
||||
float candidate_w = LoadGuestFloat(byte_offset + 12, 1.0f);
|
||||
if (candidate_w == candidate_w && abs(candidate_w) < 10000.0f) {
|
||||
raw_pos.w = candidate_w;
|
||||
}
|
||||
}
|
||||
bool has_viewport = viewport_width != 0 && viewport_height != 0;
|
||||
bool looks_screen_space =
|
||||
has_viewport &&
|
||||
(abs(raw_pos.x) > 2.5f || abs(raw_pos.y) > 2.5f || raw_pos.w > 2.0f || raw_pos.w < 0.0f);
|
||||
|
||||
VSOut o;
|
||||
if (looks_screen_space) {
|
||||
float2 viewport_size = float2(max(viewport_width, 1u), max(viewport_height, 1u));
|
||||
float2 viewport_origin = float2(viewport_x, viewport_y);
|
||||
float2 pixel = raw_pos.xy - viewport_origin;
|
||||
float2 ndc = float2(
|
||||
pixel.x / viewport_size.x * 2.0f - 1.0f,
|
||||
1.0f - pixel.y / viewport_size.y * 2.0f);
|
||||
o.pos = float4(ndc, saturate(raw_pos.z), 1.0f);
|
||||
} else {
|
||||
float w = abs(raw_pos.w) > 1.0e-6f ? raw_pos.w : 1.0f;
|
||||
o.pos = float4(raw_pos.xyz, w);
|
||||
}
|
||||
|
||||
if (color_offset != 0xFFFFFFFFu && (color_offset + 4u) <= stride) {
|
||||
o.color = LoadGuestColor(byte_offset + color_offset, default_color);
|
||||
} else {
|
||||
o.color = default_color;
|
||||
}
|
||||
o.uv = raw_pos.xy;
|
||||
return o;
|
||||
}
|
||||
)HLSL";
|
||||
|
||||
// Simple pixel shader: output the pulled vertex color.
|
||||
static constexpr const char kPassthroughPS[] = R"HLSL(
|
||||
struct PSIn {
|
||||
float4 pos : SV_Position;
|
||||
float4 color : COLOR0;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
|
||||
float4 main(PSIn i) : SV_Target {
|
||||
return i.color;
|
||||
}
|
||||
)HLSL";
|
||||
|
||||
// Diagnostic variant for post-process / present-tagged passes. This makes
|
||||
// successful fullscreen-style replay obvious even before real texture sampling
|
||||
// is implemented.
|
||||
static constexpr const char kSoftParticlePS[] = R"HLSL(
|
||||
struct PSIn {
|
||||
float4 pos : SV_Position;
|
||||
float4 color : COLOR0;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
|
||||
float4 main(PSIn i) : SV_Target {
|
||||
float2 pos_band = frac(abs(i.pos.xy) * 0.015625f);
|
||||
float2 uv_band = frac(abs(i.uv.xy) * 0.001953125f);
|
||||
float3 debug_color = float3(
|
||||
max(pos_band.x, 0.2f),
|
||||
max(pos_band.y, 0.2f),
|
||||
max(frac(uv_band.x + uv_band.y), 0.35f));
|
||||
return float4(debug_color, 1.0f);
|
||||
}
|
||||
)HLSL";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/*static*/ Microsoft::WRL::ComPtr<ID3DBlob> D3D12ShaderManager::CompileHLSL(
|
||||
const char* source, const char* entry_point, const char* target) {
|
||||
UINT flags = 0;
|
||||
#if defined(_DEBUG)
|
||||
flags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
|
||||
#else
|
||||
flags = D3DCOMPILE_OPTIMIZATION_LEVEL3;
|
||||
#endif
|
||||
|
||||
Microsoft::WRL::ComPtr<ID3DBlob> blob;
|
||||
Microsoft::WRL::ComPtr<ID3DBlob> error;
|
||||
HRESULT hr = D3DCompile(source, strlen(source), nullptr, nullptr, nullptr,
|
||||
entry_point, target, flags, 0, &blob, &error);
|
||||
if (FAILED(hr)) {
|
||||
if (error) {
|
||||
REXLOG_ERROR("Shader compile error [{}]: {}", entry_point,
|
||||
static_cast<const char*>(error->GetBufferPointer()));
|
||||
} else {
|
||||
REXLOG_ERROR("Shader compile error [{}]: hr=0x{:08X}", entry_point,
|
||||
static_cast<uint32_t>(hr));
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
|
||||
bool D3D12ShaderManager::Initialize(ID3D12Device* device) {
|
||||
device_ = device;
|
||||
|
||||
REXLOG_INFO("D3D12ShaderManager: Compiling passthrough VS...");
|
||||
passthrough_vs_ = CompileHLSL(kPassthroughVS, "main", "vs_5_0");
|
||||
if (!passthrough_vs_) {
|
||||
REXLOG_ERROR("D3D12ShaderManager: Failed to compile passthrough VS");
|
||||
return false;
|
||||
}
|
||||
|
||||
REXLOG_INFO("D3D12ShaderManager: Compiling passthrough PS...");
|
||||
passthrough_ps_ = CompileHLSL(kPassthroughPS, "main", "ps_5_0");
|
||||
if (!passthrough_ps_) {
|
||||
REXLOG_ERROR("D3D12ShaderManager: Failed to compile passthrough PS");
|
||||
return false;
|
||||
}
|
||||
|
||||
REXLOG_INFO("D3D12ShaderManager: Compiling soft-particle PS...");
|
||||
soft_particle_ps_ = CompileHLSL(kSoftParticlePS, "main", "ps_5_0");
|
||||
if (!soft_particle_ps_) {
|
||||
REXLOG_WARN("D3D12ShaderManager: Soft-particle PS compile failed, using passthrough");
|
||||
soft_particle_ps_ = passthrough_ps_;
|
||||
}
|
||||
|
||||
REXLOG_INFO("D3D12ShaderManager: All shaders compiled successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
void D3D12ShaderManager::Shutdown() {
|
||||
pso_cache_.clear();
|
||||
passthrough_vs_.Reset();
|
||||
passthrough_ps_.Reset();
|
||||
soft_particle_ps_.Reset();
|
||||
device_ = nullptr;
|
||||
}
|
||||
|
||||
ID3D12PipelineState* D3D12ShaderManager::GetOrCreatePSO(
|
||||
uint64_t state_hash,
|
||||
ID3D12RootSignature* root_sig,
|
||||
DXGI_FORMAT rt_format,
|
||||
DXGI_FORMAT ds_format,
|
||||
D3D12_PRIMITIVE_TOPOLOGY_TYPE topology_type,
|
||||
bool use_soft_particle_ps) {
|
||||
auto it = pso_cache_.find(state_hash);
|
||||
if (it != pso_cache_.end()) {
|
||||
return it->second.pso.Get();
|
||||
}
|
||||
|
||||
if (!device_ || !passthrough_vs_ || !passthrough_ps_) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ID3DBlob* ps = use_soft_particle_ps ? soft_particle_ps_.Get() : passthrough_ps_.Get();
|
||||
|
||||
D3D12_GRAPHICS_PIPELINE_STATE_DESC desc = {};
|
||||
desc.pRootSignature = root_sig;
|
||||
desc.VS = {passthrough_vs_->GetBufferPointer(), passthrough_vs_->GetBufferSize()};
|
||||
desc.PS = {ps->GetBufferPointer(), ps->GetBufferSize()};
|
||||
desc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
|
||||
desc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
|
||||
desc.RasterizerState.DepthClipEnable = TRUE;
|
||||
desc.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
|
||||
desc.BlendState.RenderTarget[0].BlendEnable = use_soft_particle_ps ? TRUE : FALSE;
|
||||
desc.BlendState.RenderTarget[0].SrcBlend = D3D12_BLEND_SRC_ALPHA;
|
||||
desc.BlendState.RenderTarget[0].DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
|
||||
desc.BlendState.RenderTarget[0].BlendOp = D3D12_BLEND_OP_ADD;
|
||||
desc.BlendState.RenderTarget[0].SrcBlendAlpha = D3D12_BLEND_ONE;
|
||||
desc.BlendState.RenderTarget[0].DestBlendAlpha = D3D12_BLEND_ZERO;
|
||||
desc.BlendState.RenderTarget[0].BlendOpAlpha = D3D12_BLEND_OP_ADD;
|
||||
desc.DepthStencilState.DepthEnable = (ds_format != DXGI_FORMAT_UNKNOWN) ? TRUE : FALSE;
|
||||
desc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
|
||||
desc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS_EQUAL;
|
||||
desc.SampleMask = UINT_MAX;
|
||||
desc.PrimitiveTopologyType = topology_type;
|
||||
desc.NumRenderTargets = (rt_format != DXGI_FORMAT_UNKNOWN) ? 1 : 0;
|
||||
if (rt_format != DXGI_FORMAT_UNKNOWN) {
|
||||
desc.RTVFormats[0] = rt_format;
|
||||
}
|
||||
desc.DSVFormat = ds_format;
|
||||
desc.SampleDesc.Count = 1;
|
||||
|
||||
Microsoft::WRL::ComPtr<ID3D12PipelineState> pso;
|
||||
HRESULT hr = device_->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&pso));
|
||||
if (FAILED(hr)) {
|
||||
REXLOG_ERROR(
|
||||
"D3D12ShaderManager: CreateGraphicsPipelineState failed 0x{:08X} hash=0x{:016X}",
|
||||
static_cast<uint32_t>(hr), state_hash);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto& entry = pso_cache_[state_hash];
|
||||
entry.pso = pso;
|
||||
return entry.pso.Get();
|
||||
}
|
||||
|
||||
} // namespace ac6::renderer
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <wrl/client.h>
|
||||
#include <d3d12.h>
|
||||
#include <d3dcompiler.h>
|
||||
|
||||
namespace ac6::renderer {
|
||||
|
||||
// Manages native passthrough shaders and a PSO cache for the native renderer.
|
||||
// Uses D3DCompile to compile embedded HLSL source inline at startup — no
|
||||
// external shader files required.
|
||||
class D3D12ShaderManager {
|
||||
public:
|
||||
bool Initialize(ID3D12Device* device);
|
||||
void Shutdown();
|
||||
|
||||
// Returns the shared passthrough vertex shader blob (compiled once).
|
||||
ID3DBlob* GetPassthroughVS() const { return passthrough_vs_.Get(); }
|
||||
|
||||
// Returns the shared solid-color pixel shader blob (compiled once).
|
||||
ID3DBlob* GetPassthroughPS() const { return passthrough_ps_.Get(); }
|
||||
|
||||
// Returns the soft-particle / high-precision effect pixel shader blob.
|
||||
ID3DBlob* GetSoftParticlePS() const { return soft_particle_ps_.Get(); }
|
||||
|
||||
// Get or create a PSO for the given state hash.
|
||||
// root_sig must already be created on the device.
|
||||
// Returns nullptr on failure.
|
||||
ID3D12PipelineState* GetOrCreatePSO(
|
||||
uint64_t state_hash,
|
||||
ID3D12RootSignature* root_sig,
|
||||
DXGI_FORMAT rt_format,
|
||||
DXGI_FORMAT ds_format,
|
||||
D3D12_PRIMITIVE_TOPOLOGY_TYPE topology_type,
|
||||
bool use_soft_particle_ps = false);
|
||||
|
||||
// Backwards compat stubs (unused but keep old callers linking)
|
||||
ID3DBlob* GetVertexShader(uint32_t /*guest_hash*/) { return passthrough_vs_.Get(); }
|
||||
ID3DBlob* GetPixelShader(uint32_t /*guest_hash*/) { return passthrough_ps_.Get(); }
|
||||
ID3DBlob* GetGenericSoftParticleShader() { return soft_particle_ps_.Get(); }
|
||||
|
||||
private:
|
||||
ID3D12Device* device_ = nullptr;
|
||||
|
||||
Microsoft::WRL::ComPtr<ID3DBlob> passthrough_vs_;
|
||||
Microsoft::WRL::ComPtr<ID3DBlob> passthrough_ps_;
|
||||
Microsoft::WRL::ComPtr<ID3DBlob> soft_particle_ps_;
|
||||
|
||||
struct CachedPSO {
|
||||
Microsoft::WRL::ComPtr<ID3D12PipelineState> pso;
|
||||
};
|
||||
std::unordered_map<uint64_t, CachedPSO> pso_cache_;
|
||||
|
||||
static Microsoft::WRL::ComPtr<ID3DBlob> CompileHLSL(
|
||||
const char* source, const char* entry_point, const char* target);
|
||||
};
|
||||
|
||||
} // namespace ac6::renderer
|
||||
@@ -12,8 +12,9 @@ bool MetalBackend::IsSupported() const {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool MetalBackend::Initialize(const NativeRendererConfig& config) {
|
||||
bool MetalBackend::Initialize(const NativeRendererConfig& config, rex::memory::Memory* memory) {
|
||||
(void)config;
|
||||
(void)memory;
|
||||
if (initialized_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ class MetalBackend final : public RenderDeviceBackend {
|
||||
BackendType GetType() const override { return BackendType::kMetal; }
|
||||
std::string_view GetName() const override { return "metal"; }
|
||||
bool IsSupported() const override;
|
||||
bool Initialize(const NativeRendererConfig& config) override;
|
||||
bool Initialize(const NativeRendererConfig& config, rex::memory::Memory* memory) override;
|
||||
bool InitializeShared(const NativeRendererConfig& config, rex::memory::Memory* memory, ID3D12Device* device, ID3D12CommandQueue* queue) override { return false; }
|
||||
bool SubmitExecutorFrame(const ReplayExecutorFrame& frame) override;
|
||||
BackendExecutorStatus GetExecutorStatus() const override { return executor_status_; }
|
||||
void Shutdown() override;
|
||||
|
||||
@@ -12,8 +12,9 @@ bool VulkanBackend::IsSupported() const {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool VulkanBackend::Initialize(const NativeRendererConfig& config) {
|
||||
bool VulkanBackend::Initialize(const NativeRendererConfig& config, rex::memory::Memory* memory) {
|
||||
(void)config;
|
||||
(void)memory;
|
||||
if (initialized_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ class VulkanBackend final : public RenderDeviceBackend {
|
||||
BackendType GetType() const override { return BackendType::kVulkan; }
|
||||
std::string_view GetName() const override { return "vulkan"; }
|
||||
bool IsSupported() const override;
|
||||
bool Initialize(const NativeRendererConfig& config) override;
|
||||
bool Initialize(const NativeRendererConfig& config, rex::memory::Memory* memory) override;
|
||||
bool InitializeShared(const NativeRendererConfig& config, rex::memory::Memory* memory, ID3D12Device* device, ID3D12CommandQueue* queue) override { return false; }
|
||||
bool SubmitExecutorFrame(const ReplayExecutorFrame& frame) override;
|
||||
BackendExecutorStatus GetExecutorStatus() const override { return executor_status_; }
|
||||
void Shutdown() override;
|
||||
|
||||
@@ -48,6 +48,7 @@ ExecutionCommandPacket BuildExecutionCommandPacket(const ReplayCommandDesc& comm
|
||||
.viewport_y = command.viewport_y,
|
||||
.viewport_width = command.viewport_width,
|
||||
.viewport_height = command.viewport_height,
|
||||
.shadow_state = command.shadow_state,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ struct ExecutionCommandPacket {
|
||||
uint32_t viewport_y = 0;
|
||||
uint32_t viewport_width = 0;
|
||||
uint32_t viewport_height = 0;
|
||||
ac6::d3d::ShadowState shadow_state{};
|
||||
};
|
||||
|
||||
struct ExecutionResourceRequirements {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "native_renderer.h"
|
||||
|
||||
#include "backends/d3d12_backend.h"
|
||||
#include <string>
|
||||
|
||||
#include <rex/logging.h>
|
||||
@@ -48,13 +49,31 @@ NativeRenderer::~NativeRenderer() {
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool NativeRenderer::Initialize(const NativeRendererConfig& config) {
|
||||
bool NativeRenderer::InitializeShared(const NativeRendererConfig& config, rex::memory::Memory* memory, ID3D12Device* device, ID3D12CommandQueue* queue) {
|
||||
Shutdown();
|
||||
|
||||
config_ = config;
|
||||
scheduler_.Configure(config_.max_frames_in_flight);
|
||||
|
||||
if (!device_.Initialize(config_)) {
|
||||
if (!device_.InitializeShared(config, memory, device, queue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stats_.initialized = true;
|
||||
stats_.active_backend = BackendType::kD3D12;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NativeRenderer::Initialize(const NativeRendererConfig& config, rex::memory::Memory* memory) {
|
||||
REXLOG_INFO("NativeRenderer::Initialize starting");
|
||||
Shutdown();
|
||||
|
||||
config_ = config;
|
||||
REXLOG_INFO("NativeRenderer: Configuring scheduler (max_frames={})", config_.max_frames_in_flight);
|
||||
scheduler_.Configure(config_.max_frames_in_flight);
|
||||
|
||||
REXLOG_INFO("NativeRenderer: Initializing render device...");
|
||||
if (!device_.Initialize(config_, memory)) {
|
||||
REXLOG_ERROR("NativeRenderer: device_.Initialize failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -136,7 +155,7 @@ void NativeRenderer::BuildCapturedFrame(
|
||||
}
|
||||
|
||||
stats_.built_pass_count += graph_.pass_count();
|
||||
REXLOG_TRACE(
|
||||
REXLOG_INFO(
|
||||
"AC6 native renderer observed frame={} frontend_passes={} replay_passes={} replay_commands={} execution_passes={} execution_commands={} executor_passes={} executor_commands={} backend_submits={} selected={} draws={} clears={} resolves={} plan_valid={} out={}x{}",
|
||||
summary.frame_index, summary.pass_count, replay_frame_.summary.pass_count,
|
||||
replay_frame_.summary.command_count, execution_plan_.summary.pass_count,
|
||||
@@ -149,3 +168,22 @@ void NativeRenderer::BuildCapturedFrame(
|
||||
}
|
||||
|
||||
} // namespace ac6::renderer
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase 4: backend accessors
|
||||
// ---------------------------------------------------------------------------
|
||||
namespace ac6::renderer {
|
||||
|
||||
D3D12Backend* NativeRenderer::GetD3D12Backend() const {
|
||||
if (!device_.backend() || device_.active_backend() != BackendType::kD3D12) {
|
||||
return nullptr;
|
||||
}
|
||||
return static_cast<D3D12Backend*>(device_.backend());
|
||||
}
|
||||
|
||||
ID3D12Resource* NativeRenderer::GetOutputTexture() const {
|
||||
D3D12Backend* b = GetD3D12Backend();
|
||||
return b ? b->GetOutputTexture() : nullptr;
|
||||
}
|
||||
|
||||
} // namespace ac6::renderer
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <rex/memory.h>
|
||||
|
||||
#include "ac6_render_frontend.h"
|
||||
#include "execution_plan.h"
|
||||
#include "frame_scheduler.h"
|
||||
@@ -12,8 +14,14 @@
|
||||
#include "render_graph.h"
|
||||
#include "types.h"
|
||||
|
||||
// Forward declare so callers can access the output texture without pulling in
|
||||
// all of d3d12_backend.h transitively.
|
||||
struct ID3D12Resource;
|
||||
|
||||
namespace ac6::renderer {
|
||||
|
||||
// Experimental capture-replay renderer retained for diagnostics and future
|
||||
// targeted overrides. It is not the default presentation path.
|
||||
class NativeRenderer {
|
||||
public:
|
||||
NativeRenderer();
|
||||
@@ -22,7 +30,8 @@ class NativeRenderer {
|
||||
NativeRenderer(const NativeRenderer&) = delete;
|
||||
NativeRenderer& operator=(const NativeRenderer&) = delete;
|
||||
|
||||
bool Initialize(const NativeRendererConfig& config);
|
||||
bool Initialize(const NativeRendererConfig& config, rex::memory::Memory* memory);
|
||||
bool InitializeShared(const NativeRendererConfig& config, rex::memory::Memory* memory, ID3D12Device* device, ID3D12CommandQueue* queue);
|
||||
void Shutdown();
|
||||
|
||||
void BeginFrame();
|
||||
@@ -48,6 +57,14 @@ class NativeRenderer {
|
||||
return device_.executor_status();
|
||||
}
|
||||
|
||||
// Phase 4: returns the native output texture produced by the D3D12 backend,
|
||||
// or nullptr if not yet available. The texture is in
|
||||
// D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE after SubmitExecutorFrame.
|
||||
ID3D12Resource* GetOutputTexture() const;
|
||||
|
||||
// Phase 4: returns the raw D3D12Backend* (nullptr for non-D3D12 backends).
|
||||
class D3D12Backend* GetD3D12Backend() const;
|
||||
|
||||
private:
|
||||
NativeRendererConfig config_{};
|
||||
NativeRendererStats stats_{};
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include <rex/logging.h>
|
||||
|
||||
#include "backends/d3d12_backend.h"
|
||||
|
||||
namespace ac6::renderer {
|
||||
|
||||
RenderDevice::RenderDevice() = default;
|
||||
@@ -12,7 +14,24 @@ RenderDevice::~RenderDevice() {
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool RenderDevice::Initialize(const NativeRendererConfig& config) {
|
||||
bool RenderDevice::InitializeShared(const NativeRendererConfig& config, rex::memory::Memory* memory, ID3D12Device* device, ID3D12CommandQueue* queue) {
|
||||
Shutdown();
|
||||
|
||||
active_backend_ = BackendType::kD3D12;
|
||||
backend_ = CreateBackend(active_backend_);
|
||||
if (!backend_) return false;
|
||||
|
||||
auto* d3d_backend = static_cast<D3D12Backend*>(backend_.get());
|
||||
if (!d3d_backend->InitializeShared(config, memory, device, queue)) {
|
||||
backend_.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
initialized_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderDevice::Initialize(const NativeRendererConfig& config, rex::memory::Memory* memory) {
|
||||
Shutdown();
|
||||
|
||||
active_backend_ = ResolveBackend(config.preferred_backend);
|
||||
@@ -30,7 +49,7 @@ bool RenderDevice::Initialize(const NativeRendererConfig& config) {
|
||||
active_backend_ = BackendType::kUnknown;
|
||||
return false;
|
||||
}
|
||||
if (!backend_->Initialize(config)) {
|
||||
if (!backend_->Initialize(config, memory)) {
|
||||
REXLOG_ERROR("AC6 native renderer backend {} failed initialization",
|
||||
backend_->GetName());
|
||||
backend_.reset();
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <d3d12.h>
|
||||
#else
|
||||
struct ID3D12Device;
|
||||
struct ID3D12CommandQueue;
|
||||
#endif
|
||||
|
||||
#include <rex/memory.h>
|
||||
|
||||
#include "replay_executor.h"
|
||||
#include "types.h"
|
||||
|
||||
@@ -15,7 +24,8 @@ class RenderDeviceBackend {
|
||||
virtual BackendType GetType() const = 0;
|
||||
virtual std::string_view GetName() const = 0;
|
||||
virtual bool IsSupported() const = 0;
|
||||
virtual bool Initialize(const NativeRendererConfig& config) = 0;
|
||||
virtual bool Initialize(const NativeRendererConfig& config, rex::memory::Memory* memory) = 0;
|
||||
virtual bool InitializeShared(const NativeRendererConfig& config, rex::memory::Memory* memory, ID3D12Device* device, ID3D12CommandQueue* queue) = 0;
|
||||
virtual bool SubmitExecutorFrame(const ReplayExecutorFrame& frame) = 0;
|
||||
virtual BackendExecutorStatus GetExecutorStatus() const = 0;
|
||||
virtual void Shutdown() = 0;
|
||||
@@ -29,7 +39,8 @@ class RenderDevice {
|
||||
RenderDevice(const RenderDevice&) = delete;
|
||||
RenderDevice& operator=(const RenderDevice&) = delete;
|
||||
|
||||
bool Initialize(const NativeRendererConfig& config);
|
||||
bool Initialize(const NativeRendererConfig& config, rex::memory::Memory* memory);
|
||||
bool InitializeShared(const NativeRendererConfig& config, rex::memory::Memory* memory, ID3D12Device* device, ID3D12CommandQueue* queue);
|
||||
void Shutdown();
|
||||
bool SubmitExecutorFrame(const ReplayExecutorFrame& frame);
|
||||
|
||||
@@ -38,6 +49,9 @@ class RenderDevice {
|
||||
std::string_view backend_name() const;
|
||||
BackendExecutorStatus executor_status() const;
|
||||
|
||||
// Raw backend pointer — used by NativeRenderer to downcast to D3D12Backend*.
|
||||
RenderDeviceBackend* backend() const { return backend_.get(); }
|
||||
|
||||
private:
|
||||
std::unique_ptr<RenderDeviceBackend> backend_;
|
||||
BackendType active_backend_ = BackendType::kUnknown;
|
||||
|
||||
@@ -32,6 +32,18 @@ ReplayExecutorCommandPacket BuildExecutorCommandPacket(
|
||||
command.fetch_constant_count != 0),
|
||||
.touches_render_target = command.render_target_0 != 0,
|
||||
.touches_depth_stencil = command.depth_stencil != 0,
|
||||
// Forwarded dispatch fields
|
||||
.draw_kind = command.draw_kind,
|
||||
.primitive_type = command.primitive_type,
|
||||
.start = command.start,
|
||||
.count = command.count,
|
||||
.flags = command.flags,
|
||||
.rect_count = command.rect_count,
|
||||
.captured_rect_count = command.captured_rect_count,
|
||||
.color = command.color,
|
||||
.stencil = command.stencil,
|
||||
.depth = command.depth,
|
||||
.shadow_state = command.shadow_state,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,18 @@ struct ReplayExecutorCommandPacket {
|
||||
bool requires_descriptor_setup = false;
|
||||
bool touches_render_target = false;
|
||||
bool touches_depth_stencil = false;
|
||||
// Draw call dispatch fields (forwarded from ExecutionCommandPacket)
|
||||
ac6::d3d::DrawCallKind draw_kind = ac6::d3d::DrawCallKind::kIndexed;
|
||||
uint32_t primitive_type = 0;
|
||||
uint32_t start = 0;
|
||||
uint32_t count = 0;
|
||||
uint32_t flags = 0;
|
||||
uint32_t rect_count = 0;
|
||||
uint32_t captured_rect_count = 0;
|
||||
uint32_t color = 0;
|
||||
uint32_t stencil = 0;
|
||||
float depth = 1.0f;
|
||||
ac6::d3d::ShadowState shadow_state{};
|
||||
};
|
||||
|
||||
struct ReplayExecutorPassPacket {
|
||||
|
||||
@@ -49,6 +49,7 @@ ReplayCommandDesc BuildReplayCommand(const ObservedCommandDesc& command) {
|
||||
.viewport_y = command.viewport_y,
|
||||
.viewport_width = command.viewport_width,
|
||||
.viewport_height = command.viewport_height,
|
||||
.shadow_state = command.shadow_state,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ struct ReplayCommandDesc {
|
||||
uint32_t viewport_y = 0;
|
||||
uint32_t viewport_width = 0;
|
||||
uint32_t viewport_height = 0;
|
||||
ac6::d3d::ShadowState shadow_state{};
|
||||
};
|
||||
|
||||
struct ReplayPassDesc {
|
||||
|
||||
@@ -49,6 +49,26 @@ struct BackendExecutorStatus {
|
||||
uint32_t resource_translation_pass_count = 0;
|
||||
uint32_t pipeline_state_pass_count = 0;
|
||||
uint32_t descriptor_setup_pass_count = 0;
|
||||
uint32_t draw_attempt_count = 0;
|
||||
uint32_t draw_success_count = 0;
|
||||
uint32_t draw_prepare_failure_count = 0;
|
||||
uint32_t draw_pso_failure_count = 0;
|
||||
uint32_t indexed_draw_count = 0;
|
||||
uint32_t non_indexed_draw_count = 0;
|
||||
uint32_t clear_command_count = 0;
|
||||
uint32_t resolve_command_count = 0;
|
||||
uint32_t invalid_stream_binding_count = 0;
|
||||
uint32_t invalid_index_buffer_count = 0;
|
||||
uint32_t index_count_overflow_count = 0;
|
||||
uint32_t index_data_unavailable_count = 0;
|
||||
uint32_t index_buffer_create_failure_count = 0;
|
||||
uint32_t index_upload_failure_count = 0;
|
||||
uint32_t zero_vertex_count = 0;
|
||||
uint32_t invalid_vertex_range_count = 0;
|
||||
uint32_t vertex_buffer_size_invalid_count = 0;
|
||||
uint32_t vertex_buffer_create_failure_count = 0;
|
||||
uint32_t vertex_data_unavailable_count = 0;
|
||||
uint32_t vertex_upload_failure_count = 0;
|
||||
};
|
||||
|
||||
constexpr std::string_view ToString(BackendType backend) {
|
||||
|
||||
+26
-1
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <rex/rex_app.h>
|
||||
|
||||
#include "ac6_native_graphics.h"
|
||||
#include "ac6_native_graphics_overlay.h"
|
||||
#include "generated/ac6recomp_config.h"
|
||||
|
||||
@@ -11,14 +12,38 @@ class Ac6recompApp : public rex::ReXApp {
|
||||
public:
|
||||
using rex::ReXApp::ReXApp;
|
||||
|
||||
Ac6recompApp(rex::ui::WindowedAppContext& ctx, std::string_view name, rex::PPCImageInfo ppc_info)
|
||||
: rex::ReXApp(ctx, name, ppc_info) {
|
||||
REXLOG_INFO("Ac6recompApp constructor");
|
||||
}
|
||||
|
||||
static std::unique_ptr<rex::ui::WindowedApp> Create(
|
||||
rex::ui::WindowedAppContext& ctx) {
|
||||
REXLOG_INFO("Ac6recompApp::Create");
|
||||
return std::unique_ptr<Ac6recompApp>(new Ac6recompApp(ctx, "ac6recomp", PPCImageConfig));
|
||||
}
|
||||
|
||||
protected:
|
||||
void OnPreSetup(rex::RuntimeConfig& config) override {
|
||||
REXLOG_INFO("Ac6recompApp::OnPreSetup");
|
||||
rex::ReXApp::OnPreSetup(config);
|
||||
}
|
||||
|
||||
void OnPostSetup() override {
|
||||
REXLOG_INFO("Ac6recompApp::OnPostSetup");
|
||||
rex::ReXApp::OnPostSetup();
|
||||
|
||||
auto* graphics_sys = runtime()->graphics_system();
|
||||
if (graphics_sys) {
|
||||
graphics_sys->SetFrameBoundaryCallback([](rex::memory::Memory* memory) {
|
||||
::ac6::graphics::OnFrameBoundary(memory);
|
||||
});
|
||||
REXLOG_INFO("Ac6recompApp: Native frame boundary callback registered");
|
||||
}
|
||||
}
|
||||
|
||||
void OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) override {
|
||||
rex::ReXApp::OnCreateDialogs(drawer);
|
||||
REXLOG_INFO("Ac6recompApp::OnCreateDialogs");
|
||||
native_graphics_status_dialog_ =
|
||||
std::make_unique<ac6::graphics::NativeGraphicsStatusDialog>(drawer);
|
||||
native_graphics_status_dialog_->Show();
|
||||
|
||||
+55
-3
@@ -35,10 +35,10 @@ std::vector<ac6::d3d::DrawCallRecord> g_live_draws;
|
||||
std::vector<ac6::d3d::ClearRecord> g_live_clears;
|
||||
std::vector<ac6::d3d::ResolveRecord> g_live_resolves;
|
||||
|
||||
template <typename T, size_t N>
|
||||
uint32_t CountNonZero(const std::array<T, N>& values) {
|
||||
template <typename Container>
|
||||
uint32_t CountNonZero(const Container& values) {
|
||||
uint32_t count = 0;
|
||||
for (const T& value : values) {
|
||||
for (const auto& value : values) {
|
||||
if (value) {
|
||||
++count;
|
||||
}
|
||||
@@ -74,6 +74,54 @@ void HashU32(uint64_t& hash, uint32_t value) {
|
||||
hash *= kFnvPrime;
|
||||
}
|
||||
|
||||
void HashU64(uint64_t& hash, uint64_t value) {
|
||||
HashU32(hash, static_cast<uint32_t>(value & 0xFFFFFFFFull));
|
||||
HashU32(hash, static_cast<uint32_t>(value >> 32));
|
||||
}
|
||||
|
||||
uint64_t ComputeVertexFetchLayoutSignature(const ac6::d3d::ShadowState& shadow) {
|
||||
constexpr uint64_t kFnvOffsetBasis = 1469598103934665603ull;
|
||||
uint64_t hash = kFnvOffsetBasis;
|
||||
HashU32(hash, shadow.vertex_declaration);
|
||||
for (const auto& stream : shadow.streams) {
|
||||
HashU32(hash, stream.buffer);
|
||||
HashU32(hash, stream.offset);
|
||||
HashU32(hash, stream.stride);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
uint64_t ComputeTextureFetchLayoutSignature(const ac6::d3d::ShadowState& shadow) {
|
||||
constexpr uint64_t kFnvOffsetBasis = 1469598103934665603ull;
|
||||
uint64_t hash = kFnvOffsetBasis;
|
||||
for (uint32_t texture : shadow.texture_fetch_ptrs) {
|
||||
HashU32(hash, texture);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
uint64_t ComputeResourceBindingSignature(const ac6::d3d::ShadowState& shadow) {
|
||||
constexpr uint64_t kFnvOffsetBasis = 1469598103934665603ull;
|
||||
uint64_t hash = kFnvOffsetBasis;
|
||||
for (uint32_t target : shadow.render_targets) {
|
||||
HashU32(hash, target);
|
||||
}
|
||||
HashU32(hash, shadow.depth_stencil);
|
||||
for (uint32_t texture : shadow.textures) {
|
||||
HashU32(hash, texture);
|
||||
}
|
||||
for (const auto& sampler : shadow.samplers) {
|
||||
HashU32(hash, sampler.mag_filter);
|
||||
HashU32(hash, sampler.min_filter);
|
||||
HashU32(hash, sampler.mip_filter);
|
||||
HashU32(hash, sampler.mip_level);
|
||||
HashU32(hash, sampler.border_color);
|
||||
}
|
||||
HashU64(hash, shadow.vertex_fetch_layout_signature);
|
||||
HashU64(hash, shadow.texture_fetch_layout_signature);
|
||||
return hash;
|
||||
}
|
||||
|
||||
void HashDrawRecord(uint64_t& hash, const ac6::d3d::DrawCallRecord& draw) {
|
||||
HashU32(hash, static_cast<uint32_t>(draw.kind));
|
||||
HashU32(hash, draw.primitive_type);
|
||||
@@ -189,6 +237,9 @@ ac6::d3d::ShadowState SnapshotShadowState(uint32_t device) {
|
||||
if (device != 0) {
|
||||
shadow.device = device;
|
||||
}
|
||||
shadow.vertex_fetch_layout_signature = ComputeVertexFetchLayoutSignature(shadow);
|
||||
shadow.texture_fetch_layout_signature = ComputeTextureFetchLayoutSignature(shadow);
|
||||
shadow.resource_binding_signature = ComputeResourceBindingSignature(shadow);
|
||||
return shadow;
|
||||
}
|
||||
|
||||
@@ -726,6 +777,7 @@ PPC_FUNC_IMPL(rex_sub_821E10C8) {
|
||||
namespace ac6::d3d {
|
||||
|
||||
void OnFrameBoundary() {
|
||||
REXLOG_INFO("d3d::OnFrameBoundary: frame_index={}", g_capture_live_frame_index);
|
||||
ac6::d3d::DrawStatsSnapshot draw_stats = SnapshotDrawStats();
|
||||
|
||||
std::unique_lock<std::shared_mutex> lock(g_snapshot_mutex);
|
||||
|
||||
@@ -106,6 +106,9 @@ struct ShadowState {
|
||||
std::array<SamplerBinding, kMaxSamplers> samplers{};
|
||||
std::array<uint32_t, kMaxFetchConstants> texture_fetch_ptrs{};
|
||||
uint32_t shader_gpr_alloc{0};
|
||||
uint64_t vertex_fetch_layout_signature{0};
|
||||
uint64_t texture_fetch_layout_signature{0};
|
||||
uint64_t resource_binding_signature{0};
|
||||
|
||||
struct {
|
||||
uint32_t x{0};
|
||||
|
||||
+59
-2
@@ -3,11 +3,68 @@
|
||||
//
|
||||
// This file is yours to edit. 'rexglue migrate' will NOT overwrite it.
|
||||
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/logging.h>
|
||||
|
||||
REXCVAR_DECLARE(bool, ac6_render_capture);
|
||||
REXCVAR_DECLARE(bool, ac6_timing_hooks_enabled);
|
||||
REXCVAR_DECLARE(bool, ac6_unlock_fps);
|
||||
REXCVAR_DECLARE(bool, ac6_native_graphics_enabled);
|
||||
REXCVAR_DECLARE(bool, ac6_experimental_replay_present);
|
||||
REXCVAR_DECLARE(std::string, ac6_graphics_mode);
|
||||
REXCVAR_DECLARE(std::string, log_file);
|
||||
REXCVAR_DECLARE(std::string, log_level);
|
||||
|
||||
#include "generated/ac6recomp_config.h"
|
||||
#include "generated/ac6recomp_init.h"
|
||||
|
||||
#include <rex/cvar.h>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
#include "ac6recomp_app.h"
|
||||
|
||||
REX_DEFINE_APP(ac6recomp, Ac6recompApp::Create)
|
||||
// Early boot log to catch crashes before the SDK logger is ready
|
||||
std::ofstream g_boot_log;
|
||||
|
||||
void InitEarlyLog() {
|
||||
g_boot_log.open("boot.log", std::ios::out | std::ios::trunc);
|
||||
if (g_boot_log.is_open()) {
|
||||
g_boot_log << "AC6 Recompiled Early Boot Log" << std::endl;
|
||||
g_boot_log << "-----------------------------" << std::endl;
|
||||
g_boot_log.flush();
|
||||
}
|
||||
std::cout << "Early boot logging initialized." << std::endl;
|
||||
}
|
||||
|
||||
std::unique_ptr<rex::ui::WindowedApp> Ac6recompAppCreate(rex::ui::WindowedAppContext& ctx) {
|
||||
if (g_boot_log.is_open()) {
|
||||
g_boot_log << "Ac6recompApp::Create called" << std::endl;
|
||||
g_boot_log.flush();
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
REXLOG_INFO("Ac6recompAppCreate: graphics mode={} replay_present={} capture={}",
|
||||
REXCVAR_GET(ac6_graphics_mode),
|
||||
REXCVAR_GET(ac6_experimental_replay_present) ? "true" : "false",
|
||||
REXCVAR_GET(ac6_render_capture) ? "true" : "false");
|
||||
|
||||
return Ac6recompApp::Create(ctx);
|
||||
}
|
||||
|
||||
REX_DEFINE_APP(ac6recomp, Ac6recompAppCreate)
|
||||
|
||||
// Hook into static initialization to start log early
|
||||
struct EarlyBoot {
|
||||
EarlyBoot() {
|
||||
InitEarlyLog();
|
||||
}
|
||||
} g_early_boot;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <mutex>
|
||||
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/system/kernel_state.h>
|
||||
|
||||
REXCVAR_DEFINE_BOOL(ac6_unlock_fps, false, "AC6", "Unlock frame rate to 60fps");
|
||||
REXCVAR_DEFINE_BOOL(ac6_timing_hooks_enabled, true, "AC6",
|
||||
@@ -47,13 +48,14 @@ void ac6DeltaDivisorHook(PPCRegister& r29) {
|
||||
}
|
||||
|
||||
void ac6PresentTimingHook(PPCRegister& /*r31*/) {
|
||||
ac6::d3d::OnFrameBoundary();
|
||||
ac6::graphics::OnFrameBoundary();
|
||||
static uint64_t last_log = 0;
|
||||
if (g_frame_count % 60 == 0 && g_frame_count != last_log) {
|
||||
REXLOG_INFO("ac6PresentTimingHook firing: frame={}", g_frame_count);
|
||||
last_log = g_frame_count;
|
||||
}
|
||||
// ac6::d3d::OnFrameBoundary(); // MOVED TO GPU THREAD
|
||||
|
||||
const auto now = Clock::now();
|
||||
double frame_time_ms = 0.0;
|
||||
double fps = 0.0;
|
||||
uint64_t frame_count = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_frame_mutex);
|
||||
if (g_frame_start.time_since_epoch().count() != 0) {
|
||||
@@ -63,9 +65,6 @@ void ac6PresentTimingHook(PPCRegister& /*r31*/) {
|
||||
++g_frame_count;
|
||||
}
|
||||
g_frame_start = now;
|
||||
frame_time_ms = g_frame_time_ms;
|
||||
fps = g_fps;
|
||||
frame_count = g_frame_count;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <rex/ppc/types.h>
|
||||
|
||||
REXCVAR_DECLARE(bool, ac6_unlock_fps);
|
||||
REXCVAR_DECLARE(bool, ac6_timing_hooks_enabled);
|
||||
|
||||
namespace ac6 {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user