Skate 3: gameplay-context plumbing, one-shot boot auto-tap, emulated draw suppression

This commit is contained in:
mchughalex 2026-07-02 22:50:13 +01:00
parent c4b0dfe2c9
commit c09b8c892d
6 changed files with 90 additions and 5 deletions

View file

@ -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

View file

@ -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);

View file

@ -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<uint32_t> 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;

View file

@ -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;
}

View file

@ -2,11 +2,26 @@
#include <atomic>
#include <rex/cvar.h>
// 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<NativeGuestOutputRenderer> g_renderer{nullptr};
std::atomic<void*> 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<bool> 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

View file

@ -10,6 +10,9 @@
*/
#include <rex/kernel/xam/apps/xgi_app.h>
#include <atomic>
#include <rex/cvar.h>
#include <rex/graphics/ultrawide_debug.h>
#include <rex/input/input.h>
@ -50,12 +53,26 @@ X_HRESULT XgiApp::DispatchMessageSync(uint32_t message, uint32_t buffer_ptr,
context_value);
if (rex::cvar::Query<bool>("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<bool> 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) {