render: presentation-entity identity store + ROPA garment lifecycle
This commit is contained in:
parent
70b4e691bc
commit
e19657a45d
6 changed files with 1312 additions and 16 deletions
|
|
@ -228,6 +228,7 @@ set(SKATE3_COMMON_SOURCES
|
||||||
src/skate3_fov.cpp
|
src/skate3_fov.cpp
|
||||||
src/skate3_iso_installer.cpp
|
src/skate3_iso_installer.cpp
|
||||||
src/native/skate3_native_diagnostics.cpp
|
src/native/skate3_native_diagnostics.cpp
|
||||||
|
src/native/skate3_native_entity.cpp
|
||||||
src/native/skate3_native_guest_read.cpp
|
src/native/skate3_native_guest_read.cpp
|
||||||
src/native/skate3_native_lw.cpp
|
src/native/skate3_native_lw.cpp
|
||||||
src/native/skate3_native_v3_shadow.cpp
|
src/native/skate3_native_v3_shadow.cpp
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,16 @@ void WriteRecording(const char* dir, const char* stem) {
|
||||||
<< (d.skinned ? 1 : 0) << ",\"pending\":" << (d.pending ? 1 : 0)
|
<< (d.skinned ? 1 : 0) << ",\"pending\":" << (d.pending ? 1 : 0)
|
||||||
<< ",\"decal\":" << (d.decal ? 1 : 0)
|
<< ",\"decal\":" << (d.decal ? 1 : 0)
|
||||||
<< ",\"transparent\":" << (d.transparent ? 1 : 0)
|
<< ",\"transparent\":" << (d.transparent ? 1 : 0)
|
||||||
<< ",\"selected\":" << (d.selected ? 1 : 0) << ",\"world\":[";
|
<< ",\"selected\":" << (d.selected ? 1 : 0)
|
||||||
|
// Provenance: which pipeline path produced this item (dbg_src
|
||||||
|
// codes in skate3_native_scene.h) and the identity keys the
|
||||||
|
// offline analyzers join on.
|
||||||
|
<< ",\"ctx\":\"" << std::hex << d.ctx << std::dec
|
||||||
|
<< "\",\"dbg_src\":" << int(d.dbg_src)
|
||||||
|
<< ",\"ropa\":" << (d.ropa ? 1 : 0)
|
||||||
|
<< ",\"fam\":" << int(d.char_family)
|
||||||
|
<< ",\"caster\":" << (d.caster_bank ? 1 : 0)
|
||||||
|
<< ",\"retained\":" << (d.retained ? 1 : 0) << ",\"world\":[";
|
||||||
for (int i = 0; i < 16; ++i) out << (i ? "," : "") << d.world[i];
|
for (int i = 0; i < 16; ++i) out << (i ? "," : "") << d.world[i];
|
||||||
out << "],\"draws\":[";
|
out << "],\"draws\":[";
|
||||||
for (size_t i = 0; i < d.draws.size(); ++i) {
|
for (size_t i = 0; i < d.draws.size(); ++i) {
|
||||||
|
|
|
||||||
667
src/native/skate3_native_entity.cpp
Normal file
667
src/native/skate3_native_entity.cpp
Normal file
|
|
@ -0,0 +1,667 @@
|
||||||
|
// Presentation-entity identity store; see skate3_native_entity.h for the
|
||||||
|
// verified guest facts behind the binding model.
|
||||||
|
// The walk below mirrors sub_827A6658 (the base BindConstants)
|
||||||
|
// exactly: it re-reads the same arrays the game just walked, so a
|
||||||
|
// conforming entity registers and a non-conforming one self-limits out.
|
||||||
|
|
||||||
|
#include "native/skate3_native_entity.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
|
#include <bit>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
#include <mutex>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <rex/cvar.h>
|
||||||
|
#include <rex/logging.h>
|
||||||
|
|
||||||
|
#include "native/skate3_native_guest_read.h"
|
||||||
|
|
||||||
|
REXCVAR_DEFINE_BOOL(
|
||||||
|
skate3_native_render_scene_entity_ident, true, "Skate 3",
|
||||||
|
"Build the presentation-entity identity store (MeshContext -> owning "
|
||||||
|
"entity, from the game's BindConstants walks) and compare the entity's "
|
||||||
|
"direct fields (world transform, opacity) against the renderer's "
|
||||||
|
"captured values. Observer only: emits the ident[] stats line, serves "
|
||||||
|
"nothing.")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
|
||||||
|
REXCVAR_DEFINE_BOOL(
|
||||||
|
skate3_native_render_scene_entity_ropa_world, true, "Skate 3",
|
||||||
|
"Serve a refused ROPA garment world matrix from the owner entity's "
|
||||||
|
"m_MatLtoWTrans (+416) via the identity store, instead of leaving the "
|
||||||
|
"item pending. Only activates where the draw-time bank read refused "
|
||||||
|
"(foreign bank / post-shadow clobber / torn state); accepted bank "
|
||||||
|
"reads are bit-exact equal to this field, so the accept path is "
|
||||||
|
"unchanged. Off = refused captures stay pending (rescue/fixup paths).")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
|
||||||
|
REXCVAR_DEFINE_BOOL(
|
||||||
|
skate3_native_render_scene_entity_ropa_world_primary, true, "Skate 3",
|
||||||
|
"Serve the ROPA garment world from the owner entity's m_MatLtoWTrans "
|
||||||
|
"on ACCEPTED bank captures too (bank value kept as fallback and as a "
|
||||||
|
"live bit-compare tripwire: wprim= in the ident[] line). Value-"
|
||||||
|
"identical where the bank is healthy (observer-proven bit-exact) and "
|
||||||
|
"keeps the garment on its own entity through every bank state. "
|
||||||
|
"Lighting-row capture still follows the bank's own acceptance. Off = "
|
||||||
|
"entity world only on bank refusal.")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
|
||||||
|
namespace skate3::native_entity {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using skate3::native_scene::GuestTryCopy;
|
||||||
|
|
||||||
|
// PresentationEntity offsets (verified: sub_827A6658 / sub_82783D68 /
|
||||||
|
// sub_82793F70 / sub_827A68B0 walk + stores in the generated code).
|
||||||
|
constexpr uint32_t kEntL2W = 352; // m_MatLtoW (Matrix44)
|
||||||
|
constexpr uint32_t kEntL2WTrans = 416; // m_MatLtoWTrans (Matrix44)
|
||||||
|
constexpr uint32_t kEntOpacitySkater = 496; // skater family, x = alpha
|
||||||
|
constexpr uint32_t kEntOpacityLw = 528; // LivingWorld family
|
||||||
|
// SkaterPresEntity cloth fields (offsets validated live before the
|
||||||
|
// shadow-mode reader that proved them was retired).
|
||||||
|
constexpr uint32_t kRopaGate = 1921; // u8 m_bSkinningEnabled
|
||||||
|
constexpr uint32_t kRopaClothTarget = 1040; // u32 m_pModelClothTarget[i]
|
||||||
|
constexpr uint32_t kRopaCount = 1096; // i32 m_numClothModels
|
||||||
|
constexpr uint32_t kRopaVb = 1936; // u32 vb pair, + i*8 + cur*4
|
||||||
|
constexpr uint32_t kInstCtxArr = 0x04; // cModelInstance MeshContext*
|
||||||
|
constexpr uint32_t kInstStride = 0x28;
|
||||||
|
constexpr uint32_t kCtxStride = 0x50;
|
||||||
|
|
||||||
|
struct EntityRec {
|
||||||
|
EntClass cls = EntClass::kUnknown;
|
||||||
|
uint32_t vtable = 0;
|
||||||
|
int32_t view_refs = 0;
|
||||||
|
uint64_t bind_count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CtxEntry {
|
||||||
|
uint32_t entity = 0;
|
||||||
|
uint32_t instance = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::mutex g_mu;
|
||||||
|
std::unordered_map<uint32_t, EntityRec> g_entities;
|
||||||
|
std::unordered_map<uint32_t, CtxEntry> g_ctx;
|
||||||
|
|
||||||
|
// Deformed-VB freshness: vb object -> last DoubleBuffer completion (ms).
|
||||||
|
// Guarded by its own lock (the sim thread stamps at cloth-tick rate; the
|
||||||
|
// render thread reads per serve).
|
||||||
|
std::mutex g_vb_mu;
|
||||||
|
std::unordered_map<uint32_t, uint64_t> g_vb_fresh;
|
||||||
|
|
||||||
|
// Telemetry (600-frame window, exchanged to zero at emission).
|
||||||
|
std::atomic<uint32_t> g_bind_calls{0};
|
||||||
|
std::atomic<uint32_t> g_add_events{0};
|
||||||
|
std::atomic<uint32_t> g_rmv_events{0};
|
||||||
|
std::atomic<uint32_t> g_item_hit{0};
|
||||||
|
std::atomic<uint32_t> g_item_miss{0};
|
||||||
|
std::atomic<uint32_t> g_item_lw_overlap{0};
|
||||||
|
std::atomic<uint32_t> g_fade_ok{0};
|
||||||
|
std::atomic<uint32_t> g_fade_div{0};
|
||||||
|
std::atomic<uint32_t> g_fade_bad{0};
|
||||||
|
// ROPA world vs entity matrices: [0]=e+416 (transposed), [1]=e+352 (raw);
|
||||||
|
// exact / near / divergent, plus unmapped ctx and unreadable entity.
|
||||||
|
std::atomic<uint32_t> g_w_exact[2]{};
|
||||||
|
std::atomic<uint32_t> g_w_near[2]{};
|
||||||
|
std::atomic<uint32_t> g_w_div[2]{};
|
||||||
|
std::atomic<uint32_t> g_ropa_unmap{0};
|
||||||
|
std::atomic<uint32_t> g_ropa_read_fail{0};
|
||||||
|
// Serve flip 1: entity-world serves on bank refusal / structural rejects /
|
||||||
|
// mode-authority refusals (the garment table does not claim the VB as a
|
||||||
|
// live CPU cloth target; asserting rigid there would flip the mode).
|
||||||
|
std::atomic<uint32_t> g_wserve{0};
|
||||||
|
std::atomic<uint32_t> g_wserve_rej{0};
|
||||||
|
std::atomic<uint32_t> g_wserve_nomode{0};
|
||||||
|
// Primary-serve tripwire: serves on accepted captures / bank divergences.
|
||||||
|
std::atomic<uint32_t> g_wprim{0};
|
||||||
|
std::atomic<uint32_t> g_wprim_div{0};
|
||||||
|
// Per-class item hits: Unknown/LW/Skater/Colorized/Cac/SkaterAux.
|
||||||
|
std::atomic<uint32_t> g_hit_cls[6]{};
|
||||||
|
|
||||||
|
std::atomic<uint32_t> g_frame_counter{0};
|
||||||
|
|
||||||
|
bool LoadU32(uint8_t* base, uint32_t addr, uint32_t* out) {
|
||||||
|
if (addr < 0x10000) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint32_t raw;
|
||||||
|
if (!GuestTryCopy(&raw, base + addr, 4)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*out = __builtin_bswap32(raw);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LoadF32Block(uint8_t* base, uint32_t addr, float* out, uint32_t count) {
|
||||||
|
if (addr < 0x10000) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint32_t raw[16];
|
||||||
|
if (count > 16 || !GuestTryCopy(raw, base + addr, size_t(count) * 4)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
|
out[i] = std::bit_cast<float>(__builtin_bswap32(raw[i]));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PlausiblePtr(uint32_t p) {
|
||||||
|
return p >= 0x10000 && p < 0x84A10000 && (p & 3) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t NowMs() {
|
||||||
|
return uint64_t(std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch())
|
||||||
|
.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* ClassName(EntClass cls) {
|
||||||
|
switch (cls) {
|
||||||
|
case EntClass::kLivingWorld: return "lw";
|
||||||
|
case EntClass::kSkater: return "skater";
|
||||||
|
case EntClass::kColorized: return "colorized";
|
||||||
|
case EntClass::kCac: return "cac";
|
||||||
|
case EntClass::kSkaterAux: return "skater-aux";
|
||||||
|
default: return "base";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare 3 staged rows (translation in component 3) against the first 3
|
||||||
|
// rows of a guest Matrix44. 0 = exact, 1 = near (within one sim step),
|
||||||
|
// 2 = divergent.
|
||||||
|
int CompareRows(const float rows[12], const float mem[12]) {
|
||||||
|
bool exact = true;
|
||||||
|
bool near_ok = true;
|
||||||
|
for (int r = 0; r < 3; ++r) {
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
const float d = std::fabs(rows[r * 4 + i] - mem[r * 4 + i]);
|
||||||
|
const float tol_near = (i == 3) ? 0.5f : 0.05f;
|
||||||
|
if (d > 1e-4f) {
|
||||||
|
exact = false;
|
||||||
|
}
|
||||||
|
if (d > tol_near) {
|
||||||
|
near_ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return exact ? 0 : (near_ok ? 1 : 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void OnBindConstants(uint8_t* base, uint32_t entity) {
|
||||||
|
if (!REXCVAR_GET(skate3_native_render_scene_entity_ident) || entity == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_bind_calls.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
uint32_t vtable = 0, idx = 0, count = 0, arr = 0;
|
||||||
|
if (!LoadU32(base, entity, &vtable) || !LoadU32(base, entity + 16, &idx) ||
|
||||||
|
idx > 1024 || !LoadU32(base, entity + (idx + 5) * 4, &count) ||
|
||||||
|
!LoadU32(base, entity + (idx + 8) * 4, &arr) || count == 0 ||
|
||||||
|
count > 32 || !PlausiblePtr(arr)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
struct CtxRec {
|
||||||
|
uint32_t ctx;
|
||||||
|
uint32_t instance;
|
||||||
|
};
|
||||||
|
CtxRec ctxs[32 * 64];
|
||||||
|
uint32_t nctx = 0;
|
||||||
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
|
const uint32_t rec = arr + i * kInstStride;
|
||||||
|
uint32_t cbegin = 0, cend = 0;
|
||||||
|
if (!LoadU32(base, rec + kInstCtxArr, &cbegin) ||
|
||||||
|
!LoadU32(base, rec + kInstCtxArr + 4, &cend) ||
|
||||||
|
!PlausiblePtr(cbegin) || cend <= cbegin ||
|
||||||
|
(cend - cbegin) % kCtxStride != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const uint32_t slots = (cend - cbegin) / kCtxStride;
|
||||||
|
if (slots > 64) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (uint32_t k = 0; k < slots && nctx < 32 * 64; ++k) {
|
||||||
|
ctxs[nctx++] = {cbegin + k * kCtxStride, rec};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nctx == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::lock_guard<std::mutex> lock(g_mu);
|
||||||
|
// Growth backstop: binds are one-shot per entity, so dead entries never
|
||||||
|
// refresh; drop view-dead entities' ctxs when the map gets large.
|
||||||
|
if (g_ctx.size() > 16384) {
|
||||||
|
for (auto it = g_ctx.begin(); it != g_ctx.end();) {
|
||||||
|
const auto ent = g_entities.find(it->second.entity);
|
||||||
|
const bool dead = ent == g_entities.end() || ent->second.view_refs <= 0;
|
||||||
|
it = dead ? g_ctx.erase(it) : std::next(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (g_entities.size() > 8192) {
|
||||||
|
for (auto it = g_entities.begin(); it != g_entities.end();) {
|
||||||
|
it = it->second.view_refs <= 0 ? g_entities.erase(it) : std::next(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EntityRec& er = g_entities[entity];
|
||||||
|
er.vtable = vtable;
|
||||||
|
er.bind_count++;
|
||||||
|
for (uint32_t i = 0; i < nctx; ++i) {
|
||||||
|
g_ctx[ctxs[i].ctx] = {entity, ctxs[i].instance};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnBindClass(uint32_t entity, EntClass cls) {
|
||||||
|
if (!REXCVAR_GET(skate3_native_render_scene_entity_ident) || entity == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::lock_guard<std::mutex> lock(g_mu);
|
||||||
|
const auto it = g_entities.find(entity);
|
||||||
|
if (it == g_entities.end()) {
|
||||||
|
return; // base walk failed its guards; keep the entity out entirely
|
||||||
|
}
|
||||||
|
// Most-derived override returns last, so plain assignment converges.
|
||||||
|
it->second.cls = cls;
|
||||||
|
// One capped line per distinct (vtable, class) pair: the live class
|
||||||
|
// table, for promoting vtable-based classification later.
|
||||||
|
static std::unordered_map<uint64_t, bool> s_seen;
|
||||||
|
const uint64_t key = (uint64_t(it->second.vtable) << 8) | uint8_t(cls);
|
||||||
|
if (s_seen.size() < 64 && s_seen.emplace(key, true).second) {
|
||||||
|
REXLOG_INFO("native-entity: class {} vtbl={:08X} entity={:08X}",
|
||||||
|
ClassName(cls), it->second.vtable, entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnEntityViewAdd(uint32_t entity) {
|
||||||
|
if (!REXCVAR_GET(skate3_native_render_scene_entity_ident) || entity == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_add_events.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
std::lock_guard<std::mutex> lock(g_mu);
|
||||||
|
// Entities can register with views before their first bind; the record
|
||||||
|
// gets its ctxs (and class) when BindConstants fires.
|
||||||
|
g_entities[entity].view_refs++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnRopaDoubleBuffer(uint8_t* base, uint32_t skater, uint32_t index) {
|
||||||
|
if (!REXCVAR_GET(skate3_native_render_scene_entity_ident) || skater == 0 ||
|
||||||
|
index >= 8) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t vb0 = 0, vb1 = 0;
|
||||||
|
if (!LoadU32(base, skater + kRopaVb + index * 8, &vb0) ||
|
||||||
|
!LoadU32(base, skater + kRopaVb + index * 8 + 4, &vb1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uint64_t now = NowMs();
|
||||||
|
std::lock_guard<std::mutex> lock(g_vb_mu);
|
||||||
|
if (g_vb_fresh.size() > 512) {
|
||||||
|
for (auto it = g_vb_fresh.begin(); it != g_vb_fresh.end();) {
|
||||||
|
it = now - it->second > 2000 ? g_vb_fresh.erase(it) : std::next(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (vb0 != 0) {
|
||||||
|
g_vb_fresh[vb0] = now;
|
||||||
|
}
|
||||||
|
if (vb1 != 0) {
|
||||||
|
g_vb_fresh[vb1] = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnEntityViewRemove(uint32_t entity) {
|
||||||
|
if (!REXCVAR_GET(skate3_native_render_scene_entity_ident) || entity == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_rmv_events.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
std::lock_guard<std::mutex> lock(g_mu);
|
||||||
|
const auto it = g_entities.find(entity);
|
||||||
|
if (it != g_entities.end()) {
|
||||||
|
it->second.view_refs = std::max(0, it->second.view_refs - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LookupCtx(uint32_t ctx, CtxInfo* out) {
|
||||||
|
if (ctx == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::lock_guard<std::mutex> lock(g_mu);
|
||||||
|
const auto it = g_ctx.find(ctx);
|
||||||
|
if (it == g_ctx.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (out != nullptr) {
|
||||||
|
out->entity = it->second.entity;
|
||||||
|
out->instance = it->second.instance;
|
||||||
|
const auto ent = g_entities.find(it->second.entity);
|
||||||
|
out->cls = ent != g_entities.end() ? ent->second.cls : EntClass::kUnknown;
|
||||||
|
out->view_live = ent != g_entities.end() && ent->second.view_refs > 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObserveRopaWorld(uint8_t* base, uint32_t ctx, const float rows[12]) {
|
||||||
|
if (!REXCVAR_GET(skate3_native_render_scene_entity_ident)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CtxInfo info;
|
||||||
|
if (!LookupCtx(ctx, &info)) {
|
||||||
|
g_ropa_unmap.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float m416[12], m352[12];
|
||||||
|
const bool ok416 = LoadF32Block(base, info.entity + kEntL2WTrans, m416, 12);
|
||||||
|
const bool ok352 = LoadF32Block(base, info.entity + kEntL2W, m352, 12);
|
||||||
|
if (!ok416 || !ok352) {
|
||||||
|
g_ropa_read_fail.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int c416 = CompareRows(rows, m416);
|
||||||
|
const int c352 = CompareRows(rows, m352);
|
||||||
|
(c416 == 0 ? g_w_exact[0] : c416 == 1 ? g_w_near[0] : g_w_div[0])
|
||||||
|
.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
(c352 == 0 ? g_w_exact[1] : c352 == 1 ? g_w_near[1] : g_w_div[1])
|
||||||
|
.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (c416 == 2 && c352 == 2) {
|
||||||
|
static std::atomic<uint32_t> s_logged{0};
|
||||||
|
const uint32_t ln = s_logged.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (ln < 8 || (ln & 1023u) == 0) {
|
||||||
|
REXLOG_INFO(
|
||||||
|
"native-entity: ropa world DIVERGES both fields ctx={:08X} "
|
||||||
|
"cls={} entity={:08X} bank=({:.3f},{:.3f},{:.3f},{:.2f}) "
|
||||||
|
"e416=({:.3f},{:.3f},{:.3f},{:.2f}) "
|
||||||
|
"e352=({:.3f},{:.3f},{:.3f},{:.2f}) (n={})",
|
||||||
|
ctx, ClassName(info.cls), info.entity, rows[0], rows[1], rows[2],
|
||||||
|
rows[3], m416[0], m416[1], m416[2], m416[3], m352[0], m352[1],
|
||||||
|
m352[2], m352[3], ln);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObserveCharItem(uint8_t* base, uint32_t ctx, uint32_t family,
|
||||||
|
bool lw_mapped, float used_alpha) {
|
||||||
|
if (!REXCVAR_GET(skate3_native_render_scene_entity_ident)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CtxInfo info;
|
||||||
|
if (!LookupCtx(ctx, &info)) {
|
||||||
|
g_item_miss.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_item_hit.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
g_hit_cls[std::min<uint32_t>(uint32_t(info.cls), 5u)].fetch_add(
|
||||||
|
1, std::memory_order_relaxed);
|
||||||
|
if (lw_mapped) {
|
||||||
|
g_item_lw_overlap.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
const bool skater_family =
|
||||||
|
info.cls == EntClass::kSkater || info.cls == EntClass::kColorized ||
|
||||||
|
info.cls == EntClass::kCac || info.cls == EntClass::kSkaterAux;
|
||||||
|
if (!skater_family) {
|
||||||
|
return; // LW opacity is already proven/served; base classes untracked
|
||||||
|
}
|
||||||
|
float alpha = 0.0f;
|
||||||
|
if (!LoadF32Block(base, info.entity + kEntOpacitySkater, &alpha, 1) ||
|
||||||
|
!(alpha >= -0.01f && alpha <= 1.01f)) {
|
||||||
|
g_fade_bad.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (std::fabs(alpha - used_alpha) <= 0.05f) {
|
||||||
|
g_fade_ok.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
} else {
|
||||||
|
g_fade_div.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
static std::atomic<uint32_t> s_logged{0};
|
||||||
|
const uint32_t ln = s_logged.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (ln < 8 || (ln & 2047u) == 0) {
|
||||||
|
REXLOG_INFO(
|
||||||
|
"native-entity: fade divergence ctx={:08X} cls={} fam={} "
|
||||||
|
"e496={:.3f} used={:.3f} (n={})",
|
||||||
|
ctx, ClassName(info.cls), family, alpha, used_alpha, ln);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NoteAcceptedWorldCompare(const float bank_rows[12],
|
||||||
|
const float ent_rows[12]) {
|
||||||
|
g_wprim.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
for (int i = 0; i < 12; ++i) {
|
||||||
|
if (std::fabs(bank_rows[i] - ent_rows[i]) > 1e-4f) {
|
||||||
|
g_wprim_div.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
static std::atomic<uint32_t> s_logged{0};
|
||||||
|
const uint32_t ln = s_logged.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (ln < 8 || (ln & 1023u) == 0) {
|
||||||
|
REXLOG_INFO(
|
||||||
|
"native-entity: PRIMARY world diverges from accepted bank "
|
||||||
|
"i={} bank={:.4f} ent={:.4f} (n={})",
|
||||||
|
i, bank_rows[i], ent_rows[i], ln);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Shared structural validation: 16-float Matrix44 with a (0,0,0,1) tail
|
||||||
|
// row and bank-gate row norms; copies the 3 affine rows on success.
|
||||||
|
bool ReadWorldRowsChecked(uint8_t* base, uint32_t entity, float out_rows[12]) {
|
||||||
|
float m[16];
|
||||||
|
if (!LoadF32Block(base, entity + kEntL2WTrans, m, 16)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const bool tail_ok = std::fabs(m[12]) <= 1e-4f && std::fabs(m[13]) <= 1e-4f &&
|
||||||
|
std::fabs(m[14]) <= 1e-4f &&
|
||||||
|
std::fabs(m[15] - 1.0f) <= 1e-4f;
|
||||||
|
bool rows_ok = tail_ok;
|
||||||
|
for (int r = 0; r < 3 && rows_ok; ++r) {
|
||||||
|
float n = 0.0f;
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
const float f = m[r * 4 + i];
|
||||||
|
rows_ok = rows_ok && f > -1e7f && f < 1e7f;
|
||||||
|
n += f * f;
|
||||||
|
}
|
||||||
|
rows_ok = rows_ok && n > 0.0025f && n < 400.0f && m[r * 4 + 3] > -20000.f &&
|
||||||
|
m[r * 4 + 3] < 20000.f;
|
||||||
|
}
|
||||||
|
if (!rows_ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::memcpy(out_rows, m, 12 * sizeof(float));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool ReadEntityWorldRows(uint8_t* base, uint32_t ctx, float out_rows[12]) {
|
||||||
|
CtxInfo info;
|
||||||
|
return LookupCtx(ctx, &info) &&
|
||||||
|
ReadWorldRowsChecked(base, info.entity, out_rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t ServeInstancePalette(uint8_t* base, uint32_t ctx, float* out,
|
||||||
|
uint32_t max_rows) {
|
||||||
|
CtxInfo info;
|
||||||
|
if (!LookupCtx(ctx, &info) || info.instance == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
uint32_t matrices = 0, count = 0;
|
||||||
|
if (!LoadU32(base, info.instance + 0x14, &matrices) ||
|
||||||
|
!LoadU32(base, info.instance + 0x18, &count) || !PlausiblePtr(matrices) ||
|
||||||
|
count < 1 || count > 96 || count > max_rows) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
uint32_t raw[96 * 12];
|
||||||
|
if (!GuestTryCopy(raw, base + matrices, size_t(count) * 48)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < count * 12; ++i) {
|
||||||
|
out[i] = std::bit_cast<float>(__builtin_bswap32(raw[i]));
|
||||||
|
}
|
||||||
|
// Row sanity (packed 3x4 rotation norms in the bank-gate range): a
|
||||||
|
// stale/unpacked buffer fails and the caller keeps its fallback.
|
||||||
|
for (uint32_t r = 0; r < count; ++r) {
|
||||||
|
float n = 0.0f;
|
||||||
|
for (int j = 0; j < 3; ++j) {
|
||||||
|
const float* m = out + r * 12;
|
||||||
|
n += m[j] * m[j] + m[4 + j] * m[4 + j] + m[8 + j] * m[8 + j];
|
||||||
|
}
|
||||||
|
if (!(n > 0.0016f && n < 2000.0f)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RopaGarmentDropped(uint8_t* base, uint32_t ctx, uint32_t vb_obj) {
|
||||||
|
if (vb_obj == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CtxInfo info;
|
||||||
|
if (!LookupCtx(ctx, &info)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint32_t gate_w = 0, n = 0;
|
||||||
|
if (!LoadU32(base, info.entity + (kRopaGate & ~3u), &gate_w) ||
|
||||||
|
!LoadU32(base, info.entity + kRopaCount, &n) || n > 8) {
|
||||||
|
return false; // unreadable/implausible: never suppress on uncertainty
|
||||||
|
}
|
||||||
|
const uint8_t gate = uint8_t(gate_w >> ((3 - (kRopaGate & 3u)) * 8));
|
||||||
|
if (gate != 0 && n >= 1) {
|
||||||
|
for (uint32_t i = 0; i < n; ++i) {
|
||||||
|
uint32_t target = 0, vb0 = 0, vb1 = 0;
|
||||||
|
if (LoadU32(base, info.entity + kRopaClothTarget + i * 4, &target) &&
|
||||||
|
target != 0 &&
|
||||||
|
LoadU32(base, info.entity + kRopaVb + i * 8, &vb0) &&
|
||||||
|
LoadU32(base, info.entity + kRopaVb + i * 8 + 4, &vb1) &&
|
||||||
|
(vb0 == vb_obj || vb1 == vb_obj)) {
|
||||||
|
return false; // still a live cloth target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true; // mapped entity, garment table does not claim this vb
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ServeRopaWorld(uint8_t* base, uint32_t ctx, uint32_t vb_obj,
|
||||||
|
float out_rows[12]) {
|
||||||
|
if (!REXCVAR_GET(skate3_native_render_scene_entity_ropa_world)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
CtxInfo info;
|
||||||
|
if (!LookupCtx(ctx, &info)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Mode authority: the entity's own garment table must claim this VB as
|
||||||
|
// a live CPU cloth target this tick. Sim-inactive garments (distance/
|
||||||
|
// activity toggles) draw SKINNED; serving them a rigid world would
|
||||||
|
// flip the mode against the post-draw fixup every frame.
|
||||||
|
bool cpu_garment = false;
|
||||||
|
{
|
||||||
|
uint8_t gate = 0;
|
||||||
|
uint32_t n = 0;
|
||||||
|
uint32_t gate_w = 0;
|
||||||
|
if (vb_obj != 0 &&
|
||||||
|
LoadU32(base, info.entity + (kRopaGate & ~3u), &gate_w) &&
|
||||||
|
LoadU32(base, info.entity + kRopaCount, &n) && n >= 1 && n <= 8) {
|
||||||
|
gate = uint8_t(gate_w >> ((3 - (kRopaGate & 3u)) * 8));
|
||||||
|
for (uint32_t i = 0; i < n && !cpu_garment && gate != 0; ++i) {
|
||||||
|
uint32_t target = 0, vb0 = 0, vb1 = 0;
|
||||||
|
cpu_garment =
|
||||||
|
LoadU32(base, info.entity + kRopaClothTarget + i * 4, &target) &&
|
||||||
|
target != 0 &&
|
||||||
|
LoadU32(base, info.entity + kRopaVb + i * 8, &vb0) &&
|
||||||
|
LoadU32(base, info.entity + kRopaVb + i * 8 + 4, &vb1) &&
|
||||||
|
(vb0 == vb_obj || vb1 == vb_obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Tick freshness: the garment-table fields persist after the game stops
|
||||||
|
// running the cloth job (sleeping/paused sims keep their last target and
|
||||||
|
// VB pointers), so the table alone misclassifies a sleeping garment as
|
||||||
|
// CPU-simulated, and its VB holds a STALE drape (the floating shirt).
|
||||||
|
// DoubleBuffer runs once per garment per COMPLETED sim tick; require it
|
||||||
|
// within the last few ticks.
|
||||||
|
if (cpu_garment) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_vb_mu);
|
||||||
|
const auto it = g_vb_fresh.find(vb_obj);
|
||||||
|
cpu_garment = it != g_vb_fresh.end() && NowMs() - it->second <= 150;
|
||||||
|
}
|
||||||
|
if (!cpu_garment) {
|
||||||
|
g_wserve_nomode.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Structural proof of a live Matrix44 ((0,0,0,1) tail + bank-gate row
|
||||||
|
// norms): a despawned entity's recycled memory does not pass both.
|
||||||
|
if (!ReadWorldRowsChecked(base, info.entity, out_rows)) {
|
||||||
|
g_wserve_rej.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
g_wserve.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
static std::atomic<uint32_t> s_logged{0};
|
||||||
|
const uint32_t ln = s_logged.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (ln < 8 || (ln & 2047u) == 0) {
|
||||||
|
REXLOG_INFO(
|
||||||
|
"native-entity: ropa world SERVED ctx={:08X} cls={} entity={:08X} "
|
||||||
|
"t=({:.2f},{:.2f},{:.2f}) (n={})",
|
||||||
|
ctx, ClassName(info.cls), info.entity, out_rows[3], out_rows[7],
|
||||||
|
out_rows[11], ln);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitStats() {
|
||||||
|
if (!REXCVAR_GET(skate3_native_render_scene_entity_ident)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uint32_t frame =
|
||||||
|
g_frame_counter.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (frame == 0 || frame % 600 != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t ents = 0, ctxs = 0, live = 0;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(g_mu);
|
||||||
|
ents = g_entities.size();
|
||||||
|
ctxs = g_ctx.size();
|
||||||
|
for (const auto& [addr, rec] : g_entities) {
|
||||||
|
live += rec.view_refs > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
REXLOG_INFO(
|
||||||
|
"native-entity: ident[ents={}/{} ctx={} bind={} add={} rmv={} "
|
||||||
|
"hit={} miss={} lw_ovl={} cls(b/lw/sk/co/cac/aux)={}/{}/{}/{}/{}/{} "
|
||||||
|
"fade ok/div/bad={}/{}/{} w416 e/n/d={}/{}/{} w352 e/n/d={}/{}/{} "
|
||||||
|
"ropa unmap={} rfail={} wserve={}/{}/{} wprim={}/{}]",
|
||||||
|
live, ents, ctxs, g_bind_calls.exchange(0, std::memory_order_relaxed),
|
||||||
|
g_add_events.exchange(0, std::memory_order_relaxed),
|
||||||
|
g_rmv_events.exchange(0, std::memory_order_relaxed),
|
||||||
|
g_item_hit.exchange(0, std::memory_order_relaxed),
|
||||||
|
g_item_miss.exchange(0, std::memory_order_relaxed),
|
||||||
|
g_item_lw_overlap.exchange(0, std::memory_order_relaxed),
|
||||||
|
g_hit_cls[0].exchange(0, std::memory_order_relaxed),
|
||||||
|
g_hit_cls[1].exchange(0, std::memory_order_relaxed),
|
||||||
|
g_hit_cls[2].exchange(0, std::memory_order_relaxed),
|
||||||
|
g_hit_cls[3].exchange(0, std::memory_order_relaxed),
|
||||||
|
g_hit_cls[4].exchange(0, std::memory_order_relaxed),
|
||||||
|
g_hit_cls[5].exchange(0, std::memory_order_relaxed),
|
||||||
|
g_fade_ok.exchange(0, std::memory_order_relaxed),
|
||||||
|
g_fade_div.exchange(0, std::memory_order_relaxed),
|
||||||
|
g_fade_bad.exchange(0, std::memory_order_relaxed),
|
||||||
|
g_w_exact[0].exchange(0, std::memory_order_relaxed),
|
||||||
|
g_w_near[0].exchange(0, std::memory_order_relaxed),
|
||||||
|
g_w_div[0].exchange(0, std::memory_order_relaxed),
|
||||||
|
g_w_exact[1].exchange(0, std::memory_order_relaxed),
|
||||||
|
g_w_near[1].exchange(0, std::memory_order_relaxed),
|
||||||
|
g_w_div[1].exchange(0, std::memory_order_relaxed),
|
||||||
|
g_ropa_unmap.exchange(0, std::memory_order_relaxed),
|
||||||
|
g_ropa_read_fail.exchange(0, std::memory_order_relaxed),
|
||||||
|
g_wserve.exchange(0, std::memory_order_relaxed),
|
||||||
|
g_wserve_rej.exchange(0, std::memory_order_relaxed),
|
||||||
|
g_wserve_nomode.exchange(0, std::memory_order_relaxed),
|
||||||
|
g_wprim.exchange(0, std::memory_order_relaxed),
|
||||||
|
g_wprim_div.exchange(0, std::memory_order_relaxed));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace skate3::native_entity
|
||||||
151
src/native/skate3_native_entity.h
Normal file
151
src/native/skate3_native_entity.h
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Presentation-entity identity store:
|
||||||
|
// draw-context -> owning entity for EVERY Sk8::PresentationEntity subclass,
|
||||||
|
// built from the game's own constant-binding walk.
|
||||||
|
//
|
||||||
|
// Write point: Sk8::PresentationEntity::BindConstants (sub_827A6658) EXIT.
|
||||||
|
// The game binds shader constants BY POINTER, once, near entity
|
||||||
|
// construction: the base bind walks LOD -> cModelInstance (stride 0x28) ->
|
||||||
|
// MeshContext (stride 0x50) and pushes {hash, pointer, count} records into
|
||||||
|
// each mesh's ShaderSystem param scope; cScope::DispatchParams dereferences
|
||||||
|
// them at draw time to fill the constant registers. Every subclass bind
|
||||||
|
// (skater / colorized / CAC / LivingWorld / props) calls the base, so one
|
||||||
|
// exit hook sees every renderable entity's full ctx set with the entity in
|
||||||
|
// r3: the identity link the draw-time constant bank cannot provide.
|
||||||
|
//
|
||||||
|
// Verified bindings (generated code, instruction level):
|
||||||
|
// - base pushes hash 0x7DDF552B -> &entity+416 (m_MatLtoWTrans, 4 vec4s)
|
||||||
|
// = the ROPA/rigid world block staged at c191..c194 (main pass) /
|
||||||
|
// c188..c191 (shadow pass).
|
||||||
|
// - SkaterPresEntity::BindConstants (sub_82783D68) additionally pushes
|
||||||
|
// hash 0xD5DF94E7 -> &entity+496 (opacity vector, x = alpha): the same
|
||||||
|
// hash the LivingWorld classes bind at entity+528.
|
||||||
|
// - CACPresEntity::BindConstants (sub_82793F70) RE-pushes 0x7DDF552B ->
|
||||||
|
// &entity+352 (m_MatLtoW, UNtransposed); CAC draws stage a transposed
|
||||||
|
// block relative to every other class.
|
||||||
|
//
|
||||||
|
// This module is currently an OBSERVER: it builds the store and compares
|
||||||
|
// the entity's direct fields against the values the capture heuristics
|
||||||
|
// decided on, without serving anything. Serving flips come one at a time
|
||||||
|
// once the live telemetry proves each field (ident[] stats line).
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace skate3::native_entity {
|
||||||
|
|
||||||
|
// Which BindConstants override reached the base walk, most-derived wins
|
||||||
|
// (the derived binds run after the base within one virtual call).
|
||||||
|
enum class EntClass : uint8_t {
|
||||||
|
kUnknown = 0, // base bind only (world props, misc presentation)
|
||||||
|
kLivingWorld, // sub_827C1720 (already served by the LW entity store)
|
||||||
|
kSkater, // sub_82783D68 (Sk8::SkaterPresEntity)
|
||||||
|
kColorized, // sub_82785260 (ColorizedSkaterPresEntity)
|
||||||
|
kCac, // sub_82793F70 (CACPresEntity, untransposed world rebind)
|
||||||
|
kSkaterAux, // sub_82785528 (unnamed class with the skater layout)
|
||||||
|
};
|
||||||
|
|
||||||
|
// sub_827A6658 EXIT (guest thread): register every MeshContext the walk
|
||||||
|
// just bound against the owning entity. Idempotent; re-registration on
|
||||||
|
// LOD/model changes refreshes the mapping.
|
||||||
|
void OnBindConstants(uint8_t* base, uint32_t entity);
|
||||||
|
|
||||||
|
// Derived-bind EXIT: tag the entity's class (most-derived override fires
|
||||||
|
// last, so plain assignment converges on the correct class).
|
||||||
|
void OnBindClass(uint32_t entity, EntClass cls);
|
||||||
|
|
||||||
|
// RenderPresentation::AddEntityToRenderViews / RmvEntityFrmRenderViews
|
||||||
|
// (sub_827A6C50 / sub_827A6CE8, r4 = entity): per-entity view refcount,
|
||||||
|
// the direct scene-membership/lifetime signal.
|
||||||
|
void OnEntityViewAdd(uint32_t entity);
|
||||||
|
void OnEntityViewRemove(uint32_t entity);
|
||||||
|
|
||||||
|
// SkaterPresEntity::DoubleBuffer EXIT (sub_82783038, r3 = skater,
|
||||||
|
// r4 = garment index): runs exactly once per garment per completed cloth
|
||||||
|
// sim tick: the authoritative "the sim wrote this VB this tick" signal.
|
||||||
|
// Stamps both double-buffer halves fresh. The garment-table fields
|
||||||
|
// (m_pModelClothTarget etc.) PERSIST after the game stops running the
|
||||||
|
// cloth job for an entity, so field checks alone misclassify a sleeping
|
||||||
|
// garment as CPU-simulated and draw its last (stale, mid-pose) deformed
|
||||||
|
// VB rigidly, the floating/perpendicular shirt.
|
||||||
|
void OnRopaDoubleBuffer(uint8_t* base, uint32_t skater, uint32_t index);
|
||||||
|
|
||||||
|
struct CtxInfo {
|
||||||
|
uint32_t entity = 0;
|
||||||
|
uint32_t instance = 0; // owning cModelInstance (m_matrices at +0x14)
|
||||||
|
EntClass cls = EntClass::kUnknown;
|
||||||
|
bool view_live = false; // Add/Rmv refcount > 0
|
||||||
|
};
|
||||||
|
bool LookupCtx(uint32_t ctx, CtxInfo* out);
|
||||||
|
|
||||||
|
// Observer probes (render thread, called from the scene builder).
|
||||||
|
//
|
||||||
|
// Accepted ROPA rigid world vs the entity's own matrix fields: rows =
|
||||||
|
// the 3 accepted bank rows (c-register layout, translation in component
|
||||||
|
// 3), compared against entity+416 and entity+352.
|
||||||
|
void ObserveRopaWorld(uint8_t* base, uint32_t ctx, const float rows[12]);
|
||||||
|
|
||||||
|
// Per published character item: identity coverage (hit/miss/LW overlap)
|
||||||
|
// and, for skater-family entities, entity+496 opacity vs the alpha the
|
||||||
|
// renderer decided to use.
|
||||||
|
void ObserveCharItem(uint8_t* base, uint32_t ctx, uint32_t family,
|
||||||
|
bool lw_mapped, float used_alpha);
|
||||||
|
|
||||||
|
// Serve flip 1: the owner entity's world matrix for a ROPA garment whose
|
||||||
|
// draw-time bank read REFUSED (foreign bank, post-shadow tail clobber,
|
||||||
|
// torn state - the pending-forever/invisible classes). The observer run
|
||||||
|
// proved the accepted bank block is a bit-exact dereference of
|
||||||
|
// entity+416 (m_MatLtoWTrans), so the entity field serves the same
|
||||||
|
// matrix through every bank state.
|
||||||
|
//
|
||||||
|
// MODE AUTHORITY: serving a rigid world implicitly asserts the garment is
|
||||||
|
// CPU-deformed, and on a refused bank the flag row proves nothing, so
|
||||||
|
// the serve additionally requires the owning entity's own garment table
|
||||||
|
// to claim this VB as a live cloth target (m_bSkinningEnabled &&
|
||||||
|
// m_pModelClothTarget[i] != 0 && vb_obj is one of that garment's
|
||||||
|
// double-buffer halves). Without that proof the caller keeps its refusal
|
||||||
|
// path (pending + post-draw fixup, which re-reads the authentic flag
|
||||||
|
// from the mesh's own draw and can resolve the item SKINNED).
|
||||||
|
//
|
||||||
|
// Fills rows in the staged c-register layout (3 rows, translation in
|
||||||
|
// component 3) after structural checks (homogeneous (0,0,0,1) tail row +
|
||||||
|
// row-norm sanity).
|
||||||
|
bool ServeRopaWorld(uint8_t* base, uint32_t ctx, uint32_t vb_obj,
|
||||||
|
float out_rows[12]);
|
||||||
|
|
||||||
|
// True when the ctx maps to an entity whose garment table no longer
|
||||||
|
// claims vb_obj as a live cloth target; the game DROPPED the garment
|
||||||
|
// (m_numClothModels back to 0, VB objects freed, memory recycled). The
|
||||||
|
// entity itself stays alive and its ctxs keep being submitted (drawn
|
||||||
|
// skinned by the game's own sim-inactive path), so any rigid ropa state
|
||||||
|
// our caches still hold for the vb is a ghost: a retained mid-sim drape
|
||||||
|
// paired with a live world tracks the character as a floating garment.
|
||||||
|
// Unmapped ctx / unreadable fields return false (never suppress on
|
||||||
|
// uncertainty).
|
||||||
|
bool RopaGarmentDropped(uint8_t* base, uint32_t ctx, uint32_t vb_obj);
|
||||||
|
|
||||||
|
// The mapped instance's live packed palette (m_matrices, 12 floats/row),
|
||||||
|
// for resolving a dropped-garment's skinned draw when bank capture fails
|
||||||
|
// chronically. Returns the row count copied (0 = unmapped/unreadable).
|
||||||
|
uint32_t ServeInstancePalette(uint8_t* base, uint32_t ctx, float* out,
|
||||||
|
uint32_t max_rows);
|
||||||
|
|
||||||
|
// Quiet variant for per-render-frame checks (no counters, no logs, no
|
||||||
|
// garment-table gate): the mapped owner entity's live world block in the
|
||||||
|
// staged c-register layout, with the same structural sanity checks.
|
||||||
|
// Used by the post-interpolation identity anchor; the ring lerp can only
|
||||||
|
// be validated against the entity, since both the published scene and the
|
||||||
|
// guest draws already carried the correct matrix.
|
||||||
|
bool ReadEntityWorldRows(uint8_t* base, uint32_t ctx, float out_rows[12]);
|
||||||
|
|
||||||
|
// Tripwire for the primary world serve: on ACCEPTED bank captures the
|
||||||
|
// entity rows are proven bit-exact equal to the bank block; count any
|
||||||
|
// divergence (a nonzero count means the mapping or read point drifted,
|
||||||
|
// and shows up in the ident[] line as wprim=<serves>/<divergences>).
|
||||||
|
void NoteAcceptedWorldCompare(const float bank_rows[12],
|
||||||
|
const float ent_rows[12]);
|
||||||
|
|
||||||
|
// Rate-limited ident[] stats line; call once per built frame.
|
||||||
|
void EmitStats();
|
||||||
|
|
||||||
|
} // namespace skate3::native_entity
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "skate3_native_render.h"
|
#include "skate3_native_render.h"
|
||||||
|
|
||||||
|
#include "native/skate3_native_entity.h"
|
||||||
#include "native/skate3_native_lw.h"
|
#include "native/skate3_native_lw.h"
|
||||||
#include "native/skate3_native_v3_shadow.h"
|
#include "native/skate3_native_v3_shadow.h"
|
||||||
#include "skate3_native_scene.h"
|
#include "skate3_native_scene.h"
|
||||||
|
|
@ -472,6 +473,11 @@ void OnFrameEnd(uint8_t* base) {
|
||||||
g_immediate = true;
|
g_immediate = true;
|
||||||
g_collect_counter = 0;
|
g_collect_counter = 0;
|
||||||
skate3::native_scene::StartRecording(1);
|
skate3::native_scene::StartRecording(1);
|
||||||
|
// Simultaneous screenshot: the recorded scene/draw data alone cannot
|
||||||
|
// prove what was on screen (render-stage state is not captured);
|
||||||
|
// the paired PNG anchors every diagnostic to the visible frame.
|
||||||
|
skate3::screenshot::CaptureWindow(skate3::screenshot::RememberedWindow(),
|
||||||
|
"f10");
|
||||||
REXLOG_INFO("native-render snapshot: F10, immediate single-frame capture at frame {}",
|
REXLOG_INFO("native-render snapshot: F10, immediate single-frame capture at frame {}",
|
||||||
g_frame_index);
|
g_frame_index);
|
||||||
}
|
}
|
||||||
|
|
@ -500,6 +506,8 @@ void OnFrameEnd(uint8_t* base) {
|
||||||
g_immediate = true;
|
g_immediate = true;
|
||||||
g_collect_counter = 0;
|
g_collect_counter = 0;
|
||||||
skate3::native_scene::StartRecording(1);
|
skate3::native_scene::StartRecording(1);
|
||||||
|
skate3::screenshot::CaptureWindow(
|
||||||
|
skate3::screenshot::RememberedWindow(), "rbx");
|
||||||
REXLOG_INFO(
|
REXLOG_INFO(
|
||||||
"native-render snapshot: RB+X, immediate single-frame capture "
|
"native-render snapshot: RB+X, immediate single-frame capture "
|
||||||
"at frame {}",
|
"at frame {}",
|
||||||
|
|
@ -790,34 +798,140 @@ extern "C" REX_FUNC(sub_82594488) {
|
||||||
|
|
||||||
// Sk8::SkaterPresEntity::StartJobs: post-call, the per-garment ROPA CPU/GPU
|
// Sk8::SkaterPresEntity::StartJobs: post-call, the per-garment ROPA CPU/GPU
|
||||||
// mode fields and deformed-VB double buffer are the frame's live values.
|
// mode fields and deformed-VB double buffer are the frame's live values.
|
||||||
|
// Bracketed as a pack owner: UpdateBoneTransforms calls inside stamp their
|
||||||
|
// snapshots with this entity.
|
||||||
extern "C" REX_FUNC(sub_827825B0) {
|
extern "C" REX_FUNC(sub_827825B0) {
|
||||||
const uint32_t skater = ctx.r3.u32;
|
const uint32_t skater = ctx.r3.u32;
|
||||||
|
const uint32_t prev_owner = skate3::native_v3::ExchangePackOwner(skater);
|
||||||
__imp__sub_827825B0(ctx, base);
|
__imp__sub_827825B0(ctx, base);
|
||||||
|
skate3::native_v3::ExchangePackOwner(prev_owner);
|
||||||
if (skate3::native_render::Enabled()) {
|
if (skate3::native_render::Enabled()) {
|
||||||
skate3::native_v3::OnRopaStartJobs(base, skater);
|
skate3::native_v3::OnRopaStartJobs(base, skater);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sk8::SkaterPresEntity::DoubleBuffer(garment index): runs once per
|
||||||
|
// garment per COMPLETED cloth sim tick (the job's output memcpy + buffer
|
||||||
|
// flip). The identity store's deformed-VB freshness signal: garment-table
|
||||||
|
// fields persist after the sim stops, so this is the only per-tick proof
|
||||||
|
// the garment is really CPU-simulated right now.
|
||||||
|
extern "C" REX_FUNC(sub_82783038) {
|
||||||
|
const uint32_t skater = ctx.r3.u32;
|
||||||
|
const uint32_t index = ctx.r4.u32;
|
||||||
|
__imp__sub_82783038(ctx, base);
|
||||||
|
if (skate3::native_render::Enabled()) {
|
||||||
|
skate3::native_entity::OnRopaDoubleBuffer(base, skater, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sk8::SkaterPresEntity::EndJobs: calls PackAndMultiplyMatricesForUpload
|
||||||
|
// per instance with the tick's final locomotion (the body pose of record).
|
||||||
|
// Bracketed as a pack owner so those snapshots carry the entity.
|
||||||
|
extern "C" REX_FUNC(sub_82782818) {
|
||||||
|
const uint32_t prev_owner =
|
||||||
|
skate3::native_v3::ExchangePackOwner(ctx.r3.u32);
|
||||||
|
__imp__sub_82782818(ctx, base);
|
||||||
|
skate3::native_v3::ExchangePackOwner(prev_owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skater-family virtual UBT+Pack driver (vtable slot +16): the remaining
|
||||||
|
// palette-write path (UpdateBoneTransforms + PackAndMultiply outside the
|
||||||
|
// StartJobs/EndJobs pair). Bracketed as a pack owner.
|
||||||
|
extern "C" REX_FUNC(sub_82785778) {
|
||||||
|
const uint32_t prev_owner =
|
||||||
|
skate3::native_v3::ExchangePackOwner(ctx.r3.u32);
|
||||||
|
__imp__sub_82785778(ctx, base);
|
||||||
|
skate3::native_v3::ExchangePackOwner(prev_owner);
|
||||||
|
}
|
||||||
|
|
||||||
// Sk8::RenderPresentation::AddEntityToRenderViews / RmvEntityFrmRenderViews
|
// Sk8::RenderPresentation::AddEntityToRenderViews / RmvEntityFrmRenderViews
|
||||||
// - r3 is the RenderPresentation instance itself: the direct capture source
|
// - r3 is the RenderPresentation instance itself: the direct capture source
|
||||||
// for the RP pointer (the 0x83063C38/0x83063C60 scene globals read ZERO at
|
// for the RP pointer (the 0x83063C38/0x83063C60 scene globals read ZERO at
|
||||||
// runtime; the chain assumption did not hold live).
|
// runtime; the chain assumption did not hold live). r4 is the
|
||||||
|
// PresentationEntity being (de)registered: the identity store's
|
||||||
|
// scene-membership/lifetime signal (native/skate3_native_entity.h).
|
||||||
extern "C" REX_FUNC(sub_827A6C50) {
|
extern "C" REX_FUNC(sub_827A6C50) {
|
||||||
const uint32_t rp = ctx.r3.u32;
|
const uint32_t rp = ctx.r3.u32;
|
||||||
|
const uint32_t entity = ctx.r4.u32;
|
||||||
if (skate3::native_render::Enabled()) {
|
if (skate3::native_render::Enabled()) {
|
||||||
skate3::native_v3::OnRenderPresentation(base, rp);
|
skate3::native_v3::OnRenderPresentation(base, rp);
|
||||||
|
skate3::native_entity::OnEntityViewAdd(entity);
|
||||||
}
|
}
|
||||||
__imp__sub_827A6C50(ctx, base);
|
__imp__sub_827A6C50(ctx, base);
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" REX_FUNC(sub_827A6CE8) {
|
extern "C" REX_FUNC(sub_827A6CE8) {
|
||||||
const uint32_t rp = ctx.r3.u32;
|
const uint32_t rp = ctx.r3.u32;
|
||||||
|
const uint32_t entity = ctx.r4.u32;
|
||||||
if (skate3::native_render::Enabled()) {
|
if (skate3::native_render::Enabled()) {
|
||||||
skate3::native_v3::OnRenderPresentation(base, rp);
|
skate3::native_v3::OnRenderPresentation(base, rp);
|
||||||
|
skate3::native_entity::OnEntityViewRemove(entity);
|
||||||
}
|
}
|
||||||
__imp__sub_827A6CE8(ctx, base);
|
__imp__sub_827A6CE8(ctx, base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sk8::PresentationEntity::BindConstants (base override): post-call, every
|
||||||
|
// MeshContext of the entity's current LOD has just received its constant
|
||||||
|
// param POINTERS (world hash 0x7DDF552B -> &entity+416, etc.; the game
|
||||||
|
// binds by pointer and dereferences at draw). Every subclass bind calls
|
||||||
|
// this base, so the exit sees every renderable presentation entity with
|
||||||
|
// its full ctx set: the ctx -> entity identity write point.
|
||||||
|
extern "C" REX_FUNC(sub_827A6658) {
|
||||||
|
const uint32_t entity = ctx.r3.u32;
|
||||||
|
__imp__sub_827A6658(ctx, base);
|
||||||
|
if (skate3::native_render::Enabled()) {
|
||||||
|
skate3::native_entity::OnBindConstants(base, entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derived BindConstants overrides: class tags for the identity store. The
|
||||||
|
// most-derived override returns last (each calls its parent first), so the
|
||||||
|
// last tag to land is the entity's concrete class.
|
||||||
|
extern "C" REX_FUNC(sub_82783D68) { // Sk8::SkaterPresEntity
|
||||||
|
const uint32_t entity = ctx.r3.u32;
|
||||||
|
__imp__sub_82783D68(ctx, base);
|
||||||
|
if (skate3::native_render::Enabled()) {
|
||||||
|
skate3::native_entity::OnBindClass(
|
||||||
|
entity, skate3::native_entity::EntClass::kSkater);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" REX_FUNC(sub_82785260) { // ColorizedSkaterPresEntity
|
||||||
|
const uint32_t entity = ctx.r3.u32;
|
||||||
|
__imp__sub_82785260(ctx, base);
|
||||||
|
if (skate3::native_render::Enabled()) {
|
||||||
|
skate3::native_entity::OnBindClass(
|
||||||
|
entity, skate3::native_entity::EntClass::kColorized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" REX_FUNC(sub_82793F70) { // CACPresEntity (untransposed world)
|
||||||
|
const uint32_t entity = ctx.r3.u32;
|
||||||
|
__imp__sub_82793F70(ctx, base);
|
||||||
|
if (skate3::native_render::Enabled()) {
|
||||||
|
skate3::native_entity::OnBindClass(
|
||||||
|
entity, skate3::native_entity::EntClass::kCac);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" REX_FUNC(sub_82785528) { // unnamed skater-layout class
|
||||||
|
const uint32_t entity = ctx.r3.u32;
|
||||||
|
__imp__sub_82785528(ctx, base);
|
||||||
|
if (skate3::native_render::Enabled()) {
|
||||||
|
skate3::native_entity::OnBindClass(
|
||||||
|
entity, skate3::native_entity::EntClass::kSkaterAux);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" REX_FUNC(sub_827C1720) { // cLivingWorldPresEntity
|
||||||
|
const uint32_t entity = ctx.r3.u32;
|
||||||
|
__imp__sub_827C1720(ctx, base);
|
||||||
|
if (skate3::native_render::Enabled()) {
|
||||||
|
skate3::native_entity::OnBindClass(
|
||||||
|
entity, skate3::native_entity::EntClass::kLivingWorld);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// pegasus::tRModelData::Fixup(void* model, rw::core::arena::ArenaIterator*)
|
// pegasus::tRModelData::Fixup(void* model, rw::core::arena::ArenaIterator*)
|
||||||
// - the rw-arena LOAD-time pointer resolve, fired once per model while its
|
// - the rw-arena LOAD-time pointer resolve, fired once per model while its
|
||||||
// arena streams in. (The `Unfix(void*, SizeAndAlignment*)` atoms are the
|
// arena streams in. (The `Unfix(void*, SizeAndAlignment*)` atoms are the
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
#include <rex/logging.h>
|
#include <rex/logging.h>
|
||||||
|
|
||||||
#include "native/skate3_native_diag.h"
|
#include "native/skate3_native_diag.h"
|
||||||
|
#include "native/skate3_native_entity.h"
|
||||||
#include "native/skate3_native_guest_read.h"
|
#include "native/skate3_native_guest_read.h"
|
||||||
#include "native/skate3_native_lw.h"
|
#include "native/skate3_native_lw.h"
|
||||||
#include "native/skate3_native_v3_shadow.h"
|
#include "native/skate3_native_v3_shadow.h"
|
||||||
|
|
@ -769,6 +770,8 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_tex_mips, true, "Skate 3",
|
||||||
// toggle refuses to flip the scene on without it: the hooks that feed the
|
// toggle refuses to flip the scene on without it: the hooks that feed the
|
||||||
// scene are only installed when it was set at boot.
|
// scene are only installed when it was set at boot.
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render);
|
REXCVAR_DECLARE(bool, skate3_native_render);
|
||||||
|
// Defined in native/skate3_native_entity.cpp (the identity store module).
|
||||||
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_entity_ropa_world_primary);
|
||||||
// Defined in native/skate3_native_lw.cpp (the LW entity store module).
|
// Defined in native/skate3_native_lw.cpp (the LW entity store module).
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_lw_palette);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_lw_palette);
|
||||||
// SDK-level emulated-draw suppression (rexglue native_guest_renderer.cpp):
|
// SDK-level emulated-draw suppression (rexglue native_guest_renderer.cpp):
|
||||||
|
|
@ -1210,6 +1213,20 @@ struct CharRowsInst {
|
||||||
uint32_t entity = 0;
|
uint32_t entity = 0;
|
||||||
};
|
};
|
||||||
std::unordered_map<uint64_t, CharRowsInst> g_char_rows_cache_inst;
|
std::unordered_map<uint64_t, CharRowsInst> g_char_rows_cache_inst;
|
||||||
|
// Entity-keyed lighting rows for ROPA garments only: the sim on/off switch
|
||||||
|
// swaps between a garment's two renderables (cloth-target vs original
|
||||||
|
// model, different mesh AND ctx), so the (mesh,ctx) caches never carry
|
||||||
|
// rows across the switch and the freshly resolved model rendered dark/flat
|
||||||
|
// until its own capture validated. Both models map to the same owning
|
||||||
|
// entity in the identity store, and they are the same garment; sharing
|
||||||
|
// rows across the pair is exact. Garment-to-garment only; body pieces keep
|
||||||
|
// their per-piece caches.
|
||||||
|
struct CharRowsEnt {
|
||||||
|
std::array<float, 60> rows;
|
||||||
|
uint8_t fam = 0; // donor family: the row layout is family-specific
|
||||||
|
};
|
||||||
|
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};
|
std::atomic<uint64_t> g_char_rows_inst_served{0};
|
||||||
// Perspective refreshes committed onto skinned-published ROPA pieces (the
|
// Perspective refreshes committed onto skinned-published ROPA pieces (the
|
||||||
// caster_refresh_all fix; see the fixup's candidate selection).
|
// caster_refresh_all fix; see the fixup's candidate selection).
|
||||||
|
|
@ -1267,6 +1284,12 @@ struct RopaResolvedState {
|
||||||
bool skinned = false;
|
bool skinned = false;
|
||||||
float world[16] = {};
|
float world[16] = {};
|
||||||
std::vector<float> bones;
|
std::vector<float> bones;
|
||||||
|
// Publish frame of the cached state: the rescues below exist to bridge
|
||||||
|
// 1-3 frame capture gaps, and re-publishing an unbounded-age state pins a
|
||||||
|
// ghost of the garment at a stale pose for as long as captures keep
|
||||||
|
// failing (the frozen-in-the-air garment behind a skater whose capture
|
||||||
|
// path was refusing every frame).
|
||||||
|
uint64_t frame = 0;
|
||||||
};
|
};
|
||||||
std::unordered_map<uint32_t, RopaResolvedState> g_ropa_state_cache;
|
std::unordered_map<uint32_t, RopaResolvedState> g_ropa_state_cache;
|
||||||
std::atomic<uint64_t> g_ropa_rescued{0};
|
std::atomic<uint64_t> g_ropa_rescued{0};
|
||||||
|
|
@ -1576,6 +1599,11 @@ std::atomic<uint64_t> g_ropa_blend_miss{0};
|
||||||
// when no previous state existed) because the GPU-resident decode still
|
// when no previous state existed) because the GPU-resident decode still
|
||||||
// pairs with the other mode; see g_ropa_resident.
|
// pairs with the other mode; see g_ropa_resident.
|
||||||
std::atomic<uint64_t> g_ropa_hold{0};
|
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:
|
// Ropa garments PUBLISHED with a caster-cascade (ortho) bank's palette:
|
||||||
// the ~40 ms-stale rows behind the garment-offset-from-body symptom. The
|
// 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
|
// same-mode graft in the dyn_slot merge should keep this at ~0 whenever a
|
||||||
|
|
@ -1807,6 +1835,26 @@ uint32_t BankPaletteBase(uint8_t* base, uint32_t bank) {
|
||||||
}
|
}
|
||||||
return !(f[2] > -1e7f && f[2] < 1e7f);
|
return !(f[2] > -1e7f && f[2] < 1e7f);
|
||||||
};
|
};
|
||||||
|
// The sim-ACTIVE variant of the ropa flag row reads (~0, JUNK, ~0, JUNK):
|
||||||
|
// x is written 0.0 (sim owns the garment) with uninitialized memory in the
|
||||||
|
// unwritten lanes (observed ~7e20 on the trick-guide demo skater's tee,
|
||||||
|
// c7/c4). The junk lanes fail every range check
|
||||||
|
// above, leaving the bank UNCLASSIFIABLE in the true main-pass state
|
||||||
|
// (return 0; the capture never resolved and the garment stayed pending),
|
||||||
|
// while the post-shadow state matched bone_at(7) mid-palette instead and
|
||||||
|
// handed the rigid-matrix read the shadow block's (0,0,0,1) tail row at
|
||||||
|
// c191. An out-of-range lane under x ~ 0 and z ~ 0 is the same structural
|
||||||
|
// proof as the junk-z inactive variant: a real bone row is never junk.
|
||||||
|
const auto ropa_active_flag_row_at = [&](uint32_t reg) -> bool {
|
||||||
|
float f[4];
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
f[i] = LoadGuestF32(base, bank + (reg * 4 + uint32_t(i)) * 4);
|
||||||
|
}
|
||||||
|
if (!(std::fabs(f[0]) <= 1e-3f && std::fabs(f[2]) <= 1e-3f)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !(f[1] > -1e7f && f[1] < 1e7f) || !(f[3] > -1e7f && f[3] < 1e7f);
|
||||||
|
};
|
||||||
// DETERMINISTIC main-pass detection first: that layout keeps the CAMERA
|
// DETERMINISTIC main-pass detection first: that layout keeps the CAMERA
|
||||||
// POSITION at c4 (palette at c7, or c8 behind a parameter row). Matching
|
// POSITION at c4 (palette at c7, or c8 behind a parameter row). Matching
|
||||||
// c4 against the frame camera pins the layout without any scoring; the
|
// c4 against the frame camera pins the layout without any scoring; the
|
||||||
|
|
@ -1821,18 +1869,28 @@ uint32_t BankPaletteBase(uint8_t* base, uint32_t bank) {
|
||||||
const float dz = LoadGuestF32(base, bank + 18 * 4) - g_fog_cam[2];
|
const float dz = LoadGuestF32(base, bank + 18 * 4) - g_fog_cam[2];
|
||||||
if (dx * dx + dy * dy + dz * dz < 25.0f &&
|
if (dx * dx + dy * dy + dz * dz < 25.0f &&
|
||||||
(g_fog_cam[0] != 0.0f || g_fog_cam[1] != 0.0f || g_fog_cam[2] != 0.0f)) {
|
(g_fog_cam[0] != 0.0f || g_fog_cam[1] != 0.0f || g_fog_cam[2] != 0.0f)) {
|
||||||
if ((param_row_at(7) || ropa_flag_row_at(7)) && bone_at(8)) return 8;
|
if ((param_row_at(7) || ropa_flag_row_at(7) ||
|
||||||
|
ropa_active_flag_row_at(7)) &&
|
||||||
|
bone_at(8)) {
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
if (bone_at(7)) return 7;
|
if (bone_at(7)) return 7;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (bone_at(4)) return 4;
|
if (bone_at(4)) return 4;
|
||||||
// The ropa junk-z flag check must run BEFORE bone_at(7): on a pre-pass
|
// The ropa junk-lane flag checks must run BEFORE bone_at(7): on a pre-pass
|
||||||
// ropa bank (flag c4, palette c5) rows c7.. are mid-palette bone rows and
|
// ropa bank (flag c4, palette c5) rows c7.. are mid-palette bone rows and
|
||||||
// bone_at(7) matches them, the one-bone-late class.
|
// bone_at(7) matches them, the one-bone-late class.
|
||||||
if ((param_row_at(4) || ropa_flag_row_at(4)) && bone_at(5)) return 5;
|
if ((param_row_at(4) || ropa_flag_row_at(4) || ropa_active_flag_row_at(4)) &&
|
||||||
|
bone_at(5)) {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
if (bone_at(7)) return 7;
|
if (bone_at(7)) return 7;
|
||||||
if ((param_row_at(7) || ropa_flag_row_at(7)) && bone_at(8)) return 8;
|
if ((param_row_at(7) || ropa_flag_row_at(7) || ropa_active_flag_row_at(7)) &&
|
||||||
|
bone_at(8)) {
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4461,18 +4519,36 @@ bool CaptureSkinnedState(uint8_t* base, uint32_t bank, uint32_t palette_base,
|
||||||
if (rigid_score < 8) {
|
if (rigid_score < 8) {
|
||||||
g_ropa_relaxed.fetch_add(1, std::memory_order_relaxed);
|
g_ropa_relaxed.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
// Primary entity-world serve: the accepted bank block is a
|
||||||
|
// dereference of the owner entity's m_MatLtoWTrans, so the entity
|
||||||
|
// field is value-identical here (bit-compare tripwire counts any
|
||||||
|
// drift as wprim divergence) and stays correct through bank states
|
||||||
|
// the acceptance can't see. Bank rows remain the fallback.
|
||||||
|
const float* src_rows = rows;
|
||||||
|
float ent_rows[12];
|
||||||
|
if (REXCVAR_GET(skate3_native_render_scene_entity_ropa_world_primary) &&
|
||||||
|
skate3::native_entity::ServeRopaWorld(base, item.ctx, item.vb_obj,
|
||||||
|
ent_rows)) {
|
||||||
|
skate3::native_entity::NoteAcceptedWorldCompare(rows, ent_rows);
|
||||||
|
src_rows = ent_rows;
|
||||||
|
}
|
||||||
// Column-vector [R | t] rows -> item.world (row-vector, t in row 3).
|
// Column-vector [R | t] rows -> item.world (row-vector, t in row 3).
|
||||||
for (int i = 0; i < 3; ++i) {
|
for (int i = 0; i < 3; ++i) {
|
||||||
for (int j = 0; j < 3; ++j) {
|
for (int j = 0; j < 3; ++j) {
|
||||||
item.world[i * 4 + j] = rows[j * 4 + i];
|
item.world[i * 4 + j] = src_rows[j * 4 + i];
|
||||||
}
|
}
|
||||||
item.world[i * 4 + 3] = 0.0f;
|
item.world[i * 4 + 3] = 0.0f;
|
||||||
item.world[12 + i] = rows[i * 4 + 3];
|
item.world[12 + i] = src_rows[i * 4 + 3];
|
||||||
}
|
}
|
||||||
item.world[15] = 1.0f;
|
item.world[15] = 1.0f;
|
||||||
item.skinned = false;
|
item.skinned = false;
|
||||||
item.bones.clear();
|
item.bones.clear();
|
||||||
g_ropa_rigid.fetch_add(1, std::memory_order_relaxed);
|
g_ropa_rigid.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
// Identity-store observer: the accepted bank block should be a
|
||||||
|
// dereference of the owner entity's bound world pointer (+416
|
||||||
|
// transposed for skater-class binds, +352 raw for CAC); the
|
||||||
|
// ident[] w416/w352 telemetry proves or refutes that live.
|
||||||
|
skate3::native_entity::ObserveRopaWorld(base, item.ctx, rows);
|
||||||
if (item.hair) {
|
if (item.hair) {
|
||||||
CaptureHairTint(base, item);
|
CaptureHairTint(base, item);
|
||||||
}
|
}
|
||||||
|
|
@ -4481,8 +4557,28 @@ bool CaptureSkinnedState(uint8_t* base, uint32_t bank, uint32_t palette_base,
|
||||||
}
|
}
|
||||||
// Implausible or off-clip matrix: the bank belongs to another mesh
|
// Implausible or off-clip matrix: the bank belongs to another mesh
|
||||||
// (the submit-exit capture runs after whatever draw happened to be
|
// (the submit-exit capture runs after whatever draw happened to be
|
||||||
// inline). Refuse it; the caller keeps the item pending and the
|
// inline). Before refusing, try the identity store: the accepted
|
||||||
// post-draw fixup re-captures from this mesh's own draw.
|
// bank block is a bit-exact dereference of the owner entity's
|
||||||
|
// m_MatLtoWTrans (+416), so
|
||||||
|
// the entity field serves the same matrix through every bank state
|
||||||
|
// the pass sequence can leave behind (post-shadow tail clobber,
|
||||||
|
// foreign bank, torn state). Geometry only; the PS lighting rows
|
||||||
|
// stay on their caches, since the bank is not proven ours here.
|
||||||
|
float ent_rows[12];
|
||||||
|
if (skate3::native_entity::ServeRopaWorld(base, item.ctx, item.vb_obj,
|
||||||
|
ent_rows)) {
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
for (int j = 0; j < 3; ++j) {
|
||||||
|
item.world[i * 4 + j] = ent_rows[j * 4 + i];
|
||||||
|
}
|
||||||
|
item.world[i * 4 + 3] = 0.0f;
|
||||||
|
item.world[12 + i] = ent_rows[i * 4 + 3];
|
||||||
|
}
|
||||||
|
item.world[15] = 1.0f;
|
||||||
|
item.skinned = false;
|
||||||
|
item.bones.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
g_ropa_stale.fetch_add(1, std::memory_order_relaxed);
|
g_ropa_stale.fetch_add(1, std::memory_order_relaxed);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -7315,6 +7411,53 @@ void InterpolateDynamicItems(uint8_t* base, FrameScene& scene, double now) {
|
||||||
std::memcpy(item.world, wacc, sizeof(wacc));
|
std::memcpy(item.world, wacc, sizeof(wacc));
|
||||||
rigid_world_boxcar = filter_w > 0.0005 && h.count >= 4;
|
rigid_world_boxcar = filter_w > 0.0005 && h.count >= 4;
|
||||||
}
|
}
|
||||||
|
// Post-interpolation identity anchor (ropa garments): the published
|
||||||
|
// world and the guest draws are proven bit-exact against the owner
|
||||||
|
// entity, so a rendered world grossly diverged from the entity here
|
||||||
|
// can only be ring corruption (mispaired claim / stale pose blended
|
||||||
|
// in). Motion smoothing legitimately offsets by centimeters; the
|
||||||
|
// thresholds (2.5 m translation, ~60 deg rotation) only catch the
|
||||||
|
// detached/perpendicular class. Repair = the entity's live matrix
|
||||||
|
// (raw for one frame beats a detached garment), and the log names
|
||||||
|
// the ring state for diagnosis.
|
||||||
|
if (item.ropa) {
|
||||||
|
float ent_rows[12];
|
||||||
|
if (skate3::native_entity::ReadEntityWorldRows(base, item.ctx,
|
||||||
|
ent_rows)) {
|
||||||
|
const float dx = item.world[12] - ent_rows[3];
|
||||||
|
const float dy = item.world[13] - ent_rows[7];
|
||||||
|
const float dz = item.world[14] - ent_rows[11];
|
||||||
|
float rot = 0.0f;
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
for (int j = 0; j < 3; ++j) {
|
||||||
|
const float d = item.world[i * 4 + j] - ent_rows[j * 4 + i];
|
||||||
|
rot += d * d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dx * dx + dy * dy + dz * dz > 2.5f * 2.5f || rot > 1.5f) {
|
||||||
|
static std::atomic<uint32_t> s_fix{0};
|
||||||
|
const uint32_t ln = s_fix.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (ln < 16 || (ln & 255u) == 0) {
|
||||||
|
REXLOG_INFO(
|
||||||
|
"native-scene: ropa interp DETACH repaired mesh={:08X} "
|
||||||
|
"ctx={:08X} dt=({:.2f},{:.2f},{:.2f}) rot2={:.2f} ba={:.2f} "
|
||||||
|
"ring[n={} q0t=({:.1f},{:.1f},{:.1f}) "
|
||||||
|
"q1t=({:.1f},{:.1f},{:.1f})] (n={})",
|
||||||
|
item.mesh, item.ctx, dx, dy, dz, rot, ba, h.count, q0.w[12],
|
||||||
|
q0.w[13], q0.w[14], q1.w[12], q1.w[13], q1.w[14], ln);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
for (int j = 0; j < 3; ++j) {
|
||||||
|
item.world[i * 4 + j] = ent_rows[j * 4 + i];
|
||||||
|
}
|
||||||
|
item.world[i * 4 + 3] = 0.0f;
|
||||||
|
item.world[12 + i] = ent_rows[i * 4 + 3];
|
||||||
|
}
|
||||||
|
item.world[15] = 1.0f;
|
||||||
|
rigid_world_boxcar = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// ROPA shape pairing: express the EXACT temporal kernel the garment's
|
// ROPA shape pairing: express the EXACT temporal kernel the garment's
|
||||||
// world (and the body's bones) were just evaluated with as weights over
|
// world (and the body's bones) were just evaluated with as weights over
|
||||||
|
|
@ -8187,7 +8330,8 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
if (item.ropa) {
|
if (item.ropa) {
|
||||||
const auto cit = g_ropa_state_cache.find(item.mesh);
|
const auto cit = g_ropa_state_cache.find(item.mesh);
|
||||||
if (cit != g_ropa_state_cache.end() && cit->second.skinned &&
|
if (cit != g_ropa_state_cache.end() && cit->second.skinned &&
|
||||||
cit->second.bones.size() == item.bones.size()) {
|
cit->second.bones.size() == item.bones.size() &&
|
||||||
|
g_guest_frame - cit->second.frame <= 30) {
|
||||||
item.bones = cit->second.bones;
|
item.bones = cit->second.bones;
|
||||||
item.dbg_src = 6;
|
item.dbg_src = 6;
|
||||||
healed = true;
|
healed = true;
|
||||||
|
|
@ -8225,6 +8369,85 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
item.bones.clear();
|
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;
|
std::unordered_map<uint32_t, uint32_t> pub_count;
|
||||||
for (const DrawItem& item : scene.items) {
|
for (const DrawItem& item : scene.items) {
|
||||||
// Ropa garments count in EITHER resolved mode (a rigid-resolved copy
|
// Ropa garments count in EITHER resolved mode (a rigid-resolved copy
|
||||||
|
|
@ -8261,6 +8484,7 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
c.skinned = now_skinned;
|
c.skinned = now_skinned;
|
||||||
std::memcpy(c.world, item.world, sizeof(c.world));
|
std::memcpy(c.world, item.world, sizeof(c.world));
|
||||||
c.bones = item.bones;
|
c.bones = item.bones;
|
||||||
|
c.frame = g_guest_frame;
|
||||||
}
|
}
|
||||||
} else if (item.skinned && !item.bones.empty() && !item.caster_bank &&
|
} else if (item.skinned && !item.bones.empty() && !item.caster_bank &&
|
||||||
g_bones_cache.size() < 512) {
|
g_bones_cache.size() < 512) {
|
||||||
|
|
@ -8347,8 +8571,48 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
if (pub_count.find(mesh) != pub_count.end()) {
|
if (pub_count.find(mesh) != pub_count.end()) {
|
||||||
continue; // a live copy published; nothing to rescue
|
continue; // a live copy published; nothing to rescue
|
||||||
}
|
}
|
||||||
|
// Dropped-garment gate: when the owning entity's garment table no
|
||||||
|
// longer claims this VB (the game removed the cloth model; its
|
||||||
|
// pieces render skinned with the body now), the cached rigid state
|
||||||
|
// is a ghost: the retained drape tracks the live character as a
|
||||||
|
// floating garment. Suppress instead of rescuing.
|
||||||
|
if (skate3::native_entity::RopaGarmentDropped(base, cand->ctx,
|
||||||
|
cand->vb_obj)) {
|
||||||
|
// The game renders a dropped garment SKINNED with the body (its
|
||||||
|
// sim-inactive path); the cached rigid state is a ghost. Resolve
|
||||||
|
// with the owning instance's own live packed palette; when that
|
||||||
|
// fails, suppress (invisible beats floating).
|
||||||
|
float rows[96 * 12];
|
||||||
|
const uint32_t n = skate3::native_entity::ServeInstancePalette(
|
||||||
|
base, cand->ctx, rows, 96);
|
||||||
|
static std::atomic<uint32_t> s_drop_logged{0};
|
||||||
|
const uint32_t ln =
|
||||||
|
s_drop_logged.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (ln < 16 || (ln & 511u) == 0) {
|
||||||
|
REXLOG_INFO(
|
||||||
|
"native-scene: ropa rescue DROPPED-GARMENT {} mesh={:08X} "
|
||||||
|
"ctx={:08X} vb={:08X} rows={} (n={})",
|
||||||
|
n ? "resolved SKINNED" : "suppressed", cand->mesh, cand->ctx,
|
||||||
|
cand->vb_obj, n, ln);
|
||||||
|
}
|
||||||
|
if (n != 0) {
|
||||||
|
scene.items.push_back(*cand);
|
||||||
|
DrawItem& resolved = scene.items.back();
|
||||||
|
resolved.skinned = true;
|
||||||
|
resolved.bones.assign(rows, rows + size_t(n) * 12);
|
||||||
|
resolved.pending = false;
|
||||||
|
resolved.caster_bank = false;
|
||||||
|
resolved.dbg_src = 4;
|
||||||
|
if (resolved.ctx != 0) {
|
||||||
|
dyn_slot.try_emplace(resolved.ctx, scene.items.size() - 1);
|
||||||
|
}
|
||||||
|
g_ropa_rescued.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const auto rit = g_ropa_state_cache.find(mesh);
|
const auto rit = g_ropa_state_cache.find(mesh);
|
||||||
if (rit == g_ropa_state_cache.end()) {
|
if (rit == g_ropa_state_cache.end() ||
|
||||||
|
g_guest_frame - rit->second.frame > 30) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
scene.items.push_back(*cand);
|
scene.items.push_back(*cand);
|
||||||
|
|
@ -8438,6 +8702,18 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
CharRowsInst& ci = g_char_rows_cache_inst[key];
|
CharRowsInst& ci = g_char_rows_cache_inst[key];
|
||||||
std::memcpy(ci.rows.data(), item.char_rows, sizeof(item.char_rows));
|
std::memcpy(ci.rows.data(), item.char_rows, sizeof(item.char_rows));
|
||||||
ci.entity = lw_entity;
|
ci.entity = lw_entity;
|
||||||
|
if (item.ropa) {
|
||||||
|
skate3::native_entity::CtxInfo ident;
|
||||||
|
if (skate3::native_entity::LookupCtx(item.ctx, &ident)) {
|
||||||
|
if (g_char_rows_cache_ent.size() > 4096) {
|
||||||
|
g_char_rows_cache_ent.clear();
|
||||||
|
}
|
||||||
|
CharRowsEnt& ce = g_char_rows_cache_ent[ident.entity];
|
||||||
|
std::memcpy(ce.rows.data(), item.char_rows,
|
||||||
|
sizeof(item.char_rows));
|
||||||
|
ce.fam = uint8_t(item.char_family);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const auto iit = g_char_rows_cache_inst.find(key);
|
const auto iit = g_char_rows_cache_inst.find(key);
|
||||||
if (iit != g_char_rows_cache_inst.end() &&
|
if (iit != g_char_rows_cache_inst.end() &&
|
||||||
|
|
@ -8445,6 +8721,33 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
std::memcpy(item.char_rows, iit->second.rows.data(),
|
std::memcpy(item.char_rows, iit->second.rows.data(),
|
||||||
sizeof(item.char_rows));
|
sizeof(item.char_rows));
|
||||||
g_char_rows_inst_served.fetch_add(1, std::memory_order_relaxed);
|
g_char_rows_inst_served.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
} else if (item.ropa) {
|
||||||
|
// Sim-switch bridge: rows from the garment's sibling renderable
|
||||||
|
// via the common owning entity (see g_char_rows_cache_ent). The
|
||||||
|
// row layout is family-specific, so a cross-family serve routes
|
||||||
|
// the receiver through the donor family's shading path too.
|
||||||
|
skate3::native_entity::CtxInfo ident;
|
||||||
|
if (skate3::native_entity::LookupCtx(item.ctx, &ident)) {
|
||||||
|
const auto eit = g_char_rows_cache_ent.find(ident.entity);
|
||||||
|
if (eit != g_char_rows_cache_ent.end()) {
|
||||||
|
std::memcpy(item.char_rows, eit->second.rows.data(),
|
||||||
|
sizeof(item.char_rows));
|
||||||
|
if (eit->second.fam != 0 &&
|
||||||
|
eit->second.fam != item.char_family) {
|
||||||
|
static std::atomic<uint32_t> s_fam_logged{0};
|
||||||
|
const uint32_t ln =
|
||||||
|
s_fam_logged.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (ln < 8 || (ln & 1023u) == 0) {
|
||||||
|
REXLOG_INFO(
|
||||||
|
"native-scene: rows bridge cross-family serve "
|
||||||
|
"mesh={:08X} fam {}->{} (n={})",
|
||||||
|
item.mesh, item.char_family, eit->second.fam, ln);
|
||||||
|
}
|
||||||
|
item.char_family = eit->second.fam;
|
||||||
|
}
|
||||||
|
g_char_rows_ent_served.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8962,6 +9265,25 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Presentation-entity identity store observer
|
||||||
|
// (native/skate3_native_entity.h): per-item coverage of the
|
||||||
|
// ctx -> entity map built from the game's BindConstants walks, and the
|
||||||
|
// skater-family entity+496 opacity vs the alpha the routing decided on.
|
||||||
|
// Read-only: the ident[] stats line is the evidence for flipping any
|
||||||
|
// serve over to the direct fields.
|
||||||
|
{
|
||||||
|
for (DrawItem& item : scene.items) {
|
||||||
|
if (item.char_family == 0 || item.ctx == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
skate3::native_entity::ObserveCharItem(base, item.ctx,
|
||||||
|
item.char_family,
|
||||||
|
item.lw_entity != 0,
|
||||||
|
CharFadeAlpha(item));
|
||||||
|
}
|
||||||
|
skate3::native_entity::EmitStats();
|
||||||
|
}
|
||||||
|
|
||||||
// Bone-palette alternation guard (the high-fps hair/garment flicker,
|
// Bone-palette alternation guard (the high-fps hair/garment flicker,
|
||||||
// probe-confirmed: alt[bones] events on fam 2/4/5 pieces while rows/
|
// probe-confirmed: alt[bones] events on fam 2/4/5 pieces while rows/
|
||||||
// coverage/route stayed stable). A served palette that returns BIT-EXACT
|
// coverage/route stayed stable). A served palette that returns BIT-EXACT
|
||||||
|
|
@ -10352,6 +10674,12 @@ struct RendererState {
|
||||||
// bone ring).
|
// bone ring).
|
||||||
struct RopaGen {
|
struct RopaGen {
|
||||||
uint64_t seq = 0;
|
uint64_t seq = 0;
|
||||||
|
// Host time at commit: the blend refuses to mix generations recorded
|
||||||
|
// across a sim-sleep gap (a garment whose cloth sim slept keeps its
|
||||||
|
// last drapes in the ring; blending one of those, recorded when the
|
||||||
|
// character stood elsewhere / faced differently, against the fresh
|
||||||
|
// drape renders the garment rotated/offset from the body).
|
||||||
|
double t = 0.0;
|
||||||
std::vector<float> verts; // num_verts x 14 floats (scene VS layout)
|
std::vector<float> verts; // num_verts x 14 floats (scene VS layout)
|
||||||
};
|
};
|
||||||
std::unordered_map<uint32_t, std::deque<RopaGen>> ropa_shapes;
|
std::unordered_map<uint32_t, std::deque<RopaGen>> ropa_shapes;
|
||||||
|
|
@ -14938,7 +15266,12 @@ void PrewarmCommit(const NativeGuestOutputRenderContext& context,
|
||||||
if (r.item.ropa && r.buffers.dyn_seq != 0 && !superseded &&
|
if (r.item.ropa && r.buffers.dyn_seq != 0 && !superseded &&
|
||||||
!r.buffers.ropa_verts.empty()) {
|
!r.buffers.ropa_verts.empty()) {
|
||||||
auto& ring = g_r.ropa_shapes[r.item.mesh];
|
auto& ring = g_r.ropa_shapes[r.item.mesh];
|
||||||
ring.push_back({r.buffers.dyn_seq, std::move(r.buffers.ropa_verts)});
|
const double now_s =
|
||||||
|
std::chrono::duration<double>(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch())
|
||||||
|
.count();
|
||||||
|
ring.push_back(
|
||||||
|
{r.buffers.dyn_seq, now_s, std::move(r.buffers.ropa_verts)});
|
||||||
// 16 generations = ~114 ms at a 140 Hz guest: the 8-tap boxcar
|
// 16 generations = ~114 ms at a 140 Hz guest: the 8-tap boxcar
|
||||||
// kernel reaches filter_w/2 (~25 ms) past the play clock (itself
|
// kernel reaches filter_w/2 (~25 ms) past the play clock (itself
|
||||||
// ~2 guest periods behind), plus decode-latency slack.
|
// ~2 guest periods behind), plus decode-latency slack.
|
||||||
|
|
@ -16716,7 +17049,7 @@ void LogFrameStats(const FrameScene& scene, uint64_t frames, uint32_t drawn,
|
||||||
"native-scene: frame {} items={} draws={} draws_2d={} drawn_2d={} "
|
"native-scene: frame {} items={} draws={} draws_2d={} drawn_2d={} "
|
||||||
"splines[{}/{}] "
|
"splines[{}/{}] "
|
||||||
"2d[other={} dropped={} askip={} astale={} textures={}] cached_meshes={} textures={} "
|
"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={} blend={} blendmiss={}] dyn_gap={} skinned={} skinned_skipped={} foreign_bank={} "
|
"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={} "
|
||||||
"rigid[pending={} dropped={} worldprops={}] "
|
"rigid[pending={} dropped={} worldprops={}] "
|
||||||
"rej[dyn={} range={} chain={} geom={} draws={} bbox={}] "
|
"rej[dyn={} range={} chain={} geom={} draws={} bbox={}] "
|
||||||
"rr[decode_fail={} no_bones={} mesh_deferred={} tex_deferred={}] "
|
"rr[decode_fail={} no_bones={} mesh_deferred={} tex_deferred={}] "
|
||||||
|
|
@ -16736,7 +17069,7 @@ void LogFrameStats(const FrameScene& scene, uint64_t frames, uint32_t drawn,
|
||||||
g_ropa_rigid.load(), g_ropa_stale.load(), g_ropa_rescued.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_flip.load(), g_ropa_mismatch.load(), g_ropa_relaxed.load(),
|
||||||
g_ropa_hold.load(), g_ropa_caster.load(), g_pub_incoherent.load(),
|
g_ropa_hold.load(), g_ropa_caster.load(), g_pub_incoherent.load(),
|
||||||
g_stretch_veto.load(),
|
g_stretch_veto.load(), g_ropa_detached.load(),
|
||||||
g_ropa_blend_drawn.load(), g_ropa_blend_miss.load(), g_dyn_gap.load(),
|
g_ropa_blend_drawn.load(), g_ropa_blend_miss.load(), g_dyn_gap.load(),
|
||||||
g_skinned_items.load(),
|
g_skinned_items.load(),
|
||||||
g_skinned_skipped.load(), g_capture_foreign_bank.load(),
|
g_skinned_skipped.load(), g_capture_foreign_bank.load(),
|
||||||
|
|
@ -18425,7 +18758,15 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
const size_t want_floats =
|
const size_t want_floats =
|
||||||
size_t(buffers.vb_view.size_bytes) / sizeof(float);
|
size_t(buffers.vb_view.size_bytes) / sizeof(float);
|
||||||
const auto rit = g_r.ropa_shapes.find(item.mesh);
|
const auto rit = g_r.ropa_shapes.find(item.mesh);
|
||||||
if (rit != g_r.ropa_shapes.end()) {
|
if (rit != g_r.ropa_shapes.end() && !rit->second.empty()) {
|
||||||
|
// Sim-sleep gate: generations grossly older than the ring's newest
|
||||||
|
// were recorded before a cloth-sim sleep (continuous streams span
|
||||||
|
// ~114 ms end to end). Blending one against the fresh drape draws
|
||||||
|
// the garment where/how the character USED to stand, the
|
||||||
|
// detached/perpendicular class. Dropped generations shed their
|
||||||
|
// kernel weight; if too much weight is stale the raw resident VB
|
||||||
|
// draws instead (self-consistent current shape).
|
||||||
|
const double ring_newest_t = rit->second.back().t;
|
||||||
for (int k = 0; k < item.shape_count; ++k) {
|
for (int k = 0; k < item.shape_count; ++k) {
|
||||||
for (const RendererState::RopaGen& g : rit->second) {
|
for (const RendererState::RopaGen& g : rit->second) {
|
||||||
if (g.seq != item.shape_seq[k]) {
|
if (g.seq != item.shape_seq[k]) {
|
||||||
|
|
@ -18434,6 +18775,19 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
if (g.verts.size() != want_floats) {
|
if (g.verts.size() != want_floats) {
|
||||||
break; // stale-size generation (re-stream/outfit swap)
|
break; // stale-size generation (re-stream/outfit swap)
|
||||||
}
|
}
|
||||||
|
if (ring_newest_t - g.t > 0.35) {
|
||||||
|
static std::atomic<uint32_t> s_stale{0};
|
||||||
|
const uint32_t ln =
|
||||||
|
s_stale.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (ln < 16 || (ln & 1023u) == 0) {
|
||||||
|
REXLOG_INFO(
|
||||||
|
"native-scene: ropa shape STALE gen dropped mesh={:08X} "
|
||||||
|
"age={:.2f}s seq_gap={} (n={})",
|
||||||
|
item.mesh, ring_newest_t - g.t,
|
||||||
|
rit->second.back().seq - g.seq, ln);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
gv[ng] = &g.verts;
|
gv[ng] = &g.verts;
|
||||||
gw[ng] = item.shape_w[k];
|
gw[ng] = item.shape_w[k];
|
||||||
total += item.shape_w[k];
|
total += item.shape_w[k];
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue