Initial source-only import

This commit is contained in:
salh
2026-04-14 12:37:39 +03:00
parent 44d4865b5a
commit 77f5da905a
14 changed files with 2362 additions and 81 deletions
+20
View File
@@ -7,9 +7,16 @@ generated/
# Game assets (user-supplied)
assets/
*.xex
*.iso
*.god
*.zar
*.pkg
*.bin
# VS local settings
.vs/
.vscode/
# Copilot and debugging workflow
.github/instructions/
@@ -19,3 +26,16 @@ CMakeUserPresets.json
# Local git worktrees
.worktrees/
# Local notes, backups, and scratch work
.claude/
backup/
OLD_XENON_BUILD/
rexglue_sdk_new/
autoresearch/
*.bak
*.tmp
tmp_*.py
build_*.txt
*.log
New Text Document.txt
+2 -1
View File
@@ -14,6 +14,8 @@ include(generated/rexglue.cmake)
# Sources
set(AC6RECOMP_SOURCES
src/main.cpp
src/ac6_native_graphics.cpp
src/ac6_native_graphics_overlay.cpp
src/render_hooks.cpp
src/d3d_hooks.cpp
)
@@ -25,4 +27,3 @@ else()
endif()
rexglue_setup_target(ac6recomp)
+35 -13
View File
@@ -1,23 +1,35 @@
# AC6Recomp
> [!CAUTION]
> This project is a work in progress, so a stable experience isn't guaranteed yet. It progresses in-game, but with likely bugs and crashes and more testing is needed.
> This project is still work in progress. It can boot and run in-game, but bugs, crashes, and missing functionality should be expected.
Recompiled using the [ReXGlue SDK](https://github.com/rexglue/rexglue-sdk).
## Repository policy
This repository is intended to contain source code only.
Do not commit or redistribute:
- retail game data
- `default.xex`
- disc images, packages, title updates, or firmware files
- console keys or any other proprietary Microsoft / publisher material
Users must supply their own legally obtained game files locally. This repository does not include those files
## Prerequisites
- [CMake](https://cmake.org/) 3.25+
- [Ninja](https://ninja-build.org/)
- [Clang](https://releases.llvm.org/) (LLVM/Clang toolchain)
- A legally obtained copy of the game.
- A legally obtained copy of the game, prepared by the end user outside this repository
## Building
## Clone
Clone the repository with submodules:
```bash
git clone --recursive https://github.com/rapidsamphire/AC6Recomp.git
git clone --recursive <your-repo-url>
cd AC6Recomp
```
@@ -27,35 +39,45 @@ If you already cloned without `--recursive`:
git submodule update --init --recursive
```
### Assets
## Local file layout
You need the game's original Xbox 360 disc image (ISO). Tools to extract it can be found [here](https://consolemods.org/wiki/Xbox:ISO_Extraction_%26_Repacking).
Place your personally obtained game files in `assets/` so they are available only on your machine and remain untracked by Git.
After extraction, drop both the game assets and `default.xex` into the `assets/` directory.
Expected minimum layout:
### Generate recompiled code
```text
assets/
default.xex
...
```
The codegen config expects `assets/default.xex`.
## Build
Generate recompiled code:
```bash
cmake --preset win-amd64-relwithdebinfo
cmake --build --preset win-amd64-relwithdebinfo --target ac6recomp_codegen
```
### Build
Build the project:
```bash
cmake --build --preset win-amd64-relwithdebinfo
```
(Relwithdebinfo is the recommended build preset as of now.)
`RelWithDebInfo` is the recommended preset at the moment.
The executable will be in `out/build/win-amd64-relwithdebinfo/`.
The executable will be produced in `out/build/win-amd64-relwithdebinfo/`.
### Running
## Run
```bash
./out/build/win-amd64-relwithdebinfo/ac6recomp assets
```
### Linux
## Linux
Replace `win-amd64-relwithdebinfo` with `linux-amd64-relwithdebinfo` in the commands above.
File diff suppressed because it is too large Load Diff
+84
View File
@@ -0,0 +1,84 @@
#pragma once
#include <cstdint>
#include <rex/cvar.h>
#include <rex/runtime.h>
#include "d3d_state.h"
REXCVAR_DECLARE(bool, ac6_allow_gpu_trace_stream);
namespace ac6::graphics {
struct SelectedPassPreviewSummary {
bool valid{false};
uint32_t draw_count{0};
uint32_t clear_count{0};
uint32_t resolve_count{0};
uint32_t sampled_clear_color_count{0};
uint32_t sampled_clear_rect_count{0};
uint32_t first_clear_color{0};
uint32_t last_clear_color{0};
bool using_clear_fill{false};
bool using_raster_replay{false};
};
struct NativeReplayPlanSummary {
bool valid{false};
uint64_t frame_index{0};
uint32_t pass_count{0};
uint32_t swap_size_match_pass_count{0};
uint32_t draw_event_count{0};
uint32_t clear_event_count{0};
uint32_t resolve_event_count{0};
uint32_t largest_pass_draw_count{0};
uint32_t first_pass_draw_count{0};
uint32_t last_pass_draw_count{0};
uint32_t selected_pass_index{0};
uint32_t selected_pass_score{0};
uint32_t selected_pass_start_sequence{0};
uint32_t selected_pass_end_sequence{0};
uint32_t selected_pass_draw_count{0};
uint32_t selected_pass_clear_count{0};
uint32_t selected_pass_resolve_count{0};
uint32_t selected_pass_streak{0};
bool selected_pass_is_last{false};
bool selected_pass_has_resolve{false};
bool selected_pass_is_stable{false};
uint32_t present_candidate_rt0{0};
uint32_t present_candidate_depth_stencil{0};
uint32_t present_candidate_viewport_x{0};
uint32_t present_candidate_viewport_y{0};
uint32_t present_candidate_viewport_width{0};
uint32_t present_candidate_viewport_height{0};
bool present_candidate_matches_swap_size{false};
};
struct NativeGraphicsStatusSnapshot {
bool bootstrap_enabled{false};
bool backend_active{false};
bool placeholder_present_enabled{false};
bool provider_ready{false};
bool presenter_ready{false};
bool placeholder_resources_initialized{false};
bool last_swap_intercepted{false};
bool last_swap_fell_back{false};
uint64_t total_swap_count{0};
uint64_t intercepted_swap_count{0};
uint64_t fallback_swap_count{0};
uint32_t last_frontbuffer_virtual_address{0};
uint32_t last_frontbuffer_physical_address{0};
uint32_t last_frontbuffer_width{0};
uint32_t last_frontbuffer_height{0};
uint32_t last_texture_format{0};
uint32_t last_color_space{0};
d3d::FrameCaptureSummary capture_summary{};
NativeReplayPlanSummary replay_plan{};
SelectedPassPreviewSummary selected_pass_preview{};
};
void ConfigureGraphicsBackend(rex::RuntimeConfig& config);
NativeGraphicsStatusSnapshot GetNativeGraphicsStatus();
} // namespace ac6::graphics
+206
View File
@@ -0,0 +1,206 @@
#include "ac6_native_graphics_overlay.h"
#include <rex/graphics/flags.h>
#include <rex/ui/keybinds.h>
#include <imgui.h>
#include "ac6_native_graphics.h"
#include "d3d_hooks.h"
#include "render_hooks.h"
namespace ac6::graphics {
namespace {
const char* DescribeNativeMode(const NativeGraphicsStatusSnapshot& status) {
if (!status.bootstrap_enabled) {
return "Disabled";
}
if (!status.backend_active) {
return "Configured, backend inactive";
}
if (status.placeholder_present_enabled) {
return "Diagnostic takeover";
}
return "Observe only";
}
const char* DescribeLastSwapOutcome(const NativeGraphicsStatusSnapshot& status) {
if (!status.total_swap_count) {
return "No direct swaps yet";
}
if (status.last_swap_intercepted) {
return "Presented through native diagnostic path";
}
if (status.last_swap_fell_back) {
return "Handled by legacy PM4 presentation";
}
return "Observed";
}
} // namespace
NativeGraphicsStatusDialog::NativeGraphicsStatusDialog(rex::ui::ImGuiDrawer* imgui_drawer)
: ImGuiDialog(imgui_drawer) {
rex::ui::RegisterBind("bind_ac6_native_graphics_overlay", "F4",
"Toggle AC6 native graphics status overlay",
[this] { ToggleVisible(); });
}
NativeGraphicsStatusDialog::~NativeGraphicsStatusDialog() {
rex::ui::UnregisterBind("bind_ac6_native_graphics_overlay");
}
void NativeGraphicsStatusDialog::OnDraw(ImGuiIO& io) {
if (!visible_) {
return;
}
NativeGraphicsStatusSnapshot native_status = GetNativeGraphicsStatus();
ac6::FrameStats frame_stats = ac6::GetFrameStats();
ac6::d3d::DrawStatsSnapshot draw_stats = ac6::d3d::GetDrawStats();
ImGui::SetNextWindowPos(ImVec2(10, 84), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(430, 486), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowBgAlpha(0.72f);
if (!ImGui::Begin("AC6 Native Graphics##status", &visible_, ImGuiWindowFlags_NoCollapse)) {
ImGui::End();
return;
}
ImGui::Text("Mode: %s", DescribeNativeMode(native_status));
ImGui::Text("Backend: %s | Provider: %s | Presenter: %s",
native_status.backend_active ? "active" : "inactive",
native_status.provider_ready ? "ready" : "missing",
native_status.presenter_ready ? "ready" : "missing");
ImGui::Text("Placeholder resources: %s",
native_status.placeholder_resources_initialized ? "initialized" : "inactive");
ImGui::Text("GPU trace stream: %s | allow=%s", REXCVAR_GET(trace_gpu_stream) ? "on" : "off",
REXCVAR_GET(ac6_allow_gpu_trace_stream) ? "true" : "false");
ImGui::Separator();
ImGui::Text("Direct swaps: %llu total | %llu native | %llu legacy",
static_cast<unsigned long long>(native_status.total_swap_count),
static_cast<unsigned long long>(native_status.intercepted_swap_count),
static_cast<unsigned long long>(native_status.fallback_swap_count));
ImGui::Text("Last outcome: %s", DescribeLastSwapOutcome(native_status));
ImGui::Text("Last frontbuffer: %ux%u fmt %08X cs %u", native_status.last_frontbuffer_width,
native_status.last_frontbuffer_height, native_status.last_texture_format,
native_status.last_color_space);
ImGui::Text("Last FB VA/PA: %08X / %08X", native_status.last_frontbuffer_virtual_address,
native_status.last_frontbuffer_physical_address);
ImGui::Separator();
ImGui::Text("Capture: %s frame=%llu draws=%u clears=%u resolves=%u",
native_status.capture_summary.capture_enabled ? "enabled" : "disabled",
static_cast<unsigned long long>(native_status.capture_summary.frame_index),
native_status.capture_summary.draw_count, native_status.capture_summary.clear_count,
native_status.capture_summary.resolve_count);
if (native_status.capture_summary.record_signature_valid) {
ImGui::Text("Record signature: %016llX",
static_cast<unsigned long long>(native_status.capture_summary.record_signature));
ImGui::Text("Recorded draw mix: indexed=%u shared=%u primitive=%u",
native_status.capture_summary.indexed_draw_count,
native_status.capture_summary.indexed_shared_draw_count,
native_status.capture_summary.primitive_draw_count);
ImGui::Text("Recorded RT0s: unique=%u switches=%u",
native_status.capture_summary.unique_rt0_count,
native_status.capture_summary.rt0_switch_count);
ImGui::Text("Recorded RT0 first/last: %08X / %08X",
native_status.capture_summary.first_draw_render_target_0,
native_status.capture_summary.last_draw_render_target_0);
} else {
ImGui::TextUnformatted("Record signature: unavailable (enable ac6_render_capture)");
}
ImGui::Text("Frame-end RT0/DS: %08X / %08X",
native_status.capture_summary.frame_end_render_target_0,
native_status.capture_summary.frame_end_depth_stencil);
ImGui::Text("Frame-end viewport: %ux%u | RTs=%u tex=%u fetch=%u",
native_status.capture_summary.frame_end_viewport_width,
native_status.capture_summary.frame_end_viewport_height,
native_status.capture_summary.frame_end_render_target_count,
native_status.capture_summary.frame_end_texture_count,
native_status.capture_summary.frame_end_texture_fetch_count);
ImGui::Text("Frame-end streams=%u samplers=%u | last draw prim=%u count=%u flags=%u",
native_status.capture_summary.frame_end_stream_count,
native_status.capture_summary.frame_end_sampler_count,
native_status.capture_summary.last_draw_primitive_type,
native_status.capture_summary.last_draw_count,
native_status.capture_summary.last_draw_flags);
ImGui::Separator();
if (native_status.replay_plan.valid) {
ImGui::Text("Replay plan: frame=%llu passes=%u draws=%u clears=%u resolves=%u",
static_cast<unsigned long long>(native_status.replay_plan.frame_index),
native_status.replay_plan.pass_count,
native_status.replay_plan.draw_event_count,
native_status.replay_plan.clear_event_count,
native_status.replay_plan.resolve_event_count);
ImGui::Text("Swap-size matching passes: %u | selected=%u score=%u",
native_status.replay_plan.swap_size_match_pass_count,
native_status.replay_plan.selected_pass_index,
native_status.replay_plan.selected_pass_score);
ImGui::Text("Pass draw counts: first=%u last=%u largest=%u",
native_status.replay_plan.first_pass_draw_count,
native_status.replay_plan.last_pass_draw_count,
native_status.replay_plan.largest_pass_draw_count);
ImGui::Text("Selected pass seq: %u..%u | draws=%u clears=%u resolves=%u",
native_status.replay_plan.selected_pass_start_sequence,
native_status.replay_plan.selected_pass_end_sequence,
native_status.replay_plan.selected_pass_draw_count,
native_status.replay_plan.selected_pass_clear_count,
native_status.replay_plan.selected_pass_resolve_count);
ImGui::Text("Selected pass flags: last=%s resolve=%s stable=%s streak=%u",
native_status.replay_plan.selected_pass_is_last ? "yes" : "no",
native_status.replay_plan.selected_pass_has_resolve ? "yes" : "no",
native_status.replay_plan.selected_pass_is_stable ? "yes" : "no",
native_status.replay_plan.selected_pass_streak);
ImGui::Text("Present candidate RT0/DS: %08X / %08X",
native_status.replay_plan.present_candidate_rt0,
native_status.replay_plan.present_candidate_depth_stencil);
ImGui::Text("Present candidate viewport: %u,%u %ux%u | swap match=%s",
native_status.replay_plan.present_candidate_viewport_x,
native_status.replay_plan.present_candidate_viewport_y,
native_status.replay_plan.present_candidate_viewport_width,
native_status.replay_plan.present_candidate_viewport_height,
native_status.replay_plan.present_candidate_matches_swap_size ? "yes" : "no");
if (native_status.selected_pass_preview.valid) {
ImGui::Text("Selected-pass preview: source=%s sampled clears=%u captured rects=%u",
native_status.selected_pass_preview.using_clear_fill
? "captured clear colors"
: "fallback candidate tint",
native_status.selected_pass_preview.sampled_clear_color_count,
native_status.selected_pass_preview.sampled_clear_rect_count);
ImGui::Text("Preview events: draws=%u clears=%u resolves=%u",
native_status.selected_pass_preview.draw_count,
native_status.selected_pass_preview.clear_count,
native_status.selected_pass_preview.resolve_count);
ImGui::Text("Preview renderer: %s",
native_status.selected_pass_preview.using_raster_replay
? "offscreen raster replay"
: "legacy diagnostic fill");
if (native_status.selected_pass_preview.clear_count) {
ImGui::Text("Preview clear colors: first=%08X last=%08X",
native_status.selected_pass_preview.first_clear_color,
native_status.selected_pass_preview.last_clear_color);
}
}
} else {
ImGui::TextUnformatted("Replay plan: unavailable (no captured frame events yet)");
}
ImGui::Separator();
ImGui::Text("Guest frame: %.1f FPS (%.2f ms) frame=%llu", frame_stats.fps,
frame_stats.frame_time_ms, static_cast<unsigned long long>(frame_stats.frame_count));
ImGui::Text("D3D stats: draws=%u clears=%u resolves=%u", draw_stats.draw_calls,
draw_stats.clear_calls, draw_stats.resolve_calls);
ImGui::Text("Indexed=%u shared=%u primitive=%u", draw_stats.draw_calls_indexed,
draw_stats.draw_calls_indexed_shared, draw_stats.draw_calls_primitive);
ImGui::Separator();
ImGui::TextUnformatted("F4 toggles this panel. F3 toggles the base debug overlay.");
ImGui::End();
}
} // namespace ac6::graphics
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <memory>
#include <rex/ui/imgui_dialog.h>
namespace rex::ui {
class ImGuiDrawer;
}
namespace ac6::graphics {
class NativeGraphicsStatusDialog final : public rex::ui::ImGuiDialog {
public:
explicit NativeGraphicsStatusDialog(rex::ui::ImGuiDrawer* imgui_drawer);
~NativeGraphicsStatusDialog();
void ToggleVisible() { visible_ = !visible_; }
bool IsVisible() const { return visible_; }
protected:
void OnDraw(ImGuiIO& io) override;
private:
bool visible_ = false;
};
} // namespace ac6::graphics
+39 -2
View File
@@ -1,13 +1,20 @@
#pragma once
#include <string>
#include <rex/cvar.h>
#include <rex/graphics/flags.h>
#include <rex/logging.h>
#include <rex/rex_app.h>
#include <rex/ui/overlay/debug_overlay.h>
#include <rex/version.h>
#include "ac6_native_graphics.h"
#include "ac6_native_graphics_overlay.h"
#include "render_hooks.h"
REXCVAR_DECLARE(bool, vfetch_index_rounding_bias);
REXCVAR_DECLARE(bool, guest_vblank_sync_to_refresh);
class Ac6recompApp : public rex::ReXApp {
public:
@@ -22,8 +29,19 @@ class Ac6recompApp : public rex::ReXApp {
protected:
void OnPreSetup(rex::RuntimeConfig& config) override {
REXCVAR_SET(vfetch_index_rounding_bias, true);
REXCVAR_SET(ac6_unlock_fps, true);
REXCVAR_SET(vsync, false);
if (REXCVAR_GET(trace_gpu_stream) && !REXCVAR_GET(ac6_allow_gpu_trace_stream)) {
gpu_trace_stream_blocked_ = true;
REXLOG_WARN(
"AC6 safety guard disabled trace_gpu_stream to avoid unstable GPU "
"trace compression during native graphics experiments. Set "
"ac6_allow_gpu_trace_stream=true to re-enable it explicitly.");
REXCVAR_SET(trace_gpu_stream, false);
}
ac6::graphics::ConfigureGraphicsBackend(config);
if (REXCVAR_GET(ac6_unlock_fps)) {
REXCVAR_SET(vsync, false);
REXCVAR_SET(guest_vblank_sync_to_refresh, false);
}
}
void OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) override {
@@ -31,6 +49,25 @@ class Ac6recompApp : public rex::ReXApp {
auto gs = ac6::GetFrameStats();
return rex::ui::FrameStats{gs.frame_time_ms, gs.fps, gs.frame_count};
});
native_graphics_status_dialog_ =
std::make_unique<ac6::graphics::NativeGraphicsStatusDialog>(drawer);
if (window()) {
std::string title = std::string(GetName()) + " " + REXGLUE_BUILD_TITLE;
auto native_status = ac6::graphics::GetNativeGraphicsStatus();
if (native_status.bootstrap_enabled) {
title += native_status.placeholder_present_enabled ? " | native swap takeover"
: " | native swap observe";
}
if (gpu_trace_stream_blocked_) {
title += " | trace stream blocked";
}
window()->SetTitle(title);
}
}
private:
bool gpu_trace_stream_blocked_ = false;
std::unique_ptr<ac6::graphics::NativeGraphicsStatusDialog> native_graphics_status_dialog_;
};
+323 -27
View File
@@ -1,11 +1,16 @@
#include "d3d_hooks.h"
#include <algorithm>
#include <shared_mutex>
#include <rex/cvar.h>
#include <rex/logging.h>
#include <rex/ppc.h>
REXCVAR_DEFINE_BOOL(ac6_d3d_trace, false, "AC6/Render",
"Log every D3D device state change and draw call");
REXCVAR_DEFINE_BOOL(ac6_render_capture, false, "AC6/Render",
"Capture per-frame draw, clear, and resolve records for the native renderer");
namespace {
const rex::LogCategoryId kLogGPU = rex::log::GPU;
@@ -13,10 +18,236 @@ const rex::LogCategoryId kLogGPU = rex::log::GPU;
namespace {
std::shared_mutex g_shadow_mutex;
ac6::d3d::ShadowState g_shadow{};
ac6::d3d::DrawStats g_live_stats{};
std::shared_mutex g_snapshot_mutex;
ac6::d3d::DrawStatsSnapshot g_snapshot{};
std::shared_mutex g_capture_mutex;
ac6::d3d::FrameCaptureSnapshot g_capture_snapshot{};
ac6::d3d::FrameCaptureSummary g_capture_summary{};
uint64_t g_capture_live_frame_index = 1;
uint32_t g_capture_live_sequence = 0;
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) {
uint32_t count = 0;
for (const T& value : values) {
if (value) {
++count;
}
}
return count;
}
uint32_t CountNonZeroStreams(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 CountNonZeroSamplers(
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 HashDrawRecord(uint64_t& hash, const ac6::d3d::DrawCallRecord& draw) {
HashU32(hash, static_cast<uint32_t>(draw.kind));
HashU32(hash, draw.primitive_type);
HashU32(hash, draw.start);
HashU32(hash, draw.count);
HashU32(hash, draw.flags);
HashU32(hash, draw.shadow_state.render_targets[0]);
HashU32(hash, draw.shadow_state.depth_stencil);
HashU32(hash, draw.shadow_state.viewport.width);
HashU32(hash, draw.shadow_state.viewport.height);
}
ac6::d3d::FrameCaptureSummary MakeFrameCaptureSummary(
const ac6::d3d::FrameCaptureSnapshot& frame_capture) {
ac6::d3d::FrameCaptureSummary summary;
summary.capture_enabled = REXCVAR_GET(ac6_render_capture);
summary.frame_index = frame_capture.frame_index;
summary.draw_count = static_cast<uint32_t>(frame_capture.draws.size());
summary.clear_count = static_cast<uint32_t>(frame_capture.clears.size());
summary.resolve_count = static_cast<uint32_t>(frame_capture.resolves.size());
summary.frame_end_render_target_count =
CountNonZero(frame_capture.frame_end_shadow.render_targets);
summary.frame_end_texture_count = CountNonZero(frame_capture.frame_end_shadow.textures);
summary.frame_end_stream_count = CountNonZeroStreams(frame_capture.frame_end_shadow.streams);
summary.frame_end_sampler_count = CountNonZeroSamplers(frame_capture.frame_end_shadow.samplers);
summary.frame_end_texture_fetch_count =
CountNonZero(frame_capture.frame_end_shadow.texture_fetch_ptrs);
summary.frame_end_render_target_0 = frame_capture.frame_end_shadow.render_targets[0];
summary.frame_end_depth_stencil = frame_capture.frame_end_shadow.depth_stencil;
summary.frame_end_viewport_width = frame_capture.frame_end_shadow.viewport.width;
summary.frame_end_viewport_height = frame_capture.frame_end_shadow.viewport.height;
if (!frame_capture.draws.empty()) {
summary.first_draw_render_target_0 =
frame_capture.draws.front().shadow_state.render_targets[0];
summary.last_draw_render_target_0 =
frame_capture.draws.back().shadow_state.render_targets[0];
const auto& last_draw = frame_capture.draws.back();
summary.last_draw_primitive_type = last_draw.primitive_type;
summary.last_draw_count = last_draw.count;
summary.last_draw_flags = last_draw.flags;
}
if (summary.capture_enabled && !frame_capture.draws.empty()) {
constexpr uint64_t kFnvOffsetBasis = 1469598103934665603ull;
uint64_t signature = kFnvOffsetBasis;
std::vector<uint32_t> unique_rt0s;
uint32_t previous_rt0 = frame_capture.draws.front().shadow_state.render_targets[0];
for (const auto& draw : frame_capture.draws) {
switch (draw.kind) {
case ac6::d3d::DrawCallKind::kIndexed:
++summary.indexed_draw_count;
break;
case ac6::d3d::DrawCallKind::kIndexedShared:
++summary.indexed_shared_draw_count;
break;
case ac6::d3d::DrawCallKind::kPrimitive:
++summary.primitive_draw_count;
break;
}
uint32_t rt0 = draw.shadow_state.render_targets[0];
if (std::find(unique_rt0s.begin(), unique_rt0s.end(), rt0) == unique_rt0s.end()) {
unique_rt0s.push_back(rt0);
}
if (rt0 != previous_rt0) {
++summary.rt0_switch_count;
previous_rt0 = rt0;
}
HashDrawRecord(signature, draw);
}
summary.unique_rt0_count = static_cast<uint32_t>(unique_rt0s.size());
summary.record_signature = signature;
summary.record_signature_valid = true;
}
return summary;
}
void RememberDevice(uint32_t device) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.device = device;
}
ac6::d3d::ShadowState SnapshotShadowState(uint32_t device) {
std::shared_lock<std::shared_mutex> lock(g_shadow_mutex);
ac6::d3d::ShadowState shadow = g_shadow;
if (device != 0) {
shadow.device = device;
}
return shadow;
}
ac6::d3d::DrawStatsSnapshot SnapshotDrawStats() {
return ac6::d3d::DrawStatsSnapshot{
g_live_stats.draw_calls.load(std::memory_order_relaxed),
g_live_stats.draw_calls_indexed.load(std::memory_order_relaxed),
g_live_stats.draw_calls_indexed_shared.load(std::memory_order_relaxed),
g_live_stats.draw_calls_primitive.load(std::memory_order_relaxed),
g_live_stats.total_indices.load(std::memory_order_relaxed),
g_live_stats.total_vertices.load(std::memory_order_relaxed),
g_live_stats.set_texture_calls.load(std::memory_order_relaxed),
g_live_stats.set_render_target_calls.load(std::memory_order_relaxed),
g_live_stats.set_depth_stencil_calls.load(std::memory_order_relaxed),
g_live_stats.set_vertex_decl_calls.load(std::memory_order_relaxed),
g_live_stats.set_index_buffer_calls.load(std::memory_order_relaxed),
g_live_stats.set_stream_source_calls.load(std::memory_order_relaxed),
g_live_stats.set_viewport_calls.load(std::memory_order_relaxed),
g_live_stats.set_sampler_state_calls.load(std::memory_order_relaxed),
g_live_stats.set_texture_fetch_calls.load(std::memory_order_relaxed),
g_live_stats.clear_calls.load(std::memory_order_relaxed),
g_live_stats.resolve_calls.load(std::memory_order_relaxed),
};
}
void CaptureDrawCall(ac6::d3d::DrawCallKind kind, uint32_t device, uint32_t primitive_type,
uint32_t start, uint32_t count, uint32_t flags) {
if (!REXCVAR_GET(ac6_render_capture)) {
return;
}
ac6::d3d::DrawCallRecord record;
record.kind = kind;
record.primitive_type = primitive_type;
record.start = start;
record.count = count;
record.flags = flags;
record.shadow_state = SnapshotShadowState(device);
std::unique_lock<std::shared_mutex> lock(g_capture_mutex);
record.sequence = ++g_capture_live_sequence;
g_live_draws.push_back(std::move(record));
}
void CaptureClear(uint8_t* base, uint32_t device, uint32_t rect_count, uint32_t rects_ptr,
uint32_t flags, uint32_t color, uint32_t stencil, float depth) {
if (!REXCVAR_GET(ac6_render_capture)) {
return;
}
ac6::d3d::ClearRecord record;
record.rect_count = rect_count;
record.flags = flags;
record.color = color;
record.stencil = stencil;
record.depth = depth;
record.shadow_state = SnapshotShadowState(device);
if (rects_ptr && rect_count) {
const uint32_t captured_rect_count =
std::min<uint32_t>(rect_count, ac6::d3d::kMaxClearRectsPerRecord);
record.captured_rect_count = captured_rect_count;
for (uint32_t i = 0; i < captured_rect_count; ++i) {
const uint32_t rect_ptr = rects_ptr + i * 16;
record.rects[i].left = PPC_LOAD_U32(rect_ptr + 0);
record.rects[i].top = PPC_LOAD_U32(rect_ptr + 4);
record.rects[i].right = PPC_LOAD_U32(rect_ptr + 8);
record.rects[i].bottom = PPC_LOAD_U32(rect_ptr + 12);
}
}
std::unique_lock<std::shared_mutex> lock(g_capture_mutex);
record.sequence = ++g_capture_live_sequence;
g_live_clears.push_back(std::move(record));
}
void CaptureResolve(uint32_t device) {
if (!REXCVAR_GET(ac6_render_capture)) {
return;
}
ac6::d3d::ResolveRecord record;
record.shadow_state = SnapshotShadowState(device);
std::unique_lock<std::shared_mutex> lock(g_capture_mutex);
record.sequence = ++g_capture_live_sequence;
g_live_resolves.push_back(std::move(record));
}
} // namespace
PPC_EXTERN_FUNC(__imp__rex_sub_821DEF18); // DrawIndexedVertices
@@ -44,10 +275,13 @@ PPC_FUNC_IMPL(rex_sub_821DEF18) {
PPC_FUNC_PROLOGUE();
uint32_t index_count = ctx.r6.u32;
RememberDevice(ctx.r3.u32);
g_live_stats.draw_calls.fetch_add(1, std::memory_order_relaxed);
g_live_stats.draw_calls_indexed.fetch_add(1, std::memory_order_relaxed);
g_live_stats.total_indices.fetch_add(index_count, std::memory_order_relaxed);
CaptureDrawCall(ac6::d3d::DrawCallKind::kIndexed, ctx.r3.u32, ctx.r4.u32, ctx.r5.u32,
index_count, 0);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
@@ -63,10 +297,13 @@ PPC_FUNC_IMPL(rex_sub_821DF300) {
PPC_FUNC_PROLOGUE();
uint32_t index_count = ctx.r7.u32;
RememberDevice(ctx.r3.u32);
g_live_stats.draw_calls.fetch_add(1, std::memory_order_relaxed);
g_live_stats.draw_calls_indexed_shared.fetch_add(1, std::memory_order_relaxed);
g_live_stats.total_indices.fetch_add(index_count, std::memory_order_relaxed);
CaptureDrawCall(ac6::d3d::DrawCallKind::kIndexedShared, ctx.r3.u32, ctx.r4.u32, ctx.r6.u32,
index_count, ctx.r5.u32);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
@@ -83,8 +320,10 @@ PPC_FUNC_IMPL(rex_sub_821DD0A8) {
uint32_t slot = ctx.r4.u32;
uint32_t texture_ptr = ctx.r5.u32;
RememberDevice(ctx.r3.u32);
if (slot < ac6::d3d::kMaxTextures) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.textures[slot] = texture_ptr;
}
g_live_stats.set_texture_calls.fetch_add(1, std::memory_order_relaxed);
@@ -104,8 +343,10 @@ PPC_FUNC_IMPL(rex_sub_821D95C8) {
uint32_t index = ctx.r4.u32;
uint32_t surface = ctx.r5.u32;
RememberDevice(ctx.r3.u32);
if (index < ac6::d3d::kMaxRenderTargets) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.render_targets[index] = surface;
}
g_live_stats.set_render_target_calls.fetch_add(1, std::memory_order_relaxed);
@@ -124,7 +365,11 @@ PPC_FUNC_IMPL(rex_sub_821D9D38) {
PPC_FUNC_PROLOGUE();
uint32_t surface = ctx.r4.u32;
g_shadow.depth_stencil = surface;
RememberDevice(ctx.r3.u32);
{
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.depth_stencil = surface;
}
g_live_stats.set_depth_stencil_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
@@ -140,7 +385,11 @@ PPC_FUNC_IMPL(rex_sub_821DE7D0) {
PPC_FUNC_PROLOGUE();
uint32_t decl = ctx.r4.u32;
g_shadow.vertex_declaration = decl;
RememberDevice(ctx.r3.u32);
{
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.vertex_declaration = decl;
}
g_live_stats.set_vertex_decl_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
@@ -156,7 +405,11 @@ PPC_FUNC_IMPL(rex_sub_821DD1C8) {
PPC_FUNC_PROLOGUE();
uint32_t buffer = ctx.r4.u32;
g_shadow.index_buffer = buffer;
RememberDevice(ctx.r3.u32);
{
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.index_buffer = buffer;
}
g_live_stats.set_index_buffer_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
@@ -171,10 +424,14 @@ PPC_FUNC_IMPL(rex_sub_821DD1C8) {
PPC_FUNC_IMPL(rex_sub_821DA698) {
PPC_FUNC_PROLOGUE();
g_shadow.viewport.x = ctx.r4.u32;
g_shadow.viewport.y = ctx.r5.u32;
g_shadow.viewport.width = ctx.r6.u32;
g_shadow.viewport.height = ctx.r7.u32;
RememberDevice(ctx.r3.u32);
{
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.viewport.x = ctx.r4.u32;
g_shadow.viewport.y = ctx.r5.u32;
g_shadow.viewport.width = ctx.r6.u32;
g_shadow.viewport.height = ctx.r7.u32;
}
g_live_stats.set_viewport_calls.fetch_add(1, std::memory_order_relaxed);
if (REXCVAR_GET(ac6_d3d_trace)) {
@@ -191,7 +448,9 @@ PPC_FUNC_IMPL(rex_sub_821DA698) {
PPC_FUNC_IMPL(rex_sub_821E2BB8) {
PPC_FUNC_PROLOGUE();
RememberDevice(ctx.r3.u32);
g_live_stats.resolve_calls.fetch_add(1, std::memory_order_relaxed);
CaptureResolve(ctx.r3.u32);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU, "Resolve");
@@ -207,10 +466,13 @@ PPC_FUNC_IMPL(rex_sub_821DEA48) {
uint32_t prim_type = ctx.r4.u32;
uint32_t vertex_count = ctx.r5.u32;
RememberDevice(ctx.r3.u32);
g_live_stats.draw_calls.fetch_add(1, std::memory_order_relaxed);
g_live_stats.draw_calls_primitive.fetch_add(1, std::memory_order_relaxed);
g_live_stats.total_vertices.fetch_add(vertex_count, std::memory_order_relaxed);
CaptureDrawCall(ac6::d3d::DrawCallKind::kPrimitive, ctx.r3.u32, prim_type, 0, vertex_count,
0);
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
@@ -230,8 +492,10 @@ PPC_FUNC_IMPL(rex_sub_821DC538) {
uint32_t buffer = ctx.r5.u32;
uint32_t offset = ctx.r6.u32;
uint32_t stride = ctx.r7.u32;
RememberDevice(ctx.r3.u32);
if (stream < ac6::d3d::kMaxStreams) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.streams[stream].buffer = buffer;
g_shadow.streams[stream].offset = offset;
g_shadow.streams[stream].stride = stride;
@@ -254,8 +518,10 @@ PPC_FUNC_IMPL(rex_sub_821DC6C8) {
uint32_t sampler = ctx.r4.u32;
uint32_t value = ctx.r5.u32;
RememberDevice(ctx.r3.u32);
if (sampler < ac6::d3d::kMaxSamplers) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.samplers[sampler].mag_filter = value;
}
g_live_stats.set_sampler_state_calls.fetch_add(1, std::memory_order_relaxed);
@@ -276,8 +542,10 @@ PPC_FUNC_IMPL(rex_sub_821DCB88) {
uint32_t sampler = ctx.r4.u32;
uint32_t value = ctx.r5.u32;
RememberDevice(ctx.r3.u32);
if (sampler < ac6::d3d::kMaxSamplers) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.samplers[sampler].min_filter = value;
}
g_live_stats.set_sampler_state_calls.fetch_add(1, std::memory_order_relaxed);
@@ -298,8 +566,10 @@ PPC_FUNC_IMPL(rex_sub_821DCA68) {
uint32_t sampler = ctx.r4.u32;
uint32_t value = ctx.r5.u32;
RememberDevice(ctx.r3.u32);
if (sampler < ac6::d3d::kMaxSamplers) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.samplers[sampler].mip_filter = value;
}
g_live_stats.set_sampler_state_calls.fetch_add(1, std::memory_order_relaxed);
@@ -320,8 +590,10 @@ PPC_FUNC_IMPL(rex_sub_821DC9C0) {
uint32_t sampler = ctx.r4.u32;
uint32_t value = ctx.r5.u32;
RememberDevice(ctx.r3.u32);
if (sampler < ac6::d3d::kMaxSamplers) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.samplers[sampler].border_color = value;
}
g_live_stats.set_sampler_state_calls.fetch_add(1, std::memory_order_relaxed);
@@ -342,8 +614,10 @@ PPC_FUNC_IMPL(rex_sub_821DCB08) {
uint32_t sampler = ctx.r4.u32;
uint32_t value = ctx.r5.u32;
RememberDevice(ctx.r3.u32);
if (sampler < ac6::d3d::kMaxSamplers) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.samplers[sampler].mip_level = value;
}
g_live_stats.set_sampler_state_calls.fetch_add(1, std::memory_order_relaxed);
@@ -363,7 +637,11 @@ PPC_FUNC_IMPL(rex_sub_821DBAF8) {
PPC_FUNC_PROLOGUE();
uint32_t flags = ctx.r4.u32;
g_shadow.shader_gpr_alloc = flags;
RememberDevice(ctx.r3.u32);
{
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.shader_gpr_alloc = flags;
}
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
@@ -378,7 +656,10 @@ PPC_FUNC_IMPL(rex_sub_821DBAF8) {
PPC_FUNC_IMPL(rex_sub_821E2380) {
PPC_FUNC_PROLOGUE();
RememberDevice(ctx.r3.u32);
g_live_stats.clear_calls.fetch_add(1, std::memory_order_relaxed);
CaptureClear(base, ctx.r3.u32, ctx.r4.u32, ctx.r5.u32, ctx.r6.u32, ctx.r7.u32, ctx.r8.u32,
static_cast<float>(ctx.f1.f64));
if (REXCVAR_GET(ac6_d3d_trace)) {
REXLOG_CAT_TRACE(kLogGPU,
@@ -396,8 +677,10 @@ PPC_FUNC_IMPL(rex_sub_821E10C8) {
uint32_t reg = ctx.r4.u32;
uint32_t texture = ctx.r5.u32;
RememberDevice(ctx.r3.u32);
if (reg < ac6::d3d::kMaxFetchConstants) {
std::unique_lock<std::shared_mutex> lock(g_shadow_mutex);
g_shadow.texture_fetch_ptrs[reg] = texture;
}
g_live_stats.set_texture_fetch_calls.fetch_add(1, std::memory_order_relaxed);
@@ -414,32 +697,45 @@ PPC_FUNC_IMPL(rex_sub_821E10C8) {
namespace ac6::d3d {
void OnFrameBoundary() {
g_snapshot.draw_calls = g_live_stats.draw_calls.load(std::memory_order_relaxed);
g_snapshot.draw_calls_indexed = g_live_stats.draw_calls_indexed.load(std::memory_order_relaxed);
g_snapshot.draw_calls_indexed_shared = g_live_stats.draw_calls_indexed_shared.load(std::memory_order_relaxed);
g_snapshot.draw_calls_primitive = g_live_stats.draw_calls_primitive.load(std::memory_order_relaxed);
g_snapshot.total_indices = g_live_stats.total_indices.load(std::memory_order_relaxed);
g_snapshot.total_vertices = g_live_stats.total_vertices.load(std::memory_order_relaxed);
g_snapshot.set_texture_calls = g_live_stats.set_texture_calls.load(std::memory_order_relaxed);
g_snapshot.set_render_target_calls = g_live_stats.set_render_target_calls.load(std::memory_order_relaxed);
g_snapshot.set_depth_stencil_calls = g_live_stats.set_depth_stencil_calls.load(std::memory_order_relaxed);
g_snapshot.set_vertex_decl_calls = g_live_stats.set_vertex_decl_calls.load(std::memory_order_relaxed);
g_snapshot.set_index_buffer_calls = g_live_stats.set_index_buffer_calls.load(std::memory_order_relaxed);
g_snapshot.set_stream_source_calls = g_live_stats.set_stream_source_calls.load(std::memory_order_relaxed);
g_snapshot.set_viewport_calls = g_live_stats.set_viewport_calls.load(std::memory_order_relaxed);
g_snapshot.set_sampler_state_calls = g_live_stats.set_sampler_state_calls.load(std::memory_order_relaxed);
g_snapshot.set_texture_fetch_calls = g_live_stats.set_texture_fetch_calls.load(std::memory_order_relaxed);
g_snapshot.clear_calls = g_live_stats.clear_calls.load(std::memory_order_relaxed);
g_snapshot.resolve_calls = g_live_stats.resolve_calls.load(std::memory_order_relaxed);
ac6::d3d::DrawStatsSnapshot draw_stats = SnapshotDrawStats();
std::unique_lock<std::shared_mutex> lock(g_snapshot_mutex);
g_snapshot = draw_stats;
lock.unlock();
ac6::d3d::FrameCaptureSnapshot frame_capture;
frame_capture.stats = draw_stats;
frame_capture.frame_end_shadow = SnapshotShadowState(0);
std::unique_lock<std::shared_mutex> capture_lock(g_capture_mutex);
frame_capture.frame_index = g_capture_live_frame_index++;
frame_capture.draws.swap(g_live_draws);
frame_capture.clears.swap(g_live_clears);
frame_capture.resolves.swap(g_live_resolves);
g_capture_live_sequence = 0;
g_capture_summary = MakeFrameCaptureSummary(frame_capture);
g_capture_snapshot = std::move(frame_capture);
g_live_stats.Reset();
}
const DrawStatsSnapshot& GetDrawStats() {
DrawStatsSnapshot GetDrawStats() {
std::shared_lock<std::shared_mutex> lock(g_snapshot_mutex);
return g_snapshot;
}
const ShadowState& GetShadowState() {
FrameCaptureSnapshot GetFrameCapture() {
std::shared_lock<std::shared_mutex> lock(g_capture_mutex);
return g_capture_snapshot;
}
FrameCaptureSummary GetFrameCaptureSummary() {
std::shared_lock<std::shared_mutex> lock(g_capture_mutex);
return g_capture_summary;
}
ShadowState GetShadowState() {
std::shared_lock<std::shared_mutex> lock(g_shadow_mutex);
return g_shadow;
}
+9 -5
View File
@@ -2,9 +2,10 @@
* @file d3d_hooks.h
* @brief Public interface for the D3D state shadowing layer.
*
* Provides per-frame draw statistics and a read-only view of the current
* D3D device shadow state. Call OnFrameBoundary() once per frame (typically
* from the present/swap hook) to snapshot stats and reset counters.
* Provides per-frame draw statistics, a frame capture snapshot for renderer
* analysis, and a read-only view of the current D3D device shadow state.
* Call OnFrameBoundary() once per frame (typically from the present/swap hook)
* to snapshot stats and reset counters.
*/
#pragma once
@@ -14,8 +15,11 @@ namespace ac6::d3d {
void OnFrameBoundary();
const DrawStatsSnapshot& GetDrawStats();
DrawStatsSnapshot GetDrawStats();
const ShadowState& GetShadowState();
FrameCaptureSnapshot GetFrameCapture();
FrameCaptureSummary GetFrameCaptureSummary();
ShadowState GetShadowState();
} // namespace ac6::d3d
+80
View File
@@ -3,6 +3,7 @@
#include <array>
#include <atomic>
#include <cstdint>
#include <vector>
namespace ac6::d3d {
@@ -11,6 +12,7 @@ inline constexpr uint32_t kMaxTextures = 16;
inline constexpr uint32_t kMaxStreams = 16;
inline constexpr uint32_t kMaxSamplers = 16;
inline constexpr uint32_t kMaxFetchConstants = 32;
inline constexpr uint32_t kMaxClearRectsPerRecord = 8;
struct DrawStats {
std::atomic<uint32_t> draw_calls{0};
@@ -72,6 +74,12 @@ struct DrawStatsSnapshot {
uint32_t resolve_calls;
};
enum class DrawCallKind : uint8_t {
kIndexed,
kIndexedShared,
kPrimitive,
};
struct StreamBinding {
uint32_t buffer{0}; // Guest address of D3DVertexBuffer
uint32_t offset{0}; // Offset in bytes
@@ -107,4 +115,76 @@ struct ShadowState {
} viewport;
};
struct DrawCallRecord {
uint32_t sequence{0};
DrawCallKind kind{DrawCallKind::kIndexed};
uint32_t primitive_type{0};
uint32_t start{0};
uint32_t count{0};
uint32_t flags{0};
ShadowState shadow_state{};
};
struct ClearRect {
uint32_t left{0};
uint32_t top{0};
uint32_t right{0};
uint32_t bottom{0};
};
struct ClearRecord {
uint32_t sequence{0};
uint32_t rect_count{0};
uint32_t captured_rect_count{0};
uint32_t flags{0};
uint32_t color{0};
uint32_t stencil{0};
float depth{1.0f};
std::array<ClearRect, kMaxClearRectsPerRecord> rects{};
ShadowState shadow_state{};
};
struct ResolveRecord {
uint32_t sequence{0};
ShadowState shadow_state{};
};
struct FrameCaptureSnapshot {
uint64_t frame_index{0};
DrawStatsSnapshot stats{};
ShadowState frame_end_shadow{};
std::vector<DrawCallRecord> draws;
std::vector<ClearRecord> clears;
std::vector<ResolveRecord> resolves;
};
struct FrameCaptureSummary {
bool capture_enabled{false};
bool record_signature_valid{false};
uint64_t frame_index{0};
uint64_t record_signature{0};
uint32_t draw_count{0};
uint32_t clear_count{0};
uint32_t resolve_count{0};
uint32_t indexed_draw_count{0};
uint32_t indexed_shared_draw_count{0};
uint32_t primitive_draw_count{0};
uint32_t unique_rt0_count{0};
uint32_t rt0_switch_count{0};
uint32_t frame_end_render_target_count{0};
uint32_t frame_end_texture_count{0};
uint32_t frame_end_stream_count{0};
uint32_t frame_end_sampler_count{0};
uint32_t frame_end_texture_fetch_count{0};
uint32_t frame_end_render_target_0{0};
uint32_t frame_end_depth_stencil{0};
uint32_t first_draw_render_target_0{0};
uint32_t last_draw_render_target_0{0};
uint32_t frame_end_viewport_width{0};
uint32_t frame_end_viewport_height{0};
uint32_t last_draw_primitive_type{0};
uint32_t last_draw_count{0};
uint32_t last_draw_flags{0};
};
} // namespace ac6::d3d
+10
View File
@@ -1,6 +1,16 @@
// ac6recomp - ReXGlue Recompiled Project
//
// This file is yours to edit. 'rexglue migrate' will NOT overwrite it.
#include "generated/ac6recomp_config.h"
#include "generated/ac6recomp_init.h"
#include <rex/cvar.h>
#include "ac6recomp_app.h"
REXCVAR_DEFINE_BOOL(audio_deep_trace, false, "Audio",
"Enable verbose runtime audio tracing");
REX_DEFINE_APP(ac6recomp, Ac6recompApp::Create)
+37 -30
View File
@@ -1,70 +1,77 @@
#include "render_hooks.h"
#include "d3d_hooks.h"
#include <atomic>
#include <chrono>
#include <mutex>
#include <rex/cvar.h>
#include <rex/ppc/types.h>
REXCVAR_DEFINE_BOOL(ac6_unlock_fps, false, "AC6", "Unlock frame rate to 60fps");
REXCVAR_DEFINE_BOOL(ac6_timing_hooks_enabled, true, "AC6",
"Enable AC6 timing hooks that alter the game's presentation cadence");
using Clock = std::chrono::steady_clock;
namespace {
std::atomic<double> g_frame_time_ms{0.0};
std::atomic<double> g_fps{0.0};
std::atomic<uint64_t> g_frame_count{0};
std::mutex g_frame_mutex;
double g_frame_time_ms{0.0};
double g_fps{0.0};
uint64_t g_frame_count{0};
Clock::time_point g_frame_start{};
bool AreTimingHooksActive() {
return REXCVAR_GET(ac6_timing_hooks_enabled) && REXCVAR_GET(ac6_unlock_fps);
}
} // namespace
// Fallback flip interval bypass — rarely fires since vblank counter cycles 0→1→0.
bool ac6FlipIntervalHook() {
return REXCVAR_GET(ac6_unlock_fps);
return AreTimingHooksActive();
}
// Primary 60fps unlock — forces D3DPRESENT_INTERVAL to 1 (every VBlank).
bool ac6PresentIntervalHook(PPCRegister& r10) {
if (REXCVAR_GET(ac6_unlock_fps)) {
r10.u64 = 1;
return true;
if (!AreTimingHooksActive()) {
return false;
}
return false;
r10.u64 = 1;
return true;
}
// Divisor=30 makes the delta time formula self-correct at any framerate.
void ac6DeltaDivisorHook(PPCRegister& r29) {
if (!AreTimingHooksActive()) {
return;
}
r29.u64 = 30;
}
// Hooked before the device[21516] branch so it fires every frame, not just VdSwap frames.
void ac6PresentTimingHook(PPCRegister& /*r31*/) {
ac6::d3d::OnFrameBoundary();
auto now = Clock::now();
if (g_frame_start.time_since_epoch().count() != 0) {
double ms =
std::chrono::duration<double, std::milli>(now - g_frame_start)
.count();
float fps_val = ms > 0.0 ? static_cast<float>(1000.0 / ms) : 0.0f;
g_frame_time_ms.store(ms, std::memory_order_relaxed);
g_fps.store(static_cast<double>(fps_val), std::memory_order_relaxed);
g_frame_count.fetch_add(1, std::memory_order_relaxed);
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) {
g_frame_time_ms =
std::chrono::duration<double, std::milli>(now - g_frame_start).count();
g_fps = g_frame_time_ms > 0.0001 ? (1000.0 / g_frame_time_ms) : 0.0;
++g_frame_count;
}
g_frame_start = now;
frame_time_ms = g_frame_time_ms;
fps = g_fps;
frame_count = g_frame_count;
}
g_frame_start = now;
}
namespace ac6 {
FrameStats GetFrameStats() {
return FrameStats{
g_frame_time_ms.load(std::memory_order_relaxed),
g_fps.load(std::memory_order_relaxed),
g_frame_count.load(std::memory_order_relaxed),
};
std::lock_guard<std::mutex> lock(g_frame_mutex);
return FrameStats{g_frame_time_ms, g_fps, g_frame_count};
}
} // namespace ac6
+9 -3
View File
@@ -3,17 +3,23 @@
#include <cstdint>
#include <rex/cvar.h>
#include <rex/ppc/types.h>
REXCVAR_DECLARE(bool, ac6_unlock_fps);
namespace ac6 {
struct FrameStats {
double frame_time_ms;
double fps;
uint64_t frame_count;
double frame_time_ms{0.0};
double fps{0.0};
uint64_t frame_count{0};
};
FrameStats GetFrameStats();
} // namespace ac6
bool ac6FlipIntervalHook();
bool ac6PresentIntervalHook(PPCRegister& r10);
void ac6DeltaDivisorHook(PPCRegister& r29);
void ac6PresentTimingHook(PPCRegister& r31);