render: port the water and ocean shaders to exact native branches

This commit is contained in:
mchughalex 2026-07-19 13:02:10 +01:00
parent fc9c9fdbcb
commit 75c8487ee6
7 changed files with 6622 additions and 4414 deletions

View file

@ -70,6 +70,30 @@ cbuffer S : register(b1) {
float4 dyn_wsx;
float4 dyn_wsy;
float4 dyn_wsz;
// flowingwateralpha m_params rows (captured per frame from the water PS
// bank; consumed by the exact water branch, cam_pos.w = -30):
float4 wat_p0; // m_params[0]: tangent-normal scale xzw, material mult y
float4 wat_p1; // m_params[1]: animated-UV scroll speeds (tap1 xy, tap2 zw)
float4 wat_p2; // m_params[2]: tap UV scales (tap1 xy, tap2 zw)
float4 wat_p3; // x = g_fAnimationTime, y = mask threshold,
// z = spec power, w = alpha floor
// ocean_defaultPS rows (exact ocean branch, cam_pos.w = -31): the PCA
// mean (pre-swizzled to R,G,B channel order) + the six weight rows
// (component-0/1 pairs for R, G, B) + m_params[0..2].
float4 oc_mean;
float4 oc_w0; // R, component 0
float4 oc_w1; // R, component 1
float4 oc_w2; // G, component 0
float4 oc_w3; // G, component 1
float4 oc_w4; // B, component 0
float4 oc_w5; // B, component 1
float4 oc_p0; // m_params[0]: ward spec color
float4 oc_p1; // m_params[1]: y = multiplier, z = tonedown scale,
// w = PCA-tap uv scale
float4 oc_p2; // m_params[2]: ward spread u/v (xy), roughness (z)
// oceanreflection_defaultPS row (fam 32): x = multiplier, y/z = the
// world-height alpha fade bounds.
float4 orf;
};
// Character-family lighting (defaultcharacter.fx and friends): canonical
// per-draw rows captured from the guest PIXEL constant bank at palette
@ -365,7 +389,8 @@ float4 ps_main(VSOut i) : SV_Target {
// GetTangentLight kd replaces the flat fold, and the phong spec returns.
// With no maps bound the flat-map fold 0.39 * 2.39562 = 0.93429 and the
// vertex normal apply (the v1 look).
if (cam_pos.w < -20.5) {
if (cam_pos.w < -20.5 && cam_pos.w > -25.5) { // dynobj variants only:
// -21/-22; the exact water family (-30) and sky (-40) pass through.
if (cam_pos.w < -21.5) {
clip(albedo.a - 0.1176); // dynamicobject.alphatest: ALPHAREF 30
}
@ -644,6 +669,143 @@ float4 ps_main(VSOut i) : SV_Target {
float3 lin;
float out_a = 1.0;
bool reduced_tone = false;
// flowingwateralpha (fam 30): the canal/waterfall water shader,
// hand-ported from flowingwateralpha_defaultPS and verified per-pixel
// against the ucode (dual time-scrolled ripple-normal taps -> TBN ->
// lightmap-lit body + phong sun spec + cube reflection, then the
// standard fog/exposure/tonemap chain; alpha = the spec term's green
// channel, floored; reflection strength drives opacity over the bed).
if (fam > 29.5 && fam < 30.5) {
float t = wat_p3.x;
float3 n1 = macro.Sample(smp, i.uv * wat_p2.xy + wat_p1.xy * t).rgb;
float3 n2 = macro.Sample(smp, i.uv * wat_p2.zw + wat_p1.zw * t).rgb;
float3 nsum = 2.0 * n1 + 2.0 * n2 - 2.0;
// vNormal = normalize(m_params[0].xzw * (n1 + n2)) in tangent space.
float3 vn = normalize(float3(nsum.x * wat_p0.x, nsum.y * wat_p0.z,
nsum.z * wat_p0.w));
float3 wn0 = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
// Stored frame: tanb = the mesh's usage-6 TANGENT here (see the water
// pack in DecodeMesh), sentinel sign = the unwrap sign.x;
// B = sign.x * cross(N, T), exactly the game's VS.
float3 wt, wb;
if (i.tanb.w > 0.1) {
wt = normalize(i.tanb.xyz);
wb = cross(wn0, wt) * (i.tanb.w > 0.6 ? 1.0 : -1.0);
} else {
ScreenTangentFrame(wn0, i.rpos, i.uv, wt, wb);
}
float3 wn = normalize(vn.x * wt + vn.y * wb + vn.z * wn0);
float3 vd = -normalize(i.rpos);
float s = SampleCsmShadow(i.rpos + cam_pos.xyz, 0.0);
// Lightmap at the unwrap + 0.01 * vNormal.xz (the ripple refraction
// wobble), console fetch semantics, min-clamped like every world
// family. White-fallback pages skip the clamp (see the env fetch).
float2 wlm_uv = i.uv2 + 0.01 * vn.xz;
float3 wlm = lightmap.SampleLevel(smp_clamp, wlm_uv, 0.0).rgb;
float3 lml = tint.r > 0.0 ? min(wlm * wlm, s + sh_color.rgb)
: wlm * wlm;
// GetTangentLight on the WORLD normal: fixed axis (0.58, 0.62, 0.39)
// with xy signs following the sun direction, x 1/normalize(axis).z.
float kd = (wn.x * 0.58 * sign(sh_sun.x) +
wn.y * 0.62 * sign(sh_sun.y) + wn.z * 0.39) * 2.39562;
lin = lml * kd * dlin;
// Spec/reflection masks (t4): x = phong mask, z = reflection mask,
// thresholded by m_params[3].y. Zero until resolved: matte water.
float2 wmask = float2(0.0, 0.0);
if (overlay.x > 0.0) {
float4 wm4 = decal_art.Sample(smp, i.uv);
wmask = saturate(float2(wm4.x, wm4.z) - wat_p3.y);
}
// Phong vs the real sun, power m_params[3].z, tint (2.1, 1.8, 1.5),
// scaled by sat(clamped lightmap green - 0.1).
float3 wrl = 2.0 * wn * dot(wn, sh_sun.xyz) - sh_sun.xyz;
float ks = pow(max(saturate(dot(vd, wrl)), 1e-6), wat_p3.z);
float3 wspec =
ks * wmask.x * float3(2.1, 1.8, 1.5) * saturate(lml.g - 0.1);
// Cube reflection, same shape as fams 5/6: reflect(E, wN) with xy
// negated, luminosity lerped toward 1 by 0.3 * sat(4*mask - 2.6),
// x mask x 1.5. misc.y = the cube LOD bias (640p-gradient parity).
float3 wrv = vd - 2.0 * wn * dot(vd, wn);
float3 wcube =
overlay.y > 0.0
? env_cube.SampleBias(smp, float3(-wrv.x, -wrv.y, wrv.z),
misc.y).rgb
: float3(0.0, 0.0, 0.0);
float wrlum = 0.3 * saturate(4.0 * wmask.y - 2.6);
float3 wrefl = wcube * (lml.g + wrlum * (1.0 - lml.g)) * wmask.y * 1.5;
float3 wspec_term = wspec + wrefl;
lin += wspec_term;
float3 wxe = (lin * (fog_a * wat_p0.y) + fog_rgb) * expo;
return ToneOut(wxe, max(wspec_term.g, wat_p3.w), false);
}
// Exact ocean surface (fam 31, ocean_defaultPS; verified per-pixel
// against the ucode): PCA-compressed dual-component normal map, lerped
// toward flat by the overlay-scaled tonedown, cube reflection x squared
// lightmap x fresnel, ward anisotropic sun spec. OPAQUE (the game's PS
// outputs alpha 0 and draws without blending).
if (fam > 30.5 && fam < 31.5) {
float2 suv = i.uv * oc_p1.w;
float4 c0v = macro.Sample(smp, suv) * 2.0 - 1.0; // t3: comp 0
float4 c1v = detail_map.Sample(smp, suv) * 2.0 - 1.0; // t8: comp 1
// PCA normal in the shader's Z-UP basis; mean/weights are stored in
// 0..1 texture space and folded 2x-1 after the sum.
float3 pca = (float3(dot(c0v, oc_w0) + dot(c1v, oc_w1),
dot(c0v, oc_w2) + dot(c1v, oc_w3),
dot(c0v, oc_w4) + dot(c1v, oc_w5)) +
oc_mean.xyz) * 2.0 - 1.0;
// tonedown = 2 * m_params[1].z * sat(dot(c1v, wB1) + overlay.x):
// the ucode folds the second PCA blue dot into the overlay term.
float ovx = overlay.z > 0.0
? spec2_map.Sample(smp, i.uv * overlay.w).x
: 1.0;
float otd = (2.0 * oc_p1.z) * saturate(dot(c1v, oc_w5) + ovx);
float3 nt = normalize(lerp(float3(0.0, 0.0, 1.0), pca, otd));
nt = normalize(max(abs(nt), 0.001));
float3 N = float3(nt.x, nt.z, nt.y); // shader Z-up -> world Y-up
float3 E = -normalize(i.rpos);
// Cube reflection: reflect(E, N) with xy negated, x the SQUARED
// single-tap lightmap (no chromaticity, no CSM clamp in this PS).
float3 orv = E - 2.0 * N * dot(E, N);
float3 ocube =
overlay.y > 0.0
? env_cube.SampleBias(smp, float3(-orv.x, -orv.y, orv.z),
misc.y).rgb
: float3(0.0, 0.0, 0.0);
float2 olm_uv = i.uv2 + 0.01 * nt.xy;
float3 olm = lightmap.SampleLevel(smp_clamp, olm_uv, 0.0).rgb;
float3 orefl = ocube * (olm * olm);
float NV = abs(dot(N, E));
float F = 0.25 + 0.75 * pow(1.0 - NV, 5.0);
// Ward anisotropic sun spec (spreads oc_p2.xy, roughness oc_p2.z).
float3 oud = normalize(float3(-N.z, 0.0, N.x));
float3 ovd = cross(N, oud);
float dNE = dot(N, E);
float dNL = dot(N, sh_sun.xyz);
float oax = oc_p2.z / oc_p2.x;
float oay = oc_p2.z / oc_p2.y;
float3 H = normalize(sh_sun.xyz + E);
float eu = dot(H, oud) / oax;
float ev = dot(H, ovd) / oay;
float oden = sqrt(abs(dNL * dNE)) * 12.566371 * oax * oay;
float ward = exp(-2.0 * (eu * eu + ev * ev) / (1.0 + dot(H, N))) /
max(oden, 1e-6);
float3 ocol = orefl * F + ward * oc_p0.rgb;
float3 oxe = (ocol * (fog_a * oc_p1.y) + fog_rgb) * expo;
return ToneOut(oxe, 1.0, false);
}
// Baked ocean horizon sheet (fam 32, oceanreflection_defaultPS;
// verified per-pixel): squared sea texture x multiplier, fogged and
// tonemapped; alpha ramps with world height between the orf.z/orf.y
// bounds (the bright haze band fading in toward the horizon).
if (fam > 31.5 && fam < 32.5) {
float3 rcol = albedo.rgb * albedo.rgb * orf.x;
float ra = saturate((i.rpos.y + cam_pos.y - orf.z) /
max(orf.y - orf.z, 1e-4));
float3 rxe = (rcol * fog_a + fog_rgb) * expo;
return ToneOut(rxe, ra, 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
@ -1126,6 +1288,37 @@ float4 ps_refl_gbuf(VSOut i) : SV_Target {
// Env families carry cam_pos.w = -fam; water rides the legacy branch with
// cam_pos.w = 0 (misc.x is the packed normal-trim on fams 5/6 here, so it
// cannot discriminate).
if (cam_pos.w < -30.5 && cam_pos.w > -32.5) {
// Exact ocean families: no SSR contribution (the ocean keeps its own
// cube/baked reflections): zero weight, in-front visibility.
return float4(OctEncode(float3(0.0, 1.0, 0.0)), i.pos.w, -0.0);
}
if (cam_pos.w < -29.5 && cam_pos.w > -30.5) {
// Exact water (fam 30): the branch's own ripple-perturbed normal;
// weight from the thresholded reflection mask (t4.z), boosted like the
// glass arm below. Never writes scene depth -> in-front visibility.
float t = wat_p3.x;
float3 n1 = macro.Sample(smp, i.uv * wat_p2.xy + wat_p1.xy * t).rgb;
float3 n2 = macro.Sample(smp, i.uv * wat_p2.zw + wat_p1.zw * t).rgb;
float3 nsum = 2.0 * n1 + 2.0 * n2 - 2.0;
float3 vn = normalize(float3(nsum.x * wat_p0.x, nsum.y * wat_p0.z,
nsum.z * wat_p0.w));
float3 wn0 = dot(i.nrm, i.nrm) > 0.01
? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
float3 wt, wb;
if (i.tanb.w > 0.1) {
wt = normalize(i.tanb.xyz);
wb = cross(wn0, wt) * (i.tanb.w > 0.6 ? 1.0 : -1.0);
} else {
ScreenTangentFrame(wn0, i.rpos, i.uv, wt, wb);
}
float3 wwn = normalize(vn.x * wt + vn.y * wb + vn.z * wn0);
float wmask = overlay.x > 0.0
? saturate(decal_art.Sample(smp, i.uv).z - wat_p3.y)
: 0.0;
return float4(OctEncode(wwn), i.pos.w, -saturate(wmask * 2.5));
}
if (cam_pos.w < -0.5) {
// Fams 5/6/13: the reflective branch's own normal + reflection mask.
// No alpha test; masked reflective draws skip the clip in ps_main too

File diff suppressed because it is too large Load diff

View file

@ -1650,6 +1650,7 @@ bool BuildItemFromMesh(uint8_t* base, uint32_t mesh, DrawItem& item) {
item.tangent_offset = 0;
item.binormal_offset = 0;
item.tb_fmt = 0;
item.tan_fmt = 0;
bool have_tan = false;
bool have_bin = false;
item.skinned = false;
@ -1692,6 +1693,9 @@ bool BuildItemFromMesh(uint8_t* base, uint32_t mesh, DrawItem& item) {
if (have_tan && have_bin) {
item.tb_fmt = 16;
}
if (have_tan) {
item.tan_fmt = 16; // tangent-only layouts (water meshes) use this
}
if (!have_pos) {
g_rej_geom.fetch_add(1, std::memory_order_relaxed);
return false;
@ -1754,7 +1758,10 @@ bool BuildItemFromMesh(uint8_t* base, uint32_t mesh, DrawItem& item) {
item.decal_tileable = false;
item.transparent = false;
item.water = false;
item.water_flowing = false;
item.water_ocean = 0;
item.water_normal = 0;
item.water_normal2 = 0;
item.water_env = 0;
item.unlit = false;
item.env_family = 0;
@ -1795,9 +1802,13 @@ bool BuildItemFromMesh(uint8_t* base, uint32_t mesh, DrawItem& item) {
} else if (std::memcmp(text, "decal", 6) == 0) {
slot = &item.decal_art;
} else if (std::memcmp(text, "normal", 7) == 0) {
// Exact match only ("normal2" is the second flowing-water tap).
// Consumed by the water branch; other families ignore it.
// Exact match only. Consumed by the water branches (the flowing
// ripple map / the ocean's first PCA component); other families
// ignore it.
slot = &item.water_normal;
} else if (std::memcmp(text, "normal2", 8) == 0) {
// The ocean's second PCA normal component map.
slot = &item.water_normal2;
} else if (std::memcmp(text, "alpha", 6) == 0) {
// Hair strand coverage (cac_hair/defaulthair tf5, sampled at the
// second texcoord), the hair alpha-blend term.
@ -1922,6 +1933,18 @@ bool BuildItemFromMesh(uint8_t* base, uint32_t mesh, DrawItem& item) {
// fallback diffuse x near-white ocean lightmap x2).
item.water = std::memcmp(mat_name, "water.", 6) == 0 ||
std::memcmp(mat_name, "ocean.", 6) == 0;
// water.flowing* = the flowingwateralpha shader family (canal /
// waterfall). Takes the exact water branch when the frame's
// m_params rows are captured (see FrameScene::water_rows).
item.water_flowing =
std::memcmp(mat_name, "water.flowing", 13) == 0;
// ocean.default / ocean.reflection: the sea surface and its
// baked horizon reflection sheet (see DrawItem::water_ocean).
if (std::memcmp(mat_name, "ocean.default", 14) == 0) {
item.water_ocean = 1;
} else if (std::memcmp(mat_name, "ocean.reflection", 17) == 0) {
item.water_ocean = 2;
}
// Exact world-shading family (see DrawItem::env_family). The
// attrib <-> pixel-shader-family mapping is 1:1; the shading
// models were verified per-pixel against the game's own shaders.
@ -3975,7 +3998,9 @@ void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t
// independent done-flag: the first main-pass draw can be a character/tree
// whose PS allocates differently (rejected by the sanity gate below).
if ((!g_fog_frame_done || !g_shadow_frame_done || !g_sky_frame_done ||
!g_tree_frame_done || !g_proxy_frame_done || !g_dynobj_frame_done) &&
!g_tree_frame_done || !g_proxy_frame_done || !g_dynobj_frame_done ||
!g_water_frame_done || !g_ocean_frame_done ||
!g_oceanrefl_frame_done) &&
func == 0 && flags2d == 0 && SceneEnabled() &&
(g_fog_cam[0] != 0.0f || g_fog_cam[1] != 0.0f || g_fog_cam[2] != 0.0f)) {
const float dx = LoadGuestF32(base, bank + 16 * 4) - g_fog_cam[0];
@ -4129,6 +4154,157 @@ void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t
}
}
}
// flowingwateralpha m_params + animation time (PS c11..c14, c15.x),
// POSITIVE debug-path gate like the receiver rows: the water bank
// keeps the whole baseenvironment receiver layout, so only the name
// discriminates. One material's rows serve the frame; every
// flowingwateralpha draw in a capture carried identical m_params, and
// the animation time is pass-global anyway.
if (!g_water_frame_done && ps_bank != 0) {
const auto water_ps = [&]() -> bool {
const auto check = [&](uint32_t obj) -> bool {
if (obj < 0x10000 || !GuestReadableApprox(base, obj)) {
return false;
}
char text[120] = {};
for (int k = 0; k < 119; ++k) {
text[k] = char(REX_LOAD_U8(obj + 0x54 + k));
if (text[k] == '\0') break;
}
return std::strstr(text, "\\flowingwateralpha") != nullptr;
};
return check(g_cur_ps_obj.load(std::memory_order_relaxed)) ||
check(g_cur_vs_obj.load(std::memory_order_relaxed));
};
if (water_ps()) {
float rows[17];
for (int i = 0; i < 16; ++i) {
rows[i] = LoadGuestF32(base, ps_bank + (44 + i) * 4); // c11..c14
}
rows[16] = LoadGuestF32(base, ps_bank + 60 * 4); // c15.x = time
// Range gate: normal scales O(1), material multiplier small
// positive, scroll speeds/UV scales bounded, spec power a real
// exponent, alpha floor a blend factor, time non-negative.
bool sane = rows[1] > 0.0f && rows[1] <= 8.0f && // c11.y mult
rows[13] >= 0.0f && rows[13] <= 1.0f && // c14.y thr
rows[14] >= 1.0f && rows[14] <= 1000.0f && // c14.z pow
rows[15] >= 0.0f && rows[15] <= 1.0f && // c14.w floor
rows[16] >= 0.0f && rows[16] < 1e7f; // time
for (int i = 0; i < 4 && sane; ++i) {
sane = std::fabs(rows[0 + i]) <= 8.0f && // c11
std::fabs(rows[4 + i]) <= 64.0f && // c12 speeds
std::fabs(rows[8 + i]) <= 256.0f; // c13 scales
}
if (sane) {
const bool first = !g_water_have;
std::memcpy(g_water_rows, rows, sizeof(rows));
g_water_have = true;
g_water_frame_done = true;
if (first) {
REXLOG_INFO(
"native-scene: water m_params captured: p0=({:.3g},{:.3g},"
"{:.3g},{:.3g}) speed=({:.3g},{:.3g},{:.3g},{:.3g}) "
"scale=({:.3g},{:.3g},{:.3g},{:.3g}) thr={:.3g} pow={:.3g} "
"floor={:.3g} t={:.2f}",
rows[0], rows[1], rows[2], rows[3], rows[4], rows[5],
rows[6], rows[7], rows[8], rows[9], rows[10], rows[11],
rows[13], rows[14], rows[15], rows[16]);
}
} else {
static std::atomic<int> s_water_rejects{0};
if (s_water_rejects.fetch_add(1, std::memory_order_relaxed) < 4) {
REXLOG_INFO(
"native-scene: water m_params REJECTED by range gate: "
"mult={:.3g} thr={:.3g} pow={:.3g} floor={:.3g} t={:.3g}",
rows[1], rows[13], rows[14], rows[15], rows[16]);
}
}
}
}
// ocean_defaultPS material rows (PCA mean/weights at c2..c8,
// m_params[0..2] at c12..c14) and the oceanreflection sheet row (c3).
// Same POSITIVE debug-path gating as the water rows.
if ((!g_ocean_frame_done || !g_oceanrefl_frame_done) && ps_bank != 0) {
const auto ps_name_has = [&](const char* needle) -> bool {
const auto check = [&](uint32_t obj) -> bool {
if (obj < 0x10000 || !GuestReadableApprox(base, obj)) {
return false;
}
char text[120] = {};
for (int k = 0; k < 119; ++k) {
text[k] = char(REX_LOAD_U8(obj + 0x54 + k));
if (text[k] == '\0') break;
}
return std::strstr(text, needle) != nullptr;
};
return check(g_cur_ps_obj.load(std::memory_order_relaxed)) ||
check(g_cur_vs_obj.load(std::memory_order_relaxed));
};
if (!g_ocean_frame_done && ps_name_has("\\ocean_default")) {
float c[64];
for (int i = 0; i < 64; ++i) {
c[i] = LoadGuestF32(base, ps_bank + (8 + i) * 4); // c2..c17
}
const float* m0 = c + (12 - 2) * 4; // c12
const float* m1 = c + (13 - 2) * 4; // c13
const float* m2 = c + (14 - 2) * 4; // c14
bool sane = m1[1] > 0.0f && m1[1] <= 8.0f && // multiplier
m1[2] >= 0.0f && m1[2] <= 8.0f && // tonedown scale
m1[3] > 0.0f && m1[3] <= 4096.0f && // uv scale
m2[0] > 0.01f && m2[0] <= 1000.0f && // spread u
m2[1] > 0.01f && m2[1] <= 1000.0f && // spread v
m2[2] > 0.001f && m2[2] <= 100.0f; // roughness
for (int i = 0; i < 4 && sane; ++i) {
sane = c[i] >= -2.0f && c[i] <= 2.0f; // PCA mean row
}
if (sane) {
float* o = g_ocean_rows;
// mean pre-swizzled for (R,G,B): PS c2.x, c2.z, c2.y
o[0] = c[0];
o[1] = c[2];
o[2] = c[1];
o[3] = 0.0f;
// weight rows in model order: c3, c4, c7, c8, c5, c6
const int worder[6] = {3, 4, 7, 8, 5, 6};
for (int w = 0; w < 6; ++w) {
std::memcpy(o + 4 + w * 4, c + (worder[w] - 2) * 4,
4 * sizeof(float));
}
std::memcpy(o + 28, m0, 4 * sizeof(float));
std::memcpy(o + 32, m1, 4 * sizeof(float));
std::memcpy(o + 36, m2, 4 * sizeof(float));
const bool first = !g_ocean_have;
g_ocean_have = true;
g_ocean_frame_done = true;
if (first) {
REXLOG_INFO(
"native-scene: ocean rows captured: mult={:.3g} "
"tonedown={:.3g} uvscale={:.3g} ward=({:.3g},{:.3g},{:.3g})",
m1[1], m1[2], m1[3], m2[0], m2[1], m2[2]);
}
}
}
if (!g_oceanrefl_frame_done && ps_name_has("\\oceanreflection")) {
float row[4];
for (int i = 0; i < 4; ++i) {
row[i] = LoadGuestF32(base, ps_bank + (12 + i) * 4); // c3
}
// multiplier small positive; the fade bounds must not coincide.
if (row[0] > 0.0f && row[0] <= 8.0f &&
std::fabs(row[1] - row[2]) > 1e-4f) {
std::memcpy(g_oceanrefl_rows, row, sizeof(row));
const bool first = !g_oceanrefl_have;
g_oceanrefl_have = true;
g_oceanrefl_frame_done = true;
if (first) {
REXLOG_INFO(
"native-scene: oceanreflection row captured: mult={:.3g} "
"fade=({:.3g},{:.3g})",
row[0], row[1], row[2]);
}
}
}
}
// Not gated on the shadows cvar: the captured rows also carry the
// scene exposure / material multiplier / sun direction consumed by the
// exact world shading (rows 40/45/24..26); shading must not die when
@ -8353,6 +8529,19 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
std::memcpy(scene.fog_ramp, g_fog_rows, 4 * sizeof(float));
std::memcpy(scene.fog_color, g_fog_rows + 4, 4 * sizeof(float));
}
if (g_water_have) {
std::memcpy(scene.water_rows, g_water_rows, sizeof(scene.water_rows));
scene.water_valid = true;
}
if (g_ocean_have) {
std::memcpy(scene.ocean_rows, g_ocean_rows, sizeof(scene.ocean_rows));
scene.ocean_valid = true;
}
if (g_oceanrefl_have) {
std::memcpy(scene.oceanrefl_rows, g_oceanrefl_rows,
sizeof(scene.oceanrefl_rows));
scene.oceanrefl_valid = true;
}
if (g_shadow_have) {
std::memcpy(scene.shadow_rows, g_shadow_rows, sizeof(g_shadow_rows));
scene.shadow_valid = true;
@ -8484,6 +8673,9 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
g_tree_frame_done = false;
g_proxy_frame_done = false;
g_dynobj_frame_done = false;
g_water_frame_done = false;
g_ocean_frame_done = false;
g_oceanrefl_frame_done = false;
// Draw-time STRETCH VETO: the last line of defense, judging what the GPU

View file

@ -139,8 +139,28 @@ struct DrawItem {
// `water_normal` is the material's `normal` channel texture, bound in the
// macro slot (water never carries a macro overlay).
bool water;
// AttribulatorMaterialName starts "water.flowing": the flowingwateralpha
// shader family (canal / waterfall surfaces). These take the exact water
// branch (hand-ported from flowingwateralpha_defaultPS and verified
// per-pixel against the ucode) when the frame's water m_params rows are
// captured; other water.* / ocean.* materials keep the legacy water
// shading until their own shader layouts are verified from a capture.
bool water_flowing = false;
// ocean materials: 1 = ocean.default (the sea surface, ocean_defaultPS:
// PCA dual-component normal + ward spec + cube reflection, drawn OPAQUE),
// 2 = ocean.reflection (the baked horizon reflection sheet, height-ramp
// alpha). Exact branches gated on the frame's ocean rows.
uint8_t water_ocean = 0;
uint32_t water_normal;
// `normal2` channel: the ocean's second PCA normal component map (the
// flowingwater families ignore it).
uint32_t water_normal2 = 0;
uint32_t water_env; // `environment` channel cube map (reflection term)
// Usage-6 tangent present WITHOUT a usage-7 binormal (tan_fmt = 16, the
// water-mesh layout: pos + uv + lm_norm + tangent). The flowingwater VS
// reconstructs the frame as N from lm_norm.zw + sign bits, B =
// sign.x * cross(N, T); DecodeMesh mirrors that (see the water pack).
uint8_t tan_fmt = 0;
// character.cloth_ropa (Ropa cloth-simulated garments, e.g. player tees):
// the VS variant branches on a flag row kept in front of the bone palette
// - sim active means the dynamic VB already holds deformed root-local
@ -295,6 +315,29 @@ struct FrameScene {
// [2] = tree PS c4.y (tint multiplier), [3] = proxyworld PS c3.y (0.45).
// Defaults from the day capture cover frames before the first hit.
float family_rows[4] = {0.3435f, 0.02f, 1.0f, 0.45f};
// flowingwateralpha.fx (the water.flowing* materials) m_params rows,
// captured once per frame from a flowingwateralpha PS bank (debug-path
// classified): [0..3] = m_params[0] (PS c11: normal scale xzw + the
// material multiplier in y), [4..7] = m_params[1] (c12: the two animated
// UV scroll speeds), [8..11] = m_params[2] (c13: the two tap UV scales),
// [12..15] = m_params[3] (c14: y = mask threshold, z = spec power,
// w = alpha floor), [16] = g_fAnimationTime (c15.x). Consumed by the
// exact water scene PS branch (cam_pos.w = -30).
bool water_valid = false;
float water_rows[17] = {};
// ocean_defaultPS material rows, captured once per frame from an ocean PS
// bank (debug-path classified): [0..3] = PCA mean pre-swizzled for the
// (R,G,B) channel order (PS c2.x, c2.z, c2.y), [4..27] = the six PCA
// weight rows in model order (c3, c4, c7, c8, c5, c6), [28..31] =
// m_params[0] (spec color, c12), [32..35] = m_params[1] (c13: y = mult,
// z = tonedown scale, w = uv scale), [36..39] = m_params[2] (c14: ward
// spread u/v + roughness).
bool ocean_valid = false;
float ocean_rows[40] = {};
// oceanreflection_defaultPS row (PS c3): x = multiplier, y/z = the
// height-ramp alpha fade bounds.
bool oceanrefl_valid = false;
float oceanrefl_rows[4] = {};
// dynamicobject.fx frame-global lighting rows, captured from a
// dynamicobject/alphatestdynamicobject PS bank (debug-path classified):
// [0..2] sun direction (PS c9), [3] scene exposure (c13.x),

View file

@ -526,8 +526,9 @@ bool DecodeMesh(nrhi::Device* device, uint8_t* base, const DrawItem& item,
// encodings (decalenvironment VS: o0.zw = |uv|; baseenvironment VS:
// maxs r2.zw = |r4.xy|). Raw signed values sample mirrored atlas cells.
// Exception: hair meshes, their second texcoord is the strand-alpha
// UV, passed RAW by the hair VS (o0.zw = uv, float2).
if (item.char_family < 4) {
// UV, passed RAW by the hair VS (o0.zw = uv, float2), and the ocean,
// whose VS passes the lightmap UV raw (Out.UV.zw = In.LM, no abs).
if (item.char_family < 4 && item.water_ocean == 0) {
dst[v * 14 + 5] = std::fabs(dst[v * 14 + 5]);
dst[v * 14 + 6] = std::fabs(dst[v * 14 + 6]);
}
@ -578,7 +579,35 @@ bool DecodeMesh(nrhi::Device* device, uint8_t* base, const DrawItem& item,
return uint32_t(std::lround((f * 0.5f + 0.5f) * 255.0f)) & 0xFFu;
};
float n3[3] = {0.0f, 0.0f, 0.0f};
if (item.env_family != 0 &&
if (item.water_flowing && !item.skinned && item.uv2_fmt == 26 &&
item.tan_fmt == 16) {
// Water meshes (flowingwater VS layout): the fmt-26 element is
// lm_norm: xy = |unwrap| with sign bits, zw = normal.xy (snorm),
// normal.z = sign.y * sqrt(1 - xy^2), and the usage-6 k_10_11_11
// element is the TANGENT (no usage-3 normal, no usage-7 binormal).
// Pack the tangent into the free blend-weight bytes with the unwrap
// sign.x in the sentinel byte; the shader rebuilds the frame as
// B = sign.x * cross(N, T), exactly the game's VS
// (vBinormal = signs.x * cross(vNormal, vTangent)).
const uint8_t* q = src_vb + size_t(v) * item.stride + item.uv2_offset;
const int16_t sx = int16_t(SwapU16(*reinterpret_cast<const uint16_t*>(q)));
const int16_t sy = int16_t(SwapU16(*reinterpret_cast<const uint16_t*>(q + 2)));
const int16_t nx = int16_t(SwapU16(*reinterpret_cast<const uint16_t*>(q + 4)));
const int16_t ny = int16_t(SwapU16(*reinterpret_cast<const uint16_t*>(q + 6)));
n3[0] = nx / 32767.0f;
n3[1] = ny / 32767.0f;
// The game clamps xy_len at 0.999 (water normals ride |xy| ~ 1, so
// z floors at 0.0316 rather than 0, verified against the VS ucode).
const float xylen =
std::min(n3[0] * n3[0] + n3[1] * n3[1], 0.999f);
n3[2] = (sy > 0 ? 1.0f : -1.0f) * std::sqrt(1.0f - xylen);
float t3[3];
unpack_10_11_11(item.tangent_offset, t3);
const uint32_t packed = pk(t3[0]) | (pk(t3[1]) << 8) |
(pk(t3[2]) << 16) |
((sx > 0 ? 200u : 100u) << 24);
std::memcpy(&dst[v * 14 + 7], &packed, 4);
} else if (item.env_family != 0 &&
(item.env_family <= 6 || item.env_family == 13) &&
item.uv2_fmt == 26) {
// Exact world families: the REAL vertex normal is packed in the
@ -1892,8 +1921,10 @@ bool EnsureGuestCubeTexture(const NativeGuestOutputRenderContext& context, uint8
// chain down to 1x1 below; other formats fall back to copying the guest
// chain (down to 32px: smaller levels live packed inside a shared 32x32
// tile, see GetPackedTileOffset).
const xenos::TextureFormat cube_base_fmt = rex::graphics::GetBaseFormat(info.format);
const bool rgba_chain =
rex::graphics::GetBaseFormat(info.format) == xenos::TextureFormat::k_DXT1 &&
(cube_base_fmt == xenos::TextureFormat::k_DXT1 ||
cube_base_fmt == xenos::TextureFormat::k_8_8_8_8) &&
width >= 8 && (width & (width - 1)) == 0 && width == height;
uint32_t mip_levels = 1;
if (!rgba_chain && info.memory.mip_address != 0 && (width & (width - 1)) == 0 &&
@ -1954,9 +1985,13 @@ bool EnsureGuestCubeTexture(const NativeGuestOutputRenderContext& context, uint8
}
if (rgba_chain) {
// Decode DXT1 mip 0 -> RGBA8 per face, box-filter the full chain to
// 1x1, upload as an RGBA cube. CPU cost is one-time per cube (runs on
// the decode workers).
// Decode mip 0 -> RGBA8 per face (DXT1 block decode, or plain 8888
// texels: the water canal cubes), box-filter the full chain to 1x1,
// upload as an RGBA cube. The GUEST cube mip chain is never trusted:
// its packed sub-32px levels and per-face slice layout are exactly
// where garbage sneaks in, and the reflection fetch runs at a deep
// gradient LOD where a bad level dominates. CPU cost is one-time per
// cube (runs on the decode workers).
nrhi::Device* device = context.device;
const uint32_t levels = 1u + uint32_t(std::countr_zero(width));
struct Level {
@ -2022,14 +2057,34 @@ bool EnsureGuestCubeTexture(const NativeGuestOutputRenderContext& context, uint8
bytes_per_block);
}
SwapGuestEndian(bc_row.data(), uint32_t(bc_row.size()), info.endianness);
if (cube_base_fmt == xenos::TextureFormat::k_DXT1) {
for (uint32_t bx = 0; bx < L0.cols; ++bx) {
uint8_t px[16][4];
DecodeBc1Block(&bc_row[size_t(bx) * bytes_per_block], px);
for (uint32_t r = 0; r < 4; ++r) {
std::memcpy(&rgba[(size_t(by * 4 + r) * width + bx * 4) * 4], px[r * 4],
16);
std::memcpy(&rgba[(size_t(by * 4 + r) * width + bx * 4) * 4],
px[r * 4], 16);
}
}
} else {
// k_8_8_8_8: post-endian-swap bytes are already component order
// X,Y,Z,W (the SRV swizzle applies the fetch's channel remap).
std::memcpy(&rgba[size_t(by) * width * 4], bc_row.data(),
size_t(width) * 4);
}
}
// Diagnostic dump (skate3_native_render_scene_lm_dump): the decoded
// face exactly as uploaded, for offline byte-diff against the
// reference decode of the same fetch words.
if (REXCVAR_GET(skate3_native_render_scene_lm_dump)) {
char path[260];
std::snprintf(path, sizeof(path),
"native_texture_dumps/cube_%08X_f%u_%ux%u.rgba",
info.memory.base_address, face, width, height);
if (FILE* f = std::fopen(path, "wb")) {
std::fwrite(rgba.data(), 1, size_t(width) * height * 4, f);
std::fclose(f);
}
}
// Upload level 0, then box-filter down the chain in place.
const uint8_t* src = rgba.data();
@ -2062,8 +2117,8 @@ bool EnsureGuestCubeTexture(const NativeGuestOutputRenderContext& context, uint8
}
}
device->Unmap(out.upload);
REXLOG_INFO("native-scene: cube {:08X} {}x{} DXT1 -> RGBA full chain ({} levels)",
tex_ptr, width, height, levels);
REXLOG_INFO("native-scene: cube {:08X} {}x{} fmt={} -> RGBA full chain ({} levels)",
tex_ptr, width, height, uint32_t(info.format), levels);
nrhi::Swizzle swizzle_mapping[4];
ComposeSrvSwizzle(fetch.swizzle, xenos::XE_GPU_TEXTURE_SWIZZLE_RGBA,
swizzle_mapping);
@ -3756,6 +3811,9 @@ void WarmItemResources(const NativeGuestOutputRenderContext& context, uint8_t* b
// per-pixel too.
warm_texture(item.water_normal);
}
if (item.water_ocean == 1) {
warm_texture(item.water_normal2); // second PCA component (t8)
}
if ((item.env_family >= 1 && item.env_family <= 4 && item.env_family != 2) ||
item.dynobj != 0) {
// v2 detail normal map (t8; fam 2 carries no base+detail pair).
@ -3990,6 +4048,9 @@ void ProcessPrewarmEntry(uint8_t* base, const PrewarmEntry& e) {
// per-pixel too.
stage_texture(item.water_normal);
}
if (item.water_ocean == 1) {
stage_texture(item.water_normal2); // second PCA component (t8)
}
if ((item.env_family >= 1 && item.env_family <= 4 && item.env_family != 2) ||
item.dynobj != 0) {
// v2 detail normal map (t8; fam 2 carries no base+detail pair).
@ -5968,7 +6029,7 @@ void LogFrameStats(const FrameScene& scene, uint64_t frames, uint32_t drawn,
"store[n={} routes={} evict={}] "
"heal[vfail={} demote={}] serve[sticky={} skipnew={} adstale={} adnone={}] "
"shadow[valid={} ready={} draws={}] char[attempt={} valid={} drawn={} "
"reused={}] dynobj[valid={} drawn={}] "
"reused={}] dynobj[valid={} drawn={}] water[valid={} drawn={}] "
"lw[ctxs={} ents={} stamp={} fade0={} resc={} fill={} pal={} rows={}] "
"refl[pair={} flat={} gate={:#x}] flips={} alt[bones={} shadow={}] "
"rows_inst={} pal_srv={}",
@ -5997,7 +6058,8 @@ void LogFrameStats(const FrameScene& scene, uint64_t frames, uint32_t drawn,
shadow_ready, shadow_draws,
g_char_attempts.load(), g_char_valid.load(), g_char_drawn.load(),
g_char_rows_reused.load(), scene.dynobj_valid,
g_dynobj_drawn.load(), lw_ctxs, lw_ents, g_lw_stamped.load(),
g_dynobj_drawn.load(), scene.water_valid, g_water_drawn.load(),
lw_ctxs, lw_ents, g_lw_stamped.load(),
g_lw_fade0.load(), g_lw_ctx_rescued.load(), g_lw_gap_filled.load(),
g_lw_pal_sub.load(), g_lw_rows_served.load(),
g_refl_pair.load(), g_refl_flat.load(), g_refl_gate.load(),
@ -6459,6 +6521,27 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
std::memcpy(cb + 64, scene.dynobj_ws, sizeof(scene.dynobj_ws));
}
}
// flowingwateralpha m_params rows (wat_p0..wat_p3 at cb[76..91]):
// m_params[0..2] verbatim, then (time, mask threshold, spec power,
// alpha floor). Consumed by the exact water branch (cam_pos.w = -30),
// which is only selected per draw when scene.water_valid.
if (scene.water_valid) {
std::memcpy(cb + 76, scene.water_rows, 12 * sizeof(float)); // c11..c13
cb[88] = scene.water_rows[16]; // g_fAnimationTime (c15.x)
cb[89] = scene.water_rows[13]; // mask threshold (c14.y)
cb[90] = scene.water_rows[14]; // spec power (c14.z)
cb[91] = scene.water_rows[15]; // alpha floor (c14.w)
}
// ocean_defaultPS rows (oc_mean/oc_w0..5/oc_p0..2 at cb[92..131]) and
// the oceanreflection fade row (orf at cb[132..135]); consumed by the
// exact ocean branches (cam_pos.w = -31 / -32).
if (scene.ocean_valid) {
std::memcpy(cb + 92, scene.ocean_rows, sizeof(scene.ocean_rows));
}
if (scene.oceanrefl_valid) {
std::memcpy(cb + 132, scene.oceanrefl_rows,
sizeof(scene.oceanrefl_rows));
}
cmd->SetConstantBuffer(6, g_r.shadow_cb, cb_offset);
// b2 (character lighting) default: point at the ring base so the root
// CBV is never left unset; character draws re-point it per item.
@ -7337,7 +7420,8 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
// refmask map (or the animated.tree noise tint) in the free decal slot;
// overlay.w == 3 tells the shader the masks are live.
bool spec_bound = false;
if (item.env_family != 0 && !item.decal && item.env_family != 10 &&
if (((item.env_family != 0 && !item.decal && item.env_family != 10) ||
item.water_flowing) &&
item.spec_tex != 0) {
const GuestTexture* spec = resolve_texture(item.spec_tex, 6);
if (spec != &g_r.white) {
@ -7475,6 +7559,29 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
v2_flags |= 8;
}
}
// Exact ocean (fam 31): the second PCA normal component rides the free
// t8 pair slot and the 16x16 macro overlay the t9 slot (water items use
// neither). v2_flags stays 0; the ocean branch signals its binds
// through the overlay row.
bool ocean_n2 = false;
bool ocean_ov = false;
if (item.water_ocean == 1 && debug_mode == 0 && scene.shadow_valid &&
scene.ocean_valid) {
if (item.water_normal2 != 0) {
const GuestTexture* n2 = resolve_texture(item.water_normal2, 7);
if (n2 != &g_r.white && n2->valid && n2->srv_mips != 0) {
v2_detail = n2;
ocean_n2 = true;
}
}
if (item.macro_tex != 0) {
const GuestTexture* mo = resolve_texture(item.macro_tex, 6);
if (mo != &g_r.white && mo->valid && mo->srv_mips != 0) {
v2_spec2 = mo;
ocean_ov = true;
}
}
}
constants[44] = item.macro_scale;
constants[45] = item.macro_opacity;
constants[46] = macro_tex != &g_r.white ? 1.0f : 0.0f;
@ -7490,7 +7597,53 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
? 4.0f
: 3.0f)
: 0.0f);
if (item.water) {
// Exact flowingwateralpha branch (cam_pos.w = -30): the canal/waterfall
// shader hand-ported from the game's own PS and verified per-pixel
// against the ucode. Requires the frame m_params rows (b1), the shared
// world frame rows (exposure/sun/fog at b1, scene.shadow_valid) and the
// ripple normal map resolved in the macro slot; the legacy water
// shading covers every gap (and every non-flowing water material).
const bool water_exact = item.water && item.water_flowing &&
debug_mode == 0 && scene.shadow_valid &&
scene.water_valid && macro_tex != &g_r.white;
// Exact ocean surface (fam 31): both PCA component maps must resolve
// (t3 = comp 0 via the water_normal macro-slot bind, t8 = comp 1); the
// legacy water shading covers every gap. The horizon sheet (fam 32)
// needs only its baked texture (t0) and the captured fade row.
const bool ocean_exact = item.water && item.water_ocean == 1 &&
debug_mode == 0 && scene.shadow_valid &&
scene.ocean_valid && macro_tex != &g_r.white &&
ocean_n2;
const bool oceanrefl_exact = item.water && item.water_ocean == 2 &&
debug_mode == 0 && scene.shadow_valid &&
scene.oceanrefl_valid;
if (water_exact) {
// overlay.x = spec/reflection masks bound at t4, overlay.y = real
// environment cube at t6, overlay.z = ripple resolved (gate above).
constants[39] = -30.0f;
constants[44] = spec_bound ? 1.0f : 0.0f;
constants[45] = cube_tex != &g_r.white_cube ? 1.0f : 0.0f;
constants[46] = 1.0f;
constants[47] = 0.0f;
g_water_drawn.fetch_add(1, std::memory_order_relaxed);
} else if (ocean_exact) {
// overlay.x = second PCA component at t8 (gate above guarantees it),
// overlay.y = real environment cube at t6, overlay.z = macro overlay
// at t9, overlay.w = macroOverlayUVScale.
constants[39] = -31.0f;
constants[44] = 1.0f;
constants[45] = cube_tex != &g_r.white_cube ? 1.0f : 0.0f;
constants[46] = ocean_ov ? 1.0f : 0.0f;
constants[47] = item.macro_scale;
g_water_drawn.fetch_add(1, std::memory_order_relaxed);
} else if (oceanrefl_exact) {
constants[39] = -32.0f;
constants[44] = 0.0f;
constants[45] = 0.0f;
constants[46] = 0.0f;
constants[47] = 0.0f;
g_water_drawn.fetch_add(1, std::memory_order_relaxed);
} else if (item.water) {
// overlay.x = ripple scroll time, overlay.y = real environment cube
// bound at t6, overlay.z = ripple normal map resolved (in the macro
// slot; the shader synthesizes procedural ripples otherwise).
@ -7514,7 +7667,15 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
// mat_tint row (color), unused by these items otherwise
// (root-signature DWORD budget).
constants[48] = item.water ? 2.0f : (item.transparent ? 1.0f : 0.0f);
if (item.transparent || item.water) {
if (water_exact || ocean_exact || oceanrefl_exact) {
// misc.y = cube LOD bias (same 640p-gradient rationale as fams 5/6);
// fog comes from the shared b1 rows; misc.zw stay free. misc.x keeps
// the water marker for the SSR G-buffer restage.
constants[49] =
log2f(std::max(1.0f, float(context.guest_output_height) / 640.0f));
constants[50] = 0.0f;
constants[51] = 0.0f;
} else if (item.transparent || item.water) {
constants[49] = scene.fog_ramp[0];
constants[50] = scene.fog_ramp[1];
constants[51] = scene.fog_ramp[2];
@ -7656,8 +7817,9 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
// t6 (cube) shares its table with t7 (shadow atlas); re-pair both.
cmd->SetTexturePair(7, cube_tex->srv,
shadow_ready ? g_r.shadow_srv_final : g_r.white.srv);
// World-shading v2 material pair (t8 detail + t9 decal spec).
if (v2_flags != 0) {
// World-shading v2 material pair (t8 detail + t9 decal spec); the exact
// ocean rides the same pair (t8 = second PCA component, t9 = overlay).
if (v2_flags != 0 || ocean_n2 || ocean_ov) {
cmd->SetTexturePair(8, (v2_detail != nullptr ? v2_detail : &g_r.white)->srv,
(v2_spec2 != nullptr ? v2_spec2 : &g_r.white)->srv);
}
@ -8008,9 +8170,16 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
}
}
};
// The exact ocean surface (fam 31) is an OPAQUE draw in the game (its
// PS outputs alpha 0 and it writes z-prepass depth); routing it through
// the sorted alpha sub-pass would composite the blended horizon sheet
// (ocean.reflection) UNDER it. The legacy fallback also renders opaque,
// so the route holds while textures/rows are still resolving.
const bool ocean_opaque = item.water && item.water_ocean == 1 &&
scene.shadow_valid && scene.ocean_valid;
if ((item.transparent || item.water || hair_blend || glass_blend ||
cac_alpha_blend || refl_trans_blend || char_fade_blend) &&
debug_mode == 0) {
!ocean_opaque && debug_mode == 0) {
if (REXCVAR_GET(skate3_native_render_scene_transparents)) {
transparent_items.push_back(&item);
stamp_route(2);

View file

@ -751,11 +751,13 @@ struct RendererState {
uint32_t shadow_dump_enqueued = 0;
uint32_t shadow_dump_written = 0;
bool shadow_dump_done = false;
// Per-frame shadow constant buffer ring (CBV b1). 512-byte slices: the
// Per-frame shadow constant buffer ring (CBV b1). 768-byte slices: the
// first 256 bytes are the original 16-row block, rows 16-18 carry the
// dynamicobject world-shadow transform (dyn_ws* in the scene shader).
// dynamicobject world-shadow transform (dyn_ws*), rows 19-22 the
// flowingwateralpha m_params (wat_p*), rows 23-33 the ocean PCA/material
// rows and the oceanreflection fade row (oc_* / orf).
static constexpr uint32_t kShadowCbRegions = 8;
static constexpr uint32_t kShadowCbSlice = 512;
static constexpr uint32_t kShadowCbSlice = 768;
nrhi::Buffer* shadow_cb = nullptr;
uint8_t* shadow_cb_cpu = nullptr;
// dynamicobject static world-shadow map (512x512, same convention as the

View file

@ -292,6 +292,20 @@ inline bool g_shadow_frame_done = false;
inline float g_family_rows[4] = {0.3435f, 0.02f, 1.0f, 0.45f};
inline bool g_tree_frame_done = false;
inline bool g_proxy_frame_done = false;
// flowingwateralpha m_params + animation-time rows (see
// FrameScene::water_rows), captured from a flowingwateralpha PS bank
// (debug-path classified). Guest render thread only.
inline float g_water_rows[17] = {};
inline bool g_water_have = false;
inline bool g_water_frame_done = false;
// ocean_defaultPS / oceanreflection_defaultPS rows (see
// FrameScene::ocean_rows / oceanrefl_rows). Guest render thread only.
inline float g_ocean_rows[40] = {};
inline bool g_ocean_have = false;
inline bool g_ocean_frame_done = false;
inline float g_oceanrefl_rows[4] = {};
inline bool g_oceanrefl_have = false;
inline bool g_oceanrefl_frame_done = false;
// dynamicobject.fx frame-global lighting rows (see FrameScene::dynobj_rows),
// captured from a dynamicobject/alphatestdynamicobject PS bank (debug-path
// classified). Guest render thread only.
@ -719,6 +733,7 @@ inline std::atomic<uint64_t> g_char_attempts{0};
inline std::atomic<uint64_t> g_char_valid{0};
inline std::atomic<uint64_t> g_char_drawn{0};
inline std::atomic<uint64_t> g_dynobj_drawn{0};
inline std::atomic<uint64_t> g_water_drawn{0}; // exact-branch water draws
// character.cloth_ropa items captured in the sim-active RIGID mode (world
// from c188/c191 instead of a bone palette, see CaptureSkinnedState).
inline std::atomic<uint64_t> g_ropa_rigid{0};