mirror of
https://github.com/sal063/AC6_recomp
synced 2026-07-07 22:22:22 -04:00
Initial source-only import
This commit is contained in:
+20
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user