render: retire inert capture-inference guards superseded by the identity store
This commit is contained in:
parent
bfb3c07839
commit
ae9720bf1a
1 changed files with 72 additions and 580 deletions
|
|
@ -405,15 +405,6 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_shadows, true, "Skate 3",
|
|||
"Cascade matrices are captured per frame from the game's own "
|
||||
"material constants.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_palette_alt_guard, true, "Skate 3",
|
||||
"Publish-side bone-palette alternation guard: a character piece "
|
||||
"whose served palette returns bit-exact to the previous-but-one "
|
||||
"frame's palette (A,B,A,B: the capture fixup alternating between "
|
||||
"two banks) keeps last frame's palette instead. Damps the "
|
||||
"high-fps hair/garment deformation flicker; real motion is never "
|
||||
"affected (a genuine pose never round-trips in two rendered "
|
||||
"frames).")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_caster_refresh_all, true, "Skate 3",
|
||||
"Perspective-bank palette refresh covers ALL caster-sourced "
|
||||
"skinned captures: skinned-published ROPA garments join the "
|
||||
|
|
@ -1151,14 +1142,9 @@ std::unordered_map<uint32_t, std::array<float, 60>> g_char_rows_cache;
|
|||
float g_char_shadow_bias[3] = {};
|
||||
uint64_t g_char_shadow_bias_frame = 0;
|
||||
std::atomic<uint64_t> g_char_rows_reused{0};
|
||||
// mesh -> last published bone palette (skinned single-instance meshes).
|
||||
// Rescue for frames whose palette capture was REFUSED by the acceptance
|
||||
// gates (RefinePaletteBase returning 0 on a stale/ambiguous bank): garments
|
||||
// that draw only once per frame (the trucker cap) have no later draw for
|
||||
// the fixup to pop on, so a refused capture used to mean a one-frame
|
||||
// disappearance (the "momentary blip" while rotating the camera). One frame
|
||||
// of pose lag is invisible; a missing hat is not. Single-instance only;
|
||||
// clones share meshes with per-instance poses. Guest-render-thread only.
|
||||
// Cached palette entry for the per-instance rescue below. A refused
|
||||
// capture re-publishes with the cached palette: one frame of pose lag is
|
||||
// invisible; a one-frame disappearance is not. Guest-render-thread only.
|
||||
struct CachedBones {
|
||||
std::vector<float> bones;
|
||||
// g_guest_frame at refresh: rescues/heals must be FRESH; a close-pass
|
||||
|
|
@ -1166,15 +1152,10 @@ struct CachedBones {
|
|||
// BEHIND its live position (the momentary ghost-back).
|
||||
uint64_t frame = 0;
|
||||
};
|
||||
std::unordered_map<uint32_t, CachedBones> g_bones_cache;
|
||||
std::atomic<uint64_t> g_bones_rescued{0};
|
||||
// ctx -> last published palette for skinned character-family items (the
|
||||
// per-INSTANCE sibling of g_bones_cache): clones share meshes, so the
|
||||
// mesh-keyed rescue is gated to pub_count==1 and a refused clone capture
|
||||
// next to a published twin got NOTHING: a one-frame invisibility blink.
|
||||
// The MeshContext is the game's own per-instance identity,
|
||||
// so this cache rescues each instance
|
||||
// with ITS OWN last palette regardless of how many clones are alive.
|
||||
// ctx -> last published palette for skinned character-family items. The
|
||||
// MeshContext is the game's own per-instance identity, so this cache
|
||||
// rescues each instance with ITS OWN last palette regardless of how many
|
||||
// clones are alive.
|
||||
std::unordered_map<uint32_t, CachedBones> g_bones_cache_ctx;
|
||||
std::atomic<uint64_t> g_lw_ctx_rescued{0};
|
||||
// LW entity store consumption counters (stats line lw[...]).
|
||||
|
|
@ -1228,12 +1209,6 @@ struct CharRowsEnt {
|
|||
std::unordered_map<uint32_t, CharRowsEnt> g_char_rows_cache_ent;
|
||||
std::atomic<uint64_t> g_char_rows_ent_served{0};
|
||||
std::atomic<uint64_t> g_char_rows_inst_served{0};
|
||||
// Perspective refreshes committed onto skinned-published ROPA pieces (the
|
||||
// caster_refresh_all fix; see the fixup's candidate selection).
|
||||
std::atomic<uint64_t> g_ropa_caster_refreshed{0};
|
||||
// Clone-group pending pairings redirected by palette identity (each count is
|
||||
// one prevented twin-palette swap; see the fixup's identity ranking).
|
||||
std::atomic<uint64_t> g_pending_pair_fixed{0};
|
||||
// Items served authoritative pack palettes this frame / total (see
|
||||
// native_palette::ServeAuthoritativePalettes).
|
||||
uint32_t g_pal_served_frame = 0;
|
||||
|
|
@ -1242,16 +1217,11 @@ std::atomic<uint64_t> g_pal_served_total{0};
|
|||
// (blended sub-pass vs legacy opaque) changed between consecutive frames;
|
||||
// each count is one visible flicker event.
|
||||
std::atomic<uint64_t> g_hair_route_flips{0};
|
||||
// A,B,A,B alternation probes on the same pieces: served lighting rows,
|
||||
// bone palettes and strand-coverage texture content returning EXACTLY to
|
||||
// the previous-but-one frame's value (legitimate animation/lighting drift
|
||||
// never does).
|
||||
std::atomic<uint64_t> g_hair_rows_alternations{0};
|
||||
// A,B,A,B alternation probes: bone palettes (per piece) and the shadow
|
||||
// cascade state (scene-wide) returning EXACTLY to the previous-but-one
|
||||
// frame's value (legitimate animation/lighting drift never does).
|
||||
std::atomic<uint64_t> g_hair_bone_alternations{0};
|
||||
std::atomic<uint64_t> g_hair_cov_alternations{0};
|
||||
std::atomic<uint64_t> g_shadow_alternations{0};
|
||||
// Palette A,B,A,B serves repaired by the publish-side alternation guard.
|
||||
std::atomic<uint64_t> g_palette_alt_repaired{0};
|
||||
// ctx -> last PUBLISHED item of a live LW entity (skinned character
|
||||
// families, non-ropa): when a ctx drops out of the submit records for a
|
||||
// frame or two while its entity is still alive in the store (observed as
|
||||
|
|
@ -1268,7 +1238,7 @@ struct LwRetained {
|
|||
};
|
||||
std::unordered_map<uint32_t, LwRetained> g_lw_last_items;
|
||||
// mesh -> last RESOLVED ropa garment state (rigid world OR skinned palette).
|
||||
// Ropa items must NOT ride the g_bones_cache rescue above: a ropa capture is
|
||||
// Ropa items must NOT ride the palette-only rescue above: a ropa capture is
|
||||
// refused exactly when the bank is stale (g_ropa_stale), and while the cloth
|
||||
// sim is ACTIVE the VB holds sim-deformed root-local positions; skinning
|
||||
// those with a cached palette is the mangled-ribbon interpretation (verified
|
||||
|
|
@ -1277,9 +1247,9 @@ std::unordered_map<uint32_t, LwRetained> g_lw_last_items;
|
|||
// a refused frame drops the garment (the momentary invisible torso). The
|
||||
// dyn decode workers already keep the drawn cloth VB one frame behind the
|
||||
// sim, so re-publishing last frame's resolved MODE + transform is exactly
|
||||
// age-consistent with the vertices being drawn. Single-instance meshes only,
|
||||
// like g_bones_cache; clones share meshes with per-instance transforms.
|
||||
// Guest-render-thread only.
|
||||
// age-consistent with the vertices being drawn. Single-instance meshes only
|
||||
// - clones share meshes with per-instance transforms. Guest-render-thread
|
||||
// only.
|
||||
struct RopaResolvedState {
|
||||
bool skinned = false;
|
||||
float world[16] = {};
|
||||
|
|
@ -1293,27 +1263,6 @@ 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
|
||||
|
|
@ -1569,15 +1518,6 @@ std::atomic<uint64_t> g_ropa_rigid{0};
|
|||
// or projected the garment off-clip (stale bank from another mesh's draw);
|
||||
// the item stays pending for the post-draw fixup.
|
||||
std::atomic<uint64_t> g_ropa_stale{0};
|
||||
// ropa garments whose resolved mode CHANGED between frames (skinned <->
|
||||
// rigid: the cloth sim toggling with distance/activity). The flip frames
|
||||
// are where mode/payload races live; see the dyn-job coherence guard.
|
||||
std::atomic<uint64_t> g_ropa_flip{0};
|
||||
// SKINNED-mode ropa payload snapshots whose frame-end VB no longer skins at
|
||||
// bind size (the cloth sim rewrote it with sim-deformed vertices after the
|
||||
// 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
|
||||
|
|
@ -1595,15 +1535,6 @@ std::unordered_map<uint32_t, uint64_t> g_ropa_last_seq;
|
|||
// stats window.
|
||||
std::atomic<uint64_t> g_ropa_blend_drawn{0};
|
||||
std::atomic<uint64_t> g_ropa_blend_miss{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};
|
||||
// Published ropa garments vetoed by the detached-garment gate (transform
|
||||
// more than ~2 m from every character bone in the frame: a wrong joint's
|
||||
// or wrong instance's transform that the spread gates cannot reject; the
|
||||
// veto log names the producing path via dbg_src).
|
||||
std::atomic<uint64_t> g_ropa_detached{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
|
||||
|
|
@ -4280,66 +4211,15 @@ int ScoreRigidAffine(uint8_t* base, uint32_t bank, uint32_t m, const DrawItem& i
|
|||
return n == 0 ? -1 : (ok * 16) / n;
|
||||
}
|
||||
|
||||
// Frame-end coherence check for SKINNED-mode ropa payloads (dyn decode
|
||||
// jobs). The item's mode and palette were captured at draw time, but the
|
||||
// cloth VB is snapshotted at BuildFrameScene, and the game's cloth sim
|
||||
// runs concurrently: when it ACTIVATES between the draws and the snapshot
|
||||
// (skating NPCs toggle with distance, "when he got near"), the copied
|
||||
// buffer holds sim-deformed root-local vertices, i.e. the ropa VS's OTHER
|
||||
// branch's input. Skinning those with the palette is the map-length-ribbon
|
||||
// interpretation (the offline-validated 0/31). Skin the acceptance gate's
|
||||
// sample vertices from the JUST-COPIED payload and require the same
|
||||
// bind-size spread the capture gate does; on failure the caller keeps last
|
||||
// frame's decode (coherent) and retries next frame, when the capture will
|
||||
// have flipped the mode to match the payload.
|
||||
bool RopaPayloadCoherent(const DrawItem& item, const std::vector<uint8_t>& vb) {
|
||||
if (item.stride == 0 || item.bones.size() < 12 || item.bw_offset == 0 ||
|
||||
item.bi_offset == 0) {
|
||||
return true;
|
||||
}
|
||||
const float bind_diag = BindDiag(item);
|
||||
const float max_spread = std::max(3.0f * bind_diag, bind_diag + 1.0f);
|
||||
constexpr uint32_t kSamples = 6;
|
||||
SkinSampleVert sverts[kSamples];
|
||||
const int got = ReadSkinSamplesRaw(vb.data(), vb.size(), item, kSamples, sverts);
|
||||
if (got < 0) {
|
||||
return true; // unsupported position format: nothing to judge
|
||||
}
|
||||
if (got != int(kSamples)) {
|
||||
// Short copy: garbage in the decoded prefix still fails (the original
|
||||
// checked each sample before its successor's bounds check); otherwise
|
||||
// there is nothing to judge.
|
||||
for (int s = 0; s < got; ++s) {
|
||||
if (!sverts[s].pos_finite) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
float spread = 0.0f;
|
||||
const int r = SkinnedSpreadHostRows(sverts, kSamples, item.bones.data(),
|
||||
item.bones.size(), /*min_n=*/2,
|
||||
/*garbage_fails=*/true, &spread);
|
||||
if (r < 0) {
|
||||
return false; // NaN/garbage positions mid-sim-write
|
||||
}
|
||||
if (r == 0) {
|
||||
return true; // nothing to judge
|
||||
}
|
||||
return spread <= 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.
|
||||
// ribbon flash. 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;
|
||||
|
|
@ -5792,67 +5672,15 @@ void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t
|
|||
// judder). The refresh entry is only retired by a perspective-bank
|
||||
// (z/main-pass) capture.
|
||||
auto oldest = range.second;
|
||||
uint32_t pending_n = 0;
|
||||
for (auto it = range.first; it != range.second; ++it) {
|
||||
if (it->second >= g_frame_dynitems.size() ||
|
||||
!g_frame_dynitems[it->second].pending) {
|
||||
continue;
|
||||
}
|
||||
++pending_n;
|
||||
if (oldest == range.second || it->second < oldest->second) {
|
||||
oldest = it;
|
||||
}
|
||||
}
|
||||
if (pending_n > 1 &&
|
||||
REXCVAR_GET(skate3_native_render_scene_caster_refresh_all)) {
|
||||
// Clone-group IDENTITY pairing. FIFO assumes registration order ==
|
||||
// deferred-draw order; a clone culled from one pass (or a permuted
|
||||
// deferred list) shifts the pairing by one, so each clone captures its
|
||||
// TWIN's palette, and frame parity flips it back: the bit-exact
|
||||
// A,B,A,B bones alternation on clone meshes (fam-5 NPC hair: another
|
||||
// NPC's head joint = the hair blink; all alternating
|
||||
// pieces were multi-ctx clone meshes, src=2 perspective-sourced). Rank
|
||||
// the pending clones by their OWN last published palette (the ctx
|
||||
// cache) against this bank's bone-0 translation; nearest within 1.5 m
|
||||
// wins. First-sight clones (no fresh cache) keep FIFO; their own later
|
||||
// draw still resolves them, and a mispair cannot bit-repeat.
|
||||
const uint32_t pb_sel = BankPaletteBase(base, bank);
|
||||
if (pb_sel != 0) {
|
||||
const float nt[3] = {
|
||||
LoadGuestF32(base, bank + (pb_sel * 4 + 3) * 4),
|
||||
LoadGuestF32(base, bank + ((pb_sel + 1) * 4 + 3) * 4),
|
||||
LoadGuestF32(base, bank + ((pb_sel + 2) * 4 + 3) * 4)};
|
||||
float best_d2 = 2.25f;
|
||||
auto best = range.second;
|
||||
for (auto it = range.first; it != range.second; ++it) {
|
||||
if (it->second >= g_frame_dynitems.size() ||
|
||||
!g_frame_dynitems[it->second].pending) {
|
||||
continue;
|
||||
}
|
||||
const DrawItem& c = g_frame_dynitems[it->second];
|
||||
if (c.ctx == 0 || c.char_family == 0) {
|
||||
continue;
|
||||
}
|
||||
const auto cit = g_bones_cache_ctx.find(c.ctx);
|
||||
if (cit == g_bones_cache_ctx.end() || cit->second.bones.size() < 12 ||
|
||||
g_guest_frame - cit->second.frame > 10) {
|
||||
continue;
|
||||
}
|
||||
const float dx = cit->second.bones[3] - nt[0];
|
||||
const float dy = cit->second.bones[7] - nt[1];
|
||||
const float dz = cit->second.bones[11] - nt[2];
|
||||
const float d2 = dx * dx + dy * dy + dz * dz;
|
||||
if (d2 < best_d2) {
|
||||
best_d2 = d2;
|
||||
best = it;
|
||||
}
|
||||
}
|
||||
if (best != range.second && best != oldest) {
|
||||
g_pending_pair_fixed.fetch_add(1, std::memory_order_relaxed);
|
||||
oldest = best;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool caster_refresh = false;
|
||||
if (oldest == range.second) {
|
||||
if (REXCVAR_GET(skate3_native_render_scene_caster_refresh_all)) {
|
||||
|
|
@ -5942,20 +5770,7 @@ void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t
|
|||
probe.caster_bank = BankIsOrtho(base, bank);
|
||||
probe.pending = false;
|
||||
probe.dbg_src = 2;
|
||||
const bool was_ropa_refresh = d.ropa && !probe.caster_bank;
|
||||
d = std::move(probe);
|
||||
if (was_ropa_refresh) {
|
||||
// Live verification for the ropa-inclusion fix (tee/hair riding stale
|
||||
// caster banks): counts perspective refreshes landing on ropa pieces.
|
||||
const uint64_t n =
|
||||
g_ropa_caster_refreshed.fetch_add(1, std::memory_order_relaxed);
|
||||
if (n < 4 || (n & 8191u) == 0) {
|
||||
REXLOG_INFO(
|
||||
"native-scene: ropa caster->perspective palette refresh "
|
||||
"mesh={:08X} fam={} (n={})",
|
||||
d.mesh, d.char_family, n);
|
||||
}
|
||||
}
|
||||
if (d.char_family != 0 && g_frame_char_refresh.size() < 256) {
|
||||
g_frame_char_refresh.emplace(key, oldest->second);
|
||||
}
|
||||
|
|
@ -8109,16 +7924,13 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
// each submission carries that pass's culled island list. Keep the fullest
|
||||
// one; a shadow-pass list can be missing body parts the main view needs.
|
||||
std::unordered_map<uint32_t, size_t> dyn_slot;
|
||||
// First refused/pending skinned capture per mesh: candidates for the
|
||||
// post-merge cross-frame palette rescue (see g_bones_cache). Ropa garments
|
||||
// are kept apart: their rescue must restore last frame's resolved MODE
|
||||
// First refused/pending ropa capture per mesh, candidates for the
|
||||
// post-merge rescue, which must restore last frame's resolved MODE
|
||||
// (rigid vs skinned), never blindly re-skin (see g_ropa_state_cache).
|
||||
std::unordered_map<uint32_t, const DrawItem*> pending_skinned_by_mesh;
|
||||
std::unordered_map<uint32_t, const DrawItem*> pending_ropa_by_mesh;
|
||||
// Per-INSTANCE candidates (ctx-keyed, character families): the mesh-keyed
|
||||
// rescue above is pub_count==1-gated, so a refused CLONE capture next to
|
||||
// a published twin dropped for the frame: the NPC visible/invisible
|
||||
// flicker (see g_bones_cache_ctx).
|
||||
// Per-INSTANCE skinned candidates (ctx-keyed, character families): a
|
||||
// refused capture re-publishes with the instance's own last palette
|
||||
// (see g_bones_cache_ctx).
|
||||
std::unordered_map<uint32_t, const DrawItem*> pending_skinned_by_ctx;
|
||||
const auto total_indices = [](const DrawItem& d) {
|
||||
uint64_t n = 0;
|
||||
|
|
@ -8154,18 +7966,15 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
const DrawItem& cand = dynitems[r.c - 1];
|
||||
if (cand.pending) {
|
||||
// Deferred mesh whose draw never came, or a capture the palette
|
||||
// acceptance gates refused. Remember it: if NO copy of this mesh
|
||||
// publishes this frame, the post-merge rescue re-publishes it with
|
||||
// LAST frame's palette (see g_bones_cache); one frame of pose lag
|
||||
// beats a one-frame-missing hat.
|
||||
// acceptance gates refused. Remember it: if the instance does not
|
||||
// publish this frame, the post-merge rescue re-publishes it with
|
||||
// LAST frame's palette; one frame of pose lag beats a
|
||||
// one-frame-missing hat.
|
||||
if (cand.ropa) {
|
||||
pending_ropa_by_mesh.try_emplace(cand.mesh, &cand);
|
||||
} else if (cand.skinned) {
|
||||
pending_skinned_by_mesh.try_emplace(cand.mesh, &cand);
|
||||
if (cand.ctx != 0 && cand.char_family != 0) {
|
||||
} else if (cand.skinned && cand.ctx != 0 && cand.char_family != 0) {
|
||||
pending_skinned_by_ctx.try_emplace(cand.ctx, &cand);
|
||||
}
|
||||
}
|
||||
(cand.skinned ? g_skinned_skipped : g_rigid_dropped)
|
||||
.fetch_add(1, std::memory_order_relaxed);
|
||||
continue;
|
||||
|
|
@ -8275,15 +8084,10 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
}
|
||||
}
|
||||
}
|
||||
// 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).
|
||||
// Cross-frame palette rescue + cache refresh (see g_bones_cache_ctx and
|
||||
// g_ropa_state_cache): published copies refresh their cache entries; a
|
||||
// refused/pending capture re-publishes with the cached palette (one frame
|
||||
// of pose lag instead of a one-frame disappearance).
|
||||
if (REXCVAR_GET(skate3_native_render_scene_dynamic_items)) {
|
||||
// v3 Phase 3 palette flip: serve every mapped skinned character item its
|
||||
// instance's own packed m_matrices rows (snapshotted at the game's
|
||||
|
|
@ -8305,7 +8109,9 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
// 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) {
|
||||
if (!item.skinned || item.bones.empty() || item.pending ||
|
||||
item.dbg_src == 10 || // LW-substituted palettes are authoritative
|
||||
item.dbg_src == 11) { // pack-served palettes are authoritative
|
||||
continue;
|
||||
}
|
||||
float spread = 0.0f;
|
||||
|
|
@ -8337,8 +8143,8 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
healed = true;
|
||||
}
|
||||
} else {
|
||||
// Prefer the per-instance cache (clone-exact) over the mesh-keyed
|
||||
// one; healing a clone with its TWIN's palette is a teleport.
|
||||
// Per-instance cache only (clone-exact); healing a clone with its
|
||||
// TWIN's palette is a teleport.
|
||||
const auto cit = item.ctx != 0 ? g_bones_cache_ctx.find(item.ctx)
|
||||
: g_bones_cache_ctx.end();
|
||||
if (cit != g_bones_cache_ctx.end() &&
|
||||
|
|
@ -8347,15 +8153,6 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
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.bones.size() == item.bones.size() &&
|
||||
g_guest_frame - bit->second.frame <= 10) {
|
||||
item.bones = bit->second.bones;
|
||||
item.dbg_src = 6;
|
||||
healed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!healed) {
|
||||
|
|
@ -8369,85 +8166,6 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
item.bones.clear();
|
||||
}
|
||||
}
|
||||
// Detached-garment gate: a ropa garment whose transform sits meters
|
||||
// from EVERY character bone in the frame is wearing another joint's or
|
||||
// another instance's transform (wrong-sibling pack pick, one-bone-late
|
||||
// palette, frozen bank block). Each of those sources yields a coherent,
|
||||
// sane-spread palette that the spread gates above cannot reject; the
|
||||
// shirt renders rotated/offset beside the body while tracking it. No
|
||||
// legitimate frame has a garment more than ~2 m from every character,
|
||||
// so veto the draws (a blink, and the caster pass shares the item) and
|
||||
// log the source path for diagnosis.
|
||||
{
|
||||
// Anchors = every live bone translation of the frame's character
|
||||
// pieces (bone 0 alone is not enough: a tee's bone 0 is the HEAD
|
||||
// joint while a body piece's bone 0 can be a foot, nearly 2 m apart
|
||||
// on one legitimate character).
|
||||
static std::vector<std::array<float, 3>> anchors; // guest thread only
|
||||
anchors.clear();
|
||||
for (const DrawItem& it : scene.items) {
|
||||
if (!it.skinned || it.ropa || it.pending || it.char_family == 0 ||
|
||||
it.bones.size() < 12 || it.draws.empty()) {
|
||||
continue;
|
||||
}
|
||||
for (size_t b = 0; b + 12 <= it.bones.size() && anchors.size() < 2048;
|
||||
b += 12) {
|
||||
const float tx = it.bones[b + 3];
|
||||
const float ty = it.bones[b + 7];
|
||||
const float tz = it.bones[b + 11];
|
||||
if (tx == 0.0f && ty == 0.0f && tz == 0.0f) {
|
||||
continue; // zeroed tail rows carry no position
|
||||
}
|
||||
anchors.push_back({tx, ty, tz});
|
||||
}
|
||||
}
|
||||
const int n_anchors = int(anchors.size());
|
||||
for (DrawItem& item : scene.items) {
|
||||
if (n_anchors == 0) {
|
||||
break;
|
||||
}
|
||||
if (!item.ropa || item.pending || item.draws.empty()) {
|
||||
continue;
|
||||
}
|
||||
float p[3];
|
||||
if (item.skinned && item.bones.size() >= 12) {
|
||||
p[0] = item.bones[3];
|
||||
p[1] = item.bones[7];
|
||||
p[2] = item.bones[11];
|
||||
} else if (!item.skinned) {
|
||||
p[0] = item.world[12];
|
||||
p[1] = item.world[13];
|
||||
p[2] = item.world[14];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
float best_sq = 1e30f;
|
||||
for (int a = 0; a < n_anchors && best_sq > 4.0f; ++a) {
|
||||
const float dx = p[0] - anchors[size_t(a)][0];
|
||||
const float dy = p[1] - anchors[size_t(a)][1];
|
||||
const float dz = p[2] - anchors[size_t(a)][2];
|
||||
const float d = dx * dx + dy * dy + dz * dz;
|
||||
best_sq = d < best_sq ? d : best_sq;
|
||||
}
|
||||
if (best_sq <= 4.0f) {
|
||||
continue;
|
||||
}
|
||||
g_ropa_detached.fetch_add(1, std::memory_order_relaxed);
|
||||
static std::atomic<uint32_t> s_detach_logged{0};
|
||||
const uint32_t ln = s_detach_logged.fetch_add(1, std::memory_order_relaxed);
|
||||
if (ln < 24 || (ln & 511u) == 0) {
|
||||
REXLOG_INFO(
|
||||
"native-scene: DETACHED ropa garment vetoed mesh={:08X} "
|
||||
"ctx={:08X} src={} skinned={} caster={} pos=({:.1f},{:.1f},{:.1f}) "
|
||||
"nearest_char={:.2f}m (n={})",
|
||||
item.mesh, item.ctx, item.dbg_src, item.skinned ? 1 : 0,
|
||||
item.caster_bank ? 1 : 0, p[0], p[1], p[2], std::sqrt(best_sq),
|
||||
ln);
|
||||
}
|
||||
item.draws.clear();
|
||||
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
|
||||
|
|
@ -8466,33 +8184,19 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
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.
|
||||
// Vetoed/dropped this frame (incoherent-palette trip): 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) {
|
||||
const bool now_skinned = item.skinned && !item.bones.empty();
|
||||
auto [cit, fresh] = g_ropa_state_cache.try_emplace(item.mesh);
|
||||
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;
|
||||
RopaResolvedState& c = g_ropa_state_cache[item.mesh];
|
||||
c.skinned = item.skinned && !item.bones.empty();
|
||||
std::memcpy(c.world, item.world, sizeof(c.world));
|
||||
c.bones = item.bones;
|
||||
c.frame = g_guest_frame;
|
||||
}
|
||||
} else if (item.skinned && !item.bones.empty() && !item.caster_bank &&
|
||||
g_bones_cache.size() < 512) {
|
||||
// caster_bank palettes are ~40 ms stale; a rescue re-publishing
|
||||
// one would jump the entity backwards (see the ring ingest guard).
|
||||
CachedBones& cb = g_bones_cache[item.mesh];
|
||||
cb.bones = item.bones;
|
||||
cb.frame = g_guest_frame;
|
||||
}
|
||||
}
|
||||
// Per-instance palette cache refresh (see g_bones_cache_ctx): every
|
||||
|
|
@ -8516,12 +8220,10 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
cb.bones = item.bones;
|
||||
cb.frame = g_guest_frame;
|
||||
}
|
||||
// Per-instance rescue, BEFORE the mesh-keyed one: a refused/pending
|
||||
// skinned character capture whose ctx did not publish this frame
|
||||
// re-publishes with ITS OWN last palette (<= 10 frames fresh, same
|
||||
// bound as the mesh rescue), regardless of how many clones of the mesh
|
||||
// are alive. Rescued items bump pub_count so the mesh-keyed loop below
|
||||
// cannot double-publish the same candidate.
|
||||
// Per-instance rescue: a refused/pending skinned character capture
|
||||
// whose ctx did not publish this frame re-publishes with ITS OWN last
|
||||
// palette (<= 10 frames fresh), regardless of how many clones of the
|
||||
// mesh are alive.
|
||||
for (const auto& [ctxk, cand] : pending_skinned_by_ctx) {
|
||||
if (dyn_slot.find(ctxk) != dyn_slot.end()) {
|
||||
continue; // this instance published live
|
||||
|
|
@ -8542,28 +8244,6 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
dyn_slot.try_emplace(ctxk, scene.items.size() - 1);
|
||||
g_lw_ctx_rescued.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
for (const auto& [mesh, cand] : pending_skinned_by_mesh) {
|
||||
if (pub_count.find(mesh) != pub_count.end()) {
|
||||
continue; // a live copy published; nothing to rescue
|
||||
}
|
||||
const auto bit = g_bones_cache.find(mesh);
|
||||
if (bit == g_bones_cache.end() ||
|
||||
g_guest_frame - bit->second.frame > 10) {
|
||||
// Stale cache = an OLD pose: for a moving vehicle the rescue
|
||||
// rendered it 10-20 m behind its live position (the momentary
|
||||
// ghost-back). One missing frame beats a teleport.
|
||||
continue;
|
||||
}
|
||||
scene.items.push_back(*cand);
|
||||
DrawItem& rescued = scene.items.back();
|
||||
rescued.bones = bit->second.bones;
|
||||
rescued.pending = false;
|
||||
rescued.dbg_src = 3;
|
||||
if (rescued.ctx != 0) {
|
||||
dyn_slot.try_emplace(rescued.ctx, scene.items.size() - 1);
|
||||
}
|
||||
g_bones_rescued.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
// Refused ropa captures re-publish last frame's resolved state: mode,
|
||||
// world AND palette together (mixing frames' interpretations is the
|
||||
// mangled-ribbon bug; see g_ropa_state_cache).
|
||||
|
|
@ -9089,16 +8769,6 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
if (!GuestTryCopy(job.vb.data(), base + item.vb_addr, item.vb_bytes)) {
|
||||
continue;
|
||||
}
|
||||
if (item.ropa && item.skinned && !item.bones.empty() &&
|
||||
!RopaPayloadCoherent(item, job.vb)) {
|
||||
// The cloth sim rewrote this VB between the draw-time capture and
|
||||
// this frame-end snapshot (mode flip in flight): decoding it under
|
||||
// the skinned palette is the mangled ribbon. Keep the old decode
|
||||
// this frame; next frame's capture sees the flipped flag and
|
||||
// resolves mode and payload together.
|
||||
g_ropa_mismatch.fetch_add(1, std::memory_order_relaxed);
|
||||
continue;
|
||||
}
|
||||
job.ib.resize(size_t(item.ib_count) * 2);
|
||||
if (!GuestTryCopy(job.ib.data(), base + item.ib_addr, job.ib.size())) {
|
||||
continue;
|
||||
|
|
@ -9301,54 +8971,6 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
skate3::native_entity::EmitStats();
|
||||
}
|
||||
|
||||
// Bone-palette alternation guard (the high-fps hair/garment flicker,
|
||||
// probe-confirmed: alt[bones] events on fam 2/4/5 pieces while rows/
|
||||
// coverage/route stayed stable). A served palette that returns BIT-EXACT
|
||||
// to the previous-but-one frame's palette while differing from the last
|
||||
// frame's is never a real pose (the sim holds poses across several
|
||||
// rendered frames at high fps; genuine motion never round-trips in two
|
||||
// rendered frames); it is the capture fixup alternating between two
|
||||
// banks (stale tick / nearby twin). Repair: keep serving the LAST frame's
|
||||
// palette, damping A,B,A,B to a steady pose until the capture settles.
|
||||
if (REXCVAR_GET(skate3_native_render_scene_palette_alt_guard)) {
|
||||
struct PaletteGuard {
|
||||
uint64_t h1 = 0;
|
||||
uint64_t h2 = 0;
|
||||
std::vector<float> last; // the palette h1 hashes
|
||||
};
|
||||
static std::unordered_map<uint64_t, PaletteGuard> s_pal_guard; // guest thread
|
||||
for (DrawItem& item : scene.items) {
|
||||
if (item.char_family == 0 || !item.skinned || item.bones.empty() ||
|
||||
item.dbg_src == 10 || // LW-substituted palettes are authoritative
|
||||
item.dbg_src == 11) { // pack-served palettes are authoritative
|
||||
continue;
|
||||
}
|
||||
uint64_t h = 1469598103934665603ull;
|
||||
const uint8_t* pb = reinterpret_cast<const uint8_t*>(item.bones.data());
|
||||
const size_t pn = item.bones.size() * sizeof(float);
|
||||
for (size_t bi = 0; bi < pn; ++bi) {
|
||||
h = (h ^ pb[bi]) * 1099511628211ull;
|
||||
}
|
||||
const uint64_t key = (uint64_t(item.mesh) << 32) | item.ctx;
|
||||
auto [git, gfresh] = s_pal_guard.try_emplace(key);
|
||||
PaletteGuard& pg = git->second;
|
||||
if (!gfresh && h != pg.h1 && h == pg.h2 &&
|
||||
pg.last.size() == item.bones.size()) {
|
||||
std::memcpy(item.bones.data(), pg.last.data(),
|
||||
item.bones.size() * sizeof(float));
|
||||
g_palette_alt_repaired.fetch_add(1, std::memory_order_relaxed);
|
||||
// h1/last stay: the repaired frame served h1's palette again.
|
||||
} else {
|
||||
pg.h2 = pg.h1;
|
||||
pg.h1 = h;
|
||||
pg.last.assign(item.bones.begin(), item.bones.end());
|
||||
}
|
||||
}
|
||||
if (s_pal_guard.size() > 1024) {
|
||||
s_pal_guard.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Camera re-timing (see SmoothCamera): replace the guest's sim-stepped
|
||||
// pose with a host-clock-interpolated one so panning is smooth at render
|
||||
// rate. All consumers (items, sky follow, sorting, outline) use the
|
||||
|
|
@ -9829,62 +9451,15 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
g_dynobj_frame_done = false;
|
||||
|
||||
|
||||
// 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.
|
||||
// rescues and interpolation 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. 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 ||
|
||||
|
|
@ -10303,11 +9878,9 @@ uint64_t SamplePayloadFingerprint(uint8_t* base, uint32_t addr, uint32_t size) {
|
|||
}
|
||||
|
||||
// Heal-pipeline telemetry (see the 600-frame stats line): verify-fail =
|
||||
// commit-time stability rejections, decode-fail = failed re-decodes of a
|
||||
// still-serving entry, demote-hold = mip-0 demotes served from the cached
|
||||
// full-chain decode without a doomed re-decode.
|
||||
// commit-time stability rejections, demote-hold = mip-0 demotes served
|
||||
// from the cached full-chain decode without a doomed re-decode.
|
||||
std::atomic<uint64_t> g_heal_verify_fail{0};
|
||||
std::atomic<uint64_t> g_heal_decode_fail{0};
|
||||
std::atomic<uint64_t> g_demote_hold{0};
|
||||
// Sticky/skip serving (the anti-white-flash layer): sticky = draws served
|
||||
// with the item's previous texture while a fresh object decodes; skipnew =
|
||||
|
|
@ -15261,21 +14834,6 @@ void PrewarmCommit(const NativeGuestOutputRenderContext& context,
|
|||
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;
|
||||
}
|
||||
// ROPA shape-generation ring: retain this decode's vertex array
|
||||
// (keyed by dyn_seq) for the draw-time blend onto the play clock.
|
||||
// Runs even when the GPU buffers get dropped as identical below;
|
||||
|
|
@ -15402,9 +14960,6 @@ void PrewarmCommit(const NativeGuestOutputRenderContext& context,
|
|||
// of a still-serving entry needs no retry stamp: the payload
|
||||
// poll re-detects on its own cadence and the miss-inflight set
|
||||
// already dedupes.
|
||||
if (!t.valid && !t.verify_failed) {
|
||||
g_heal_decode_fail.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
if (same_content && t.gt.near_black && wit->second.near_black &&
|
||||
wit->second.nb_redecodes < 255) {
|
||||
// A forced near-black re-decode came back identical: one more
|
||||
|
|
@ -17066,17 +16621,17 @@ void LogFrameStats(const FrameScene& scene, uint64_t frames, uint32_t drawn,
|
|||
"native-scene: frame {} items={} draws={} draws_2d={} drawn_2d={} "
|
||||
"splines[{}/{}] "
|
||||
"2d[other={} dropped={} askip={} astale={} textures={}] cached_meshes={} textures={} "
|
||||
"vs_uploads={} palettes={} palette_base_plus1={} ropa[rigid={} stale={} rescued={} flip={} mismatch={} relax={} hold={} caster={} incoh={} stretch={} detach={} blend={} blendmiss={}] dyn_gap={} skinned={} skinned_skipped={} foreign_bank={} "
|
||||
"vs_uploads={} palettes={} palette_base_plus1={} ropa[rigid={} stale={} rescued={} relax={} caster={} incoh={} stretch={} blend={} blendmiss={}] 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={}] "
|
||||
"store[n={} routes={} evict={}] "
|
||||
"heal[vfail={} dfail={} demote={}] serve[sticky={} skipnew={} adstale={} adnone={}] "
|
||||
"shadow[valid={} ready={} draws={}] char[attempt={} valid={} drawn={} reused={} "
|
||||
"bones_rescued={}] dynobj[valid={} drawn={}] "
|
||||
"heal[vfail={} demote={}] serve[sticky={} skipnew={} adstale={} adnone={}] "
|
||||
"shadow[valid={} ready={} draws={}] char[attempt={} valid={} drawn={} "
|
||||
"reused={}] dynobj[valid={} drawn={}] "
|
||||
"lw[ctxs={} ents={} stamp={} fade0={} resc={} fill={} pal={} rows={}] "
|
||||
"refl[pair={} flat={} gate={:#x}] flips={} alt[rows={} bones={} cov={} "
|
||||
"shadow={}] rows_inst={} pal_rep={} cref_ropa={} pair_fix={} pal_srv={}",
|
||||
"refl[pair={} flat={} gate={:#x}] flips={} alt[bones={} shadow={}] "
|
||||
"rows_inst={} pal_srv={}",
|
||||
frames, scene.items.size(), drawn, g_draws_2d.load(), drawn_2d,
|
||||
drawn_spline, g_draws_spline.load(),
|
||||
g_draws_2d_other.load(), g_draws_2d_dropped.load(),
|
||||
|
|
@ -17084,9 +16639,8 @@ void LogFrameStats(const FrameScene& scene, uint64_t frames, uint32_t drawn,
|
|||
g_r.meshes.size(), g_r.tex_store.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_ropa_relaxed.load(),
|
||||
g_ropa_hold.load(), g_ropa_caster.load(), g_pub_incoherent.load(),
|
||||
g_stretch_veto.load(), g_ropa_detached.load(),
|
||||
g_ropa_relaxed.load(), g_ropa_caster.load(), g_pub_incoherent.load(),
|
||||
g_stretch_veto.load(),
|
||||
g_ropa_blend_drawn.load(), g_ropa_blend_miss.load(), g_dyn_gap.load(),
|
||||
g_skinned_items.load(),
|
||||
g_skinned_skipped.load(), g_capture_foreign_bank.load(),
|
||||
|
|
@ -17097,21 +16651,19 @@ void LogFrameStats(const FrameScene& scene, uint64_t frames, uint32_t drawn,
|
|||
g_rr_decode_fail.load(), g_rr_no_bones.load(), g_rr_mesh_deferred.load(),
|
||||
g_rr_tex_deferred.load(), g_r.tex_store.size(), g_r.tex_routes.size(),
|
||||
g_store_evicted.load(), g_heal_verify_fail.load(),
|
||||
g_heal_decode_fail.load(), g_demote_hold.load(),
|
||||
g_demote_hold.load(),
|
||||
g_tex_sticky_served.load(), g_skip_new.load(), g_ad_stale_served.load(),
|
||||
g_ad_placeholder.load(), scene.shadow_valid,
|
||||
shadow_ready, shadow_draws,
|
||||
g_char_attempts.load(), g_char_valid.load(), g_char_drawn.load(),
|
||||
g_char_rows_reused.load(), g_bones_rescued.load(), scene.dynobj_valid,
|
||||
g_char_rows_reused.load(), scene.dynobj_valid,
|
||||
g_dynobj_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(),
|
||||
g_hair_route_flips.load(), g_hair_rows_alternations.load(),
|
||||
g_hair_bone_alternations.load(), g_hair_cov_alternations.load(),
|
||||
g_hair_route_flips.load(), g_hair_bone_alternations.load(),
|
||||
g_shadow_alternations.load(), g_char_rows_inst_served.load(),
|
||||
g_palette_alt_repaired.load(), g_ropa_caster_refreshed.load(),
|
||||
g_pending_pair_fixed.load(), g_pal_served_total.load());
|
||||
g_pal_served_total.load());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -18351,41 +17903,6 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
// character.alpha lenses use the same plumbing (ch_misc.z signals the
|
||||
// fam-1/2 branch to take alpha from this texture).
|
||||
decal_tex = resolve_texture(item.hair_alpha_tex, 5);
|
||||
// Coverage-serve alternation probe (see g_hair_cov_alternations): the
|
||||
// served texture flipping real<->white-fallback frame to frame is a
|
||||
// hair-flicker mechanism (white = every strand opaque).
|
||||
{
|
||||
struct CovProbe {
|
||||
uint64_t fp1 = 0;
|
||||
uint64_t fp2 = 0;
|
||||
};
|
||||
static std::unordered_map<uint64_t, CovProbe> s_cov; // render thread
|
||||
const uint64_t ck = (uint64_t(item.mesh) << 32) | item.ctx;
|
||||
const uint64_t fp = decal_tex == &g_r.white
|
||||
? 1u
|
||||
: (decal_tex != nullptr && decal_tex->valid
|
||||
? decal_tex->payload_fp
|
||||
: 2u);
|
||||
auto [cit2, cfresh] = s_cov.try_emplace(ck);
|
||||
CovProbe& cp = cit2->second;
|
||||
if (!cfresh && fp != cp.fp1 && fp == cp.fp2) {
|
||||
g_hair_cov_alternations.fetch_add(1, std::memory_order_relaxed);
|
||||
static std::atomic<uint32_t> s_cov_logged{0};
|
||||
const uint32_t cl = s_cov_logged.fetch_add(1, std::memory_order_relaxed);
|
||||
if (cl < 24 || (cl & 1023u) == 0) {
|
||||
REXLOG_INFO(
|
||||
"native-scene: hair COVERAGE ALTERNATION mesh={:08X} "
|
||||
"ctx={:08X} fam={} tex={:08X} fp={:016X} prev={:016X} (n={})",
|
||||
item.mesh, item.ctx, item.char_family, item.hair_alpha_tex, fp,
|
||||
cp.fp1, cl);
|
||||
}
|
||||
}
|
||||
cp.fp2 = cp.fp1;
|
||||
cp.fp1 = fp;
|
||||
if (s_cov.size() > 4096) {
|
||||
s_cov.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
// F7 ring: stamp the SERVED content fingerprints (see SceneRingItem);
|
||||
// pointer identity cannot see an in-place content swap.
|
||||
|
|
@ -18964,27 +18481,20 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
// without validated rows ch_misc.z is 0 and the item stays opaque.
|
||||
const bool cac_alpha_blend = item.char_alpha && char_capture_ok &&
|
||||
item.char_rows[14 * 4 + 2] > 0.0f;
|
||||
// Flicker probes: a hair/character-alpha piece whose pass route OR
|
||||
// served rows ALTERNATE between consecutive frames (A,B,A,B) is the
|
||||
// visible hair flicker. Route flips and rows alternations are counted
|
||||
// separately; rows legitimately drift with lighting, so only the
|
||||
// exact return to the previous-but-one value counts as alternation.
|
||||
// Flicker probes: a hair/character-alpha piece whose pass route flips
|
||||
// between consecutive frames, or whose bone palette returns bit-exact
|
||||
// to the previous-but-one frame's value (A,B,A,B: genuine motion
|
||||
// never round-trips in two rendered frames), is the visible hair
|
||||
// flicker.
|
||||
if (item.char_family >= 4 || item.char_alpha) {
|
||||
struct RouteProbe {
|
||||
uint8_t route = 0;
|
||||
uint64_t rows_h1 = 0; // last frame's rows hash
|
||||
uint64_t rows_h2 = 0; // the frame before
|
||||
uint64_t bones_h1 = 0;
|
||||
uint64_t bones_h2 = 0;
|
||||
uint64_t bones_h1 = 0; // last frame's palette hash
|
||||
uint64_t bones_h2 = 0; // the frame before
|
||||
};
|
||||
static std::unordered_map<uint64_t, RouteProbe> s_probe; // render thread
|
||||
const uint64_t rk = (uint64_t(item.mesh) << 32) | item.ctx;
|
||||
const uint8_t route = uint8_t((hair_blend || cac_alpha_blend) ? 2 : 1);
|
||||
uint64_t rows_h = 1469598103934665603ull;
|
||||
const uint8_t* rb = reinterpret_cast<const uint8_t*>(item.char_rows);
|
||||
for (size_t bi = 0; bi < sizeof(item.char_rows); ++bi) {
|
||||
rows_h = (rows_h ^ rb[bi]) * 1099511628211ull;
|
||||
}
|
||||
uint64_t bones_h = 1469598103934665603ull;
|
||||
if (!item.bones.empty()) {
|
||||
const uint8_t* bb = reinterpret_cast<const uint8_t*>(item.bones.data());
|
||||
|
|
@ -19008,22 +18518,6 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
item.char_rows[14 * 4 + 2], fl);
|
||||
}
|
||||
}
|
||||
if (!fresh2 && rows_h != pr.rows_h1 && rows_h == pr.rows_h2) {
|
||||
g_hair_rows_alternations.fetch_add(1, std::memory_order_relaxed);
|
||||
static std::atomic<uint32_t> s_alt_logged{0};
|
||||
const uint32_t al = s_alt_logged.fetch_add(1, std::memory_order_relaxed);
|
||||
if (al < 24 || (al & 1023u) == 0) {
|
||||
REXLOG_INFO(
|
||||
"native-scene: hair ROWS ALTERNATION mesh={:08X} ctx={:08X} "
|
||||
"fam={} light=({:.3f},{:.3f},{:.3f}) key=({:.3f},{:.3f},{:.3f}) "
|
||||
"expo={:.3f} rows14=({:.3f},{:.3f},{:.3f}) (n={})",
|
||||
item.mesh, item.ctx, item.char_family, item.char_rows[0],
|
||||
item.char_rows[1], item.char_rows[2], item.char_rows[4],
|
||||
item.char_rows[5], item.char_rows[6], item.char_rows[7],
|
||||
item.char_rows[14 * 4 + 0], item.char_rows[14 * 4 + 1],
|
||||
item.char_rows[14 * 4 + 2], al);
|
||||
}
|
||||
}
|
||||
if (!fresh2 && !item.bones.empty() && bones_h != pr.bones_h1 &&
|
||||
bones_h == pr.bones_h2) {
|
||||
g_hair_bone_alternations.fetch_add(1, std::memory_order_relaxed);
|
||||
|
|
@ -19038,8 +18532,6 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
|||
}
|
||||
}
|
||||
pr.route = route;
|
||||
pr.rows_h2 = pr.rows_h1;
|
||||
pr.rows_h1 = rows_h;
|
||||
pr.bones_h2 = pr.bones_h1;
|
||||
pr.bones_h1 = bones_h;
|
||||
if (s_probe.size() > 4096) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue