feat: wire AC6 native graphics runtime and bootstrap configure path

Run the native renderer every frame from AC6 present timing hooks with a visible runtime status overlay so migration progress is observable in-game. Also add a tracked CMake bootstrap include and migration mapping docs so fresh clones can configure before generated glue exists.

Made-with: Cursor
This commit is contained in:
salh
2026-04-17 21:47:18 +03:00
parent 2dbd1fbbfc
commit 5bdc93d055
11 changed files with 554 additions and 6 deletions
+142 -1
View File
@@ -1 +1,142 @@
#include "ac6_native_graphics.h"
#include <mutex>
#include <string_view>
#include <rex/cvar.h>
#include <rex/logging.h>
#include "ac6_native_renderer/native_renderer.h"
#include "d3d_hooks.h"
REXCVAR_DEFINE_BOOL(ac6_native_graphics_enabled, true, "AC6/NativeGraphics",
"Enable AC6 native renderer frame-plan execution from captured D3D state");
REXCVAR_DEFINE_BOOL(ac6_native_graphics_require_capture, true, "AC6/NativeGraphics",
"Force render-capture on while native graphics execution is enabled");
REXCVAR_DEFINE_STRING(ac6_native_graphics_backend, "auto", "AC6/NativeGraphics",
"Preferred native backend: auto, d3d12, vulkan, metal")
.allowed({"auto", "d3d12", "vulkan", "metal"});
REXCVAR_DEFINE_STRING(ac6_native_graphics_feature_level, "scene_submission", "AC6/NativeGraphics",
"Native renderer feature level: bootstrap, scene_submission, parity_validation, shipping")
.allowed({"bootstrap", "scene_submission", "parity_validation", "shipping"});
REXCVAR_DEFINE_INT32(ac6_native_graphics_frames_in_flight, 2, "AC6/NativeGraphics",
"Native renderer max frames in flight")
.range(1, 4);
namespace ac6::graphics {
namespace {
std::mutex g_native_graphics_mutex;
ac6::renderer::NativeRenderer g_native_renderer;
NativeGraphicsRuntimeStatus g_runtime_status{};
ac6::renderer::BackendType ParseBackend(std::string_view value) {
if (value == "d3d12") {
return ac6::renderer::BackendType::kD3D12;
}
if (value == "vulkan") {
return ac6::renderer::BackendType::kVulkan;
}
if (value == "metal") {
return ac6::renderer::BackendType::kMetal;
}
return ac6::renderer::BackendType::kUnknown;
}
ac6::renderer::FeatureLevel ParseFeatureLevel(std::string_view value) {
if (value == "bootstrap") {
return ac6::renderer::FeatureLevel::kBootstrap;
}
if (value == "parity_validation") {
return ac6::renderer::FeatureLevel::kParityValidation;
}
if (value == "shipping") {
return ac6::renderer::FeatureLevel::kShipping;
}
return ac6::renderer::FeatureLevel::kSceneSubmission;
}
ac6::renderer::NativeRendererConfig BuildRendererConfig() {
ac6::renderer::NativeRendererConfig config;
config.preferred_backend = ParseBackend(REXCVAR_GET(ac6_native_graphics_backend));
config.feature_level = ParseFeatureLevel(REXCVAR_GET(ac6_native_graphics_feature_level));
config.max_frames_in_flight = static_cast<uint32_t>(REXCVAR_GET(ac6_native_graphics_frames_in_flight));
config.enable_debug_markers = true;
config.enable_validation = true;
return config;
}
bool EnsureInitialized() {
if (g_runtime_status.initialized) {
return true;
}
++g_runtime_status.init_attempts;
const ac6::renderer::NativeRendererConfig config = BuildRendererConfig();
if (!g_native_renderer.Initialize(config)) {
g_runtime_status.had_init_failure = true;
REXLOG_ERROR("AC6 native graphics failed to initialize backend={}",
ac6::renderer::ToString(ac6::renderer::ResolveBackend(config.preferred_backend)));
return false;
}
g_runtime_status.initialized = true;
g_runtime_status.had_init_failure = false;
++g_runtime_status.init_successes;
g_runtime_status.feature_level = config.feature_level;
return true;
}
void UpdateStatusFromRendererUnlocked() {
g_runtime_status.renderer_stats = g_native_renderer.GetStats();
g_runtime_status.active_backend = g_runtime_status.renderer_stats.active_backend;
g_runtime_status.frame_plan = g_native_renderer.frame_plan();
}
} // namespace
void OnFrameBoundary() {
std::scoped_lock<std::mutex> lock(g_native_graphics_mutex);
g_runtime_status.enabled = REXCVAR_GET(ac6_native_graphics_enabled);
if (!g_runtime_status.enabled) {
if (g_runtime_status.initialized) {
g_native_renderer.Shutdown();
g_runtime_status.initialized = false;
}
return;
}
if (REXCVAR_GET(ac6_native_graphics_require_capture) && !REXCVAR_GET(ac6_render_capture)) {
REXCVAR_SET(ac6_render_capture, true);
}
if (!EnsureInitialized()) {
return;
}
const ac6::d3d::FrameCaptureSnapshot frame_capture = ac6::d3d::GetFrameCapture();
g_runtime_status.capture_summary = ac6::d3d::GetFrameCaptureSummary();
g_native_renderer.BeginFrame();
g_native_renderer.BuildCapturedFrame(frame_capture);
++g_runtime_status.frames_built;
UpdateStatusFromRendererUnlocked();
}
void Shutdown() {
std::scoped_lock<std::mutex> lock(g_native_graphics_mutex);
if (!g_runtime_status.initialized) {
return;
}
g_native_renderer.Shutdown();
g_runtime_status.initialized = false;
}
NativeGraphicsRuntimeStatus GetRuntimeStatus() {
std::scoped_lock<std::mutex> lock(g_native_graphics_mutex);
return g_runtime_status;
}
} // namespace ac6::graphics
+32 -1
View File
@@ -1 +1,32 @@
#pragma once
#include <cstdint>
#include "ac6_native_renderer/frame_plan.h"
#include "ac6_native_renderer/types.h"
#include "d3d_state.h"
namespace ac6::graphics {
struct NativeGraphicsRuntimeStatus {
bool enabled = false;
bool initialized = false;
bool had_init_failure = false;
uint64_t init_attempts = 0;
uint64_t init_successes = 0;
uint64_t frames_built = 0;
ac6::renderer::BackendType active_backend = ac6::renderer::BackendType::kUnknown;
ac6::renderer::FeatureLevel feature_level = ac6::renderer::FeatureLevel::kBootstrap;
ac6::renderer::NativeRendererStats renderer_stats{};
ac6::d3d::FrameCaptureSummary capture_summary{};
ac6::renderer::NativeFramePlan frame_plan{};
};
void OnFrameBoundary();
void Shutdown();
NativeGraphicsRuntimeStatus GetRuntimeStatus();
} // namespace ac6::graphics
+57 -1
View File
@@ -1 +1,57 @@
#include "ac6_native_graphics_overlay.h"
#include <imgui.h>
#include "ac6_native_graphics.h"
namespace ac6::graphics {
NativeGraphicsStatusDialog::NativeGraphicsStatusDialog(rex::ui::ImGuiDrawer* imgui_drawer)
: ImGuiDialog(imgui_drawer) {}
NativeGraphicsStatusDialog::~NativeGraphicsStatusDialog() = default;
void NativeGraphicsStatusDialog::OnDraw(ImGuiIO& io) {
(void)io;
if (!visible_) {
return;
}
if (!ImGui::Begin("AC6 Native Graphics##status", &visible_, ImGuiWindowFlags_NoCollapse)) {
ImGui::End();
return;
}
const NativeGraphicsRuntimeStatus status = GetRuntimeStatus();
ImGui::Text("enabled: %s", status.enabled ? "true" : "false");
ImGui::Text("initialized: %s", status.initialized ? "true" : "false");
ImGui::Text("init failures seen: %s", status.had_init_failure ? "true" : "false");
ImGui::Text("init attempts/successes: %llu / %llu",
static_cast<unsigned long long>(status.init_attempts),
static_cast<unsigned long long>(status.init_successes));
ImGui::Text("frames built: %llu", static_cast<unsigned long long>(status.frames_built));
ImGui::Separator();
ImGui::Text("backend: %s", ac6::renderer::ToString(status.active_backend).data());
ImGui::Text("feature level: %s", ac6::renderer::ToString(status.feature_level).data());
ImGui::Text("renderer frames: %llu",
static_cast<unsigned long long>(status.renderer_stats.frame_count));
ImGui::Text("render passes built: %llu",
static_cast<unsigned long long>(status.renderer_stats.built_pass_count));
ImGui::Separator();
ImGui::Text("capture frame: %llu",
static_cast<unsigned long long>(status.capture_summary.frame_index));
ImGui::Text("capture draws/clears/resolves: %u / %u / %u",
status.capture_summary.draw_count, status.capture_summary.clear_count,
status.capture_summary.resolve_count);
ImGui::Text("planned output: %ux%u", status.frame_plan.output_width,
status.frame_plan.output_height);
ImGui::Text("stages scene/post/ui: %s / %s / %s",
status.frame_plan.has_scene_stage ? "yes" : "no",
status.frame_plan.has_post_process_stage ? "yes" : "no",
status.frame_plan.has_ui_stage ? "yes" : "no");
ImGui::End();
}
} // namespace ac6::graphics
+1
View File
@@ -15,6 +15,7 @@ class NativeGraphicsStatusDialog final : public rex::ui::ImGuiDialog {
explicit NativeGraphicsStatusDialog(rex::ui::ImGuiDrawer* imgui_drawer);
~NativeGraphicsStatusDialog();
void Show() { visible_ = true; }
void ToggleVisible() { visible_ = !visible_; }
bool IsVisible() const { return visible_; }
+30 -1
View File
@@ -1 +1,30 @@
#pragma once
#include <memory>
#include <rex/rex_app.h>
#include "ac6_native_graphics_overlay.h"
#include "generated/ac6recomp_config.h"
class Ac6recompApp : public rex::ReXApp {
public:
using rex::ReXApp::ReXApp;
static std::unique_ptr<rex::ui::WindowedApp> Create(
rex::ui::WindowedAppContext& ctx) {
return std::unique_ptr<Ac6recompApp>(new Ac6recompApp(ctx, "ac6recomp", PPCImageConfig));
}
protected:
void OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) override {
rex::ReXApp::OnCreateDialogs(drawer);
native_graphics_status_dialog_ =
std::make_unique<ac6::graphics::NativeGraphicsStatusDialog>(drawer);
native_graphics_status_dialog_->Show();
}
private:
std::unique_ptr<ac6::graphics::NativeGraphicsStatusDialog> native_graphics_status_dialog_;
};
+19 -1
View File
@@ -1 +1,19 @@
#pragma once
#include <rex/cvar.h>
#include "d3d_state.h"
REXCVAR_DECLARE(bool, ac6_render_capture);
namespace ac6::d3d {
void OnFrameBoundary();
DrawStatsSnapshot GetDrawStats();
FrameCaptureSnapshot GetFrameCapture();
FrameCaptureSummary GetFrameCaptureSummary();
ShadowState GetShadowState();
} // namespace ac6::d3d
+2
View File
@@ -1,5 +1,6 @@
#include "render_hooks.h"
#include "d3d_hooks.h"
#include "ac6_native_graphics.h"
#include <chrono>
#include <mutex>
@@ -47,6 +48,7 @@ void ac6DeltaDivisorHook(PPCRegister& r29) {
void ac6PresentTimingHook(PPCRegister& /*r31*/) {
ac6::d3d::OnFrameBoundary();
ac6::graphics::OnFrameBoundary();
const auto now = Clock::now();
double frame_time_ms = 0.0;