render: exact scrollincandescent port - the LED chyron scrolls and glows

This commit is contained in:
mchughalex 2026-07-19 18:49:29 +01:00
parent 146a158f90
commit cbc492a407
6 changed files with 12091 additions and 11941 deletions

View file

@ -28,6 +28,8 @@ cbuffer C : register(b0) {
// items never use otherwise. Fog is currently applied to transparent // items never use otherwise. Fog is currently applied to transparent
// items only (the km-distant mist sheets; everything else we render is // items only (the km-distant mist sheets; everything else we render is
// near enough for fog to be negligible). // near enough for fog to be negligible).
// Fam 14 (scrollincandescent, opaque, so every fog slot is free):
// xy = the period-wrapped UV scroll offset, z = the material multiplier.
float4 misc; float4 misc;
}; };
// Per-frame dynamic-shadow (CSM) receiver constants, captured from the // Per-frame dynamic-shadow (CSM) receiver constants, captured from the
@ -648,8 +650,10 @@ float4 ps_main(VSOut i) : SV_Target {
// dynamicobject items (cam_pos.w <= -21) are excluded here and clip in // dynamicobject items (cam_pos.w <= -21) are excluded here and clip in
// their own branch (only the .alphatest variant tests, at ALPHAREF 30). // their own branch (only the .alphatest variant tests, at ALPHAREF 30).
// Fam 13 (reflective_trans glass, cam_pos.w = -13) never alpha-tests; // Fam 13 (reflective_trans glass, cam_pos.w = -13) never alpha-tests;
// it alpha-BLENDS in the sorted sub-pass. // it alpha-BLENDS in the sorted sub-pass. Fam 14 (scrollincandescent)
if (cam_pos.w > -12.5 || cam_pos.w < -13.5) { // never alpha-tests either (its technique sets no test state, and its
// PS ignores the diffuse alpha entirely).
if (cam_pos.w > -12.5 || cam_pos.w < -14.5) {
float aref = cam_pos.w < -0.5 ? 0.1176 : 0.35; 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); clip(misc.x > 0.0 ? albedo.a * albedo.a - 0.0627 : albedo.a - aref);
} }
@ -1128,6 +1132,16 @@ float4 ps_main(VSOut i) : SV_Target {
} else { } else {
lin = dlin * (fam < 11.5 ? sh_fogp.w : 1.0); lin = dlin * (fam < 11.5 ? sh_fogp.w : 1.0);
} }
} else if (fam > 13.5 && fam < 14.5) {
// scrollincandescent (the stadium LED chyron band): emissive scrolled
// diffuse. The game's VS adds g_fAnimationTime * (uSpeed, vSpeed) to
// the texcoord (misc.xy carries that offset, period-wrapped CPU-side);
// its PS is one diffuse fetch -> D^2 * m_params[0].y (misc.z) ->
// the shared fog/exposure/tone tail. No lightmap term, no kd, no
// shadow receive, no material multiplier on the fog term
// (ucode-exact: scrollincandescent_defaultPS).
float3 sd = diffuse.Sample(smp, i.uv + misc.xy).rgb;
lin = sd * sd * misc.z;
} else { } else {
// Environment families: macro overlay (0.5-neutral, fades under decal // Environment families: macro overlay (0.5-neutral, fades under decal
// art), linear decal composite, lightmap squared and min-clamped // art), linear decal composite, lightmap squared and min-clamped

File diff suppressed because it is too large Load diff

View file

@ -1910,6 +1910,8 @@ bool BuildItemFromMesh(uint8_t* base, uint32_t mesh, DrawItem& item) {
item.macro_tex = 0; item.macro_tex = 0;
item.macro_scale = 1.0f; item.macro_scale = 1.0f;
item.macro_opacity = 1.0f; item.macro_opacity = 1.0f;
item.scroll_u = 0.0f;
item.scroll_v = 0.0f;
item.decal_art = 0; item.decal_art = 0;
item.hair = false; item.hair = false;
item.char_family = 0; item.char_family = 0;
@ -1997,6 +1999,16 @@ bool BuildItemFromMesh(uint8_t* base, uint32_t mesh, DrawItem& item) {
item.detail_scale = f; item.detail_scale = f;
} }
continue; continue;
} else if (std::memcmp(text, "uAnimationSpeed", 16) == 0 ||
std::memcmp(text, "vAnimationSpeed", 16) == 0) {
// Shader-constant channels (the game's VS c7.x / c8.x): UV scroll
// speed in texcoords per second, consumed by the fam-14
// scrollincandescent branch. Signed; the direction is authored.
const float f = std::bit_cast<float>(chan_u32(i, 0x10));
if (std::fabs(f) < 1e6f) { // false for NaN too
(text[0] == 'u' ? item.scroll_u : item.scroll_v) = f;
}
continue;
} else if (std::memcmp(text, "macroOverlayUVScale", 20) == 0 || } else if (std::memcmp(text, "macroOverlayUVScale", 20) == 0 ||
std::memcmp(text, "macroOverlayOpacity", 20) == 0) { std::memcmp(text, "macroOverlayOpacity", 20) == 0) {
// Shader-constant channel: the float lives in the first guid word. // Shader-constant channel: the float lives in the first guid word.
@ -2142,6 +2154,11 @@ bool BuildItemFromMesh(uint8_t* base, uint32_t mesh, DrawItem& item) {
item.env_family = 11; item.env_family = 11;
} else if (is("incandescent.default", 21)) { } else if (is("incandescent.default", 21)) {
item.env_family = 12; item.env_family = 12;
} else if (is("incandescent.backlituvscroll", 29)) {
// scrollincandescent.fx: emissive diffuse scrolled by
// g_fAnimationTime x the u/vAnimationSpeed channels (the
// stadium LED chyron band).
item.env_family = 14;
} }
// dynamicobject.fx props (dispensers, dumpsters, benches, cans): // dynamicobject.fx props (dispensers, dumpsters, benches, cans):
// rigid movable objects with their own dual-shadow lit PS // rigid movable objects with their own dual-shadow lit PS
@ -4161,7 +4178,7 @@ void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t
if ((!g_fog_frame_done || !g_shadow_frame_done || !g_sky_frame_done || 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_water_frame_done || !g_ocean_frame_done ||
!g_oceanrefl_frame_done) && !g_oceanrefl_frame_done || !g_scroll_frame_done) &&
func == 0 && flags2d == 0 && SceneEnabled() && func == 0 && flags2d == 0 && SceneEnabled() &&
(g_fog_cam[0] != 0.0f || g_fog_cam[1] != 0.0f || g_fog_cam[2] != 0.0f)) { (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]; const float dx = LoadGuestF32(base, bank + 16 * 4) - g_fog_cam[0];
@ -4382,6 +4399,46 @@ void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t
} }
} }
} }
// scrollincandescent.fx (the emissive time-scrolled LED chyron):
// g_fAnimationTime from the VERTEX bank (c9.x) and the material
// multiplier m_params[0].y from the PIXEL bank (c3.y). Same POSITIVE
// debug-path gate as the water rows; the bank values alone cannot
// discriminate this family.
if (!g_scroll_frame_done && ps_bank != 0) {
const auto scroll_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, "\\scrollincandescent") != nullptr;
};
return check(g_cur_ps_obj.load(std::memory_order_relaxed)) ||
check(g_cur_vs_obj.load(std::memory_order_relaxed));
};
if (scroll_ps()) {
const float t = LoadGuestF32(base, bank + 36 * 4); // VS c9.x
const float mult = LoadGuestF32(base, ps_bank + 13 * 4); // PS c3.y
// Range gate: the animation clock is a non-negative seconds
// counter, the multiplier a small positive scale.
if (t >= 0.0f && t < 1e7f && mult > 0.0f && mult <= 8.0f) {
const bool first = !g_scroll_have;
g_scroll_rows[0] = t;
g_scroll_rows[1] = mult;
g_scroll_have = true;
g_scroll_frame_done = true;
if (first) {
REXLOG_INFO(
"native-scene: scroll rows captured: t={:.2f} mult={:.3g}",
t, mult);
}
}
}
}
// ocean_defaultPS material rows (PCA mean/weights at c2..c8, // ocean_defaultPS material rows (PCA mean/weights at c2..c8,
// m_params[0..2] at c12..c14) and the oceanreflection sheet row (c3). // m_params[0..2] at c12..c14) and the oceanreflection sheet row (c3).
// Same POSITIVE debug-path gating as the water rows. // Same POSITIVE debug-path gating as the water rows.
@ -9080,6 +9137,10 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
sizeof(scene.oceanrefl_rows)); sizeof(scene.oceanrefl_rows));
scene.oceanrefl_valid = true; scene.oceanrefl_valid = true;
} }
if (g_scroll_have) {
std::memcpy(scene.scroll_rows, g_scroll_rows, sizeof(scene.scroll_rows));
scene.scroll_valid = true;
}
if (g_shadow_have) { if (g_shadow_have) {
std::memcpy(scene.shadow_rows, g_shadow_rows, sizeof(g_shadow_rows)); std::memcpy(scene.shadow_rows, g_shadow_rows, sizeof(g_shadow_rows));
scene.shadow_valid = true; scene.shadow_valid = true;
@ -9226,6 +9287,7 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
g_water_frame_done = false; g_water_frame_done = false;
g_ocean_frame_done = false; g_ocean_frame_done = false;
g_oceanrefl_frame_done = false; g_oceanrefl_frame_done = false;
g_scroll_frame_done = false;
// Draw-time STRETCH VETO: the last line of defense, judging what the GPU // Draw-time STRETCH VETO: the last line of defense, judging what the GPU

View file

@ -64,6 +64,11 @@ struct DrawItem {
uint32_t macro_tex; // guest renderengine::Texture address (0 = none) uint32_t macro_tex; // guest renderengine::Texture address (0 = none)
float macro_scale; // macroOverlayUVScale channel constant float macro_scale; // macroOverlayUVScale channel constant
float macro_opacity; // macroOverlayOpacity channel constant float macro_opacity; // macroOverlayOpacity channel constant
// uAnimationSpeed / vAnimationSpeed channel constants (texcoords per
// second of the frame's animation time): the scrollincandescent UV
// scroll (fam 14: the stadium LED chyron band). 0 = no scroll.
float scroll_u;
float scroll_v;
uint16_t pos_offset; uint16_t pos_offset;
uint16_t uv_offset; uint16_t uv_offset;
uint16_t uv2_offset; uint16_t uv2_offset;
@ -210,7 +215,9 @@ struct DrawItem {
// 7 = environmentsimple.alphatest, 8 = environmentsimple.diffuse, // 7 = environmentsimple.alphatest, 8 = environmentsimple.diffuse,
// 9 = tree.default, 10 = animated.tree, 11 = proxyworld.default, // 9 = tree.default, 10 = animated.tree, 11 = proxyworld.default,
// 12 = incandescent.default, 13 = environment.reflective_trans // 12 = incandescent.default, 13 = environment.reflective_trans
// (transparentenvironmentreflective: blended glass, alpha sub-pass). // (transparentenvironmentreflective: blended glass, alpha sub-pass),
// 14 = incandescent.backlituvscroll (scrollincandescent: the emissive
// time-scrolled LED chyron band).
uint8_t env_family = 0; uint8_t env_family = 0;
// dynamicobject.fx family (movable props: dispensers, dumpsters, benches, // dynamicobject.fx family (movable props: dispensers, dumpsters, benches,
// cans, ...). 0 = not a dynamic object, 1 = dynamicobject.default, // cans, ...). 0 = not a dynamic object, 1 = dynamicobject.default,
@ -340,6 +347,12 @@ struct FrameScene {
// height-ramp alpha fade bounds. // height-ramp alpha fade bounds.
bool oceanrefl_valid = false; bool oceanrefl_valid = false;
float oceanrefl_rows[4] = {}; float oceanrefl_rows[4] = {};
// scrollincandescent.fx rows, captured once per frame from a
// scrollincandescent draw (debug-path classified): [0] = g_fAnimationTime
// (VS c9.x), [1] = the material multiplier m_params[0].y (PS c3.y).
// Consumed by the fam-14 branch (the scrolling LED chyron).
bool scroll_valid = false;
float scroll_rows[2] = {};
// dynamicobject.fx frame-global lighting rows, captured from a // dynamicobject.fx frame-global lighting rows, captured from a
// dynamicobject/alphatestdynamicobject PS bank (debug-path classified): // dynamicobject/alphatestdynamicobject PS bank (debug-path classified):
// [0..2] sun direction (PS c9), [3] scene exposure (c13.x), // [0..2] sun direction (PS c9), [3] scene exposure (c13.x),

View file

@ -7801,7 +7801,11 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
// exposure / material multiplier at b1); without them the tone chain // exposure / material multiplier at b1); without them the tone chain
// would multiply by zero and render black. // would multiply by zero and render black.
if (debug_mode == 0 && item.env_family != 0 && scene.shadow_valid && if (debug_mode == 0 && item.env_family != 0 && scene.shadow_valid &&
!item.water && !item.transparent) { !item.water && !item.transparent &&
// Fam 14 needs the frame's scroll rows (time + multiplier); the
// tone chain would multiply by zero without them; the legacy
// shading covers the capture-less frames.
(item.env_family != 14 || scene.scroll_valid)) {
constants[39] = -float(item.env_family); constants[39] = -float(item.env_family);
} }
// cam_pos.w = -(20 + variant) selects the exact dynamicobject branch // cam_pos.w = -(20 + variant) selects the exact dynamicobject branch
@ -8190,6 +8194,18 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
constants[50] = scene.fog_ramp[1]; constants[50] = scene.fog_ramp[1];
constants[51] = scene.fog_ramp[2]; constants[51] = scene.fog_ramp[2];
std::memcpy(constants + 40, scene.fog_color, 4 * sizeof(float)); std::memcpy(constants + 40, scene.fog_color, 4 * sizeof(float));
} else if (item.env_family == 14 && constants[39] < -13.5f) {
// Fam 14 (scrollincandescent, the LED chyron): misc.xy = the frame's
// UV scroll offset (g_fAnimationTime x the material's channel speeds,
// wrapped to one texture period here; the shader adds a small
// bounded offset instead of losing uv precision to a large raw
// time x speed product), misc.z = the material multiplier
// m_params[0].y.
const float ou = scene.scroll_rows[0] * item.scroll_u;
const float ov = scene.scroll_rows[0] * item.scroll_v;
constants[48] = ou - std::floor(ou);
constants[49] = ov - std::floor(ov);
constants[50] = scene.scroll_rows[1];
} else if (v2_flags != 0) { } else if (v2_flags != 0) {
// misc.z = the v2 material bind flags, misc.w = detailNormalUVScale // misc.z = the v2 material bind flags, misc.w = detailNormalUVScale
// (opaque fams 1-4 and the dynobj families; see the v2 blocks above). // (opaque fams 1-4 and the dynobj families; see the v2 blocks above).

View file

@ -310,6 +310,13 @@ inline bool g_ocean_frame_done = false;
inline float g_oceanrefl_rows[4] = {}; inline float g_oceanrefl_rows[4] = {};
inline bool g_oceanrefl_have = false; inline bool g_oceanrefl_have = false;
inline bool g_oceanrefl_frame_done = false; inline bool g_oceanrefl_frame_done = false;
// scrollincandescent.fx rows (see FrameScene::scroll_rows): [0] =
// g_fAnimationTime (VS c9.x), [1] = the material multiplier m_params[0].y
// (PS c3.y). Captured from a scrollincandescent draw (debug-path
// classified). Guest render thread only.
inline float g_scroll_rows[2] = {};
inline bool g_scroll_have = false;
inline bool g_scroll_frame_done = false;
// dynamicobject.fx frame-global lighting rows (see FrameScene::dynobj_rows), // dynamicobject.fx frame-global lighting rows (see FrameScene::dynobj_rows),
// captured from a dynamicobject/alphatestdynamicobject PS bank (debug-path // captured from a dynamicobject/alphatestdynamicobject PS bank (debug-path
// classified). Guest render thread only. // classified). Guest render thread only.