graphics: rate-limit the targeted preview-resolve readbacks

This commit is contained in:
mchughalex 2026-07-19 18:57:28 +01:00
parent b4d018c405
commit f8cbe94367
4 changed files with 56 additions and 1 deletions

View file

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

View file

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

View file

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

View file

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