Refactor native graphics backend and fix build errors

This commit is contained in:
salh
2026-04-19 13:05:43 +03:00
parent 0a1c92d048
commit 882d20f686
49 changed files with 3383 additions and 567 deletions
+1
View File
@@ -35,6 +35,7 @@ rexglue_sdk_new/
autoresearch/
*.bak
*.tmp
*.md
tmp_*.py
build_*.txt
*.log
+6
View File
@@ -22,6 +22,9 @@ set(AC6RECOMP_SOURCES
src/render_hooks.cpp
src/ac6_native_graphics.cpp
src/ac6_native_graphics_overlay.cpp
src/ac6_backend_fixes/ac6_backend_capture_bridge.cpp
src/ac6_backend_fixes/ac6_backend_hooks.cpp
src/ac6_backend_fixes/ac6_backend_pass_classifier.cpp
src/ac6_native_renderer/ac6_render_frontend.cpp
src/ac6_native_renderer/execution_plan.cpp
src/ac6_native_renderer/frame_plan.cpp
@@ -29,6 +32,9 @@ set(AC6RECOMP_SOURCES
src/ac6_native_renderer/replay_executor.cpp
src/ac6_native_renderer/backends/backend_factory.cpp
src/ac6_native_renderer/backends/d3d12_backend.cpp
src/ac6_native_renderer/backends/d3d12_resource_manager.cpp
src/ac6_native_renderer/backends/d3d12_resource_tracker.cpp
src/ac6_native_renderer/backends/d3d12_shader_manager.cpp
src/ac6_native_renderer/backends/metal_backend.cpp
src/ac6_native_renderer/backends/vulkan_backend.cpp
src/ac6_native_renderer/frame_scheduler.cpp
+29 -125
View File
@@ -5,7 +5,16 @@ DEV TESTING BRANCH. THINGS WILL BREAK HERE
> [!CAUTION]
> This project is still work in progress. It can boot and run in-game, but bugs, crashes, and missing functionality should be expected.
A native PC port of **Ace Combat 6: Fires of Liberation** (Xbox 360), built on top of the [ReXGlue SDK](https://github.com/rexglue/rexglue-sdk). The Xbox 360 PowerPC binary is statically recompiled to x86-64 so the original game logic runs natively on your host CPU, with a fully native D3D12/Vulkan renderer replacing the original Xenos GPU pipeline.
A native PC port of **Ace Combat 6: Fires of Liberation** built on top of the [ReXGlue SDK](https://github.com/rexglue/rexglue-sdk). The Xbox 360 PowerPC game code is statically recompiled to x86-64, while visible rendering currently remains authoritative in the vendored RexGlue/Xenia graphics backend.
The AC6-specific graphics layer in this repo is now focused on:
- frame capture and diagnostics
- swap-path inspection and overlay reporting
- backend-fix routing for AC6-specific rendering issues
- future selective override and modding hooks
The legacy AC6 replay renderer is still present as experimental tooling, but it is **not** the default render path and it does **not** hijack presentation unless `ac6_graphics_mode=legacy_replay_experimental` and `ac6_experimental_replay_present=true`.
## Repository policy
@@ -20,149 +29,44 @@ Do **not** commit or redistribute:
Users must supply their own legally obtained game files locally.
---
## Prerequisites
| Tool | Version | Notes |
|---|---|---|
| [CMake](https://cmake.org/) | 3.25+ | |
| [Ninja](https://ninja-build.org/) | any recent | required generator |
| [Clang/LLVM](https://releases.llvm.org/) | any recent | `clang` / `clang++` must be on `PATH` |
| Windows SDK | 10.0.19041+ | D3D12 headers (Windows only) |
> [!NOTE]
> The Linux preset uses `clang-20` / `clang++-20` directly. Install the versioned binaries via your distro's package manager (`apt install clang-20`) or via the [LLVM APT repository](https://apt.llvm.org).
---
## Acquiring the game files
1. Obtain the original Xbox 360 disc image (ISO) by dumping your own disc.
Guides and tools: [consolemods.org ISO Extraction & Repacking](https://consolemods.org/wiki/Xbox:ISO_Extraction_%26_Repacking)
2. Extract the XEX and game data from the ISO.
3. Place the resulting files inside the `assets/` directory (created manually — it is git-ignored):
```text
assets/
default.xex ← required by the codegen step
media/ ← game data (audio, video, maps, …)
```
---
## Clone
```bash
git clone https://github.com/sal063/AC6_recomp.git
cd AC6_recomp
```
> [!NOTE]
> The ReXGlue SDK (`thirdparty/rexglue-sdk/`) is vendored directly in the repository. No submodule init is needed.
---
## Build
### 1 — Configure
```bash
cmake --preset win-amd64-relwithdebinfo
```
### 2 — Generate the recompiled code (first time, and after updating `default.xex`)
```bash
cmake --build --preset win-amd64-relwithdebinfo --target ac6recomp_codegen
```
This step reads `assets/default.xex`, lifts all PowerPC instructions to C++, and writes the output to `generated/`. It can take a few minutes.
### 3 — Re-run CMake configure
```bash
cmake --preset win-amd64-relwithdebinfo
```
Re-run configure after codegen so CMake picks up the generated `generated/sources.cmake` file and adds the generated `.cpp` sources to the target.
### 4 — Build the runtime
```bash
cmake --build --preset win-amd64-relwithdebinfo
```
The executable is placed at:
The executable is placed at `out/build/win-amd64-relwithdebinfo/ac6recomp.exe`.
```
out/build/win-amd64-relwithdebinfo/ac6recomp.exe
```
## Runtime Defaults
> [!TIP]
> `RelWithDebInfo` is the recommended preset — it gives near-release performance with symbols intact for debugging. A full `Release` build disables assertions and can be used for distribution.
The default AC6 graphics configuration after this pivot is:
### Available presets
- `ac6_native_graphics_enabled=true`
- `ac6_graphics_mode=hybrid_backend_fixes`
- `ac6_render_capture=true`
- `ac6_experimental_replay_present=false`
| Preset | Platform | Build type |
|---|---|---|
| `win-amd64-debug` | Windows | Debug |
| `win-amd64-release` | Windows | Release |
| `win-amd64-relwithdebinfo` | Windows | RelWithDebInfo ✅ recommended |
| `linux-amd64-debug` | Linux | Debug |
| `linux-amd64-release` | Linux | Release |
| `linux-amd64-relwithdebinfo` | Linux | RelWithDebInfo |
---
## Run
```bash
./out/build/win-amd64-relwithdebinfo/ac6recomp assets
```
The single argument is the path to the directory containing your game files (`assets/` by default). The runtime resolves all paths relative to it.
---
## Linux
Substitute `win-amd64-relwithdebinfo` with `linux-amd64-relwithdebinfo` in every command above.
```bash
cmake --preset linux-amd64-relwithdebinfo
cmake --build --preset linux-amd64-relwithdebinfo --target ac6recomp_codegen
cmake --preset linux-amd64-relwithdebinfo
cmake --build --preset linux-amd64-relwithdebinfo
./out/build/linux-amd64-relwithdebinfo/ac6recomp assets
```
---
That means the RexGlue/Xenia D3D12 backend remains the visible renderer by default, while AC6-specific analysis and diagnostics stay active.
## Project layout
```
```text
AC6_recomp/
├── src/ Host-side runtime & renderer
│ ├── main.cpp
│ ├── ac6_native_graphics.* Xenon → native GPU command translation
│ ├── ac6_native_renderer/ Native rendering backend (D3D12 / Vulkan)
│ ├── backends/ Per-API backend implementations
│ │ ├── frame_plan.* Frame dependency graph construction
│ │ ├── frame_scheduler.* CPU/GPU timeline management
├── native_renderer.* Top-level renderer orchestration
│ │ └── render_device.* Device abstraction layer
│ └── d3d_hooks.* Low-level D3D intercept layer
├── thirdparty/rexglue-sdk/ ReXGlue SDK (vendored)
├── assets/ ← NOT in repo; place your game files here
├── generated/ ← NOT in repo; output of codegen step
├── CMakeLists.txt
└── CMakePresets.json
|- src/
| |- ac6_backend_fixes/ AC6-specific backend diagnostics and fix routing
| |- ac6_native_graphics.* AC6 frame-boundary analysis and overlay status
| |- ac6_native_renderer/ Experimental replay renderer and research tooling
| |- d3d_hooks.* Guest D3D capture and shadow-state hooks
| `- render_hooks.* Timing and frame pacing hooks
|- thirdparty/rexglue-sdk/ Vendored RexGlue SDK
|- generated/ Generated recomp sources
|- assets/ Local game files, not kept in repo
`- docs/RENDERER_ARCHITECTURE.txt
```
---
## License
See [LICENSE](LICENSE).
+30
View File
@@ -0,0 +1,30 @@
AC6 renderer architecture after the pivot
=========================================
Authoritative renderer
----------------------
Visible rendering now defaults to the vendored RexGlue/Xenia graphics backend.
The guest GPU command stream, the RexGlue D3D12 command processor, and the presenter remain the only authoritative presentation path in the default configuration.
AC6-specific layer
------------------
The AC6 code in this repository sits alongside that backend and provides:
- frame-boundary capture and shadow-state analysis
- swap-path inspection and overlay reporting
- AC6-specific pass classification and repeated-signature tracking
- timing telemetry for host frame pacing, guest vblank cadence, and audio queue state
- narrow extension points for future AC6 fixes and selective modding overrides
Experimental replay renderer
----------------------------
The legacy AC6 native replay renderer is retained only as experimental tooling.
It may still be initialized for research or selective override work, but it is not the shipping render path and it does not override presentation unless explicitly opted in at runtime.
Current default runtime behavior
--------------------------------
- `ac6_graphics_mode=hybrid_backend_fixes`
- `ac6_experimental_replay_present=false`
- render capture remains enabled by default
This keeps diagnostics and backend-fix infrastructure active without introducing a competing render authority.
@@ -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
+197
View File
@@ -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
+102
View File
@@ -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
View File
@@ -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
+30 -4
View File
@@ -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
+92 -71
View File
@@ -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,
};
}
+1
View File
@@ -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 {
+42 -4
View File
@@ -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
+18 -1
View File
@@ -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_{};
+21 -2
View File
@@ -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();
+16 -2
View File
@@ -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,
};
}
+12
View File
@@ -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 {
+1
View File
@@ -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,
};
}
+1
View File
@@ -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 {
+20
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+3
View File
@@ -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
View File
@@ -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;
+7 -8
View File
@@ -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;
}
}
+1
View File
@@ -6,6 +6,7 @@
#include <rex/ppc/types.h>
REXCVAR_DECLARE(bool, ac6_unlock_fps);
REXCVAR_DECLARE(bool, ac6_timing_hooks_enabled);
namespace ac6 {
@@ -74,7 +74,10 @@ class GraphicsSystem : public system::IGraphicsSystem {
virtual void EnableReadPointerWriteBack(uint32_t ptr, uint32_t block_size_log2) override;
virtual void SetInterruptCallback(uint32_t callback, uint32_t user_data) override;
virtual void SetFrameBoundaryCallback(std::function<void(rex::memory::Memory*)> callback) override;
virtual bool HandleVideoSwap(const system::GraphicsSwapSubmission& submission) override;
bool GetLastSwapSubmission(system::GraphicsSwapSubmission* out_submission,
uint64_t* out_sequence = nullptr) const;
void DispatchInterruptCallback(uint32_t source, uint32_t cpu);
virtual void ClearCaches();
@@ -91,6 +94,13 @@ class GraphicsSystem : public system::IGraphicsSystem {
void Pause();
void Resume();
uint64_t guest_vblank_interval_ticks() const {
return guest_vblank_interval_ticks_.load(std::memory_order_acquire);
}
uint64_t last_vblank_interrupt_guest_tick() const {
return last_vblank_interrupt_guest_tick_.load(std::memory_order_acquire);
}
bool Save(::rex::stream::ByteStream* stream);
bool Restore(::rex::stream::ByteStream* stream);
@@ -107,12 +117,6 @@ class GraphicsSystem : public system::IGraphicsSystem {
void WriteRegister(uint32_t addr, uint32_t value);
void MarkVblank();
uint64_t guest_vblank_interval_ticks() const {
return guest_vblank_interval_ticks_.load(std::memory_order_acquire);
}
uint64_t last_vblank_interrupt_guest_tick() const {
return last_vblank_interrupt_guest_tick_.load(std::memory_order_acquire);
}
memory::Memory* memory_ = nullptr;
runtime::FunctionDispatcher* function_dispatcher_ = nullptr;
@@ -127,6 +131,11 @@ class GraphicsSystem : public system::IGraphicsSystem {
system::object_ref<system::XHostThread> vsync_worker_thread_;
std::atomic<uint64_t> guest_vblank_interval_ticks_{0};
std::atomic<uint64_t> last_vblank_interrupt_guest_tick_{0};
mutable std::mutex last_swap_submission_mutex_;
system::GraphicsSwapSubmission last_swap_submission_{};
uint64_t last_swap_submission_sequence_ = 0;
std::function<void(rex::memory::Memory*)> frame_boundary_callback_;
RegisterFile register_file_;
std::unique_ptr<CommandProcessor> command_processor_;
@@ -12,10 +12,14 @@
#pragma once
#include <array>
#include <functional>
#include <rex/system/xtypes.h>
// Forward declarations
namespace rex::memory {
class Memory;
}
namespace rex::runtime {
class FunctionDispatcher;
}
@@ -52,6 +56,7 @@ class IGraphicsSystem {
virtual void InitializeRingBuffer(uint32_t ptr, uint32_t size_log2) = 0;
virtual void EnableReadPointerWriteBack(uint32_t ptr, uint32_t block_size_log2) = 0;
virtual void SetInterruptCallback(uint32_t callback, uint32_t user_data) = 0;
virtual void SetFrameBoundaryCallback(std::function<void(rex::memory::Memory*)> callback) = 0;
virtual bool HandleVideoSwap(const GraphicsSwapSubmission& submission) = 0;
};
@@ -31,6 +31,9 @@
#include <rex/ui/d3d12/d3d12_presenter.h>
#include <rex/ui/d3d12/d3d12_util.h>
#include "../../../../../src/ac6_backend_fixes/ac6_backend_hooks.h"
#include "../../../../../src/ac6_native_graphics.h"
REXCVAR_DEFINE_BOOL(d3d12_bindless, true, "GPU/D3D12", "Use bindless resources where available")
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
@@ -1989,39 +1992,62 @@ bool D3D12CommandProcessor::IssueSwapInternal(uint32_t frontbuffer_ptr,
return false;
}
// Let AC6 consume the frame-boundary callback on the GPU thread before
// choosing the swap source. This is analysis-first by default; the legacy
// replay path only overrides presentation if explicitly enabled.
{
system::GraphicsSwapSubmission frame_boundary_submission = {};
frame_boundary_submission.frontbuffer_virtual_address = frontbuffer_ptr;
frame_boundary_submission.frontbuffer_width = frontbuffer_width;
frame_boundary_submission.frontbuffer_height = frontbuffer_height;
graphics_system_->HandleVideoSwap(frame_boundary_submission);
}
// Obtain the actual swap source texture size (resolution-scaled if it's a
// resolve destination, or not otherwise).
D3D12_SHADER_RESOURCE_VIEW_DESC swap_texture_srv_desc = {};
xenos::TextureFormat frontbuffer_format;
uint32_t frontbuffer_width_unscaled = 0, frontbuffer_height_unscaled = 0;
REXGPU_ERROR("IssueSwap: Calling RequestSwapTexture for fb={:08X}", frontbuffer_ptr);
ID3D12Resource* swap_texture_resource =
texture_cache_->RequestSwapTexture(swap_texture_srv_desc, frontbuffer_format,
&frontbuffer_width_unscaled, &frontbuffer_height_unscaled);
if (!swap_texture_resource) {
REXGPU_ERROR("IssueSwap: RequestSwapTexture returned null, trying UpdateDirectDisplayTexture");
swap_texture_resource = UpdateDirectDisplayTexture(
frontbuffer_ptr, frontbuffer_width, frontbuffer_height,
bool using_native_swap_texture = false;
bool used_direct_display_fallback = false;
ID3D12Resource* swap_texture_resource = ac6::graphics::GetNativeOutputTexture();
if (swap_texture_resource) {
D3D12_RESOURCE_DESC native_desc = swap_texture_resource->GetDesc();
swap_texture_srv_desc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
swap_texture_srv_desc.Format = native_desc.Format;
swap_texture_srv_desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
swap_texture_srv_desc.Texture2D.MostDetailedMip = 0;
swap_texture_srv_desc.Texture2D.MipLevels = 1;
swap_texture_srv_desc.Texture2D.PlaneSlice = 0;
swap_texture_srv_desc.Texture2D.ResourceMinLODClamp = 0.0f;
frontbuffer_format = xenos::TextureFormat::k_8_8_8_8;
frontbuffer_width_unscaled = uint32_t(native_desc.Width);
frontbuffer_height_unscaled = native_desc.Height;
frontbuffer_width = frontbuffer_width_unscaled;
frontbuffer_height = frontbuffer_height_unscaled;
using_native_swap_texture = true;
} else {
swap_texture_resource = texture_cache_->RequestSwapTexture(
swap_texture_srv_desc, frontbuffer_format,
&frontbuffer_width_unscaled, &frontbuffer_height_unscaled);
if (!swap_texture_resource) {
// Dump texture fetch constant 0 for debugging
const auto& regs = *register_file_;
auto fetch = regs.GetTextureFetch(0);
REXGPU_ERROR(
"IssueSwap: RequestSwapTexture failed - fetch0: {:08X} {:08X} {:08X} {:08X} {:08X} {:08X}",
fetch.dword_0, fetch.dword_1, fetch.dword_2, fetch.dword_3, fetch.dword_4, fetch.dword_5);
EndSubmission(true);
return false;
}
} else {
REXGPU_ERROR("IssueSwap: RequestSwapTexture SUCCESS {:016X}", (uint64_t)swap_texture_resource);
static bool logged_non_black = false;
if (!logged_non_black) {
REXGPU_ERROR("Non-black frame detected");
logged_non_black = true;
swap_texture_resource = UpdateDirectDisplayTexture(
frontbuffer_ptr, frontbuffer_width, frontbuffer_height,
swap_texture_srv_desc, frontbuffer_format,
&frontbuffer_width_unscaled, &frontbuffer_height_unscaled);
used_direct_display_fallback = swap_texture_resource != nullptr;
if (!swap_texture_resource) {
// Dump texture fetch constant 0 for debugging
const auto& regs = *register_file_;
auto fetch = regs.GetTextureFetch(0);
REXGPU_ERROR(
"IssueSwap: RequestSwapTexture failed - fetch0: {:08X} {:08X} {:08X} {:08X} {:08X} {:08X}",
fetch.dword_0, fetch.dword_1, fetch.dword_2, fetch.dword_3, fetch.dword_4, fetch.dword_5);
EndSubmission(true);
return false;
}
}
}
D3D12_RESOURCE_DESC swap_texture_desc = swap_texture_resource->GetDesc();
@@ -2076,6 +2102,36 @@ bool D3D12CommandProcessor::IssueSwapInternal(uint32_t frontbuffer_ptr,
}
}
system::GraphicsSwapSubmission ac6_submission = {};
uint64_t ac6_submission_sequence = 0;
graphics_system_->GetLastSwapSubmission(&ac6_submission, &ac6_submission_sequence);
if (!ac6_submission_sequence) {
ac6_submission.frontbuffer_virtual_address = frontbuffer_ptr;
ac6_submission.frontbuffer_width = frontbuffer_width;
ac6_submission.frontbuffer_height = frontbuffer_height;
}
auto* ac6_vertex_shader = static_cast<D3D12Shader*>(active_vertex_shader());
auto* ac6_pixel_shader = static_cast<D3D12Shader*>(active_pixel_shader());
uint64_t ac6_vertex_shader_hash =
ac6_vertex_shader ? ac6_vertex_shader->ucode_data_hash() : 0;
uint64_t ac6_pixel_shader_hash =
ac6_pixel_shader ? ac6_pixel_shader->ucode_data_hash() : 0;
ac6::backend::SwapSourceType ac6_swap_source =
ac6::backend::SwapSourceType::kGuestSwapTexture;
if (using_native_swap_texture) {
ac6_swap_source = ac6::backend::SwapSourceType::kExperimentalReplayOverride;
} else if (used_direct_display_fallback) {
ac6_swap_source = ac6::backend::SwapSourceType::kDirectDisplayFallback;
}
ac6::backend::ReportSwapDecision(
ac6_submission, ac6_submission_sequence, ac6_swap_source,
swap_source_scaled, guest_output_width, guest_output_height,
source_width_scaled, source_height_scaled, ac6_vertex_shader_hash,
ac6_pixel_shader_hash);
system::X_VIDEO_MODE video_mode;
kernel::xboxkrnl::VdQueryVideoMode(&video_mode);
uint32_t display_width = std::max(uint32_t(1), uint32_t(video_mode.display_width));
@@ -2083,9 +2139,9 @@ bool D3D12CommandProcessor::IssueSwapInternal(uint32_t frontbuffer_ptr,
bool refreshed = presenter->RefreshGuestOutput(
guest_output_width, guest_output_height, display_width, display_height,
[this, &swap_texture_srv_desc, frontbuffer_format, swap_texture_resource, guest_output_width,
[this, &swap_texture_srv_desc, frontbuffer_format, swap_texture_resource,
using_native_swap_texture, guest_output_width,
guest_output_height](ui::Presenter::GuestOutputRefreshContext& context) -> bool {
REXGPU_ERROR("Inside RefreshGuestOutput lambda for fb!");
const ui::d3d12::D3D12Provider& provider = GetD3D12Provider();
ID3D12Device* device = provider.GetDevice();
@@ -2258,6 +2314,11 @@ bool D3D12CommandProcessor::IssueSwapInternal(uint32_t frontbuffer_ptr,
apply_gamma_descriptors[1].first);
REXGPU_ERROR("RefreshGuestOutput: checkpoint 3.3 - PushTransitionBarrier");
if (using_native_swap_texture) {
PushTransitionBarrier(swap_texture_resource,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE);
}
PushTransitionBarrier(gamma_ramp_buffer_.Get(), gamma_ramp_buffer_state_,
D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE);
gamma_ramp_buffer_state_ = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE;
@@ -2389,6 +2450,11 @@ bool D3D12CommandProcessor::IssueSwapInternal(uint32_t frontbuffer_ptr,
// Need to submit all the commands before giving the image back to the
// presenter so it can submit its own commands for displaying it to the
// queue.
if (using_native_swap_texture) {
PushTransitionBarrier(swap_texture_resource,
D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
}
REXGPU_ERROR("RefreshGuestOutput: checkpoint 6 (SubmitBarriers pre-EndSubmission)");
SubmitBarriers();
REXGPU_ERROR("RefreshGuestOutput: checkpoint 7 (EndSubmission)");
@@ -5205,7 +5271,10 @@ ID3D12Resource* rex::graphics::d3d12::D3D12CommandProcessor::UpdateDirectDisplay
uint32_t height = frontbuffer_height ? frontbuffer_height : 720;
uint32_t bpp = 4;
REXGPU_ERROR("UpdateDirectDisplayTexture: processing fb_ptr={:08X} w={} h={}", frontbuffer_ptr, width, height);
if (REXCVAR_GET(ac6_backend_debug_swap)) {
REXGPU_INFO("UpdateDirectDisplayTexture: fb_ptr={:08X} w={} h={}", frontbuffer_ptr,
width, height);
}
bool is_direct_fb = false;
uint32_t fb_addr = 0;
@@ -5260,7 +5329,9 @@ ID3D12Resource* rex::graphics::d3d12::D3D12CommandProcessor::UpdateDirectDisplay
ID3D12Device* device = GetD3D12Provider().GetDevice();
if (!direct_display_texture_) {
REXGPU_ERROR("UpdateDirectDisplayTexture: creating direct_display_texture_");
if (REXCVAR_GET(ac6_backend_debug_swap)) {
REXGPU_INFO("UpdateDirectDisplayTexture: creating fallback frontbuffer texture");
}
D3D12_RESOURCE_DESC desc = {};
desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
desc.Width = width;
@@ -5295,7 +5366,6 @@ ID3D12Resource* rex::graphics::d3d12::D3D12CommandProcessor::UpdateDirectDisplay
uint32_t row_pitch = rex::align(width * bpp, uint32_t(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT));
bool non_black_detected = false;
uint32_t chunk_height = 256;
for (uint32_t y = 0; y < height; y += chunk_height) {
uint32_t rows_to_copy = std::min(height - y, chunk_height);
@@ -5315,15 +5385,6 @@ ID3D12Resource* rex::graphics::d3d12::D3D12CommandProcessor::UpdateDirectDisplay
std::memcpy(upload_mapping + cy * row_pitch,
src_ptr,
width * bpp);
if (!non_black_detected) {
uint32_t* p = reinterpret_cast<uint32_t*>(src_ptr);
for (uint32_t x = 0; x < width; ++x) {
if ((p[x] & 0xFFFFFF) != 0) {
non_black_detected = true;
break;
}
}
}
}
}
@@ -5349,20 +5410,10 @@ ID3D12Resource* rex::graphics::d3d12::D3D12CommandProcessor::UpdateDirectDisplay
}
}
if (non_black_detected) {
static bool logged_non_black = false;
if (!logged_non_black) {
REXGPU_ERROR("Non-black frame detected");
logged_non_black = true;
}
}
PushTransitionBarrier(direct_display_texture_.Get(), direct_display_texture_state_, D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE);
direct_display_texture_state_ = D3D12_RESOURCE_STATE_NON_PIXEL_SHADER_RESOURCE;
SubmitBarriers();
REXGPU_ERROR("UpdateDirectDisplayTexture: SUCCESS for fb={:08X}", fb_addr);
srv_desc_out.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
srv_desc_out.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srv_desc_out.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
+61 -1
View File
@@ -45,6 +45,43 @@ REXCVAR_DEFINE_STRING(swap_post_effect, "none", "GPU", "Swap post effect: none,
namespace {
constexpr bool kStoreShaders = true;
bool HasMeaningfulTextureFetch(const rex::system::GraphicsSwapSubmission& submission) {
for (uint32_t word : submission.texture_fetch) {
if (word != 0) {
return true;
}
}
return false;
}
rex::system::GraphicsSwapSubmission MergeSwapSubmission(
const rex::system::GraphicsSwapSubmission& base,
const rex::system::GraphicsSwapSubmission& incoming) {
rex::system::GraphicsSwapSubmission merged = base;
if (incoming.frontbuffer_virtual_address) {
merged.frontbuffer_virtual_address = incoming.frontbuffer_virtual_address;
}
if (incoming.frontbuffer_physical_address) {
merged.frontbuffer_physical_address = incoming.frontbuffer_physical_address;
}
if (incoming.frontbuffer_width) {
merged.frontbuffer_width = incoming.frontbuffer_width;
}
if (incoming.frontbuffer_height) {
merged.frontbuffer_height = incoming.frontbuffer_height;
}
if (incoming.texture_format) {
merged.texture_format = incoming.texture_format;
}
if (incoming.color_space) {
merged.color_space = incoming.color_space;
}
if (HasMeaningfulTextureFetch(incoming)) {
merged.texture_fetch = incoming.texture_fetch;
}
return merged;
}
rex::graphics::CommandProcessor::SwapPostEffect ParseSwapPostEffect(
const std::string& effect_name) {
std::string lowered = effect_name;
@@ -284,11 +321,34 @@ void GraphicsSystem::SetInterruptCallback(uint32_t callback, uint32_t user_data)
REXGPU_INFO("SetInterruptCallback({:08X}, {:08X})", callback, user_data);
}
void GraphicsSystem::SetFrameBoundaryCallback(std::function<void(rex::memory::Memory*)> callback) {
frame_boundary_callback_ = std::move(callback);
}
bool GraphicsSystem::HandleVideoSwap(const system::GraphicsSwapSubmission& submission) {
(void)submission;
{
std::lock_guard<std::mutex> lock(last_swap_submission_mutex_);
last_swap_submission_ = MergeSwapSubmission(last_swap_submission_, submission);
++last_swap_submission_sequence_;
}
if (frame_boundary_callback_) {
frame_boundary_callback_(memory_);
}
return false;
}
bool GraphicsSystem::GetLastSwapSubmission(system::GraphicsSwapSubmission* out_submission,
uint64_t* out_sequence) const {
std::lock_guard<std::mutex> lock(last_swap_submission_mutex_);
if (out_submission) {
*out_submission = last_swap_submission_;
}
if (out_sequence) {
*out_sequence = last_swap_submission_sequence_;
}
return last_swap_submission_sequence_ != 0;
}
void GraphicsSystem::DispatchInterruptCallback(uint32_t source, uint32_t cpu) {
if (!interrupt_callback_) {
return;
@@ -60,6 +60,11 @@ int WINAPI wWinMain(HINSTANCE hinstance, HINSTANCE hinstance_prev, LPWSTR comman
auto remaining = rex::cvar::Init(static_cast<int>(argv_ptrs.size()), argv_ptrs.data());
rex::cvar::ApplyEnvironment();
// Force logging to a file immediately
auto log_config = rex::BuildLogConfig("ac6_boot.log", "info", {});
rex::InitLogging(log_config);
REXLOG_INFO("wWinMain started");
// Allocate a console for debugging if enabled
if (REXCVAR_GET(enable_console)) {
AllocConsole();
@@ -73,14 +78,19 @@ int WINAPI wWinMain(HINSTANCE hinstance, HINSTANCE hinstance_prev, LPWSTR comman
int result;
{
REXLOG_INFO("wWinMain: Creating Win32WindowedAppContext...");
rex::ui::Win32WindowedAppContext app_context(hinstance, show_cmd);
// TODO(Triang3l): Initialize creates a window. Set DPI awareness via the
// manifest.
REXLOG_INFO("wWinMain: Initializing app context...");
if (!app_context.Initialize()) {
REXLOG_ERROR("wWinMain: app_context.Initialize failed");
return EXIT_FAILURE;
}
REXLOG_INFO("wWinMain: Getting app creator...");
std::unique_ptr<rex::ui::WindowedApp> app = rex::ui::GetWindowedAppCreator()(app_context);
REXLOG_INFO("wWinMain: App instance created");
// Match remaining positional args to app's expected options
const auto& option_names = app->GetPositionalOptions();
@@ -93,14 +103,24 @@ int WINAPI wWinMain(HINSTANCE hinstance, HINSTANCE hinstance_prev, LPWSTR comman
// Initialize COM on the UI thread with the apartment-threaded concurrency
// model, so dialogs can be used.
REXLOG_INFO("wWinMain: Initializing COM...");
if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED))) {
REXLOG_ERROR("wWinMain: CoInitializeEx failed");
return EXIT_FAILURE;
}
// TODO: Port InitializeWin32App from Xenia
// rex::InitializeWin32App(app->GetName());
result = app->OnInitialize() ? app_context.RunMainMessageLoop() : EXIT_FAILURE;
REXLOG_INFO("wWinMain: Calling app->OnInitialize()...");
if (!app->OnInitialize()) {
REXLOG_ERROR("wWinMain: app->OnInitialize failed");
return EXIT_FAILURE;
}
REXLOG_INFO("wWinMain: Entering main message loop...");
result = app_context.RunMainMessageLoop();
REXLOG_INFO("wWinMain: Main message loop exited with result {}", result);
app->InvokeOnDestroy();
}