diff --git a/include/rex/graphics/flags.h b/include/rex/graphics/flags.h index 912050d..583a474 100644 --- a/include/rex/graphics/flags.h +++ b/include/rex/graphics/flags.h @@ -37,6 +37,7 @@ REXCVAR_DECLARE(std::string, readback_resolve); REXCVAR_DECLARE(bool, readback_resolve_half_pixel_offset); REXCVAR_DECLARE(int32_t, native_render_force_resolve_readback_max_length); REXCVAR_DECLARE(int32_t, native_render_force_resolve_readback_min_length); +REXCVAR_DECLARE(int32_t, native_render_targeted_readback_min_interval_ms); REXCVAR_DECLARE(bool, readback_memexport); REXCVAR_DECLARE(bool, readback_memexport_fast); REXCVAR_DECLARE(bool, occlusion_query_enable); diff --git a/src/graphics/command_processor.cpp b/src/graphics/command_processor.cpp index 96da205..8c40a62 100644 --- a/src/graphics/command_processor.cpp +++ b/src/graphics/command_processor.cpp @@ -101,6 +101,19 @@ REXCVAR_DEFINE_INT32(native_render_force_resolve_readback_max_length, 0, "GPU", .range(0, 16 * 1024 * 1024) .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_INT32(native_render_targeted_readback_min_interval_ms, 33, "GPU", + "Minimum interval between the targeted always-on resolve readbacks " + "(the non-gameplay Import-Skater/preview target) in milliseconds. " + "Each readback is a synchronous GPU drain plus a multi-megabyte CPU " + "copy; menu flows that re-resolve the preview target every frame " + "(trick guide demo pages) otherwise saturate the command processor " + "with hundreds of drains per second. Skipped copies leave the " + "previous CPU-visible contents in place (the consumer is a preview " + "image). 0 disables the throttle. Does not affect the app-armed " + "photo-grab window.") + .range(0, 1000) + .lifecycle(rex::cvar::Lifecycle::kHotReload); + REXCVAR_DEFINE_INT32(native_render_force_resolve_readback_min_length, 0, "GPU", "Lower bound companion to " "native_render_force_resolve_readback_max_length: when > 0, resolves " diff --git a/src/graphics/d3d12/command_processor.cpp b/src/graphics/d3d12/command_processor.cpp index 41ce645..aef9c03 100644 --- a/src/graphics/d3d12/command_processor.cpp +++ b/src/graphics/d3d12/command_processor.cpp @@ -3616,10 +3616,30 @@ bool D3D12CommandProcessor::IssueCopy_ReadbackResolvePath() { if (readback_mode == ReadbackResolveMode::kDisabled) { constexpr uint32_t kImportSkaterPreviewResolveAddress = UINT32_C(0x04911000); constexpr uint32_t kImportSkaterPreviewResolveLength = UINT32_C(0x2D0000); - const bool force_scaled_resolve_cpu_copy = + bool force_scaled_resolve_cpu_copy = !IsGameplayStateActive(kernel_state_) && is_scaled && written_address == kImportSkaterPreviewResolveAddress && written_length == kImportSkaterPreviewResolveLength; + // Rate-limit the targeted copies: each one is a synchronous GPU drain + // plus a multi-megabyte CPU copy, and menu flows that re-resolve the + // preview target every frame (trick guide demo pages) issue hundreds + // per second. A skipped copy leaves the previous CPU-visible contents + // in place, which a preview consumer tolerates. The app-armed + // photo-grab window below is deliberately NOT throttled (the display + // card composes from exact frames). Mirrors the Vulkan gate. + if (force_scaled_resolve_cpu_copy) { + const int32_t min_interval_ms = + REXCVAR_GET(native_render_targeted_readback_min_interval_ms); + if (min_interval_ms > 0) { + static uint64_t s_last_targeted_copy_ms = 0; + const uint64_t now_ms = rex::chrono::Clock::QueryHostUptimeMillis(); + if (now_ms - s_last_targeted_copy_ms < uint64_t(min_interval_ms)) { + force_scaled_resolve_cpu_copy = false; + } else { + s_last_targeted_copy_ms = now_ms; + } + } + } // App-armed readback window (Skate 3 photo grab): the game CPU-reads the // resolved 1152x640 PostFX screenshot target (also 0x04911000) while the // gameplay presence context is still 1 (photo missions), so the targeted diff --git a/src/graphics/vulkan/command_processor.cpp b/src/graphics/vulkan/command_processor.cpp index 753d9cf..d326281 100644 --- a/src/graphics/vulkan/command_processor.cpp +++ b/src/graphics/vulkan/command_processor.cpp @@ -5824,6 +5824,27 @@ bool VulkanCommandProcessor::IssueCopy_ReadbackResolvePath() { !IsGameplayStateActive(kernel_state_) && is_scaled && written_address == kImportSkaterPreviewResolveAddress && written_length == kImportSkaterPreviewResolveLength; + // Rate-limit the targeted copies: each one is a synchronous GPU drain + // plus a multi-megabyte CPU copy, and menu flows that re-resolve the + // preview target every frame (trick guide demo pages) issue hundreds + // per second, enough to halve the frame rate on their own. A skipped + // copy leaves the previous CPU-visible contents in place, which a + // preview consumer tolerates. The app-armed photo-grab window below is + // deliberately NOT throttled (the display card composes from exact + // frames). + if (force_scaled_resolve_cpu_copy) { + const int32_t min_interval_ms = + REXCVAR_GET(native_render_targeted_readback_min_interval_ms); + if (min_interval_ms > 0) { + static uint64_t s_last_targeted_copy_ms = 0; + const uint64_t now_ms = rex::chrono::Clock::QueryHostUptimeMillis(); + if (now_ms - s_last_targeted_copy_ms < uint64_t(min_interval_ms)) { + force_scaled_resolve_cpu_copy = false; + } else { + s_last_targeted_copy_ms = now_ms; + } + } + } // App-armed readback window (Skate 3 photo grab): the game CPU-reads the // resolved 1152x640 PostFX screenshot target (also 0x04911000) while the // gameplay presence context is still 1 (photo missions), so the targeted