From c09b8c892d6038236def81e245dce6aef53aab19 Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Thu, 2 Jul 2026 22:50:13 +0100 Subject: [PATCH] Skate 3: gameplay-context plumbing, one-shot boot auto-tap, emulated draw suppression --- include/rex/graphics/native_guest_renderer.h | 8 ++++++ include/rex/graphics/ultrawide_debug.h | 3 ++ src/core/ultrawide_debug.cpp | 15 +++++++++- src/graphics/d3d12/command_processor.cpp | 17 ++++++++++++ src/graphics/native_guest_renderer.cpp | 29 +++++++++++++++++++- src/kernel/xam/apps/xgi_app.cpp | 23 ++++++++++++++-- 6 files changed, 90 insertions(+), 5 deletions(-) diff --git a/include/rex/graphics/native_guest_renderer.h b/include/rex/graphics/native_guest_renderer.h index 3c93833..eebd30a 100644 --- a/include/rex/graphics/native_guest_renderer.h +++ b/include/rex/graphics/native_guest_renderer.h @@ -86,4 +86,12 @@ using NativeGuestOutputRenderer = bool (*)(const NativeGuestOutputRenderContext& void SetNativeGuestOutputRenderer(NativeGuestOutputRenderer renderer, void* user_data); bool TryRenderNativeGuestOutput(const NativeGuestOutputRenderContext& context); +// True while the registered renderer actually replaced the last presented +// frame (false when it yields to the emulated output). +bool IsNativeGuestOutputActive(); +// native_render_suppress_emulated_draws && IsNativeGuestOutputActive(): +// command processors skip emulated draw/resolve execution (memexport draws, +// fences, queries and PM4 parsing still run). +bool ShouldSuppressEmulatedDraws(); + } // namespace rex::graphics diff --git a/include/rex/graphics/ultrawide_debug.h b/include/rex/graphics/ultrawide_debug.h index db6acd1..ee5c9c4 100644 --- a/include/rex/graphics/ultrawide_debug.h +++ b/include/rex/graphics/ultrawide_debug.h @@ -135,6 +135,9 @@ enum class ApplyMode : uint32_t { uint64_t HashKey(const TargetKey& key); void NotifySkate3GameplayContext(uint32_t user_index, uint32_t context_id, uint32_t value); bool IsSkate3GameplayUltrawideActive(); +// Live value of the Skate 3 presence context 0x8001 (1 = in gameplay, +// 0 = frontend / pause menu / loading). +uint32_t Skate3GameplayContextValue(); bool ShouldApplyAndRecord(const TargetKey& key, bool default_enabled); uint32_t ShadowMapCandidateLevel(const TargetKey& key); bool IsShadowMapCandidate(const TargetKey& key, uint32_t mode = 1); diff --git a/src/core/ultrawide_debug.cpp b/src/core/ultrawide_debug.cpp index 4a8e1f0..be5532d 100644 --- a/src/core/ultrawide_debug.cpp +++ b/src/core/ultrawide_debug.cpp @@ -723,10 +723,19 @@ std::string CurrentGuestFunctionStack() { } // namespace +// Live value of the Skate 3 presence context (0x8001): 1 = in gameplay, +// 0 = frontend / pause menu / loading. Consumers (the native scene renderer) +// yield to the emulated output while the game is in menus. +static std::atomic g_skate3_gameplay_context_value{0}; + void NotifySkate3GameplayContext(uint32_t user_index, uint32_t context_id, uint32_t value) { constexpr uint32_t kSkate3GameplayContext = 0x8001; constexpr uint32_t kSkate3GameplayValue = 1; - if (user_index != 0 || context_id != kSkate3GameplayContext || value != kSkate3GameplayValue) { + if (user_index != 0 || context_id != kSkate3GameplayContext) { + return; + } + g_skate3_gameplay_context_value.store(value, std::memory_order_relaxed); + if (value != kSkate3GameplayValue) { return; } @@ -740,6 +749,10 @@ bool IsSkate3GameplayUltrawideActive() { return g_skate3_gameplay_ultrawide_latched.load(std::memory_order_relaxed); } +uint32_t Skate3GameplayContextValue() { + return g_skate3_gameplay_context_value.load(std::memory_order_relaxed); +} + GuestFunctionScope::GuestFunctionScope(const char* function_name) { if (!IsGuestStoreWatchActive()) { return; diff --git a/src/graphics/d3d12/command_processor.cpp b/src/graphics/d3d12/command_processor.cpp index 0917a3e..8530fab 100644 --- a/src/graphics/d3d12/command_processor.cpp +++ b/src/graphics/d3d12/command_processor.cpp @@ -2770,6 +2770,13 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3 PROFILE_DRAW_FINGERPRINT(fingerprint); } #endif + if (ShouldSuppressEmulatedDraws()) { + // Native output active: skip the resolve. Suppressed draws leave only + // garbage in the EDRAM buffer; copying it out would overwrite guest + // texture payloads (e.g. previously composed CAS textures) that the + // native renderer still samples. + return true; + } return IssueCopy(); } @@ -2816,6 +2823,16 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3 bool memexport_used_pixel = pixel_shader && (pixel_shader->memexport_eM_written() != 0); bool memexport_used = memexport_used_vertex || memexport_used_pixel; + // Native guest-output renderer active: the emulated frame is never shown, + // so skip the draw entirely (pipeline setup, texture cache, render target + // cache, GPU work). Memexport draws still execute; the game reads their + // results back from memory. Fences, queries and PM4 processing are + // unaffected. Menus/pause flip the native renderer inactive, so emulated + // rendering (and CAS texture composition) works normally there. + if (!memexport_used && ShouldSuppressEmulatedDraws()) { + return true; + } + if (!BeginSubmission(true)) { return false; } diff --git a/src/graphics/native_guest_renderer.cpp b/src/graphics/native_guest_renderer.cpp index 04d0a98..f1bc3b8 100644 --- a/src/graphics/native_guest_renderer.cpp +++ b/src/graphics/native_guest_renderer.cpp @@ -2,11 +2,26 @@ #include +#include + +// Defined here (shared TU) rather than in a backend's command_processor.cpp +// so both D3D12 and Vulkan see the same definition. +REXCVAR_DEFINE_BOOL(native_render_suppress_emulated_draws, false, "GPU", + "While the registered native guest-output renderer is actively " + "replacing frames, skip emulated draw and resolve execution in the " + "command processor (PM4 parsing, fences, queries and memexport draws " + "still run). Menus/pause yield to the emulated path and execute " + "normally.") + .lifecycle(rex::cvar::Lifecycle::kHotReload); + namespace rex::graphics { namespace { std::atomic g_renderer{nullptr}; std::atomic g_renderer_user_data{nullptr}; +// True while the registered renderer actually replaced the last presented +// frame (false when it yields: menus, early-outs, no renderer). +std::atomic g_native_output_active{false}; } // namespace @@ -18,10 +33,22 @@ void SetNativeGuestOutputRenderer(NativeGuestOutputRenderer renderer, void* user bool TryRenderNativeGuestOutput(const NativeGuestOutputRenderContext& context) { NativeGuestOutputRenderer renderer = g_renderer.load(std::memory_order_acquire); if (renderer == nullptr) { + g_native_output_active.store(false, std::memory_order_relaxed); return false; } void* user_data = g_renderer_user_data.load(std::memory_order_acquire); - return renderer(context, user_data); + const bool rendered = renderer(context, user_data); + g_native_output_active.store(rendered, std::memory_order_relaxed); + return rendered; +} + +bool IsNativeGuestOutputActive() { + return g_native_output_active.load(std::memory_order_relaxed); +} + +bool ShouldSuppressEmulatedDraws() { + return REXCVAR_GET(native_render_suppress_emulated_draws) && + g_native_output_active.load(std::memory_order_relaxed); } } // namespace rex::graphics diff --git a/src/kernel/xam/apps/xgi_app.cpp b/src/kernel/xam/apps/xgi_app.cpp index a1ca1c1..2bde125 100644 --- a/src/kernel/xam/apps/xgi_app.cpp +++ b/src/kernel/xam/apps/xgi_app.cpp @@ -10,6 +10,9 @@ */ #include + +#include + #include #include #include @@ -50,12 +53,26 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr, context_value); if (rex::cvar::Query("skate3_demo_path") && user_index == 0 && context_id == 0x8001) { + // Boot automation is strictly ONE-SHOT: the dialog A auto-tap exists + // only to accept boot-flow dialogs (autosave prompt etc.) between the + // press-start screen and gameplay. The game also sets this context to + // 0 for the PAUSE MENU and loading screens; re-arming the auto-tap + // there blindly "confirmed" pause menu entries and teleported the + // player across the map. Once gameplay (context 1) has been seen, + // never inject input again for the rest of the session. + static std::atomic boot_automation_done{false}; if (context_value == 0) { - rex::kernel::xam::SetSyntheticAutoTap(rex::input::X_INPUT_GAMEPAD_A, true); - REXKRNL_INFO("Skate 3 demo path: gameplay context 0; enabling dialog A auto-tap"); + if (!boot_automation_done.load(std::memory_order_relaxed)) { + rex::kernel::xam::SetSyntheticAutoTap(rex::input::X_INPUT_GAMEPAD_A, true); + REXKRNL_INFO("Skate 3 demo path: gameplay context 0; enabling dialog A auto-tap"); + } } else if (context_value == 1) { rex::kernel::xam::ClearSyntheticInput(); - REXKRNL_INFO("Skate 3 demo path: gameplay context 1; stopping synthetic input"); + if (!boot_automation_done.exchange(true, std::memory_order_relaxed)) { + REXKRNL_INFO( + "Skate 3 demo path: gameplay reached; boot automation complete, synthetic " + "input disabled for the session"); + } } } if (!old_value || *old_value != context_value) {