render: texture decode, ropa cloth timing and scene fixes

This commit is contained in:
mchughalex 2026-07-13 10:31:07 +01:00
parent cf274e7457
commit 2ac10ead44
6 changed files with 1963 additions and 153 deletions

View file

@ -230,6 +230,8 @@ set(SKATE3_COMMON_SOURCES
src/native/skate3_native_diagnostics.cpp
src/native/skate3_native_guest_read.cpp
src/native/skate3_native_v3_shadow.cpp
src/native/skate3_native_v3_shadow_mat.cpp
src/native/skate3_hud_tile.cpp
src/skate3_native_debug_dialog.cpp
src/skate3_native_render.cpp
src/skate3_native_scene.cpp

View file

@ -5,7 +5,8 @@
// 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)
// m[9].x = apply D3D9 half-pixel (2D ortho draws only),
// m[9].y = sharp-magnification amount (0 = plain bilinear)
};
Texture2D<float4> tex : register(t0);
SamplerState smp : register(s1);
@ -31,6 +32,67 @@ VSOut vs_main(float4 p : POSITION, float2 uv : TEXCOORD0, float4 color : COLOR0)
o.color = color;
return o;
}
float4 ps_main(VSOut i) : SV_Target {
return tex.Sample(smp, i.uv) * m[8] * i.color;
// Catmull-Rom reconstruction (9 bilinear fetches). Sharper interpolation
// than plain bilinear for magnified art; the sharpen pass below restores
// edge contrast the source's own 1-texel antialiasing loses under
// magnification.
float4 SampleCR(float2 uv, float2 ts) {
float2 sp = uv * ts;
float2 tc = floor(sp - 0.5) + 0.5;
float2 f = sp - tc;
float2 f2 = f * f;
float2 f3 = f2 * f;
float2 w0 = f2 - 0.5 * (f3 + f);
float2 w1 = 1.5 * f3 - 2.5 * f2 + 1.0;
float2 w3 = 0.5 * (f3 - f2);
float2 w2 = 1.0 - w0 - w1 - w3;
float2 w12 = w1 + w2;
float2 t0 = (tc - 1.0) / ts;
float2 t3 = (tc + 2.0) / ts;
float2 t12 = (tc + w2 / w12) / ts;
float4 c = tex.Sample(smp, float2(t0.x, t0.y)) * (w0.x * w0.y);
c += tex.Sample(smp, float2(t12.x, t0.y)) * (w12.x * w0.y);
c += tex.Sample(smp, float2(t3.x, t0.y)) * (w3.x * w0.y);
c += tex.Sample(smp, float2(t0.x, t12.y)) * (w0.x * w12.y);
c += tex.Sample(smp, float2(t12.x, t12.y)) * (w12.x * w12.y);
c += tex.Sample(smp, float2(t3.x, t12.y)) * (w3.x * w12.y);
c += tex.Sample(smp, float2(t0.x, t3.y)) * (w0.x * w3.y);
c += tex.Sample(smp, float2(t12.x, t3.y)) * (w12.x * w3.y);
c += tex.Sample(smp, float2(t3.x, t3.y)) * (w3.x * w3.y);
return c;
}
float4 ps_main(VSOut i) : SV_Target {
float4 c = tex.Sample(smp, i.uv);
// Sharp magnification (m[9].y = skate3_native_render_scene_2d_sharp):
// much of the HUD samples APT cached-bitmap tiles whose texel count
// equals their 720p display size; at 2-3x output scales they magnify
// into a soft blur under plain bilinear while atlas-glyph text (8-10
// texels per 720p pixel) stays crisp. Where the fetch is MAGNIFIED,
// reconstruct with Catmull-Rom and apply a locally-CLAMPED unsharp mask
// (no halos: the result never leaves the neighborhood's value range).
// Minified/1:1 content (density-rich atlases, replay video at 1x) is
// untouched by the magnification gate.
if (m[9].y > 0.0) {
float2 ts;
tex.GetDimensions(ts.x, ts.y);
float2 tpp = fwidth(i.uv) * ts; // texel footprint per output pixel
float mag = 1.0 / max(max(tpp.x, tpp.y), 1e-4);
if (mag > 1.25) {
float4 cr = SampleCR(i.uv, ts);
float2 px = 1.0 / ts;
float4 n0 = tex.Sample(smp, i.uv + float2(-0.5, -0.5) * px);
float4 n1 = tex.Sample(smp, i.uv + float2(0.5, -0.5) * px);
float4 n2 = tex.Sample(smp, i.uv + float2(-0.5, 0.5) * px);
float4 n3 = tex.Sample(smp, i.uv + float2(0.5, 0.5) * px);
float4 blur = (n0 + n1 + n2 + n3) * 0.25;
// Ramp in across 1.25x..2x so density-1 tiles at 2-3x output get the
// full amount while barely-magnified art is barely touched.
float amt = m[9].y * saturate((mag - 1.25) / 0.75);
float4 s = cr + (cr - blur) * amt;
float4 lo = min(min(n0, n1), min(n2, n3));
float4 hi = max(max(n0, n1), max(n2, n3));
c = clamp(s, min(lo, cr), max(hi, cr));
}
}
return c * m[8] * i.color;
}

View file

@ -419,7 +419,9 @@ float4 ps_main(VSOut i) : SV_Target {
// 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;
// Console lightmap semantics: bilinear, clamped, mip 0 (see the
// main env fetch below).
float3 lmg = lightmap.SampleLevel(smp_clamp, i.uv2, 0.0).rgb;
lin = dlin * max(lmg * lmg, sh_env.z) * sh_env.y;
if (fam < 9.5) {
lin *= sh_env.w;
@ -445,7 +447,13 @@ float4 ps_main(VSOut i) : SV_Target {
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);
// The macro weathering overlay applies OVER the composited art:
// ApplyOverlay(cOverlay, ApplyDecal(...)) in the decalenvironment
// source. The previous `ov = lerp(1, ov, 1-dk.a)` fade rendered the
// paint unweathered: on the PCU Library ramp stencils the measured
// native/emulated brightness error was 1.19x at paint alpha~1,
// 1.05x at ~0.35 and 1.0x off-patch: exactly 1/ov for ov~0.84,
// the no-fade model (measured from a matched A/B capture pair).
}
float3 dcol = dlin * ov;
// CSM shadow term s = sat(infront + 1 - coverage) from the native
@ -495,7 +503,12 @@ float4 ps_main(VSOut i) : SV_Target {
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);
// tint.r == 0 = the real lightmap has not resolved yet (first-sight
// decode in flight; t1 = the white fallback). Min-clamping the
// fallback against the CSM term rendered in-shadow surfaces as BLACK
// patches for the decode window (the ramp-stencil "black square
// flash"); serve unshadowed brightness until the real page lands.
float3 lml = tint.r > 0.0 ? min(lmg * lmg, s + sh_color.rgb) : lmg * lmg;
// 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
@ -642,6 +655,24 @@ float4 ps_main(VSOut i) : SV_Target {
float3 cce = saturate(sqrt(max(tme * 0.5, 0.0)) * 1.41);
return float4(cce, out_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). Composited BEFORE the
// macro overlay: the weathering applies over the paint too
// (ApplyOverlay(cOverlay, ApplyDecal(...)); the old order left the paint
// unweathered, the too-white ramp stencils).
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);
}
// 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
@ -653,21 +684,6 @@ float4 ps_main(VSOut i) : SV_Target {
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
@ -676,7 +692,10 @@ float4 ps_main(VSOut i) : SV_Target {
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;
// Console lightmap semantics: bilinear, clamped, mip 0; the composed
// atlas pages are single-level; mip/wrap sampling bled neighbor cells
// and page edges (see the exact env fetch).
light = lightmap.SampleLevel(smp_clamp, i.uv2, 0.0).rgb * 2.0;
} else {
// Smooth per-vertex normal when the mesh has one; face normal from
// position derivatives otherwise.

View file

@ -21,6 +21,10 @@
#if defined(_WIN32)
#include <windows.h>
// Xbox-controller capture combos (see the hotkey block): the artifacts under
// investigation are too brief to reach the keyboard from the pad.
#include <Xinput.h>
#pragma comment(lib, "xinput9_1_0.lib")
#endif
#include <rex/cvar.h>
@ -366,6 +370,17 @@ void OnFrameEnd(uint8_t* base) {
REXLOG_INFO("native-render: F8, texture + mesh caches flushed");
}
f8_was_down = f8_down;
// F7: dump the rolling scene-composition ring (last ~900 frames of
// per-item signatures); press within a few seconds of SEEING a 1-2
// frame artifact; diffing the artifact frame against neighbors in the
// CSV names the item that flashed.
static bool f7_was_down = false;
const bool f7_down = (GetAsyncKeyState(VK_F7) & 0x8000) != 0;
if (f7_down && !f7_was_down) {
skate3::native_scene::RequestSceneRingDump();
REXLOG_INFO("native-render: F7, scene ring dump requested");
}
f7_was_down = f7_down;
static bool f10_was_down = false;
const bool f10_down = (GetAsyncKeyState(VK_F10) & 0x8000) != 0;
if (f10_down && !f10_was_down) {
@ -377,6 +392,47 @@ void OnFrameEnd(uint8_t* base) {
g_frame_index);
}
f10_was_down = f10_down;
// Xbox controller capture combos (the artifacts are too brief to reach
// the keyboard from the pad): RB+X = F10-style immediate capture,
// RB+A = F7 scene-ring dump (retroactive ~17 s, so a slightly late
// press still contains the flash frame). Pad 0; disconnected pads
// re-probe on a backoff; XInputGetState is slow for absent devices.
{
static bool combo_x_was = false;
static bool combo_a_was = false;
static uint32_t pad_retry = 0;
static bool pad_seen = true;
if (pad_seen || ++pad_retry >= 240) {
pad_retry = 0;
XINPUT_STATE xs{};
if (XInputGetState(0, &xs) == ERROR_SUCCESS) {
pad_seen = true;
const WORD b = xs.Gamepad.wButtons;
const bool rb = (b & XINPUT_GAMEPAD_RIGHT_SHOULDER) != 0;
const bool cx = rb && (b & XINPUT_GAMEPAD_X) != 0;
const bool ca = rb && (b & XINPUT_GAMEPAD_A) != 0;
if (cx && !combo_x_was) {
g_collecting = true;
g_immediate = true;
g_collect_counter = 0;
skate3::native_scene::StartRecording(1);
REXLOG_INFO(
"native-render snapshot: RB+X, immediate single-frame capture "
"at frame {}",
g_frame_index);
}
combo_x_was = cx;
if (ca && !combo_a_was) {
skate3::native_scene::RequestSceneRingDump();
REXLOG_INFO("native-render: RB+A, scene ring dump requested");
}
combo_a_was = ca;
} else {
pad_seen = false;
combo_x_was = combo_a_was = false;
}
}
}
}
// P: cycle the synthetic camera pan probe (judder isolation: a host-
// driven constant-rate pan injected at a selectable pipeline stage; see
@ -904,3 +960,42 @@ extern "C" REX_FUNC(sub_82B79FC0) {
skate3::native_scene::OnDrawDone(base, 2, r4, r5, r6, ctx.r3.u32 != 0 ? ctx.r3.u32 : r7);
}
}
// ---- v3 shadow-mat reader hooks (observers only; see
// native/skate3_native_v3_shadow_mat.h). Declared here instead of including
// the header so this section is a pure append.
namespace skate3::native_v3_mat {
void OnConstantTable(uint8_t* base, uint32_t table, uint32_t count);
} // namespace skate3::native_v3_mat
// renderengine::ProgramBuffer::Xbox2CreateConstantTable: builds a shader's
// 8-byte {name, register, dataType, numConstants} record array. Register
// conventions VERIFIED in generated/skate3_recomp.60.cpp:37720: ENTRY r3 =
// shader-blob holder (+16 = CTAB rel offset), r4 = destination record array
// (NULL on the sizing pass: the Initialize caller at .60.cpp:38221 passes
// ProgramBuffer + *(u32*)(PB+8) + 20 on the fill pass and stores the return
// count at PB+4), r5 = out name-bytes; EXIT r3 = record count. Firing once
// per shader table as it is built gives the reflect reader every table with
// no ProgramBuffer back-pointer needed.
extern "C" REX_FUNC(sub_82A77EA0) {
const uint32_t table = ctx.r4.u32;
__imp__sub_82A77EA0(ctx, base);
if (skate3::native_render::Enabled()) {
skate3::native_v3_mat::OnConstantTable(base, table, ctx.r3.u32);
}
}
// LivingWorld batch pack writer (unnamed; called per entity per sim tick
// from the tail of cLivingWorldPresEntityManager::Update), the LOD-
// pedestrian skinning-palette source: fused UpdateBoneTransforms+Pack writing the
// concatenated per-mesh 4x3 palettes into cModelInstance.m_matrices. r3 =
// the cLivingWorldPresEntity-derived entity; post-call m_matrices holds the
// packed rows.
extern "C" REX_FUNC(sub_827C1D38) {
const uint32_t entity = ctx.r3.u32;
__imp__sub_827C1D38(ctx, base);
if (skate3::native_render::Enabled()) {
skate3::native_v3::OnLwPack(base, entity);
}
}

File diff suppressed because it is too large Load diff

View file

@ -139,6 +139,21 @@ struct DrawItem {
// Detected via the AttribulatorMaterialName channel; consumed by
// CaptureSkinnedState.
bool ropa;
// ROPA shape blending (set by InterpolateDynamicItems): decoded shape
// generations (DynDecodeJob seq) + weights reproducing the SAME temporal
// kernel the body bones / garment world were evaluated with this frame:
// the 8-tap boxcar when the world took it, else the plain bracketing
// lerp. shape_count == 0 = no pairing this frame (draw the newest cached
// decode). The stepped cloth shape against the interpolated body was the
// tee jelly/clip-through (worse at LOWER fps: the excursion scales with
// the guest period); a plain 2-generation lerp against the BOXCAR-
// filtered body left the same period-scaled mismatch; the two filters
// answer the same 60 Hz limb signal with different frequency responses,
// so the kernels must match exactly.
static constexpr int kShapeGens = 10;
uint64_t shape_seq[kShapeGens] = {};
float shape_w[kShapeGens] = {};
int shape_count = 0;
// Exact world-material shading family, classified from
// AttribulatorMaterialName (models verified against the game's own pixel
// shaders):
@ -447,6 +462,13 @@ int CycleSyntheticPan();
// shows whether the game's own pose sequence is irregular at the source.
void RecordCameraSignal(double seconds);
// F7: dump the rolling per-frame scene-composition ring (one compact
// signature per item per frame for the last ~900 frames) to
// logs/scene_ring_<ts>.csv on the render thread's next frame. Diffing the
// artifact frame against its neighbors names exactly which item appeared /
// vanished / changed textures during a 1-2 frame flash no capture can catch.
void RequestSceneRingDump();
// Bone-signal recorder (wheel-guard diagnosis): for `seconds`, records every
// raw entity pose (ring pushes: bone palettes / rigid worlds, timestamps)
// and each frame's interpolated output, then writes