From a16314df0c55e1ca643d807289196948ba9bd391 Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:08:25 +0100 Subject: [PATCH] capture: place sort-list batched piece clones with their instance matrix --- src/skate3_native_scene.cpp | 45 +++++++++++++++++++++++++++++++++ src/skate3_native_scene_state.h | 14 ++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/skate3_native_scene.cpp b/src/skate3_native_scene.cpp index 3f6e7b2..c1105fd 100644 --- a/src/skate3_native_scene.cpp +++ b/src/skate3_native_scene.cpp @@ -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 // skinned palettes. 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: // own_draw_last only proves SOME clone of this mesh drew last (clones // 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 // this frame; rescued with their cached world (see g_rigid_world_cache). std::unordered_map 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 sortlist_local_by_ctx; const auto total_indices = [](const DrawItem& d) { uint64_t n = 0; 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); 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)); } else { // 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); 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, // world AND palette together (mixing frames' interpretations is the // mangled-ribbon bug; see g_ropa_state_cache). diff --git a/src/skate3_native_scene_state.h b/src/skate3_native_scene_state.h index e7a1f5c..da5599d 100644 --- a/src/skate3_native_scene_state.h +++ b/src/skate3_native_scene_state.h @@ -825,6 +825,9 @@ inline std::atomic g_rigid_rescued{0}; // Rigid worlds served from the guest instance matrix (ReadCtxInstanceWorld) // instead of a constant bank. inline std::atomic g_rigid_ctx_world{0}; +// Sort-list piece copies published with the instance matrix (see +// g_dyn_dispatch_meshes). +inline std::atomic g_sortlist_local_pub{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 @@ -839,6 +842,17 @@ struct RigidWorldCache { uint64_t frame = 0; }; inline std::unordered_map 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 g_dyn_dispatch_meshes; // Model-space props in the world sort lists, excluded from the identity // world path (their placed copies come from the kind-2 capture). inline std::atomic g_world_props{0};