render: promote the palette serve into its own module, retire the shadow observers
This commit is contained in:
parent
4ee3c458bf
commit
cb92f6aec5
6 changed files with 1036 additions and 73 deletions
|
|
@ -231,7 +231,7 @@ set(SKATE3_COMMON_SOURCES
|
|||
src/native/skate3_native_entity.cpp
|
||||
src/native/skate3_native_guest_read.cpp
|
||||
src/native/skate3_native_lw.cpp
|
||||
src/native/skate3_native_v3_shadow.cpp
|
||||
src/native/skate3_native_palette.cpp
|
||||
src/skate3_native_debug_dialog.cpp
|
||||
src/skate3_native_render.cpp
|
||||
src/skate3_native_scene.cpp
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
// stride 0x50), mesh count = u16 at model+0x32. This is the alpha the
|
||||
// character-family shaders output (c13.x / c21.x / c22.x / c20.x).
|
||||
//
|
||||
// The walk below mirrors SnapshotLwEntity (skate3_native_v3_shadow.cpp),
|
||||
// The walk below mirrors SnapshotLwEntity (skate3_native_palette.cpp),
|
||||
// whose ctx containment matching against DrawItem::ctx was live-proven
|
||||
// (measured zero unmapped palettes).
|
||||
|
||||
|
|
|
|||
935
src/native/skate3_native_palette.cpp
Normal file
935
src/native/skate3_native_palette.cpp
Normal file
|
|
@ -0,0 +1,935 @@
|
|||
// Authoritative character bone-palette serve (see the header).
|
||||
|
||||
#include "native/skate3_native_palette.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/logging.h>
|
||||
|
||||
#include "native/skate3_native_entity.h"
|
||||
#include "native/skate3_native_guest_read.h"
|
||||
#include "skate3_native_scene.h"
|
||||
|
||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_palette_serve, true, "Skate 3",
|
||||
"Serve character bone palettes from the game's own packed "
|
||||
"m_matrices snapshots (taken at pack-function exit: the exact "
|
||||
"bytes the game's VS constant upload consumes) instead of the "
|
||||
"draw-time bank captures. Per-instance and per-piece, one sim "
|
||||
"tick, coherent by construction. Items with no mapped snapshot "
|
||||
"keep the capture pipeline's palette unchanged.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_palette_ident_primary, true, "Skate 3",
|
||||
"Entity-exact palette picks: for items the ctx -> entity "
|
||||
"identity store maps, a snapshot stamped with the SAME owning "
|
||||
"entity is identity-proven; it is exempt from the "
|
||||
"anchor/proximity caps (which exist only to reject other "
|
||||
"characters' packs) and wins over any unstamped candidate the "
|
||||
"content heuristics matched. Unmapped items and unstamped "
|
||||
"candidates keep the content matching unchanged.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
REXCVAR_DEFINE_INT32(skate3_native_render_scene_palette_log_interval, 0, "Skate 3",
|
||||
"Frames between palette-serve stats log lines (0 = off)")
|
||||
.range(0, 100000)
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
namespace skate3::native_palette {
|
||||
namespace {
|
||||
|
||||
using skate3::native_scene::DrawItem;
|
||||
using skate3::native_scene::FrameScene;
|
||||
using skate3::native_scene::GuestTryCopy;
|
||||
|
||||
// cModelInstance field offsets (sizeof 0x28).
|
||||
constexpr uint32_t kInstModel = 0x00;
|
||||
constexpr uint32_t kInstArrCtx = 0x04; // m_arrMeshContext begin
|
||||
constexpr uint32_t kInstArrCtxEnd = 0x08; // VectorBase end (validated, else 50-slot cap)
|
||||
constexpr uint32_t kInstMatrices = 0x14; // packed palette, 48-byte 3x4 rows
|
||||
constexpr uint32_t kCtxStride = 0x50;
|
||||
constexpr uint32_t kCtxSlotsFallback = 50;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Plausible guest heap/image pointer (the guest address space tops out under
|
||||
// 0x84A10000).
|
||||
bool PlausiblePtr(uint32_t p) { return p >= 0x10000 && p < 0x84A10000 && (p & 3) == 0; }
|
||||
|
||||
std::mutex g_mu; // guards every per-frame collection below
|
||||
|
||||
// Pack-exit palette snapshot: header + m_matrices rows copied into the
|
||||
// per-frame arena the moment the game finished packing them; comparing at
|
||||
// frame end is too late (m_matrices is repacked with the NEXT animation step
|
||||
// by then), so the copy happens the moment the pack function returns: those
|
||||
// bytes are exactly what the VS constant upload reads. An instance can pack
|
||||
// more than once per frame (multi-pass poses); every snapshot is kept.
|
||||
struct PackSnap {
|
||||
uint32_t inst = 0;
|
||||
// Owning presentation entity, stamped from the bracketed pack-owner
|
||||
// (every pack call sits inside an entity method whose r3 is the owner;
|
||||
// LivingWorld slices carry the writer's entity directly). 0 = the pack
|
||||
// fired outside any bracketed owner (unknown).
|
||||
uint32_t entity = 0;
|
||||
uint32_t arr_begin = 0; // m_arrMeshContext block (ctx containment key)
|
||||
uint32_t arr_bytes = 0;
|
||||
uint32_t row_ofs = 0; // offset into g_pack_rows (floats)
|
||||
uint32_t rows = 0; // 48-byte 3x4 rows copied
|
||||
// Sk8::UpdateBoneTransforms part-record snapshot (the player skater's
|
||||
// pieces): rows serve only after the per-session layout vote confirms the
|
||||
// row interpretation (see ServeAuthoritativePalettes).
|
||||
bool ubt = false;
|
||||
};
|
||||
std::vector<PackSnap> g_packs;
|
||||
std::vector<float> g_pack_rows; // host-order floats, 12 per row (arena)
|
||||
// Previous frame's snapshots: the draws of frame N may consume the palette
|
||||
// packed during frame N-1 (pack at frame end, upload next frame).
|
||||
std::vector<PackSnap> g_packs_prev;
|
||||
std::vector<float> g_pack_rows_prev;
|
||||
|
||||
uint64_t g_frames = 0; // OnFrameBuilt calls (serve-history clock)
|
||||
|
||||
// Self-validation probe health: if the cModelInstance layout fails its
|
||||
// probes the module turns itself off for the session (logged once); image
|
||||
// drift must degrade, not misread.
|
||||
std::atomic<bool> g_pal_ok{true};
|
||||
|
||||
// ---- serve stats (window; exchanged to 0 at each stats line) ---------------
|
||||
|
||||
std::atomic<uint64_t> c_pal_unreadable{0};
|
||||
std::atomic<uint64_t> c_pal_served{0};
|
||||
std::atomic<uint64_t> c_pal_serve_miss{0};
|
||||
std::atomic<uint64_t> c_pal_hyst{0};
|
||||
// Identity filter (ctx -> entity store): candidates stamped with a
|
||||
// DIFFERENT owning entity than the item's are never served (the
|
||||
// wrong-sibling/wrong-character serve class: garments tracking the wrong
|
||||
// skater).
|
||||
std::atomic<uint64_t> c_pal_serve_ident_skip{0};
|
||||
// Content gate: candidates whose served range contains a degenerate
|
||||
// (zeroed/garbage) bone row are never served; a partially-written
|
||||
// m_matrices snapshotted around instance teardown/transition serves rows
|
||||
// the game would never upload (weighted verts collapse to the origin).
|
||||
std::atomic<uint64_t> c_pal_serve_bad_rows{0};
|
||||
// Entity-exact primary picks: serves where the winning candidate carried the
|
||||
// item's own entity stamp / serves of a mapped item that had to fall back to
|
||||
// content matching / picks where the entity-exact candidate and the
|
||||
// content-matched candidate held DIFFERENT rows; each divergence is a serve
|
||||
// the proximity heuristics would have gotten wrong.
|
||||
std::atomic<uint64_t> c_pal_ident_exact{0};
|
||||
std::atomic<uint64_t> c_pal_ident_fallback{0};
|
||||
std::atomic<uint64_t> c_pal_ident_prim_div{0};
|
||||
std::atomic<uint64_t> c_ubt_snap_ok{0};
|
||||
std::atomic<uint64_t> c_ubt_rej_ptrs{0};
|
||||
std::atomic<uint64_t> c_ubt_arr_fallback{0};
|
||||
|
||||
// Every packed 3x4 row in [0, rows) must carry a sane rotation (Frobenius
|
||||
// norm-squared of the 3x3 within the bank-gate range). Packed-3x4 rows
|
||||
// only; UBT 16-float rows are gated by the layout vote instead.
|
||||
bool ServedRowsUsable(const float* r, uint32_t rows) {
|
||||
for (uint32_t i = 0; i < rows; ++i) {
|
||||
const float* m = r + size_t(i) * 12;
|
||||
float n = 0.0f;
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
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 false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// The entity whose pack-producing method is on this thread's call stack
|
||||
// (the producing entity methods are bracketed by the hook layer).
|
||||
thread_local uint32_t tl_pack_owner = 0;
|
||||
|
||||
// Sk8::UpdateBoneTransforms m_matrices layout vote: fresh bank captures
|
||||
// score the UBT rows; >=75% near/exact over 24 votes locks the
|
||||
// interpretation, otherwise UBT serving stays off.
|
||||
std::atomic<int> g_ubt_vote48{0};
|
||||
std::atomic<int> g_ubt_vote44{0};
|
||||
std::atomic<int> g_ubt_vote_other{0};
|
||||
// 0 = voting, 1 = packed 3x4, 2 = 4x4 Matrix44 (transpose on serve),
|
||||
// -1 = refused (neither interpretation matched fresh bank captures).
|
||||
std::atomic<int> g_ubt_layout{0};
|
||||
|
||||
// Highest blend index + 1 actually referenced by a mesh's vertices, cached
|
||||
// per mesh (VB content is stable; the serve needs it to pick the right
|
||||
// per-piece pack among an instance's multi-pass snapshots). g_mu-guarded.
|
||||
std::unordered_map<uint32_t, uint32_t> g_needed_rows_cache;
|
||||
|
||||
// Rate-limited detail logging: returns true for the first `first` events and
|
||||
// every `every`-th after.
|
||||
bool LogGate(std::atomic<uint64_t>& counter, uint64_t first, uint64_t every) {
|
||||
const uint64_t n = counter.fetch_add(1, std::memory_order_relaxed);
|
||||
return n < first || (every != 0 && n % every == 0);
|
||||
}
|
||||
std::atomic<uint64_t> g_log_prim{0};
|
||||
|
||||
// Self-validation probe for the cModelInstance layout: the first packed
|
||||
// palette row must look like a column-vector affine (sane rotation norms,
|
||||
// bounded translation). 16 samples; >4 failures turns the module off.
|
||||
void ProbeInstance(const float* row) {
|
||||
static std::atomic<uint32_t> s_seen{0};
|
||||
static std::atomic<uint32_t> s_fail{0};
|
||||
const uint32_t n = s_seen.fetch_add(1, std::memory_order_relaxed);
|
||||
if (n >= 16) {
|
||||
return;
|
||||
}
|
||||
bool ok = true;
|
||||
// Packed 4x3 column-vector rows: [ r.x r.y r.z t ] x3; column norms of
|
||||
// the 3x3 block land in a scale band, translations stay world-plausible.
|
||||
for (int c = 0; c < 3 && ok; ++c) {
|
||||
const float norm = std::sqrt(row[c] * row[c] + row[4 + c] * row[4 + c] +
|
||||
row[8 + c] * row[8 + c]);
|
||||
ok = norm > 1e-3f && norm < 1e3f && std::isfinite(norm);
|
||||
}
|
||||
for (int r = 0; r < 3 && ok; ++r) {
|
||||
ok = std::isfinite(row[r * 4 + 3]) && std::fabs(row[r * 4 + 3]) < 1e6f;
|
||||
}
|
||||
if (!ok) {
|
||||
s_fail.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
if (n == 15) {
|
||||
const uint32_t fails = s_fail.load(std::memory_order_relaxed);
|
||||
if (fails > 4) {
|
||||
g_pal_ok.store(false, std::memory_order_relaxed);
|
||||
REXLOG_WARN(
|
||||
"native-palette: cModelInstance layout FAILED validation ({}/16 bad "
|
||||
"first rows); palette serve disabled (image drift?)",
|
||||
fails);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy one instance's header + m_matrices rows. ubt = the record came from
|
||||
// an UpdateBoneTransforms parts array (stride 0x28, same header layout),
|
||||
// snapshotted at the hook because the parts arena AND its m_matrices buffers
|
||||
// zero by frame end.
|
||||
void SnapshotPack(uint8_t* base, uint32_t inst, bool ubt = false) {
|
||||
uint32_t head[0x28 / 4];
|
||||
if (!GuestTryCopy(head, base + inst, sizeof(head))) {
|
||||
return;
|
||||
}
|
||||
const uint32_t model = __builtin_bswap32(head[kInstModel / 4]);
|
||||
const uint32_t arr = __builtin_bswap32(head[kInstArrCtx / 4]);
|
||||
const uint32_t arr_end = __builtin_bswap32(head[kInstArrCtxEnd / 4]);
|
||||
const uint32_t matrices = __builtin_bswap32(head[kInstMatrices / 4]);
|
||||
const uint32_t count = __builtin_bswap32(head[0x18 / 4]);
|
||||
if (!PlausiblePtr(model) || !PlausiblePtr(arr) || !PlausiblePtr(matrices)) {
|
||||
if (ubt) {
|
||||
c_ubt_rej_ptrs.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
PackSnap snap;
|
||||
snap.inst = inst;
|
||||
snap.entity = tl_pack_owner;
|
||||
snap.arr_begin = arr;
|
||||
snap.ubt = ubt;
|
||||
if (arr_end > arr && (arr_end - arr) % kCtxStride == 0 &&
|
||||
(arr_end - arr) / kCtxStride <= 256) {
|
||||
snap.arr_bytes = arr_end - arr;
|
||||
} else if (ubt) {
|
||||
// Live UBT arena records don't always carry a populated end pointer.
|
||||
// Structurally each part owns exactly ONE MeshContext, so a one-slot
|
||||
// window is the safe assumption; it can only ever match the piece's
|
||||
// own ctx.
|
||||
snap.arr_bytes = kCtxStride;
|
||||
c_ubt_arr_fallback.fetch_add(1, std::memory_order_relaxed);
|
||||
} else {
|
||||
snap.arr_bytes = kCtxSlotsFallback * kCtxStride;
|
||||
}
|
||||
// +0x18 tracked as the pack count: it equals the highest blend index the
|
||||
// mesh references, so it is trusted within bounds.
|
||||
snap.rows = (count >= 1 && count <= 96) ? count : 84;
|
||||
// UBT snapshots copy 16 floats/row: rw::math::vpu::Matrix44 is a 64-byte
|
||||
// 4x4, and UpdateBoneTransforms (unlike PackAndMultiply) may leave
|
||||
// m_matrices in that shape; the serve's layout vote evaluates BOTH the
|
||||
// packed-3x4 (12-float rows over the same contiguous span) and the
|
||||
// 4x4-transposed interpretation from this one copy.
|
||||
const uint32_t fpr = ubt ? 16u : 12u;
|
||||
uint32_t raw[96 * 16];
|
||||
if (!GuestTryCopy(raw, base + matrices, size_t(snap.rows) * fpr * 4)) {
|
||||
c_pal_unreadable.fetch_add(1, std::memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_mu);
|
||||
if (g_packs.size() >= 512 || g_pack_rows.size() > 512 * 1024) {
|
||||
return; // growth cap (scene path off / frame never built)
|
||||
}
|
||||
snap.row_ofs = uint32_t(g_pack_rows.size());
|
||||
g_pack_rows.resize(g_pack_rows.size() + size_t(snap.rows) * fpr);
|
||||
float* dst = g_pack_rows.data() + snap.row_ofs;
|
||||
for (uint32_t i = 0; i < snap.rows * fpr; ++i) {
|
||||
dst[i] = std::bit_cast<float>(__builtin_bswap32(raw[i]));
|
||||
}
|
||||
g_packs.push_back(snap);
|
||||
if (ubt) {
|
||||
c_ubt_snap_ok.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
// Layout self-probe on the first pack-hook snapshot only: UBT rows carry
|
||||
// their own vote-based validation (a failed probe here would kill the
|
||||
// whole module for the session on an unproven layout).
|
||||
static std::atomic<bool> s_probed{false};
|
||||
if (!ubt && !s_probed.exchange(true)) {
|
||||
ProbeInstance(dst);
|
||||
}
|
||||
}
|
||||
|
||||
// LOD pedestrians. The LivingWorld pack writer fuses UpdateBoneTransforms +
|
||||
// PackAndMultiply for cLivingWorldPresEntity-derived classes (their vtable
|
||||
// slot +16 is an Update thunk, so they never reach the pack/UBT hooks) and
|
||||
// writes the CONCATENATION of per-mesh packed palettes into m_matrices;
|
||||
// mesh k's rows start at sum(boneCount(0..k-1)). Each mesh slice becomes
|
||||
// its own PackSnap keyed to exactly its MeshContext, so the containment
|
||||
// matching works unchanged.
|
||||
void SnapshotLwEntity(uint8_t* base, uint32_t entity) {
|
||||
uint32_t idx = 0, count = 0, arr = 0;
|
||||
if (!LoadU32(base, entity + 16, &idx) || idx > 1024 ||
|
||||
!LoadU32(base, entity + (idx + 5) * 4, &count) ||
|
||||
!LoadU32(base, entity + (idx + 8) * 4, &arr) || count == 0 || count > 8 ||
|
||||
!PlausiblePtr(arr)) {
|
||||
return;
|
||||
}
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
const uint32_t rec = arr + i * 0x28;
|
||||
uint32_t head[0x28 / 4];
|
||||
if (!GuestTryCopy(head, base + rec, sizeof(head))) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t model = __builtin_bswap32(head[kInstModel / 4]);
|
||||
const uint32_t actx = __builtin_bswap32(head[kInstArrCtx / 4]);
|
||||
const uint32_t matrices = __builtin_bswap32(head[kInstMatrices / 4]);
|
||||
if (!PlausiblePtr(model) || !PlausiblePtr(actx) || !PlausiblePtr(matrices)) {
|
||||
continue;
|
||||
}
|
||||
// Model walk mirrors the writer's own: mesh table at +0x24 (ptr array,
|
||||
// stride 4), mesh count u16 at +0x32, bone-count gate u16 at +0x34
|
||||
// (<= 80, same gate the writer applies). Every bone count must land in
|
||||
// 1..96; a wrong table shape fails here and the instance is skipped.
|
||||
uint32_t count_w = 0, table = 0, gate_w = 0;
|
||||
if (!LoadU32(base, model + 0x30, &count_w) ||
|
||||
!LoadU32(base, model + 0x24, &table) ||
|
||||
!LoadU32(base, model + 0x34, &gate_w)) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t nmesh = count_w & 0xFFFF;
|
||||
if (nmesh == 0 || nmesh > 16 || (gate_w >> 16) > 80) {
|
||||
continue;
|
||||
}
|
||||
uint32_t bones[16];
|
||||
uint32_t total = 0;
|
||||
bool ok = true;
|
||||
for (uint32_t k = 0; k < nmesh && ok; ++k) {
|
||||
uint32_t mesh = 0, bc = 0;
|
||||
ok = LoadU32(base, table + k * 4, &mesh) && PlausiblePtr(mesh) &&
|
||||
LoadU32(base, mesh + 0x48, &bc) && bc >= 1 && bc <= 96;
|
||||
if (ok) {
|
||||
bones[k] = bc;
|
||||
total += bc;
|
||||
}
|
||||
}
|
||||
if (!ok || total == 0 || total > 96) {
|
||||
continue;
|
||||
}
|
||||
uint32_t raw[96 * 12];
|
||||
if (!GuestTryCopy(raw, base + matrices, size_t(total) * 48)) {
|
||||
continue;
|
||||
}
|
||||
// Byteswap OUTSIDE g_mu: this runs on the sim thread, and holding the
|
||||
// lock through a multi-KB swap loop stalls the guest render thread's
|
||||
// per-frame palette consumers.
|
||||
float host[96 * 12];
|
||||
for (uint32_t f = 0; f < total * 12; ++f) {
|
||||
host[f] = std::bit_cast<float>(__builtin_bswap32(raw[f]));
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_mu);
|
||||
if (g_packs.size() + nmesh > 512 || g_pack_rows.size() > 512 * 1024) {
|
||||
return;
|
||||
}
|
||||
const uint32_t ofs = uint32_t(g_pack_rows.size());
|
||||
g_pack_rows.resize(g_pack_rows.size() + size_t(total) * 12);
|
||||
std::memcpy(g_pack_rows.data() + ofs, host, size_t(total) * 12 * sizeof(float));
|
||||
uint32_t row = 0;
|
||||
for (uint32_t k = 0; k < nmesh; ++k) {
|
||||
PackSnap snap;
|
||||
snap.inst = rec;
|
||||
snap.entity = entity;
|
||||
snap.arr_begin = actx + k * kCtxStride; // exactly this mesh's ctx
|
||||
snap.arr_bytes = kCtxStride;
|
||||
snap.row_ofs = ofs + row * 12;
|
||||
snap.rows = bones[k];
|
||||
g_packs.push_back(snap);
|
||||
row += bones[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timing-tolerant pose compare, used by the UBT layout vote: the game packs
|
||||
// per render frame (interpolated poses), so a bank captured at draw time
|
||||
// legitimately sits within ~1-2 animation steps of any single-point
|
||||
// snapshot; translations move up to ~0.2/step at skating speed, rotation
|
||||
// elements only a few hundredths. Wrong rows/layout show rotation deltas
|
||||
// O(1) and translation deltas of whole limb offsets.
|
||||
struct RowScore {
|
||||
float max_rot = 1e30f; // max delta over the 3x3 rotation elements
|
||||
float max_trans = 1e30f; // max delta over the translation lane (f%4==3)
|
||||
bool exact = false;
|
||||
bool near = false;
|
||||
};
|
||||
|
||||
void ScoreRows(const float* auth, uint32_t nrows, const DrawItem& item,
|
||||
RowScore* out) {
|
||||
float max_rot = 0.0f, max_trans = 0.0f, max_diff = 0.0f;
|
||||
for (uint32_t r = 0; r < nrows; ++r) {
|
||||
for (int f = 0; f < 12; ++f) {
|
||||
const float diff = std::fabs(auth[r * 12 + f] - item.bones[r * 12 + f]);
|
||||
((f % 4 == 3) ? max_trans : max_rot) =
|
||||
std::max((f % 4 == 3) ? max_trans : max_rot, diff);
|
||||
max_diff = std::max(max_diff, diff);
|
||||
}
|
||||
}
|
||||
out->max_rot = max_rot;
|
||||
out->max_trans = max_trans;
|
||||
out->exact = max_diff <= 2.5e-3f;
|
||||
// Measured pose-step deltas top out ~0.10 rotation / ~0.2 translation
|
||||
// during tricks at 140 fps; wrong rows/layout show rotation deltas
|
||||
// O(0.5-2.0) and limb-offset translations; the bands sit between.
|
||||
out->near = max_rot <= 0.25f && max_trans <= 1.0f;
|
||||
}
|
||||
|
||||
// Highest blend index + 1 referenced by the mesh's own vertices, cached per
|
||||
// mesh under g_mu (VB content is stable per mesh asset). 0 = undeterminable.
|
||||
uint32_t NeededRowsCached(uint8_t* base, const DrawItem& item) {
|
||||
const auto it = g_needed_rows_cache.find(item.mesh);
|
||||
if (it != g_needed_rows_cache.end()) {
|
||||
return it->second;
|
||||
}
|
||||
skate3::native_scene::SkinSampleVert sv[32];
|
||||
uint32_t needed = 0;
|
||||
if (skate3::native_scene::ReadSkinSamplesGuest(base, item, 32, sv)) {
|
||||
needed = 1;
|
||||
for (const auto& s : sv) {
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
if (s.w[k] > 0 && uint32_t(s.bone[k]) + 1 > needed) {
|
||||
needed = uint32_t(s.bone[k]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (g_needed_rows_cache.size() >= 2048) {
|
||||
g_needed_rows_cache.clear(); // mesh addresses recycle across map loads
|
||||
}
|
||||
g_needed_rows_cache.emplace(item.mesh, needed);
|
||||
return needed;
|
||||
}
|
||||
|
||||
// 64-byte row-vector Matrix44 (translation in row 3) -> our column-vector
|
||||
// [R|t] 3x4 rows (transpose the 3x3, splice the translation).
|
||||
void Convert44Rows(const float* in, uint32_t rows_n, float* out) {
|
||||
for (uint32_t r = 0; r < rows_n; ++r) {
|
||||
const float* m = in + r * 16;
|
||||
float* o = out + r * 12;
|
||||
o[0] = m[0]; o[1] = m[4]; o[2] = m[8]; o[3] = m[12];
|
||||
o[4] = m[1]; o[5] = m[5]; o[6] = m[9]; o[7] = m[13];
|
||||
o[8] = m[2]; o[9] = m[6]; o[10] = m[10]; o[11] = m[14];
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t HashRows(const float* p, size_t nfloats) {
|
||||
uint64_t h = 1469598103934665603ull;
|
||||
const uint8_t* b = reinterpret_cast<const uint8_t*>(p);
|
||||
for (size_t i = 0; i < nfloats * sizeof(float); ++i) {
|
||||
h = (h ^ b[i]) * 1099511628211ull;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
// Per-instance serve history for the hysteresis (ctx-keyed, g_mu-guarded).
|
||||
struct ServeHist {
|
||||
uint64_t h1 = 0; // last served palette hash
|
||||
uint64_t h2 = 0; // previous-but-one
|
||||
uint64_t frame = 0;
|
||||
float t[3] = {}; // last served bone-0 translation (the identity anchor)
|
||||
};
|
||||
std::unordered_map<uint32_t, ServeHist> g_serve_hist;
|
||||
|
||||
uint32_t ServeAuthoritativePalettesImpl(uint8_t* base,
|
||||
skate3::native_scene::FrameScene& scene) {
|
||||
if (!REXCVAR_GET(skate3_native_render_scene_palette_serve) ||
|
||||
!g_pal_ok.load(std::memory_order_relaxed)) {
|
||||
return 0;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_mu);
|
||||
if (g_packs.empty() && g_packs_prev.empty()) {
|
||||
return 0;
|
||||
}
|
||||
uint32_t served = 0;
|
||||
for (DrawItem& item : scene.items) {
|
||||
// Pending items are half-built (the fixup/rescue machinery owns them);
|
||||
// LW-store substitutions (dbg_src 10) are already authoritative.
|
||||
if (!item.skinned || item.ctx == 0 || item.bones.size() < 12 ||
|
||||
item.pending || item.dbg_src == 10) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t published = uint32_t(item.bones.size() / 12);
|
||||
const uint32_t needed = NeededRowsCached(base, item);
|
||||
// Identity filter: when the ctx -> entity store maps this item, any
|
||||
// candidate stamped with a DIFFERENT owning entity is structurally the
|
||||
// wrong character; no containment/content/proximity evidence can
|
||||
// rehabilitate it. Unstamped candidates and unmapped items keep the
|
||||
// content matching below.
|
||||
skate3::native_entity::CtxInfo ident;
|
||||
const bool ident_mapped =
|
||||
skate3::native_entity::LookupCtx(item.ctx, &ident);
|
||||
// Entity-exact primary: a candidate stamped with the item's own entity
|
||||
// is identity-proven, so the anchor/proximity caps (which exist to
|
||||
// reject OTHER characters' packs) do not apply to it, and it outranks
|
||||
// any unstamped candidate the content heuristics matched. Tracked as a
|
||||
// separate pick so the content matching below stays bit-identical for
|
||||
// unmapped items and as the fallback.
|
||||
const bool ident_primary =
|
||||
ident_mapped && ident.entity != 0 &&
|
||||
REXCVAR_GET(skate3_native_render_scene_palette_ident_primary);
|
||||
const PackSnap* pick = nullptr;
|
||||
const float* pick_rows = nullptr;
|
||||
const PackSnap* pick_x = nullptr;
|
||||
const float* pick_x_rows = nullptr;
|
||||
// Ring of the last few matching candidates (any pass, any sibling): the
|
||||
// hysteresis below may switch the serve to whichever candidate
|
||||
// CONTINUES the previous serve bit-exactly.
|
||||
struct Cand {
|
||||
const float* rows;
|
||||
uint32_t nrows;
|
||||
bool ubt;
|
||||
};
|
||||
Cand ring[8];
|
||||
uint32_t ring_n = 0;
|
||||
// Newest set first: this frame's snapshots, then the previous frame's
|
||||
// (an instance that didn't pack this frame holds its last pose anyway).
|
||||
// WITHIN a set, the LAST matching pack wins: an instance packs once per
|
||||
// PASS (casters first, sometimes a stale-sample pose, main pass
|
||||
// last), and first-match picking alternates between passes with the
|
||||
// per-frame pack order. The last pack is the main-pass pose, i.e. what
|
||||
// the game's own main draw consumes. Same-count sibling pieces of one
|
||||
// instance-wide window are disambiguated by bone-0 translation against
|
||||
// the item's captured bone-0 (different joints sit
|
||||
// decimeters-to-meters apart; near-ties within 5 cm are the same joint,
|
||||
// where the later pack still wins).
|
||||
// Identity anchor for proximity matching: the last SERVED bone-0
|
||||
// translation when fresh (stable across bank-capture corruption), the
|
||||
// item's captured bone-0 otherwise.
|
||||
float anchor[3] = {item.bones[3], item.bones[7], item.bones[11]};
|
||||
{
|
||||
const auto hit = g_serve_hist.find(item.ctx);
|
||||
if (hit != g_serve_hist.end() && g_frames - hit->second.frame <= 60 &&
|
||||
(hit->second.t[0] != 0.0f || hit->second.t[1] != 0.0f ||
|
||||
hit->second.t[2] != 0.0f)) {
|
||||
// A fresh bank capture is identity ground truth: when it sits meters
|
||||
// from the served-history anchor, the anchor has been tracking
|
||||
// ANOTHER character's packs (each wrong serve re-stamps the anchor
|
||||
// at the wrong joint, so the loop is self-sustaining). Bank-capture
|
||||
// corruption the anchor exists to ride out is sub-meter; a multi-
|
||||
// meter disagreement with a same-frame capture means the anchor is
|
||||
// the wrong side.
|
||||
const float ax = item.bones[3] - hit->second.t[0];
|
||||
const float ay = item.bones[7] - hit->second.t[1];
|
||||
const float az = item.bones[11] - hit->second.t[2];
|
||||
if (!(item.dbg_src <= 2 && ax * ax + ay * ay + az * az > 9.0f)) {
|
||||
anchor[0] = hit->second.t[0];
|
||||
anchor[1] = hit->second.t[1];
|
||||
anchor[2] = hit->second.t[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int set = 0; set < 2; ++set) {
|
||||
// The previous-frame set is searched only while the deciding pick is
|
||||
// still open (entity-exact when primary applies, content otherwise). A
|
||||
// content pick found in an earlier set is locked; later sets may only
|
||||
// add the entity-exact pick on top of it.
|
||||
if (ident_primary ? pick_x != nullptr : pick != nullptr) {
|
||||
break;
|
||||
}
|
||||
const bool legacy_open = pick == nullptr;
|
||||
const auto& packs = set == 0 ? g_packs : g_packs_prev;
|
||||
const auto& rows = set == 0 ? g_pack_rows : g_pack_rows_prev;
|
||||
float best_d = 1e30f;
|
||||
float best_dx = 1e30f;
|
||||
for (const PackSnap& snap : packs) {
|
||||
if (ident_mapped && snap.entity != 0 && snap.entity != ident.entity) {
|
||||
c_pal_serve_ident_skip.fetch_add(1, std::memory_order_relaxed);
|
||||
continue;
|
||||
}
|
||||
const bool contains =
|
||||
item.ctx >= snap.arr_begin &&
|
||||
item.ctx < snap.arr_begin + snap.arr_bytes &&
|
||||
(item.ctx - snap.arr_begin) % kCtxStride == 0;
|
||||
if (snap.ubt && !contains) {
|
||||
// Live UBT arena records reference per-frame ARENA MeshContext
|
||||
// copies, never the persistent ctx the scene items carry; so ctx
|
||||
// containment cannot map them. Match by CONTENT instead: the
|
||||
// game's own packed bone count names the piece, joint proximity
|
||||
// to the anchor names the character. Translation is read under
|
||||
// BOTH row interpretations while the layout vote is still open
|
||||
// (packed-3x4: floats 3/7/11; 4x4 row-vector: floats 12/13/14 of
|
||||
// the 16-float row).
|
||||
if (needed == 0 || snap.rows != needed) {
|
||||
continue;
|
||||
}
|
||||
const float* r = rows.data() + snap.row_ofs;
|
||||
const float da = std::sqrt((r[3] - anchor[0]) * (r[3] - anchor[0]) +
|
||||
(r[7] - anchor[1]) * (r[7] - anchor[1]) +
|
||||
(r[11] - anchor[2]) * (r[11] - anchor[2]));
|
||||
const float db = std::sqrt((r[12] - anchor[0]) * (r[12] - anchor[0]) +
|
||||
(r[13] - anchor[1]) * (r[13] - anchor[1]) +
|
||||
(r[14] - anchor[2]) * (r[14] - anchor[2]));
|
||||
const float d = std::min(da, db);
|
||||
// UBT rows serve only after the layout vote resolves; until then
|
||||
// a UBT candidate must not claim the entity-exact pick (it would
|
||||
// displace a servable pack candidate and leave the item on its
|
||||
// captured palette while the vote is still open).
|
||||
const bool entity_exact =
|
||||
ident_primary && snap.entity == ident.entity &&
|
||||
g_ubt_layout.load(std::memory_order_relaxed) > 0;
|
||||
if (d > 1.5f && !entity_exact) {
|
||||
continue; // another character's piece
|
||||
}
|
||||
ring[ring_n++ & 7] = {r, snap.rows, true};
|
||||
if (entity_exact && d < best_dx + 0.05f) {
|
||||
pick_x = &snap;
|
||||
pick_x_rows = r;
|
||||
best_dx = std::min(best_dx, d);
|
||||
}
|
||||
if (legacy_open && d <= 1.5f && d < best_d + 0.05f) {
|
||||
pick = &snap;
|
||||
pick_rows = r;
|
||||
best_d = std::min(best_d, d);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!contains) {
|
||||
continue;
|
||||
}
|
||||
if (snap.arr_bytes == kCtxStride) {
|
||||
// Exact per-piece ctx window (UBT part / LW slice): unambiguous;
|
||||
// keep overwriting so the frame's last pack wins.
|
||||
const float* r = rows.data() + snap.row_ofs;
|
||||
if (!snap.ubt && !ServedRowsUsable(r, snap.rows)) {
|
||||
c_pal_serve_bad_rows.fetch_add(1, std::memory_order_relaxed);
|
||||
continue;
|
||||
}
|
||||
if (legacy_open) {
|
||||
pick = &snap;
|
||||
pick_rows = r;
|
||||
best_d = 0.0f;
|
||||
}
|
||||
if (ident_primary && snap.entity == ident.entity &&
|
||||
(!snap.ubt ||
|
||||
g_ubt_layout.load(std::memory_order_relaxed) > 0)) {
|
||||
pick_x = &snap;
|
||||
pick_x_rows = r;
|
||||
best_dx = 0.0f;
|
||||
}
|
||||
ring[ring_n++ & 7] = {r, snap.rows, snap.ubt};
|
||||
continue;
|
||||
}
|
||||
// Instance-wide window (PackAndMultiply): the packed bone count
|
||||
// names the piece among the instance's per-piece packs.
|
||||
if (needed == 0 || snap.rows != needed) {
|
||||
continue;
|
||||
}
|
||||
const float* r = rows.data() + snap.row_ofs;
|
||||
if (!ServedRowsUsable(r, snap.rows)) {
|
||||
c_pal_serve_bad_rows.fetch_add(1, std::memory_order_relaxed);
|
||||
continue;
|
||||
}
|
||||
const float dx = r[3] - anchor[0];
|
||||
const float dy = r[7] - anchor[1];
|
||||
const float dz = r[11] - anchor[2];
|
||||
const float d = std::sqrt(dx * dx + dy * dy + dz * dz);
|
||||
const bool entity_exact = ident_primary && snap.entity == ident.entity;
|
||||
if (d > 1.5f && !entity_exact) {
|
||||
// Same identity cap as the content-matched path: ctx containment
|
||||
// is not proof of identity when instance arrays are reused across
|
||||
// presentation contexts (menu demos), and serving a far pack
|
||||
// teleports the piece to the other character. With no in-range
|
||||
// candidate the item keeps its captured palette.
|
||||
continue;
|
||||
}
|
||||
ring[ring_n++ & 7] = {r, snap.rows, snap.ubt};
|
||||
if (entity_exact && d < best_dx + 0.05f) {
|
||||
pick_x = &snap;
|
||||
pick_x_rows = r;
|
||||
best_dx = std::min(best_dx, d);
|
||||
}
|
||||
if (legacy_open && d <= 1.5f && d < best_d + 0.05f) {
|
||||
pick = &snap;
|
||||
pick_rows = r;
|
||||
best_d = std::min(best_d, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Promote the entity-exact pick over the content match. A divergence
|
||||
// between the two IS the wrong-sibling serve class the primary mode
|
||||
// exists to eliminate; count and (capped) name it.
|
||||
bool served_exact = false;
|
||||
if (pick_x != nullptr) {
|
||||
if (pick != nullptr && pick_rows != pick_x_rows) {
|
||||
bool differs = pick->ubt != pick_x->ubt || pick->rows != pick_x->rows;
|
||||
if (!differs) {
|
||||
const size_t nf = size_t(std::min(published, pick->rows)) *
|
||||
(pick->ubt ? 16 : 12);
|
||||
differs =
|
||||
std::memcmp(pick_rows, pick_x_rows, nf * sizeof(float)) != 0;
|
||||
}
|
||||
if (differs) {
|
||||
c_pal_ident_prim_div.fetch_add(1, std::memory_order_relaxed);
|
||||
if (LogGate(g_log_prim, 8, 4096)) {
|
||||
REXLOG_INFO(
|
||||
"native-palette: entity-exact pick OVERRODE content match "
|
||||
"mesh={:08X} ctx={:08X} ent={:08X} content(ent={:08X} "
|
||||
"rows={} ubt={} t=({:.2f},{:.2f},{:.2f})) exact(rows={} "
|
||||
"ubt={} t=({:.2f},{:.2f},{:.2f}))",
|
||||
item.mesh, item.ctx, ident.entity, pick->entity, pick->rows,
|
||||
pick->ubt, pick_rows[3], pick_rows[7], pick_rows[11],
|
||||
pick_x->rows, pick_x->ubt, pick_x_rows[3], pick_x_rows[7],
|
||||
pick_x_rows[11]);
|
||||
}
|
||||
}
|
||||
}
|
||||
pick = pick_x;
|
||||
pick_rows = pick_x_rows;
|
||||
served_exact = true;
|
||||
}
|
||||
if (pick == nullptr) {
|
||||
c_pal_serve_miss.fetch_add(1, std::memory_order_relaxed);
|
||||
continue;
|
||||
}
|
||||
uint32_t n = std::min(published, pick->rows);
|
||||
float conv[96 * 12]; // loop-scoped: pick_rows may alias it below
|
||||
if (pick->ubt) {
|
||||
// UpdateBoneTransforms writes rw::math::vpu::Matrix44 (64-byte 4x4)
|
||||
// records; whether m_matrices holds those raw or already-packed 3x4
|
||||
// rows is image-dependent; vote BOTH interpretations against FRESH
|
||||
// bank captures (the game's own upload bytes) before ever serving.
|
||||
// The snapshot copied 16 floats/row, so A = the same span sliced as
|
||||
// contiguous 12-float rows, B = 4x4 rows transposed (row-vector M,
|
||||
// translation in row 3 -> our column-vector [R|t] rows).
|
||||
const int layout = g_ubt_layout.load(std::memory_order_relaxed);
|
||||
const uint32_t vn = std::min(n, needed ? needed : n);
|
||||
if (layout == 0) {
|
||||
if (item.dbg_src <= 2 && !item.caster_bank && vn > 0) {
|
||||
RowScore sa, sb;
|
||||
ScoreRows(pick_rows, vn, item, &sa);
|
||||
Convert44Rows(pick_rows, vn, conv);
|
||||
ScoreRows(conv, vn, item, &sb);
|
||||
((sa.exact || sa.near) ? g_ubt_vote48
|
||||
: (sb.exact || sb.near) ? g_ubt_vote44
|
||||
: g_ubt_vote_other)
|
||||
.fetch_add(1, std::memory_order_relaxed);
|
||||
const int va = g_ubt_vote48.load(std::memory_order_relaxed);
|
||||
const int vb = g_ubt_vote44.load(std::memory_order_relaxed);
|
||||
const int vo = g_ubt_vote_other.load(std::memory_order_relaxed);
|
||||
if (va + vb + vo >= 24) {
|
||||
const int total = va + vb + vo;
|
||||
const int lock = va * 4 >= total * 3 ? 1 : vb * 4 >= total * 3 ? 2 : -1;
|
||||
g_ubt_layout.store(lock, std::memory_order_relaxed);
|
||||
REXLOG_INFO(
|
||||
"native-palette: UpdateBoneTransforms m_matrices layout {} "
|
||||
"(votes: packed-3x4 {} / 4x4-transposed {} / other {})",
|
||||
lock == 1 ? "confirmed packed 3x4; player pieces now served"
|
||||
: lock == 2 ? "confirmed 4x4 Matrix44; player pieces now "
|
||||
"served via transpose"
|
||||
: "REFUSED (neither read matches); UBT serving "
|
||||
"disabled this session",
|
||||
va, vb, vo);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (layout < 0) {
|
||||
continue;
|
||||
}
|
||||
if (layout == 2) {
|
||||
Convert44Rows(pick_rows, n, conv);
|
||||
pick_rows = conv;
|
||||
}
|
||||
// layout == 1: contiguous packed 3x4; serve pick_rows as-is.
|
||||
}
|
||||
// Serve-history hysteresis: packs run from sim jobs, so per-frame pack
|
||||
// ORDER is scheduler-dependent; no order-based pick is stable, and
|
||||
// anchoring on the item's captured bone-0 follows the very bank
|
||||
// alternation being replaced. Content rule instead: never serve a
|
||||
// bit-exact RETURN to the previous-but-one serve while some candidate
|
||||
// CONTINUES the last serve; every candidate is a real pack of this
|
||||
// piece, so re-serving the last pose is always valid, and when the sim
|
||||
// ticks forward no candidate matches h2 so fresh poses serve
|
||||
// immediately (the stream can hold or advance, never step backward).
|
||||
uint64_t h = HashRows(pick_rows, size_t(n) * 12);
|
||||
if (g_serve_hist.size() > 4096) {
|
||||
for (auto it = g_serve_hist.begin(); it != g_serve_hist.end();) {
|
||||
it = g_frames - it->second.frame > 1200 ? g_serve_hist.erase(it)
|
||||
: std::next(it);
|
||||
}
|
||||
}
|
||||
ServeHist& hist = g_serve_hist[item.ctx];
|
||||
if (hist.h1 != 0 && h == hist.h2 && h != hist.h1) {
|
||||
float cbuf[96 * 12];
|
||||
const int layout = g_ubt_layout.load(std::memory_order_relaxed);
|
||||
for (uint32_t ci = 0; ci < std::min<uint32_t>(ring_n, 8); ++ci) {
|
||||
const Cand& c = ring[ci];
|
||||
if (std::min(published, c.nrows) != n) {
|
||||
continue;
|
||||
}
|
||||
const float* cr = c.rows;
|
||||
if (c.ubt && layout == 2) {
|
||||
Convert44Rows(c.rows, n, cbuf);
|
||||
cr = cbuf;
|
||||
}
|
||||
if (HashRows(cr, size_t(n) * 12) == hist.h1) {
|
||||
std::memcpy(item.bones.data(), cr, size_t(n) * 12 * sizeof(float));
|
||||
std::fill(item.bones.begin() + size_t(n) * 12, item.bones.end(),
|
||||
0.0f);
|
||||
item.caster_bank = false;
|
||||
item.dbg_src = 11;
|
||||
++served;
|
||||
c_pal_hyst.fetch_add(1, std::memory_order_relaxed);
|
||||
hist.frame = g_frames;
|
||||
hist.t[0] = cr[3];
|
||||
hist.t[1] = cr[7];
|
||||
hist.t[2] = cr[11];
|
||||
goto next_item;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (h != hist.h1) {
|
||||
hist.h2 = hist.h1;
|
||||
hist.h1 = h;
|
||||
}
|
||||
hist.frame = g_frames;
|
||||
hist.t[0] = pick_rows[3];
|
||||
hist.t[1] = pick_rows[7];
|
||||
hist.t[2] = pick_rows[11];
|
||||
std::memcpy(item.bones.data(), pick_rows, size_t(n) * 12 * sizeof(float));
|
||||
// Zero the tail: rows beyond the game's own packed count are never
|
||||
// indexed by the mesh, and leftover bank-capture garbage there would
|
||||
// read as palette churn to any downstream consumer.
|
||||
std::fill(item.bones.begin() + size_t(n) * 12, item.bones.end(), 0.0f);
|
||||
// The palette no longer comes from a caster bank; downstream stale-class
|
||||
// gates (LW substitution, mesh-cache refusal) must treat it as fresh.
|
||||
item.caster_bank = false;
|
||||
item.dbg_src = 11;
|
||||
if (served_exact) {
|
||||
c_pal_ident_exact.fetch_add(1, std::memory_order_relaxed);
|
||||
} else if (ident_primary) {
|
||||
c_pal_ident_fallback.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
++served;
|
||||
next_item:;
|
||||
}
|
||||
c_pal_served.fetch_add(served, std::memory_order_relaxed);
|
||||
return served;
|
||||
}
|
||||
|
||||
void MaybeLog() {
|
||||
const int32_t interval =
|
||||
REXCVAR_GET(skate3_native_render_scene_palette_log_interval);
|
||||
if (interval <= 0 || g_frames % uint64_t(interval) != 0) {
|
||||
return;
|
||||
}
|
||||
const auto x = [](std::atomic<uint64_t>& a) {
|
||||
return a.exchange(0, std::memory_order_relaxed);
|
||||
};
|
||||
REXLOG_INFO(
|
||||
"native-palette: frame {} srv[hit={} miss={} hyst={} ident_skip={} "
|
||||
"bad_rows={} eprim={}/{}/{} unread={}] ubt[48={} 44={} o={} lay={} "
|
||||
"snap={} arr={} rej={}]",
|
||||
g_frames, x(c_pal_served), x(c_pal_serve_miss), x(c_pal_hyst),
|
||||
x(c_pal_serve_ident_skip), x(c_pal_serve_bad_rows), x(c_pal_ident_exact),
|
||||
x(c_pal_ident_fallback), x(c_pal_ident_prim_div), x(c_pal_unreadable),
|
||||
g_ubt_vote48.load(std::memory_order_relaxed),
|
||||
g_ubt_vote44.load(std::memory_order_relaxed),
|
||||
g_ubt_vote_other.load(std::memory_order_relaxed),
|
||||
g_ubt_layout.load(std::memory_order_relaxed), x(c_ubt_snap_ok),
|
||||
x(c_ubt_arr_fallback), x(c_ubt_rej_ptrs));
|
||||
}
|
||||
|
||||
bool CaptureActive() {
|
||||
return REXCVAR_GET(skate3_native_render_scene_palette_serve) &&
|
||||
g_pal_ok.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t ServeAuthoritativePalettes(uint8_t* base,
|
||||
skate3::native_scene::FrameScene& scene) {
|
||||
return ServeAuthoritativePalettesImpl(base, scene);
|
||||
}
|
||||
|
||||
uint32_t ExchangePackOwner(uint32_t entity) {
|
||||
const uint32_t prev = tl_pack_owner;
|
||||
tl_pack_owner = entity;
|
||||
return prev;
|
||||
}
|
||||
|
||||
void OnPackPalette(uint8_t* base, uint32_t instance) {
|
||||
if (instance == 0 || !CaptureActive()) {
|
||||
return;
|
||||
}
|
||||
SnapshotPack(base, instance);
|
||||
}
|
||||
|
||||
void OnBoneTransforms(uint8_t* base, uint32_t parts, uint32_t count) {
|
||||
if (parts == 0 || count == 0 || count > 64 || !CaptureActive()) {
|
||||
return;
|
||||
}
|
||||
// Snapshot every part record NOW: the parts array is per-frame arena
|
||||
// scratch (all fields read zero by frame end), so this hook exit is the
|
||||
// only moment the player skater's per-piece palettes are readable. Each
|
||||
// part becomes a per-piece PackSnap (its ctx window is exactly one
|
||||
// MeshContext), which maps the player through the same containment
|
||||
// matching as every other instance.
|
||||
for (uint32_t k = 0; k < count; ++k) {
|
||||
SnapshotPack(base, parts + k * 0x28, /*ubt=*/true);
|
||||
}
|
||||
}
|
||||
|
||||
void OnLwPack(uint8_t* base, uint32_t entity) {
|
||||
if (entity == 0 || !CaptureActive()) {
|
||||
return;
|
||||
}
|
||||
SnapshotLwEntity(base, entity);
|
||||
}
|
||||
|
||||
void OnFrameBuilt() {
|
||||
++g_frames;
|
||||
{
|
||||
// Rotate this frame's snapshots into the previous-frame slot: the serve
|
||||
// at frame N reads g_packs_prev from frame N-1.
|
||||
std::lock_guard<std::mutex> lock(g_mu);
|
||||
g_packs_prev = std::move(g_packs);
|
||||
g_pack_rows_prev = std::move(g_pack_rows);
|
||||
g_packs.clear();
|
||||
g_pack_rows.clear();
|
||||
}
|
||||
MaybeLog();
|
||||
}
|
||||
|
||||
} // namespace skate3::native_palette
|
||||
68
src/native/skate3_native_palette.h
Normal file
68
src/native/skate3_native_palette.h
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
#pragma once
|
||||
|
||||
// Authoritative character bone-palette serve.
|
||||
//
|
||||
// The game packs each skinned character's bone palette into
|
||||
// cModelInstance.m_matrices (48-byte 3x4 rows) right before uploading it to
|
||||
// the vertex shader. This module snapshots those rows the moment the game
|
||||
// finishes packing them, per instance, per piece, one sim tick, coherent by
|
||||
// construction, and serves them to the scene builder in place of the
|
||||
// draw-time constant-bank captures, which can be stale or belong to another
|
||||
// character (multi-pass draws, shadow casters, shared banks).
|
||||
//
|
||||
// Snapshot sources (hooked at function exit, when m_matrices is settled):
|
||||
// - cModelInstance::PackAndMultiplyMatricesForUpload: most characters.
|
||||
// - Sk8::UpdateBoneTransforms: the player skater's per-piece part records
|
||||
// (a per-frame arena that zeroes by frame end, so the hook exit is the
|
||||
// only readable moment). Row layout is confirmed by a per-session vote
|
||||
// against fresh bank captures before these rows are ever served.
|
||||
// - The LivingWorld batch pack writer: LOD pedestrians (fused
|
||||
// UpdateBoneTransforms+Pack; per-mesh slices of one concatenated buffer).
|
||||
//
|
||||
// Items are matched to snapshots by MeshContext containment in the owning
|
||||
// instance's m_arrMeshContext window, refined by the ctx -> entity identity
|
||||
// store (skate3_native_entity.h) and joint-proximity/bone-count content
|
||||
// checks. Unmapped items keep the capture pipeline's palette unchanged.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace skate3::native_scene {
|
||||
struct FrameScene;
|
||||
} // namespace skate3::native_scene
|
||||
|
||||
namespace skate3::native_palette {
|
||||
|
||||
// cModelInstance::PackAndMultiplyMatricesForUpload EXIT: m_matrices (+0x14)
|
||||
// holds exactly the packed upload palette; snapshot it.
|
||||
void OnPackPalette(uint8_t* base, uint32_t instance);
|
||||
|
||||
// Sk8::UpdateBoneTransforms EXIT: parts = an ARRAY of cModelInstance part
|
||||
// records (stride 0x28), count = record count. Snapshots every part
|
||||
// (per-frame arena, unreadable by frame end).
|
||||
void OnBoneTransforms(uint8_t* base, uint32_t parts, uint32_t count);
|
||||
|
||||
// LivingWorld batch pack writer EXIT: entity = the
|
||||
// cLivingWorldPresEntity-derived writer. Snapshots each instance's
|
||||
// m_matrices as per-mesh slices.
|
||||
void OnLwPack(uint8_t* base, uint32_t entity);
|
||||
|
||||
// Pack-owner bracket (thread-local): the hook layer wraps every entity
|
||||
// method that produces palette packs; set the owner on entry, restore the
|
||||
// returned previous value after the original call. Snapshots taken inside
|
||||
// the bracket carry the owning entity for identity-exact serving.
|
||||
uint32_t ExchangePackOwner(uint32_t entity);
|
||||
|
||||
// Called from BuildFrameScene while the frame's items are finalized:
|
||||
// replaces each mapped skinned character item's captured bank palette with
|
||||
// its instance's own packed rows. Served items get dbg_src=11 and
|
||||
// caster_bank=false. Returns the number of items served.
|
||||
uint32_t ServeAuthoritativePalettes(uint8_t* base,
|
||||
skate3::native_scene::FrameScene& scene);
|
||||
|
||||
// Called from BuildFrameScene at the frame tail: rotates this frame's
|
||||
// snapshots into the previous-frame slot (draws of frame N may consume the
|
||||
// palette packed during frame N-1) and emits the optional stats line.
|
||||
void OnFrameBuilt();
|
||||
|
||||
} // namespace skate3::native_palette
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
#include "native/skate3_native_entity.h"
|
||||
#include "native/skate3_native_lw.h"
|
||||
#include "native/skate3_native_v3_shadow.h"
|
||||
#include "native/skate3_native_palette.h"
|
||||
#include "skate3_native_scene.h"
|
||||
#include "skate3_screenshot.h"
|
||||
|
||||
|
|
@ -723,25 +723,12 @@ extern "C" REX_FUNC(sub_82B82E08) {
|
|||
extern "C" REX_FUNC(sub_82C9A618) {
|
||||
if (skate3::native_render::Enabled()) {
|
||||
skate3::native_scene::OnRegisterTexture(ctx.r4.u64, ctx.r5.u32);
|
||||
skate3::native_v3::OnRegisterTexture(base, ctx.r4.u64, ctx.r5.u32);
|
||||
}
|
||||
__imp__sub_82C9A618(ctx, base);
|
||||
}
|
||||
|
||||
// ---- v3 Phase 2 shadow-mode reader hooks (observers only; see
|
||||
// native/skate3_native_v3_shadow.h). Each fires POST-call so the structure
|
||||
// it reads is in its settled per-frame state.
|
||||
|
||||
// RenderMan::RenderModeScene: the pass sequencer. At exit every
|
||||
// SceneRenderView has sorted and submitted its lists: the coherent per-frame
|
||||
// read point of the v3 architecture.
|
||||
extern "C" REX_FUNC(sub_827FF7D0) {
|
||||
const uint32_t renderman = ctx.r3.u32;
|
||||
__imp__sub_827FF7D0(ctx, base);
|
||||
if (skate3::native_render::Enabled()) {
|
||||
skate3::native_v3::OnRenderModeSceneExit(base, renderman);
|
||||
}
|
||||
}
|
||||
// ---- palette snapshot hooks (native/skate3_native_palette.h). Each fires
|
||||
// POST-call, when m_matrices holds the settled packed upload palette.
|
||||
|
||||
// cModelInstance::PackAndMultiplyMatricesForUpload(Matrix44& localToWorld):
|
||||
// post-call, m_matrices (+0x14) holds exactly the packed 4x3 column-major
|
||||
|
|
@ -750,61 +737,41 @@ extern "C" REX_FUNC(sub_827E5B30) {
|
|||
const uint32_t instance = ctx.r3.u32;
|
||||
__imp__sub_827E5B30(ctx, base);
|
||||
if (skate3::native_render::Enabled()) {
|
||||
skate3::native_v3::OnPackPalette(base, instance);
|
||||
skate3::native_palette::OnPackPalette(base, instance);
|
||||
}
|
||||
}
|
||||
|
||||
// Sk8::UpdateBoneTransforms(cModelInstance* parts, uint count, Matrix44*
|
||||
// srcPalette, uint** remaps, Matrix44& out): r3 is an ARRAY of
|
||||
// cModelInstance part records (stride 0x28). The v3 shadow reader's
|
||||
// secondary instance enumeration (LOD pedestrians never reach the pack
|
||||
// function above).
|
||||
// cModelInstance part records (stride 0x28): the player skater's per-piece
|
||||
// palette source (its parts never reach the pack function above).
|
||||
extern "C" REX_FUNC(sub_827A52C8) {
|
||||
const uint32_t parts = ctx.r3.u32;
|
||||
const uint32_t count = ctx.r4.u32;
|
||||
const uint32_t src_palette = ctx.r5.u32;
|
||||
const uint32_t out_mat = ctx.r7.u32;
|
||||
__imp__sub_827A52C8(ctx, base);
|
||||
if (skate3::native_render::Enabled()) {
|
||||
skate3::native_v3::OnBoneTransforms(base, parts, count, src_palette, out_mat);
|
||||
skate3::native_palette::OnBoneTransforms(base, parts, count);
|
||||
}
|
||||
}
|
||||
|
||||
// Sk8::cLivingWorldPresEntity::Update: post-call, this+528 holds the
|
||||
// entity's evaluated spawn/distance fade opacity (x = alpha), this+16 the
|
||||
// current LOD index. Feeds BOTH the v3 shadow fade comparison and the
|
||||
// LW entity store (per-instance ctx -> alpha/identity, the serving path).
|
||||
// current LOD index. Feeds the LW entity store (per-instance ctx ->
|
||||
// alpha/identity, the serving path).
|
||||
extern "C" REX_FUNC(sub_827C1188) {
|
||||
const uint32_t entity = ctx.r3.u32;
|
||||
__imp__sub_827C1188(ctx, base);
|
||||
if (skate3::native_render::Enabled()) {
|
||||
skate3::native_lw::OnLwEntityTick(base, entity);
|
||||
skate3::native_v3::OnLivingWorldUpdate(base, entity);
|
||||
}
|
||||
}
|
||||
|
||||
// Sk8::Actor::UpdateSkaterOpacity(float): post-call, Actor+1868 is the
|
||||
// animated current player-skater opacity.
|
||||
extern "C" REX_FUNC(sub_82594488) {
|
||||
const uint32_t actor = ctx.r3.u32;
|
||||
__imp__sub_82594488(ctx, base);
|
||||
if (skate3::native_render::Enabled()) {
|
||||
skate3::native_v3::OnSkaterOpacity(base, actor);
|
||||
}
|
||||
}
|
||||
|
||||
// Sk8::SkaterPresEntity::StartJobs: post-call, the per-garment ROPA CPU/GPU
|
||||
// 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.
|
||||
// Sk8::SkaterPresEntity::StartJobs, bracketed as a pack owner:
|
||||
// UpdateBoneTransforms calls inside stamp their snapshots with this entity.
|
||||
extern "C" REX_FUNC(sub_827825B0) {
|
||||
const uint32_t skater = ctx.r3.u32;
|
||||
const uint32_t prev_owner = skate3::native_v3::ExchangePackOwner(skater);
|
||||
const uint32_t prev_owner = skate3::native_palette::ExchangePackOwner(ctx.r3.u32);
|
||||
__imp__sub_827825B0(ctx, base);
|
||||
skate3::native_v3::ExchangePackOwner(prev_owner);
|
||||
if (skate3::native_render::Enabled()) {
|
||||
skate3::native_v3::OnRopaStartJobs(base, skater);
|
||||
}
|
||||
skate3::native_palette::ExchangePackOwner(prev_owner);
|
||||
}
|
||||
|
||||
// Sk8::SkaterPresEntity::DoubleBuffer(garment index): runs once per
|
||||
|
|
@ -826,9 +793,9 @@ extern "C" REX_FUNC(sub_82783038) {
|
|||
// 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);
|
||||
skate3::native_palette::ExchangePackOwner(ctx.r3.u32);
|
||||
__imp__sub_82782818(ctx, base);
|
||||
skate3::native_v3::ExchangePackOwner(prev_owner);
|
||||
skate3::native_palette::ExchangePackOwner(prev_owner);
|
||||
}
|
||||
|
||||
// Skater-family virtual UBT+Pack driver (vtable slot +16): the remaining
|
||||
|
|
@ -836,32 +803,25 @@ extern "C" REX_FUNC(sub_82782818) {
|
|||
// 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);
|
||||
skate3::native_palette::ExchangePackOwner(ctx.r3.u32);
|
||||
__imp__sub_82785778(ctx, base);
|
||||
skate3::native_v3::ExchangePackOwner(prev_owner);
|
||||
skate3::native_palette::ExchangePackOwner(prev_owner);
|
||||
}
|
||||
|
||||
// Sk8::RenderPresentation::AddEntityToRenderViews / RmvEntityFrmRenderViews
|
||||
// - r3 is the RenderPresentation instance itself: the direct capture source
|
||||
// for the RP pointer (the 0x83063C38/0x83063C60 scene globals read ZERO at
|
||||
// runtime; the chain assumption did not hold live). r4 is the
|
||||
// PresentationEntity being (de)registered: the identity store's
|
||||
// - 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) {
|
||||
const uint32_t rp = ctx.r3.u32;
|
||||
const uint32_t entity = ctx.r4.u32;
|
||||
if (skate3::native_render::Enabled()) {
|
||||
skate3::native_v3::OnRenderPresentation(base, rp);
|
||||
skate3::native_entity::OnEntityViewAdd(entity);
|
||||
}
|
||||
__imp__sub_827A6C50(ctx, base);
|
||||
}
|
||||
|
||||
extern "C" REX_FUNC(sub_827A6CE8) {
|
||||
const uint32_t rp = ctx.r3.u32;
|
||||
const uint32_t entity = ctx.r4.u32;
|
||||
if (skate3::native_render::Enabled()) {
|
||||
skate3::native_v3::OnRenderPresentation(base, rp);
|
||||
skate3::native_entity::OnEntityViewRemove(entity);
|
||||
}
|
||||
__imp__sub_827A6CE8(ctx, base);
|
||||
|
|
@ -1237,14 +1197,14 @@ extern "C" REX_FUNC(sub_82B79FC0) {
|
|||
|
||||
// LivingWorld batch pack writer (unnamed; called per entity per sim tick
|
||||
// from the tail of cLivingWorldPresEntityManager::Update), the LOD-
|
||||
// pedestrian skinning-palette source: fused UpdateBoneTransforms+Pack writing the
|
||||
// concatenated per-mesh 4x3 palettes into cModelInstance.m_matrices. r3 =
|
||||
// the cLivingWorldPresEntity-derived entity; post-call m_matrices holds the
|
||||
// packed rows.
|
||||
// pedestrian skinning-palette source: fused UpdateBoneTransforms+Pack
|
||||
// writing the concatenated per-mesh 4x3 palettes into
|
||||
// cModelInstance.m_matrices. r3 = the cLivingWorldPresEntity-derived
|
||||
// entity; post-call m_matrices holds the packed rows.
|
||||
extern "C" REX_FUNC(sub_827C1D38) {
|
||||
const uint32_t entity = ctx.r3.u32;
|
||||
__imp__sub_827C1D38(ctx, base);
|
||||
if (skate3::native_render::Enabled()) {
|
||||
skate3::native_v3::OnLwPack(base, entity);
|
||||
skate3::native_palette::OnLwPack(base, entity);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
#include "native/skate3_native_entity.h"
|
||||
#include "native/skate3_native_guest_read.h"
|
||||
#include "native/skate3_native_lw.h"
|
||||
#include "native/skate3_native_v3_shadow.h"
|
||||
#include "native/skate3_native_palette.h"
|
||||
// Offline-compiled SPIR-V for the native shaders (compiled from the HLSL
|
||||
// sources with DXC): the Vulkan RHI backend consumes these blobs; the D3D12
|
||||
// backend runtime-compiles the embedded HLSL as before.
|
||||
|
|
@ -8167,14 +8167,14 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
// refused/pending capture re-publishes with the cached palette (one frame
|
||||
// of pose lag instead of a one-frame disappearance).
|
||||
if (REXCVAR_GET(skate3_native_render_scene_dynamic_items)) {
|
||||
// v3 Phase 3 palette flip: serve every mapped skinned character item its
|
||||
// instance's own packed m_matrices rows (snapshotted at the game's
|
||||
// Authoritative palette serve: give every mapped skinned character item
|
||||
// its instance's own packed m_matrices rows (snapshotted at the game's
|
||||
// Pack/UpdateBoneTransforms exits, the exact bytes the VS upload
|
||||
// consumes; per piece, one sim tick, coherent by construction). Runs
|
||||
// FIRST so the sanity gate below validates the authoritative rows and
|
||||
// the rescue caches learn them; unmapped items keep the bank pipeline.
|
||||
const auto pal_t0 = PerfClock::now();
|
||||
g_pal_served_frame = skate3::native_v3::ServeAuthoritativePalettes(base, scene);
|
||||
g_pal_served_frame = skate3::native_palette::ServeAuthoritativePalettes(base, scene);
|
||||
g_frame_bpal_ns = uint64_t(std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
PerfClock::now() - pal_t0)
|
||||
.count());
|
||||
|
|
@ -9653,12 +9653,12 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
|||
g_frame_draw_fetch.clear();
|
||||
}
|
||||
|
||||
// Frame tail: rotate the bone-palette snapshot ring (and run its optional
|
||||
// shadow-mode compares). Timed as its own block in the perf line; this
|
||||
// tail runs on the guest render thread, where spikes are visible stutter.
|
||||
// Frame tail: rotate the bone-palette snapshot ring. Timed as its own
|
||||
// block in the perf line; this tail runs on the guest render thread,
|
||||
// where spikes are visible stutter.
|
||||
{
|
||||
const auto v3_t0 = PerfClock::now();
|
||||
skate3::native_v3::OnFrameBuilt(base, records, count, scene);
|
||||
skate3::native_palette::OnFrameBuilt();
|
||||
g_frame_v3_ns = uint64_t(std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
PerfClock::now() - v3_t0)
|
||||
.count());
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue