From cb14799e47af86fcbd1fcf7e6895f560a449388f Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:50:32 +0100 Subject: [PATCH] render: apply MSAA and shadow map resolution changes live --- src/skate3_native_scene.cpp | 14 +-- src/skate3_native_scene_gpu.cpp | 131 ++++++++++++++++++------- src/skate3_native_scene_gpu_internal.h | 3 + 3 files changed, 108 insertions(+), 40 deletions(-) diff --git a/src/skate3_native_scene.cpp b/src/skate3_native_scene.cpp index 5b2aea9..3ca1181 100644 --- a/src/skate3_native_scene.cpp +++ b/src/skate3_native_scene.cpp @@ -86,9 +86,10 @@ REXCVAR_DEFINE_INT32(skate3_native_render_scene_debug, 0, "Skate 3", REXCVAR_DEFINE_INT32(skate3_native_render_scene_msaa, 4, "Skate 3", "MSAA sample count for the native scene (1 = off, 2/4/8). Distant " "thin geometry (railings, wires) shimmers without it; mipmaps only " - "fix texture aliasing.") + "fix texture aliasing. Applies live: the scene pipeline family and " + "MSAA targets rebuild on change.") .range(1, 8) - .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); + .lifecycle(rex::cvar::Lifecycle::kHotReload); REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_world_v2_tan_sign, 1.0, "Skate 3", "World-shading v2: polarity relating the stored frame's " "cross(binormal, normal) x handedness to the game's tangent " @@ -650,9 +651,10 @@ REXCVAR_DEFINE_INT32(skate3_native_render_scene_shadow_tile, 0, "Skate 3", "original-console look, blocky up close. 0 = auto: 512 x " "the render resolution scale (the Resolution Scale " "setting), matching the emulated renderer's shadow " - "crispness at any render resolution.") + "crispness at any render resolution. Applies live: the " + "atlas chain recreates on change.") .range(0, 4096) - .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); + .lifecycle(rex::cvar::Lifecycle::kHotReload); REXCVAR_DEFINE_BOOL(skate3_native_render_scene_shadow_caster_parity, true, "Skate 3", "Character pieces cast dynamic shadows only when the game " @@ -696,9 +698,9 @@ REXCVAR_DEFINE_INT32(skate3_native_render_scene_shadow_static_size, 4096, "Static sun-shadow map resolution per cascade tile " "(the map is three tiles). At the default radius the " "inner cascade gets ~1.5 cm texels, mid ~4 cm, far " - "~9 cm.") + "~9 cm. Applies live: the map recreates on change.") .range(1024, 8192) - .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); + .lifecycle(rex::cvar::Lifecycle::kHotReload); REXCVAR_DEFINE_BOOL(skate3_native_render_scene_shadow_pcss, true, "Skate 3", "Contact-hardening soft shadows (PCSS): a blocker search " "estimates the caster distance per pixel and the filter " diff --git a/src/skate3_native_scene_gpu.cpp b/src/skate3_native_scene_gpu.cpp index 1dce58b..3561e46 100644 --- a/src/skate3_native_scene_gpu.cpp +++ b/src/skate3_native_scene_gpu.cpp @@ -2420,9 +2420,9 @@ bool EnsureRootSignature(const NativeGuestOutputRenderContext& context) { return true; } -// MSAA level selection + the main scene PSO and its culling/blend/depth -// variants (cull-back sheets, transparent, entity fade, hair 2-pass, -// no-depth, outline mask). +// The main scene PSO and its culling/blend/depth variants (cull-back +// sheets, transparent, entity fade, hair 2-pass, no-depth, outline mask), +// built at the g_r.msaa sample count EnsurePipeline selected. bool EnsureScenePsoFamily(const NativeGuestOutputRenderContext& context) { nrhi::Device* device = context.device; // Scene color format: the float HDR intermediate when the HDR post chain @@ -2431,16 +2431,10 @@ bool EnsureScenePsoFamily(const NativeGuestOutputRenderContext& context) { const nrhi::Format scene_fmt = g_r.hdr_active ? g_r.hdr_scene_format : context.guest_output->format(); - // MSAA level: the requested count, reduced to what the scene color format - // supports (1 disables and renders directly into the 1x target). The - // RHI walks the requested count down in powers of two, exactly like the - // old CheckFeatureSupport loop. - const int32_t req = REXCVAR_GET(skate3_native_render_scene_msaa); - uint32_t msaa = req >= 8 ? 8u : req >= 4 ? 4u : req >= 2 ? 2u : 1u; - msaa = device->GetSupportedSampleCount(scene_fmt, msaa); - g_r.msaa = msaa; + // g_r.msaa is selected by EnsurePipeline (which triggers this rebuild on + // change). - // The family rebuilds on HDR toggles / output-format changes: retire the + // The family rebuilds on HDR/MSAA toggles / output-format changes: retire the // previous pipelines (in-flight frames keep them alive via the deferred // destruction queue). for (nrhi::Pipeline** p : @@ -3189,6 +3183,38 @@ bool EnsurePhotoFxPipeline(const NativeGuestOutputRenderContext& context) { // Shadow atlas targets + the always-bound b1 receiver constant buffer. bool EnsureShadowResources(const NativeGuestOutputRenderContext& context) { nrhi::Device* device = context.device; + // Cascade tile size: explicit, or 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. + const int32_t tile_cfg = REXCVAR_GET(skate3_native_render_scene_shadow_tile); + const uint32_t want_tile = + tile_cfg > 0 + ? uint32_t(tile_cfg) + : std::min(512u * std::max(1u, (context.guest_output_height + 719u) / + 720u), + 4096u); + if (g_r.shadow_raw != nullptr && g_r.shadow_tile != want_tile) { + // Hot tile-size change: retire the atlas chain; recreated below. The + // new atlas starts in RENDER_TARGET state and is re-rendered by this + // frame's shadow pass. + nrhi::Texture** targets[3] = {&g_r.shadow_raw, &g_r.shadow_mid, + &g_r.shadow_final}; + nrhi::TextureView** views[3] = {&g_r.shadow_srv_raw, &g_r.shadow_srv_mid, + &g_r.shadow_srv_final}; + for (int t = 0; t < 3; ++t) { + if (*views[t] != nullptr) { + device->DestroyDeferred(*views[t]); + *views[t] = nullptr; + } + if (*targets[t] != nullptr) { + device->DestroyDeferred(*targets[t]); + *targets[t] = nullptr; + } + } + } if (!g_r.shadow_raw && REXCVAR_GET(skate3_native_render_scene_shadows)) { // Dynamic-shadow atlas chain: raw casters -> hblur intermediate -> // blurred final (the texture the scene pass samples). Three fixed-size @@ -3196,22 +3222,7 @@ bool EnsureShadowResources(const NativeGuestOutputRenderContext& context) { // 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), // 3 tiles of tile x tile each. - 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); - } + g_r.shadow_tile = want_tile; nrhi::TextureDesc desc; desc.width = g_r.shadow_tile * 3; desc.height = g_r.shadow_tile; @@ -3271,15 +3282,27 @@ bool EnsureShadowResources(const NativeGuestOutputRenderContext& context) { g_r.world_shadow_in_srv = false; g_r.world_shadow_primed = false; } + const uint32_t want_static_size = uint32_t(std::clamp( + REXCVAR_GET(skate3_native_render_scene_shadow_static_size), 1024, 8192)); + if (g_r.static_sun != nullptr && g_r.static_sun_size != want_static_size) { + // Hot size change: retire the map; recreated below. The recreated map + // is uninitialized, so force the cross-frame cache to re-render it + // (RenderStaticSunMap rebuilds whenever nsm_built_radius <= 0). + if (g_r.static_sun_srv != nullptr) { + device->DestroyDeferred(g_r.static_sun_srv); + g_r.static_sun_srv = nullptr; + } + device->DestroyDeferred(g_r.static_sun); + g_r.static_sun = nullptr; + g_r.nsm_built_radius = 0.0f; + } if (!g_r.static_sun && g_r.shadow_raw != nullptr && REXCVAR_GET(skate3_native_render_scene_shadow_static_casters)) { // Native static sun-shadow map (see RendererState). THREE cascade // tiles side by side: inner (r/6, centimeter contact detail with // useful reach), mid (r/2) and far (full radius, large-caster - // coverage); size is per tile, restart-scoped. - const int32_t size_cfg = - REXCVAR_GET(skate3_native_render_scene_shadow_static_size); - g_r.static_sun_size = uint32_t(std::clamp(size_cfg, 1024, 8192)); + // coverage); size is per tile. + g_r.static_sun_size = want_static_size; nrhi::TextureDesc desc; desc.width = g_r.static_sun_size * 3; desc.height = g_r.static_sun_size; @@ -3545,7 +3568,8 @@ bool EnsureOutputSizedTargets(const NativeGuestOutputRenderContext& context) { g_r.hdr_active ? g_r.hdr_scene_format : nrhi::Format::kUnknown; if (!g_r.depth || g_r.depth_width != width || g_r.depth_height != height || g_r.targets_hdr != g_r.hdr_active || - g_r.targets_scene_fmt != want_scene_fmt) { + g_r.targets_scene_fmt != want_scene_fmt || + g_r.targets_msaa != g_r.msaa) { if (g_r.depth) { g_r.device->DestroyDeferred(g_r.depth); g_r.depth = nullptr; @@ -3615,6 +3639,18 @@ bool EnsureOutputSizedTargets(const NativeGuestOutputRenderContext& context) { vd.dimension = nrhi::ViewDimension::k2DMS; g_r.msaa_srv_slot = device->CreateTextureView(g_r.msaa_color, vd); g_r.msaa_srv_allocated = g_r.msaa_srv_slot != nullptr; + } else { + // MSAA switched off: retire the multisample color target and its + // resolve view (the scene renders directly into the 1x target). + if (g_r.msaa_srv_slot) { + g_r.device->DestroyDeferred(g_r.msaa_srv_slot); + g_r.msaa_srv_slot = nullptr; + g_r.msaa_srv_allocated = false; + } + if (g_r.msaa_color) { + g_r.device->DestroyDeferred(g_r.msaa_color); + g_r.msaa_color = nullptr; + } } // 1x float scene plane for the HDR post chain: the MSAA resolve @@ -3654,6 +3690,7 @@ bool EnsureOutputSizedTargets(const NativeGuestOutputRenderContext& context) { } g_r.targets_hdr = g_r.hdr_active; g_r.targets_scene_fmt = want_scene_fmt; + g_r.targets_msaa = g_r.msaa; } return true; } @@ -3677,11 +3714,37 @@ bool EnsurePipeline(const NativeGuestOutputRenderContext& context) { REXCVAR_GET(skate3_native_render_scene_hdr_packed) ? nrhi::Format::kR11G11B10_FLOAT : nrhi::Format::kR16G16B16A16_FLOAT; + // MSAA level: the requested count, reduced to what the scene color format + // supports (1 disables and renders directly into the 1x target). The RHI + // walks the requested count down in powers of two, exactly like the old + // CheckFeatureSupport loop. Hot: a change rebuilds the pipeline family + // (the AO/SSR/volumetric passes and the MSAA targets latch on g_r.msaa + // and follow). + const nrhi::Format scene_fmt_want = + hdr_want ? hdr_fmt_want : context.guest_output->format(); + const int32_t msaa_req = REXCVAR_GET(skate3_native_render_scene_msaa); + uint32_t msaa_want = + msaa_req >= 8 ? 8u : msaa_req >= 4 ? 4u : msaa_req >= 2 ? 2u : 1u; + msaa_want = device->GetSupportedSampleCount(scene_fmt_want, msaa_want); if (!g_r.pso || g_r.rtv_format != context.guest_output->format() || g_r.hdr_active != hdr_want || - (hdr_want && g_r.hdr_scene_format != hdr_fmt_want)) { + (hdr_want && g_r.hdr_scene_format != hdr_fmt_want) || + g_r.msaa != msaa_want) { + if (g_r.msaa != msaa_want && g_r.pfx_ready) { + // The photo-postfx depth-pack pass is compiled against the depth + // buffer's sample count (PFX_MSAA variant); retire the chain's PSOs + // so the next photo-editor frame rebuilds them. + for (nrhi::Pipeline*& p : g_r.pfx_pso) { + if (p != nullptr) { + device->DestroyDeferred(p); + p = nullptr; + } + } + g_r.pfx_ready = false; + } g_r.hdr_active = hdr_want; g_r.hdr_scene_format = hdr_fmt_want; + g_r.msaa = msaa_want; if (!EnsureScenePsoFamily(context) || !EnsureResolvePso(context) || !EnsureBlurPsos(context) || !EnsureOutlineEdgePso(context) || !Ensure2dPso(context) || !EnsureSplinePsos(context) || diff --git a/src/skate3_native_scene_gpu_internal.h b/src/skate3_native_scene_gpu_internal.h index 7f87e76..956e078 100644 --- a/src/skate3_native_scene_gpu_internal.h +++ b/src/skate3_native_scene_gpu_internal.h @@ -648,6 +648,9 @@ struct RendererState { // Scene float format the output-sized targets were built with (kUnknown // when classic); packed-format toggles rebuild them. nrhi::Format targets_scene_fmt = nrhi::Format::kUnknown; + // MSAA level the output-sized targets were built with; hot MSAA changes + // rebuild the depth buffer and the multisample color target. + uint32_t targets_msaa = 0; // Screen-space reflections (ssr.hlsl + scene.hlsl ps_refl_gbuf): a // half-res reflection G-buffer re-rendered from the frame's reflective // items (env fams 5/6/13 + water), a half-res screen-space march over the