render: move HLSL to source files, diagnostics TU, staged RenderScene

This commit is contained in:
mchughalex 2026-07-11 10:58:36 +01:00
parent df6156d14e
commit 406f47c79c
14 changed files with 3448 additions and 2973 deletions

View file

@ -227,6 +227,8 @@ set(SKATE3_COMMON_SOURCES
src/skate3_demo_path.cpp src/skate3_demo_path.cpp
src/skate3_fov.cpp src/skate3_fov.cpp
src/skate3_iso_installer.cpp src/skate3_iso_installer.cpp
src/native/skate3_native_diagnostics.cpp
src/native/skate3_native_guest_read.cpp
src/skate3_native_debug_dialog.cpp src/skate3_native_debug_dialog.cpp
src/skate3_native_render.cpp src/skate3_native_render.cpp
src/skate3_native_scene.cpp src/skate3_native_scene.cpp
@ -264,6 +266,29 @@ target_include_directories(skate3 PRIVATE
${CMAKE_CURRENT_BINARY_DIR}/include ${CMAKE_CURRENT_BINARY_DIR}/include
${REXSDK_DIR}/thirdparty/xxHash ${REXSDK_DIR}/thirdparty/xxHash
) )
set(SKATE3_NATIVE_SHADER_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/scene.hlsl
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/shadow_blur.hlsl
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/resolve.hlsl
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/blur.hlsl
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/outline.hlsl
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/overlay2d.hlsl
${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders/spline.hlsl
)
set(SKATE3_NATIVE_SHADER_HEADER
${CMAKE_CURRENT_BINARY_DIR}/include/skate3_native_shaders.h)
add_custom_command(
OUTPUT ${SKATE3_NATIVE_SHADER_HEADER}
COMMAND ${CMAKE_COMMAND}
-DSHADER_DIR=${CMAKE_CURRENT_SOURCE_DIR}/src/native/shaders
-DOUT=${SKATE3_NATIVE_SHADER_HEADER}
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/EmbedShaders.cmake
DEPENDS ${SKATE3_NATIVE_SHADER_SOURCES}
${CMAKE_CURRENT_SOURCE_DIR}/cmake/EmbedShaders.cmake
COMMENT "Embedding native renderer shaders"
VERBATIM)
target_sources(skate3 PRIVATE ${SKATE3_NATIVE_SHADER_HEADER})
target_compile_definitions(skate3 PRIVATE SKATE3_BUILD_CONFIG="$<CONFIG>") target_compile_definitions(skate3 PRIVATE SKATE3_BUILD_CONFIG="$<CONFIG>")
if(NOT SKATE3_EFFECTIVE_TITLE_UPDATE_PACKAGE STREQUAL "") if(NOT SKATE3_EFFECTIVE_TITLE_UPDATE_PACKAGE STREQUAL "")
target_compile_definitions(skate3 PRIVATE SKATE3_HAS_TITLE_UPDATE=1) target_compile_definitions(skate3 PRIVATE SKATE3_HAS_TITLE_UPDATE=1)

41
cmake/EmbedShaders.cmake Normal file
View file

@ -0,0 +1,41 @@
# Embeds the native renderer HLSL sources (src/native/shaders/*.hlsl) into a
# generated C++ header so the exe stays self-contained. Run as a script:
# cmake -DSHADER_DIR=<dir> -DOUT=<header> -P EmbedShaders.cmake
# Each shader becomes `inline constexpr char <var>[] = R"__hlsl__(...)__hlsl__";`
# with a leading newline, matching the original in-cpp raw string literals
# (consumers use sizeof(kX) - 1).
set(SHADER_VARS
kShaderSource
kShadowBlurSource
kResolveShaderSource
kBlurShaderSource
kOutlineShaderSource
kShader2dSource
kShaderSplineSource
)
set(SHADER_FILES
scene.hlsl
shadow_blur.hlsl
resolve.hlsl
blur.hlsl
outline.hlsl
overlay2d.hlsl
spline.hlsl
)
set(content "// Generated by cmake/EmbedShaders.cmake from src/native/shaders/*.hlsl.\n")
string(APPEND content "// DO NOT EDIT - edit the .hlsl files instead.\n")
string(APPEND content "#pragma once\n\n")
foreach(var file IN ZIP_LISTS SHADER_VARS SHADER_FILES)
file(READ "${SHADER_DIR}/${file}" src)
string(FIND "${src}" ")__hlsl__" collision)
if(NOT collision EQUAL -1)
message(FATAL_ERROR "${file} contains the raw-string delimiter )__hlsl__")
endif()
string(APPEND content "// ${file}\n")
string(APPEND content "inline constexpr char ${var}[] = R\"__hlsl__(\n${src})__hlsl__\";\n\n")
endforeach()
file(WRITE "${OUT}" "${content}")

View file

@ -0,0 +1,59 @@
// Popup background blur: EXACT port of the game's dedicated pass chain
// (captured from a live frame with the Rewards popup up):
// blur_hBlurPS: 11 gaussian taps along +X over the finished frame,
// offsets k * 0.0003125 * PS c0.x (c0.x = 8 -> 0.0025/tap,
// +/-1.25% of the buffer), literal weights below (sum 1);
// blur_vBlurPS: the same kernel along +Y;
// postfx_basictex: plain fullscreen REPLACE of the frame with the result.
// The game runs it at its fixed 1152x640 internal resolution and the
// console's bilinear upscale of that buffer is what reads as the "frosted
// glass" lattice, so the passes here render into 1152x640 intermediates and
// stretch back, reproducing both the kernel and the lattice.
cbuffer C : register(b0) {
float4 dir; // xy = blur axis, z = kernel scale (the game's PS c0.x, 8)
};
Texture2D<float4> src : register(t0);
SamplerState smp_clamp : 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;
}
float4 ps_main(VSOut i) : SV_Target {
// Literal kernel from the blur_hBlurPS ucode (c251..c255).
const float w[6] = {0.2005654, 0.1769984, 0.1216491, 0.0651141,
0.0271436, 0.0088122};
float2 step = dir.xy * (0.0003125 * dir.z);
float3 c = src.SampleLevel(smp_clamp, i.uv, 0).rgb * w[0];
[unroll] for (int k = 1; k < 6; ++k) {
c += src.SampleLevel(smp_clamp, i.uv + step * k, 0).rgb * w[k];
c += src.SampleLevel(smp_clamp, i.uv - step * k, 0).rgb * w[k];
}
return float4(c, 1.0);
}
// Prefiltered downsample of the (higher-res) native output into the game's
// 1152x640 blur space (dir.xy = source texel size). A 4x4 grid of bilinear
// taps covers reduction ratios up to ~4x (4K -> 1152 is 3.33x); narrower
// footprints undersampled the scale and the aliased detail crawled as the
// scene animated: the whole blurred backdrop shimmered, worst around
// high-contrast edges.
float4 ps_down(VSOut i) : SV_Target {
float3 c = 0.0;
[unroll] for (int y = 0; y < 4; ++y) {
[unroll] for (int x = 0; x < 4; ++x) {
float2 off = float2(float(x) - 1.5, float(y) - 1.5) * dir.xy;
c += src.SampleLevel(smp_clamp, i.uv + off, 0).rgb;
}
}
return float4(c / 16.0, 1.0);
}
// postfx_basictex: oC0 = tex (verified: `max oC0, r0, r0`).
float4 ps_blit(VSOut i) : SV_Target {
return float4(src.SampleLevel(smp_clamp, i.uv, 0).rgb, 1.0);
}

View file

@ -0,0 +1,46 @@
// Selected-object outline composite (park editor / object mover): port of
// postfx_edgedetectstencilPS. The game stencil-marks the selected object,
// resolves stencil to a 576x320 texture and adds |threshold crossings| x
// the outline color (PS c0, the editor blue) onto the frame; our mask pass
// renders the selected items into a small R8 target instead (see
// pso_outline_mask) and this pass composites additively over the resolved
// output. Tap offsets are 1/576 x 1/320 UV fractions like the original, so
// the contour thickness matches at any output resolution.
cbuffer C : register(b0) {
float4 color; // rgb = outline color (guest edge-detect PS c0)
};
Texture2D<float4> src : register(t0);
SamplerState smp_clamp : 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;
}
float4 ps_main(VSOut i) : SV_Target {
// Averaged 5x5 neighborhood of the (binary, full-resolution) mask with a
// fixed UV-fraction pitch -> a smooth 0..1 ramp across the silhouette
// boundary; the peaked profile 4m(1-m) turns the ramp into an
// antialiased line of constant screen-fraction width. The core is
// whitened (the emulated line goes through the game's uber grade +
// bloom, which lifts its center toward white). A literal port:
// binarized crossing counts on the 576x320 grid, stairstepped visibly
// at native output resolutions.
const float2 t = float2(0.8 / 1152.0, 0.8 / 640.0);
float m = 0.0;
[unroll] for (int y = -2; y <= 2; ++y) {
[unroll] for (int x = -2; x <= 2; ++x) {
m += src.SampleLevel(smp_clamp, i.uv + float2(x, y) * t, 0).r;
}
}
m /= 25.0;
float band = saturate(6.4 * m * (1.0 - m));
float core = band * band * band;
// Additive blend (ONE, ONE) onto the frame; alpha untouched (ZERO, ONE).
return float4(color.rgb * band + 0.5 * core, 0.0);
}

View file

@ -0,0 +1,36 @@
// 2D/APT overlay shader. Verified against a captured HUD draw stream:
// vertices are {float4 pos, float2 uv} in 1280x720
// APT movie space; the game's VS constants apply as clip = ortho * (world *
// pos) with c0..c3 the ortho rows, c4..c7 the element's transform rows and
// c8 the color multiplier, used exactly as staged.
cbuffer C : register(b0) {
float4 m[10]; // m[0..3] proj rows, m[4..7] world rows, m[8] color,
// m[9].x = apply D3D9 half-pixel (2D ortho draws only)
};
Texture2D<float4> tex : register(t0);
SamplerState smp : register(s1);
struct VSOut {
float4 pos : SV_Position;
float2 uv : TEXCOORD0;
float4 color : COLOR0;
};
VSOut vs_main(float4 p : POSITION, float2 uv : TEXCOORD0, float4 color : COLOR0) {
float4 wp = float4(dot(p, m[4]), dot(p, m[5]), dot(p, m[6]), dot(p, m[7]));
VSOut o;
o.pos = float4(dot(wp, m[0]), dot(wp, m[1]), dot(wp, m[2]), dot(wp, m[3]));
// D3D9 half-pixel convention: the art bakes half-texel UVs expecting
// pixel centers at integer coordinates; without this the clock-face
// quadrant tiles show their wrapped border rows as dark seam lines.
// Scaled for the 2D ortho; must not apply to 3D (world-space
// SimpleDraw markers), where m[0].x is a projection scale.
if (m[9].x > 0.0) {
o.pos.x -= 0.5 * m[0].x * o.pos.w;
o.pos.y -= 0.5 * m[1].y * o.pos.w;
}
o.uv = uv;
o.color = color;
return o;
}
float4 ps_main(VSOut i) : SV_Target {
return tex.Sample(smp, i.uv) * m[8] * i.color;
}

View file

@ -0,0 +1,16 @@
// Fullscreen MSAA resolve: the deferred command list has no
// ResolveSubresource, so average the samples in a pixel shader (SAMPLES is
// injected as a compile-time macro).
Texture2DMS<float4> src : register(t0);
float4 vs_main(uint id : SV_VertexID) : SV_Position {
float2 uv = float2((id << 1) & 2, id & 2);
return float4(uv * float2(2, -2) + float2(-1, 1), 0, 1);
}
float4 ps_main(float4 pos : SV_Position) : SV_Target {
int2 p = int2(pos.xy);
float4 c = 0;
[unroll] for (int k = 0; k < SAMPLES; ++k) {
c += src.Load(p, k);
}
return c / SAMPLES;
}

View file

@ -0,0 +1,816 @@
// Face-normal shading uses the camera-relative world position: interpolating
// absolute world coordinates (hundreds of meters) destroys ddx/ddy precision
// and produces per-pixel noise.
cbuffer C : register(b0) {
row_major float4x4 world;
row_major float4x4 mvp;
float4 tint;
float4 cam_pos;
// Per-material color multiplier (w > 0 enables): CAS hair is a grayscale
// texture times the character's hair color.
float4 mat_tint;
// x = macroOverlayUVScale, y = macroOverlayOpacity, z > 0 = macro overlay
// texture bound at t3, w > 0 = environment.decal item with its `decal`
// art bound at t4 (composited in-shader over the diffuse, exactly like
// the game's decalenvironment PS: lerp by the art's alpha; opaque).
// Macro and decal art are INDEPENDENT: decal ground/wall sections carry
// the same macrooverlay as their non-decal neighbors, sharing one slot
// dropped the macro grime there and rendered alternating plaza sections
// ~1.4x too bright (the large-scale ground checkerboard).
float4 overlay;
// x > 0 = environment.transparent item (alpha-blended sub-pass): shading
// follows the game's transparentenvironment.fx; the opaque pass's
// alpha-test turned the soft mist gradients into solid white cloud blobs.
// yzw = the global distance-fog RAMP (scale/bias/exponent from main-pass
// VS c5), and mat_tint doubles as the linear-space fog COLOR (rgb +
// transmittance scale in w, VS c6) for transparent items; the root
// signature is capped at 64 DWORDs, so fog rides in slots transparent
// items never use otherwise. Fog is currently applied to transparent
// items only (the km-distant mist sheets; everything else we render is
// near enough for fog to be negligible).
float4 misc;
};
// Per-frame dynamic-shadow (CSM) receiver constants, captured from the
// game's world-material PIXEL banks.
cbuffer S : register(b1) {
float4 sh_x; // light-space X row (xyz) + translation (w) [PS c0]
float4 sh_y; // light-space Y row [PS c3]
float4 sh_z; // depth row (height ramp) [PS c4]
float4 sh_c1; // cascade 1 scale.xy + offset.zw [PS c1]
float4 sh_c2; // cascade 2 scale.xy + offset.zw [PS c2]
float4 sh_color; // shadow color rgb [PS c8] + its luma in w
float4 sh_misc; // x = depth bias [PS c5.x], y = enable
// Exact world-shading frame rows (consumed by the env-family branch):
float4 sh_sun; // xyz = sun direction [PS c6], w = scene exposure [c10.x]
float4 sh_env; // x = material multiplier [PS c11.y], yz = tree lightmap
// scale/floor [tree PS c0.xy], w = tree tint mult [c4.y]
float4 sh_fogp; // xyz = global fog ramp scale/bias/exp [VS c5],
// w = proxyworld scale [proxy PS c3.y]
float4 sh_fogc; // fog color rgb + transmittance scale in w [VS c6]
// dynamicobject.fx frame-global lighting rows (see FrameScene::dynobj_rows).
float4 dyn_sun; // xyz = sun direction (PS c9), w = scene exposure (c13.x)
float4 dyn_amb; // xyz = flat ambient (c15.rgb), w = bounce scale (c15.w)
float4 dyn_misc; // x = material multiplier (c14.y),
// y = static world-shadow floor (c8.w)
};
// Character-family lighting (defaultcharacter.fx and friends): canonical
// per-draw rows captured from the guest PIXEL constant bank at palette
// capture (CaptureCharLighting has the per-family register maps; the math
// below was validated offline by executing the game's own pixel shaders).
// Enabled per draw via cam_pos.w = family (0 = not a character / capture
// failed -> the legacy empirical shading below).
cbuffer CH : register(b2) {
float4 ch_light; // xyz = sun direction, w = hair fresnel power
float4 ch_key; // rgb = key (sun) color, w = exposure
float4 ch_amb; // rgb = flat ambient, w = SH ambient multiplier / hair ambient
float4 ch_sh[9]; // SH irradiance rows, pre-scaled (see capture);
// vehicles keep spec color + power in row 0 instead
float4 ch_tintA; // CAC diffuse tint / livingworld red-mask tint (w = apply)
float4 ch_tintB; // livingworld blue-mask tint / hair fresnel tint (w = strand-alpha scale)
float4 ch_misc; // x = alpha out, y = family
};
Texture2D<float4> diffuse : register(t0);
Texture2D<float4> lightmap : register(t1);
Texture2D<float4> macro : register(t3);
Texture2D<float4> decal_art : register(t4);
// Paired second descriptor of the t4 table: the reflective families'
// (fam 5/6) normal map. Only valid, and only sampled, when overlay.w == 4.
Texture2D<float4> normal_map : register(t5);
Texture2D<float2> shadow_atlas : register(t7);
TextureCube<float4> env_cube : register(t6);
// Raw bone palette: 3 float4 rows per bone, column-vector affine [R | t],
// applied with explicit dots (StructuredBuffer<float4x4> default packing is
// column-major and would silently transpose the matrices).
StructuredBuffer<float4> bones : register(t2);
SamplerState smp : register(s0);
// s1 = bilinear CLAMP (shared with the 2D pass). Decal art must clamp: the
// art UV runs far outside [0,1] across big ground sheets and the art's
// transparent border keeps everything outside the single placement clear;
// wrap sampling tiled the graffiti across the whole plaza.
SamplerState smp_clamp : register(s1);
struct VSOut {
float4 pos : SV_Position;
float3 rpos : TEXCOORD0;
float2 uv : TEXCOORD1;
float2 uv2 : TEXCOORD2;
float3 nrm : TEXCOORD3;
float2 uv3 : TEXCOORD4;
};
VSOut vs_main(float3 p : POSITION, float2 uv : TEXCOORD0, float2 uv2 : TEXCOORD1,
float4 bw : BLENDWEIGHT0, uint4 bi : BLENDINDICES0,
float3 nrm : NORMAL0, float2 uv3 : TEXCOORD2) {
VSOut o;
float4 mp = float4(p, 1.0);
float3 n = nrm;
// tint.g > 0 marks a skinned item: the bone palette (row-vector matrices)
// maps model space to world space; mvp is then just view*proj.
float wsum = dot(bw, float4(1, 1, 1, 1));
if (tint.g > 0.0 && wsum > 0.001) {
float3 skinned = float3(0, 0, 0);
float3 sn = float3(0, 0, 0);
// Guest blend indices are plain bone numbers (verified live: byte
// streams like 02 00 03 01); bone k = palette rows 3k..3k+2.
[unroll] for (int k = 0; k < 4; ++k) {
uint row = bi[k] * 3u;
skinned += bw[k] * float3(dot(mp, bones[row]), dot(mp, bones[row + 1]),
dot(mp, bones[row + 2]));
sn += bw[k] * float3(dot(n, bones[row].xyz), dot(n, bones[row + 1].xyz),
dot(n, bones[row + 2].xyz));
}
mp = float4(skinned / wsum, 1.0);
n = sn;
} else {
n = mul(n, (float3x3)world);
}
o.pos = mul(mp, mvp);
o.rpos = mul(mp, world).xyz - cam_pos.xyz;
o.uv = uv;
o.uv2 = uv2;
o.nrm = n;
o.uv3 = uv3;
return o;
}
// Native CSM atlas sample at a world position: finest covering cascade,
// s = saturate(infront + 1 - coverage). Returns 1 (lit) when uncovered or
// shadows are off. extra_bias suppresses receiver self-shadow acne on
// surfaces that are themselves casters (characters, held board). A NEGATIVE
// extra_bias selects the game's dynamicobject receive bias instead: a
// per-cascade literal 0.007 (finest) / 0.015 (outer) replacing sh_misc.x
// (from the dynamicobject_defaultPS ucode). Props
// are casters themselves, and with only the world bias (c5.x = 0 in every
// capture) their flat tops compared against their own atlas depth; the
// lit/dark whole-surface flicker on benches, signs and sails as the
// camera-following cascades drifted frame to frame.
float SampleCsmShadow(float3 wp, float extra_bias) {
if (sh_misc.y <= 0.0) {
return 1.0;
}
float2 lsv = float2(dot(sh_x.xyz, wp) + sh_x.w, dot(sh_y.xyz, wp) + sh_y.w);
float2 luv = 0.0;
float casc = 0.0;
float2 l2 = lsv * sh_c2.xy + sh_c2.zw;
if (max(abs(l2.x), abs(l2.y)) < 0.99) { luv = l2; casc = 3.0; }
float2 l1 = lsv * sh_c1.xy + sh_c1.zw;
if (max(abs(l1.x), abs(l1.y)) < 0.99) { luv = l1; casc = 2.0; }
if (max(abs(lsv.x), abs(lsv.y)) < 0.99) { luv = lsv; casc = 1.0; }
if (casc <= 0.0) {
return 1.0;
}
float bias = extra_bias < 0.0 ? (casc > 1.5 ? 0.015 : 0.007)
: sh_misc.x + extra_bias;
float rd = dot(sh_z.xyz, wp) + sh_z.w - bias;
float2 suv = float2(luv.x / 6.0 + (casc * 2.0 - 1.0) / 6.0,
luv.y * -0.5 + 0.5);
float2 sm2 = shadow_atlas.Sample(smp_clamp, suv);
return saturate((sm2.x >= rd ? 1.0 : 0.0) + (1.0 - sm2.y));
}
float4 ps_main(VSOut i) : SV_Target {
if (tint.a > 0.0) {
return tint;
}
float4 albedo = diffuse.Sample(smp, i.uv);
// Alpha-tested foliage/fences; opaque formats sample alpha = 1. Character
// diffuse packs GLOSS in alpha; never clip characters. tint.g > 0 marks
// them: set for bones-bound skinned items AND for ropa cloth garments
// rendered rigid (sim-active player tees, clipping their gloss alpha
// discarded every pixel: the invisible-shirt bug; their decode writes
// zero blend weights, so the VS skinning branch stays off).
if (tint.g == 0.0 && overlay.w < 0.5 && cam_pos.w > -20.5) {
// environment.transparent alpha-tests its SQUARED alpha at ref 16/255
// (transparentenvironment.xml: ALPHAREF 16, PS outputs a = diffuse.a^2).
// Exact env families (cam_pos.w < 0) use the game's world ALPHAREF 30.
// dynamicobject items (cam_pos.w <= -21) are excluded here and clip in
// their own branch (only the .alphatest variant tests, at ALPHAREF 30).
// Fam 13 (reflective_trans glass, cam_pos.w = -13) never alpha-tests;
// it alpha-BLENDS in the sorted sub-pass.
if (cam_pos.w > -12.5 || cam_pos.w < -13.5) {
float aref = cam_pos.w < -0.5 ? 0.1176 : 0.35;
clip(misc.x > 0.0 ? albedo.a * albedo.a - 0.0627 : albedo.a - aref);
}
}
// Exact sky dome (cam_pos.w = -40; sky_defaultPS transcribed from the
// Skate 3 ucode in a live capture). The emulated frame's big sun
// glow is computed IN the dome shader: a 1D radial gradient (the sky
// material's `specular` channel, bound at t4 here) indexed by the sine of
// the angle between the dome direction and the sun, its rgb^2 amplified
// by 1/sat(a + 0.01), the alpha falloff makes the core HDR-bright, then
// added to the squared panorama and run through the standard exposure/
// tonemap/sqrt chain (no fog on the sky) and the postfx uber 1.41.
// mat_tint.xyz = sun dir, mat_tint.w = the level sky elevation;
// overlay.x = sun angular scale, overlay.y = pre-tone multiplier,
// misc.y = scene exposure.
if (cam_pos.w < -39.5 && cam_pos.w > -40.5) {
// The dome mesh is camera-relative: world = mesh + (cam.x, sky_h,
// cam.z), so the shader's dome-local direction (sky.fx In.vPos) is
// rpos + (0, cam.y - sky_h, 0).
float3 dome = normalize(i.rpos + float3(0.0, cam_pos.y - mat_tint.w, 0.0));
float3 lin = albedo.rgb * albedo.rgb;
float dotPL = saturate(dot(dome, mat_tint.xyz));
float sinA = sqrt(saturate(1.0 - dotPL * dotPL));
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);
}
// dynamicobject.fx props (cam_pos.w = -(20 + variant): -21 default,
// -22 alphatest). Rigid movable objects (dispensers, dumpsters, benches,
// cans). Lit with the game's own dynamicobject model (verified exact):
// key sun light + bounce + flat ambient, gated by
// the CSM shadow (the static world-shadow map is approximated as fully lit
// - its floor c8.w bounds it), then fog -> exposure -> tonemap -> sqrt and
// the postfx uber 1.41. v1 uses the geometric normal (cross of the mesh
// tangent/binormal) with the flat normal-map kd 0.93429; the base/detail/
// spec normal maps are v2.
if (cam_pos.w < -20.5) {
if (cam_pos.w < -21.5) {
clip(albedo.a - 0.1176); // dynamicobject.alphatest: ALPHAREF 30
}
float3 dlin = albedo.rgb * albedo.rgb;
float3 n = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
float ndl = dot(n, dyn_sun.xyz);
float key = saturate(ndl); // key light gated on N.L >= 0
float bounce = saturate(dot(n, float3(-dyn_sun.x, dyn_sun.y, -dyn_sun.z)));
// CSM shadow (shared receiver rows at t5); the static world-shadow map is
// not rendered natively, so its term is 1 and the min collapses to the
// CSM (bounded below by the c8.w floor, dyn_misc.y). extra_bias -1 =
// the game's own per-cascade dynamicobject receive bias (props are
// casters; without it their flat tops self-shadow and flicker).
float s = SampleCsmShadow(i.rpos + cam_pos.xyz, -1.0);
float shadow = min(s * (ndl >= 0.0 ? 1.0 : 0.0), max(1.0, dyn_misc.y));
float3 lighting = key * shadow + bounce * dyn_amb.w + dyn_amb.rgb;
// GetTangentLight with the neutral (flat) normal map: 0.39 * 2.39562.
float3 lin = lighting * 0.93429 * dlin;
// Fog -> exposure -> tonemap -> sqrt, then the 1.41 uber scene multiplier.
float fdist = length(i.rpos);
float f1 = saturate(fdist * sh_fogp.x + sh_fogp.y);
if (sh_fogp.z != 1.0) {
f1 = pow(max(f1, 1e-6), sh_fogp.z);
}
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);
}
// Character families: the game's own lighting in LINEAR space (diffuse is
// gamma -> square it), then the exact tone chain from the disassembly and
// the postfx uber's 1.41 scene multiplier (which the empirical world
// shading already folds into its constants; without it characters sit
// ~30% darker than their surroundings, measured on an F11 A/B pair).
if (cam_pos.w > 0.5) {
float fam = cam_pos.w;
float3 dlin = albedo.rgb * albedo.rgb;
float3 cn = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
float ndl = saturate(dot(cn, ch_light.xyz));
float3 vd = -normalize(i.rpos);
float3 lin;
float out_a = 1.0;
if (fam > 5.5) {
// Traffic vehicles (vehicle.fx fam 6 body / vehicle_glass.fx fam 7
// windows, disassembled from vehicle_defaultPS): paint recolor where
// the diffuse green channel is below the mask threshold (red-channel
// mask * colorize_red + blue-channel mask * colorize_blue, the taxi
// yellow), key light + the livingworld flat ambient, phong specular
// along the reflected sun, and an environment-cube reflection scaled
// by fresnel and the gloss packed in the diffuse alpha. Glass keeps
// only the reflection terms (its tint rows are zero) and blends at
// the captured alpha. overlay.y > 0 = the material's cube resolved
// at t6 (same convention as water).
float3 sel;
if (fam < 6.5) {
sel = dlin.g > 0.001225
? dlin
: ch_tintA.rgb * dlin.r + ch_tintB.rgb * dlin.b;
} else {
sel = ch_tintA.rgb;
}
// DXN panel normal map (the material's `normal` channel, riding the
// macro slot; overlay.z > 0 = resolved). The vertex layout carries no
// tangent frame, so build a screen-space cotangent frame from the
// position/uv derivatives; it reproduces the authored panel shading
// including mirrored UV islands. Skipping the map entirely shades the
// hinged panels by their vertex normals, which face away from the sun
// - the dark ambient-blue "misdrawn shadow" that stopped at the door
// seam (verified against the ucode: flat map = the artifact, real
// map = the emulated car).
float3 vn = cn;
if (overlay.z > 0.5) {
float2 nm = macro.Sample(smp, i.uv).rg * 2.0 - 1.0;
float3 dp1 = ddx(i.rpos), dp2 = ddy(i.rpos);
float2 du1 = ddx(i.uv), du2 = ddy(i.uv);
float3 dp2p = cross(dp2, cn), dp1p = cross(cn, dp1);
float3 tt = dp2p * du1.x + dp1p * du2.x;
float3 bb = dp2p * du1.y + dp1p * du2.y;
float im = rsqrt(max(max(dot(tt, tt), dot(bb, bb)), 1e-12));
vn = normalize(nm.x * tt * im + nm.y * bb * im +
cn * sqrt(saturate(1.0 - dot(nm, nm))));
}
float vndl = saturate(dot(vn, ch_light.xyz));
float3 rfl = ch_light.xyz - 2.0 * dot(vn, ch_light.xyz) * vn;
// The sun spec is gated on N.L >= 0 (the ucode multiplies the spec
// term by an sge result); the cube reflection is not.
float spec = pow(saturate(dot(vd, -rfl)), max(ch_sh[0].w, 1.0)) *
(dot(vn, ch_light.xyz) >= 0.0 ? 1.0 : 0.0);
float fres = pow(1.0 - saturate(dot(vn, vd)), max(ch_light.w, 1.0));
float3 cube = float3(0.0, 0.0, 0.0);
if (overlay.y > 0.5) {
cube = env_cube.Sample(smp, reflect(-vd, vn)).rgb;
cube *= cube; // the PS consumes the cube squared (linear space)
}
// Gloss = the diffuse alpha SQUARED: the ucode squares the whole
// diffuse fetch (linear-space decode), alpha included; raw alpha
// over-specs ~5x and mottles the body panels.
float gloss = fam < 6.5 ? albedo.a * albedo.a : 1.0;
lin = sel * (ch_key.rgb * vndl + ch_amb.rgb) +
(spec * ch_sh[0].rgb + cube) * fres * gloss;
out_a = ch_misc.x;
} else if (fam > 3.5) {
// Hair (cac_hair / defaulthair): key on a wrapped N.L ramp + flat
// ambient, fresnel rim tint on a steeper ramp; strand coverage from
// the mesh's "alpha" channel at the raw second texcoord (bound at t4)
// - alpha-blended in the sorted sub-pass (hair drawn opaque is the
// blocky-helmet look).
float fres = pow(1.0 - saturate(dot(cn, vd)), max(ch_light.w, 1.0));
float3 hl = ch_key.rgb * (saturate(ndl * 0.75 + 0.25) + ch_amb.w) +
ch_tintB.rgb * fres * saturate(ndl * 1.75 + 0.25);
lin = dlin * hl;
out_a = saturate(decal_art.Sample(smp, i.uv2).r * ch_tintB.w);
} else if (fam > 2.5) {
// livingworld pedestrians: the diffuse is a stamp-mask atlas: red
// regions recolor with tintA, blue with tintB (judged in linear
// space; real-color regions have green above the threshold).
// The game's character PSes multiply the key light by the CSM shadow
// (tap >= ray = lit); characters are casters themselves, so an extra
// receiver bias suppresses self-shadow acne while the body-onto-board
// / body-onto-NPC shading survives (the sun-axis depth gap there is
// tens of cm). Without this the held skateboard, a big flat surface
// that is almost always inside the skater's own shadow, renders
// fully sunlit (near-white) against the emulated dark deck.
float csm = SampleCsmShadow(i.rpos + cam_pos.xyz, 0.012);
float3 sel = dlin.g > 0.001225
? dlin
: ch_tintA.rgb * dlin.r + ch_tintB.rgb * dlin.b;
lin = sel * (ch_key.rgb * ndl * csm + ch_amb.rgb);
// livingworld_stamp_defaultPS ends `max oC0.w, c21.x`: the entity's
// spawn/distance fade. Only visible when the item is routed to the
// blended sub-pass (alpha < 1); the opaque pass ignores it.
out_a = ch_misc.x;
} else {
// defaultcharacter / CAC pieces: key light + SH irradiance ambient,
// key gated by the CSM shadow (see the livingworld comment above).
float csm = SampleCsmShadow(i.rpos + cam_pos.xyz, 0.012);
if (ch_tintA.w > 0.0) {
dlin *= ch_tintA.rgb;
}
float3 irr = saturate(
ch_sh[0].rgb + cn.x * ch_sh[1].rgb + cn.y * ch_sh[2].rgb +
cn.z * ch_sh[3].rgb + (cn.x * cn.z) * ch_sh[4].rgb +
(cn.z * cn.y) * ch_sh[5].rgb + (cn.y * cn.x) * ch_sh[6].rgb +
(cn.z * cn.z) * ch_sh[7].rgb +
(cn.x * cn.x - cn.y * cn.y) * ch_sh[8].rgb);
lin = dlin * (ch_key.rgb * ndl * csm + irr * ch_amb.w);
// defaultcharacter/cacstamp PSes end `max oC0.w, c13.x / c22.x`:
// the entity's spawn fade (see the livingworld comment above).
out_a = ch_misc.x;
}
// 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);
}
// Exact world-material families (cam_pos.w = -family): hand-ported from
// the game's own pixel shaders and verified per-pixel against them with
// an offline ucode interpreter. All texture
// colors linearize IN-SHADER as x^2 (the fetch signs are unsigned on every
// world texture). Families: 1 baseenvironment, 2 defaultenvironment,
// 3/4 decalenvironment(_tileable), 5/6 reflective(_simple), 7 alphatest,
// 8 environmentdiffuse, 9/10 tree(animate), 11 proxyworld,
// 12 incandescent. v1 runs with NEUTRAL normal/detail maps (kd is the
// exact flat-map constant 0.39 * 2.39562); spec/reflection masks bind at
// t4 (overlay.w == 3) on families without decal art.
if (cam_pos.w < -0.5) {
float fam = -cam_pos.w;
float3 dlin = albedo.rgb * albedo.rgb;
// Global distance fog (VS c5/c6, captured per frame): every world PS
// ends with col * fog.a + fog.rgb before exposure/tonemap.
float fdist = length(i.rpos);
float f1 = saturate(fdist * sh_fogp.x + sh_fogp.y);
if (sh_fogp.z != 1.0) {
f1 = pow(max(f1, 1e-6), sh_fogp.z);
}
float3 fog_rgb = sh_fogc.rgb * f1;
float fog_a = 1.0 + sh_fogc.a * f1;
float expo = sh_sun.w;
float3 lin;
float out_a = 1.0;
bool reduced_tone = false;
if (fam > 8.5 && fam < 12.5) {
// tree/treeanimate: D^2 * max(lm^2, floor) * scale [* tint mult];
// proxyworld/incandescent: D^2 * scale. No shadow receive, no kd, no
// material multiplier on the fog term.
if (fam < 10.5) {
float3 lmg = lightmap.Sample(smp, i.uv2).rgb;
lin = dlin * max(lmg * lmg, sh_env.z) * sh_env.y;
if (fam < 9.5) {
lin *= sh_env.w;
}
out_a = albedo.a;
} else {
lin = dlin * (fam < 11.5 ? sh_fogp.w : 1.0);
}
} else {
// Environment families: macro overlay (0.5-neutral, fades under decal
// art), linear decal composite, lightmap squared and min-clamped
// against (CSM s + shadow color), kd, phong spec vs the shader's
// fixed literal light, cube reflection on 5/6.
float3 ov = float3(1.0, 1.0, 1.0);
// Fam 13 (transparentenvironmentreflective) carries no macro term.
if (overlay.z > 0.0 && fam < 12.5) {
float3 mo = macro.Sample(smp, i.uv * overlay.x).rgb;
ov = saturate((mo - 0.5) * overlay.y + 0.5);
}
if (fam > 2.5 && fam < 4.5 && overlay.w > 0.5) {
// overlay.w == 0 = art unresolved (white fallback alpha 1 would
// whitewash the whole surface).
float4 dk = overlay.w > 1.5 ? decal_art.Sample(smp, i.uv3)
: decal_art.Sample(smp_clamp, i.uv3);
dlin = lerp(dlin, dk.rgb * dk.rgb, dk.a);
ov = lerp(float3(1.0, 1.0, 1.0), ov, 1.0 - dk.a);
}
float3 dcol = dlin * ov;
// CSM shadow term s = sat(infront + 1 - coverage) from the native
// atlas (same cascade select as the legacy receive path).
float s = 1.0;
if (sh_misc.y > 0.0) {
float3 wp = i.rpos + cam_pos.xyz;
float2 lsv =
float2(dot(sh_x.xyz, wp) + sh_x.w, dot(sh_y.xyz, wp) + sh_y.w);
float rd = dot(sh_z.xyz, wp) + sh_z.w - sh_misc.x;
float2 luv = 0.0;
float casc = 0.0;
float2 l2 = lsv * sh_c2.xy + sh_c2.zw;
if (max(abs(l2.x), abs(l2.y)) < 0.99) { luv = l2; casc = 3.0; }
float2 l1 = lsv * sh_c1.xy + sh_c1.zw;
if (max(abs(l1.x), abs(l1.y)) < 0.99) { luv = l1; casc = 2.0; }
if (max(abs(lsv.x), abs(lsv.y)) < 0.99) { luv = lsv; casc = 1.0; }
if (casc > 0.0) {
float2 suv = float2(luv.x / 6.0 + (casc * 2.0 - 1.0) / 6.0,
luv.y * -0.5 + 0.5);
float2 sm2 = shadow_atlas.Sample(smp_clamp, suv);
s = saturate((sm2.x >= rd ? 1.0 : 0.0) + (1.0 - sm2.y));
}
}
// Lightmap fetch = the console's semantics: BILINEAR, CLAMPED, mip 0.
// The composed atlas pages are single-level on console and the fetch
// constants carry clamp_x/clamp_y = 2. Sampling them with the aniso-8
// WRAP sampler over our generated mip chain averaged NEIGHBORING
// atlas cells together at grazing angles (deep aniso LOD), and at the
// page border the wrap pulled in the opposite edge of the page: the
// reflective_trans canopy slope (cells at v ~ 0.996, white-cliff
// cells wrapping in from v ~ 0) glowed ~2x bright while the right
// awning went dark, with the decode, UVs and constants all verified
// exact (diagnosed via the mode-7 raw-lightmap isolation view).
float3 lmg = lightmap.SampleLevel(smp_clamp, i.uv2, 0.0).rgb;
// F12 isolation mode 7: visualize the RAW lightmap sample; mode 8:
// visualize the lightmap unwrap coordinate (frac(uv2*16) in rg).
// Debug-only taps on fams 5/6/13 (misc.z is 0 on the other fams).
if (abs(misc.z - 7.0) < 0.5) {
return float4(lmg, 1.0);
}
if (abs(misc.z - 8.0) < 0.5) {
return float4(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 (abs(misc.z - 9.0) < 0.5) {
return float4(tint.r, 0.0, 1.0 - tint.r, 1.0);
}
float3 lml = min(lmg * lmg, s + sh_color.rgb);
// GetTangentLight with the neutral (flat) normal map:
// 0.39 * 2.39562 exactly. Fam 13 has no kd term at all; its body is
// D^2 * lml * ALPHA, premultiplied once in the shader on top of the
// a^2 blend factor (verified 0.0-error vs the ucode).
lin = fam > 12.5 ? lml * dcol * albedo.a : lml * 0.93429 * dcol;
if (overlay.w > 2.5) {
// spec/ecc/refmask at t4: phong vs the FIXED literal light
// (-0.14, 0.5, 0.9), power 10 + 290*ecc, tint (2.1, 1.8, 1.5),
// scaled by the clamped lightmap green and the spec mask.
float4 masks = decal_art.Sample(smp, i.uv);
float3 wn = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
// misc.z = 3 (F12 reflection isolation): force the flat normal.
if (overlay.w > 3.5 && abs(misc.z - 3.0) > 0.5) {
// Per-pixel normal map (t5, paired descriptor): the real PS
// (baseenvironmentreflective_defaultPS) reflects off the
// normal-mapped normal: tangent normal = 2*(n + detail - 1) on
// xy, 2*n.z - 1 on z (the material's detail map is a constant
// neutral 16x16, folded here as 0.5). With the FLAT vertex normal
// every panel of a glass facade reflects the same tiny cube
// region: one giant magnified smear of the plaza cube's
// lamp-heads/trees (the "streetlight head" artifact, ucode-traced
// on a captured pixel; flat N lands on the
// face-0 tree/lamp texels, the mapped N tilts onto sky). The
// per-panel tilts break the reflection up exactly like the
// emulated frame. Screen-space cotangent frame (same shape as
// the vehicle DXN branch; the world VS carries no tangent
// attributes we decode).
// F12 mode 6: the slider drives the normal-map LOD bias live;
// the console fetches the nm at its 640p gradient LOD (blurrier,
// weaker bump tilts), so the matching bias is the remaining
// reflection-rotation candidate. Default stays SHARP (bias 0):
// an unconditional misc.y bias visibly degraded the glass;
// tune with the
// slider first, promote the found value to a default after.
float nm_bias = misc.z > 5.5 ? misc.w : 0.0;
float3 nmv = normal_map.SampleBias(smp, i.uv, nm_bias).rgb;
// Exact composition: xy = 2*normal + 2*detail - 2, z = 2*n.z - 1.
// The detail map is a CONSTANT 16x16 (0.514, 0.506), NOT the
// formula's 0.5 neutral, so its fold is a constant tangent tilt.
// The tilt rides the two F12 trim sliders (packed in misc.x;
// defaults = the exact fold) so the residual reflection rotation
// can be dialed live against the emulated frame.
float trim_yi = floor(misc.x / 1000.0);
float2 trim = float2(misc.x - trim_yi * 1000.0 - 500.0, trim_yi - 500.0) *
0.001;
float3 nt = float3(nmv.xy * 2.0 - 1.0 + trim, nmv.z * 2.0 - 1.0);
float3 dp1 = ddx(i.rpos), dp2 = ddy(i.rpos);
float2 du1 = ddx(i.uv), du2 = ddy(i.uv);
float3 dp2p = cross(dp2, wn), dp1p = cross(wn, dp1);
float3 tt = dp2p * du1.x + dp1p * du2.x;
float3 bb = dp2p * du1.y + dp1p * du2.y;
// Signs/lengths calibrated against the mesh's REAL stored TBN
// (10_11_11 tangent @ +24, s16 normal xy @ +20, handedness in the
// unwrap sign bits, decoded mesh-wide):
// stored T == +dP/dU on 100% of tris (this formula's tt comes out
// as -dP/dU -> negate), stored B == this bb direction (dot 0.999),
// and the game's rows are UNIT so each axis normalizes SEPARATELY
// - the shared max-normalizer both halved and (with the flip)
// mirrored the horizontal tilt, which pushed the reflection
// DEEPER into the dark cube region instead of off it (first
// deploy looked unchanged). With these signs the reconstruction
// matches the ucode's mapped normal to 4 decimals at the traced
// pixel. An inward geometric wn flips all three terms together;
// reflect() is invariant to that.
tt *= -rsqrt(max(dot(tt, tt), 1e-12));
bb *= rsqrt(max(dot(bb, bb), 1e-12));
// The mesh's STORED TBN (what the game's VS interpolates) is the
// world-up frame, not the exact UV-gradient frame: B = up
// projected into the surface, T = cross(B, N), verified on the
// decoded window vertices (B = (0, 0.999, 0), T = horizontal).
// The screen-space frame lands ~3 deg off it (authored TBN vs UV
// gradients), which rotates reflections by degrees, visible as
// parallax against the emulated frame. Use the analytic axes and
// keep only the screen-space frame's SIGNS (mirrored UV islands
// flip the tangent; derivatives track that, the analytic frame
// cannot). Near-horizontal surfaces keep the screen frame.
float3 bb2 = float3(0.0, 1.0, 0.0) - wn * wn.y;
float lb2 = length(bb2);
if (lb2 > 0.05) {
bb2 /= lb2;
float3 tt2 = cross(bb2, wn);
tt = tt2 * (dot(tt2, tt) >= 0.0 ? 1.0 : -1.0);
bb = bb2 * (dot(bb2, bb) >= 0.0 ? 1.0 : -1.0);
}
wn = normalize(nt.x * tt + nt.y * bb + wn * max(nt.z, 0.05));
}
float3 vd = -normalize(i.rpos);
float3 Ls = float3(-0.14, 0.5, 0.9);
float3 refl = Ls - 2.0 * wn * dot(wn, Ls);
float bp = saturate(dot(vd, -refl));
float ks = pow(max(bp, 1e-6), 10.0 + 290.0 * masks.y);
// misc.z = 5 (F12 reflection isolation): body only, no spec/cube.
if (abs(misc.z - 5.0) > 0.5) {
lin += ks * float3(2.1, 1.8, 1.5) * lml.g * masks.x;
}
if (((fam > 4.5 && fam < 6.5) || fam > 12.5) &&
abs(misc.z - 5.0) > 0.5 && abs(misc.z - 1.0) > 0.5) {
// Cube reflection: reflect(E, wN) with xy negated (the source's
// ref_vec.xy *= -1), luminosity lerped toward 1 by
// 0.3 * sat(4*refmask - 2.6), x refmask x reflection_scale 1.5.
float3 rv = vd - 2.0 * wn * dot(vd, wn);
// misc.y = cube LOD bias log2(render_height / 640): the guest
// computes the cube fetch's gradient LOD at its own 1152x640
// render; at 4K our per-pixel gradients are ~3.4x smaller, so
// without the bias baked cube detail (the plaza streetlight
// heads) survives through mips the console's fetch blurs away.
// F12 isolation (misc.z): mode 2 samples the ABSOLUTE level in
// misc.w; other modes add misc.w as extra bias; mode 4 shows the
// raw cube sample in place of the shaded result.
float3 dir = float3(-rv.x, -rv.y, rv.z);
// Mode 6 gives the slider to the NM fetch; the cube keeps just
// the automatic bias there.
float cube_extra = misc.z > 5.5 ? 0.0 : misc.w;
float3 cube = abs(misc.z - 2.0) < 0.5
? env_cube.SampleLevel(smp, dir, misc.w).rgb
: env_cube.SampleBias(smp, dir, misc.y + cube_extra).rgb;
float rl = 0.3 * saturate(4.0 * masks.z - 2.6);
float lum = lml.g + rl * (1.0 - lml.g);
lin += cube * lum * masks.z * 1.5;
if (abs(misc.z - 4.0) < 0.5) {
lin = cube * cube;
}
}
}
if (fam > 12.5) {
// Fam 13 blend factor: the PS outputs a^2 (straight-alpha blend on
// top of the in-shader premultiplied body; wisps thin as ~a^3,
// same convention as transparentenvironment).
out_a = albedo.a * albedo.a;
} else if (fam > 6.5) {
out_a = albedo.a;
reduced_tone = fam > 7.5; // environmentdiffuse's cheap tonemap
}
fog_a *= sh_env.x; // material multiplier (PS c11.y)
}
// 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);
}
// Macro overlay: large-scale grime/cracks multiplied over the diffuse at
// uv * macroOverlayUVScale, faded by macroOverlayOpacity: the ground and
// wall weathering. WHITE is the neutral (materials without weathering
// bind a 16x16 "default_white"). The game multiplies it ONCE in its
// linear (squared) color space, so the gamma-space equivalent is
// sqrt(m); a direct multiply doubles the darkening (harsh black
// patchwork vs the emulated subtle weathering).
if (overlay.z > 0.0 && misc.x < 1.5) { // water reuses overlay.z (ripple map flag)
float4 m = macro.Sample(smp, i.uv * overlay.x);
albedo.rgb *= lerp(float3(1.0, 1.0, 1.0), sqrt(m.rgb), overlay.y * m.a);
}
// environment.decal surfaces: the paint/graffiti art (t4) is composited
// over the base diffuse by ITS alpha, opaque output; these meshes ARE
// the wall/ground there. The art maps with uv3, the second half-pair of
// the packed half4 first texcoord (validated offline: sampling with the
// tiling uv0 repeats it: "Stereo Stereo Stereo"; the fmt-26 second
// element is the lightmap unwrap, not the decal's).
if (overlay.w > 0.0) {
// overlay.w == 2 marks environment.decal_tileable: the art tiles across
// the surface (rock/cliff faces) and must WRAP; clamp stretched the
// border texels into giant streaks. Single placements clamp (their
// transparent border keeps everything outside the placement clear).
float4 dk = overlay.w > 1.5 ? decal_art.Sample(smp, i.uv3)
: decal_art.Sample(smp_clamp, i.uv3);
albedo.rgb = lerp(albedo.rgb, dk.rgb, dk.a);
}
// tint.r > 0 marks items with a lightmap bound (2x baked lighting);
// otherwise fall back to derivative face shading. The lighting term stays
// separate from the albedo so the CSM receive below can min-clamp IT, the
// way the game's GetShadowedLightMap clamps the lightmap lighting.
float3 light;
if (tint.b > 0.0) {
light = float3(1.0, 1.0, 1.0); // unlit (sky dome)
} else if (tint.r > 0.0) {
light = lightmap.Sample(smp, i.uv2).rgb * 2.0;
} else {
// Smooth per-vertex normal when the mesh has one; face normal from
// position derivatives otherwise.
float3 n = dot(i.nrm, i.nrm) > 0.01 ? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
light = abs(dot(n, normalize(float3(0.4, 0.8, 0.3)))) * 0.35 + 0.75;
}
// Dynamic CSM shadow receive (world geometry + rigid props; characters
// need the game's separate PCF/bias variant; skipping them avoids
// self-shadow acne, and the ground shadow is 95% of the visible effect).
// Exact receiver math from the baseenvironment PS disassembly: finest
// cascade whose |ls| < 0.99 wins; shadow = saturate(infront + 1 -
// coverage), then the game min-clamps the LINEAR lighting term:
// light_linear = min(light_linear, s + c8.rgb)
// Full shadow clamps to the dim bluish c8 ambient, the penumbra saturates
// wherever the clamp exceeds the lit level (which is what keeps the edge
// crisp), and surfaces already darker than the clamp, baked shade under
// bridges/trees, show NO dynamic shadow at all. Our light term is
// gamma-space (light^2 ~ the game's linear term: the lightmap x2 folds
// its x4 linear multiplier), so the clamp maps to min(light, sqrt(s+c8))
// per channel. A fixed-denominator curve here read pitch-black and
// double-darkened baked shade.
if (sh_misc.y > 0.0 && tint.g == 0.0 && tint.b == 0.0 && misc.x == 0.0) {
float3 wp = i.rpos + cam_pos.xyz;
float2 lsv = float2(dot(sh_x.xyz, wp) + sh_x.w, dot(sh_y.xyz, wp) + sh_y.w);
float rd = dot(sh_z.xyz, wp) + sh_z.w - sh_misc.x;
float2 luv = 0.0;
float casc = 0.0;
float2 l2 = lsv * sh_c2.xy + sh_c2.zw;
if (max(abs(l2.x), abs(l2.y)) < 0.99) { luv = l2; casc = 3.0; }
float2 l1 = lsv * sh_c1.xy + sh_c1.zw;
if (max(abs(l1.x), abs(l1.y)) < 0.99) { luv = l1; casc = 2.0; }
if (max(abs(lsv.x), abs(lsv.y)) < 0.99) { luv = lsv; casc = 1.0; }
if (casc > 0.0) {
float2 uv = float2(luv.x / 6.0 + (casc * 2.0 - 1.0) / 6.0, luv.y * -0.5 + 0.5);
float2 m = shadow_atlas.Sample(smp_clamp, uv);
float s = saturate((m.x >= rd ? 1.0 : 0.0) + (1.0 - m.y));
light = min(light, sqrt(s + sh_color.rgb));
}
}
float3 lit = albedo.rgb * light;
if (mat_tint.w > 0.0 && misc.x == 0.0) {
lit *= mat_tint.rgb;
}
if (misc.x > 1.5) {
// water.* (flowingwater.fx approximation): the real shader is
// near-black diffuse + dual time-scrolled ripple-normal taps + an
// environment-cube reflection + sun specular. We have no cube map
// bound, so the reflection term is the frame fog color (the best
// single approximation of the surroundings' haze tone) scaled by a
// fresnel curve; the ripple normal perturbs both the fresnel and a sun
// sparkle along the captured CSM light axis. The lightmap (x2) keeps
// the baked bridge/wall shading on the surface. Calibrated against the
// canal capture (emulated mid-canal mean ~(24,28,32)/255).
float t = overlay.x;
float2 rip;
if (overlay.z > 0.0) {
// Dual scrolled taps of the material's ripple normal map (macro slot).
float2 wuv = i.uv * 6.0;
float3 n1 = macro.Sample(smp, wuv + t * float2(0.11, 0.06)).rgb;
float3 n2 = macro.Sample(smp, wuv * 1.71 - t * float2(0.07, 0.13)).rgb;
rip = (n1.xy + n2.xy) - 1.0;
} else {
// Normal map unresolved: procedural ripples from world position.
// Wavelengths ~0.4-1m (emulated ripples are decimeter-scale); low
// frequencies formed meter-wide chevron interference bands that read
// as giant arrows on the surface.
float3 wp = i.rpos + cam_pos.xyz;
rip = 0.35 * float2(sin(wp.x * 9.7 + wp.z * 6.1 + t * 2.3) +
0.6 * sin(wp.x * 17.3 - wp.z * 11.9 + t * 3.4),
cos(wp.x * 7.1 - wp.z * 13.7 + t * 2.7) +
0.6 * cos(wp.x * 12.9 + wp.z * 18.3 + t * 3.1));
}
float3 wn = normalize(float3(rip.x * 0.4, 2.0, rip.y * 0.4));
float3 vd = -normalize(i.rpos);
float fres = pow(1.0 - saturate(dot(vd, wn)), 3.0);
// The flowing-water lightmap unwrap decodes unreliably (bands across
// atlas gutters), so the water term deliberately ignores it: near-black
// body + ripple-perturbed cube reflection + sun sparkle.
// Deep body: the water "diffuse" is a faint STRIPE MASK (max 24/255,
// WaterFallFoamAlpha, a lookup for the real shader, not a color). The
// game consumes it in linear space where 0.09^2 vanishes; squaring here
// likewise kills the visible blue/black banding. overlay.w > 0 = no
// diffuse channel at all (ocean.default); body is zero there, NOT the
// white fallback (ocean.fx: diffTerm = (0,0,0), color is all reflection).
float3 col = overlay.w > 0.5 ? 0.0 : albedo.rgb * albedo.rgb * 0.6;
// Reflection tint: the environment cube when resolved (t6); otherwise a
// haze derived from the frame fog color, lifted toward neutral so dark
// dusk fog doesn't collapse the water to black (fit: emulated canal
// mean ~(24,28,32)/255 with fog color (0.02,0.07,0.13)).
float3 renv = overlay.y > 0.0
? env_cube.Sample(smp, reflect(-vd, wn)).rgb
: mat_tint.rgb * 0.5 + 0.06;
col += renv * (0.55 + 0.45 * fres);
if (sh_misc.y > 0.0) {
float3 h = normalize(vd + normalize(-sh_z.xyz));
col += pow(saturate(dot(wn, h)), 90.0) * 0.35; // sun sparkle
}
float fade = saturate(length(i.rpos) * misc.y + misc.z);
if (misc.w != 1.0) {
fade = pow(max(fade, 1e-4), misc.w);
}
col = sqrt(max(col * col * saturate(1.0 + fade * mat_tint.w) +
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);
}
if (misc.x > 0.0) {
// transparentenvironment.fx (Skate 2 source; disassembled Skate 3 PS
// matches): outcolor.rgb = diffTerm * diffuse.rgb * diffuse.a; the rgb
// is premultiplied by alpha ONCE IN THE SHADER on top of the a^2 blend
// factor, so wisps thin out as ~a^3. That cubic falloff is most of the
// emulated "sparse clouds" look. Fog is applied in the game's linear
// (squared) color space: fade = saturate(dist * ramp.x + ramp.y)^ramp.z
// toward the fog color, transmittance = 1 + fade * fogcolor.w.
lit *= albedo.a;
float fade = saturate(length(i.rpos) * misc.y + misc.z);
if (misc.w != 1.0) {
fade = pow(max(fade, 1e-4), misc.w);
}
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(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
// (the height-ramp row; viewport z range 0..1, DepthClip off so casters
// outside the 12 m depth window clamp like the game accepts). MIN blend on
// both channels against a (1, 1) clear: R accumulates the min depth, G
// drops to 0 where any caster drew ("uncoverage"; the blur pass converts
// to the game's coverage convention).
float2 ps_shadow_caster(VSOut i) : SV_Target {
return float2(i.pos.z, 0.0);
}

View file

@ -0,0 +1,45 @@
// Shadow blur: the game's shadow_h/vblur passes, exact semantics from the
// disassembled Xenos ucode. One fullscreen-triangle draw
// per tile per direction; taps stay inside the tile (clamped). Coverage is
// Gaussian-blurred; depth keeps the exact center value where covered and
// dilates (binary-coverage-weighted neighbour average) into the penumbra
// otherwise, so depth compares stay valid across the blurred edge.
// b0 floats: c0 = (dir.x, dir.y, ntaps, src_is_raw), c1 = (w0, w1, w2, 0),
// c2 = (tile_x0, tile_x1, tile_y1, 0). ntaps 0 = format-convert only
// (cascade 2 is never blurred).
cbuffer C : register(b0) {
float4 c0;
float4 c1;
float4 c2;
};
Texture2D<float2> src : register(t0);
float4 vs_main(uint id : SV_VertexID) : SV_Position {
float2 uv = float2((id << 1) & 2, id & 2);
return float4(uv * float2(2, -2) + float2(-1, 1), 0, 1);
}
float2 ps_main(float4 pos : SV_Position) : SV_Target {
int2 p = int2(pos.xy);
int ntaps = int(c0.z);
float w[3] = {c1.x, c1.y, c1.z};
float2 center = src.Load(int3(p, 0));
float ccen = c0.w > 0.0 ? 1.0 - center.y : center.y;
float cov = 0.0;
float dsum = 0.0;
float dcnt = 0.0;
[unroll] for (int k = -2; k <= 2; ++k) {
if (abs(k) > ntaps) continue;
int2 q = p + int2(c0.xy) * k;
q.x = clamp(q.x, int(c2.x), int(c2.y));
q.y = clamp(q.y, 0, int(c2.z));
float2 t = src.Load(int3(q, 0));
float c = c0.w > 0.0 ? 1.0 - t.y : t.y;
cov += w[abs(k)] * c;
if (k != 0) {
float hard = c > 0.001 ? 1.0 : 0.0;
dcnt += hard;
dsum += hard * t.x;
}
}
float depth = ccen > 0.001 ? center.x : (dcnt > 0.0 ? dsum / dcnt : center.x);
return float2(depth, saturate(cov));
}

View file

@ -0,0 +1,47 @@
// In-world neon spline shader (waypoint arrows / marker beams). The guest
// B-spline VS is evaluated on the CPU at publish time into WORLD-space
// vertices; the VS here projects them with the scene's (smoothed) view_proj
// rows so the signs stay glued to the world under camera re-timing. The two
// PS variants transcribe the game's own spline.fx (Skate-Shaders repo):
// "default" = additive glow with the squared-gamma trick, "darken" =
// straight-alpha backdrop dimming. The gradient texture uses the wrap/aniso
// sampler like the original i_diffuse (U runs 0..N along the band).
cbuffer C : register(b0) {
float4 vp0; // scene view_proj rows (row-vector: clip = x*vp0+y*vp1+z*vp2+w*vp3)
float4 vp1;
float4 vp2;
float4 vp3;
float4 intensity; // i_intensity as staged (x = default gain, y = darken gain)
};
Texture2D<float4> tex : register(t0);
SamplerState smp : register(s0);
struct VSOut {
float4 pos : SV_Position;
float2 uv : TEXCOORD0;
float fade : TEXCOORD1;
};
VSOut vs_main(float4 p : POSITION, float2 uv : TEXCOORD0, float fade : TEXCOORD1) {
VSOut o;
o.pos = p.x * vp0 + p.y * vp1 + p.z * vp2 + p.w * vp3;
o.uv = uv;
o.fade = fade;
return o;
}
// Exact transcription of the Skate 3 spline pixel shaders (disassembled
// ucode, NOT the older Skate 2 spline.fx source: EA added in-shader sqrt
// gamma compensation). default: oC0.rgb = sqrt(rgb^2 * i.x * fade / a);
// darken: oC0.rgb = sqrt(rgb^2 * i.y), oC0.a = sqrt(a * i.y * fade).
// 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.
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);
}
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);
}

View file

@ -0,0 +1,174 @@
#pragma once
// Internal diagnostics interface shared between skate3_native_scene.cpp and
// skate3_native_diagnostics.cpp: the offline-analysis recording
// (StartRecording/WriteRecording), the camera/bone signal recorders and the
// synthetic-pan probe. Pure diagnosis machinery; nothing here participates
// in rendering; capture call sites live in the scene TU because they read
// per-draw locals, but all state and file writing lives in the diagnostics
// TU.
#include <atomic>
#include <cstdint>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "skate3_native_scene.h"
namespace skate3::native_scene {
// ---- Offline-analysis recording (see StartRecording/WriteRecording) --------
// The full per-draw constant bank stream is the ground truth for what the
// game's shaders saw; the per-frame item lists are what our pipeline made of
// it.
struct RecordedDraw {
uint32_t frame;
uint32_t func; // 0 = DrawIndexedVertices, 1 = DrawVertices, 2 = BeginVertices
uint32_t ib;
uint32_t vb;
uint32_t vb_offset; // stream-0 OffsetInBytes at bind time
uint32_t vb_stride; // stream-0 stride at bind time
uint32_t streams[4][3]; // all streams: {vb, offset, stride}
uint32_t vfetch[12]; // fetch-constant shadow groups 0-1 (device+0x480)
uint32_t args[4];
float bank[1024]; // VS c0..c255
float ps[256]; // PS c0..c63 (material colors, e.g. CAS hair tint)
// 2D recon fields (SK3DRAW7):
uint32_t flags2d; // bit per active 2D bracket (see On2dPhase)
uint32_t ps_obj; // current guest pixel/vertex shader objects
uint32_t vs_obj;
uint32_t viewport[6]; // last SetViewport payload (raw guest dwords)
uint32_t scissor[4]; // last SetScissorRect payload
uint32_t rstates[256]; // render-state shadow snapshot (blend/depth)
uint32_t vfetch_all[192]; // full 32-slot texture fetch shadow
uint32_t vb_dump; // index into buffers.bin for this draw's payloads
uint32_t ib_dump; // (~0u = none)
};
struct RecordedFrame {
uint64_t generation;
float view_proj[16];
float cam_pos[3];
std::vector<DrawItem> dynitems;
std::vector<DrawItem> items;
};
// Dynamic meshes live in streaming arenas that are recycled DURING a long
// recording; the end-of-window .gsnap holds garbage for them. Dump each
// captured buffer payload once per content fingerprint so offline tools
// decode exactly what the game used.
struct RecordedBuffer {
uint32_t vb_addr;
uint32_t ib_addr;
uint64_t fingerprint;
std::vector<uint8_t> vb;
std::vector<uint8_t> ib;
};
// BeginVertices (inline ring) payloads are written by the CPU AFTER the
// call returns; dump them at frame end, when the writes are complete but
// the ring has not yet wrapped. draw_index links the dump back to its
// RecordedDraw.
struct PendingInlineDump {
uint32_t addr;
uint32_t bytes;
size_t draw_index;
};
extern std::mutex g_record_mutex;
extern std::atomic<bool> g_recording;
extern uint32_t g_record_frame; // index of the NEXT recorded frame
extern uint32_t g_record_stride; // record every Nth frame
extern uint32_t g_frames_seen; // frames completed since StartRecording
extern std::vector<std::unique_ptr<RecordedDraw>> g_recorded_draws;
extern std::vector<RecordedFrame> g_recorded_frames;
extern std::vector<RecordedBuffer> g_recorded_buffers;
extern std::unordered_set<uint64_t> g_recorded_buffer_keys;
extern size_t g_recorded_buffer_bytes;
extern std::vector<PendingInlineDump> g_pending_inline_dumps;
// One payload dump per (guest address, frame): 2D dynamic buffers hold many
// draws' vertices; the repeated full-buffer dump would be pure duplication.
extern std::unordered_map<uint64_t, uint32_t> g_frame_dump_ids;
// ---- Camera-signal recorder (judder diagnosis) ------------------------------
// Records the guest camera SIGNAL during a real stick pan: every distinct
// pose the ~1 kHz sampler accepts (kind 0, precise host timestamps), the raw
// per-frame pose at scene build (kind 2) and the smoother's output (kind 1,
// with its playback time). Written as logs/cam_signal_<ts>.csv when the
// window closes. Entry storage is private to the diagnostics TU; call sites
// gate on the deadline before computing anything.
extern std::atomic<double> g_camsig_deadline; // record until this host time
// Heading from a row-vector view matrix: camera forward in world space is
// the view rotation's third column (v[2], v[6], v[10]).
float YawFromViewRows(const float view[16]);
// Append one entry while the window is open (bounded; thread-safe).
void CamSigPush(double t, double play_t, float yaw, uint8_t kind);
// Per-frame tick (guest render thread, scene build): appends the raw frame
// pose (kind 2) and, when smoothed_rot is non-null, the smoother's output
// (kind 1 at play_t); writes + resets once the window closes. Call only when
// g_camsig_deadline > 0.
void CamSigFrameTick(double rec_now, const float cam_view[16],
const float* smoothed_rot, double play_t);
// ---- Bone-signal recorder (wheel-guard diagnosis) ---------------------------
// Records the raw per-entity pose stream (every ring push: bone palettes /
// rigid worlds with timestamps, kind 0) plus what the interpolation actually
// output each frame (kind 1, with the playback time) to
// logs/bone_signal_<ts>.bin. Binary records after a "BSIG1\n" header:
// u8 kind, u64 key, f64 t, f64 play, u32 n, float[n]. Guest render thread
// only (written off-thread at close).
//
// Per-frame arm/flush tick (guest render thread): consumes a pending
// RecordBoneSignal request, returns true while the window is open, and
// spawns the file write once it closes.
bool BoneSigTick(double now);
void BoneSigAppend(uint8_t kind, uint64_t key, double t, double play, const float* v,
uint32_t n);
// ---- Synthetic camera pan (judder isolation probe) --------------------------
// See the skate3_native_render_scene_synthetic_pan cvar comment for the mode
// semantics. Engage state is captured from the RAW guest camera on the guest
// render thread; the sampler thread reads it for mode 3 under g_synpan_mutex.
extern std::atomic<int> g_synpan_active; // engaged mode (0 = off)
extern std::mutex g_synpan_mutex;
extern float g_synpan_view0[16]; // raw guest view at engage
extern float g_synpan_proj0[16];
extern float g_synpan_c0[3]; // camera world position at engage (held fixed)
extern double g_synpan_t0;
extern double g_synpan_step_phase; // mode 2: accumulated phase (degrees)
extern double g_synpan_ema_dt; // mode 2: slow EMA of the publish dt
// Telemetry (guest render thread only).
extern uint64_t g_synpan_frames;
extern double g_synpan_last_build;
extern double g_synpan_dt_sum, g_synpan_dt_sum2;
extern double g_synpan_dt_min, g_synpan_dt_max;
extern double g_synpan_err_sum2, g_synpan_err_max;
extern uint64_t g_synpan_err_n;
// Full-surround world union for the probe: the game only submits what ITS
// frustum sees each frame, so a synthetic pan away from the real heading
// would show an empty world (sky dome only). While the probe is engaged,
// every static item published is accumulated by identity and appended to
// each frame's scene; sweep the REAL camera around once with the stick
// after engaging and the union fills in the whole surround, making a full
// 360-degree spin (amplitude 0) usable. Guest render thread only.
extern std::unordered_map<uint64_t, DrawItem> g_synpan_union;
uint64_t SynPanItemKey(const DrawItem& it);
// Pan phase (degrees of accumulated rotation) -> heading angle in degrees.
// Triangle wave in [-amp, +amp] starting at 0 heading upward; amp <= 0 =
// unbounded continuous rotation.
double SynPanAngleDeg(double phase_deg, double amp);
// View matrix for the engage pose yawed by `angle_deg` about world up (+Y),
// camera position held at the engage position. Row-vector convention
// throughout (p_view = p * V): the 3x3 becomes Ry * R0 (world rotates first,
// then the engage view, a level pan; direction sign is irrelevant for the
// probe), translation row rebuilt as -c0 * R'.
void SynPanView(double angle_deg, float view_out[16]);
} // namespace skate3::native_scene

View file

@ -0,0 +1,388 @@
// Native renderer diagnostics: offline-analysis recording, camera/bone
// signal recorders, synthetic-pan probe. Extracted verbatim from
// skate3_native_scene.cpp; no rendering logic lives here.
#include "native/skate3_native_diag.h"
#include <algorithm>
#include <bit>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <thread>
#include <rex/logging.h>
namespace skate3::native_scene {
// ---- Offline-analysis recording ---------------------------------------------
std::mutex g_record_mutex;
std::atomic<bool> g_recording{false};
uint32_t g_record_frame = 0; // index of the NEXT recorded frame
uint32_t g_record_stride = 1; // record every Nth frame
uint32_t g_frames_seen = 0; // frames completed since StartRecording
std::vector<std::unique_ptr<RecordedDraw>> g_recorded_draws;
std::vector<RecordedFrame> g_recorded_frames;
std::vector<RecordedBuffer> g_recorded_buffers;
std::unordered_set<uint64_t> g_recorded_buffer_keys;
size_t g_recorded_buffer_bytes = 0;
std::vector<PendingInlineDump> g_pending_inline_dumps;
std::unordered_map<uint64_t, uint32_t> g_frame_dump_ids;
void StartRecording(uint32_t stride) {
std::lock_guard<std::mutex> lock(g_record_mutex);
g_recorded_draws.clear();
g_recorded_frames.clear();
g_recorded_buffers.clear();
g_recorded_buffer_keys.clear();
g_recorded_buffer_bytes = 0;
g_pending_inline_dumps.clear();
g_frame_dump_ids.clear();
g_record_frame = 0;
g_frames_seen = 0;
g_record_stride = stride == 0 ? 1 : stride;
g_recording.store(true, std::memory_order_relaxed);
}
void WriteRecording(const char* dir, const char* stem) {
g_recording.store(false, std::memory_order_relaxed);
std::lock_guard<std::mutex> lock(g_record_mutex);
const std::filesystem::path base_path = std::filesystem::path(dir) / stem;
// Binary draw stream: header "SK3DRAW7", fixed records {u32 frame, u32
// func, u32 ib, u32 vb, u32 vb_offset, u32 vb_stride, u32 streams[4][3],
// u32 vfetch[12], u32 args[4], f32 vs_bank[1024], f32 ps_bank[256],
// u32 flags2d, u32 ps_obj, u32 vs_obj, u32 viewport[6], u32 scissor[4],
// u32 rstates[256], u32 vfetch_all[192], u32 vb_dump, u32 ib_dump}
// (little-endian). func: 0 DrawIndexedVertices, 1 DrawVertices,
// 2 BeginVertices. flags2d: bit0 FrontEndManager::Render2D, bit1
// AptMovieIntegration::Render, bit2 DrawRenderingUnit. vb_dump/ib_dump
// index into buffers.bin records (~0u = none).
{
std::ofstream out(base_path.string() + ".draws.bin", std::ios::binary);
out.write("SK3DRAW7", 8);
for (const auto& d : g_recorded_draws) {
out.write(reinterpret_cast<const char*>(&d->frame), 4);
out.write(reinterpret_cast<const char*>(&d->func), 4);
out.write(reinterpret_cast<const char*>(&d->ib), 4);
out.write(reinterpret_cast<const char*>(&d->vb), 4);
out.write(reinterpret_cast<const char*>(&d->vb_offset), 4);
out.write(reinterpret_cast<const char*>(&d->vb_stride), 4);
out.write(reinterpret_cast<const char*>(d->streams), 48);
out.write(reinterpret_cast<const char*>(d->vfetch), 48);
out.write(reinterpret_cast<const char*>(d->args), 16);
out.write(reinterpret_cast<const char*>(d->bank), 4096);
out.write(reinterpret_cast<const char*>(d->ps), 1024);
out.write(reinterpret_cast<const char*>(&d->flags2d), 4);
out.write(reinterpret_cast<const char*>(&d->ps_obj), 4);
out.write(reinterpret_cast<const char*>(&d->vs_obj), 4);
out.write(reinterpret_cast<const char*>(d->viewport), 24);
out.write(reinterpret_cast<const char*>(d->scissor), 16);
out.write(reinterpret_cast<const char*>(d->rstates), 1024);
out.write(reinterpret_cast<const char*>(d->vfetch_all), 768);
out.write(reinterpret_cast<const char*>(&d->vb_dump), 4);
out.write(reinterpret_cast<const char*>(&d->ib_dump), 4);
}
}
// Captured buffer payloads: header "SK3BUFS1", then records {u32 vb_addr,
// u32 ib_addr, u64 fingerprint, u32 vb_len, u32 ib_len, raw vb, raw ib}.
{
std::ofstream out(base_path.string() + ".buffers.bin", std::ios::binary);
out.write("SK3BUFS1", 8);
for (const RecordedBuffer& b : g_recorded_buffers) {
const uint32_t vb_len = uint32_t(b.vb.size());
const uint32_t ib_len = uint32_t(b.ib.size());
out.write(reinterpret_cast<const char*>(&b.vb_addr), 4);
out.write(reinterpret_cast<const char*>(&b.ib_addr), 4);
out.write(reinterpret_cast<const char*>(&b.fingerprint), 8);
out.write(reinterpret_cast<const char*>(&vb_len), 4);
out.write(reinterpret_cast<const char*>(&ib_len), 4);
out.write(reinterpret_cast<const char*>(b.vb.data()), vb_len);
out.write(reinterpret_cast<const char*>(b.ib.data()), ib_len);
}
}
// Per-frame item dump.
{
std::ofstream out(base_path.string() + ".scene.jsonl");
const auto write_item = [&out](const DrawItem& d) {
out << "{\"mesh\":\"" << std::hex << d.mesh << "\",\"ib_obj\":\"" << d.ib_obj
<< "\",\"vb_obj\":\"" << d.vb_obj << "\",\"vb_addr\":\"" << d.vb_addr
<< "\",\"ib_addr\":\"" << d.ib_addr << "\",\"diffuse\":\"" << d.diffuse_tex
<< "\",\"diffuse_fetch\":\"" << d.diffuse_fetch[1]
<< "\",\"lightmap\":\"" << d.lightmap_tex << "\",\"macro\":\"" << d.macro_tex
<< "\",\"decal_art\":\"" << d.decal_art
<< "\",\"decal_fetch\":\"" << d.decal_fetch[1]
<< "\",\"fp\":\"" << d.fingerprint
<< "\"" << std::dec << ",\"vb_bytes\":" << d.vb_bytes << ",\"ib_count\":"
<< d.ib_count << ",\"stride\":" << int(d.stride) << ",\"pos_fmt\":"
<< int(d.pos_fmt) << ",\"pos_off\":" << d.pos_offset << ",\"bw_off\":"
<< d.bw_offset << ",\"bi_off\":" << d.bi_offset << ",\"skinned\":"
<< (d.skinned ? 1 : 0) << ",\"pending\":" << (d.pending ? 1 : 0)
<< ",\"decal\":" << (d.decal ? 1 : 0)
<< ",\"transparent\":" << (d.transparent ? 1 : 0)
<< ",\"selected\":" << (d.selected ? 1 : 0) << ",\"world\":[";
for (int i = 0; i < 16; ++i) out << (i ? "," : "") << d.world[i];
out << "],\"draws\":[";
for (size_t i = 0; i < d.draws.size(); ++i) {
const DrawEntry& e = d.draws[i];
out << (i ? "," : "") << "[" << e.prim << "," << e.base_vertex << ","
<< e.start_index << "," << e.index_count << "]";
}
out << "],\"bones\":[";
for (size_t i = 0; i < d.bones.size(); ++i) out << (i ? "," : "") << d.bones[i];
out << "]}";
};
for (const RecordedFrame& rf : g_recorded_frames) {
out << "{\"generation\":" << rf.generation << ",\"cam\":[" << rf.cam_pos[0] << ","
<< rf.cam_pos[1] << "," << rf.cam_pos[2] << "],\"view_proj\":[";
for (int i = 0; i < 16; ++i) out << (i ? "," : "") << rf.view_proj[i];
out << "],\"dynitems\":[";
for (size_t i = 0; i < rf.dynitems.size(); ++i) {
if (i) out << ",";
write_item(rf.dynitems[i]);
}
out << "],\"items\":[";
for (size_t i = 0; i < rf.items.size(); ++i) {
if (i) out << ",";
write_item(rf.items[i]);
}
out << "]}\n";
}
}
REXLOG_INFO(
"native-scene: recording written ({} draws, {} frames, {} buffers {} MiB) -> "
"{}.draws.bin/.scene.jsonl/.buffers.bin",
g_recorded_draws.size(), g_recorded_frames.size(), g_recorded_buffers.size(),
g_recorded_buffer_bytes >> 20, base_path.string());
g_recorded_draws.clear();
g_recorded_frames.clear();
g_recorded_buffers.clear();
g_recorded_buffer_keys.clear();
g_recorded_buffer_bytes = 0;
g_pending_inline_dumps.clear();
g_frame_dump_ids.clear();
}
// ---- Camera-signal recorder ---------------------------------------------------
namespace {
struct CamSigEntry {
double t; // host time (sampler push / frame build)
double play_t; // kind 1 only: the smoother's playback time
float yaw; // heading, degrees
uint8_t kind; // 0 = raw sampler pose, 1 = smoothed frame, 2 = raw frame
};
std::mutex g_camsig_mutex;
std::vector<CamSigEntry> g_camsig;
} // namespace
std::atomic<double> g_camsig_deadline{0.0};
float YawFromViewRows(const float view[16]) {
return std::atan2(view[2], view[10]) * float(180.0 / 3.14159265358979323846);
}
void CamSigPush(double t, double play_t, float yaw, uint8_t kind) {
std::lock_guard<std::mutex> lock(g_camsig_mutex);
if (g_camsig.size() < 200000) {
g_camsig.push_back({t, play_t, yaw, kind});
}
}
void CamSigFrameTick(double rec_now, const float cam_view[16],
const float* smoothed_rot, double play_t) {
const double dl = g_camsig_deadline.load(std::memory_order_relaxed);
if (dl <= 0.0) {
return;
}
if (rec_now < dl) {
std::lock_guard<std::mutex> lock(g_camsig_mutex);
if (g_camsig.size() < 200000) {
g_camsig.push_back({rec_now, 0.0, YawFromViewRows(cam_view), 2});
if (smoothed_rot) {
g_camsig.push_back({rec_now, play_t, YawFromViewRows(smoothed_rot), 1});
}
}
} else {
std::vector<CamSigEntry> entries;
{
std::lock_guard<std::mutex> lock(g_camsig_mutex);
entries.swap(g_camsig);
}
g_camsig_deadline.store(0.0, std::memory_order_release);
std::thread([entries = std::move(entries)]() {
std::error_code ec;
std::filesystem::create_directories("logs", ec);
char path[128];
std::snprintf(path, sizeof(path), "logs/cam_signal_%lld.csv",
static_cast<long long>(std::time(nullptr)));
std::ofstream f(path);
f << "kind,t,play_t,yaw_deg\n";
char line[128];
for (const CamSigEntry& e : entries) {
std::snprintf(line, sizeof(line), "%d,%.6f,%.6f,%.5f\n", int(e.kind), e.t,
e.play_t, double(e.yaw));
f << line;
}
REXLOG_INFO("native-scene cam-signal: wrote {} entries -> {}", entries.size(),
path);
}).detach();
}
}
void RecordCameraSignal(double seconds) {
{
std::lock_guard<std::mutex> lock(g_camsig_mutex);
g_camsig.clear();
g_camsig.reserve(size_t(seconds * 1400.0));
}
const double now = std::chrono::duration<double>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
g_camsig_deadline.store(now + seconds, std::memory_order_release);
REXLOG_INFO(
"native-scene cam-signal: recording {} s; pan the camera with the stick at a "
"steady rate NOW",
seconds);
}
// ---- Bone-signal recorder -----------------------------------------------------
namespace {
std::vector<uint8_t> g_bonesig;
std::atomic<double> g_bonesig_deadline{0.0};
// Armed from the UI thread (F12 button), consumed on the guest thread; the
// blob itself is guest-thread-only.
std::atomic<double> g_bonesig_request{0.0};
} // namespace
void BoneSigAppend(uint8_t kind, uint64_t key, double t, double play, const float* v,
uint32_t n) {
if (g_bonesig.size() > (200u << 20)) {
return; // runaway cap
}
const size_t off = g_bonesig.size();
g_bonesig.resize(off + 29 + size_t(n) * 4);
uint8_t* p = g_bonesig.data() + off;
*p = kind;
std::memcpy(p + 1, &key, 8);
std::memcpy(p + 9, &t, 8);
std::memcpy(p + 17, &play, 8);
std::memcpy(p + 25, &n, 4);
std::memcpy(p + 29, v, size_t(n) * 4);
}
bool BoneSigTick(double now) {
const double req = g_bonesig_request.exchange(0.0, std::memory_order_acq_rel);
if (req > 0.0) {
g_bonesig.clear();
g_bonesig.reserve(16u << 20);
g_bonesig_deadline.store(now + req, std::memory_order_relaxed);
}
const double dl = g_bonesig_deadline.load(std::memory_order_relaxed);
if (dl <= 0.0) {
return false;
}
if (now < dl) {
return true;
}
std::vector<uint8_t> blob;
blob.swap(g_bonesig);
g_bonesig_deadline.store(0.0, std::memory_order_release);
std::thread([blob = std::move(blob)]() {
std::error_code ec;
std::filesystem::create_directories("logs", ec);
char path[128];
std::snprintf(path, sizeof(path), "logs/bone_signal_%lld.bin",
static_cast<long long>(std::time(nullptr)));
std::ofstream f(path, std::ios::binary);
f.write("BSIG1\n", 6);
f.write(reinterpret_cast<const char*>(blob.data()), std::streamsize(blob.size()));
REXLOG_INFO("native-scene bone-signal: wrote {} bytes -> {}", blob.size() + 6,
path);
}).detach();
return false;
}
void RecordBoneSignal(double seconds) {
g_bonesig_request.store(seconds, std::memory_order_release);
REXLOG_INFO(
"native-scene bone-signal: recording {} s; skate past the camera at a steady "
"speed NOW",
seconds);
}
// ---- Synthetic camera pan -----------------------------------------------------
std::atomic<int> g_synpan_active{0}; // engaged mode (0 = off)
std::mutex g_synpan_mutex;
float g_synpan_view0[16]; // raw guest view at engage
float g_synpan_proj0[16];
float g_synpan_c0[3]; // camera world position at engage (held fixed)
double g_synpan_t0 = 0.0;
double g_synpan_step_phase = 0.0; // mode 2: accumulated phase (degrees)
double g_synpan_ema_dt = 0.0; // mode 2: slow EMA of the publish dt
uint64_t g_synpan_frames = 0;
double g_synpan_last_build = 0.0;
double g_synpan_dt_sum = 0.0, g_synpan_dt_sum2 = 0.0;
double g_synpan_dt_min = 0.0, g_synpan_dt_max = 0.0;
double g_synpan_err_sum2 = 0.0, g_synpan_err_max = 0.0;
uint64_t g_synpan_err_n = 0;
std::unordered_map<uint64_t, DrawItem> g_synpan_union;
uint64_t SynPanItemKey(const DrawItem& it) {
uint64_t h = 1469598103934665603ull;
const auto mix = [&h](uint64_t v) {
h ^= v;
h *= 1099511628211ull;
};
mix(it.mesh);
mix(it.vb_obj);
mix(it.ib_obj);
mix(std::bit_cast<uint32_t>(it.world[12]));
mix(std::bit_cast<uint32_t>(it.world[13]));
mix(std::bit_cast<uint32_t>(it.world[14]));
return h;
}
double SynPanAngleDeg(double phase_deg, double amp) {
if (amp <= 0.0) {
return phase_deg;
}
const double period = 4.0 * amp;
double m = std::fmod(phase_deg, period);
if (m < 0.0) {
m += period;
}
return m < amp ? m : (m < 3.0 * amp ? 2.0 * amp - m : m - period);
}
void SynPanView(double angle_deg, float view_out[16]) {
const double a = angle_deg * (3.14159265358979323846 / 180.0);
const float c = float(std::cos(a)), s = float(std::sin(a));
const float ry[3][3] = {{c, 0.0f, -s}, {0.0f, 1.0f, 0.0f}, {s, 0.0f, c}};
std::memset(view_out, 0, 16 * sizeof(float));
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
float sum = 0.0f;
for (int k = 0; k < 3; ++k) {
sum += ry[i][k] * g_synpan_view0[k * 4 + j];
}
view_out[i * 4 + j] = sum;
}
}
for (int k = 0; k < 3; ++k) {
view_out[12 + k] = -(g_synpan_c0[0] * view_out[0 * 4 + k] +
g_synpan_c0[1] * view_out[1 * 4 + k] +
g_synpan_c0[2] * view_out[2 * 4 + k]);
}
view_out[15] = 1.0f;
}
} // namespace skate3::native_scene

View file

@ -0,0 +1,256 @@
// Shared sample-vertex decode + skin + spread helpers (see the header).
// Extracted verbatim from the five open-coded copies in
// skate3_native_scene.cpp, behavior-preserving.
#include "native/skate3_native_guest_read.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include "generated/skate3_init.h"
namespace skate3::native_scene {
float GuestHalfToFloat(uint16_t h) {
const uint32_t sign = uint32_t(h & 0x8000u) << 16;
const uint32_t exp = (h >> 10) & 0x1F;
const uint32_t man = h & 0x3FF;
if (exp == 0) return std::bit_cast<float>(sign); // denorms ~0
if (exp == 31) return std::bit_cast<float>(sign | 0x7F800000u);
return std::bit_cast<float>(sign | ((exp + 112) << 23) | (man << 13));
}
float BindDiag(const DrawItem& item) {
float d2 = 0.0f;
for (int a = 0; a < 3; ++a) {
const float d = item.bbox_max[a] - item.bbox_min[a];
d2 += d * d;
}
return std::sqrt(d2);
}
namespace {
bool PosFinite(const float p[3]) {
return p[0] > -1e7f && p[0] < 1e7f && p[1] > -1e7f && p[1] < 1e7f &&
p[2] > -1e7f && p[2] < 1e7f;
}
} // namespace
bool ReadSkinVertGuest(uint8_t* base, const DrawItem& item, uint32_t vtx,
SkinSampleVert* out) {
const uint32_t v = item.vb_addr + vtx * item.stride;
const uint32_t pa = v + item.pos_offset;
float* p = out->p;
switch (item.pos_fmt) {
case 57:
for (int a = 0; a < 3; ++a) {
const uint32_t u = REX_LOAD_U32(pa + a * 4);
std::memcpy(&p[a], &u, 4);
}
break;
case 32:
for (int a = 0; a < 3; ++a) {
p[a] = GuestHalfToFloat(uint16_t(REX_LOAD_U16(pa + a * 2)));
}
break;
case 26: {
constexpr float kScale = 2.0f / 32767.0f;
for (int a = 0; a < 3; ++a) {
p[a] = int16_t(REX_LOAD_U16(pa + a * 2)) * kScale + (a == 1 ? 0.8f : 0.0f);
}
break;
}
default:
return false;
}
out->pos_finite = PosFinite(p);
if (item.bw_offset != 0 && item.bi_offset != 0) {
// u8x4 attributes are big-endian per 32-bit word: component k is byte
// (24 - 8k) of the host-order load.
const uint32_t bw = REX_LOAD_U32(v + item.bw_offset);
const uint32_t bi = REX_LOAD_U32(v + item.bi_offset);
for (int k = 0; k < 4; ++k) {
out->w[k] = uint8_t((bw >> (24 - 8 * k)) & 0xFF);
out->bone[k] = uint8_t((bi >> (24 - 8 * k)) & 0xFF);
}
} else {
std::memset(out->w, 0, sizeof(out->w));
std::memset(out->bone, 0, sizeof(out->bone));
}
return true;
}
bool ReadSkinSamplesGuest(uint8_t* base, const DrawItem& item, uint32_t n,
SkinSampleVert* out) {
if (item.stride == 0) {
return false;
}
const uint32_t count = item.vb_bytes / item.stride;
if (count < 2 || n < 2) {
return false;
}
for (uint32_t s = 0; s < n; ++s) {
if (!ReadSkinVertGuest(base, item, s * (count - 1) / (n - 1), &out[s])) {
return false;
}
}
return true;
}
int ReadSkinSamplesRaw(const uint8_t* vb, size_t vb_size, const DrawItem& item,
uint32_t n, SkinSampleVert* out) {
if (item.stride == 0) {
return -1;
}
const uint32_t count = item.vb_bytes / item.stride;
if (count < 2 || n < 2) {
return -1;
}
for (uint32_t s = 0; s < n; ++s) {
const uint32_t v = (s * (count - 1) / (n - 1)) * item.stride;
if (uint64_t(v) + item.stride > vb_size) {
return int(s); // short read: caller treats as nothing-to-judge
}
const uint8_t* vp = vb + v;
SkinSampleVert& sv = out[s];
float* p = sv.p;
// Guest payload copied raw = big-endian attributes.
switch (item.pos_fmt) {
case 57:
for (int a = 0; a < 3; ++a) {
const uint8_t* b = vp + item.pos_offset + a * 4;
const uint32_t w = uint32_t(b[0]) << 24 | uint32_t(b[1]) << 16 |
uint32_t(b[2]) << 8 | b[3];
p[a] = std::bit_cast<float>(w);
}
break;
case 32:
for (int a = 0; a < 3; ++a) {
const uint8_t* b = vp + item.pos_offset + a * 2;
p[a] = GuestHalfToFloat(uint16_t(uint16_t(b[0]) << 8 | b[1]));
}
break;
case 26: {
constexpr float kScale = 2.0f / 32767.0f;
for (int a = 0; a < 3; ++a) {
const uint8_t* b = vp + item.pos_offset + a * 2;
p[a] = int16_t(uint16_t(b[0]) << 8 | b[1]) * kScale + (a == 1 ? 0.8f : 0.0f);
}
break;
}
default:
return -1;
}
sv.pos_finite = PosFinite(p);
if (item.bw_offset != 0 && item.bi_offset != 0) {
// Component k = guest byte k (matches the live path's
// byte-(24-8k)-of-host-load convention).
const uint8_t* bw = vp + item.bw_offset;
const uint8_t* bi = vp + item.bi_offset;
for (int k = 0; k < 4; ++k) {
sv.w[k] = bw[k];
sv.bone[k] = bi[k];
}
} else {
std::memset(sv.w, 0, sizeof(sv.w));
std::memset(sv.bone, 0, sizeof(sv.bone));
}
}
return int(n);
}
uint32_t SkinPointHostRows(const SkinSampleVert& sv, const float* rows,
size_t rows_floats, float q[3]) {
uint32_t total = 0;
q[0] = q[1] = q[2] = 0.0f;
for (int k = 0; k < 4; ++k) {
const uint32_t w = sv.w[k];
if (w == 0) continue;
const uint32_t r0 = 3u * sv.bone[k];
if ((r0 + 3) * 4 > rows_floats) continue;
total += w;
for (int a = 0; a < 3; ++a) {
const float* row = rows + (r0 + uint32_t(a)) * 4;
q[a] += float(w) * (row[0] * sv.p[0] + row[1] * sv.p[1] + row[2] * sv.p[2] +
row[3]);
}
}
if (total != 0) {
for (int a = 0; a < 3; ++a) q[a] /= float(total);
}
return total;
}
uint32_t SkinPointBankRows(uint8_t* base, uint32_t bank, uint32_t pb,
const SkinSampleVert& sv, float q[3], bool* rows_sane) {
uint32_t total = 0;
q[0] = q[1] = q[2] = 0.0f;
for (int k = 0; k < 4; ++k) {
const uint32_t w = sv.w[k];
if (w == 0) continue;
const uint32_t r0 = pb + 3 * uint32_t(sv.bone[k]);
if (r0 + 3 > 256) continue;
total += w;
for (int a = 0; a < 3; ++a) {
float row[4];
for (int i = 0; i < 4; ++i) {
row[i] = std::bit_cast<float>(REX_LOAD_U32(bank + ((r0 + a) * 4 + i) * 4));
}
// A weighted bone's rotation row must be rotation-shaped (norm near
// the entity scale). The vehicle-flick captures carried WORLD
// POSITIONS in the rotation slots (norm ~140); those can still
// project on-screen and pass the geometric gate.
const float rn = row[0] * row[0] + row[1] * row[1] + row[2] * row[2];
if (rn < 0.04f || rn > 25.0f) {
*rows_sane = false;
}
q[a] += float(w) * (row[0] * sv.p[0] + row[1] * sv.p[1] + row[2] * sv.p[2] +
row[3]);
}
}
if (total != 0) {
for (int a = 0; a < 3; ++a) q[a] /= float(total);
}
return total;
}
int SkinnedSpreadHostRows(const SkinSampleVert* sv, uint32_t n, const float* rows,
size_t rows_floats, int min_n, bool garbage_fails,
float* out_spread, int* out_n) {
float qmin[3] = {1e9f, 1e9f, 1e9f};
float qmax[3] = {-1e9f, -1e9f, -1e9f};
int weighted = 0;
for (uint32_t s = 0; s < n; ++s) {
if (!sv[s].pos_finite) {
if (garbage_fails) {
return -1; // NaN/garbage positions mid-sim-write
}
continue;
}
float q[3];
if (SkinPointHostRows(sv[s], rows, rows_floats, q) == 0) {
continue;
}
++weighted;
for (int a = 0; a < 3; ++a) {
qmin[a] = std::min(qmin[a], q[a]);
qmax[a] = std::max(qmax[a], q[a]);
}
}
if (out_n) {
*out_n = weighted;
}
if (weighted < min_n) {
return 0; // nothing to judge
}
const float dx = qmax[0] - qmin[0];
const float dy = qmax[1] - qmin[1];
const float dz = qmax[2] - qmin[2];
*out_spread = std::sqrt(dx * dx + dy * dy + dz * dz);
return 1;
}
} // namespace skate3::native_scene

View file

@ -0,0 +1,86 @@
#pragma once
// Shared sample-vertex decode + skin + spread helpers for the native scene
// renderer: the ONE copy of the decode+skin loop that was open-coded five
// times (RefinePaletteBase, ScoreRigidAffine, RopaPayloadCoherent,
// PublishedPaletteSane, the InterpolateDynamicItems centroid pass).
//
// Decode conventions unified here (verified against the original sites):
// - guest u8x4 blend attribs are big-endian per 32-bit word: influence k is
// guest byte k, i.e. byte (24 - 8k) of the host-order load;
// - position fmt 57 = float3, 32 = half4 (xyz), 26 = s16x4 with scale
// 2/32767 and the +0.8 Y bias;
// - a raw copied payload holds the same guest bytes (big-endian).
#include <bit>
#include <cstddef>
#include <cstdint>
#include "skate3_native_scene.h"
namespace skate3::native_scene {
// One decoded vertex: bind-space position + u8x4 blend attribs (influence k
// = w[k]/bone[k]; all zero when the item carries no blend attributes).
struct SkinSampleVert {
float p[3];
uint8_t w[4];
uint8_t bone[4];
bool pos_finite; // every |p| < 1e7 (mid-sim-write garbage detection)
};
float GuestHalfToFloat(uint16_t h);
// Bind-pose bbox diagonal of the item: the reference size every
// spread-vs-bind coherence gate compares against.
float BindDiag(const DrawItem& item);
// Decode vertex `vtx` (index) of the item's LIVE guest VB. Blend attribs are
// read only when both offsets are non-zero. Returns false on an unsupported
// position format.
bool ReadSkinVertGuest(uint8_t* base, const DrawItem& item, uint32_t vtx,
SkinSampleVert* out);
// Decode `n` evenly spaced sample verts (index s*(count-1)/(n-1)) of the
// item's LIVE guest VB. Returns false when the position format is
// unsupported or the VB holds fewer than 2 vertices.
bool ReadSkinSamplesGuest(uint8_t* base, const DrawItem& item, uint32_t n,
SkinSampleVert* out);
// Same sampling from a COPIED raw payload (big-endian guest bytes). Sample
// addressing uses the item's stride/count (item.vb_bytes), bounds-checked
// against the copy: returns the number of samples decoded before the first
// out-of-bounds sample (callers treat a short read as "nothing to judge"),
// or -1 when the position format is unsupported / count < 2.
int ReadSkinSamplesRaw(const uint8_t* vb, size_t vb_size, const DrawItem& item,
uint32_t n, SkinSampleVert* out);
// Weighted [R | t] skin with HOST palette rows (DrawItem::bones layout:
// 3 float4 rows per bone, column-vector affine). Influences whose rows fall
// outside the palette are skipped. Returns the total u8 weight (0 = nothing
// weighted); q is the weight-normalized skinned position when total > 0.
uint32_t SkinPointHostRows(const SkinSampleVert& sv, const float* rows,
size_t rows_floats, float q[3]);
// Weighted skin with the palette staged in a guest VS constant bank at base
// register pb (3 rows per bone). Influences past register 256 are skipped.
// *rows_sane is CLEARED when any weighted rotation row's xyz norm falls
// outside [0.04, 25]; world positions in rotation slots can still project
// on-screen and pass the geometric gates.
uint32_t SkinPointBankRows(uint8_t* base, uint32_t bank, uint32_t pb,
const SkinSampleVert& sv, float q[3], bool* rows_sane);
// Spread (bbox diagonal) of the decoded samples skinned with host palette
// rows: the shared core of the three coherence gates. Samples with a
// non-finite position either fail the gate (garbage_fails, the frame-end
// ropa check: mid-sim-write garbage means incoherent) or are skipped (the
// publish-time gate). Returns:
// -1 = garbage position encountered (garbage_fails only),
// 0 = fewer than min_n weighted samples (nothing to judge),
// 1 = *out_spread measured.
// *out_n (optional) receives the weighted-sample count.
int SkinnedSpreadHostRows(const SkinSampleVert* sv, uint32_t n, const float* rows,
size_t rows_floats, int min_n, bool garbage_fails,
float* out_spread, int* out_n = nullptr);
} // namespace skate3::native_scene

File diff suppressed because it is too large Load diff