render: HDR scene pipeline + real bloom (SDK bump)
This commit is contained in:
parent
ae9720bf1a
commit
3e09f33daf
9 changed files with 5773 additions and 1959 deletions
|
|
@ -281,6 +281,7 @@ set(SKATE3_NATIVE_SHADER_SOURCES
|
|||
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/spline.hlsl
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/photo_fx.hlsl
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/ssao.hlsl
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/hdr.hlsl
|
||||
)
|
||||
set(SKATE3_NATIVE_SHADER_HEADER
|
||||
${CMAKE_CURRENT_BINARY_DIR}/include/skate3_native_shaders.h)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ set(SHADER_VARS
|
|||
kShaderSplineSource
|
||||
kPhotoFxShaderSource
|
||||
kSsaoShaderSource
|
||||
kHdrShaderSource
|
||||
)
|
||||
set(SHADER_FILES
|
||||
scene.hlsl
|
||||
|
|
@ -26,6 +27,7 @@ set(SHADER_FILES
|
|||
spline.hlsl
|
||||
photo_fx.hlsl
|
||||
ssao.hlsl
|
||||
hdr.hlsl
|
||||
)
|
||||
|
||||
set(content "// Generated by cmake/EmbedShaders.cmake from src/native/shaders/*.hlsl.\n")
|
||||
|
|
|
|||
143
src/native/shaders/hdr.hlsl
Normal file
143
src/native/shaders/hdr.hlsl
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// HDR post chain: bloom pyramid + the scene tonemap, applied once over the
|
||||
// float scene intermediate (see the HDR path in skate3_native_scene.cpp).
|
||||
//
|
||||
// The material shaders' shared tone chain, fog -> exposure -> tonemap ->
|
||||
// sqrt -> the postfx uber's 1.41 scene multiplier, is IDENTICAL across every
|
||||
// branch after the per-branch exposure multiply, so under HDR the branches
|
||||
// write that chain's INPUT (pre-tonemap linear, "xe") into an RGBA16F target
|
||||
// and this pass applies the chain exactly once. Bloom energy is injected
|
||||
// pre-tonemap (x + bloom * intensity), so intensity 0 reproduces the classic
|
||||
// in-material output bit-for-bit modulo fp16 storage rounding.
|
||||
//
|
||||
// Pass chain (fullscreen triangles, own binding layout = the SSAO shape):
|
||||
// ps_bloom_down 13-tap Jimenez downsample; the BLOOM_FIRST=1 variant adds
|
||||
// the soft-knee threshold + per-group Karis luma weighting
|
||||
// (anti-firefly) on the mip-0 extraction
|
||||
// ps_bloom_up 3x3 tent upsample, additive-blended onto the next-larger
|
||||
// level (ONE/ONE at the PSO)
|
||||
// ps_tonemap scene xe + bloom -> the exact classic tone chain -> the
|
||||
// gamma guest output
|
||||
cbuffer C : register(b0) {
|
||||
float4 size; // xy = destination size in px, zw = 1 / destination size
|
||||
float4 src; // xy = source size in px, zw = 1 / source size
|
||||
float4 p0; // x = bloom threshold (xe space), y = soft knee,
|
||||
// z = bloom intensity, w = debug (1 = bloom term, 2 = raw xe)
|
||||
float4 p1; // x = downsample tap spread in source texels (2 on the 4x
|
||||
// extraction so the 13-tap footprint covers the whole
|
||||
// source block, 1 on the 2x chain), yzw spare
|
||||
};
|
||||
Texture2D<float4> tex0 : register(t0); // per-pass primary input
|
||||
Texture2D<float4> tex1 : register(t1); // ps_tonemap: the bloom plane
|
||||
// ps_tonemap: the SSAO multiplier plane (fused pre-tonemap apply, replaces
|
||||
// the classic path's separate full-res composite draw; white when AO is
|
||||
// off). Bilinear upsample from the AO raster, like the classic composite.
|
||||
Texture2D<float> ao_plane : register(t2);
|
||||
SamplerState smp_point : register(s0);
|
||||
SamplerState smp_linear : register(s1);
|
||||
|
||||
struct VSOut {
|
||||
float4 pos : SV_Position;
|
||||
float2 uv : TEXCOORD0;
|
||||
};
|
||||
VSOut vs_main(uint id : SV_VertexID) {
|
||||
VSOut o;
|
||||
float2 uv = float2((id << 1) & 2, id & 2);
|
||||
o.pos = float4(uv * float2(2, -2) + float2(-1, 1), 0, 1);
|
||||
o.uv = uv;
|
||||
return o;
|
||||
}
|
||||
|
||||
// The exact scene tone chain (see scene.hlsl ToneOut): input = the
|
||||
// materials' post-fog, post-exposure linear value.
|
||||
float3 ToneMapScene(float3 x) {
|
||||
float3 t1 = saturate(1.0 - x);
|
||||
float3 tm = max(x * 0.25 + 0.75, 1.0) - t1 * t1;
|
||||
return saturate(sqrt(max(tm * 0.5, 0.0)) * 1.41);
|
||||
}
|
||||
|
||||
// Soft-knee brightpass (kept in xe space, where 1.0 is the tonemapper's
|
||||
// saturation point): quadratic ramp from (threshold - knee) to
|
||||
// (threshold + knee), linear excess beyond.
|
||||
float3 SoftThreshold(float3 c) {
|
||||
float br = max(c.r, max(c.g, c.b));
|
||||
float rq = clamp(br - p0.x + p0.y, 0.0, 2.0 * p0.y);
|
||||
rq = rq * rq / (4.0 * p0.y + 1e-4);
|
||||
return c * (max(rq, br - p0.x) / max(br, 1e-4));
|
||||
}
|
||||
|
||||
// 13-tap downsample (Jimenez, SIGGRAPH 2014): five overlapping 2x2 groups,
|
||||
// the half-texel-centered inner group at weight 0.5 and four outer groups at
|
||||
// 0.125 each. On the first (extraction) pass each group is Karis-weighted by
|
||||
// 1/(1 + luma) before mixing, which stops single HDR-bright samples
|
||||
// (specular fireflies, the sun-core texels) from flickering as they cross
|
||||
// pyramid texel grids.
|
||||
float4 ps_bloom_down(VSOut i) : SV_Target {
|
||||
float2 ts = src.zw * p1.x;
|
||||
float3 tA = tex0.SampleLevel(smp_linear, i.uv + ts * float2(-2, -2), 0).rgb;
|
||||
float3 tB = tex0.SampleLevel(smp_linear, i.uv + ts * float2(0, -2), 0).rgb;
|
||||
float3 tC = tex0.SampleLevel(smp_linear, i.uv + ts * float2(2, -2), 0).rgb;
|
||||
float3 tD = tex0.SampleLevel(smp_linear, i.uv + ts * float2(-1, -1), 0).rgb;
|
||||
float3 tE = tex0.SampleLevel(smp_linear, i.uv + ts * float2(1, -1), 0).rgb;
|
||||
float3 tF = tex0.SampleLevel(smp_linear, i.uv + ts * float2(-2, 0), 0).rgb;
|
||||
float3 tG = tex0.SampleLevel(smp_linear, i.uv, 0).rgb;
|
||||
float3 tH = tex0.SampleLevel(smp_linear, i.uv + ts * float2(2, 0), 0).rgb;
|
||||
float3 tI = tex0.SampleLevel(smp_linear, i.uv + ts * float2(-1, 1), 0).rgb;
|
||||
float3 tJ = tex0.SampleLevel(smp_linear, i.uv + ts * float2(1, 1), 0).rgb;
|
||||
float3 tK = tex0.SampleLevel(smp_linear, i.uv + ts * float2(-2, 2), 0).rgb;
|
||||
float3 tL = tex0.SampleLevel(smp_linear, i.uv + ts * float2(0, 2), 0).rgb;
|
||||
float3 tM = tex0.SampleLevel(smp_linear, i.uv + ts * float2(2, 2), 0).rgb;
|
||||
float3 g0 = (tD + tE + tI + tJ) * 0.25; // inner group
|
||||
float3 g1 = (tA + tB + tF + tG) * 0.25;
|
||||
float3 g2 = (tB + tC + tG + tH) * 0.25;
|
||||
float3 g3 = (tF + tG + tK + tL) * 0.25;
|
||||
float3 g4 = (tG + tH + tL + tM) * 0.25;
|
||||
#ifdef BLOOM_FIRST
|
||||
float w0 = 0.5 / (1.0 + dot(g0, float3(0.2126, 0.7152, 0.0722)));
|
||||
float w1 = 0.125 / (1.0 + dot(g1, float3(0.2126, 0.7152, 0.0722)));
|
||||
float w2 = 0.125 / (1.0 + dot(g2, float3(0.2126, 0.7152, 0.0722)));
|
||||
float w3 = 0.125 / (1.0 + dot(g3, float3(0.2126, 0.7152, 0.0722)));
|
||||
float w4 = 0.125 / (1.0 + dot(g4, float3(0.2126, 0.7152, 0.0722)));
|
||||
float3 c = (g0 * w0 + g1 * w1 + g2 * w2 + g3 * w3 + g4 * w4) /
|
||||
(w0 + w1 + w2 + w3 + w4);
|
||||
return float4(SoftThreshold(max(c, 0.0)), 1.0);
|
||||
#else
|
||||
return float4(g0 * 0.5 + (g1 + g2 + g3 + g4) * 0.125, 1.0);
|
||||
#endif
|
||||
}
|
||||
|
||||
// 3x3 tent upsample of the next-smaller level; the PSO blends it additively
|
||||
// onto the destination level (progressive upsampling accumulates every
|
||||
// pyramid level's contribution with widening support).
|
||||
float4 ps_bloom_up(VSOut i) : SV_Target {
|
||||
float2 ts = src.zw;
|
||||
float3 c = tex0.SampleLevel(smp_linear, i.uv + ts * float2(-1, -1), 0).rgb;
|
||||
c += tex0.SampleLevel(smp_linear, i.uv + ts * float2(0, -1), 0).rgb * 2.0;
|
||||
c += tex0.SampleLevel(smp_linear, i.uv + ts * float2(1, -1), 0).rgb;
|
||||
c += tex0.SampleLevel(smp_linear, i.uv + ts * float2(-1, 0), 0).rgb * 2.0;
|
||||
c += tex0.SampleLevel(smp_linear, i.uv, 0).rgb * 4.0;
|
||||
c += tex0.SampleLevel(smp_linear, i.uv + ts * float2(1, 0), 0).rgb * 2.0;
|
||||
c += tex0.SampleLevel(smp_linear, i.uv + ts * float2(-1, 1), 0).rgb;
|
||||
c += tex0.SampleLevel(smp_linear, i.uv + ts * float2(0, 1), 0).rgb * 2.0;
|
||||
c += tex0.SampleLevel(smp_linear, i.uv + ts * float2(1, 1), 0).rgb;
|
||||
return float4(c * (1.0 / 16.0), 1.0);
|
||||
}
|
||||
|
||||
// Final tonemap into the gamma guest output. tex0 = the full-res scene xe
|
||||
// plane (point-sampled, 1:1), tex1 = bloom pyramid level 0 (bilinear
|
||||
// upsample; the white fallback rides here with intensity 0 when bloom is
|
||||
// off).
|
||||
float4 ps_tonemap(VSOut i) : SV_Target {
|
||||
float ao = ao_plane.SampleLevel(smp_linear, i.uv, 0);
|
||||
float3 x = max(tex0.SampleLevel(smp_point, i.uv, 0).rgb, 0.0) * ao;
|
||||
float3 bloom = tex1.SampleLevel(smp_linear, i.uv, 0).rgb * p0.z;
|
||||
if (p0.w > 2.5) {
|
||||
return float4(ao, ao, ao, 1.0); // debug: AO multiplier plane
|
||||
}
|
||||
if (p0.w > 1.5) {
|
||||
return float4(saturate(x), 1.0); // debug: raw xe
|
||||
}
|
||||
if (p0.w > 0.5) {
|
||||
return float4(ToneMapScene(bloom), 1.0); // debug: bloom term only
|
||||
}
|
||||
return float4(ToneMapScene(x + bloom), 1.0);
|
||||
}
|
||||
|
|
@ -270,9 +270,43 @@ void KdAxes(float4 tanb, float3 wn, float3 tt_s, float3 bb_s,
|
|||
kt = cross(kb, wn) * ((tanb.w > 0.6 ? 1.0 : -1.0) * sh_v2.x);
|
||||
}
|
||||
}
|
||||
// ---- Output encode --------------------------------------------------------
|
||||
// Every exact branch ends in the same chain: x = the post-fog,
|
||||
// post-exposure linear value ("xe"), then
|
||||
// tonemap = max(x*0.25 + 0.75, 1) - sat(1 - x)^2
|
||||
// out = sat(sqrt(0.5 * tonemap) * 1.41) (the uber's 1.41 fold)
|
||||
// Under HDR=1 the branch returns xe itself into the float scene
|
||||
// intermediate and the host post pass (hdr.hlsl ps_tonemap) applies the
|
||||
// chain exactly once, which is what lets real bloom inject energy
|
||||
// pre-tonemap. `reduced` = environmentdiffuse's cheap curve 1 - sat(1-x)^2;
|
||||
// it equals the full curve evaluated at min(x, 1) (the two only differ past
|
||||
// the x = 1 saturation point), so the HDR encode is a clamp.
|
||||
float4 ToneOut(float3 xe, float a, bool reduced) {
|
||||
#ifdef HDR
|
||||
float3 x = max(xe, 0.0);
|
||||
return float4(reduced ? min(x, 1.0) : x, a);
|
||||
#else
|
||||
float3 t1 = saturate(1.0 - xe);
|
||||
float3 tm = reduced ? 1.0 - t1 * t1 : max(xe * 0.25 + 0.75, 1.0) - t1 * t1;
|
||||
return float4(saturate(sqrt(max(tm * 0.5, 0.0)) * 1.41), a);
|
||||
#endif
|
||||
}
|
||||
// Passthrough for values authored in the FINAL gamma space (the legacy
|
||||
// empirical branches, water, debug visualizations): under HDR they encode
|
||||
// as the tone chain's exact inverse, so the host tonemap restores them.
|
||||
float3 PassGamma(float3 c) {
|
||||
#ifdef HDR
|
||||
float3 tm = c * c * (2.0 / (1.41 * 1.41));
|
||||
float3 lo = 1.0 - sqrt(saturate(1.0 - tm));
|
||||
float3 hi = 4.0 * tm - 3.0;
|
||||
return lerp(lo, hi, step(1.0, tm)); // per-channel select (fxc + dxc)
|
||||
#else
|
||||
return c;
|
||||
#endif
|
||||
}
|
||||
float4 ps_main(VSOut i) : SV_Target {
|
||||
if (tint.a > 0.0) {
|
||||
return tint;
|
||||
return float4(PassGamma(tint.rgb), tint.a);
|
||||
}
|
||||
float4 albedo = diffuse.Sample(smp, i.uv);
|
||||
// Alpha-tested foliage/fences; opaque formats sample alpha = 1. Character
|
||||
|
|
@ -316,10 +350,7 @@ float4 ps_main(VSOut i) : SV_Target {
|
|||
float4 sun = decal_art.Sample(smp_clamp, float2(sinA / overlay.x, 0.5 / 16.0));
|
||||
lin += sun.rgb * sun.rgb / saturate(sun.a + 0.01);
|
||||
float3 xe = lin * overlay.y * misc.y;
|
||||
float3 t1 = saturate(1.0 - xe);
|
||||
float3 tm = max(xe * 0.25 + 0.75, 1.0) - t1 * t1;
|
||||
float3 cc = saturate(sqrt(max(tm * 0.5, 0.0)) * 1.41);
|
||||
return float4(cc, 1.0);
|
||||
return ToneOut(xe, 1.0, false);
|
||||
}
|
||||
// dynamicobject.fx props (cam_pos.w = -(20 + variant): -21 default,
|
||||
// -22 alphatest). Rigid movable objects (dispensers, dumpsters, benches,
|
||||
|
|
@ -452,9 +483,7 @@ float4 ps_main(VSOut i) : SV_Target {
|
|||
float3 fog_rgb = sh_fogc.rgb * f1;
|
||||
float fog_a = (1.0 + sh_fogc.a * f1) * dyn_misc.x; // x material multiplier
|
||||
float3 xe = (lin * fog_a + fog_rgb) * dyn_sun.w;
|
||||
float3 t1 = saturate(1.0 - xe);
|
||||
float3 tm = max(xe * 0.25 + 0.75, 1.0) - t1 * t1;
|
||||
return float4(saturate(sqrt(max(tm * 0.5, 0.0)) * 1.41), 1.0);
|
||||
return ToneOut(xe, 1.0, false);
|
||||
}
|
||||
// Character families: the game's own lighting in LINEAR space (diffuse is
|
||||
// gamma -> square it), then the exact tone chain from the disassembly and
|
||||
|
|
@ -587,10 +616,7 @@ float4 ps_main(VSOut i) : SV_Target {
|
|||
}
|
||||
// Exact tone chain: sqrt(0.5 * (max(x*E/4 + 0.75, 1) - sat(1 - x*E)^2)).
|
||||
float E = max(ch_key.w, 0.01);
|
||||
float3 t1 = saturate(1.0 - lin * E);
|
||||
float3 tm = max(lin * 0.25 * E + 0.75, 1.0) - t1 * t1;
|
||||
float3 cc = saturate(sqrt(max(tm * 0.5, 0.0)) * 1.41);
|
||||
return float4(cc, out_a);
|
||||
return ToneOut(lin * E, out_a, false);
|
||||
}
|
||||
// Exact world-material families (cam_pos.w = -family): hand-ported from
|
||||
// the game's own pixel shaders and verified per-pixel against them with
|
||||
|
|
@ -698,15 +724,15 @@ float4 ps_main(VSOut i) : SV_Target {
|
|||
// Debug taps live on fams 5/6/13 only; on fams 1-4 misc.z carries
|
||||
// the v2 material bind flags instead (family-gated below).
|
||||
if (fam > 4.5 && abs(misc.z - 7.0) < 0.5) {
|
||||
return float4(lmg, 1.0);
|
||||
return float4(PassGamma(lmg), 1.0);
|
||||
}
|
||||
if (fam > 4.5 && abs(misc.z - 8.0) < 0.5) {
|
||||
return float4(frac(i.uv2 * 16.0), 0.0, 1.0);
|
||||
return float4(PassGamma(float3(frac(i.uv2 * 16.0), 0.0)), 1.0);
|
||||
}
|
||||
// Mode 9: lightmap-resolve status; RED = a real lightmap is bound
|
||||
// (tint.r set by the C++ resolve), BLUE = white fallback in t1.
|
||||
if (fam > 4.5 && abs(misc.z - 9.0) < 0.5) {
|
||||
return float4(tint.r, 0.0, 1.0 - tint.r, 1.0);
|
||||
return float4(PassGamma(float3(tint.r, 0.0, 1.0 - tint.r)), 1.0);
|
||||
}
|
||||
// tint.r == 0 = the real lightmap has not resolved yet (first-sight
|
||||
// decode in flight; t1 = the white fallback). Min-clamping the
|
||||
|
|
@ -906,11 +932,7 @@ float4 ps_main(VSOut i) : SV_Target {
|
|||
// Fog -> exposure -> tonemap -> sqrt, then the postfx uber's measured
|
||||
// 1.41 scene multiplier (same as the character branch).
|
||||
float3 xe = (lin * fog_a + fog_rgb) * expo;
|
||||
float3 t1e = saturate(1.0 - xe);
|
||||
float3 tme = reduced_tone ? 1.0 - t1e * t1e
|
||||
: max(xe * 0.25 + 0.75, 1.0) - t1e * t1e;
|
||||
float3 cce = saturate(sqrt(max(tme * 0.5, 0.0)) * 1.41);
|
||||
return float4(cce, out_a);
|
||||
return ToneOut(xe, out_a, reduced_tone);
|
||||
}
|
||||
// environment.decal surfaces: the paint/graffiti art (t4) is composited
|
||||
// over the base diffuse by ITS alpha, opaque output; these meshes ARE
|
||||
|
|
@ -1059,7 +1081,7 @@ float4 ps_main(VSOut i) : SV_Target {
|
|||
fade * mat_tint.rgb, 0.0));
|
||||
// Opaque: the game's murk hides the canal bed entirely (and our bed
|
||||
// shading is untrustworthy under water: striped lightmap unwraps).
|
||||
return float4(col, 1.0);
|
||||
return float4(PassGamma(col), 1.0);
|
||||
}
|
||||
if (misc.x > 0.0) {
|
||||
// transparentenvironment.fx (Skate 2 source; disassembled Skate 3 PS
|
||||
|
|
@ -1076,9 +1098,9 @@ float4 ps_main(VSOut i) : SV_Target {
|
|||
}
|
||||
lit = sqrt(max(lit * lit * saturate(1.0 + fade * mat_tint.w) +
|
||||
fade * mat_tint.rgb, 0.0));
|
||||
return float4(lit, albedo.a * albedo.a);
|
||||
return float4(PassGamma(lit), albedo.a * albedo.a);
|
||||
}
|
||||
return float4(lit, 1.0);
|
||||
return float4(PassGamma(lit), 1.0);
|
||||
}
|
||||
// Shadow caster pass: vs_main runs with mvp = (world *) lightVP built from
|
||||
// the captured receiver rows, so SV_Position.z IS the light-space depth
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -34,14 +34,28 @@ VSOut vs_main(float4 p : POSITION, float2 uv : TEXCOORD0, float fade : TEXCOORD1
|
|||
// Getting this wrong is visible: a linear 1/a over-brightens the low-alpha
|
||||
// glow fringe (fuzzy, blown-out edges) and a linear darken alpha weakens the
|
||||
// backdrop dimming that makes the neon read as solid.
|
||||
// HDR=1: the scene target holds the tone chain's pre-tonemap input (see
|
||||
// scene.hlsl ToneOut), so the gamma-authored spline colors encode through
|
||||
// the chain's exact inverse before blending; the host tonemap pass then
|
||||
// restores them (and the neon glow feeds bloom).
|
||||
float3 SplineOut(float3 c) {
|
||||
#ifdef HDR
|
||||
float3 tm = c * c * (2.0 / (1.41 * 1.41));
|
||||
float3 lo = 1.0 - sqrt(saturate(1.0 - tm));
|
||||
float3 hi = 4.0 * tm - 3.0;
|
||||
return lerp(lo, hi, step(1.0, tm)); // per-channel select (fxc + dxc)
|
||||
#else
|
||||
return c;
|
||||
#endif
|
||||
}
|
||||
float4 ps_default(VSOut i) : SV_Target {
|
||||
float4 c = tex.Sample(smp, i.uv);
|
||||
float3 sq = c.rgb * c.rgb * (intensity.x * i.fade / max(c.a, 1.0 / 255.0));
|
||||
return float4(sqrt(abs(sq)), 1.0);
|
||||
return float4(SplineOut(sqrt(abs(sq))), 1.0);
|
||||
}
|
||||
float4 ps_darken(VSOut i) : SV_Target {
|
||||
float4 c = tex.Sample(smp, i.uv);
|
||||
float3 rgb = sqrt(abs(c.rgb * c.rgb * intensity.y));
|
||||
float a = sqrt(abs(c.a * intensity.y * i.fade));
|
||||
return float4(rgb, a);
|
||||
return float4(SplineOut(rgb), a);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,9 +117,17 @@ float Ign(float2 p) {
|
|||
|
||||
// Half-res scene luminance for the sun-lit protection mask (low-frequency
|
||||
// by nature, so the AO raster resolution is plenty; bilinear at the 2x
|
||||
// reduction is an exact 2x2 box).
|
||||
// reduction is an exact 2x2 box). Under HDR=1 the scene plane holds the
|
||||
// pre-tonemap linear value; apply the scene tone chain first so the
|
||||
// protection thresholds keep their display-space meaning.
|
||||
float ps_luma(VSOut i) : SV_Target {
|
||||
float3 scene = tex0c.SampleLevel(smp_linear, i.uv, 0).rgb;
|
||||
#ifdef HDR
|
||||
float3 x = max(scene, 0.0);
|
||||
float3 t1 = saturate(1.0 - x);
|
||||
float3 tm = max(x * 0.25 + 0.75, 1.0) - t1 * t1;
|
||||
scene = saturate(sqrt(max(tm * 0.5, 0.0)) * 1.41);
|
||||
#endif
|
||||
return dot(scene, float3(0.299f, 0.587f, 0.114f));
|
||||
}
|
||||
|
||||
|
|
@ -207,9 +215,16 @@ float ps_gtao(VSOut i) : SV_Target {
|
|||
vis = saturate(vis / float(AO_SLICES));
|
||||
// Bake the full multiplier here so the blur smooths the finished term and
|
||||
// the composite is a plain blend: intensity exponent on the linear
|
||||
// visibility, the extra 0.5 converting the multiply into the target's
|
||||
// gamma-2 encoding, then the protection and distance fades toward 1.
|
||||
// visibility, then the protection and distance fades toward 1. The
|
||||
// classic target is gamma-2-encoded, so the exponent halves to express a
|
||||
// linear-space multiply there; the HDR target is pre-tonemap linear
|
||||
// (near-proportional to display-linear below the tonemapper's knee) and
|
||||
// takes the exponent directly.
|
||||
#ifdef HDR
|
||||
float a = pow(vis, p0.y);
|
||||
#else
|
||||
float a = pow(vis, p0.y * 0.5f);
|
||||
#endif
|
||||
a = lerp(a, 1.0f, protect);
|
||||
return lerp(a, 1.0f, fade);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,49 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_ssao_full_res, false, "Skate 3",
|
|||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_ssao_debug, false, "Skate 3",
|
||||
"Replace the frame with the SSAO visibility term (tuning aid).")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_hdr, true, "Skate 3",
|
||||
"Render the 3D scene into a float (HDR) intermediate and apply the "
|
||||
"game's shared tone chain once in a host post pass, the basis for "
|
||||
"real bloom (and later HDR effects). With bloom off the output is "
|
||||
"equivalent to the classic in-material tonemap. Off = the classic "
|
||||
"path, for A/B parity checks.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_bloom, true, "Skate 3",
|
||||
"Real bloom from the HDR scene: a downsample/upsample pyramid driven "
|
||||
"by pre-tonemap brightness (night lamps, neon, the sun and sky "
|
||||
"glow). Requires the HDR intermediate.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_bloom_threshold, 0.9, "Skate 3",
|
||||
"Bloom brightness threshold in pre-tonemap units (1.0 = the tone "
|
||||
"curve's saturation point; 0.85 corresponds to ~97% display "
|
||||
"white). Sunlit surfaces sit around 0.5-0.65 pre-tonemap; "
|
||||
"thresholds that reach them veil the whole daytime frame in "
|
||||
"glow; near-clipped lamp cores, neon, sky glare and the sun "
|
||||
"stay above 0.85.")
|
||||
.range(0.0, 8.0)
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_bloom_knee, 0.08, "Skate 3",
|
||||
"Bloom soft-knee width around the threshold (0 = hard cutoff).")
|
||||
.range(0.0, 2.0)
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_DOUBLE(skate3_native_render_scene_bloom_intensity, 0.06, "Skate 3",
|
||||
"Bloom strength: the pyramid's contribution added to the "
|
||||
"pre-tonemap scene (0 = off; the pyramid accumulates ~5 levels, "
|
||||
"so small values are already visible around bright lights).")
|
||||
.range(0.0, 2.0)
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_hdr_packed, true, "Skate 3",
|
||||
"Use the packed R11G11B10 float format for the HDR scene targets "
|
||||
"instead of RGBA16F: halves the scene-pass color bandwidth (most "
|
||||
"of the HDR cost at high resolution + MSAA) at slightly lower "
|
||||
"precision in very dark gradients.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_INT32(skate3_native_render_scene_hdr_debug, 0, "Skate 3",
|
||||
"HDR post debug: 0 = off, 1 = show the bloom term only, 2 = show "
|
||||
"the raw pre-tonemap scene (clamped), 3 = show the fused SSAO "
|
||||
"multiplier plane.")
|
||||
.range(0, 3)
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_2d, true, "Skate 3",
|
||||
"Replay the game's 2D/APT (Flash HUD) draws as a native overlay pass "
|
||||
"on top of the 3D scene")
|
||||
|
|
@ -10232,7 +10275,46 @@ struct RendererState {
|
|||
uint32_t ao_lin_width = 0, ao_lin_height = 0; // linear depth (always full)
|
||||
uint32_t ao_msaa = 0;
|
||||
nrhi::Format ao_rtv_format = nrhi::Format::kUnknown;
|
||||
bool ao_hdr = false; // luma/gtao/composite built for the HDR target
|
||||
bool ao_failed = false;
|
||||
// Fused-AO handoff (HDR path): the AO pass leaves the finished multiplier
|
||||
// plane (ao_tex[0]) in PIXEL_SHADER_RESOURCE state for ps_tonemap to
|
||||
// consume at t2 (replacing the classic full-res composite draw);
|
||||
// ApplyHdrPost restores it to RENDER_TARGET and clears the flag.
|
||||
bool ao_plane_in_psr = false;
|
||||
// HDR pipeline (hdr.hlsl): the scene renders pre-tonemap linear into a
|
||||
// float intermediate (the MSAA color target and/or hdr_resolved), a bloom
|
||||
// pyramid extracts real HDR energy, and ps_tonemap applies the game's
|
||||
// shared tone chain once into the gamma guest output. Binding layout =
|
||||
// the SSAO shape (root constants b0 + two single-texture tables +
|
||||
// point/linear clamp samplers). All targets idle in RENDER_TARGET state.
|
||||
bool hdr_active = false; // latched per pipeline build (PSO formats/variants)
|
||||
bool hdr_failed = false; // float targets unavailable -> classic path
|
||||
// Latched scene float format (RGBA16F or the packed R11G11B10 option);
|
||||
// every HDR-path PSO/target creation reads this, never the cvar.
|
||||
nrhi::Format hdr_scene_format = nrhi::Format::kR16G16B16A16_FLOAT;
|
||||
nrhi::BindingLayout* hdr_layout = nullptr;
|
||||
nrhi::Pipeline* pso_bloom_first = nullptr; // mip-0 extract (threshold+Karis)
|
||||
nrhi::Pipeline* pso_bloom_down = nullptr;
|
||||
nrhi::Pipeline* pso_bloom_up = nullptr; // additive ONE/ONE tent upsample
|
||||
nrhi::Pipeline* pso_tonemap = nullptr;
|
||||
nrhi::Format hdr_pso_out_format = nrhi::Format::kUnknown;
|
||||
// 1x float scene plane: the MSAA resolve destination (or the scene target
|
||||
// itself when MSAA is off); the AO composite, bloom extract and tonemap
|
||||
// all consume/write it.
|
||||
nrhi::Texture* hdr_resolved = nullptr;
|
||||
nrhi::TextureView* hdr_srv = nullptr;
|
||||
static constexpr uint32_t kBloomMaxLevels = 6;
|
||||
nrhi::Texture* bloom_tex[kBloomMaxLevels] = {};
|
||||
nrhi::TextureView* bloom_srv[kBloomMaxLevels] = {};
|
||||
uint32_t bloom_w[kBloomMaxLevels] = {};
|
||||
uint32_t bloom_h[kBloomMaxLevels] = {};
|
||||
uint32_t bloom_levels = 0;
|
||||
uint32_t bloom_base_w = 0, bloom_base_h = 0; // output size the chain fits
|
||||
bool targets_hdr = false; // output-sized targets built for the HDR path
|
||||
// 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;
|
||||
std::unordered_map<uint32_t, MeshBuffers> meshes;
|
||||
// (The old D3D12 bookkeeping, the retired-resource vector, the CPU SRV
|
||||
// staging heap and its slot allocator/recycling lists, is gone: resource
|
||||
|
|
@ -12608,21 +12690,44 @@ bool EnsureRootSignature(const NativeGuestOutputRenderContext& context) {
|
|||
// no-depth, outline mask).
|
||||
bool EnsureScenePsoFamily(const NativeGuestOutputRenderContext& context) {
|
||||
nrhi::Device* device = context.device;
|
||||
// MSAA level: the requested count, reduced to what the output format
|
||||
// supports (1 disables and renders directly into the guest output). The
|
||||
// Scene color format: the float HDR intermediate when the HDR post chain
|
||||
// is active (the tonemap pass then writes the gamma guest output),
|
||||
// otherwise the guest output itself.
|
||||
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(context.guest_output->format(), msaa);
|
||||
msaa = device->GetSupportedSampleCount(scene_fmt, msaa);
|
||||
g_r.msaa = msaa;
|
||||
|
||||
// The family rebuilds on HDR toggles / output-format changes: retire the
|
||||
// previous pipelines (in-flight frames keep them alive via the deferred
|
||||
// destruction queue).
|
||||
for (nrhi::Pipeline** p :
|
||||
{&g_r.pso, &g_r.pso_cullback, &g_r.pso_transparent, &g_r.pso_fade,
|
||||
&g_r.pso_hair_a, &g_r.pso_hair_b, &g_r.pso_nodepth,
|
||||
&g_r.pso_outline_mask}) {
|
||||
if (*p != nullptr) {
|
||||
device->DestroyDeferred(*p);
|
||||
*p = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// HDR=1 switches the pixel shader's output encode to pre-tonemap linear
|
||||
// (see scene.hlsl ToneOut/PassGamma).
|
||||
const nrhi::ShaderMacro hdr_defs[] = {{"HDR", "1"}, {nullptr, nullptr}};
|
||||
nrhi::Shader* vs = device->CreateShader(
|
||||
MakeShaderDesc(nrhi::ShaderStage::kVertex, "scene.hlsl", kShaderSource,
|
||||
"vs_main", nullptr, ""));
|
||||
nrhi::Shader* ps = device->CreateShader(
|
||||
MakeShaderDesc(nrhi::ShaderStage::kPixel, "scene.hlsl", kShaderSource,
|
||||
"ps_main", nullptr, ""));
|
||||
"ps_main", g_r.hdr_active ? hdr_defs : nullptr,
|
||||
g_r.hdr_active ? "HDR=1" : ""));
|
||||
if (vs == nullptr || ps == nullptr) {
|
||||
REXLOG_ERROR("native-scene: shader compile failed");
|
||||
g_r.failed = true;
|
||||
|
|
@ -12640,7 +12745,7 @@ bool EnsureScenePsoFamily(const NativeGuestOutputRenderContext& context) {
|
|||
pso.depth.test_enable = true;
|
||||
pso.depth.write_enable = true;
|
||||
pso.depth.func = nrhi::CompareFunc::kLess;
|
||||
pso.rtv_format = context.guest_output->format();
|
||||
pso.rtv_format = scene_fmt;
|
||||
pso.dsv_format = nrhi::Format::kD32_FLOAT;
|
||||
pso.sample_count = g_r.msaa;
|
||||
g_r.pso = device->CreateGraphicsPipeline(pso);
|
||||
|
|
@ -12703,7 +12808,7 @@ bool EnsureScenePsoFamily(const NativeGuestOutputRenderContext& context) {
|
|||
REXLOG_WARN("native-scene: outline mask PSO creation failed");
|
||||
// outline pass disables itself
|
||||
}
|
||||
pso.rtv_format = context.guest_output->format();
|
||||
pso.rtv_format = scene_fmt;
|
||||
pso.sample_count = g_r.msaa;
|
||||
device->DestroyDeferred(vs);
|
||||
device->DestroyDeferred(ps);
|
||||
|
|
@ -12719,6 +12824,10 @@ bool EnsureScenePsoFamily(const NativeGuestOutputRenderContext& context) {
|
|||
// Fullscreen MSAA resolve PSO (no-op below MSAA 2x).
|
||||
bool EnsureResolvePso(const NativeGuestOutputRenderContext& context) {
|
||||
nrhi::Device* device = context.device;
|
||||
if (g_r.resolve_pso != nullptr) {
|
||||
device->DestroyDeferred(g_r.resolve_pso);
|
||||
g_r.resolve_pso = nullptr;
|
||||
}
|
||||
if (g_r.msaa > 1) {
|
||||
char samples[8];
|
||||
std::snprintf(samples, sizeof(samples), "%u", g_r.msaa);
|
||||
|
|
@ -12742,7 +12851,10 @@ bool EnsureResolvePso(const NativeGuestOutputRenderContext& context) {
|
|||
rp.vs = rvs;
|
||||
rp.ps = rps;
|
||||
rp.cull = nrhi::CullMode::kNone;
|
||||
rp.rtv_format = context.guest_output->format();
|
||||
// Under HDR the resolve averages the float MSAA scene into the 1x float
|
||||
// plane (hdr_resolved); classic resolves straight into the guest output.
|
||||
rp.rtv_format = g_r.hdr_active ? g_r.hdr_scene_format
|
||||
: context.guest_output->format();
|
||||
rp.sample_count = 1;
|
||||
g_r.resolve_pso = device->CreateGraphicsPipeline(rp);
|
||||
device->DestroyDeferred(rvs);
|
||||
|
|
@ -12938,15 +13050,26 @@ bool EnsureSplinePsos(const NativeGuestOutputRenderContext& context) {
|
|||
// In-world spline pipelines: drawn inside the scene pass (MSAA sample
|
||||
// count, depth test LESS_EQUAL, no z-write) with the game's own blend
|
||||
// states (spline.xml): darken = straight alpha, default = additive.
|
||||
// Under HDR the pixel shaders encode through the tone chain's inverse
|
||||
// (SplineOut) so the host tonemap restores the authored look.
|
||||
for (nrhi::Pipeline** p : {&g_r.pso_spline_darken, &g_r.pso_spline_default}) {
|
||||
if (*p != nullptr) {
|
||||
device->DestroyDeferred(*p);
|
||||
*p = nullptr;
|
||||
}
|
||||
}
|
||||
const nrhi::ShaderMacro hdr_defs[] = {{"HDR", "1"}, {nullptr, nullptr}};
|
||||
const nrhi::ShaderMacro* sp_defs = g_r.hdr_active ? hdr_defs : nullptr;
|
||||
const char* sp_variant = g_r.hdr_active ? "HDR=1" : "";
|
||||
nrhi::Shader* svs = device->CreateShader(
|
||||
MakeShaderDesc(nrhi::ShaderStage::kVertex, "spline.hlsl",
|
||||
kShaderSplineSource, "vs_main", nullptr, ""));
|
||||
nrhi::Shader* spd = device->CreateShader(
|
||||
MakeShaderDesc(nrhi::ShaderStage::kPixel, "spline.hlsl",
|
||||
kShaderSplineSource, "ps_default", nullptr, ""));
|
||||
kShaderSplineSource, "ps_default", sp_defs, sp_variant));
|
||||
nrhi::Shader* spk = device->CreateShader(
|
||||
MakeShaderDesc(nrhi::ShaderStage::kPixel, "spline.hlsl",
|
||||
kShaderSplineSource, "ps_darken", nullptr, ""));
|
||||
kShaderSplineSource, "ps_darken", sp_defs, sp_variant));
|
||||
if (svs == nullptr || spd == nullptr || spk == nullptr) {
|
||||
REXLOG_ERROR("native-scene: spline shader compile failed");
|
||||
g_r.failed = true;
|
||||
|
|
@ -12967,7 +13090,8 @@ bool EnsureSplinePsos(const NativeGuestOutputRenderContext& context) {
|
|||
sp.input_elements = input_spline;
|
||||
sp.input_element_count = 3;
|
||||
sp.vertex_stride = 28;
|
||||
sp.rtv_format = context.guest_output->format();
|
||||
sp.rtv_format = g_r.hdr_active ? g_r.hdr_scene_format
|
||||
: context.guest_output->format();
|
||||
sp.dsv_format = nrhi::Format::kD32_FLOAT;
|
||||
sp.sample_count = g_r.msaa;
|
||||
sp.blend.enable = true;
|
||||
|
|
@ -13640,7 +13764,11 @@ bool EnsureOutputSizedTargets(const NativeGuestOutputRenderContext& context) {
|
|||
nrhi::Device* device = context.device;
|
||||
const uint32_t width = context.guest_output_width;
|
||||
const uint32_t height = context.guest_output_height;
|
||||
if (!g_r.depth || g_r.depth_width != width || g_r.depth_height != height) {
|
||||
const nrhi::Format want_scene_fmt =
|
||||
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) {
|
||||
if (g_r.depth) {
|
||||
g_r.device->DestroyDeferred(g_r.depth);
|
||||
g_r.depth = nullptr;
|
||||
|
|
@ -13671,13 +13799,15 @@ bool EnsureOutputSizedTargets(const NativeGuestOutputRenderContext& context) {
|
|||
|
||||
if (g_r.msaa > 1) {
|
||||
// MSAA color target + its Texture2DMS view for the fullscreen resolve
|
||||
// pass. Lives in RENDER_TARGET state between frames.
|
||||
// pass. Lives in RENDER_TARGET state between frames. Float under HDR
|
||||
// (the scene writes pre-tonemap linear).
|
||||
if (g_r.msaa_color) {
|
||||
g_r.device->DestroyDeferred(g_r.msaa_color);
|
||||
g_r.msaa_color = nullptr;
|
||||
}
|
||||
nrhi::TextureDesc cdesc = desc;
|
||||
cdesc.format = context.guest_output->format();
|
||||
cdesc.format = g_r.hdr_active ? g_r.hdr_scene_format
|
||||
: context.guest_output->format();
|
||||
cdesc.usage = nrhi::kTextureUsageRenderTarget;
|
||||
cdesc.initial_state = nrhi::ResourceState::kRenderTarget;
|
||||
cdesc.clear_color[0] = 0.25f;
|
||||
|
|
@ -13686,6 +13816,16 @@ bool EnsureOutputSizedTargets(const NativeGuestOutputRenderContext& context) {
|
|||
cdesc.clear_color[3] = 1.0f;
|
||||
g_r.msaa_color = g_r.device->CreateTexture(cdesc);
|
||||
if (g_r.msaa_color == nullptr) {
|
||||
if (g_r.hdr_active) {
|
||||
// Float MSAA target unavailable: drop to the classic path instead
|
||||
// of killing the native renderer (rebuilt next frame).
|
||||
REXLOG_WARN(
|
||||
"native-scene: HDR MSAA color target creation failed, "
|
||||
"falling back to the classic tonemap path");
|
||||
g_r.hdr_failed = true;
|
||||
g_r.depth_width = 0;
|
||||
return false;
|
||||
}
|
||||
g_r.failed = true;
|
||||
return false;
|
||||
}
|
||||
|
|
@ -13699,6 +13839,44 @@ bool EnsureOutputSizedTargets(const NativeGuestOutputRenderContext& context) {
|
|||
g_r.msaa_srv_slot = device->CreateTextureView(g_r.msaa_color, vd);
|
||||
g_r.msaa_srv_allocated = g_r.msaa_srv_slot != nullptr;
|
||||
}
|
||||
|
||||
// 1x float scene plane for the HDR post chain: the MSAA resolve
|
||||
// destination, or the scene target itself when MSAA is off. Idles in
|
||||
// RENDER_TARGET state; the bloom/tonemap passes sample it through
|
||||
// hdr_srv.
|
||||
if (g_r.hdr_srv != nullptr) {
|
||||
g_r.device->DestroyDeferred(g_r.hdr_srv);
|
||||
g_r.hdr_srv = nullptr;
|
||||
}
|
||||
if (g_r.hdr_resolved != nullptr) {
|
||||
g_r.device->DestroyDeferred(g_r.hdr_resolved);
|
||||
g_r.hdr_resolved = nullptr;
|
||||
}
|
||||
if (g_r.hdr_active) {
|
||||
nrhi::TextureDesc hdesc;
|
||||
hdesc.width = width;
|
||||
hdesc.height = height;
|
||||
hdesc.mip_levels = 1;
|
||||
hdesc.format = g_r.hdr_scene_format;
|
||||
hdesc.usage = nrhi::kTextureUsageRenderTarget;
|
||||
hdesc.initial_state = nrhi::ResourceState::kRenderTarget;
|
||||
g_r.hdr_resolved = g_r.device->CreateTexture(hdesc);
|
||||
if (g_r.hdr_resolved != nullptr) {
|
||||
nrhi::TextureViewDesc vd;
|
||||
vd.mip_levels = 1;
|
||||
g_r.hdr_srv = g_r.device->CreateTextureView(g_r.hdr_resolved, vd);
|
||||
}
|
||||
if (g_r.hdr_resolved == nullptr || g_r.hdr_srv == nullptr) {
|
||||
REXLOG_WARN(
|
||||
"native-scene: HDR scene plane creation failed, falling back "
|
||||
"to the classic tonemap path");
|
||||
g_r.hdr_failed = true;
|
||||
g_r.depth_width = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
g_r.targets_hdr = g_r.hdr_active;
|
||||
g_r.targets_scene_fmt = want_scene_fmt;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -13711,8 +13889,13 @@ bool EnsureSsaoPipeline(const NativeGuestOutputRenderContext& context) {
|
|||
if (g_r.ao_failed) {
|
||||
return false;
|
||||
}
|
||||
// Composite target: the float HDR plane (pre-tonemap multiply) or the
|
||||
// gamma guest output (classic).
|
||||
const nrhi::Format ao_out_fmt = g_r.hdr_active
|
||||
? g_r.hdr_scene_format
|
||||
: context.guest_output->format();
|
||||
if (g_r.pso_ao_linearize != nullptr && g_r.ao_msaa == g_r.msaa &&
|
||||
g_r.ao_rtv_format == context.guest_output->format()) {
|
||||
g_r.ao_rtv_format == ao_out_fmt && g_r.ao_hdr == g_r.hdr_active) {
|
||||
return true;
|
||||
}
|
||||
nrhi::Device* device = context.device;
|
||||
|
|
@ -13752,6 +13935,9 @@ bool EnsureSsaoPipeline(const NativeGuestOutputRenderContext& context) {
|
|||
}
|
||||
const bool msaa = g_r.msaa > 1;
|
||||
const nrhi::ShaderMacro msaa_defs[] = {{"AO_MSAA", "1"}, {nullptr, nullptr}};
|
||||
// HDR=1 on the luma pass (tonemap the pre-tonemap scene before the
|
||||
// protection luma) and the march (linear-space intensity exponent).
|
||||
const nrhi::ShaderMacro hdr_defs[] = {{"HDR", "1"}, {nullptr, nullptr}};
|
||||
nrhi::Shader* vs = device->CreateShader(
|
||||
MakeShaderDesc(nrhi::ShaderStage::kVertex, "ssao.hlsl",
|
||||
kSsaoShaderSource, "vs_main", nullptr, ""));
|
||||
|
|
@ -13761,10 +13947,12 @@ bool EnsureSsaoPipeline(const NativeGuestOutputRenderContext& context) {
|
|||
bool ok = vs != nullptr;
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
const bool msaa_entry = i == 0 && msaa;
|
||||
const bool hdr_entry = (i == 1 || i == 3) && g_r.hdr_active;
|
||||
ps[i] = device->CreateShader(MakeShaderDesc(
|
||||
nrhi::ShaderStage::kPixel, "ssao.hlsl", kSsaoShaderSource,
|
||||
ps_entries[i], msaa_entry ? msaa_defs : nullptr,
|
||||
msaa_entry ? "AO_MSAA=1" : ""));
|
||||
ps_entries[i],
|
||||
msaa_entry ? msaa_defs : (hdr_entry ? hdr_defs : nullptr),
|
||||
msaa_entry ? "AO_MSAA=1" : (hdr_entry ? "HDR=1" : "")));
|
||||
ok = ok && ps[i] != nullptr;
|
||||
}
|
||||
if (!ok) {
|
||||
|
|
@ -13790,10 +13978,10 @@ bool EnsureSsaoPipeline(const NativeGuestOutputRenderContext& context) {
|
|||
pso.ps = ps[3];
|
||||
g_r.pso_ao_luma = device->CreateGraphicsPipeline(pso);
|
||||
// Composite: dst.rgb * src.a; the AO plane carries the finished
|
||||
// multiplier, and the RGB-only write mask keeps the output's 2-bit alpha
|
||||
// multiplier, and the RGB-only write mask keeps the output's alpha
|
||||
// untouched.
|
||||
pso.ps = ps[4];
|
||||
pso.rtv_format = context.guest_output->format();
|
||||
pso.rtv_format = ao_out_fmt;
|
||||
pso.blend.enable = true;
|
||||
pso.blend.src = nrhi::BlendFactor::kZero;
|
||||
pso.blend.dst = nrhi::BlendFactor::kSrcAlpha;
|
||||
|
|
@ -13816,8 +14004,10 @@ bool EnsureSsaoPipeline(const NativeGuestOutputRenderContext& context) {
|
|||
return fail("pso");
|
||||
}
|
||||
g_r.ao_msaa = g_r.msaa;
|
||||
g_r.ao_rtv_format = context.guest_output->format();
|
||||
REXLOG_INFO("native-scene: ssao pipeline created (MSAA x{})", g_r.msaa);
|
||||
g_r.ao_rtv_format = ao_out_fmt;
|
||||
g_r.ao_hdr = g_r.hdr_active;
|
||||
REXLOG_INFO("native-scene: ssao pipeline created (MSAA x{}, {})", g_r.msaa,
|
||||
g_r.hdr_active ? "HDR" : "classic");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -13838,8 +14028,16 @@ bool ApplySsaoPass(const NativeGuestOutputRenderContext& context,
|
|||
if (!EnsureSsaoPipeline(context)) {
|
||||
return false;
|
||||
}
|
||||
if (g_r.output_srv_slot == nullptr) {
|
||||
return false; // composite samples the guest output through this view
|
||||
// The luma pass samples the scene through this view; the composite writes
|
||||
// the same plane (HDR: the float scene plane pre-tonemap, classic: the
|
||||
// guest output).
|
||||
const bool hdr = g_r.hdr_active && g_r.hdr_resolved != nullptr;
|
||||
nrhi::Texture* const scene_plane =
|
||||
hdr ? g_r.hdr_resolved : context.guest_output;
|
||||
nrhi::TextureView* const scene_srv =
|
||||
hdr ? g_r.hdr_srv : g_r.output_srv_slot;
|
||||
if (scene_srv == nullptr) {
|
||||
return false;
|
||||
}
|
||||
nrhi::Device* device = context.device;
|
||||
const uint32_t width = context.guest_output_width;
|
||||
|
|
@ -13966,19 +14164,19 @@ bool ApplySsaoPass(const NativeGuestOutputRenderContext& context,
|
|||
cmd->FlushBarriers();
|
||||
|
||||
// 1b) Scene luminance -> ao_luma (AO raster): the sun-lit protection
|
||||
// mask. The guest output pauses as a sampled source for one draw.
|
||||
cmd->Barrier(context.guest_output, nrhi::ResourceState::kRenderTarget,
|
||||
// mask. The scene plane pauses as a sampled source for one draw.
|
||||
cmd->Barrier(scene_plane, nrhi::ResourceState::kRenderTarget,
|
||||
nrhi::ResourceState::kPixelShaderResource);
|
||||
cmd->FlushBarriers();
|
||||
cmd->SetViewport(ao_vp);
|
||||
cmd->SetScissor(ao_sc);
|
||||
cmd->SetRenderTargets(g_r.ao_luma, nullptr);
|
||||
cmd->SetPipeline(g_r.pso_ao_luma);
|
||||
cmd->SetTexture(1, g_r.output_srv_slot);
|
||||
cmd->SetTexture(1, scene_srv);
|
||||
cmd->Draw(3, 0);
|
||||
cmd->Barrier(g_r.ao_luma, nrhi::ResourceState::kRenderTarget,
|
||||
nrhi::ResourceState::kPixelShaderResource);
|
||||
cmd->Barrier(context.guest_output, nrhi::ResourceState::kPixelShaderResource,
|
||||
cmd->Barrier(scene_plane, nrhi::ResourceState::kPixelShaderResource,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
cmd->FlushBarriers();
|
||||
|
||||
|
|
@ -14015,20 +14213,28 @@ bool ApplySsaoPass(const NativeGuestOutputRenderContext& context,
|
|||
nrhi::ResourceState::kPixelShaderResource);
|
||||
cmd->FlushBarriers();
|
||||
|
||||
// 5) Composite onto the guest output (full res; s1 bilinearly upsamples
|
||||
// the finished multiplier plane). Debug replaces the frame with it.
|
||||
cmd->SetViewport(viewport);
|
||||
cmd->SetScissor(scissor);
|
||||
cmd->SetRenderTargets(context.guest_output, nullptr);
|
||||
cmd->SetPipeline(REXCVAR_GET(skate3_native_render_scene_ssao_debug)
|
||||
? g_r.pso_ao_debug
|
||||
: g_r.pso_ao_composite);
|
||||
cmd->SetTexture(1, g_r.ao_srv[0]);
|
||||
cmd->Draw(3, 0);
|
||||
// 5) Consume the finished multiplier plane. Classic: a full-res composite
|
||||
// (or debug replace) draw onto the scene plane, s1 bilinearly
|
||||
// upsampling. HDR: no draw at all: ps_tonemap samples the plane at t2
|
||||
// and multiplies pre-tonemap (identical math, one less full-res pass);
|
||||
// the plane stays in PIXEL_SHADER_RESOURCE for ApplyHdrPost, which
|
||||
// restores it.
|
||||
if (hdr) {
|
||||
g_r.ao_plane_in_psr = true;
|
||||
} else {
|
||||
cmd->SetViewport(viewport);
|
||||
cmd->SetScissor(scissor);
|
||||
cmd->SetRenderTargets(scene_plane, nullptr);
|
||||
cmd->SetPipeline(REXCVAR_GET(skate3_native_render_scene_ssao_debug)
|
||||
? g_r.pso_ao_debug
|
||||
: g_r.pso_ao_composite);
|
||||
cmd->SetTexture(1, g_r.ao_srv[0]);
|
||||
cmd->Draw(3, 0);
|
||||
cmd->Barrier(g_r.ao_tex[0], nrhi::ResourceState::kPixelShaderResource,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
}
|
||||
|
||||
// Restore intermediate steady states.
|
||||
cmd->Barrier(g_r.ao_tex[0], nrhi::ResourceState::kPixelShaderResource,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
cmd->Barrier(g_r.ao_tex[1], nrhi::ResourceState::kPixelShaderResource,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
cmd->Barrier(g_r.ao_lin_depth, nrhi::ResourceState::kPixelShaderResource,
|
||||
|
|
@ -14039,6 +14245,309 @@ bool ApplySsaoPass(const NativeGuestOutputRenderContext& context,
|
|||
return true;
|
||||
}
|
||||
|
||||
// ---- HDR post chain (hdr.hlsl: bloom pyramid + the extracted tonemap) ----
|
||||
// Layout + PSOs, built when the HDR path activates and rebuilt when the
|
||||
// guest output format (the tonemap target) changes. Failure falls back to
|
||||
// the classic in-material tonemap instead of killing the native renderer.
|
||||
bool EnsureHdrPipeline(const NativeGuestOutputRenderContext& context) {
|
||||
if (!g_r.hdr_active || g_r.hdr_failed) {
|
||||
return false;
|
||||
}
|
||||
if (g_r.pso_tonemap != nullptr &&
|
||||
g_r.hdr_pso_out_format == context.guest_output->format()) {
|
||||
return true;
|
||||
}
|
||||
nrhi::Device* device = context.device;
|
||||
const auto fail = [&](const char* what) {
|
||||
REXLOG_WARN(
|
||||
"native-scene: HDR pipeline setup failed ({}), falling back to the "
|
||||
"classic tonemap path",
|
||||
what);
|
||||
g_r.hdr_failed = true;
|
||||
return false;
|
||||
};
|
||||
if (g_r.hdr_layout == nullptr) {
|
||||
// The SSAO layout shape plus a third single-texture table (t2 = the
|
||||
// fused AO multiplier plane): root constants b0 + three single-texture
|
||||
// tables + point/linear clamp samplers (hdr.hlsl's frozen Vulkan plan).
|
||||
nrhi::BindingLayoutDesc ld;
|
||||
ld.param_count = 4;
|
||||
ld.params[0] = {nrhi::BindingParamKind::kConstants, /*b*/ 0, 16,
|
||||
nrhi::Visibility::kAll};
|
||||
ld.params[1] = {nrhi::BindingParamKind::kTextureTable, /*t*/ 0, 1,
|
||||
nrhi::Visibility::kPixel};
|
||||
ld.params[2] = {nrhi::BindingParamKind::kTextureTable, 1, 1,
|
||||
nrhi::Visibility::kPixel};
|
||||
ld.params[3] = {nrhi::BindingParamKind::kTextureTable, 2, 1,
|
||||
nrhi::Visibility::kPixel};
|
||||
ld.static_sampler_count = 2;
|
||||
ld.static_samplers[0] = {/*s*/ 0, nrhi::Filter::kPoint,
|
||||
nrhi::AddressMode::kClamp, 1};
|
||||
ld.static_samplers[1] = {1, nrhi::Filter::kLinear,
|
||||
nrhi::AddressMode::kClamp, 1};
|
||||
ld.allow_input_layout = false;
|
||||
g_r.hdr_layout = device->CreateBindingLayout(ld);
|
||||
if (g_r.hdr_layout == nullptr) {
|
||||
return fail("binding layout");
|
||||
}
|
||||
}
|
||||
for (nrhi::Pipeline** p : {&g_r.pso_bloom_first, &g_r.pso_bloom_down,
|
||||
&g_r.pso_bloom_up, &g_r.pso_tonemap}) {
|
||||
if (*p != nullptr) {
|
||||
device->DestroyDeferred(*p);
|
||||
*p = nullptr;
|
||||
}
|
||||
}
|
||||
const nrhi::ShaderMacro first_defs[] = {{"BLOOM_FIRST", "1"},
|
||||
{nullptr, nullptr}};
|
||||
nrhi::Shader* vs = device->CreateShader(
|
||||
MakeShaderDesc(nrhi::ShaderStage::kVertex, "hdr.hlsl", kHdrShaderSource,
|
||||
"vs_main", nullptr, ""));
|
||||
nrhi::Shader* ps_first = device->CreateShader(MakeShaderDesc(
|
||||
nrhi::ShaderStage::kPixel, "hdr.hlsl", kHdrShaderSource,
|
||||
"ps_bloom_down", first_defs, "BLOOM_FIRST=1"));
|
||||
nrhi::Shader* ps_down = device->CreateShader(
|
||||
MakeShaderDesc(nrhi::ShaderStage::kPixel, "hdr.hlsl", kHdrShaderSource,
|
||||
"ps_bloom_down", nullptr, ""));
|
||||
nrhi::Shader* ps_up = device->CreateShader(
|
||||
MakeShaderDesc(nrhi::ShaderStage::kPixel, "hdr.hlsl", kHdrShaderSource,
|
||||
"ps_bloom_up", nullptr, ""));
|
||||
nrhi::Shader* ps_tone = device->CreateShader(
|
||||
MakeShaderDesc(nrhi::ShaderStage::kPixel, "hdr.hlsl", kHdrShaderSource,
|
||||
"ps_tonemap", nullptr, ""));
|
||||
const bool shaders_ok = vs != nullptr && ps_first != nullptr &&
|
||||
ps_down != nullptr && ps_up != nullptr &&
|
||||
ps_tone != nullptr;
|
||||
if (!shaders_ok) {
|
||||
for (nrhi::Shader* s : {vs, ps_first, ps_down, ps_up, ps_tone}) {
|
||||
device->DestroyDeferred(s);
|
||||
}
|
||||
return fail("shader compile");
|
||||
}
|
||||
nrhi::GraphicsPipelineDesc pso;
|
||||
pso.layout = g_r.hdr_layout;
|
||||
pso.vs = vs;
|
||||
pso.cull = nrhi::CullMode::kNone;
|
||||
pso.sample_count = 1;
|
||||
pso.rtv_format = nrhi::Format::kR16G16B16A16_FLOAT;
|
||||
pso.ps = ps_first;
|
||||
g_r.pso_bloom_first = device->CreateGraphicsPipeline(pso);
|
||||
pso.ps = ps_down;
|
||||
g_r.pso_bloom_down = device->CreateGraphicsPipeline(pso);
|
||||
// Progressive upsample: additive ONE/ONE onto the next-larger level.
|
||||
pso.ps = ps_up;
|
||||
pso.blend.enable = true;
|
||||
pso.blend.src = nrhi::BlendFactor::kOne;
|
||||
pso.blend.dst = nrhi::BlendFactor::kOne;
|
||||
pso.blend.op = nrhi::BlendOp::kAdd;
|
||||
pso.blend.src_alpha = nrhi::BlendFactor::kOne;
|
||||
pso.blend.dst_alpha = nrhi::BlendFactor::kOne;
|
||||
pso.blend.op_alpha = nrhi::BlendOp::kAdd;
|
||||
g_r.pso_bloom_up = device->CreateGraphicsPipeline(pso);
|
||||
pso.blend = {};
|
||||
pso.ps = ps_tone;
|
||||
pso.rtv_format = context.guest_output->format();
|
||||
g_r.pso_tonemap = device->CreateGraphicsPipeline(pso);
|
||||
for (nrhi::Shader* s : {vs, ps_first, ps_down, ps_up, ps_tone}) {
|
||||
device->DestroyDeferred(s);
|
||||
}
|
||||
if (g_r.pso_bloom_first == nullptr || g_r.pso_bloom_down == nullptr ||
|
||||
g_r.pso_bloom_up == nullptr || g_r.pso_tonemap == nullptr) {
|
||||
return fail("pso");
|
||||
}
|
||||
g_r.hdr_pso_out_format = context.guest_output->format();
|
||||
REXLOG_INFO("native-scene: HDR post pipeline created");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bloom pyramid + tonemap over the float scene plane into the guest output.
|
||||
// Runs after the AO composite (bloom sees the occluded scene). Transitions
|
||||
// the guest output kGuestOutput -> kRenderTarget (the state every later
|
||||
// consumer expects); the caller restores the main binding layout after.
|
||||
void ApplyHdrPost(const NativeGuestOutputRenderContext& context,
|
||||
nrhi::Cmd* cmd, const nrhi::Viewport& viewport,
|
||||
const nrhi::Rect& scissor, bool loading_native) {
|
||||
nrhi::Device* device = context.device;
|
||||
const uint32_t width = context.guest_output_width;
|
||||
const uint32_t height = context.guest_output_height;
|
||||
// Bloom chain intermediates: QUARTER res halving down to the level cap or
|
||||
// an 8-px floor, RGBA16F, steady state RENDER_TARGET. Quarter start =
|
||||
// 1/4 the pyramid cost of a half-res chain; the extraction pass widens
|
||||
// its 13-tap spread (p1.x = 2) so the 4x reduction still integrates the
|
||||
// whole source block. Creation failure only loses bloom (the tonemap
|
||||
// still runs).
|
||||
if (g_r.bloom_base_w != width || g_r.bloom_base_h != height ||
|
||||
(g_r.bloom_levels > 0 && g_r.bloom_tex[0] == nullptr)) {
|
||||
for (uint32_t i = 0; i < RendererState::kBloomMaxLevels; ++i) {
|
||||
if (g_r.bloom_srv[i] != nullptr) {
|
||||
device->DestroyDeferred(g_r.bloom_srv[i]);
|
||||
g_r.bloom_srv[i] = nullptr;
|
||||
}
|
||||
if (g_r.bloom_tex[i] != nullptr) {
|
||||
device->DestroyDeferred(g_r.bloom_tex[i]);
|
||||
g_r.bloom_tex[i] = nullptr;
|
||||
}
|
||||
}
|
||||
g_r.bloom_levels = 0;
|
||||
uint32_t w = std::max(1u, (width + 1) / 2);
|
||||
uint32_t h = std::max(1u, (height + 1) / 2);
|
||||
for (uint32_t i = 0; i < RendererState::kBloomMaxLevels; ++i) {
|
||||
w = std::max(1u, (w + 1) / 2);
|
||||
h = std::max(1u, (h + 1) / 2);
|
||||
if (w < 8 || h < 8) {
|
||||
break;
|
||||
}
|
||||
nrhi::TextureDesc desc;
|
||||
desc.width = w;
|
||||
desc.height = h;
|
||||
desc.mip_levels = 1;
|
||||
desc.format = nrhi::Format::kR16G16B16A16_FLOAT;
|
||||
desc.usage = nrhi::kTextureUsageRenderTarget;
|
||||
desc.initial_state = nrhi::ResourceState::kRenderTarget;
|
||||
g_r.bloom_tex[i] = device->CreateTexture(desc);
|
||||
if (g_r.bloom_tex[i] == nullptr) {
|
||||
break;
|
||||
}
|
||||
nrhi::TextureViewDesc vd;
|
||||
vd.mip_levels = 1;
|
||||
g_r.bloom_srv[i] = device->CreateTextureView(g_r.bloom_tex[i], vd);
|
||||
if (g_r.bloom_srv[i] == nullptr) {
|
||||
device->DestroyDeferred(g_r.bloom_tex[i]);
|
||||
g_r.bloom_tex[i] = nullptr;
|
||||
break;
|
||||
}
|
||||
g_r.bloom_w[i] = w;
|
||||
g_r.bloom_h[i] = h;
|
||||
++g_r.bloom_levels;
|
||||
}
|
||||
g_r.bloom_base_w = width;
|
||||
g_r.bloom_base_h = height;
|
||||
if (g_r.bloom_levels == 0) {
|
||||
REXLOG_WARN("native-scene: bloom chain allocation failed (bloom off)");
|
||||
}
|
||||
}
|
||||
const bool bloom = REXCVAR_GET(skate3_native_render_scene_bloom) &&
|
||||
!loading_native && g_r.bloom_levels > 0 &&
|
||||
g_r.pso_bloom_first != nullptr &&
|
||||
g_r.pso_bloom_down != nullptr &&
|
||||
g_r.pso_bloom_up != nullptr;
|
||||
|
||||
cmd->SetBindingLayout(g_r.hdr_layout);
|
||||
cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList);
|
||||
// The scene plane is the sampled source for the whole chain.
|
||||
cmd->Barrier(g_r.hdr_resolved, nrhi::ResourceState::kRenderTarget,
|
||||
nrhi::ResourceState::kPixelShaderResource);
|
||||
cmd->FlushBarriers();
|
||||
|
||||
float c[16];
|
||||
const auto set_consts = [&](uint32_t dw, uint32_t dh, uint32_t sw,
|
||||
uint32_t sh, float intensity, float tap_scale) {
|
||||
c[0] = float(dw);
|
||||
c[1] = float(dh);
|
||||
c[2] = 1.0f / float(dw);
|
||||
c[3] = 1.0f / float(dh);
|
||||
c[4] = float(sw);
|
||||
c[5] = float(sh);
|
||||
c[6] = 1.0f / float(sw);
|
||||
c[7] = 1.0f / float(sh);
|
||||
c[8] = float(REXCVAR_GET(skate3_native_render_scene_bloom_threshold));
|
||||
c[9] = float(REXCVAR_GET(skate3_native_render_scene_bloom_knee));
|
||||
c[10] = intensity;
|
||||
// Debug view (ps_tonemap p0.w); the classic ssao_debug cvar maps onto
|
||||
// the fused AO view when the plane rode along this frame.
|
||||
c[11] = float(REXCVAR_GET(skate3_native_render_scene_hdr_debug));
|
||||
if (c[11] == 0.0f && g_r.ao_plane_in_psr &&
|
||||
REXCVAR_GET(skate3_native_render_scene_ssao_debug)) {
|
||||
c[11] = 3.0f;
|
||||
}
|
||||
c[12] = tap_scale;
|
||||
c[13] = c[14] = c[15] = 0.0f;
|
||||
cmd->SetRootConstants(0, 16, c, 0);
|
||||
};
|
||||
|
||||
if (bloom) {
|
||||
// Downsample chain (level 0 extracts with threshold + Karis weighting;
|
||||
// its 4x reduction doubles the tap spread to cover the source block).
|
||||
for (uint32_t i = 0; i < g_r.bloom_levels; ++i) {
|
||||
const uint32_t sw = i == 0 ? width : g_r.bloom_w[i - 1];
|
||||
const uint32_t sh = i == 0 ? height : g_r.bloom_h[i - 1];
|
||||
set_consts(g_r.bloom_w[i], g_r.bloom_h[i], sw, sh, 0.0f,
|
||||
i == 0 ? 2.0f : 1.0f);
|
||||
const nrhi::Viewport vp{0.0f, 0.0f, float(g_r.bloom_w[i]),
|
||||
float(g_r.bloom_h[i]), 0.0f, 1.0f};
|
||||
const nrhi::Rect sc{0, 0, int32_t(g_r.bloom_w[i]),
|
||||
int32_t(g_r.bloom_h[i])};
|
||||
cmd->SetViewport(vp);
|
||||
cmd->SetScissor(sc);
|
||||
cmd->SetRenderTargets(g_r.bloom_tex[i], nullptr);
|
||||
cmd->SetPipeline(i == 0 ? g_r.pso_bloom_first : g_r.pso_bloom_down);
|
||||
cmd->SetTexture(1, i == 0 ? g_r.hdr_srv : g_r.bloom_srv[i - 1]);
|
||||
cmd->Draw(3, 0);
|
||||
cmd->Barrier(g_r.bloom_tex[i], nrhi::ResourceState::kRenderTarget,
|
||||
nrhi::ResourceState::kPixelShaderResource);
|
||||
cmd->FlushBarriers();
|
||||
}
|
||||
// Progressive tent upsample, additive onto each larger level.
|
||||
for (int32_t i = int32_t(g_r.bloom_levels) - 2; i >= 0; --i) {
|
||||
cmd->Barrier(g_r.bloom_tex[i], nrhi::ResourceState::kPixelShaderResource,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
cmd->FlushBarriers();
|
||||
set_consts(g_r.bloom_w[i], g_r.bloom_h[i], g_r.bloom_w[i + 1],
|
||||
g_r.bloom_h[i + 1], 0.0f, 1.0f);
|
||||
const nrhi::Viewport vp{0.0f, 0.0f, float(g_r.bloom_w[i]),
|
||||
float(g_r.bloom_h[i]), 0.0f, 1.0f};
|
||||
const nrhi::Rect sc{0, 0, int32_t(g_r.bloom_w[i]),
|
||||
int32_t(g_r.bloom_h[i])};
|
||||
cmd->SetViewport(vp);
|
||||
cmd->SetScissor(sc);
|
||||
cmd->SetRenderTargets(g_r.bloom_tex[i], nullptr);
|
||||
cmd->SetPipeline(g_r.pso_bloom_up);
|
||||
cmd->SetTexture(1, g_r.bloom_srv[i + 1]);
|
||||
cmd->Draw(3, 0);
|
||||
cmd->Barrier(g_r.bloom_tex[i], nrhi::ResourceState::kRenderTarget,
|
||||
nrhi::ResourceState::kPixelShaderResource);
|
||||
cmd->FlushBarriers();
|
||||
}
|
||||
}
|
||||
|
||||
// Tonemap into the guest output (the single application of the game's
|
||||
// shared tone chain; bloom energy joins pre-tonemap).
|
||||
cmd->Barrier(context.guest_output, nrhi::ResourceState::kGuestOutput,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
cmd->FlushBarriers();
|
||||
set_consts(width, height, width, height,
|
||||
bloom ? float(REXCVAR_GET(
|
||||
skate3_native_render_scene_bloom_intensity))
|
||||
: 0.0f,
|
||||
1.0f);
|
||||
cmd->SetViewport(viewport);
|
||||
cmd->SetScissor(scissor);
|
||||
cmd->SetRenderTargets(context.guest_output, nullptr);
|
||||
cmd->SetPipeline(g_r.pso_tonemap);
|
||||
cmd->SetTexture(1, g_r.hdr_srv);
|
||||
cmd->SetTexture(2, bloom ? g_r.bloom_srv[0] : g_r.white.srv);
|
||||
// t2 = the fused AO multiplier plane (white = no AO this frame).
|
||||
cmd->SetTexture(3, g_r.ao_plane_in_psr ? g_r.ao_srv[0] : g_r.white.srv);
|
||||
cmd->Draw(3, 0);
|
||||
|
||||
// Restore steady states.
|
||||
cmd->Barrier(g_r.hdr_resolved, nrhi::ResourceState::kPixelShaderResource,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
if (g_r.ao_plane_in_psr) {
|
||||
cmd->Barrier(g_r.ao_tex[0], nrhi::ResourceState::kPixelShaderResource,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
g_r.ao_plane_in_psr = false;
|
||||
}
|
||||
if (bloom) {
|
||||
for (uint32_t i = 0; i < g_r.bloom_levels; ++i) {
|
||||
cmd->Barrier(g_r.bloom_tex[i],
|
||||
nrhi::ResourceState::kPixelShaderResource,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
}
|
||||
}
|
||||
cmd->FlushBarriers();
|
||||
}
|
||||
|
||||
bool EnsurePipeline(const NativeGuestOutputRenderContext& context) {
|
||||
if (g_r.failed) return false;
|
||||
nrhi::Device* device = context.device;
|
||||
|
|
@ -14048,14 +14557,29 @@ bool EnsurePipeline(const NativeGuestOutputRenderContext& context) {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!g_r.pso || g_r.rtv_format != context.guest_output->format()) {
|
||||
// HDR path decision, latched for the whole pipeline family (scene/spline
|
||||
// PSO formats + shader variants, resolve target, AO composite target,
|
||||
// output-sized targets). Hot cvar toggles rebuild everything below;
|
||||
// hdr_failed pins the classic path after a float-target failure.
|
||||
const bool hdr_want =
|
||||
REXCVAR_GET(skate3_native_render_scene_hdr) && !g_r.hdr_failed;
|
||||
const nrhi::Format hdr_fmt_want =
|
||||
REXCVAR_GET(skate3_native_render_scene_hdr_packed)
|
||||
? nrhi::Format::kR11G11B10_FLOAT
|
||||
: nrhi::Format::kR16G16B16A16_FLOAT;
|
||||
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)) {
|
||||
g_r.hdr_active = hdr_want;
|
||||
g_r.hdr_scene_format = hdr_fmt_want;
|
||||
if (!EnsureScenePsoFamily(context) || !EnsureResolvePso(context) ||
|
||||
!EnsureBlurPsos(context) || !EnsureOutlineEdgePso(context) ||
|
||||
!Ensure2dPso(context) || !EnsureSplinePsos(context) ||
|
||||
!EnsureShadowPsos(context)) {
|
||||
return false;
|
||||
}
|
||||
REXLOG_INFO("native-scene: pipelines created (MSAA x{})", g_r.msaa);
|
||||
REXLOG_INFO("native-scene: pipelines created (MSAA x{}, {})", g_r.msaa,
|
||||
g_r.hdr_active ? "HDR" : "classic");
|
||||
g_r.rtv_format = context.guest_output->format();
|
||||
}
|
||||
|
||||
|
|
@ -14066,6 +14590,11 @@ bool EnsurePipeline(const NativeGuestOutputRenderContext& context) {
|
|||
if (!EnsureFallbackTextures(context) || !EnsureOutputSizedTargets(context)) {
|
||||
return false;
|
||||
}
|
||||
if (g_r.hdr_active && !EnsureHdrPipeline(context)) {
|
||||
// hdr_failed is set: the next frame rebuilds the classic path. Abort
|
||||
// this frame (the scene PSOs already target the float format).
|
||||
return false;
|
||||
}
|
||||
|
||||
if (g_r.rtv_resource != context.guest_output) {
|
||||
// The presenter recreated the output image (resize). Render-target
|
||||
|
|
@ -16979,11 +17508,17 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
s_sh_h1 = sh_h;
|
||||
}
|
||||
|
||||
// The scene draws into the MSAA target when enabled (resolved into the
|
||||
// guest output at the end of the pass), or straight into the guest output.
|
||||
// The scene draws into the MSAA target when enabled (resolved into the 1x
|
||||
// scene plane at the end of the pass), or straight into the 1x plane. The
|
||||
// 1x plane is the float HDR intermediate when the HDR post chain is live
|
||||
// (ps_tonemap then writes the guest output), else the guest output itself.
|
||||
const bool hdr_on = g_r.hdr_active && g_r.hdr_resolved != nullptr &&
|
||||
g_r.hdr_srv != nullptr && g_r.pso_tonemap != nullptr;
|
||||
const bool msaa_on = g_r.msaa > 1 && g_r.msaa_color != nullptr && g_r.resolve_pso != nullptr;
|
||||
nrhi::Texture* scene_color = msaa_on ? g_r.msaa_color : context.guest_output;
|
||||
if (!msaa_on) {
|
||||
nrhi::Texture* scene_color =
|
||||
msaa_on ? g_r.msaa_color
|
||||
: (hdr_on ? g_r.hdr_resolved : context.guest_output);
|
||||
if (!msaa_on && !hdr_on) {
|
||||
cmd->Barrier(context.guest_output, nrhi::ResourceState::kGuestOutput,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
cmd->FlushBarriers();
|
||||
|
|
@ -16993,9 +17528,22 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
// Loading frames clear to black (the game's loading UI composes over
|
||||
// black); real scenes keep the sky-ish debug clear that shows through
|
||||
// undecoded holes.
|
||||
const float clear_color[4] = {loading_native ? 0.0f : 0.25f,
|
||||
loading_native ? 0.0f : 0.35f,
|
||||
loading_native ? 0.0f : 0.55f, 1.0f};
|
||||
float clear_color[4] = {loading_native ? 0.0f : 0.25f,
|
||||
loading_native ? 0.0f : 0.35f,
|
||||
loading_native ? 0.0f : 0.55f, 1.0f};
|
||||
if (hdr_on) {
|
||||
// The clear is authored in the final gamma space; the HDR plane holds
|
||||
// pre-tonemap values, so encode through the tone chain's inverse (the
|
||||
// same transform as scene.hlsl PassGamma) so undecoded holes keep the
|
||||
// same debug color after the tonemap pass.
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const float tm =
|
||||
clear_color[k] * clear_color[k] * (2.0f / (1.41f * 1.41f));
|
||||
clear_color[k] = tm > 1.0f
|
||||
? tm * 4.0f - 3.0f
|
||||
: 1.0f - std::sqrt(std::max(1.0f - tm, 0.0f));
|
||||
}
|
||||
}
|
||||
cmd->ClearRenderTarget(scene_color, clear_color);
|
||||
if (use_depth) {
|
||||
cmd->ClearDepth(g_r.depth, 1.0f);
|
||||
|
|
@ -18897,14 +19445,18 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
g_r.depth, use_depth);
|
||||
|
||||
if (msaa_on) {
|
||||
// Resolve: average the MSAA samples into the guest output with a
|
||||
// fullscreen pass, then restore steady-state resource states.
|
||||
// Resolve: average the MSAA samples into the 1x scene plane (the float
|
||||
// HDR plane, or the guest output on the classic path) with a fullscreen
|
||||
// pass, then restore steady-state resource states.
|
||||
cmd->Barrier(g_r.msaa_color, nrhi::ResourceState::kRenderTarget,
|
||||
nrhi::ResourceState::kPixelShaderResource);
|
||||
cmd->Barrier(context.guest_output, nrhi::ResourceState::kGuestOutput,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
if (!hdr_on) {
|
||||
cmd->Barrier(context.guest_output, nrhi::ResourceState::kGuestOutput,
|
||||
nrhi::ResourceState::kRenderTarget);
|
||||
}
|
||||
cmd->FlushBarriers();
|
||||
cmd->SetRenderTargets(context.guest_output, nullptr);
|
||||
cmd->SetRenderTargets(hdr_on ? g_r.hdr_resolved : context.guest_output,
|
||||
nullptr);
|
||||
cmd->SetPipeline(g_r.resolve_pso);
|
||||
cmd->SetTexture(1, g_r.msaa_srv_slot);
|
||||
cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList);
|
||||
|
|
@ -18913,21 +19465,40 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
nrhi::ResourceState::kRenderTarget);
|
||||
}
|
||||
|
||||
if (outline_ready) {
|
||||
// Classic path: the outline composites straight onto the resolved guest
|
||||
// output. Under HDR it runs after the tonemap below (a gamma-space
|
||||
// screen overlay, not scene lighting).
|
||||
if (outline_ready && !hdr_on) {
|
||||
RenderOutlineComposite(context, scene, context.guest_output, viewport,
|
||||
scissor);
|
||||
}
|
||||
|
||||
// ---- Screen-space ambient occlusion (ssao.hlsl: GTAO) ----
|
||||
// Multiplies the resolved scene by a horizon-based visibility term before
|
||||
// any downstream consumer (photo chain, 2D overlay, blur). Runs after the
|
||||
// outline composite, which relies on the main layout latched from the
|
||||
// scene pass.
|
||||
// any downstream consumer (bloom/tonemap under HDR, photo chain, 2D
|
||||
// overlay, blur). Runs after the outline composite, which relies on the
|
||||
// main layout latched from the scene pass.
|
||||
bool post_ran = false;
|
||||
if (use_depth && !loading_native &&
|
||||
REXCVAR_GET(skate3_native_render_scene_ssao) &&
|
||||
ApplySsaoPass(context, cmd, scene, viewport, scissor)) {
|
||||
// The AO chain switched binding layouts; restore the main-pass root
|
||||
// bindings the later passes latch (same restore as the photo chain's).
|
||||
post_ran = true;
|
||||
}
|
||||
|
||||
// ---- HDR post (hdr.hlsl: bloom pyramid + the extracted tonemap) ----
|
||||
// Bloom reads the AO-composited float scene; ps_tonemap applies the
|
||||
// game's shared tone chain once into the guest output, which every later
|
||||
// consumer (photo chain, 2D overlay, blur, grab) reads exactly as on the
|
||||
// classic path.
|
||||
if (hdr_on) {
|
||||
ApplyHdrPost(context, cmd, viewport, scissor, loading_native);
|
||||
post_ran = true;
|
||||
}
|
||||
|
||||
if (post_ran) {
|
||||
// The AO/HDR chains switched binding layouts; restore the main-pass
|
||||
// root bindings the later passes latch (same restore as the photo
|
||||
// chain's).
|
||||
cmd->SetViewport(viewport);
|
||||
cmd->SetScissor(scissor);
|
||||
cmd->SetBindingLayout(g_r.layout);
|
||||
|
|
@ -18940,6 +19511,11 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
}
|
||||
}
|
||||
|
||||
if (outline_ready && hdr_on) {
|
||||
RenderOutlineComposite(context, scene, context.guest_output, viewport,
|
||||
scissor);
|
||||
}
|
||||
|
||||
// ---- Photo-editor postfx chain (photo_fx.hlsl: exact ucode ports) ----
|
||||
// While the photo-mission photo editor is up and every pass's live
|
||||
// constants were captured this frame, apply the game's own chain over the
|
||||
|
|
|
|||
2
third_party/rexglue-sdk
vendored
2
third_party/rexglue-sdk
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 7f6a5b5b7c846616c19cd2a9efac586de6175bd6
|
||||
Subproject commit a8e28405f9bd56924b494012166212189c8948aa
|
||||
Loading…
Add table
Reference in a new issue