capture: place sort-list batched piece clones with their instance matrix

This commit is contained in:
mchughalex 2026-07-22 19:08:25 +01:00
parent 14acde031b
commit a16314df0c
2 changed files with 59 additions and 0 deletions

View file

@ -4209,6 +4209,15 @@ uint32_t CaptureDynamicState(uint8_t* base, uint32_t ctx, bool world_path,
// invisibly at the origin). Defer those to the post-draw fixup, like // invisibly at the origin). Defer those to the post-draw fixup, like
// skinned palettes. // skinned palettes.
if (!item.skinned) { if (!item.skinned) {
if (!world_path && item.ctx != 0) {
// Mark the mesh as dynamically dispatched (see g_dyn_dispatch_meshes):
// far clones of it arriving through the static sort lists need their
// instance matrix, not the world-item identity.
if (g_dyn_dispatch_meshes.size() > 16384) {
g_dyn_dispatch_meshes.clear();
}
g_dyn_dispatch_meshes[item.mesh] = g_guest_frame;
}
// The instance's own matrix (ReadCtxInstanceWorld) outranks the bank: // The instance's own matrix (ReadCtxInstanceWorld) outranks the bank:
// own_draw_last only proves SOME clone of this mesh drew last (clones // own_draw_last only proves SOME clone of this mesh drew last (clones
// share (ib,vb)), so under batched dispatch the bank world routinely // share (ib,vb)), so under batched dispatch the bank world routinely
@ -8474,6 +8483,11 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
// Per-instance rigid candidates whose post-draw world fixup never landed // Per-instance rigid candidates whose post-draw world fixup never landed
// this frame; rescued with their cached world (see g_rigid_world_cache). // this frame; rescued with their cached world (see g_rigid_world_cache).
std::unordered_map<uint32_t, const DrawItem*> pending_rigid_by_ctx; std::unordered_map<uint32_t, const DrawItem*> pending_rigid_by_ctx;
// Sort-list records of dynamically-dispatched piece meshes (see
// g_dyn_dispatch_meshes): built here, published AFTER the record loop with
// the ctx owner's instance matrix, and only for ctxs no live dynamic
// capture already covered this frame.
std::unordered_map<uint32_t, DrawItem> sortlist_local_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;
@ -8635,6 +8649,18 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
g_world_props.fetch_add(1, std::memory_order_relaxed); g_world_props.fetch_add(1, std::memory_order_relaxed);
continue; continue;
} }
const auto dit = g_dyn_dispatch_meshes.find(item.mesh);
if (dit != g_dyn_dispatch_meshes.end() &&
g_guest_frame - dit->second <= 1800) {
// Instance-transformed piece batched through the static sort lists
// (the game hands FAR clones of dynamically-dispatched pieces to
// the batcher): fmt-57 but MESH-LOCAL, so the identity world of
// this path collapses it at the origin. Defer; the post-loop
// publish serves the ctx owner's instance matrix, and only for
// ctxs no live dynamic capture covered this frame.
sortlist_local_by_ctx.try_emplace(r.a, std::move(item));
continue;
}
scene.items.push_back(std::move(item)); scene.items.push_back(std::move(item));
} else { } else {
// Silent world-item drop attribution (the F7 rings show world meshes // Silent world-item drop attribution (the F7 rings show world meshes
@ -8842,6 +8868,25 @@ 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_rigid_rescued.fetch_add(1, std::memory_order_relaxed); g_rigid_rescued.fetch_add(1, std::memory_order_relaxed);
} }
// Sort-list copies of dynamically-dispatched piece meshes (see
// g_dyn_dispatch_meshes): the game batches FAR clones through the
// static lists, where no per-entity capture carries their placement.
// Publish with the ctx owner's instance matrix; a live dynamic capture
// for the same ctx always wins (near clones arrive through both paths
// in the same frame, and double-publishing would z-fight).
for (auto& [ctxk, item] : sortlist_local_by_ctx) {
if (dyn_slot.find(ctxk) != dyn_slot.end()) {
continue; // this instance published live
}
if (!ReadCtxInstanceWorld(base, ctxk, item.world)) {
g_world_props.fetch_add(1, std::memory_order_relaxed);
continue; // no placement: dropping beats an origin ghost
}
++pub_count[item.mesh];
scene.items.push_back(std::move(item));
dyn_slot.try_emplace(ctxk, scene.items.size() - 1);
g_sortlist_local_pub.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

@ -825,6 +825,9 @@ inline std::atomic<uint64_t> g_rigid_rescued{0};
// Rigid worlds served from the guest instance matrix (ReadCtxInstanceWorld) // Rigid worlds served from the guest instance matrix (ReadCtxInstanceWorld)
// instead of a constant bank. // instead of a constant bank.
inline std::atomic<uint64_t> g_rigid_ctx_world{0}; inline std::atomic<uint64_t> g_rigid_ctx_world{0};
// Sort-list piece copies published with the instance matrix (see
// g_dyn_dispatch_meshes).
inline std::atomic<uint64_t> g_sortlist_local_pub{0};
// Last published world per rigid dynamic context. Guest render thread only. // Last published world per rigid dynamic context. Guest render thread only.
// Same-mesh piece clones (park-editor venues submit dozens) share (ib,vb), // 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 // and their hook-time captures defer to the post-draw fixup whenever the
@ -839,6 +842,17 @@ struct RigidWorldCache {
uint64_t frame = 0; uint64_t frame = 0;
}; };
inline std::unordered_map<uint32_t, RigidWorldCache> g_rigid_world_cache; inline std::unordered_map<uint32_t, RigidWorldCache> g_rigid_world_cache;
// Meshes seen on the per-entity dynamic dispatch (kind-0 captures), stamped
// with the guest frame of the last sighting. The game batches FAR instances
// of such pieces through the static sort lists instead of dispatching them
// per entity; their fmt-57 vertices are MESH-LOCAL, so the world-item
// path's identity world collapses them at the origin (the far-corner
// "teardown": one sub-mesh of each distant piece went invisible). A
// sort-list record whose mesh is fresh here publishes with the ctx owner's
// instance matrix instead. The freshness window doubles as map-change
// hygiene: a mesh address reused by a later load expires out on its own.
// Guest render thread only.
inline std::unordered_map<uint32_t, uint64_t> g_dyn_dispatch_meshes;
// 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};