ropa cloth capture/publish coherence (garment offset, ribbon flash, palette gates)
This commit is contained in:
parent
f689775536
commit
c44458dec9
2 changed files with 556 additions and 16 deletions
|
|
@ -216,6 +216,13 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_tex_revalidate, true, "Skate 3",
|
|||
"Re-fingerprint cached texture payloads every 16 frames and "
|
||||
"re-decode on change (heals late-composed lightmap pages)")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_stretch_guard, true, "Skate 3",
|
||||
"Draw-time stretch veto: skin cached sample verts of each "
|
||||
"skinned mesh's GPU-resident decode with the final palette "
|
||||
"every frame; wider than bind size = the 1-frame mangled-"
|
||||
"ribbon flash: skip the item's draws (blink) and dump the "
|
||||
"palette to logs/stretch_*.txt for diagnosis")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_lm_dump, false, "Skate 3",
|
||||
"Diagnostic: dump the decoded mip 0 of every generated-mip "
|
||||
"(no-chain composed page) texture to native_texture_dumps/ "
|
||||
|
|
@ -664,6 +671,51 @@ struct RopaResolvedState {
|
|||
};
|
||||
std::unordered_map<uint32_t, RopaResolvedState> g_ropa_state_cache;
|
||||
std::atomic<uint64_t> g_ropa_rescued{0};
|
||||
// mesh -> payload identity + resolved MODE of the ropa decode currently
|
||||
// RESIDENT on the GPU (written by PrewarmCommit on the render thread when a
|
||||
// dyn decode job lands; read by BuildFrameScene on the guest thread, hence
|
||||
// the mutex). The dyn decode workers run 1-2 frames behind the capture, and
|
||||
// the draw path deliberately tolerates a fingerprint mismatch on dynamic
|
||||
// payloads (cloth one frame behind the sim). At a sim MODE FLIP that
|
||||
// tolerance was the residual ribbon: the flip frame publishes the NEW
|
||||
// mode's interpretation (skinned palette) while the GPU still draws the OLD
|
||||
// payload's decode (sim-deformed root-local verts) until the worker's
|
||||
// re-decode commits: skinned-over-sim content for 1-2 frames, the brief
|
||||
// hard-to-catch stretched-strip flash. The frame-end coherence guard cannot
|
||||
// see this (ropa[mismatch] stayed 0 in testing): the
|
||||
// SNAPSHOT it checks is coherent with the new mode; the RESIDENT decode is
|
||||
// not. BuildFrameScene's flip-hold pass keeps publishing the previous
|
||||
// resolved state until the resident decode matches the published mode.
|
||||
struct RopaResidentDecode {
|
||||
uint64_t fp = 0;
|
||||
bool skinned = false;
|
||||
};
|
||||
std::mutex g_ropa_resident_mutex;
|
||||
std::unordered_map<uint32_t, RopaResidentDecode> g_ropa_resident;
|
||||
// mesh -> sample verts of the most recent DECODE of a skinned mesh (bind-
|
||||
// space position + raw blend attribs, ~32 evenly spaced), written by
|
||||
// DecodeMesh on whichever thread decodes (workers / render), read by the
|
||||
// guest thread's draw-time STRETCH VETO in BuildFrameScene. The veto skins
|
||||
// these samples with the FINAL post-interpolation palette, i.e. it judges
|
||||
// what the GPU will ACTUALLY draw. Every upstream gate (capture acceptance,
|
||||
// publish coherence) judges the LIVE guest VB instead, so a decode-content
|
||||
// vs palette pairing mismatch, or junk introduced by the interpolation
|
||||
// substitutions, passes all of them and still flashes the 1-frame
|
||||
// map-length ribbon (observed with every ropa[] counter clean).
|
||||
// `fp` = the decode's payload fingerprint so the veto can log whether the
|
||||
// drawn content even matches the frame's payload (content lag vs junk
|
||||
// palette: the decisive diagnostic split).
|
||||
struct SkinProbeSample {
|
||||
float p[3];
|
||||
uint32_t bw;
|
||||
uint32_t bi;
|
||||
};
|
||||
struct SkinProbe {
|
||||
std::vector<SkinProbeSample> s;
|
||||
uint64_t fp = 0;
|
||||
};
|
||||
std::mutex g_skin_probe_mutex;
|
||||
std::unordered_map<uint32_t, SkinProbe> g_skin_probe;
|
||||
std::atomic<uint32_t> g_cur_ib{0};
|
||||
std::atomic<uint32_t> g_cur_vb{0};
|
||||
std::atomic<uint32_t> g_cur_vb_offset{0};
|
||||
|
|
@ -850,6 +902,34 @@ std::atomic<uint64_t> g_ropa_flip{0};
|
|||
// draw-time capture): the re-decode is skipped so the GPU keeps last
|
||||
// frame's coherent verts instead of rendering the mangled ribbon.
|
||||
std::atomic<uint64_t> g_ropa_mismatch{0};
|
||||
// ropa captures accepted through the RELAXED near-camera criterion (all
|
||||
// samples in FRONT of the projection inside a loose 6x guard band + sane
|
||||
// spread) after the strict half-in-clip score refused them. Up close the
|
||||
// garment fills/overflows the screen and most of the 6 sample verts clip;
|
||||
// the strict gate refused the CORRECT palette every frame, and with no
|
||||
// cached resolved state to rescue from the torso stayed invisible until the
|
||||
// NPC was far enough away again (log signature: `ropa mesh=... score=2
|
||||
// flag=(1.000,...)` repeating while stale climbed with rescued flat).
|
||||
std::atomic<uint64_t> g_ropa_relaxed{0};
|
||||
// Frames a ropa garment was HELD on its previous resolved state (or dropped
|
||||
// when no previous state existed) because the GPU-resident decode still
|
||||
// pairs with the other mode; see g_ropa_resident.
|
||||
std::atomic<uint64_t> g_ropa_hold{0};
|
||||
// Ropa garments PUBLISHED with a caster-cascade (ortho) bank's palette:
|
||||
// the ~40 ms-stale rows behind the garment-offset-from-body symptom. The
|
||||
// same-mode graft in the dyn_slot merge should keep this at ~0 whenever a
|
||||
// perspective capture exists in the same frame.
|
||||
std::atomic<uint64_t> g_ropa_caster{0};
|
||||
// Published skinned items whose palette FAILED the dense 32-sample
|
||||
// publish-time coherence gate (see PublishedPaletteSane): the 1-frame
|
||||
// junk-palette ribbon flash that passes every 6-sample capture gate. The
|
||||
// item re-publishes its cached state instead (or drops for one frame).
|
||||
std::atomic<uint64_t> g_pub_incoherent{0};
|
||||
// Draw-time STRETCH VETO trips (see g_skin_probe): the final palette skins
|
||||
// the resident decode's sample verts wider than bind size; the item's
|
||||
// draws are cleared for the frame (blink, not ribbon) and the first events
|
||||
// dump full palette + samples to logs/stretch_*.txt for offline diagnosis.
|
||||
std::atomic<uint64_t> g_stretch_veto{0};
|
||||
// skinned/ropa meshes that published after a 1-3 frame publish GAP (was on
|
||||
// screen, vanished, came back): the "flickered invisible" telemetry.
|
||||
std::atomic<uint64_t> g_dyn_gap{0};
|
||||
|
|
@ -1060,8 +1140,19 @@ float GuestHalfToFloat(uint16_t h) {
|
|||
// disappearing-hat bug. Returns the winning base, or 0 when no candidate
|
||||
// both projects in-clip and keeps a sane spread; the caller then refuses
|
||||
// the capture and the post-draw fixup retries on a later draw.
|
||||
// structural_guess: the caller PROVED the guess base's layout structurally
|
||||
// (the ropa flag row read exactly (1,...) from the mesh's own bank; every
|
||||
// capture site guarantees own-draw provenance now, so the foreign-bank
|
||||
// hypothesis the strict gate defends against is off the table). When the
|
||||
// strict half-in-clip score still refuses every home, accept the guess if
|
||||
// all samples skin IN FRONT of the projection inside a loose 6x guard band
|
||||
// at a sane spread: up close the garment fills/overflows the screen and
|
||||
// most sample verts clip the tight 1.5x band; the strict gate refused the
|
||||
// CORRECT palette every frame and the torso stayed invisible until the NPC
|
||||
// walked far enough away. A row-shifted palette still fails this (skins
|
||||
// hundreds of meters off-view or behind the camera).
|
||||
uint32_t RefinePaletteBase(uint8_t* base, uint32_t bank, uint32_t palette_base,
|
||||
const DrawItem& item) {
|
||||
const DrawItem& item, bool structural_guess = false) {
|
||||
if (item.bw_offset == 0 || item.bi_offset == 0 || item.stride == 0) {
|
||||
return palette_base;
|
||||
}
|
||||
|
|
@ -1077,9 +1168,13 @@ uint32_t RefinePaletteBase(uint8_t* base, uint32_t bank, uint32_t palette_base,
|
|||
constexpr uint32_t kSamples = 6;
|
||||
// score: fraction (0..16) of samples that skin in front of and inside the
|
||||
// bank's own clip volume; *out_spread = the skinned samples' bbox diagonal
|
||||
// (world units) for the bind-size sanity test below.
|
||||
const auto score = [&](uint32_t pb, float* out_spread) -> int {
|
||||
// (world units) for the bind-size sanity test below. *out_front_all (when
|
||||
// asked): every sample is in FRONT of the projection within a loose 6x
|
||||
// guard band, the relaxed near-camera criterion (see structural_guess).
|
||||
const auto score = [&](uint32_t pb, float* out_spread,
|
||||
bool* out_front_all = nullptr) -> int {
|
||||
int ok = 0;
|
||||
int loose = 0;
|
||||
int n = 0;
|
||||
float qmin[3] = {1e9f, 1e9f, 1e9f};
|
||||
float qmax[3] = {-1e9f, -1e9f, -1e9f};
|
||||
|
|
@ -1147,10 +1242,17 @@ uint32_t RefinePaletteBase(uint8_t* base, uint32_t bank, uint32_t palette_base,
|
|||
std::abs(clip[1]) <= 1.5f * aw) {
|
||||
++ok;
|
||||
}
|
||||
if (clip[3] > 0.0f && std::abs(clip[0]) <= 6.0f * aw &&
|
||||
std::abs(clip[1]) <= 6.0f * aw) {
|
||||
++loose;
|
||||
}
|
||||
}
|
||||
if (n == 0) {
|
||||
return -1;
|
||||
}
|
||||
if (out_front_all) {
|
||||
*out_front_all = n >= 2 && loose == n;
|
||||
}
|
||||
const float dx = qmax[0] - qmin[0];
|
||||
const float dy = qmax[1] - qmin[1];
|
||||
const float dz = qmax[2] - qmin[2];
|
||||
|
|
@ -1276,6 +1378,18 @@ uint32_t RefinePaletteBase(uint8_t* base, uint32_t bank, uint32_t palette_base,
|
|||
return pb;
|
||||
}
|
||||
}
|
||||
// Structurally-proven guess (ropa flag row, own-draw bank): near-camera
|
||||
// relaxed acceptance; see the function comment. Tried LAST so a strictly
|
||||
// passing home always wins first.
|
||||
if (structural_guess) {
|
||||
float spread = 0.0f;
|
||||
bool front_all = false;
|
||||
if (score(palette_base, &spread, &front_all) >= 0 && front_all &&
|
||||
spread <= max_spread) {
|
||||
g_ropa_relaxed.fetch_add(1, std::memory_order_relaxed);
|
||||
return palette_base;
|
||||
}
|
||||
}
|
||||
// No candidate skins this mesh into the bank's own view at a sane size:
|
||||
// the bank belongs to another mesh. The caller refuses the capture (item
|
||||
// stays pending) and the post-draw fixup re-captures from a real draw.
|
||||
|
|
@ -2700,7 +2814,13 @@ void CaptureCharLighting(uint8_t* base, DrawItem& item) {
|
|||
// other object's matrix at c188/c191; geometry projected with it lands
|
||||
// far off-clip, while the true matrix scores full (validated offline:
|
||||
// 31/31 vs 0/31 on an F10 capture).
|
||||
int ScoreRigidAffine(uint8_t* base, uint32_t bank, uint32_t m, const DrawItem& item) {
|
||||
// *out_front_all (when asked): every sample lands IN FRONT of the
|
||||
// projection within a loose 6x guard band, the relaxed near-camera
|
||||
// criterion (a sim-active garment right at the camera clips most of its
|
||||
// samples out of the tight 1.5x band, and the strict >=8 gate refused the
|
||||
// CORRECT rigid matrix; same failure mode as the skinned branch).
|
||||
int ScoreRigidAffine(uint8_t* base, uint32_t bank, uint32_t m, const DrawItem& item,
|
||||
bool* out_front_all = nullptr) {
|
||||
if (item.stride == 0) return -1;
|
||||
const uint32_t count = item.vb_bytes / item.stride;
|
||||
if (count < 2) return -1;
|
||||
|
|
@ -2715,6 +2835,7 @@ int ScoreRigidAffine(uint8_t* base, uint32_t bank, uint32_t m, const DrawItem& i
|
|||
}
|
||||
constexpr uint32_t kSamples = 6;
|
||||
int ok = 0;
|
||||
int loose = 0;
|
||||
int n = 0;
|
||||
for (uint32_t s = 0; s < kSamples; ++s) {
|
||||
const uint32_t v = item.vb_addr + (s * (count - 1) / (kSamples - 1)) * item.stride;
|
||||
|
|
@ -2754,6 +2875,13 @@ int ScoreRigidAffine(uint8_t* base, uint32_t bank, uint32_t m, const DrawItem& i
|
|||
if (std::abs(clip[0]) <= 1.5f * aw && std::abs(clip[1]) <= 1.5f * aw) {
|
||||
++ok;
|
||||
}
|
||||
if (clip[3] > 0.0f && std::abs(clip[0]) <= 6.0f * aw &&
|
||||
std::abs(clip[1]) <= 6.0f * aw) {
|
||||
++loose;
|
||||
}
|
||||
}
|
||||
if (out_front_all) {
|
||||
*out_front_all = n >= 2 && loose == n;
|
||||
}
|
||||
return n == 0 ? -1 : (ok * 16) / n;
|
||||
}
|
||||
|
|
@ -2863,6 +2991,105 @@ bool RopaPayloadCoherent(const DrawItem& item, const std::vector<uint8_t>& vb) {
|
|||
return std::sqrt(dx * dx + dy * dy + dz * dz) <= max_spread;
|
||||
}
|
||||
|
||||
// Dense publish-time coherence gate: skin ~32 samples of the LIVE guest VB
|
||||
// with the item's PUBLISHED palette and require bind-pose spread. The
|
||||
// capture acceptance gates sample only 6 verts, so a palette whose junk
|
||||
// rows sit on UNSAMPLED bones (e.g. a staging bank partially overwritten
|
||||
// by the next entity's rows between this mesh's draw and our capture)
|
||||
// passes them and stretches the unsampled islands into the map-length-
|
||||
// ribbon flash, with zero refusals, zero mismatches, zero gaps in the
|
||||
// telemetry (ropa[stale=0 mismatch=0 hold=0
|
||||
// caster=0] yet a momentary stretch on screen). 32 evenly-spaced samples
|
||||
// cover the islands the 6-vert gates miss. Returns the measured spread via
|
||||
// out_spread for the diagnosis log.
|
||||
bool PublishedPaletteSane(uint8_t* base, const DrawItem& item,
|
||||
float* out_spread) {
|
||||
*out_spread = 0.0f;
|
||||
if (item.stride == 0 || item.bones.size() < 12 || item.bw_offset == 0 ||
|
||||
item.bi_offset == 0 || item.vb_addr == 0) {
|
||||
return true;
|
||||
}
|
||||
const uint32_t count = item.vb_bytes / item.stride;
|
||||
if (count < 2) {
|
||||
return true;
|
||||
}
|
||||
float bind_diag = 0.0f;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
const float d = item.bbox_max[a] - item.bbox_min[a];
|
||||
bind_diag += d * d;
|
||||
}
|
||||
bind_diag = std::sqrt(bind_diag);
|
||||
// 6x, NOT the capture gates' 3x: this judges already-ACCEPTED palettes,
|
||||
// where the observed junk measures 183-405 while legit articulation on
|
||||
// small meshes (a 0.5 m accessory mid-stride) reached 3.1-3.3x and
|
||||
// tripped a 3x bound repeatedly (log 1285 mesh=436E7210, spread 1.6 vs
|
||||
// 1.56 - real world-space palette, needlessly healed/frozen).
|
||||
const float max_spread = std::max(6.0f * bind_diag, bind_diag + 2.0f);
|
||||
constexpr uint32_t kSamples = 32;
|
||||
float qmin[3] = {1e9f, 1e9f, 1e9f};
|
||||
float qmax[3] = {-1e9f, -1e9f, -1e9f};
|
||||
int n = 0;
|
||||
for (uint32_t s = 0; s < kSamples; ++s) {
|
||||
const uint32_t v =
|
||||
item.vb_addr + (s * (count - 1) / (kSamples - 1)) * item.stride;
|
||||
const uint32_t pa = v + item.pos_offset;
|
||||
float p[3];
|
||||
switch (item.pos_fmt) {
|
||||
case 57:
|
||||
for (int a = 0; a < 3; ++a) p[a] = LoadGuestF32(base, pa + a * 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 true;
|
||||
}
|
||||
if (!(p[0] > -1e7f && p[0] < 1e7f && p[1] > -1e7f && p[1] < 1e7f &&
|
||||
p[2] > -1e7f && p[2] < 1e7f)) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t bw = REX_LOAD_U32(v + item.bw_offset);
|
||||
const uint32_t bi = REX_LOAD_U32(v + item.bi_offset);
|
||||
uint32_t total = 0;
|
||||
float q[3] = {0.0f, 0.0f, 0.0f};
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
const uint32_t w = (bw >> (24 - 8 * k)) & 0xFF;
|
||||
if (w == 0) continue;
|
||||
const uint32_t r0 = 3u * ((bi >> (24 - 8 * k)) & 0xFF);
|
||||
if ((r0 + 3) * 4 > item.bones.size()) continue;
|
||||
total += w;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
const float* row = item.bones.data() + (r0 + uint32_t(a)) * 4;
|
||||
q[a] += float(w) * (row[0] * p[0] + row[1] * p[1] + row[2] * p[2] + row[3]);
|
||||
}
|
||||
}
|
||||
if (total == 0) continue;
|
||||
++n;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
q[a] /= float(total);
|
||||
qmin[a] = std::min(qmin[a], q[a]);
|
||||
qmax[a] = std::max(qmax[a], q[a]);
|
||||
}
|
||||
}
|
||||
if (n < 4) {
|
||||
return true; // 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 *out_spread <= max_spread;
|
||||
}
|
||||
|
||||
// Returns false when the bank could not be consumed for this item (ropa
|
||||
// rigid matrix implausible or off-clip = stale bank); the caller must
|
||||
// leave/mark the item pending so a later matching draw re-captures it.
|
||||
|
|
@ -2913,8 +3140,13 @@ bool CaptureSkinnedState(uint8_t* base, uint32_t bank, uint32_t palette_base,
|
|||
// interpretation), shadows matching because the caster pass shares the
|
||||
// item. Gate through the same sample-projection acceptance as every
|
||||
// other palette; on refusal the item stays pending (post-draw fixup /
|
||||
// ropa state rescue).
|
||||
palette_base = RefinePaletteBase(base, bank, main_pass ? 8u : 5u, item);
|
||||
// ropa state rescue). The game writes exactly 1.0 into the flag row's
|
||||
// x for sim-inactive (observed on every live capture, incl. the
|
||||
// (1,junk,junk,junk) NPC variant); a bit-exact 1.0 is the structural
|
||||
// proof that unlocks the relaxed near-camera acceptance; any other
|
||||
// positive x (a bone row of some other layout) keeps the strict gate.
|
||||
palette_base = RefinePaletteBase(base, bank, main_pass ? 8u : 5u, item,
|
||||
/*structural_guess=*/flag_x == 1.0f);
|
||||
if (palette_base == 0) {
|
||||
g_ropa_stale.fetch_add(1, std::memory_order_relaxed);
|
||||
return false;
|
||||
|
|
@ -2937,7 +3169,17 @@ bool CaptureSkinnedState(uint8_t* base, uint32_t bank, uint32_t palette_base,
|
|||
plausible = plausible && n > 0.0025f && n < 400.0f &&
|
||||
rows[r * 4 + 3] > -20000.f && rows[r * 4 + 3] < 20000.f;
|
||||
}
|
||||
if (plausible && ScoreRigidAffine(base, bank, m, item) >= 8) {
|
||||
bool front_all = false;
|
||||
const int rigid_score =
|
||||
plausible ? ScoreRigidAffine(base, bank, m, item, &front_all) : -1;
|
||||
// Relaxed near-camera acceptance mirrors the skinned branch: a
|
||||
// sim-active garment right at the camera clips most samples out of
|
||||
// the strict band, but a stale/foreign matrix throws them behind the
|
||||
// projection or far outside even the loose band.
|
||||
if (plausible && (rigid_score >= 8 || front_all)) {
|
||||
if (rigid_score < 8) {
|
||||
g_ropa_relaxed.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
// Column-vector [R | t] rows -> item.world (row-vector, t in row 3).
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
|
|
@ -5694,9 +5936,16 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
// State/geometry grafts require the same resolved mode: ropa
|
||||
// garments flip between rigid and skinned per capture, and mixing
|
||||
// one copy's palette with another's interpretation is the
|
||||
// mangled-ribbon bug.
|
||||
const bool graftable = cand.skinned == cur.skinned && !cand.ropa &&
|
||||
!cur.ropa && cand.mesh == cur.mesh;
|
||||
// mangled-ribbon bug. Same-mode ropa IS graftable; excluding ropa
|
||||
// wholesale meant a fuller caster-cascade list wholesale-won the
|
||||
// merge and the garment published the ortho bank's ~40 ms-stale
|
||||
// bone rows while the body meshes (graftable) got fresh palettes:
|
||||
// the garment rode ~10 cm off the moving body (speed x 40 ms), the
|
||||
// visible successor of the invisible-torso bug once the near-camera
|
||||
// acceptance landed. The mode equality check is what prevents the
|
||||
// ribbon, not the ropa flag.
|
||||
const bool graftable = cand.skinned == cur.skinned &&
|
||||
cand.ropa == cur.ropa && cand.mesh == cur.mesh;
|
||||
if (fresher && fuller) {
|
||||
cur = cand;
|
||||
} else if (fresher && graftable) {
|
||||
|
|
@ -5751,11 +6000,73 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
scene.items.push_back(std::move(item));
|
||||
}
|
||||
}
|
||||
// Ropa garments whose resolved mode FLIPPED this frame: state as published
|
||||
// LAST frame, stashed before the cache refresh overwrites it: the
|
||||
// flip-hold pass below (after the dyn-job enqueue) may need to re-publish
|
||||
// it while the new mode's decode is still in flight (see g_ropa_resident).
|
||||
std::unordered_map<uint32_t, RopaResolvedState> ropa_flip_prev;
|
||||
// Cross-frame palette rescue + cache refresh (see g_bones_cache): exactly
|
||||
// one published copy of a skinned mesh -> refresh its cache entry; zero
|
||||
// copies but a refused/pending capture -> re-publish with the cached
|
||||
// palette (one frame of pose lag instead of a one-frame disappearance).
|
||||
if (REXCVAR_GET(skate3_native_render_scene_dynamic_items)) {
|
||||
// Dense publish-time coherence gate (see PublishedPaletteSane): a
|
||||
// 1-frame junk palette that passed the 6-sample capture gates would
|
||||
// otherwise draw the map-length-ribbon flash AND poison the rescue
|
||||
// caches (this runs before the refresh below). Re-publish the cached
|
||||
// state when a same-mode one exists; else drop the draws; a 1-frame
|
||||
// blink beats the ribbon.
|
||||
for (DrawItem& item : scene.items) {
|
||||
if (!item.skinned || item.bones.empty() || item.pending) {
|
||||
continue;
|
||||
}
|
||||
float spread = 0.0f;
|
||||
if (PublishedPaletteSane(base, item, &spread)) {
|
||||
continue;
|
||||
}
|
||||
g_pub_incoherent.fetch_add(1, std::memory_order_relaxed);
|
||||
static std::atomic<uint32_t> incoh_logged{0};
|
||||
const uint32_t ln = incoh_logged.fetch_add(1, std::memory_order_relaxed);
|
||||
if (ln < 32 || (ln & 255u) == 0) {
|
||||
REXLOG_INFO(
|
||||
"native-scene: publish INCOHERENT palette mesh={:08X} fam={} "
|
||||
"ropa={} src={} spread={:.2f} bind=({:.2f},{:.2f},{:.2f}) "
|
||||
"bone0_t=({:.1f},{:.1f},{:.1f})",
|
||||
item.mesh, item.char_family, item.ropa ? 1 : 0, item.dbg_src,
|
||||
spread, item.bbox_max[0] - item.bbox_min[0],
|
||||
item.bbox_max[1] - item.bbox_min[1],
|
||||
item.bbox_max[2] - item.bbox_min[2], item.bones[3], item.bones[7],
|
||||
item.bones[11]);
|
||||
}
|
||||
bool healed = false;
|
||||
if (item.ropa) {
|
||||
const auto cit = g_ropa_state_cache.find(item.mesh);
|
||||
if (cit != g_ropa_state_cache.end() && cit->second.skinned &&
|
||||
cit->second.bones.size() == item.bones.size()) {
|
||||
item.bones = cit->second.bones;
|
||||
item.dbg_src = 6;
|
||||
healed = true;
|
||||
}
|
||||
} else {
|
||||
const auto bit = g_bones_cache.find(item.mesh);
|
||||
if (bit != g_bones_cache.end() &&
|
||||
bit->second.size() == item.bones.size()) {
|
||||
item.bones = bit->second;
|
||||
item.dbg_src = 6;
|
||||
healed = true;
|
||||
}
|
||||
}
|
||||
if (!healed) {
|
||||
item.draws.clear();
|
||||
// CRITICAL: also drop the junk palette. Leaving it on the item let
|
||||
// the cache-refresh loop below store it into the rescue caches;
|
||||
// the NEXT frame's trip then "healed" with the poisoned entry and
|
||||
// published the ribbon with draws intact (log 1284/1285: the
|
||||
// fam=6/fam=1 spread 183-405 bind-local palettes, bone0_t=(0,0,-.5),
|
||||
// were re-captured every frame by the src=2 fixup).
|
||||
item.bones.clear();
|
||||
}
|
||||
}
|
||||
std::unordered_map<uint32_t, uint32_t> pub_count;
|
||||
for (const DrawItem& item : scene.items) {
|
||||
// Ropa garments count in EITHER resolved mode (a rigid-resolved copy
|
||||
|
|
@ -5764,12 +6075,21 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
if (item.ropa || (item.skinned && !item.bones.empty())) {
|
||||
++pub_count[item.mesh];
|
||||
}
|
||||
if (item.ropa && item.caster_bank) {
|
||||
g_ropa_caster.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
for (const DrawItem& item : scene.items) {
|
||||
if (!(item.ropa || (item.skinned && !item.bones.empty())) ||
|
||||
pub_count[item.mesh] != 1) {
|
||||
continue;
|
||||
}
|
||||
if (item.draws.empty()) {
|
||||
// Vetoed/dropped this frame (incoherent-palette trip, flip-hold
|
||||
// clone drop): whatever state it carries must not become the
|
||||
// "last good" rescue entry.
|
||||
continue;
|
||||
}
|
||||
if (item.ropa) {
|
||||
// Remember the resolved mode + transform (see g_ropa_state_cache).
|
||||
if (g_ropa_state_cache.size() < 512) {
|
||||
|
|
@ -5778,6 +6098,7 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
RopaResolvedState& c = cit->second;
|
||||
if (!fresh && c.skinned != now_skinned) {
|
||||
g_ropa_flip.fetch_add(1, std::memory_order_relaxed);
|
||||
ropa_flip_prev.emplace(item.mesh, c); // pre-flip published state
|
||||
}
|
||||
c.skinned = now_skinned;
|
||||
std::memcpy(c.world, item.world, sizeof(c.world));
|
||||
|
|
@ -6330,10 +6651,17 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
// tolerates the fingerprint mismatch for dynamic payloads).
|
||||
continue;
|
||||
}
|
||||
// The decode is MODE-dependent for ropa (rigid decodes zero the blend
|
||||
// weight/index attributes, see DecodeMesh), so a mode flip must
|
||||
// re-enqueue even when the payload bytes did not change: fold the
|
||||
// resolved mode into the dedup key.
|
||||
const uint64_t fp_key =
|
||||
item.fingerprint ^
|
||||
((item.ropa && item.skinned && !item.bones.empty()) ? 1u : 0u);
|
||||
const auto prev = s_dyn_fp_sent.find(item.mesh);
|
||||
const bool first_sight = prev == s_dyn_fp_sent.end();
|
||||
const uint64_t prev_fp = first_sight ? 0 : prev->second;
|
||||
if (!first_sight && prev_fp == item.fingerprint) {
|
||||
if (!first_sight && prev_fp == fp_key) {
|
||||
continue;
|
||||
}
|
||||
DynDecodeJob job;
|
||||
|
|
@ -6373,6 +6701,173 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
}
|
||||
}
|
||||
|
||||
// Ropa mode-flip HOLD (must run AFTER the dyn-job enqueue above so the
|
||||
// new mode's decode is already in flight with the fresh capture): the
|
||||
// GPU-resident decode still pairs with the OTHER mode; drawing the new
|
||||
// mode over it is the 1-2 frame ribbon (skinned palette over sim-deformed
|
||||
// verts) or a collapsed garment (rigid decodes zero the blend attributes;
|
||||
// skinning them lands every vert at the origin). Re-publish the pre-flip
|
||||
// state until the flipped decode commits and g_ropa_resident catches up;
|
||||
// with no pre-flip state to hold (clones: the cache is single-instance)
|
||||
// drop the garment for the flip frames: a blink beats the ribbon.
|
||||
if (REXCVAR_GET(skate3_native_render_scene_dynamic_items)) {
|
||||
for (DrawItem& item : scene.items) {
|
||||
if (!item.ropa || item.pending || item.dbg_src == 4 ||
|
||||
item.fingerprint == 0 || item.draws.empty()) {
|
||||
continue;
|
||||
}
|
||||
const bool mode_skinned = item.skinned && !item.bones.empty();
|
||||
RopaResidentDecode res;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_ropa_resident_mutex);
|
||||
const auto rit = g_ropa_resident.find(item.mesh);
|
||||
if (rit == g_ropa_resident.end()) {
|
||||
continue; // nothing resident yet: the draw defers until commit
|
||||
}
|
||||
res = rit->second;
|
||||
}
|
||||
if (res.skinned == mode_skinned) {
|
||||
continue; // resident decode pairs with the published mode
|
||||
}
|
||||
const auto pit = ropa_flip_prev.find(item.mesh);
|
||||
if (pit != ropa_flip_prev.end() && pit->second.skinned == res.skinned) {
|
||||
item.skinned = pit->second.skinned;
|
||||
item.bones = pit->second.bones;
|
||||
std::memcpy(item.world, pit->second.world, sizeof(item.world));
|
||||
item.dbg_src = 5;
|
||||
// Keep the cache on the HELD state so a multi-frame hold (decode
|
||||
// still in flight next frame) re-detects the flip and re-stashes.
|
||||
if (g_ropa_state_cache.size() < 512) {
|
||||
g_ropa_state_cache[item.mesh] = pit->second;
|
||||
}
|
||||
} else {
|
||||
item.draws.clear();
|
||||
}
|
||||
g_ropa_hold.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw-time STRETCH VETO: the last line of defense, judging what the GPU
|
||||
// will ACTUALLY draw: the RESIDENT decode's sample verts (g_skin_probe,
|
||||
// cached by DecodeMesh) skinned with the FINAL palette (after the merge,
|
||||
// rescues, interpolation and flip-hold above). Every upstream gate judges
|
||||
// the LIVE guest VB, so a decode-content/palette pairing mismatch, or
|
||||
// junk introduced by an interpolation substitution, passes all of them
|
||||
// and still flashes the 1-frame map-length ribbon (every ropa[] counter
|
||||
// stayed clean in testing). Trip: clear the item's draws (a blink,
|
||||
// and the caster pass shares the item so no ribbon shadow either), log,
|
||||
// and dump palette + samples for offline diagnosis.
|
||||
if (REXCVAR_GET(skate3_native_render_scene_stretch_guard)) {
|
||||
for (DrawItem& item : scene.items) {
|
||||
if (!item.skinned || item.bones.size() < 12 || item.pending ||
|
||||
item.draws.empty()) {
|
||||
continue;
|
||||
}
|
||||
SkinProbe probe;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_skin_probe_mutex);
|
||||
const auto pit = g_skin_probe.find(item.mesh);
|
||||
if (pit == g_skin_probe.end()) {
|
||||
continue; // not decoded yet: nothing will be drawn either
|
||||
}
|
||||
probe = pit->second;
|
||||
}
|
||||
float bind_diag = 0.0f;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
const float d = item.bbox_max[a] - item.bbox_min[a];
|
||||
bind_diag += d * d;
|
||||
}
|
||||
bind_diag = std::sqrt(bind_diag);
|
||||
// 6x like PublishedPaletteSane: judging accepted palettes, where junk
|
||||
// measures 100-400x and legit small-mesh articulation reaches ~3.3x.
|
||||
const float max_spread = std::max(6.0f * bind_diag, bind_diag + 2.0f);
|
||||
float qmin[3] = {1e9f, 1e9f, 1e9f};
|
||||
float qmax[3] = {-1e9f, -1e9f, -1e9f};
|
||||
int n = 0;
|
||||
for (const SkinProbeSample& ps : probe.s) {
|
||||
uint32_t total = 0;
|
||||
float q[3] = {0.0f, 0.0f, 0.0f};
|
||||
// Decoded-buffer convention: component k = byte k (see DecodeMesh's
|
||||
// SwapU32 + per-byte extraction).
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
const uint32_t w = (ps.bw >> (8 * k)) & 0xFF;
|
||||
if (w == 0) continue;
|
||||
const uint32_t r0 = 3u * ((ps.bi >> (8 * k)) & 0xFF);
|
||||
if ((r0 + 3) * 4 > item.bones.size()) continue;
|
||||
total += w;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
const float* row = item.bones.data() + (r0 + uint32_t(a)) * 4;
|
||||
q[a] += float(w) *
|
||||
(row[0] * ps.p[0] + row[1] * ps.p[1] + row[2] * ps.p[2] +
|
||||
row[3]);
|
||||
}
|
||||
}
|
||||
if (total == 0) continue;
|
||||
++n;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
q[a] /= float(total);
|
||||
qmin[a] = std::min(qmin[a], q[a]);
|
||||
qmax[a] = std::max(qmax[a], q[a]);
|
||||
}
|
||||
}
|
||||
if (n < 4) {
|
||||
continue;
|
||||
}
|
||||
const float dx = qmax[0] - qmin[0];
|
||||
const float dy = qmax[1] - qmin[1];
|
||||
const float dz = qmax[2] - qmin[2];
|
||||
const float spread = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (spread <= max_spread) {
|
||||
continue;
|
||||
}
|
||||
g_stretch_veto.fetch_add(1, std::memory_order_relaxed);
|
||||
static std::atomic<uint32_t> s_stretch_logged{0};
|
||||
const uint32_t ln =
|
||||
s_stretch_logged.fetch_add(1, std::memory_order_relaxed);
|
||||
if (ln < 24 || (ln & 255u) == 0) {
|
||||
REXLOG_INFO(
|
||||
"native-scene: STRETCH veto mesh={:08X} fam={} ropa={} src={} "
|
||||
"caster={} spread={:.1f} bind={:.2f} n={} fp_match={} "
|
||||
"bone0_t=({:.1f},{:.1f},{:.1f})",
|
||||
item.mesh, item.char_family, item.ropa ? 1 : 0, item.dbg_src,
|
||||
item.caster_bank ? 1 : 0, spread, bind_diag, n,
|
||||
probe.fp == item.fingerprint ? 1 : 0, item.bones[3],
|
||||
item.bones[7], item.bones[11]);
|
||||
}
|
||||
if (ln < 6) {
|
||||
// Full palette + probe dump for offline diagnosis (which rows are
|
||||
// junk, which bones the stretched samples weight to).
|
||||
char path[128];
|
||||
std::snprintf(path, sizeof(path), "logs/stretch_%u_%08X.txt", ln,
|
||||
item.mesh);
|
||||
if (FILE* f = std::fopen(path, "wb")) {
|
||||
std::fprintf(f,
|
||||
"mesh=%08X fam=%u ropa=%d src=%u caster=%d spread=%f "
|
||||
"bind=%f fp=%016llX probe_fp=%016llX vb=%08X\n",
|
||||
item.mesh, item.char_family, item.ropa ? 1 : 0,
|
||||
item.dbg_src, item.caster_bank ? 1 : 0, spread,
|
||||
bind_diag, (unsigned long long)item.fingerprint,
|
||||
(unsigned long long)probe.fp, item.vb_addr);
|
||||
const uint32_t bones = uint32_t(item.bones.size() / 12);
|
||||
for (uint32_t b = 0; b < bones; ++b) {
|
||||
const float* r = item.bones.data() + b * 12;
|
||||
std::fprintf(f,
|
||||
"bone %3u | %9.4f %9.4f %9.4f %10.3f | %9.4f %9.4f "
|
||||
"%9.4f %10.3f | %9.4f %9.4f %9.4f %10.3f\n",
|
||||
b, r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7],
|
||||
r[8], r[9], r[10], r[11]);
|
||||
}
|
||||
for (const SkinProbeSample& ps : probe.s) {
|
||||
std::fprintf(f, "vert p=(%f,%f,%f) bw=%08X bi=%08X\n", ps.p[0],
|
||||
ps.p[1], ps.p[2], ps.bw, ps.bi);
|
||||
}
|
||||
std::fclose(f);
|
||||
}
|
||||
}
|
||||
item.draws.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (g_recording.load(std::memory_order_relaxed)) {
|
||||
std::lock_guard<std::mutex> lock(g_record_mutex);
|
||||
if (++g_frames_seen % g_record_stride == 0) {
|
||||
|
|
@ -8369,6 +8864,30 @@ bool DecodeMesh(ID3D12Device* device, uint8_t* base, const DrawItem& item,
|
|||
dst[v * 14 + 10] = n3[1];
|
||||
dst[v * 14 + 11] = n3[2];
|
||||
}
|
||||
// Stretch-veto probe (see g_skin_probe): cache ~32 decoded sample verts
|
||||
// so the guest thread can cheaply skin what the GPU will actually draw.
|
||||
if (item.skinned) {
|
||||
constexpr uint32_t kProbe = 32;
|
||||
const uint32_t pn = std::min(kProbe, num_verts);
|
||||
SkinProbe probe;
|
||||
probe.fp = item.fingerprint;
|
||||
probe.s.reserve(pn);
|
||||
for (uint32_t s = 0; s < pn; ++s) {
|
||||
const uint32_t v = pn > 1 ? (s * (num_verts - 1) / (pn - 1)) : 0;
|
||||
SkinProbeSample ps;
|
||||
ps.p[0] = dst[v * 14 + 0];
|
||||
ps.p[1] = dst[v * 14 + 1];
|
||||
ps.p[2] = dst[v * 14 + 2];
|
||||
std::memcpy(&ps.bw, &dst[v * 14 + 7], 4);
|
||||
std::memcpy(&ps.bi, &dst[v * 14 + 8], 4);
|
||||
probe.s.push_back(ps);
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_skin_probe_mutex);
|
||||
if (g_skin_probe.size() > 4096) {
|
||||
g_skin_probe.clear();
|
||||
}
|
||||
g_skin_probe[item.mesh] = std::move(probe);
|
||||
}
|
||||
vb->Unmap(0, nullptr);
|
||||
// Blend indices outside the captured palette read garbage rows and mangle
|
||||
// the vertex. Indices are plain bone numbers, bone k = palette rows 3k.
|
||||
|
|
@ -11113,9 +11632,25 @@ void PrewarmCommit(const NativeGuestOutputRenderContext& context,
|
|||
for (PrewarmResult& r : done) {
|
||||
if (r.mesh_valid) {
|
||||
auto mit = g_r.meshes.find(r.item.mesh);
|
||||
const bool superseded =
|
||||
mit != g_r.meshes.end() && mit->second.dyn_seq > r.buffers.dyn_seq;
|
||||
if (r.item.ropa && r.buffers.dyn_seq != 0 && !superseded) {
|
||||
// Record the payload identity + mode this ropa decode pairs with:
|
||||
// the guest thread's flip-hold pass keeps publishing the previous
|
||||
// resolved state until the resident decode matches the published
|
||||
// mode (see g_ropa_resident). Also correct on the identical-
|
||||
// fingerprint drop below: the cached content equals this job's, so
|
||||
// the pairing is this job's mode either way.
|
||||
std::lock_guard<std::mutex> lock(g_ropa_resident_mutex);
|
||||
if (g_ropa_resident.size() > 512) {
|
||||
g_ropa_resident.clear();
|
||||
}
|
||||
RopaResidentDecode& res = g_ropa_resident[r.item.mesh];
|
||||
res.fp = r.buffers.fingerprint;
|
||||
res.skinned = r.item.skinned;
|
||||
}
|
||||
if (mit != g_r.meshes.end() &&
|
||||
(mit->second.fingerprint == r.buffers.fingerprint ||
|
||||
mit->second.dyn_seq > r.buffers.dyn_seq)) {
|
||||
(mit->second.fingerprint == r.buffers.fingerprint || superseded)) {
|
||||
// Identical content already cached (lost the race against the draw
|
||||
// path / an earlier result), or a NEWER dynamic decode already
|
||||
// landed (multi-worker reordering must not step the cloth
|
||||
|
|
@ -13104,7 +13639,7 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
"native-scene: frame {} items={} draws={} draws_2d={} drawn_2d={} "
|
||||
"splines[{}/{}] "
|
||||
"2d[other={} dropped={} textures={}] cached_meshes={} textures={} "
|
||||
"vs_uploads={} palettes={} palette_base_plus1={} ropa[rigid={} stale={} rescued={} flip={} mismatch={}] dyn_gap={} skinned={} skinned_skipped={} foreign_bank={} "
|
||||
"vs_uploads={} palettes={} palette_base_plus1={} ropa[rigid={} stale={} rescued={} flip={} mismatch={} relax={} hold={} caster={} incoh={} stretch={}] dyn_gap={} skinned={} skinned_skipped={} foreign_bank={} "
|
||||
"rigid[pending={} dropped={} worldprops={}] "
|
||||
"rej[dyn={} range={} chain={} geom={} draws={} bbox={}] "
|
||||
"rr[decode_fail={} no_bones={} mesh_deferred={} tex_deferred={}] "
|
||||
|
|
@ -13116,7 +13651,9 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
g_r.meshes.size(), g_r.textures.size(),
|
||||
g_vs_uploads.load(), g_palette_snapshots.load(), g_palette_base_plus1.load(),
|
||||
g_ropa_rigid.load(), g_ropa_stale.load(), g_ropa_rescued.load(),
|
||||
g_ropa_flip.load(), g_ropa_mismatch.load(), g_dyn_gap.load(),
|
||||
g_ropa_flip.load(), g_ropa_mismatch.load(), g_ropa_relaxed.load(),
|
||||
g_ropa_hold.load(), g_ropa_caster.load(), g_pub_incoherent.load(),
|
||||
g_stretch_veto.load(), g_dyn_gap.load(),
|
||||
g_skinned_items.load(),
|
||||
g_skinned_skipped.load(), g_capture_foreign_bank.load(),
|
||||
g_rigid_pending.load(), g_rigid_dropped.load(),
|
||||
|
|
|
|||
|
|
@ -171,7 +171,10 @@ struct DrawItem {
|
|||
bool pending;
|
||||
// Diagnosis: which path staged this item's palette/world. 0 = none/raw,
|
||||
// 1 = submit-exit capture, 2 = post-draw (ib,vb) fixup, 3 = bones-cache
|
||||
// rescue, 4 = ropa state rescue.
|
||||
// rescue, 4 = ropa state rescue, 5 = ropa mode-flip hold (previous state
|
||||
// re-published while the flipped mode's decode is still in flight),
|
||||
// 6 = publish-time incoherent-palette heal (cached palette substituted
|
||||
// after the dense 32-sample spread gate failed).
|
||||
uint8_t dbg_src = 0;
|
||||
// The palette was captured from an ORTHOGRAPHIC (CSM caster-cascade)
|
||||
// bank. The game does not reliably re-evaluate fine bone animation for
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue