diff --git a/.gitignore b/.gitignore index 27f3739d..d4dc3997 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ff01a86..b35a3a86 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) - diff --git a/README.md b/README.md index 324dfb0f..30076f6a 100644 --- a/README.md +++ b/README.md @@ -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 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. diff --git a/src/ac6_native_graphics.cpp b/src/ac6_native_graphics.cpp new file mode 100644 index 00000000..edeee874 --- /dev/null +++ b/src/ac6_native_graphics.cpp @@ -0,0 +1,1480 @@ +#include "ac6_native_graphics.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "d3d_hooks.h" + +#if REX_HAS_D3D12 +#include +#include +#include +#include +#include +#endif + +REXCVAR_DEFINE_BOOL(ac6_native_graphics_bootstrap, true, "AC6/Render", + "Use the experimental native graphics bootstrap backend"); +REXCVAR_DEFINE_BOOL( + ac6_native_graphics_placeholder_present, false, "AC6/Render", + "Allow the native graphics bootstrap backend to replace the legacy swap path with a " + "preview frame generated by the experimental native replay compositor"); +REXCVAR_DEFINE_BOOL( + ac6_allow_gpu_trace_stream, false, "AC6/Render", + "Allow legacy GPU trace streaming during AC6 native graphics experiments"); + +namespace ac6::graphics { +namespace { + +using rex::X_STATUS; + +#if REX_HAS_D3D12 +namespace shaders { +#include "thirdparty/rexglue-sdk/src/ui/shaders/bytecode/d3d12_5_1/immediate_ps.h" +#include "thirdparty/rexglue-sdk/src/ui/shaders/bytecode/d3d12_5_1/immediate_vs.h" +} // namespace shaders +#endif + +std::mutex g_native_graphics_status_mutex; +NativeGraphicsStatusSnapshot g_native_graphics_status{}; + +struct CapturedFrameEvent { + enum class Type { + kDraw, + kClear, + kResolve, + }; + + uint32_t sequence = 0; + Type type = Type::kDraw; + const ac6::d3d::ShadowState* shadow_state = nullptr; +}; + +struct ReplayPassCandidate { + ac6::d3d::ShadowState binding{}; + uint32_t start_sequence = 0; + uint32_t end_sequence = 0; + uint32_t draw_count = 0; + uint32_t clear_count = 0; + uint32_t resolve_count = 0; +}; + +struct ReplayCandidateKey { + uint32_t rt0 = 0; + uint32_t depth_stencil = 0; + uint32_t viewport_x = 0; + uint32_t viewport_y = 0; + uint32_t viewport_width = 0; + uint32_t viewport_height = 0; +}; + +constexpr uint32_t kSelectedPassPreviewColorSampleCount = 4; +constexpr uint32_t kSelectedPassPreviewStepCount = 4; + +using FloatColor = std::array; + +struct SelectedPassClearStep { + uint32_t color = 0; + uint32_t rect_count = 0; + std::array rects{}; +}; + +struct SelectedPassPreviewData { + SelectedPassPreviewSummary summary{}; + std::array clear_steps{}; + uint32_t clear_step_count = 0; +}; + +bool SamePassBinding(const ac6::d3d::ShadowState& left, + const ac6::d3d::ShadowState& right) { + return left.render_targets[0] == right.render_targets[0] && + left.depth_stencil == right.depth_stencil && + left.viewport.width == right.viewport.width && + left.viewport.height == right.viewport.height; +} + +bool SameReplayCandidate(const ReplayCandidateKey& left, const ReplayCandidateKey& right) { + return left.rt0 == right.rt0 && left.depth_stencil == right.depth_stencil && + left.viewport_x == right.viewport_x && left.viewport_y == right.viewport_y && + left.viewport_width == right.viewport_width && + left.viewport_height == right.viewport_height; +} + +ReplayCandidateKey MakeReplayCandidateKey(const NativeReplayPlanSummary& replay_plan) { + return ReplayCandidateKey{replay_plan.present_candidate_rt0, + replay_plan.present_candidate_depth_stencil, + replay_plan.present_candidate_viewport_x, + replay_plan.present_candidate_viewport_y, + replay_plan.present_candidate_viewport_width, + replay_plan.present_candidate_viewport_height}; +} + +bool SequenceInSelectedPass(uint32_t sequence, const NativeReplayPlanSummary& replay_plan) { + return replay_plan.valid && sequence >= replay_plan.selected_pass_start_sequence && + sequence <= replay_plan.selected_pass_end_sequence; +} + +FloatColor DecodeArgbColor(uint32_t packed_color) { + return {float((packed_color >> 16) & 0xFFu) / 255.0f, + float((packed_color >> 8) & 0xFFu) / 255.0f, + float(packed_color & 0xFFu) / 255.0f, 1.0f}; +} + +FloatColor MixColors(const FloatColor& left, const FloatColor& right, float t) { + const float clamped_t = std::clamp(t, 0.0f, 1.0f); + const float inverse_t = 1.0f - clamped_t; + return {left[0] * inverse_t + right[0] * clamped_t, + left[1] * inverse_t + right[1] * clamped_t, + left[2] * inverse_t + right[2] * clamped_t, 1.0f}; +} + +uint32_t PackImmediateColor(const FloatColor& color) { + const auto pack_channel = [](float value) -> uint32_t { + return uint32_t(std::clamp(value, 0.0f, 1.0f) * 255.0f + 0.5f); + }; + const uint32_t r = pack_channel(color[0]); + const uint32_t g = pack_channel(color[1]); + const uint32_t b = pack_channel(color[2]); + const uint32_t a = pack_channel(color[3]); + return r | (g << 8) | (b << 16) | (a << 24); +} + +uint32_t ScoreReplayPass(const ReplayPassCandidate& pass, + const rex::system::GraphicsSwapSubmission& submission, + bool is_last_pass) { + uint32_t score = 0; + if (pass.binding.viewport.width == submission.frontbuffer_width && + pass.binding.viewport.height == submission.frontbuffer_height) { + score += 100; + } + if (pass.draw_count) { + score += 40 + std::min(pass.draw_count, 64); + } + if (pass.resolve_count) { + score += 20; + } + if (pass.binding.render_targets[0]) { + score += 10; + } + if (is_last_pass) { + score += 25; + } + return score; +} + +NativeReplayPlanSummary AnalyzeReplayPlan( + const ac6::d3d::FrameCaptureSnapshot& frame_capture, + const rex::system::GraphicsSwapSubmission& submission) { + NativeReplayPlanSummary summary; + summary.frame_index = frame_capture.frame_index; + + std::vector events; + events.reserve(frame_capture.draws.size() + frame_capture.clears.size() + + frame_capture.resolves.size()); + + for (const auto& draw : frame_capture.draws) { + events.push_back( + {draw.sequence, CapturedFrameEvent::Type::kDraw, &draw.shadow_state}); + } + for (const auto& clear : frame_capture.clears) { + events.push_back( + {clear.sequence, CapturedFrameEvent::Type::kClear, &clear.shadow_state}); + } + for (const auto& resolve : frame_capture.resolves) { + events.push_back( + {resolve.sequence, CapturedFrameEvent::Type::kResolve, &resolve.shadow_state}); + } + + if (events.empty()) { + return summary; + } + + std::sort(events.begin(), events.end(), + [](const CapturedFrameEvent& left, const CapturedFrameEvent& right) { + return left.sequence < right.sequence; + }); + + std::vector passes; + ReplayPassCandidate current_pass; + bool current_pass_valid = false; + auto flush_pass = [&]() { + if (!current_pass_valid) { + return; + } + passes.push_back(current_pass); + current_pass = {}; + current_pass_valid = false; + }; + + for (const CapturedFrameEvent& event : events) { + if (!event.shadow_state) { + continue; + } + if (!current_pass_valid || !SamePassBinding(current_pass.binding, *event.shadow_state)) { + flush_pass(); + current_pass.binding = *event.shadow_state; + current_pass.start_sequence = event.sequence; + current_pass_valid = true; + } + current_pass.end_sequence = event.sequence; + + switch (event.type) { + case CapturedFrameEvent::Type::kDraw: + ++summary.draw_event_count; + ++current_pass.draw_count; + break; + case CapturedFrameEvent::Type::kClear: + ++summary.clear_event_count; + ++current_pass.clear_count; + break; + case CapturedFrameEvent::Type::kResolve: + ++summary.resolve_event_count; + ++current_pass.resolve_count; + break; + } + } + + flush_pass(); + + summary.pass_count = static_cast(passes.size()); + if (!passes.empty()) { + summary.first_pass_draw_count = passes.front().draw_count; + summary.last_pass_draw_count = passes.back().draw_count; + } + + uint32_t best_score = 0; + uint32_t best_index = 0; + bool best_index_valid = false; + for (uint32_t i = 0; i < passes.size(); ++i) { + const ReplayPassCandidate& pass = passes[i]; + summary.largest_pass_draw_count = + std::max(summary.largest_pass_draw_count, pass.draw_count); + bool matches_swap_size = pass.binding.viewport.width == submission.frontbuffer_width && + pass.binding.viewport.height == submission.frontbuffer_height; + if (matches_swap_size) { + ++summary.swap_size_match_pass_count; + } + bool is_last_pass = (i + 1) == passes.size(); + uint32_t score = ScoreReplayPass(pass, submission, is_last_pass); + if (!best_index_valid || score > best_score || + (score == best_score && i > best_index)) { + best_index_valid = true; + best_index = i; + best_score = score; + } + } + + if (best_index_valid) { + const ReplayPassCandidate& selected_pass = passes[best_index]; + summary.selected_pass_index = best_index; + summary.selected_pass_score = best_score; + summary.selected_pass_start_sequence = selected_pass.start_sequence; + summary.selected_pass_end_sequence = selected_pass.end_sequence; + summary.selected_pass_draw_count = selected_pass.draw_count; + summary.selected_pass_clear_count = selected_pass.clear_count; + summary.selected_pass_resolve_count = selected_pass.resolve_count; + summary.selected_pass_is_last = (best_index + 1) == passes.size(); + summary.selected_pass_has_resolve = selected_pass.resolve_count != 0; + summary.present_candidate_rt0 = selected_pass.binding.render_targets[0]; + summary.present_candidate_depth_stencil = selected_pass.binding.depth_stencil; + summary.present_candidate_viewport_x = selected_pass.binding.viewport.x; + summary.present_candidate_viewport_y = selected_pass.binding.viewport.y; + summary.present_candidate_viewport_width = selected_pass.binding.viewport.width; + summary.present_candidate_viewport_height = selected_pass.binding.viewport.height; + summary.present_candidate_matches_swap_size = + summary.present_candidate_viewport_width == submission.frontbuffer_width && + summary.present_candidate_viewport_height == submission.frontbuffer_height; + summary.valid = true; + } + return summary; +} + +SelectedPassPreviewData BuildSelectedPassPreview( + const ac6::d3d::FrameCaptureSnapshot& frame_capture, + const NativeReplayPlanSummary& replay_plan) { + SelectedPassPreviewData preview; + if (!replay_plan.valid) { + return preview; + } + + preview.summary.valid = true; + preview.summary.draw_count = replay_plan.selected_pass_draw_count; + preview.summary.clear_count = replay_plan.selected_pass_clear_count; + preview.summary.resolve_count = replay_plan.selected_pass_resolve_count; + + bool has_first_clear = false; + for (const auto& clear : frame_capture.clears) { + if (!SequenceInSelectedPass(clear.sequence, replay_plan)) { + continue; + } + + if (!has_first_clear) { + preview.summary.first_clear_color = clear.color; + has_first_clear = true; + } + preview.summary.last_clear_color = clear.color; + preview.summary.using_clear_fill = true; + + preview.summary.sampled_clear_rect_count += clear.captured_rect_count; + if (preview.clear_step_count < preview.clear_steps.size()) { + SelectedPassClearStep& step = preview.clear_steps[preview.clear_step_count++]; + step.color = clear.color; + step.rect_count = clear.captured_rect_count; + step.rects = clear.rects; + } else { + SelectedPassClearStep& step = preview.clear_steps.back(); + step.color = clear.color; + step.rect_count = clear.captured_rect_count; + step.rects = clear.rects; + } + } + + preview.summary.sampled_clear_color_count = preview.clear_step_count; + return preview; +} + +#if REX_HAS_D3D12 +class Ac6NativeGraphicsSystem final : public rex::graphics::d3d12::D3D12GraphicsSystem { + public: + X_STATUS Setup(rex::runtime::FunctionDispatcher* function_dispatcher, + rex::system::KernelState* kernel_state, + rex::ui::WindowedAppContext* app_context, + bool with_presentation) override { + X_STATUS status = rex::graphics::d3d12::D3D12GraphicsSystem::Setup( + function_dispatcher, kernel_state, app_context, with_presentation); + if (XFAILED(status)) { + return status; + } + + d3d12_provider_ = static_cast(provider()); + d3d12_presenter_ = with_presentation + ? static_cast(presenter()) + : nullptr; + if (!d3d12_provider_) { + REXLOG_ERROR("AC6 native graphics bootstrap failed to acquire the D3D12 provider"); + rex::graphics::d3d12::D3D12GraphicsSystem::Shutdown(); + return X_STATUS_UNSUCCESSFUL; + } + + { + std::lock_guard lock(g_native_graphics_status_mutex); + g_native_graphics_status.backend_active = true; + g_native_graphics_status.provider_ready = d3d12_provider_ != nullptr; + g_native_graphics_status.presenter_ready = d3d12_presenter_ != nullptr; + } + + if (REXCVAR_GET(ac6_native_graphics_placeholder_present)) { + REXLOG_WARN( + "AC6 native graphics bootstrap backend is active. The legacy D3D12 " + "graphics core remains enabled for compatibility, while direct swap " + "presentation is intercepted by the native replay preview path."); + } else { + REXLOG_WARN( + "AC6 native graphics bootstrap backend is active. The legacy D3D12 " + "graphics core remains enabled for compatibility, and direct swap " + "submissions are observed natively while legacy PM4 presentation " + "remains authoritative."); + } + return X_STATUS_SUCCESS; + } + + void Shutdown() override { + ShutdownPlaceholderRefreshResources(); + { + std::lock_guard lock(g_native_graphics_status_mutex); + g_native_graphics_status.backend_active = false; + g_native_graphics_status.provider_ready = false; + g_native_graphics_status.presenter_ready = false; + g_native_graphics_status.placeholder_resources_initialized = false; + g_native_graphics_status.last_swap_intercepted = false; + g_native_graphics_status.last_swap_fell_back = false; + } + d3d12_presenter_ = nullptr; + d3d12_provider_ = nullptr; + last_swap_submission_ = {}; + swap_count_ = 0; + last_replay_plan_ = {}; + last_selected_pass_preview_ = {}; + last_selected_candidate_ = {}; + last_selected_candidate_valid_ = false; + selected_candidate_streak_ = 0; + logged_first_swap_ = false; + logged_first_present_ = false; + logged_passthrough_swap_ = false; + logged_refresh_failure_ = false; + last_present_used_raster_replay_ = false; + rex::graphics::d3d12::D3D12GraphicsSystem::Shutdown(); + } + + bool HandleVideoSwap(const rex::system::GraphicsSwapSubmission& submission) override { + last_swap_submission_ = submission; + ++swap_count_; + last_present_used_raster_replay_ = false; + ac6::d3d::FrameCaptureSnapshot frame_capture = ac6::d3d::GetFrameCapture(); + NativeReplayPlanSummary replay_plan = AnalyzeReplayPlan(frame_capture, submission); + if (replay_plan.valid) { + ReplayCandidateKey candidate_key = MakeReplayCandidateKey(replay_plan); + if (last_selected_candidate_valid_ && + SameReplayCandidate(last_selected_candidate_, candidate_key)) { + ++selected_candidate_streak_; + } else { + last_selected_candidate_ = candidate_key; + last_selected_candidate_valid_ = true; + selected_candidate_streak_ = 1; + } + replay_plan.selected_pass_streak = selected_candidate_streak_; + replay_plan.selected_pass_is_stable = selected_candidate_streak_ >= 3; + } else { + last_selected_candidate_valid_ = false; + selected_candidate_streak_ = 0; + } + last_replay_plan_ = replay_plan; + last_selected_pass_preview_ = BuildSelectedPassPreview(frame_capture, replay_plan); + + { + std::lock_guard lock(g_native_graphics_status_mutex); + g_native_graphics_status.total_swap_count = swap_count_; + g_native_graphics_status.last_frontbuffer_virtual_address = + submission.frontbuffer_virtual_address; + g_native_graphics_status.last_frontbuffer_physical_address = + submission.frontbuffer_physical_address; + g_native_graphics_status.last_frontbuffer_width = submission.frontbuffer_width; + g_native_graphics_status.last_frontbuffer_height = submission.frontbuffer_height; + g_native_graphics_status.last_texture_format = submission.texture_format; + g_native_graphics_status.last_color_space = submission.color_space; + g_native_graphics_status.capture_summary = ac6::d3d::GetFrameCaptureSummary(); + g_native_graphics_status.replay_plan = replay_plan; + g_native_graphics_status.selected_pass_preview = last_selected_pass_preview_.summary; + g_native_graphics_status.last_swap_intercepted = false; + g_native_graphics_status.last_swap_fell_back = false; + } + + if (!logged_first_swap_) { + logged_first_swap_ = true; + REXLOG_WARN( + "AC6 native graphics bootstrap received the first direct swap " + "(fb_va={:08X}, fb_pa={:08X}, {}x{}, fmt={:08X})", + submission.frontbuffer_virtual_address, submission.frontbuffer_physical_address, + submission.frontbuffer_width, submission.frontbuffer_height, submission.texture_format); + } + + if (!REXCVAR_GET(ac6_native_graphics_placeholder_present)) { + if (!logged_passthrough_swap_) { + logged_passthrough_swap_ = true; + REXLOG_WARN( + "AC6 native graphics bootstrap is leaving direct swap presentation " + "on the legacy PM4 path because ac6_native_graphics_placeholder_present=false"); + } + { + std::lock_guard lock(g_native_graphics_status_mutex); + ++g_native_graphics_status.fallback_swap_count; + g_native_graphics_status.last_swap_fell_back = true; + } + return false; + } + + if (!EnsurePlaceholderRefreshResources()) { + if (!logged_refresh_failure_) { + logged_refresh_failure_ = true; + REXLOG_WARN( + "AC6 native graphics bootstrap could not initialize diagnostic " + "direct-swap replay resources; falling back to the legacy PM4 swap path"); + } + { + std::lock_guard lock(g_native_graphics_status_mutex); + ++g_native_graphics_status.fallback_swap_count; + g_native_graphics_status.last_swap_fell_back = true; + } + return false; + } + + if (!RefreshPlaceholderFrame(submission)) { + if (!logged_refresh_failure_) { + logged_refresh_failure_ = true; + REXLOG_WARN( + "AC6 native graphics bootstrap could not refresh the diagnostic " + "direct-swap replay frame; falling back to the legacy PM4 swap path"); + } + { + std::lock_guard lock(g_native_graphics_status_mutex); + ++g_native_graphics_status.fallback_swap_count; + g_native_graphics_status.last_swap_fell_back = true; + } + return false; + } + + if (!logged_first_present_) { + logged_first_present_ = true; + REXLOG_WARN( + "AC6 native graphics bootstrap is now presenting a raster replay " + "preview frame through the direct swap path"); + } + + { + std::lock_guard lock(g_native_graphics_status_mutex); + ++g_native_graphics_status.intercepted_swap_count; + g_native_graphics_status.selected_pass_preview.using_raster_replay = + last_present_used_raster_replay_; + g_native_graphics_status.last_swap_intercepted = true; + } + DispatchInterruptCallback(0, 2); + return true; + } + + private: + static constexpr uint32_t kPlaceholderRefreshSlotCount = 3; + static constexpr uint32_t kPlaceholderHeartbeatPeriod = 120; + static constexpr uint32_t kRasterPreviewMaxRectCount = 64; + static constexpr uint32_t kRasterPreviewMaxVertexCount = kRasterPreviewMaxRectCount * 6; + + struct PlaceholderRefreshSlot { + Microsoft::WRL::ComPtr command_allocator; + Microsoft::WRL::ComPtr shader_visible_uav_heap; + Microsoft::WRL::ComPtr cpu_uav_heap; + Microsoft::WRL::ComPtr rtv_heap; + Microsoft::WRL::ComPtr raster_render_target; + Microsoft::WRL::ComPtr vertex_upload_buffer; + uint8_t* vertex_upload_mapping = nullptr; + uint32_t raster_target_width = 0; + uint32_t raster_target_height = 0; + uint64_t last_submission = 0; + }; + + bool EnsurePlaceholderRefreshResources() { + if (placeholder_resources_initialized_) { + return true; + } + if (!InitializePlaceholderRefreshResources()) { + return false; + } + placeholder_resources_initialized_ = true; + { + std::lock_guard lock(g_native_graphics_status_mutex); + g_native_graphics_status.placeholder_resources_initialized = true; + } + return true; + } + + bool InitializeRasterPreviewResources(ID3D12Device* device) { + D3D12_ROOT_PARAMETER root_parameters[3] = {}; + D3D12_DESCRIPTOR_RANGE descriptor_range_texture = {}; + D3D12_DESCRIPTOR_RANGE descriptor_range_sampler = {}; + + root_parameters[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; + root_parameters[0].DescriptorTable.NumDescriptorRanges = 1; + root_parameters[0].DescriptorTable.pDescriptorRanges = &descriptor_range_texture; + root_parameters[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + descriptor_range_texture.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV; + descriptor_range_texture.NumDescriptors = 1; + descriptor_range_texture.BaseShaderRegister = 0; + descriptor_range_texture.RegisterSpace = 0; + descriptor_range_texture.OffsetInDescriptorsFromTableStart = 0; + + root_parameters[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE; + root_parameters[1].DescriptorTable.NumDescriptorRanges = 1; + root_parameters[1].DescriptorTable.pDescriptorRanges = &descriptor_range_sampler; + root_parameters[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; + descriptor_range_sampler.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER; + descriptor_range_sampler.NumDescriptors = 1; + descriptor_range_sampler.BaseShaderRegister = 0; + descriptor_range_sampler.RegisterSpace = 0; + descriptor_range_sampler.OffsetInDescriptorsFromTableStart = 0; + + root_parameters[2].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS; + root_parameters[2].Constants.ShaderRegister = 0; + root_parameters[2].Constants.RegisterSpace = 0; + root_parameters[2].Constants.Num32BitValues = 2; + root_parameters[2].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX; + + D3D12_ROOT_SIGNATURE_DESC root_signature_desc = {}; + root_signature_desc.NumParameters = uint32_t(std::size(root_parameters)); + root_signature_desc.pParameters = root_parameters; + root_signature_desc.NumStaticSamplers = 0; + root_signature_desc.pStaticSamplers = nullptr; + root_signature_desc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT; + raster_preview_root_signature_.Attach( + rex::ui::d3d12::util::CreateRootSignature(*d3d12_provider_, root_signature_desc)); + if (!raster_preview_root_signature_) { + REXLOG_ERROR( + "AC6 native graphics bootstrap failed to create the raster replay " + "root signature"); + return false; + } + + D3D12_GRAPHICS_PIPELINE_STATE_DESC pipeline_desc = {}; + pipeline_desc.pRootSignature = raster_preview_root_signature_.Get(); + pipeline_desc.VS.pShaderBytecode = shaders::immediate_vs; + pipeline_desc.VS.BytecodeLength = sizeof(shaders::immediate_vs); + pipeline_desc.PS.pShaderBytecode = shaders::immediate_ps; + pipeline_desc.PS.BytecodeLength = sizeof(shaders::immediate_ps); + D3D12_RENDER_TARGET_BLEND_DESC& blend_desc = pipeline_desc.BlendState.RenderTarget[0]; + blend_desc.BlendEnable = TRUE; + blend_desc.SrcBlend = D3D12_BLEND_SRC_ALPHA; + blend_desc.DestBlend = D3D12_BLEND_INV_SRC_ALPHA; + blend_desc.BlendOp = D3D12_BLEND_OP_ADD; + blend_desc.SrcBlendAlpha = D3D12_BLEND_ONE; + blend_desc.DestBlendAlpha = D3D12_BLEND_ONE; + blend_desc.BlendOpAlpha = D3D12_BLEND_OP_ADD; + blend_desc.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL; + pipeline_desc.SampleMask = UINT_MAX; + pipeline_desc.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID; + pipeline_desc.RasterizerState.CullMode = D3D12_CULL_MODE_NONE; + pipeline_desc.RasterizerState.FrontCounterClockwise = FALSE; + pipeline_desc.RasterizerState.DepthClipEnable = TRUE; + D3D12_INPUT_ELEMENT_DESC input_elements[3] = {}; + input_elements[0].SemanticName = "POSITION"; + input_elements[0].Format = DXGI_FORMAT_R32G32_FLOAT; + input_elements[0].AlignedByteOffset = offsetof(rex::ui::ImmediateVertex, x); + input_elements[1].SemanticName = "TEXCOORD"; + input_elements[1].Format = DXGI_FORMAT_R32G32_FLOAT; + input_elements[1].AlignedByteOffset = offsetof(rex::ui::ImmediateVertex, u); + input_elements[2].SemanticName = "COLOR"; + input_elements[2].Format = DXGI_FORMAT_R8G8B8A8_UNORM; + input_elements[2].AlignedByteOffset = offsetof(rex::ui::ImmediateVertex, color); + pipeline_desc.InputLayout.pInputElementDescs = input_elements; + pipeline_desc.InputLayout.NumElements = uint32_t(std::size(input_elements)); + pipeline_desc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; + pipeline_desc.NumRenderTargets = 1; + pipeline_desc.RTVFormats[0] = rex::ui::d3d12::D3D12Presenter::kGuestOutputFormat; + pipeline_desc.SampleDesc.Count = 1; + if (FAILED(device->CreateGraphicsPipelineState(&pipeline_desc, + IID_PPV_ARGS(&raster_preview_pipeline_)))) { + REXLOG_ERROR( + "AC6 native graphics bootstrap failed to create the raster replay " + "pipeline"); + return false; + } + + D3D12_DESCRIPTOR_HEAP_DESC view_heap_desc = {}; + view_heap_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; + view_heap_desc.NumDescriptors = 1; + view_heap_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; + if (FAILED(device->CreateDescriptorHeap(&view_heap_desc, + IID_PPV_ARGS(&raster_preview_view_heap_)))) { + REXLOG_ERROR( + "AC6 native graphics bootstrap failed to create the raster replay " + "view heap"); + return false; + } + + D3D12_SHADER_RESOURCE_VIEW_DESC texture_view_desc = {}; + texture_view_desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + texture_view_desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; + texture_view_desc.Shader4ComponentMapping = + D3D12_ENCODE_SHADER_4_COMPONENT_MAPPING( + D3D12_SHADER_COMPONENT_MAPPING_FORCE_VALUE_1, + D3D12_SHADER_COMPONENT_MAPPING_FORCE_VALUE_1, + D3D12_SHADER_COMPONENT_MAPPING_FORCE_VALUE_1, + D3D12_SHADER_COMPONENT_MAPPING_FORCE_VALUE_1); + texture_view_desc.Texture2D.MostDetailedMip = 0; + texture_view_desc.Texture2D.MipLevels = 1; + texture_view_desc.Texture2D.PlaneSlice = 0; + texture_view_desc.Texture2D.ResourceMinLODClamp = 0.0f; + device->CreateShaderResourceView( + nullptr, &texture_view_desc, + raster_preview_view_heap_->GetCPUDescriptorHandleForHeapStart()); + + D3D12_DESCRIPTOR_HEAP_DESC sampler_heap_desc = {}; + sampler_heap_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER; + sampler_heap_desc.NumDescriptors = 1; + sampler_heap_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; + if (FAILED(device->CreateDescriptorHeap(&sampler_heap_desc, + IID_PPV_ARGS(&raster_preview_sampler_heap_)))) { + REXLOG_ERROR( + "AC6 native graphics bootstrap failed to create the raster replay " + "sampler heap"); + return false; + } + + D3D12_SAMPLER_DESC sampler_desc = {}; + sampler_desc.Filter = D3D12_FILTER_MIN_MAG_MIP_POINT; + sampler_desc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler_desc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler_desc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP; + sampler_desc.MaxAnisotropy = 1; + device->CreateSampler(&sampler_desc, + raster_preview_sampler_heap_->GetCPUDescriptorHandleForHeapStart()); + + D3D12_DESCRIPTOR_HEAP_DESC rtv_heap_desc = {}; + rtv_heap_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; + rtv_heap_desc.NumDescriptors = 1; + rtv_heap_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; + rtv_heap_desc.NodeMask = 0; + + D3D12_RESOURCE_DESC upload_buffer_desc = {}; + rex::ui::d3d12::util::FillBufferResourceDesc( + upload_buffer_desc, sizeof(rex::ui::ImmediateVertex) * kRasterPreviewMaxVertexCount, + D3D12_RESOURCE_FLAG_NONE); + + for (PlaceholderRefreshSlot& slot : placeholder_refresh_slots_) { + if (FAILED(device->CreateDescriptorHeap(&rtv_heap_desc, IID_PPV_ARGS(&slot.rtv_heap)))) { + REXLOG_ERROR( + "AC6 native graphics bootstrap failed to create a raster replay " + "RTV heap"); + return false; + } + if (FAILED(device->CreateCommittedResource( + &rex::ui::d3d12::util::kHeapPropertiesUpload, D3D12_HEAP_FLAG_NONE, + &upload_buffer_desc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, + IID_PPV_ARGS(&slot.vertex_upload_buffer)))) { + REXLOG_ERROR( + "AC6 native graphics bootstrap failed to create a raster replay " + "vertex upload buffer"); + return false; + } + D3D12_RANGE read_range = {0, 0}; + if (FAILED(slot.vertex_upload_buffer->Map(0, &read_range, + reinterpret_cast(&slot.vertex_upload_mapping)))) { + REXLOG_ERROR( + "AC6 native graphics bootstrap failed to map a raster replay " + "vertex upload buffer"); + return false; + } + } + + return true; + } + + bool EnsureRasterPreviewRenderTarget(PlaceholderRefreshSlot& slot, uint32_t width, + uint32_t height) { + if (slot.raster_render_target && slot.raster_target_width == width && + slot.raster_target_height == height) { + return true; + } + + slot.raster_render_target.Reset(); + slot.raster_target_width = 0; + slot.raster_target_height = 0; + + D3D12_CLEAR_VALUE clear_value = {}; + clear_value.Format = rex::ui::d3d12::D3D12Presenter::kGuestOutputFormat; + clear_value.Color[0] = 0.0f; + clear_value.Color[1] = 0.0f; + clear_value.Color[2] = 0.0f; + clear_value.Color[3] = 1.0f; + + D3D12_RESOURCE_DESC render_target_desc = {}; + render_target_desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + render_target_desc.Alignment = 0; + render_target_desc.Width = width; + render_target_desc.Height = height; + render_target_desc.DepthOrArraySize = 1; + render_target_desc.MipLevels = 1; + render_target_desc.Format = rex::ui::d3d12::D3D12Presenter::kGuestOutputFormat; + render_target_desc.SampleDesc.Count = 1; + render_target_desc.SampleDesc.Quality = 0; + render_target_desc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; + render_target_desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; + if (FAILED(d3d12_provider_->GetDevice()->CreateCommittedResource( + &rex::ui::d3d12::util::kHeapPropertiesDefault, + d3d12_provider_->GetHeapFlagCreateNotZeroed(), &render_target_desc, + D3D12_RESOURCE_STATE_RENDER_TARGET, &clear_value, + IID_PPV_ARGS(&slot.raster_render_target)))) { + REXLOG_ERROR( + "AC6 native graphics bootstrap failed to create a {}x{} raster replay " + "render target", + width, height); + return false; + } + + d3d12_provider_->GetDevice()->CreateRenderTargetView( + slot.raster_render_target.Get(), nullptr, slot.rtv_heap->GetCPUDescriptorHandleForHeapStart()); + slot.raster_target_width = width; + slot.raster_target_height = height; + return true; + } + + bool InitializePlaceholderRefreshResources() { + auto fail = [this](const char* message) { + REXLOG_ERROR("{}", message); + ShutdownPlaceholderRefreshResources(); + return false; + }; + + ID3D12Device* device = d3d12_provider_->GetDevice(); + ID3D12CommandQueue* direct_queue = d3d12_provider_->GetDirectQueue(); + if (!device || !direct_queue) { + return fail("AC6 native graphics bootstrap D3D12 provider is missing device or queue"); + } + + if (!placeholder_submission_tracker_.Initialize(device, direct_queue)) { + return fail("AC6 native graphics bootstrap failed to create the placeholder submission tracker"); + } + + D3D12_DESCRIPTOR_HEAP_DESC uav_heap_desc = {}; + uav_heap_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; + uav_heap_desc.NumDescriptors = 1; + uav_heap_desc.NodeMask = 0; + + for (uint32_t i = 0; i < kPlaceholderRefreshSlotCount; ++i) { + PlaceholderRefreshSlot& slot = placeholder_refresh_slots_[i]; + if (FAILED(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, + IID_PPV_ARGS(&slot.command_allocator)))) { + return fail("AC6 native graphics bootstrap failed to create a placeholder command allocator"); + } + + uav_heap_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; + if (FAILED(device->CreateDescriptorHeap(&uav_heap_desc, + IID_PPV_ARGS(&slot.shader_visible_uav_heap)))) { + return fail( + "AC6 native graphics bootstrap failed to create a shader-visible UAV heap"); + } + + uav_heap_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; + if (FAILED(device->CreateDescriptorHeap(&uav_heap_desc, IID_PPV_ARGS(&slot.cpu_uav_heap)))) { + return fail("AC6 native graphics bootstrap failed to create a CPU UAV heap"); + } + } + + if (FAILED(device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, + placeholder_refresh_slots_[0].command_allocator.Get(), + nullptr, IID_PPV_ARGS(&placeholder_command_list_)))) { + return fail("AC6 native graphics bootstrap failed to create the placeholder command list"); + } + if (FAILED(placeholder_command_list_->Close())) { + return fail("AC6 native graphics bootstrap failed to close the placeholder command list"); + } + + if (!InitializeRasterPreviewResources(device)) { + return fail("AC6 native graphics bootstrap failed to initialize raster replay resources"); + } + + placeholder_refresh_slot_index_ = 0; + return true; + } + + void ShutdownPlaceholderRefreshResources() { + placeholder_submission_tracker_.Shutdown(); + placeholder_command_list_.Reset(); + raster_preview_pipeline_.Reset(); + raster_preview_root_signature_.Reset(); + raster_preview_view_heap_.Reset(); + raster_preview_sampler_heap_.Reset(); + placeholder_refresh_slot_index_ = 0; + placeholder_resources_initialized_ = false; + { + std::lock_guard lock(g_native_graphics_status_mutex); + g_native_graphics_status.placeholder_resources_initialized = false; + } + for (PlaceholderRefreshSlot& slot : placeholder_refresh_slots_) { + slot.last_submission = 0; + if (slot.vertex_upload_buffer && slot.vertex_upload_mapping) { + slot.vertex_upload_buffer->Unmap(0, nullptr); + } + slot.vertex_upload_mapping = nullptr; + slot.vertex_upload_buffer.Reset(); + slot.raster_render_target.Reset(); + slot.rtv_heap.Reset(); + slot.raster_target_width = 0; + slot.raster_target_height = 0; + slot.cpu_uav_heap.Reset(); + slot.shader_visible_uav_heap.Reset(); + slot.command_allocator.Reset(); + } + } + + bool RefreshPlaceholderFrame(const rex::system::GraphicsSwapSubmission& submission) { + if (!d3d12_presenter_ || !submission.frontbuffer_width || !submission.frontbuffer_height) { + return false; + } + + return d3d12_presenter_->RefreshGuestOutput( + submission.frontbuffer_width, submission.frontbuffer_height, + submission.frontbuffer_width, submission.frontbuffer_height, + [this, submission](rex::ui::Presenter::GuestOutputRefreshContext& context) { + return RecordPlaceholderFrame( + static_cast( + context), + submission); + }); + } + + bool SubmitPlaceholderCommandList(PlaceholderRefreshSlot& slot) { + if (FAILED(placeholder_command_list_->Close())) { + REXLOG_ERROR("AC6 native graphics bootstrap failed to close the placeholder command list"); + return false; + } + + ID3D12CommandList* execute_command_list = placeholder_command_list_.Get(); + d3d12_provider_->GetDirectQueue()->ExecuteCommandLists(1, &execute_command_list); + slot.last_submission = placeholder_submission_tracker_.GetCurrentSubmission(); + if (!placeholder_submission_tracker_.NextSubmission()) { + REXLOG_WARN( + "AC6 native graphics bootstrap could not signal the placeholder " + "refresh fence immediately"); + } + placeholder_refresh_slot_index_ = + (placeholder_refresh_slot_index_ + 1) % kPlaceholderRefreshSlotCount; + return true; + } + + bool TryRecordRasterPlaceholderFrame( + PlaceholderRefreshSlot& slot, ID3D12Resource* guest_output_resource, + const rex::system::GraphicsSwapSubmission& submission) { + if (!raster_preview_pipeline_ || !raster_preview_root_signature_ || !raster_preview_view_heap_ || + !raster_preview_sampler_heap_ || !slot.rtv_heap || !slot.vertex_upload_mapping || + !guest_output_resource) { + return false; + } + if (!EnsureRasterPreviewRenderTarget(slot, submission.frontbuffer_width, + submission.frontbuffer_height)) { + return false; + } + + const UINT width = submission.frontbuffer_width; + const UINT height = submission.frontbuffer_height; + const UINT title_band_height = std::max(1, height / 8); + const UINT status_band_height = std::max(1, height / 20); + const UINT heartbeat_width = + std::max(1, UINT((uint64_t(width) * + (((swap_count_ - 1) % kPlaceholderHeartbeatPeriod) + 1)) / + kPlaceholderHeartbeatPeriod)); + + const NativeReplayPlanSummary& replay_plan = last_replay_plan_; + const SelectedPassPreviewData& selected_pass_preview = last_selected_pass_preview_; + const uint32_t candidate_seed = + replay_plan.present_candidate_rt0 ^ (replay_plan.present_candidate_depth_stencil * 33u) ^ + (replay_plan.selected_pass_score * 2654435761u); + auto channel = [candidate_seed](uint32_t shift, float low, float high) { + const float t = float((candidate_seed >> shift) & 0xFFu) / 255.0f; + return low + (high - low) * t; + }; + + const FloatColor background_color = {replay_plan.valid ? 0.03f : 0.08f, + replay_plan.valid ? 0.04f : 0.06f, + replay_plan.valid ? 0.07f : 0.10f, 1.0f}; + const FloatColor title_band_color = { + replay_plan.selected_pass_is_stable ? 0.18f + : (replay_plan.present_candidate_matches_swap_size ? 0.18f : 0.55f), + replay_plan.selected_pass_is_stable ? 0.62f + : (replay_plan.present_candidate_matches_swap_size ? 0.56f : 0.22f), + replay_plan.selected_pass_is_stable ? 0.46f + : (replay_plan.present_candidate_matches_swap_size ? 0.24f : 0.18f), 1.0f}; + const FloatColor fallback_candidate_color = {channel(0, 0.20f, 0.92f), + channel(8, 0.18f, 0.75f), + channel(16, 0.16f, 0.88f), 1.0f}; + const FloatColor resolve_color = {0.98f, 0.95f, 0.28f, 0.92f}; + const FloatColor heartbeat_color = {0.96f, 0.92f, 0.22f, 1.0f}; + const FloatColor candidate_base_color = + selected_pass_preview.summary.using_clear_fill + ? DecodeArgbColor(selected_pass_preview.summary.last_clear_color) + : fallback_candidate_color; + const FloatColor stripe_color = MixColors(candidate_base_color, title_band_color, 0.40f); + const FloatColor candidate_outline_color = + MixColors(candidate_base_color, heartbeat_color, 0.55f); + const FloatColor score_color = MixColors(candidate_outline_color, heartbeat_color, 0.35f); + + UINT candidate_left = 0; + UINT candidate_top = title_band_height; + UINT candidate_width = width; + UINT candidate_height = std::max(1, height - title_band_height - status_band_height); + if (replay_plan.valid && replay_plan.present_candidate_viewport_width && + replay_plan.present_candidate_viewport_height) { + candidate_left = std::min(width - 1, replay_plan.present_candidate_viewport_x); + candidate_top = std::min(height - 1, replay_plan.present_candidate_viewport_y); + candidate_width = + std::max(1, std::min(replay_plan.present_candidate_viewport_width, + width - candidate_left)); + candidate_height = + std::max(1, std::min(replay_plan.present_candidate_viewport_height, + height - candidate_top)); + } + const UINT candidate_right = std::min(width, candidate_left + candidate_width); + const UINT candidate_bottom = std::min(height, candidate_top + candidate_height); + + std::array vertices = {}; + uint32_t vertex_count = 0; + auto push_rect = [&](float left, float top, float right, float bottom, FloatColor color) { + left = std::clamp(left, 0.0f, float(width)); + top = std::clamp(top, 0.0f, float(height)); + right = std::clamp(right, 0.0f, float(width)); + bottom = std::clamp(bottom, 0.0f, float(height)); + if (left >= right || top >= bottom || vertex_count + 6 > vertices.size()) { + return; + } + const uint32_t packed_color = PackImmediateColor(color); + auto emit_vertex = [&](float x, float y) { + rex::ui::ImmediateVertex& vertex = vertices[vertex_count++]; + vertex.x = x; + vertex.y = y; + vertex.u = 0.0f; + vertex.v = 0.0f; + vertex.color = packed_color; + }; + emit_vertex(left, top); + emit_vertex(right, top); + emit_vertex(right, bottom); + emit_vertex(left, top); + emit_vertex(right, bottom); + emit_vertex(left, bottom); + }; + + push_rect(float(candidate_left), float(candidate_top), float(candidate_right), + float(candidate_bottom), candidate_base_color); + + const FloatColor clear_overlay_tint = {1.0f, 1.0f, 1.0f, 0.92f}; + for (uint32_t i = 0; i < selected_pass_preview.clear_step_count; ++i) { + const SelectedPassClearStep& step = selected_pass_preview.clear_steps[i]; + FloatColor step_color = DecodeArgbColor(step.color); + step_color[3] = clear_overlay_tint[3]; + if (step.rect_count) { + for (uint32_t rect_index = 0; rect_index < step.rect_count; ++rect_index) { + const ac6::d3d::ClearRect& rect = step.rects[rect_index]; + push_rect(float(rect.left), float(rect.top), float(rect.right), float(rect.bottom), + step_color); + } + } else { + push_rect(float(candidate_left), float(candidate_top), float(candidate_right), + float(candidate_bottom), step_color); + } + } + + UINT resolve_band_height = 0; + if (replay_plan.valid && replay_plan.selected_pass_has_resolve) { + resolve_band_height = std::max(1, candidate_height / 10); + push_rect(float(candidate_left), float(candidate_bottom - resolve_band_height), + float(candidate_right), float(candidate_bottom), resolve_color); + } + + if (selected_pass_preview.summary.draw_count && candidate_width > 8 && candidate_height > 8) { + const UINT stripe_count = + std::min(12, std::max(1, 1 + (selected_pass_preview.summary.draw_count / 6))); + const UINT stripe_width = + std::max(1, candidate_width / std::max(24, stripe_count * 5)); + const UINT stripe_top = candidate_top; + const UINT stripe_bottom = + candidate_bottom > resolve_band_height ? (candidate_bottom - resolve_band_height) + : candidate_bottom; + const UINT stripe_area_height = + stripe_bottom > stripe_top ? (stripe_bottom - stripe_top) : 0; + for (UINT i = 0; i < stripe_count && stripe_area_height; ++i) { + const UINT stripe_center_x = + candidate_left + ((i + 1) * candidate_width) / (stripe_count + 1); + const UINT stripe_left = stripe_center_x > stripe_width / 2 + ? stripe_center_x - stripe_width / 2 + : candidate_left; + const UINT stripe_right = std::min(candidate_right, stripe_left + stripe_width); + const UINT stripe_height = + std::max(1, stripe_area_height * (45 + ((i * 13) % 40)) / 100); + const UINT stripe_start = stripe_bottom > stripe_height ? (stripe_bottom - stripe_height) + : stripe_top; + FloatColor lane_color = + (i & 1u) ? MixColors(stripe_color, heartbeat_color, 0.22f) + : MixColors(stripe_color, background_color, 0.10f); + lane_color[3] = 0.42f; + push_rect(float(stripe_left), float(stripe_start), float(stripe_right), + float(stripe_bottom), lane_color); + } + } + + if (candidate_width > 2 && candidate_height > 2) { + const UINT outline_thickness = + std::max(1, std::min(4, std::min(candidate_width, candidate_height) / 96 + 1)); + const UINT bottom_outline_top = + candidate_bottom > outline_thickness ? (candidate_bottom - outline_thickness) + : candidate_top; + push_rect(float(candidate_left), float(candidate_top), float(candidate_right), + float(std::min(candidate_bottom, candidate_top + outline_thickness)), + candidate_outline_color); + push_rect(float(candidate_left), float(bottom_outline_top), float(candidate_right), + float(candidate_bottom), candidate_outline_color); + push_rect(float(candidate_left), float(candidate_top), + float(std::min(candidate_right, candidate_left + outline_thickness)), + float(candidate_bottom), candidate_outline_color); + push_rect(float(candidate_right > outline_thickness ? (candidate_right - outline_thickness) + : candidate_left), + float(candidate_top), float(candidate_right), float(candidate_bottom), + candidate_outline_color); + } + + if (replay_plan.valid && replay_plan.pass_count) { + const UINT score_width = std::max( + 1, UINT((uint64_t(width) * std::min(replay_plan.selected_pass_score, 220u)) / + 220u)); + push_rect(0.0f, float(title_band_height), float(score_width), + float(std::min(height, title_band_height + status_band_height)), score_color); + } + push_rect(0.0f, 0.0f, float(width), float(title_band_height), title_band_color); + push_rect(0.0f, float(height - status_band_height), float(heartbeat_width), float(height), + heartbeat_color); + + D3D12_VIEWPORT viewport = {}; + viewport.TopLeftX = 0.0f; + viewport.TopLeftY = 0.0f; + viewport.Width = float(width); + viewport.Height = float(height); + viewport.MinDepth = 0.0f; + viewport.MaxDepth = 1.0f; + D3D12_RECT scissor = {0, 0, LONG(width), LONG(height)}; + placeholder_command_list_->RSSetViewports(1, &viewport); + placeholder_command_list_->RSSetScissorRects(1, &scissor); + + const D3D12_CPU_DESCRIPTOR_HANDLE rtv = + slot.rtv_heap->GetCPUDescriptorHandleForHeapStart(); + placeholder_command_list_->OMSetRenderTargets(1, &rtv, TRUE, nullptr); + placeholder_command_list_->ClearRenderTargetView(rtv, background_color.data(), 0, nullptr); + + if (vertex_count) { + std::memcpy(slot.vertex_upload_mapping, vertices.data(), + sizeof(rex::ui::ImmediateVertex) * vertex_count); + D3D12_VERTEX_BUFFER_VIEW vertex_buffer_view = {}; + vertex_buffer_view.BufferLocation = slot.vertex_upload_buffer->GetGPUVirtualAddress(); + vertex_buffer_view.SizeInBytes = UINT(sizeof(rex::ui::ImmediateVertex) * vertex_count); + vertex_buffer_view.StrideInBytes = UINT(sizeof(rex::ui::ImmediateVertex)); + + ID3D12DescriptorHeap* descriptor_heaps[] = {raster_preview_view_heap_.Get(), + raster_preview_sampler_heap_.Get()}; + placeholder_command_list_->SetDescriptorHeaps(uint32_t(std::size(descriptor_heaps)), + descriptor_heaps); + placeholder_command_list_->SetGraphicsRootSignature(raster_preview_root_signature_.Get()); + const float coordinate_space_size_inv[2] = {1.0f / float(width), 1.0f / float(height)}; + placeholder_command_list_->SetGraphicsRoot32BitConstants(2, 2, coordinate_space_size_inv, 0); + placeholder_command_list_->SetGraphicsRootDescriptorTable( + 0, raster_preview_view_heap_->GetGPUDescriptorHandleForHeapStart()); + placeholder_command_list_->SetGraphicsRootDescriptorTable( + 1, raster_preview_sampler_heap_->GetGPUDescriptorHandleForHeapStart()); + placeholder_command_list_->SetPipelineState(raster_preview_pipeline_.Get()); + placeholder_command_list_->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + placeholder_command_list_->IASetVertexBuffers(0, 1, &vertex_buffer_view); + placeholder_command_list_->DrawInstanced(vertex_count, 1, 0, 0); + } + + D3D12_RESOURCE_BARRIER barriers[3] = {}; + barriers[0].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[0].Transition.pResource = slot.raster_render_target.Get(); + barriers[0].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barriers[0].Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; + barriers[0].Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE; + barriers[1].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barriers[1].Transition.pResource = guest_output_resource; + barriers[1].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barriers[1].Transition.StateBefore = rex::ui::d3d12::D3D12Presenter::kGuestOutputInternalState; + barriers[1].Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_DEST; + placeholder_command_list_->ResourceBarrier(2, barriers); + + D3D12_TEXTURE_COPY_LOCATION copy_dest = {}; + copy_dest.pResource = guest_output_resource; + copy_dest.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + copy_dest.SubresourceIndex = 0; + D3D12_TEXTURE_COPY_LOCATION copy_source = {}; + copy_source.pResource = slot.raster_render_target.Get(); + copy_source.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + copy_source.SubresourceIndex = 0; + placeholder_command_list_->CopyTextureRegion(©_dest, 0, 0, 0, ©_source, nullptr); + + barriers[0].Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_DEST; + barriers[0].Transition.StateAfter = rex::ui::d3d12::D3D12Presenter::kGuestOutputInternalState; + barriers[0].Transition.pResource = guest_output_resource; + barriers[1].Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE; + barriers[1].Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; + barriers[1].Transition.pResource = slot.raster_render_target.Get(); + placeholder_command_list_->ResourceBarrier(2, barriers); + + return true; + } + + bool RecordPlaceholderFrame( + rex::ui::d3d12::D3D12Presenter::D3D12GuestOutputRefreshContext& context, + const rex::system::GraphicsSwapSubmission& submission) { + std::lock_guard lock(placeholder_refresh_mutex_); + + if (!d3d12_provider_ || !placeholder_command_list_) { + return false; + } + + PlaceholderRefreshSlot& slot = placeholder_refresh_slots_[placeholder_refresh_slot_index_]; + if (slot.last_submission && + !placeholder_submission_tracker_.AwaitSubmissionCompletion(slot.last_submission)) { + REXLOG_ERROR( + "AC6 native graphics bootstrap failed to await placeholder refresh " + "slot {}", placeholder_refresh_slot_index_); + return false; + } + + ID3D12Resource* guest_output_resource = context.resource_uav_capable(); + if (!guest_output_resource) { + return false; + } + + if (FAILED(slot.command_allocator->Reset())) { + REXLOG_ERROR("AC6 native graphics bootstrap failed to reset a placeholder command allocator"); + return false; + } + if (FAILED(placeholder_command_list_->Reset(slot.command_allocator.Get(), nullptr))) { + REXLOG_ERROR("AC6 native graphics bootstrap failed to reset the placeholder command list"); + return false; + } + + last_present_used_raster_replay_ = false; + if (TryRecordRasterPlaceholderFrame(slot, guest_output_resource, submission)) { + context.SetIs8bpc(true); + last_present_used_raster_replay_ = true; + return SubmitPlaceholderCommandList(slot); + } + if (FAILED(placeholder_command_list_->Reset(slot.command_allocator.Get(), nullptr))) { + REXLOG_ERROR( + "AC6 native graphics bootstrap failed to reset the placeholder command list " + "for the legacy fallback path"); + return false; + } + + ID3D12Device* device = d3d12_provider_->GetDevice(); + D3D12_UNORDERED_ACCESS_VIEW_DESC uav_desc = {}; + uav_desc.Format = rex::ui::d3d12::D3D12Presenter::kGuestOutputFormat; + uav_desc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D; + uav_desc.Texture2D.MipSlice = 0; + uav_desc.Texture2D.PlaneSlice = 0; + + D3D12_CPU_DESCRIPTOR_HANDLE uav_cpu_visible = + slot.shader_visible_uav_heap->GetCPUDescriptorHandleForHeapStart(); + D3D12_GPU_DESCRIPTOR_HANDLE uav_gpu_visible = + slot.shader_visible_uav_heap->GetGPUDescriptorHandleForHeapStart(); + D3D12_CPU_DESCRIPTOR_HANDLE uav_cpu = + slot.cpu_uav_heap->GetCPUDescriptorHandleForHeapStart(); + device->CreateUnorderedAccessView(guest_output_resource, nullptr, &uav_desc, uav_cpu_visible); + device->CreateUnorderedAccessView(guest_output_resource, nullptr, &uav_desc, uav_cpu); + + ID3D12DescriptorHeap* descriptor_heaps[] = {slot.shader_visible_uav_heap.Get()}; + placeholder_command_list_->SetDescriptorHeaps(1, descriptor_heaps); + + D3D12_RESOURCE_BARRIER barrier = {}; + barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; + barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE; + barrier.Transition.pResource = guest_output_resource; + barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; + barrier.Transition.StateBefore = rex::ui::d3d12::D3D12Presenter::kGuestOutputInternalState; + barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_UNORDERED_ACCESS; + placeholder_command_list_->ResourceBarrier(1, &barrier); + + context.SetIs8bpc(true); + + const UINT width = submission.frontbuffer_width; + const UINT height = submission.frontbuffer_height; + const UINT title_band_height = std::max(1, height / 8); + const UINT status_band_height = std::max(1, height / 20); + const UINT heartbeat_width = + std::max(1, UINT((uint64_t(width) * + (((swap_count_ - 1) % kPlaceholderHeartbeatPeriod) + 1)) / + kPlaceholderHeartbeatPeriod)); + + const NativeReplayPlanSummary& replay_plan = last_replay_plan_; + const SelectedPassPreviewData& selected_pass_preview = last_selected_pass_preview_; + const uint32_t candidate_seed = + replay_plan.present_candidate_rt0 ^ (replay_plan.present_candidate_depth_stencil * 33u) ^ + (replay_plan.selected_pass_score * 2654435761u); + auto channel = [candidate_seed](uint32_t shift, float low, float high) { + float t = float((candidate_seed >> shift) & 0xFFu) / 255.0f; + return low + (high - low) * t; + }; + + const FloatColor background_color = {replay_plan.valid ? 0.04f : 0.08f, + replay_plan.valid ? 0.05f : 0.06f, + replay_plan.valid ? 0.08f : 0.10f, 1.0f}; + const FloatColor title_band_color = { + replay_plan.selected_pass_is_stable ? 0.18f + : (replay_plan.present_candidate_matches_swap_size ? 0.18f : 0.55f), + replay_plan.selected_pass_is_stable ? 0.62f + : (replay_plan.present_candidate_matches_swap_size ? 0.56f : 0.22f), + replay_plan.selected_pass_is_stable ? 0.46f + : (replay_plan.present_candidate_matches_swap_size ? 0.24f : 0.18f), 1.0f}; + const FloatColor fallback_candidate_color = {channel(0, 0.20f, 0.92f), + channel(8, 0.18f, 0.75f), + channel(16, 0.16f, 0.88f), 1.0f}; + const FloatColor resolve_color = {0.96f, 0.94f, 0.30f, 1.0f}; + const FloatColor heartbeat_color = {0.95f, 0.92f, 0.24f, 1.0f}; + const FloatColor candidate_base_color = + selected_pass_preview.summary.using_clear_fill + ? DecodeArgbColor(selected_pass_preview.summary.last_clear_color) + : fallback_candidate_color; + const FloatColor candidate_outline_color = MixColors(candidate_base_color, heartbeat_color, 0.5f); + const FloatColor stripe_color = MixColors(candidate_base_color, title_band_color, 0.4f); + + auto clear_rect = [&](const FloatColor& color, UINT left, UINT top, UINT right, UINT bottom) { + if (left >= right || top >= bottom) { + return; + } + D3D12_RECT rect = {LONG(left), LONG(top), LONG(right), LONG(bottom)}; + placeholder_command_list_->ClearUnorderedAccessViewFloat( + uav_gpu_visible, uav_cpu, guest_output_resource, color.data(), 1, &rect); + }; + + placeholder_command_list_->ClearUnorderedAccessViewFloat( + uav_gpu_visible, uav_cpu, guest_output_resource, background_color.data(), 0, nullptr); + + UINT candidate_left = 0; + UINT candidate_top = title_band_height; + UINT candidate_width = width; + UINT candidate_height = std::max(1, height - title_band_height - status_band_height); + if (replay_plan.valid && replay_plan.present_candidate_viewport_width && + replay_plan.present_candidate_viewport_height) { + candidate_left = std::min(width - 1, replay_plan.present_candidate_viewport_x); + candidate_top = std::min(height - 1, replay_plan.present_candidate_viewport_y); + candidate_width = + std::max(1, std::min(replay_plan.present_candidate_viewport_width, + width - candidate_left)); + candidate_height = + std::max(1, std::min(replay_plan.present_candidate_viewport_height, + height - candidate_top)); + } + D3D12_RECT center_block_rect = {LONG(candidate_left), LONG(candidate_top), + LONG(std::min(width, candidate_left + candidate_width)), + LONG(std::min(height, candidate_top + candidate_height))}; + placeholder_command_list_->ClearUnorderedAccessViewFloat( + uav_gpu_visible, uav_cpu, guest_output_resource, candidate_base_color.data(), 1, + ¢er_block_rect); + + const UINT candidate_right = std::min(width, candidate_left + candidate_width); + const UINT candidate_bottom = std::min(height, candidate_top + candidate_height); + + UINT sampled_clear_band_height = 0; + if (selected_pass_preview.clear_step_count && candidate_height > 2) { + sampled_clear_band_height = + std::max(1, std::min(candidate_height / 12, height / 32 + 1)); + for (uint32_t i = 0; i < selected_pass_preview.clear_step_count; ++i) { + const UINT band_top = std::min(candidate_bottom, candidate_top + sampled_clear_band_height * i); + const UINT band_bottom = + std::min(candidate_bottom, band_top + sampled_clear_band_height); + clear_rect(DecodeArgbColor(selected_pass_preview.clear_steps[i].color), candidate_left, + band_top, candidate_right, band_bottom); + } + } + + UINT resolve_band_height = 0; + if (replay_plan.valid && replay_plan.selected_pass_has_resolve) { + resolve_band_height = std::max(1, candidate_height / 10); + clear_rect(resolve_color, candidate_left, + std::min(candidate_bottom, candidate_bottom - resolve_band_height), + candidate_right, candidate_bottom); + } + + if (selected_pass_preview.summary.draw_count && candidate_width > 8 && candidate_height > 8) { + const UINT stripe_count = + std::min(12, std::max(1, 1 + (selected_pass_preview.summary.draw_count / 6))); + const UINT stripe_width = + std::max(1, candidate_width / std::max(24, stripe_count * 5)); + const UINT stripe_top = std::min(candidate_bottom, candidate_top + sampled_clear_band_height); + const UINT stripe_bottom = + candidate_bottom > resolve_band_height ? (candidate_bottom - resolve_band_height) + : candidate_bottom; + const UINT stripe_area_height = + stripe_bottom > stripe_top ? (stripe_bottom - stripe_top) : 0; + for (UINT i = 0; i < stripe_count && stripe_area_height; ++i) { + const UINT stripe_center_x = + candidate_left + ((i + 1) * candidate_width) / (stripe_count + 1); + const UINT stripe_left = stripe_center_x > stripe_width / 2 + ? stripe_center_x - stripe_width / 2 + : candidate_left; + const UINT stripe_right = std::min(candidate_right, stripe_left + stripe_width); + const UINT stripe_height = + std::max(1, stripe_area_height * (45 + ((i * 13) % 40)) / 100); + const UINT stripe_start = stripe_bottom > stripe_height ? (stripe_bottom - stripe_height) + : stripe_top; + const FloatColor lane_color = + (i & 1u) ? MixColors(stripe_color, heartbeat_color, 0.22f) + : MixColors(stripe_color, background_color, 0.10f); + clear_rect(lane_color, stripe_left, stripe_start, stripe_right, stripe_bottom); + } + } + + if (candidate_width > 2 && candidate_height > 2) { + const UINT outline_thickness = + std::max(1, std::min(4, std::min(candidate_width, candidate_height) / 96 + 1)); + const UINT bottom_outline_top = + candidate_bottom > outline_thickness ? (candidate_bottom - outline_thickness) + : candidate_top; + clear_rect(candidate_outline_color, candidate_left, candidate_top, candidate_right, + std::min(candidate_bottom, candidate_top + outline_thickness)); + clear_rect(candidate_outline_color, candidate_left, bottom_outline_top, candidate_right, + candidate_bottom); + clear_rect(candidate_outline_color, candidate_left, candidate_top, + std::min(candidate_right, candidate_left + outline_thickness), candidate_bottom); + clear_rect(candidate_outline_color, + candidate_right > outline_thickness ? (candidate_right - outline_thickness) + : candidate_left, + candidate_top, candidate_right, candidate_bottom); + } + + if (replay_plan.valid && replay_plan.pass_count) { + const UINT score_width = std::max( + 1, UINT((uint64_t(width) * std::min(replay_plan.selected_pass_score, 220u)) / + 220u)); + clear_rect(candidate_outline_color, 0, title_band_height, score_width, + std::min(height, title_band_height + status_band_height)); + } + + clear_rect(title_band_color, 0, 0, width, title_band_height); + clear_rect(heartbeat_color, 0, height - status_band_height, heartbeat_width, height); + + barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_UNORDERED_ACCESS; + barrier.Transition.StateAfter = rex::ui::d3d12::D3D12Presenter::kGuestOutputInternalState; + placeholder_command_list_->ResourceBarrier(1, &barrier); + + return SubmitPlaceholderCommandList(slot); + } + + rex::ui::d3d12::D3D12Provider* d3d12_provider_ = nullptr; + rex::ui::d3d12::D3D12Presenter* d3d12_presenter_ = nullptr; + std::mutex placeholder_refresh_mutex_; + rex::ui::d3d12::D3D12SubmissionTracker placeholder_submission_tracker_; + std::array placeholder_refresh_slots_; + Microsoft::WRL::ComPtr placeholder_command_list_; + Microsoft::WRL::ComPtr raster_preview_root_signature_; + Microsoft::WRL::ComPtr raster_preview_pipeline_; + Microsoft::WRL::ComPtr raster_preview_view_heap_; + Microsoft::WRL::ComPtr raster_preview_sampler_heap_; + uint32_t placeholder_refresh_slot_index_ = 0; + rex::system::GraphicsSwapSubmission last_swap_submission_; + NativeReplayPlanSummary last_replay_plan_{}; + SelectedPassPreviewData last_selected_pass_preview_{}; + ReplayCandidateKey last_selected_candidate_{}; + bool last_selected_candidate_valid_ = false; + uint32_t selected_candidate_streak_ = 0; + uint64_t swap_count_ = 0; + bool logged_first_swap_ = false; + bool logged_first_present_ = false; + bool logged_passthrough_swap_ = false; + bool logged_refresh_failure_ = false; + bool last_present_used_raster_replay_ = false; + bool placeholder_resources_initialized_ = false; +}; +#endif + +} // namespace + +void ConfigureGraphicsBackend(rex::RuntimeConfig& config) { + { + std::lock_guard lock(g_native_graphics_status_mutex); + g_native_graphics_status = {}; + g_native_graphics_status.bootstrap_enabled = REXCVAR_GET(ac6_native_graphics_bootstrap); + g_native_graphics_status.placeholder_present_enabled = + REXCVAR_GET(ac6_native_graphics_placeholder_present); + } +#if REX_HAS_D3D12 + if (!REXCVAR_GET(ac6_native_graphics_bootstrap)) { + return; + } + + config.graphics = std::make_unique(); +#else + (void)config; +#endif +} + +NativeGraphicsStatusSnapshot GetNativeGraphicsStatus() { + std::lock_guard lock(g_native_graphics_status_mutex); + NativeGraphicsStatusSnapshot snapshot = g_native_graphics_status; + snapshot.bootstrap_enabled = REXCVAR_GET(ac6_native_graphics_bootstrap); + snapshot.placeholder_present_enabled = REXCVAR_GET(ac6_native_graphics_placeholder_present); + return snapshot; +} + +} // namespace ac6::graphics diff --git a/src/ac6_native_graphics.h b/src/ac6_native_graphics.h new file mode 100644 index 00000000..4d578ed2 --- /dev/null +++ b/src/ac6_native_graphics.h @@ -0,0 +1,84 @@ +#pragma once + +#include + +#include +#include + +#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 diff --git a/src/ac6_native_graphics_overlay.cpp b/src/ac6_native_graphics_overlay.cpp new file mode 100644 index 00000000..7ba93546 --- /dev/null +++ b/src/ac6_native_graphics_overlay.cpp @@ -0,0 +1,206 @@ +#include "ac6_native_graphics_overlay.h" + +#include +#include + +#include + +#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(native_status.total_swap_count), + static_cast(native_status.intercepted_swap_count), + static_cast(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(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(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(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(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 diff --git a/src/ac6_native_graphics_overlay.h b/src/ac6_native_graphics_overlay.h new file mode 100644 index 00000000..eba57a8d --- /dev/null +++ b/src/ac6_native_graphics_overlay.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +#include + +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 diff --git a/src/ac6recomp_app.h b/src/ac6recomp_app.h index 667c254e..d82565a6 100644 --- a/src/ac6recomp_app.h +++ b/src/ac6recomp_app.h @@ -1,13 +1,20 @@ #pragma once +#include + #include #include +#include #include #include +#include +#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(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 native_graphics_status_dialog_; }; diff --git a/src/d3d_hooks.cpp b/src/d3d_hooks.cpp index 6d02663e..c31788ee 100644 --- a/src/d3d_hooks.cpp +++ b/src/d3d_hooks.cpp @@ -1,11 +1,16 @@ #include "d3d_hooks.h" +#include +#include + #include #include #include 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 g_live_draws; +std::vector g_live_clears; +std::vector g_live_resolves; + +template +uint32_t CountNonZero(const std::array& values) { + uint32_t count = 0; + for (const T& value : values) { + if (value) { + ++count; + } + } + return count; +} + +uint32_t CountNonZeroStreams(const std::array& streams) { + uint32_t count = 0; + for (const auto& stream : streams) { + if (stream.buffer) { + ++count; + } + } + return count; +} + +uint32_t CountNonZeroSamplers( + const std::array& 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(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(frame_capture.draws.size()); + summary.clear_count = static_cast(frame_capture.clears.size()); + summary.resolve_count = static_cast(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 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(unique_rt0s.size()); + summary.record_signature = signature; + summary.record_signature_valid = true; + } + return summary; +} + +void RememberDevice(uint32_t device) { + std::unique_lock lock(g_shadow_mutex); + g_shadow.device = device; +} + +ac6::d3d::ShadowState SnapshotShadowState(uint32_t device) { + std::shared_lock 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 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(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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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(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 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 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 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 lock(g_snapshot_mutex); return g_snapshot; } -const ShadowState& GetShadowState() { +FrameCaptureSnapshot GetFrameCapture() { + std::shared_lock lock(g_capture_mutex); + return g_capture_snapshot; +} + +FrameCaptureSummary GetFrameCaptureSummary() { + std::shared_lock lock(g_capture_mutex); + return g_capture_summary; +} + +ShadowState GetShadowState() { + std::shared_lock lock(g_shadow_mutex); return g_shadow; } diff --git a/src/d3d_hooks.h b/src/d3d_hooks.h index ecd352c8..2c56ad7a 100644 --- a/src/d3d_hooks.h +++ b/src/d3d_hooks.h @@ -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 diff --git a/src/d3d_state.h b/src/d3d_state.h index 6678d852..0966f19c 100644 --- a/src/d3d_state.h +++ b/src/d3d_state.h @@ -3,6 +3,7 @@ #include #include #include +#include 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 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 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 draws; + std::vector clears; + std::vector 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 diff --git a/src/main.cpp b/src/main.cpp index 9c0a5b6d..37769e98 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 + #include "ac6recomp_app.h" +REXCVAR_DEFINE_BOOL(audio_deep_trace, false, "Audio", + "Enable verbose runtime audio tracing"); + REX_DEFINE_APP(ac6recomp, Ac6recompApp::Create) diff --git a/src/render_hooks.cpp b/src/render_hooks.cpp index 756acc88..1cf05557 100644 --- a/src/render_hooks.cpp +++ b/src/render_hooks.cpp @@ -1,70 +1,77 @@ #include "render_hooks.h" #include "d3d_hooks.h" -#include #include +#include #include -#include 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 g_frame_time_ms{0.0}; -std::atomic g_fps{0.0}; -std::atomic 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(now - g_frame_start) - .count(); - float fps_val = ms > 0.0 ? static_cast(1000.0 / ms) : 0.0f; - - g_frame_time_ms.store(ms, std::memory_order_relaxed); - g_fps.store(static_cast(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 lock(g_frame_mutex); + if (g_frame_start.time_since_epoch().count() != 0) { + g_frame_time_ms = + std::chrono::duration(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 lock(g_frame_mutex); + return FrameStats{g_frame_time_ms, g_fps, g_frame_count}; } } // namespace ac6 diff --git a/src/render_hooks.h b/src/render_hooks.h index 95ddcca5..8515ba1e 100644 --- a/src/render_hooks.h +++ b/src/render_hooks.h @@ -3,17 +3,23 @@ #include #include +#include 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);