capture: pair deferred rigid clone worlds by identity instead of submission order

This commit is contained in:
mchughalex 2026-07-22 18:11:38 +01:00
parent 186c7de6f4
commit 89360e53fc
2 changed files with 100 additions and 0 deletions

View file

@ -4172,6 +4172,14 @@ uint32_t CaptureDynamicState(uint8_t* base, uint32_t ctx, bool world_path,
!BankRigidWorld(base, bank, item.world)) { !BankRigidWorld(base, bank, item.world)) {
item.pending = true; item.pending = true;
g_rigid_pending.fetch_add(1, std::memory_order_relaxed); g_rigid_pending.fetch_add(1, std::memory_order_relaxed);
} else if (item.ctx != 0) {
if (g_rigid_world_cache.size() > 32768) {
g_rigid_world_cache.clear(); // map-change growth backstop
}
RigidWorldCache& rc = g_rigid_world_cache[item.ctx];
rc.mesh = item.mesh;
std::memcpy(rc.world, item.world, sizeof(rc.world));
rc.frame = g_guest_frame;
} }
} }
@ -5540,6 +5548,47 @@ void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t
} }
caster_refresh = true; caster_refresh = true;
} }
// Rigid clone disambiguation: same-mesh piece clones share these buffers,
// and under batched dispatch (park-editor venues: every capture defers,
// 100+ pending rigids per frame) the FIFO capture order is NOT the draw
// order; pairing a draw with the wrong clone swaps piece worlds frame to
// frame (pieces visibly shuffle and flip 180 degrees). Prefer the pending
// clone whose LAST PUBLISHED world sits nearest this draw's world; FIFO
// stays the fallback for first-sight clones with no history. Movables stay
// matchable: they drift well under the 0.5 m bound per frame and the cache
// re-stamps on every resolved draw.
if (!caster_refresh && oldest->second < g_frame_dynitems.size() &&
!g_frame_dynitems[oldest->second].skinned) {
float w[16];
if (!BankIsAuxPerspective(base, bank) && BankRigidWorld(base, bank, w)) {
float best_d2 = 0.25f;
auto best = range.second;
for (auto it = range.first; it != range.second; ++it) {
if (it->second >= g_frame_dynitems.size()) {
continue;
}
const DrawItem& c = g_frame_dynitems[it->second];
if (!c.pending || c.skinned || c.ctx == 0) {
continue;
}
const auto cit = g_rigid_world_cache.find(c.ctx);
if (cit == g_rigid_world_cache.end() || cit->second.mesh != c.mesh) {
continue;
}
const float dx = cit->second.world[12] - w[12];
const float dy = cit->second.world[13] - w[13];
const float dz = cit->second.world[14] - w[14];
const float d2 = dx * dx + dy * dy + dz * dz;
if (d2 < best_d2) {
best_d2 = d2;
best = it;
}
}
if (best != range.second) {
oldest = best;
}
}
}
DrawItem& d = g_frame_dynitems[oldest->second]; DrawItem& d = g_frame_dynitems[oldest->second];
if (caster_refresh) { if (caster_refresh) {
// Refresh of a caster-sourced capture from a later (ideally main-pass) // Refresh of a caster-sourced capture from a later (ideally main-pass)
@ -5598,6 +5647,12 @@ void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t
return; return;
} }
d.caster_bank = false; d.caster_bank = false;
if (d.ctx != 0) {
RigidWorldCache& rc = g_rigid_world_cache[d.ctx];
rc.mesh = d.mesh;
std::memcpy(rc.world, d.world, sizeof(rc.world));
rc.frame = g_guest_frame;
}
} }
d.pending = false; d.pending = false;
d.dbg_src = 2; d.dbg_src = 2;
@ -8293,6 +8348,9 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
// refused capture re-publishes with the instance's own last palette // refused capture re-publishes with the instance's own last palette
// (see g_bones_cache_ctx). // (see g_bones_cache_ctx).
std::unordered_map<uint32_t, const DrawItem*> pending_skinned_by_ctx; std::unordered_map<uint32_t, const DrawItem*> pending_skinned_by_ctx;
// Per-instance rigid candidates whose post-draw world fixup never landed
// this frame; rescued with their cached world (see g_rigid_world_cache).
std::unordered_map<uint32_t, const DrawItem*> pending_rigid_by_ctx;
const auto total_indices = [](const DrawItem& d) { const auto total_indices = [](const DrawItem& d) {
uint64_t n = 0; uint64_t n = 0;
for (const DrawEntry& e : d.draws) n += e.index_count; for (const DrawEntry& e : d.draws) n += e.index_count;
@ -8355,6 +8413,8 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
pending_ropa_by_mesh.try_emplace(cand.mesh, &cand); pending_ropa_by_mesh.try_emplace(cand.mesh, &cand);
} else if (cand.skinned && 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); pending_skinned_by_ctx.try_emplace(cand.ctx, &cand);
} else if (!cand.skinned && !cand.cloth_quads && cand.ctx != 0) {
pending_rigid_by_ctx.try_emplace(cand.ctx, &cand);
} }
(cand.skinned ? g_skinned_skipped : g_rigid_dropped) (cand.skinned ? g_skinned_skipped : g_rigid_dropped)
.fetch_add(1, std::memory_order_relaxed); .fetch_add(1, std::memory_order_relaxed);
@ -8636,6 +8696,29 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
dyn_slot.try_emplace(ctxk, scene.items.size() - 1); dyn_slot.try_emplace(ctxk, scene.items.size() - 1);
g_lw_ctx_rescued.fetch_add(1, std::memory_order_relaxed); g_lw_ctx_rescued.fetch_add(1, std::memory_order_relaxed);
} }
// Rigid clone rescue: a piece whose world-bearing draw was missed this
// frame (culled, or claimed by another clone of the mesh) re-publishes
// with its own last published world instead of vanishing for a frame.
// Statics never move, and movables at most drift one frame behind; the
// mesh check keeps a reused ctx address from wearing a stale placement.
for (const auto& [ctxk, cand] : pending_rigid_by_ctx) {
if (dyn_slot.find(ctxk) != dyn_slot.end()) {
continue; // this instance published live
}
const auto cit = g_rigid_world_cache.find(ctxk);
if (cit == g_rigid_world_cache.end() || cit->second.mesh != cand->mesh ||
g_guest_frame - cit->second.frame > 600) {
continue; // no trusted placement; a dropped frame beats an origin ghost
}
scene.items.push_back(*cand);
DrawItem& rescued = scene.items.back();
std::memcpy(rescued.world, cit->second.world, sizeof(rescued.world));
rescued.pending = false;
rescued.dbg_src = 12;
++pub_count[rescued.mesh];
dyn_slot.try_emplace(ctxk, scene.items.size() - 1);
g_rigid_rescued.fetch_add(1, std::memory_order_relaxed);
}
// Refused ropa captures re-publish last frame's resolved state: mode, // Refused ropa captures re-publish last frame's resolved state: mode,
// world AND palette together (mixing frames' interpretations is the // world AND palette together (mixing frames' interpretations is the
// mangled-ribbon bug; see g_ropa_state_cache). // mangled-ribbon bug; see g_ropa_state_cache).

View file

@ -819,6 +819,23 @@ inline std::atomic<uint64_t> g_skinned_skipped{0};
// draws), and those still pending at frame end (dropped). // draws), and those still pending at frame end (dropped).
inline std::atomic<uint64_t> g_rigid_pending{0}; inline std::atomic<uint64_t> g_rigid_pending{0};
inline std::atomic<uint64_t> g_rigid_dropped{0}; inline std::atomic<uint64_t> g_rigid_dropped{0};
// Pending rigid clones re-published with their cached world at merge time
// (see g_rigid_world_cache).
inline std::atomic<uint64_t> g_rigid_rescued{0};
// Last published world per rigid dynamic context. Guest render thread only.
// Same-mesh piece clones (park-editor venues submit dozens) share (ib,vb),
// and their hook-time captures defer to the post-draw fixup whenever the
// venue batches dispatch (own_draw_last false); FIFO pairing there scrambles
// clone worlds whenever capture order and draw order diverge - pieces
// visibly swap placements and flip 180 degrees frame to frame. The fixup
// pairs each draw to the pending clone whose cached world matches instead,
// and clones whose draw never landed re-publish their cached world at merge.
struct RigidWorldCache {
uint32_t mesh = 0;
float world[16] = {};
uint64_t frame = 0;
};
inline std::unordered_map<uint32_t, RigidWorldCache> g_rigid_world_cache;
// Model-space props in the world sort lists, excluded from the identity // Model-space props in the world sort lists, excluded from the identity
// world path (their placed copies come from the kind-2 capture). // world path (their placed copies come from the kind-2 capture).
inline std::atomic<uint64_t> g_world_props{0}; inline std::atomic<uint64_t> g_world_props{0};