shadows: raster-texel blur steps + auto tile sizing + F11 atlas dump (SDK bump)

This commit is contained in:
mchughalex 2026-07-17 06:35:27 +01:00
parent f9d0ac9adf
commit 5dce670fb3
2 changed files with 135 additions and 21 deletions

View file

@ -50,6 +50,8 @@
#endif #endif
#endif #endif
REXCVAR_DECLARE(std::string, skate3_native_render_snapshot_dir);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene, false, "Skate 3", REXCVAR_DEFINE_BOOL(skate3_native_render_scene, false, "Skate 3",
"Render the game scene natively from the hooked MeshContext stream, " "Render the game scene natively from the hooked MeshContext stream, "
"replacing the emulated GPU output (requires skate3_native_render). " "replacing the emulated GPU output (requires skate3_native_render). "
@ -381,14 +383,17 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_char_rows_inst, true, "Skate 3",
"legacy opaque path on capture-failed frames (a frame-to-frame " "legacy opaque path on capture-failed frames (a frame-to-frame "
"flicker whose rate scales with fps).") "flicker whose rate scales with fps).")
.lifecycle(rex::cvar::Lifecycle::kHotReload); .lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_INT32(skate3_native_render_scene_shadow_tile, 1024, "Skate 3", REXCVAR_DEFINE_INT32(skate3_native_render_scene_shadow_tile, 0, "Skate 3",
"Shadow cascade tile resolution. The game's own atlas is " "Shadow cascade tile resolution. The game's own atlas is "
"three 512 tiles; the blur offsets scale with this value so " "three 512 tiles; the blur steps raster texels (the game's "
"the penumbra WIDTH stays console-equal at any resolution " "tfetch offsets, which the emulated GPU applies in scaled "
"(like the emulated GPU: scaled raster, logical-texel blur). " "texels), so higher values sharpen both the silhouette and "
"1024 = fine silhouettes at the game's softness; 512 = " "the penumbra like the emulated baseline. 512 = the softer "
"console-exact but visibly blocky edges up close.") "original-console look, blocky up close. 0 = auto: 512 x "
.range(256, 4096) "the render resolution scale (the Resolution Scale "
"setting), matching the emulated renderer's shadow "
"crispness at any render resolution.")
.range(0, 4096)
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart); .lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_shadow_caster_parity, true, REXCVAR_DEFINE_BOOL(skate3_native_render_scene_shadow_caster_parity, true,
"Skate 3", "Skate 3",
@ -10217,6 +10222,19 @@ struct RendererState {
// After a shadow pass all three textures sit in PIXEL_SHADER_RESOURCE; // After a shadow pass all three textures sit in PIXEL_SHADER_RESOURCE;
// the next pass transitions them back to RENDER_TARGET first. // the next pass transitions them back to RENDER_TARGET first.
bool shadow_in_srv_state = false; bool shadow_in_srv_state = false;
// Shadow-atlas readback, taken on kShadowDumpFrames CONSECUTIVE frames
// once per F11 recording session: the raw and blurred planes (R16G16)
// land in the snapshot dir for offline analysis; the guest's own atlas
// resolve never reaches CPU memory, so this is the only view of what
// receivers actually sample. Consecutive frames make temporal-stability
// diffs possible (silhouette buzz vs animation).
static constexpr uint32_t kShadowDumpFrames = 4;
nrhi::Buffer* shadow_dump_buf[kShadowDumpFrames] = {};
uint8_t* shadow_dump_ptr[kShadowDumpFrames] = {};
uint64_t shadow_dump_submission[kShadowDumpFrames] = {};
uint32_t shadow_dump_enqueued = 0;
uint32_t shadow_dump_written = 0;
bool shadow_dump_done = false;
// Per-frame shadow constant buffer ring (CBV b1). // Per-frame shadow constant buffer ring (CBV b1).
static constexpr uint32_t kShadowCbRegions = 8; static constexpr uint32_t kShadowCbRegions = 8;
nrhi::Buffer* shadow_cb = nullptr; nrhi::Buffer* shadow_cb = nullptr;
@ -13150,7 +13168,22 @@ bool EnsureShadowResources(const NativeGuestOutputRenderContext& context) {
// half-float ulp at the typical ~0.85 depth is ~6 mm of world height, // half-float ulp at the typical ~0.85 depth is ~6 mm of world height,
// too coarse for board/feet-height casters 1-2 cm off the ground), // too coarse for board/feet-height casters 1-2 cm off the ground),
// 3 tiles of tile x tile each. // 3 tiles of tile x tile each.
g_r.shadow_tile = uint32_t(REXCVAR_GET(skate3_native_render_scene_shadow_tile)); const int32_t tile_cfg = REXCVAR_GET(skate3_native_render_scene_shadow_tile);
if (tile_cfg > 0) {
g_r.shadow_tile = uint32_t(tile_cfg);
} else {
// Auto: the game's 512 tiles at the RENDER resolution scale.
// guest_output is the scaled frontbuffer (720p x the draw resolution
// scale, independent of window/monitor size), so deriving from its
// height follows the Resolution Scale setting, including any
// device-limit clamping the texture cache applied, and gives the
// same effective shadow raster the emulated GPU renders at that
// scale. Resolved once at atlas creation (the cvar is
// restart-scoped).
const uint32_t scale =
std::max(1u, (context.guest_output_height + 719u) / 720u);
g_r.shadow_tile = std::min(512u * scale, 4096u);
}
nrhi::TextureDesc desc; nrhi::TextureDesc desc;
desc.width = g_r.shadow_tile * 3; desc.width = g_r.shadow_tile * 3;
desc.height = g_r.shadow_tile; desc.height = g_r.shadow_tile;
@ -13180,8 +13213,9 @@ bool EnsureShadowResources(const NativeGuestOutputRenderContext& context) {
} }
} }
g_r.shadow_in_srv_state = false; g_r.shadow_in_srv_state = false;
REXLOG_INFO("native-scene: shadow atlas created ({}x{} tiles)", g_r.shadow_tile, REXLOG_INFO("native-scene: shadow atlas created ({}x{} tiles{})",
g_r.shadow_tile); g_r.shadow_tile, g_r.shadow_tile,
tile_cfg > 0 ? "" : ", auto");
} }
if (!g_r.shadow_cb) { if (!g_r.shadow_cb) {
// Always created (even with shadows off): the scene PS declares b1 and // Always created (even with shadows off): the scene PS declares b1 and
@ -15467,6 +15501,41 @@ bool RenderShadowAtlas(const NativeGuestOutputRenderContext& context,
const float* sh = scene.shadow_rows; const float* sh = scene.shadow_rows;
bool shadow_ready = false; bool shadow_ready = false;
uint32_t shadow_draws = 0; uint32_t shadow_draws = 0;
// Atlas-readback bookkeeping: write completed dumps to disk in frame
// order; re-arm once the recording session that requested them ends.
if (!g_recording.load(std::memory_order_relaxed) &&
g_r.shadow_dump_written == g_r.shadow_dump_enqueued) {
g_r.shadow_dump_done = false;
g_r.shadow_dump_enqueued = 0;
g_r.shadow_dump_written = 0;
}
while (g_r.shadow_dump_written < g_r.shadow_dump_enqueued &&
context.device->CompletedSubmission() >
g_r.shadow_dump_submission[g_r.shadow_dump_written]) {
const uint32_t k = g_r.shadow_dump_written;
const uint32_t tile_px = g_r.shadow_tile;
const uint32_t pitch = tile_px * 3u * 4u;
const uint64_t plane = uint64_t(pitch) * tile_px;
context.device->InvalidateForRead(g_r.shadow_dump_buf[k], 0, plane * 2);
std::string dir = REXCVAR_GET(skate3_native_render_snapshot_dir);
if (dir.empty()) {
dir = "native_render_snapshots";
}
std::error_code ec;
std::filesystem::create_directories(dir, ec);
const std::filesystem::path path =
std::filesystem::path(dir) /
("shadow_atlas_" + std::to_string(uint64_t(std::time(nullptr))) + "_f" +
std::to_string(k) + "_" + std::to_string(tile_px * 3) + "x" +
std::to_string(tile_px) + "_rg16.bin");
std::ofstream out(path, std::ios::binary);
out.write(reinterpret_cast<const char*>(g_r.shadow_dump_ptr[k]),
std::streamsize(plane * 2));
REXLOG_INFO(
"native-scene: shadow atlas frame {} dumped (raw+blurred planes) -> {}",
k, path.string());
++g_r.shadow_dump_written;
}
if (REXCVAR_GET(skate3_native_render_scene_shadows) && scene.shadow_valid && if (REXCVAR_GET(skate3_native_render_scene_shadows) && scene.shadow_valid &&
g_r.shadow_raw != nullptr && g_r.pso_shadow_caster != nullptr && g_r.shadow_raw != nullptr && g_r.pso_shadow_caster != nullptr &&
g_r.pso_shadow_blur != nullptr && debug_mode == 0) { g_r.pso_shadow_blur != nullptr && debug_mode == 0) {
@ -15642,16 +15711,15 @@ bool RenderShadowAtlas(const NativeGuestOutputRenderContext& context,
float(tile), 0.0f, 1.0f}); float(tile), 0.0f, 1.0f});
cmd->SetScissor(nrhi::Rect{int32_t(tile) * ci, 0, cmd->SetScissor(nrhi::Rect{int32_t(tile) * ci, 0,
int32_t(tile) * (ci + 1), int32_t(tile)}); int32_t(tile) * (ci + 1), int32_t(tile)});
// Blur tap offsets in GAME-map texels (the game's atlas tile is 512; // Blur tap offsets step RASTER texels, matching the emulated GPU:
// its blur shader steps whole logical texels). At higher native tile // the game's blur PS taps via tfetch integer offsets (OffsetX/Y
// resolutions the step scales up so the blur's WORLD reach, the // -2..+2), which a resolution-scaled
// penumbra width every receiver sees, stays console-equal: 512 tiles // raster applies in physical texels, so the penumbra NARROWS as the
// rendered the exact width but visibly blocky silhouettes; // tile grows (the emulated baseline is sharper than original
// 1024 with unscaled 1-texel steps // hardware; 512 reproduces the softer original-360 look). Scaling
// halved the game's softness. Matches the emulated GPU, which // the step by tile/512 to hold a console-width penumbra was tried
// resolution-scales the raster while the blur offsets stay logical. // and made receivers visibly blurrier than the emulated reference.
const float step = std::max(1.0f, float(tile) / 512.0f); const float bc[12] = {horizontal ? 1.0f : 0.0f, horizontal ? 0.0f : 1.0f,
const float bc[12] = {horizontal ? step : 0.0f, horizontal ? 0.0f : step,
ntaps[ci], src_raw ? 1.0f : 0.0f, ntaps[ci], src_raw ? 1.0f : 0.0f,
kernels[ci][0], kernels[ci][1], kernels[ci][2], 0.0f, kernels[ci][0], kernels[ci][1], kernels[ci][2], 0.0f,
float(tile) * ci, float(tile) * (ci + 1) - 1.0f, float(tile) * ci, float(tile) * (ci + 1) - 1.0f,
@ -15674,6 +15742,52 @@ bool RenderShadowAtlas(const NativeGuestOutputRenderContext& context,
cmd->FlushBarriers(); cmd->FlushBarriers();
g_r.shadow_in_srv_state = true; g_r.shadow_in_srv_state = true;
shadow_ready = shadow_draws > 0; shadow_ready = shadow_draws > 0;
// Atlas readback on kShadowDumpFrames consecutive frames per
// recording session (see the completion handler at function entry):
// per frame, raw plane at offset 0, blurred plane after it. Each
// frame gets its own readback buffer so no frame waits on a copy.
if (g_recording.load(std::memory_order_relaxed) &&
!g_r.shadow_dump_done &&
g_r.shadow_dump_enqueued < RendererState::kShadowDumpFrames) {
const uint32_t k = g_r.shadow_dump_enqueued;
const uint32_t pitch = tile * 3u * 4u;
const uint64_t plane = uint64_t(pitch) * tile;
if (g_r.shadow_dump_buf[k] == nullptr) {
nrhi::BufferDesc desc;
desc.size = plane * 2;
desc.heap = nrhi::HeapKind::kReadback;
g_r.shadow_dump_buf[k] = context.device->CreateBuffer(desc);
g_r.shadow_dump_ptr[k] =
g_r.shadow_dump_buf[k] != nullptr
? static_cast<uint8_t*>(
context.device->Map(g_r.shadow_dump_buf[k]))
: nullptr;
}
if (g_r.shadow_dump_ptr[k] != nullptr) {
cmd->Barrier(g_r.shadow_raw, nrhi::ResourceState::kPixelShaderResource,
nrhi::ResourceState::kCopySource);
cmd->Barrier(g_r.shadow_final,
nrhi::ResourceState::kPixelShaderResource,
nrhi::ResourceState::kCopySource);
cmd->FlushBarriers();
cmd->CopyTextureToBuffer(g_r.shadow_dump_buf[k], 0, pitch,
g_r.shadow_raw, 0, tile * 3, tile);
cmd->CopyTextureToBuffer(g_r.shadow_dump_buf[k], plane, pitch,
g_r.shadow_final, 0, tile * 3, tile);
cmd->Barrier(g_r.shadow_raw, nrhi::ResourceState::kCopySource,
nrhi::ResourceState::kPixelShaderResource);
cmd->Barrier(g_r.shadow_final, nrhi::ResourceState::kCopySource,
nrhi::ResourceState::kPixelShaderResource);
cmd->FlushBarriers();
g_r.shadow_dump_submission[k] = context.device->CurrentSubmission();
++g_r.shadow_dump_enqueued;
if (g_r.shadow_dump_enqueued == RendererState::kShadowDumpFrames) {
g_r.shadow_dump_done = true;
}
} else {
g_r.shadow_dump_done = true; // allocation failed; don't retry
}
}
} }
} }
*out_draws = shadow_draws; *out_draws = shadow_draws;

@ -1 +1 @@
Subproject commit 02e6166402c58aaacbc292446cc9e30d929bbb40 Subproject commit 0084c845427c3cac620002c836076ac0581ff2d7