npc: LivingWorld entity store (per-instance opacity + identity + palettes)
This commit is contained in:
parent
de59412235
commit
b4d2e59b70
7 changed files with 767 additions and 13 deletions
|
|
@ -229,6 +229,7 @@ set(SKATE3_COMMON_SOURCES
|
||||||
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_guest_read.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_v3_shadow.cpp
|
||||||
src/native/skate3_native_v3_shadow_mat.cpp
|
src/native/skate3_native_v3_shadow_mat.cpp
|
||||||
src/native/skate3_hud_tile.cpp
|
src/native/skate3_hud_tile.cpp
|
||||||
|
|
|
||||||
289
src/native/skate3_native_lw.cpp
Normal file
289
src/native/skate3_native_lw.cpp
Normal file
|
|
@ -0,0 +1,289 @@
|
||||||
|
// LivingWorld entity store; see skate3_native_lw.h. Verified guest facts
|
||||||
|
// (from the recompiled game code, generated/skate3_recomp.36.cpp):
|
||||||
|
//
|
||||||
|
// - cLivingWorldPresEntity::Update (sub_827C1188, :43097) computes the
|
||||||
|
// spawn ramp (entity+576, += const per tick, clamped [0,1]) and the
|
||||||
|
// distance term (EvaluateOpacityDistance 827C1870 / the +612/+616
|
||||||
|
// override), stores min(ramp, distance) as the .x of the Vector4 at
|
||||||
|
// entity+528 and the "is fading" byte at +580.
|
||||||
|
// - cLivingWorldPresEntity::BindConstants (sub_827C1720, :43927) pushes
|
||||||
|
// &entity+528 into the material params of EVERY mesh ctx of every
|
||||||
|
// instance: idx=[e+16], count=[e+(idx+5)*4], array=[e+(idx+8)*4]
|
||||||
|
// stride 0x28; instance: model +0, MeshContext array +4 (end +8,
|
||||||
|
// 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),
|
||||||
|
// whose ctx containment matching against DrawItem::ctx was live-proven
|
||||||
|
// (measured zero unmapped palettes).
|
||||||
|
|
||||||
|
#include "native/skate3_native_lw.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <bit>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
#include <mutex>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <rex/cvar.h>
|
||||||
|
#include <rex/logging.h>
|
||||||
|
|
||||||
|
#include "native/skate3_native_guest_read.h"
|
||||||
|
|
||||||
|
REXCVAR_DEFINE_BOOL(
|
||||||
|
skate3_native_render_scene_lw_palette, true, "Skate 3",
|
||||||
|
"Snapshot each LivingWorld entity's OWN packed per-mesh palette "
|
||||||
|
"(cModelInstance.m_matrices, written by the pack writer every sim "
|
||||||
|
"tick) into the LW entity store, and substitute it for GUESSED ortho "
|
||||||
|
"caster-bank palettes on edge-of-view vehicles (the mangle/transform "
|
||||||
|
"class). Off = the caster heuristics alone.")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
|
||||||
|
namespace skate3::native_lw {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using skate3::native_scene::GuestTryCopy;
|
||||||
|
|
||||||
|
// Entity/instance layout (verified: SnapshotLwEntity + sub_827C1720).
|
||||||
|
constexpr uint32_t kLwOpacity = 528; // Vector4, x = alpha
|
||||||
|
constexpr uint32_t kInstModel = 0x00; // cModelInstance model ptr
|
||||||
|
constexpr uint32_t kInstArrCtx = 0x04; // m_arrMeshContext begin
|
||||||
|
constexpr uint32_t kInstArrCtxEnd = 0x08; // VectorBase end
|
||||||
|
constexpr uint32_t kInstMatrices = 0x14; // packed palette, 48-byte rows
|
||||||
|
constexpr uint32_t kInstStride = 0x28;
|
||||||
|
constexpr uint32_t kCtxStride = 0x50;
|
||||||
|
|
||||||
|
struct LwCtxEntry {
|
||||||
|
float alpha = 1.0f;
|
||||||
|
uint32_t entity = 0;
|
||||||
|
uint64_t stamp_ms = 0;
|
||||||
|
// The entity's own packed palette slice for this ctx's mesh (host-order,
|
||||||
|
// 3 float4 rows per bone), snapshotted at the same tick as the alpha.
|
||||||
|
uint32_t pal_rows = 0;
|
||||||
|
uint64_t pal_stamp_ms = 0;
|
||||||
|
std::vector<float> palette;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::mutex g_mu;
|
||||||
|
std::unordered_map<uint32_t, LwCtxEntry> g_ctx; // MeshContext -> entity state
|
||||||
|
|
||||||
|
// Distinct-entity rate counter (stats line): entities ticked in the
|
||||||
|
// current 1 s window.
|
||||||
|
std::atomic<uint32_t> g_ents_window{0};
|
||||||
|
std::atomic<uint32_t> g_ents_last{0};
|
||||||
|
std::atomic<uint64_t> g_ents_win_start{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 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void OnLwEntityTick(uint8_t* base, uint32_t entity) {
|
||||||
|
// Fresh opacity first: an unreadable/insane alpha means teardown or a
|
||||||
|
// non-conforming subclass; skip the entity, never store garbage.
|
||||||
|
uint32_t alpha_u = 0;
|
||||||
|
if (!LoadU32(base, entity + kLwOpacity, &alpha_u)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float alpha = std::bit_cast<float>(alpha_u);
|
||||||
|
if (!(alpha >= -0.01f && alpha <= 1.01f)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const uint64_t now = NowMs();
|
||||||
|
{
|
||||||
|
uint64_t win = g_ents_win_start.load(std::memory_order_relaxed);
|
||||||
|
if (now - win >= 1000) {
|
||||||
|
if (g_ents_win_start.compare_exchange_strong(win, now,
|
||||||
|
std::memory_order_relaxed)) {
|
||||||
|
g_ents_last.store(g_ents_window.exchange(0, std::memory_order_relaxed),
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_ents_window.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
const bool want_pal = REXCVAR_GET(skate3_native_render_scene_lw_palette);
|
||||||
|
struct CtxRec {
|
||||||
|
uint32_t ctx;
|
||||||
|
uint32_t pal_ofs; // float offset into pal[] (UINT32_MAX = no slice)
|
||||||
|
uint32_t pal_rows; // bone rows
|
||||||
|
};
|
||||||
|
CtxRec ctxs[8 * 64];
|
||||||
|
uint32_t nctx = 0;
|
||||||
|
// Palette staging: copied + byteswapped OUTSIDE the lock (the sim thread
|
||||||
|
// holding g_mu through a multi-KB swap loop stalled the render thread in
|
||||||
|
// the shadow reader's history, same rule here).
|
||||||
|
float pal[8 * 96 * 12];
|
||||||
|
uint32_t pal_used = 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 + kInstArrCtx, &cbegin) ||
|
||||||
|
!LoadU32(base, rec + kInstArrCtxEnd, &cend) || !PlausiblePtr(cbegin) ||
|
||||||
|
cend <= cbegin || (cend - cbegin) % kCtxStride != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const uint32_t slots = (cend - cbegin) / kCtxStride;
|
||||||
|
if (slots > 64) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// The entity's own packed palette (m_matrices): per-mesh bone counts
|
||||||
|
// from the model walk (mesh table +0x24, count u16 +0x32, bone-count
|
||||||
|
// gate u16 +0x34 <= 80, mesh bone count +0x48 - the verified
|
||||||
|
// SnapshotLwEntity conventions); mesh k's slice starts after the
|
||||||
|
// preceding meshes' rows and pairs with ctx slot k.
|
||||||
|
uint32_t bones[16];
|
||||||
|
uint32_t nmesh = 0;
|
||||||
|
uint32_t inst_pal_ofs = UINT32_MAX;
|
||||||
|
if (want_pal) {
|
||||||
|
uint32_t model = 0, matrices = 0, count_w = 0, table = 0, gate_w = 0;
|
||||||
|
if (LoadU32(base, rec + kInstModel, &model) &&
|
||||||
|
LoadU32(base, rec + kInstMatrices, &matrices) &&
|
||||||
|
PlausiblePtr(model) && PlausiblePtr(matrices) &&
|
||||||
|
LoadU32(base, model + 0x30, &count_w) &&
|
||||||
|
LoadU32(base, model + 0x24, &table) &&
|
||||||
|
LoadU32(base, model + 0x34, &gate_w) && (gate_w >> 16) <= 80) {
|
||||||
|
const uint32_t nm = count_w & 0xFFFF;
|
||||||
|
if (nm >= 1 && nm <= 16) {
|
||||||
|
uint32_t total = 0;
|
||||||
|
bool ok = true;
|
||||||
|
for (uint32_t k = 0; k < nm && 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint32_t raw[96 * 12];
|
||||||
|
if (ok && total >= 1 && total <= 96 &&
|
||||||
|
pal_used + total * 12 <= 8 * 96 * 12 &&
|
||||||
|
GuestTryCopy(raw, base + matrices, size_t(total) * 48)) {
|
||||||
|
for (uint32_t f = 0; f < total * 12; ++f) {
|
||||||
|
pal[pal_used + f] =
|
||||||
|
std::bit_cast<float>(__builtin_bswap32(raw[f]));
|
||||||
|
}
|
||||||
|
inst_pal_ofs = pal_used;
|
||||||
|
pal_used += total * 12;
|
||||||
|
nmesh = nm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint32_t row = 0;
|
||||||
|
for (uint32_t k = 0; k < slots && nctx < 8 * 64; ++k) {
|
||||||
|
CtxRec& c = ctxs[nctx++];
|
||||||
|
c.ctx = cbegin + k * kCtxStride;
|
||||||
|
if (k < nmesh && inst_pal_ofs != UINT32_MAX) {
|
||||||
|
c.pal_ofs = inst_pal_ofs + row * 12;
|
||||||
|
c.pal_rows = bones[k];
|
||||||
|
row += bones[k];
|
||||||
|
} else {
|
||||||
|
c.pal_ofs = UINT32_MAX;
|
||||||
|
c.pal_rows = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nctx == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::lock_guard<std::mutex> lock(g_mu);
|
||||||
|
// Growth backstop: entries stop refreshing when their entity despawns;
|
||||||
|
// sweep stale ones (2 s) before the map can grow past the cap.
|
||||||
|
if (g_ctx.size() > 8192) {
|
||||||
|
for (auto it = g_ctx.begin(); it != g_ctx.end();) {
|
||||||
|
it = now - it->second.stamp_ms > 2000 ? g_ctx.erase(it) : std::next(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < nctx; ++i) {
|
||||||
|
LwCtxEntry& e = g_ctx[ctxs[i].ctx];
|
||||||
|
e.alpha = alpha;
|
||||||
|
e.entity = entity;
|
||||||
|
e.stamp_ms = now;
|
||||||
|
if (ctxs[i].pal_ofs != UINT32_MAX) {
|
||||||
|
e.pal_rows = ctxs[i].pal_rows;
|
||||||
|
e.pal_stamp_ms = now;
|
||||||
|
e.palette.assign(pal + ctxs[i].pal_ofs,
|
||||||
|
pal + ctxs[i].pal_ofs + size_t(ctxs[i].pal_rows) * 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LookupLwCtx(uint32_t ctx, float* out_alpha, uint32_t* out_entity) {
|
||||||
|
if (ctx == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const uint64_t now = NowMs();
|
||||||
|
std::lock_guard<std::mutex> lock(g_mu);
|
||||||
|
const auto it = g_ctx.find(ctx);
|
||||||
|
if (it == g_ctx.end() || now - it->second.stamp_ms > 500) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (out_alpha != nullptr) {
|
||||||
|
*out_alpha = it->second.alpha;
|
||||||
|
}
|
||||||
|
if (out_entity != nullptr) {
|
||||||
|
*out_entity = it->second.entity;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t LookupLwPalette(uint32_t ctx, float* out, uint32_t max_floats) {
|
||||||
|
if (ctx == 0 || out == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const uint64_t now = NowMs();
|
||||||
|
std::lock_guard<std::mutex> lock(g_mu);
|
||||||
|
const auto it = g_ctx.find(ctx);
|
||||||
|
// 100 ms freshness: a palette older than a few sim ticks is a FROZEN
|
||||||
|
// pose, worse than the caster heuristics it would replace.
|
||||||
|
if (it == g_ctx.end() || it->second.pal_rows == 0 ||
|
||||||
|
now - it->second.pal_stamp_ms > 100 ||
|
||||||
|
it->second.palette.size() != size_t(it->second.pal_rows) * 12 ||
|
||||||
|
it->second.palette.size() > max_floats) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
std::memcpy(out, it->second.palette.data(),
|
||||||
|
it->second.palette.size() * sizeof(float));
|
||||||
|
return it->second.pal_rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
void QueryLwStats(uint32_t* out_ctxs, uint32_t* out_entities) {
|
||||||
|
if (out_entities != nullptr) {
|
||||||
|
*out_entities = g_ents_last.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
if (out_ctxs != nullptr) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_mu);
|
||||||
|
*out_ctxs = uint32_t(g_ctx.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace skate3::native_lw
|
||||||
49
src/native/skate3_native_lw.h
Normal file
49
src/native/skate3_native_lw.h
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// LivingWorld entity store: the game's own
|
||||||
|
// per-instance identity + opacity for cLivingWorldPresEntity-derived
|
||||||
|
// entities (census pedestrians, traffic vehicles, spawned props), served to
|
||||||
|
// the native scene the way the console serves it.
|
||||||
|
//
|
||||||
|
// Write point: the sub_827C1188 (cLivingWorldPresEntity::Update) EXIT hook,
|
||||||
|
// per entity per sim tick: the instant the game finishes writing the
|
||||||
|
// entity's evaluated opacity Vector4 at entity+528 (x = alpha, the value
|
||||||
|
// BindConstants pushes into every mesh ctx's material params, which the
|
||||||
|
// character-family shaders output as alpha: c13.x / c21.x / c22.x / c20.x).
|
||||||
|
// The walk mirrors sub_827C1720 (BindConstants): instance array at
|
||||||
|
// [e+(idx+8)*4] stride 0x28, per-instance MeshContext array at +4 stride
|
||||||
|
// 0x50; each ctx IS a DrawItem::ctx, so lookups are exact per instance
|
||||||
|
// per mesh. Entities whose layout fails a guard simply never enter the
|
||||||
|
// store (self-limiting: they keep the captured-row fade path).
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace skate3::native_lw {
|
||||||
|
|
||||||
|
// sub_827C1188 EXIT (sim thread): read the entity's fresh opacity and map
|
||||||
|
// it onto every MeshContext the entity binds. Cheap (a dozen guarded loads
|
||||||
|
// per entity); called only while the native renderer is enabled.
|
||||||
|
void OnLwEntityTick(uint8_t* base, uint32_t entity);
|
||||||
|
|
||||||
|
// Fresh (<= 500 ms) store entry for a MeshContext. Returns false when the
|
||||||
|
// ctx is unknown or stale (despawned entity / reused arena memory). Guest
|
||||||
|
// render thread; internally locked: use the bulk pattern (one call per
|
||||||
|
// item per frame is fine, the map is small).
|
||||||
|
bool LookupLwCtx(uint32_t ctx, float* out_alpha, uint32_t* out_entity);
|
||||||
|
|
||||||
|
// The entity's OWN packed palette slice for this ctx's mesh, snapshotted at
|
||||||
|
// the entity's last sim tick (the LivingWorld pack writer sub_827C1D38
|
||||||
|
// writes the concatenated per-mesh 4x3 palettes into the instance's
|
||||||
|
// m_matrices; mesh k's slice starts at 3*sum(preceding bone counts), the
|
||||||
|
// same walk the live-proven SnapshotLwEntity shadow reader uses). Copies up
|
||||||
|
// to max_floats host-order floats (3 float4 rows per bone) into out and
|
||||||
|
// returns the bone-row count, or 0 when the ctx has no fresh slice. This is
|
||||||
|
// the authoritative replacement for GUESSED ortho caster-bank palettes on
|
||||||
|
// edge-of-view vehicles.
|
||||||
|
uint32_t LookupLwPalette(uint32_t ctx, float* out, uint32_t max_floats);
|
||||||
|
|
||||||
|
// Store size counters for the periodic stats line: live ctx entries and
|
||||||
|
// distinct entities ticked in the last second.
|
||||||
|
void QueryLwStats(uint32_t* out_ctxs, uint32_t* out_entities);
|
||||||
|
|
||||||
|
} // namespace skate3::native_lw
|
||||||
|
|
@ -21,6 +21,10 @@ REXCVAR_DECLARE(bool, skate3_native_render_scene_splines);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_quadlists);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_quadlists);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_world_items);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_world_items);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_dynamic_items);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_dynamic_items);
|
||||||
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_lw_fade);
|
||||||
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_lw_identity);
|
||||||
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_lw_gap_fill);
|
||||||
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_lw_palette);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_retain_offscreen);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_retain_offscreen);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_tex_revalidate);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_tex_revalidate);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_mesh_revalidate);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_mesh_revalidate);
|
||||||
|
|
@ -217,6 +221,37 @@ void NativeDebugDialog::OnDraw(ImGuiIO& io) {
|
||||||
REXCVAR_GET(skate3_native_render_scene_dynamic_items),
|
REXCVAR_GET(skate3_native_render_scene_dynamic_items),
|
||||||
"Characters, movable props, cloth (RenderMesh/world-path "
|
"Characters, movable props, cloth (RenderMesh/world-path "
|
||||||
"captures)"));
|
"captures)"));
|
||||||
|
REXCVAR_SET(
|
||||||
|
skate3_native_render_scene_lw_fade,
|
||||||
|
CvarCheckbox("LW entity fade (store)",
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_lw_fade),
|
||||||
|
"Serve NPC/traffic fade alpha from the LivingWorld entity "
|
||||||
|
"itself (per-instance store) instead of the per-draw "
|
||||||
|
"captured constant row; fixes opaque mid-air spawns, "
|
||||||
|
"missing fade-ins and clone alpha blinks"));
|
||||||
|
REXCVAR_SET(
|
||||||
|
skate3_native_render_scene_lw_identity,
|
||||||
|
CvarCheckbox("LW entity identity (pose rings)",
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_lw_identity),
|
||||||
|
"Key NPC/traffic pose-smoothing rings by the game's own "
|
||||||
|
"per-instance MeshContext instead of (mesh, occurrence) "
|
||||||
|
"pairing; clone reshuffles can no longer mispair "
|
||||||
|
"(teleport/slide class)"));
|
||||||
|
REXCVAR_SET(
|
||||||
|
skate3_native_render_scene_lw_gap_fill,
|
||||||
|
CvarCheckbox("LW gap fill (1-2 frame republish)",
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_lw_gap_fill),
|
||||||
|
"Republish a live NPC whose MeshContext skipped this "
|
||||||
|
"frame's submit records (the 1-3 frame publish GAPs that "
|
||||||
|
"read as blinks/small teleports)"));
|
||||||
|
REXCVAR_SET(
|
||||||
|
skate3_native_render_scene_lw_palette,
|
||||||
|
CvarCheckbox("LW authoritative caster palettes",
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_lw_palette),
|
||||||
|
"Replace GUESSED ortho caster-bank palettes on "
|
||||||
|
"edge-of-view vehicles with the entity's own packed "
|
||||||
|
"palette from the pack writer (the mangle/transform "
|
||||||
|
"class)"));
|
||||||
REXCVAR_SET(skate3_native_render_scene_quadlists,
|
REXCVAR_SET(skate3_native_render_scene_quadlists,
|
||||||
CvarCheckbox("Quad-list particles",
|
CvarCheckbox("Quad-list particles",
|
||||||
REXCVAR_GET(skate3_native_render_scene_quadlists),
|
REXCVAR_GET(skate3_native_render_scene_quadlists),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "skate3_native_render.h"
|
#include "skate3_native_render.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"
|
||||||
#include "skate3_screenshot.h"
|
#include "skate3_screenshot.h"
|
||||||
|
|
@ -667,11 +668,13 @@ extern "C" REX_FUNC(sub_827A52C8) {
|
||||||
|
|
||||||
// Sk8::cLivingWorldPresEntity::Update: post-call, this+528 holds the
|
// Sk8::cLivingWorldPresEntity::Update: post-call, this+528 holds the
|
||||||
// entity's evaluated spawn/distance fade opacity (x = alpha), this+16 the
|
// entity's evaluated spawn/distance fade opacity (x = alpha), this+16 the
|
||||||
// current LOD index.
|
// current LOD index. Feeds BOTH the v3 shadow fade comparison and the
|
||||||
|
// LW entity store (per-instance ctx -> alpha/identity, the serving path).
|
||||||
extern "C" REX_FUNC(sub_827C1188) {
|
extern "C" REX_FUNC(sub_827C1188) {
|
||||||
const uint32_t entity = ctx.r3.u32;
|
const uint32_t entity = ctx.r3.u32;
|
||||||
__imp__sub_827C1188(ctx, base);
|
__imp__sub_827C1188(ctx, base);
|
||||||
if (skate3::native_render::Enabled()) {
|
if (skate3::native_render::Enabled()) {
|
||||||
|
skate3::native_lw::OnLwEntityTick(base, entity);
|
||||||
skate3::native_v3::OnLivingWorldUpdate(base, entity);
|
skate3::native_v3::OnLivingWorldUpdate(base, entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
|
|
||||||
#include "native/skate3_native_diag.h"
|
#include "native/skate3_native_diag.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_v3_shadow.h"
|
#include "native/skate3_native_v3_shadow.h"
|
||||||
#include "native/skate3_native_v3_shadow_mat.h"
|
#include "native/skate3_native_v3_shadow_mat.h"
|
||||||
#include "native/skate3_hud_tile.h"
|
#include "native/skate3_hud_tile.h"
|
||||||
|
|
@ -230,6 +231,30 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_entity_fade, true, "Skate 3",
|
||||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_dynamic_items, true, "Skate 3",
|
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_dynamic_items, true, "Skate 3",
|
||||||
"Publish dynamic entities (characters, props, cloth)")
|
"Publish dynamic entities (characters, props, cloth)")
|
||||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_BOOL(
|
||||||
|
skate3_native_render_scene_lw_fade, true, "Skate 3",
|
||||||
|
"Serve LivingWorld NPC/vehicle fade alpha from the entity itself "
|
||||||
|
"(entity+528 via the LW entity store, mapped per MeshContext) instead "
|
||||||
|
"of the per-draw captured constant row. The captured row is a per-draw "
|
||||||
|
"inference: capture races serve alpha=1 (opaque mid-air spawns, no "
|
||||||
|
"fade-in) or a clone's foreign row (one-frame invisibility blinks). "
|
||||||
|
"Off = the pre-store captured-row behavior.")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_BOOL(
|
||||||
|
skate3_native_render_scene_lw_gap_fill, true, "Skate 3",
|
||||||
|
"Republish a LivingWorld NPC for up to 2 frames when its MeshContext "
|
||||||
|
"drops out of the submit records while the entity is still alive (the "
|
||||||
|
"1-3 frame publish GAPs: an in-view NPC vanishing for a frame reads as "
|
||||||
|
"a blink/teleport). Off = gaps render as-is.")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_BOOL(
|
||||||
|
skate3_native_render_scene_lw_identity, true, "Skate 3",
|
||||||
|
"Key LivingWorld entities' pose-smoothing rings by their MeshContext "
|
||||||
|
"(the game's own per-instance identity) instead of (mesh, occurrence) "
|
||||||
|
"pairing. Same-mesh clone reshuffles in the sort lists can then never "
|
||||||
|
"mispair a ring (the NPC/prop teleport-slide class). Off = the "
|
||||||
|
"positional re-pair heuristics alone.")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_ropa_inline, false, "Skate 3",
|
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_ropa_inline, false, "Skate 3",
|
||||||
"Decode CPU-cloth (ROPA) garment VBs inline on the render thread "
|
"Decode CPU-cloth (ROPA) garment VBs inline on the render thread "
|
||||||
"instead of the worker jobs. Measured 4.3ms avg per garment "
|
"instead of the worker jobs. Measured 4.3ms avg per garment "
|
||||||
|
|
@ -435,6 +460,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_lw.cpp (the LW entity store module).
|
||||||
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_lw_palette);
|
||||||
|
|
||||||
namespace skate3::native_scene {
|
namespace skate3::native_scene {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
@ -748,6 +775,50 @@ struct CachedBones {
|
||||||
};
|
};
|
||||||
std::unordered_map<uint32_t, CachedBones> g_bones_cache;
|
std::unordered_map<uint32_t, CachedBones> g_bones_cache;
|
||||||
std::atomic<uint64_t> g_bones_rescued{0};
|
std::atomic<uint64_t> g_bones_rescued{0};
|
||||||
|
// ctx -> last published palette for skinned character-family items (the
|
||||||
|
// per-INSTANCE sibling of g_bones_cache): clones share meshes, so the
|
||||||
|
// mesh-keyed rescue is gated to pub_count==1 and a refused clone capture
|
||||||
|
// next to a published twin got NOTHING: a one-frame invisibility blink.
|
||||||
|
// The MeshContext is the game's own per-instance identity,
|
||||||
|
// so this cache rescues each instance
|
||||||
|
// with ITS OWN last palette regardless of how many clones are alive.
|
||||||
|
std::unordered_map<uint32_t, CachedBones> g_bones_cache_ctx;
|
||||||
|
std::atomic<uint64_t> g_lw_ctx_rescued{0};
|
||||||
|
// LW entity store consumption counters (stats line lw[...]).
|
||||||
|
std::atomic<uint64_t> g_lw_stamped{0};
|
||||||
|
std::atomic<uint64_t> g_lw_fade0{0};
|
||||||
|
std::atomic<uint64_t> g_lw_gap_filled{0};
|
||||||
|
// Authoritative caster-palette substitutions + per-ctx lighting-rows serves
|
||||||
|
// (edge-of-view vehicles; see the stamp pass).
|
||||||
|
std::atomic<uint64_t> g_lw_pal_sub{0};
|
||||||
|
std::atomic<uint64_t> g_lw_rows_served{0};
|
||||||
|
// ctx -> last VALIDATED lighting/paint rows, entity-checked (a recycled
|
||||||
|
// instance must not inherit the previous occupant's paint). The mesh-keyed
|
||||||
|
// g_char_rows_cache fallback is gated to single-instance meshes; cloned
|
||||||
|
// traffic never qualified, so a caster-only stretch (main view culls the
|
||||||
|
// vehicle at the screen edge before it leaves the screen) dropped to
|
||||||
|
// legacy flat shading: the "vehicle loses its texture/color at the edge"
|
||||||
|
// sighting.
|
||||||
|
struct CharRowsCtx {
|
||||||
|
std::array<float, 60> rows;
|
||||||
|
uint32_t entity = 0;
|
||||||
|
};
|
||||||
|
std::unordered_map<uint32_t, CharRowsCtx> g_char_rows_cache_ctx;
|
||||||
|
// ctx -> last PUBLISHED item of a live LW entity (skinned character
|
||||||
|
// families, non-ropa): when a ctx drops out of the submit records for a
|
||||||
|
// frame or two while its entity is still alive in the store (observed as
|
||||||
|
// "dyn publish GAP" incidents on fam-3/5 ped meshes, 1-3
|
||||||
|
// frames each: an in-view NPC vanishing for a frame reads as a blink or,
|
||||||
|
// moving, a small teleport), the whole item republishes. Console behavior:
|
||||||
|
// a live entity never skips a frame. Self-limiting: entries expire after 2
|
||||||
|
// unpublished frames and refresh only from LIVE publishes (never from a
|
||||||
|
// fill, dbg_src 9), so a real despawn shows at most 2 filled frames, at
|
||||||
|
// the entity's own served alpha.
|
||||||
|
struct LwRetained {
|
||||||
|
DrawItem item;
|
||||||
|
uint64_t frame = 0;
|
||||||
|
};
|
||||||
|
std::unordered_map<uint32_t, LwRetained> g_lw_last_items;
|
||||||
// mesh -> last RESOLVED ropa garment state (rigid world OR skinned palette).
|
// mesh -> last RESOLVED ropa garment state (rigid world OR skinned palette).
|
||||||
// Ropa items must NOT ride the g_bones_cache rescue above: a ropa capture is
|
// Ropa items must NOT ride the g_bones_cache rescue above: a ropa capture is
|
||||||
// refused exactly when the bank is stale (g_ropa_stale), and while the cloth
|
// refused exactly when the bank is stale (g_ropa_stale), and while the cloth
|
||||||
|
|
@ -2914,6 +2985,33 @@ void CaptureCharLighting(uint8_t* base, DrawItem& item) {
|
||||||
// that alpha for items whose validated capture carries it, 1.0 otherwise
|
// that alpha for items whose validated capture carries it, 1.0 otherwise
|
||||||
// (legacy-shaded items keep the old always-opaque behavior).
|
// (legacy-shaded items keep the old always-opaque behavior).
|
||||||
float CharFadeAlpha(const DrawItem& item) {
|
float CharFadeAlpha(const DrawItem& item) {
|
||||||
|
// LW-mapped items: the entity's own
|
||||||
|
// opacity is authoritative; it is the exact value the game serves this
|
||||||
|
// ctx's shader as output alpha, independent of whether the per-draw row
|
||||||
|
// capture validated (or captured a clone's foreign row). Families whose
|
||||||
|
// shader COMPOSES the entity fade with another factor (hair strand-scale,
|
||||||
|
// vehicle-glass tint alpha) keep their captured value bounded by it.
|
||||||
|
if (item.lw_alpha >= 0.0f) {
|
||||||
|
const float a = std::clamp(item.lw_alpha, 0.0f, 1.0f);
|
||||||
|
switch (item.char_family) {
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
case 3:
|
||||||
|
case 6:
|
||||||
|
return a;
|
||||||
|
case 4:
|
||||||
|
case 5:
|
||||||
|
return item.char_rows[14 * 4 + 1] > 0.0f
|
||||||
|
? std::min(a, std::clamp(item.char_rows[13 * 4 + 3], 0.0f, 1.0f))
|
||||||
|
: a;
|
||||||
|
case 7:
|
||||||
|
return item.char_rows[14 * 4 + 1] > 0.0f
|
||||||
|
? std::min(a, std::clamp(item.char_rows[14 * 4 + 0], 0.0f, 1.0f))
|
||||||
|
: a;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (item.char_rows[14 * 4 + 1] <= 0.0f) {
|
if (item.char_rows[14 * 4 + 1] <= 0.0f) {
|
||||||
return 1.0f;
|
return 1.0f;
|
||||||
}
|
}
|
||||||
|
|
@ -5174,10 +5272,29 @@ void InterpolateDynamicItems(uint8_t* base, FrameScene& scene, double now) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
uint64_t key = (uint64_t(item.mesh) << 8) | (k & 0xFF);
|
// LW-mapped items key their ring by the game's own per-instance
|
||||||
|
// identity (the MeshContext); sort-
|
||||||
|
// list clone reshuffles cannot mispair an identity key, so the whole
|
||||||
|
// positional claim search below is skipped for them. Mesh stays in the
|
||||||
|
// key so a model/LOD swap on the same instance starts a fresh ring
|
||||||
|
// (pose sizes differ). Legacy (mesh, occurrence) pairing continues to
|
||||||
|
// serve everything without a store entry (player, CAC, non-LW).
|
||||||
|
bool lw_keyed = item.lw_entity != 0 && item.ctx != 0 &&
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_lw_identity);
|
||||||
|
uint64_t key = lw_keyed ? ((1ull << 63) | (uint64_t(item.ctx) << 32) |
|
||||||
|
uint64_t(item.mesh))
|
||||||
|
: ((uint64_t(item.mesh) << 8) | (k & 0xFF));
|
||||||
DynHist* hp = &s_hist[key];
|
DynHist* hp = &s_hist[key];
|
||||||
|
if (lw_keyed && hp->seen == s_frame) {
|
||||||
|
// One ctx published twice in a frame (should not happen; dyn_slot
|
||||||
|
// dedups per ctx): fall back to the legacy pairing for this copy
|
||||||
|
// rather than double-ingesting the identity ring.
|
||||||
|
lw_keyed = false;
|
||||||
|
key = (uint64_t(item.mesh) << 8) | (k & 0xFF);
|
||||||
|
hp = &s_hist[key];
|
||||||
|
}
|
||||||
const float own_d2 = hp->seen == s_frame ? 1e30f : hist_dist2(*hp);
|
const float own_d2 = hp->seen == s_frame ? 1e30f : hist_dist2(*hp);
|
||||||
if (own_d2 > 1e-4f) {
|
if (!lw_keyed && own_d2 > 1e-4f) {
|
||||||
// The k-th slot mispairs: the game's sort lists RESHUFFLE same-mesh
|
// The k-th slot mispairs: the game's sort lists RESHUFFLE same-mesh
|
||||||
// clones as they and the camera move. For static props the
|
// clones as they and the camera move. For static props the
|
||||||
// reset-on-jump guard below was enough (a mispair rendered raw for a
|
// reset-on-jump guard below was enough (a mispair rendered raw for a
|
||||||
|
|
@ -5413,7 +5530,11 @@ void InterpolateDynamicItems(uint8_t* base, FrameScene& scene, double now) {
|
||||||
const float d2 = skinned_dist2(item.bones, latest.b, h);
|
const float d2 = skinned_dist2(item.bones, latest.b, h);
|
||||||
return d2 > 9.0f && d2 < 900.0f;
|
return d2 > 9.0f && d2 < 900.0f;
|
||||||
}();
|
}();
|
||||||
|
// dbg_src 10 = LW-authoritative palette substitution: the pose IS the
|
||||||
|
// entity's current sim tick; holding the ring's (older) pose over it
|
||||||
|
// would re-stale exactly what the substitution fixed.
|
||||||
if (skinned && h.count > 0 && latest.b.size() == item.bones.size() &&
|
if (skinned && h.count > 0 && latest.b.size() == item.bones.size() &&
|
||||||
|
item.dbg_src != 10 &&
|
||||||
((item.caster_bank && now - h.last_persp_t <= 0.05) || item.retained ||
|
((item.caster_bank && now - h.last_persp_t <= 0.05) || item.retained ||
|
||||||
caster_stale_jump)) {
|
caster_stale_jump)) {
|
||||||
if (caster_stale_jump) {
|
if (caster_stale_jump) {
|
||||||
|
|
@ -6254,6 +6375,11 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
// (rigid vs skinned), never blindly re-skin (see g_ropa_state_cache).
|
// (rigid vs skinned), never blindly re-skin (see g_ropa_state_cache).
|
||||||
std::unordered_map<uint32_t, const DrawItem*> pending_skinned_by_mesh;
|
std::unordered_map<uint32_t, const DrawItem*> pending_skinned_by_mesh;
|
||||||
std::unordered_map<uint32_t, const DrawItem*> pending_ropa_by_mesh;
|
std::unordered_map<uint32_t, const DrawItem*> pending_ropa_by_mesh;
|
||||||
|
// Per-INSTANCE candidates (ctx-keyed, character families): the mesh-keyed
|
||||||
|
// rescue above is pub_count==1-gated, so a refused CLONE capture next to
|
||||||
|
// a published twin dropped for the frame: the NPC visible/invisible
|
||||||
|
// flicker (see g_bones_cache_ctx).
|
||||||
|
std::unordered_map<uint32_t, const DrawItem*> pending_skinned_by_ctx;
|
||||||
const auto total_indices = [](const DrawItem& d) {
|
const auto total_indices = [](const DrawItem& d) {
|
||||||
uint64_t n = 0;
|
uint64_t n = 0;
|
||||||
for (const DrawEntry& e : d.draws) n += e.index_count;
|
for (const DrawEntry& e : d.draws) n += e.index_count;
|
||||||
|
|
@ -6296,6 +6422,9 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
pending_ropa_by_mesh.try_emplace(cand.mesh, &cand);
|
pending_ropa_by_mesh.try_emplace(cand.mesh, &cand);
|
||||||
} else if (cand.skinned) {
|
} else if (cand.skinned) {
|
||||||
pending_skinned_by_mesh.try_emplace(cand.mesh, &cand);
|
pending_skinned_by_mesh.try_emplace(cand.mesh, &cand);
|
||||||
|
if (cand.ctx != 0 && cand.char_family != 0) {
|
||||||
|
pending_skinned_by_ctx.try_emplace(cand.ctx, &cand);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(cand.skinned ? g_skinned_skipped : g_rigid_dropped)
|
(cand.skinned ? g_skinned_skipped : g_rigid_dropped)
|
||||||
.fetch_add(1, std::memory_order_relaxed);
|
.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
|
@ -6453,6 +6582,17 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
item.dbg_src = 6;
|
item.dbg_src = 6;
|
||||||
healed = true;
|
healed = true;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Prefer the per-instance cache (clone-exact) over the mesh-keyed
|
||||||
|
// one; healing a clone with its TWIN's palette is a teleport.
|
||||||
|
const auto cit = item.ctx != 0 ? g_bones_cache_ctx.find(item.ctx)
|
||||||
|
: g_bones_cache_ctx.end();
|
||||||
|
if (cit != g_bones_cache_ctx.end() &&
|
||||||
|
cit->second.bones.size() == item.bones.size() &&
|
||||||
|
g_guest_frame - cit->second.frame <= 10) {
|
||||||
|
item.bones = cit->second.bones;
|
||||||
|
item.dbg_src = 6;
|
||||||
|
healed = true;
|
||||||
} else {
|
} else {
|
||||||
const auto bit = g_bones_cache.find(item.mesh);
|
const auto bit = g_bones_cache.find(item.mesh);
|
||||||
if (bit != g_bones_cache.end() &&
|
if (bit != g_bones_cache.end() &&
|
||||||
|
|
@ -6463,6 +6603,7 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
healed = true;
|
healed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (!healed) {
|
if (!healed) {
|
||||||
item.draws.clear();
|
item.draws.clear();
|
||||||
// CRITICAL: also drop the junk palette. Leaving it on the item let
|
// CRITICAL: also drop the junk palette. Leaving it on the item let
|
||||||
|
|
@ -6520,6 +6661,53 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
cb.frame = g_guest_frame;
|
cb.frame = g_guest_frame;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Per-instance palette cache refresh (see g_bones_cache_ctx): every
|
||||||
|
// published skinned character-family item keeps its OWN last palette
|
||||||
|
// keyed by ctx: no pub_count gate needed, the key IS the instance.
|
||||||
|
for (const DrawItem& item : scene.items) {
|
||||||
|
if (!item.skinned || item.bones.empty() || item.caster_bank ||
|
||||||
|
item.ctx == 0 || item.char_family == 0 || item.ropa ||
|
||||||
|
item.draws.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (g_bones_cache_ctx.size() > 2048) {
|
||||||
|
for (auto it = g_bones_cache_ctx.begin();
|
||||||
|
it != g_bones_cache_ctx.end();) {
|
||||||
|
it = g_guest_frame - it->second.frame > 60
|
||||||
|
? g_bones_cache_ctx.erase(it)
|
||||||
|
: std::next(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CachedBones& cb = g_bones_cache_ctx[item.ctx];
|
||||||
|
cb.bones = item.bones;
|
||||||
|
cb.frame = g_guest_frame;
|
||||||
|
}
|
||||||
|
// Per-instance rescue, BEFORE the mesh-keyed one: a refused/pending
|
||||||
|
// skinned character capture whose ctx did not publish this frame
|
||||||
|
// re-publishes with ITS OWN last palette (<= 10 frames fresh, same
|
||||||
|
// bound as the mesh rescue), regardless of how many clones of the mesh
|
||||||
|
// are alive. Rescued items bump pub_count so the mesh-keyed loop below
|
||||||
|
// cannot double-publish the same candidate.
|
||||||
|
for (const auto& [ctxk, cand] : pending_skinned_by_ctx) {
|
||||||
|
if (dyn_slot.find(ctxk) != dyn_slot.end()) {
|
||||||
|
continue; // this instance published live
|
||||||
|
}
|
||||||
|
const auto bit = g_bones_cache_ctx.find(ctxk);
|
||||||
|
if (bit == g_bones_cache_ctx.end() ||
|
||||||
|
g_guest_frame - bit->second.frame > 10) {
|
||||||
|
continue; // stale = an old pose; one missing frame beats a teleport
|
||||||
|
}
|
||||||
|
scene.items.push_back(*cand);
|
||||||
|
DrawItem& rescued = scene.items.back();
|
||||||
|
rescued.bones = bit->second.bones;
|
||||||
|
rescued.pending = false;
|
||||||
|
rescued.dbg_src = 3;
|
||||||
|
++pub_count[rescued.mesh];
|
||||||
|
// Mark the ctx published so the LW gap fill below cannot double-
|
||||||
|
// publish the same instance this frame.
|
||||||
|
dyn_slot.try_emplace(ctxk, scene.items.size() - 1);
|
||||||
|
g_lw_ctx_rescued.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
for (const auto& [mesh, cand] : pending_skinned_by_mesh) {
|
for (const auto& [mesh, cand] : pending_skinned_by_mesh) {
|
||||||
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
|
||||||
|
|
@ -6537,6 +6725,9 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
rescued.bones = bit->second.bones;
|
rescued.bones = bit->second.bones;
|
||||||
rescued.pending = false;
|
rescued.pending = false;
|
||||||
rescued.dbg_src = 3;
|
rescued.dbg_src = 3;
|
||||||
|
if (rescued.ctx != 0) {
|
||||||
|
dyn_slot.try_emplace(rescued.ctx, scene.items.size() - 1);
|
||||||
|
}
|
||||||
g_bones_rescued.fetch_add(1, std::memory_order_relaxed);
|
g_bones_rescued.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
// Refused ropa captures re-publish last frame's resolved state: mode,
|
// Refused ropa captures re-publish last frame's resolved state: mode,
|
||||||
|
|
@ -6557,8 +6748,63 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
std::memcpy(rescued.world, rit->second.world, sizeof(rescued.world));
|
std::memcpy(rescued.world, rit->second.world, sizeof(rescued.world));
|
||||||
rescued.pending = false;
|
rescued.pending = false;
|
||||||
rescued.dbg_src = 4;
|
rescued.dbg_src = 4;
|
||||||
|
if (rescued.ctx != 0) {
|
||||||
|
dyn_slot.try_emplace(rescued.ctx, scene.items.size() - 1);
|
||||||
|
}
|
||||||
g_ropa_rescued.fetch_add(1, std::memory_order_relaxed);
|
g_ropa_rescued.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
// LW gap fill (see g_lw_last_items): republish live entities' items
|
||||||
|
// whose ctx skipped this frame's records entirely, the class the
|
||||||
|
// pending rescues cannot see (no capture happened at all).
|
||||||
|
if (REXCVAR_GET(skate3_native_render_scene_lw_gap_fill)) {
|
||||||
|
const uint64_t now = g_guest_frame;
|
||||||
|
for (auto it = g_lw_last_items.begin(); it != g_lw_last_items.end();) {
|
||||||
|
if (dyn_slot.find(it->first) != dyn_slot.end()) {
|
||||||
|
++it; // published live this frame; refreshed below
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
LwRetained& r = it->second;
|
||||||
|
float alpha = 1.0f;
|
||||||
|
uint32_t entity = 0;
|
||||||
|
if (now - r.frame > 2 ||
|
||||||
|
!skate3::native_lw::LookupLwCtx(it->first, &alpha, &entity)) {
|
||||||
|
it = g_lw_last_items.erase(it);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
scene.items.push_back(r.item);
|
||||||
|
scene.items.back().dbg_src = 9; // gap fill (refresh below skips it)
|
||||||
|
g_lw_gap_filled.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
static std::atomic<uint32_t> s_fill_logged{0};
|
||||||
|
const uint32_t ln = s_fill_logged.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (ln < 16 || (ln & 255u) == 0) {
|
||||||
|
REXLOG_INFO(
|
||||||
|
"native-scene: LW gap fill ctx={:08X} mesh={:08X} fam={} "
|
||||||
|
"age={} alpha={:.2f} (n={})",
|
||||||
|
it->first, r.item.mesh, r.item.char_family, now - r.frame,
|
||||||
|
alpha, ln);
|
||||||
|
}
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
for (const DrawItem& item : scene.items) {
|
||||||
|
// NOTE: lw_entity is not stamped yet here (the stamp pass runs just
|
||||||
|
// before the smoothing block); LW membership is enforced at FILL
|
||||||
|
// time by the store lookup; non-LW entries simply expire unused.
|
||||||
|
if (item.char_family == 0 || item.ctx == 0 || item.ropa ||
|
||||||
|
item.pending || item.caster_bank || item.dbg_src == 9 ||
|
||||||
|
!item.skinned || item.bones.empty() || item.draws.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (g_lw_last_items.size() > 512 &&
|
||||||
|
g_lw_last_items.find(item.ctx) == g_lw_last_items.end()) {
|
||||||
|
continue; // growth backstop
|
||||||
|
}
|
||||||
|
LwRetained& r = g_lw_last_items[item.ctx];
|
||||||
|
r.item = item;
|
||||||
|
r.frame = now;
|
||||||
|
}
|
||||||
|
} else if (!g_lw_last_items.empty()) {
|
||||||
|
g_lw_last_items.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Cross-frame character-lighting fallback (see g_char_rows_cache): items
|
// Cross-frame character-lighting fallback (see g_char_rows_cache): items
|
||||||
// whose capture chain never validated THIS frame reuse their garment's
|
// whose capture chain never validated THIS frame reuse their garment's
|
||||||
|
|
@ -6963,6 +7209,110 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LivingWorld entity store stamp: map
|
||||||
|
// each character-family item's MeshContext to its owning LW entity and
|
||||||
|
// stamp the entity's authoritative opacity + per-instance identity. Runs
|
||||||
|
// after every dyn publish path (captures, merges, rescues, vehicle
|
||||||
|
// retention) and before the smoothing block so the pose rings can key by
|
||||||
|
// identity. Items with no fresh store entry (player skater - a different
|
||||||
|
// fade system, CAC entities, despawned/retained leftovers) keep the
|
||||||
|
// captured-row behavior untouched.
|
||||||
|
if (REXCVAR_GET(skate3_native_render_scene_lw_fade) ||
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_lw_identity)) {
|
||||||
|
// Fade serving also honors the master entity-fade switch: with it off
|
||||||
|
// the routing ignores fades entirely, so the alpha row must keep its
|
||||||
|
// captured value (the shader still reads it on some paths).
|
||||||
|
const bool serve_fade =
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_lw_fade) &&
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_entity_fade);
|
||||||
|
const bool serve_id = REXCVAR_GET(skate3_native_render_scene_lw_identity);
|
||||||
|
for (DrawItem& item : scene.items) {
|
||||||
|
if (item.char_family == 0 || item.ctx == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float alpha = 1.0f;
|
||||||
|
uint32_t entity = 0;
|
||||||
|
if (!skate3::native_lw::LookupLwCtx(item.ctx, &alpha, &entity)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
g_lw_stamped.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (serve_id) {
|
||||||
|
item.lw_entity = entity;
|
||||||
|
}
|
||||||
|
// Per-ctx lighting/paint rows (see g_char_rows_cache_ctx): refresh
|
||||||
|
// from validated captures, serve on capture-failed frames, entity-
|
||||||
|
// checked so a recycled instance never inherits foreign paint. This
|
||||||
|
// keeps edge-of-view vehicles (caster-only capture stretches, every
|
||||||
|
// read rejected) on their own shading instead of legacy flat.
|
||||||
|
if (item.char_rows[14 * 4 + 1] > 0.0f) {
|
||||||
|
if (g_char_rows_cache_ctx.size() > 4096) {
|
||||||
|
g_char_rows_cache_ctx.clear();
|
||||||
|
}
|
||||||
|
CharRowsCtx& cr = g_char_rows_cache_ctx[item.ctx];
|
||||||
|
std::memcpy(cr.rows.data(), item.char_rows, sizeof(item.char_rows));
|
||||||
|
cr.entity = entity;
|
||||||
|
} else {
|
||||||
|
const auto rit = g_char_rows_cache_ctx.find(item.ctx);
|
||||||
|
if (rit != g_char_rows_cache_ctx.end() &&
|
||||||
|
rit->second.entity == entity) {
|
||||||
|
std::memcpy(item.char_rows, rit->second.rows.data(),
|
||||||
|
sizeof(item.char_rows));
|
||||||
|
g_lw_rows_served.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Authoritative palette for every vehicle pose that did NOT come
|
||||||
|
// from a fresh perspective capture: caster-only frames (ortho banks
|
||||||
|
// = GUESSED palette base + ~40 ms-stale animation, the edge-of-view
|
||||||
|
// mangle class), retention re-publishes (last capture aging up to 10
|
||||||
|
// frames; at driving speed a stale republish renders a GHOST
|
||||||
|
// meters BEHIND the live position right after the vehicle exits the
|
||||||
|
// view; a garbage last capture makes the ghost sideways), rescues
|
||||||
|
// and gap fills. The entity's OWN packed palette (m_matrices,
|
||||||
|
// written by the pack writer this sim tick) is its true current
|
||||||
|
// pose: a genuinely-exited vehicle lands off-screen (no ghost), a
|
||||||
|
// pan-trailing retention case lands exactly right.
|
||||||
|
if (item.skinned && !item.ropa &&
|
||||||
|
(item.char_family == 6 || item.char_family == 7) &&
|
||||||
|
(item.caster_bank || item.retained || item.dbg_src == 3 ||
|
||||||
|
item.dbg_src == 6 || item.dbg_src == 9) &&
|
||||||
|
!item.bones.empty() &&
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_lw_palette)) {
|
||||||
|
float rows[96 * 12];
|
||||||
|
const uint32_t n =
|
||||||
|
skate3::native_lw::LookupLwPalette(item.ctx, rows, 96 * 12);
|
||||||
|
if (n != 0 && size_t(n) * 12 <= item.bones.size()) {
|
||||||
|
std::memcpy(item.bones.data(), rows, size_t(n) * 12 * sizeof(float));
|
||||||
|
item.caster_bank = false;
|
||||||
|
item.dbg_src = 10; // lw palette substitution
|
||||||
|
g_lw_pal_sub.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
static std::atomic<uint32_t> s_sub_logged{0};
|
||||||
|
const uint32_t ln =
|
||||||
|
s_sub_logged.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (ln < 16 || (ln & 511u) == 0) {
|
||||||
|
REXLOG_INFO(
|
||||||
|
"native-scene: LW palette substituted ctx={:08X} mesh={:08X} "
|
||||||
|
"fam={} rows={} (n={})",
|
||||||
|
item.ctx, item.mesh, item.char_family, n, ln);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (serve_fade) {
|
||||||
|
item.lw_alpha = alpha;
|
||||||
|
// The exact shading path reads the alpha ROW (cbuffer CH row 14.x)
|
||||||
|
// - overwrite it with the entity value for the families where that
|
||||||
|
// row IS the raw entity fade on console (c13.x / c21.x / c22.x /
|
||||||
|
// c20.x). Hair (strand-scale composed) and vehicle glass
|
||||||
|
// (tint-composed) keep their captured rows; CharFadeAlpha bounds
|
||||||
|
// them by the entity alpha instead.
|
||||||
|
if (item.char_rows[14 * 4 + 1] > 0.0f &&
|
||||||
|
(item.char_family == 1 || item.char_family == 2 ||
|
||||||
|
item.char_family == 3 || item.char_family == 6)) {
|
||||||
|
item.char_rows[14 * 4 + 0] = std::clamp(alpha, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Camera re-timing (see SmoothCamera): replace the guest's sim-stepped
|
// Camera re-timing (see SmoothCamera): replace the guest's sim-stepped
|
||||||
// pose with a host-clock-interpolated one so panning is smooth at render
|
// pose with a host-clock-interpolated one so panning is smooth at render
|
||||||
// rate. All consumers (items, sky follow, sorting, outline) use the
|
// rate. All consumers (items, sky follow, sorting, outline) use the
|
||||||
|
|
@ -13107,6 +13457,8 @@ void LogFrameStats(const FrameScene& scene, uint64_t frames, uint32_t drawn,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (frames % 600 == 0) {
|
if (frames % 600 == 0) {
|
||||||
|
uint32_t lw_ctxs = 0, lw_ents = 0;
|
||||||
|
skate3::native_lw::QueryLwStats(&lw_ctxs, &lw_ents);
|
||||||
REXLOG_INFO(
|
REXLOG_INFO(
|
||||||
"native-scene: frame {} items={} draws={} draws_2d={} drawn_2d={} "
|
"native-scene: frame {} items={} draws={} draws_2d={} drawn_2d={} "
|
||||||
"splines[{}/{}] "
|
"splines[{}/{}] "
|
||||||
|
|
@ -13118,7 +13470,9 @@ void LogFrameStats(const FrameScene& scene, uint64_t frames, uint32_t drawn,
|
||||||
"store[n={} routes={} evict={}] "
|
"store[n={} routes={} evict={}] "
|
||||||
"heal[vfail={} dfail={} demote={}] serve[sticky={} skipnew={} adstale={} adnone={}] "
|
"heal[vfail={} dfail={} demote={}] serve[sticky={} skipnew={} adstale={} adnone={}] "
|
||||||
"shadow[valid={} ready={} draws={}] char[attempt={} valid={} drawn={} reused={} "
|
"shadow[valid={} ready={} draws={}] char[attempt={} valid={} drawn={} reused={} "
|
||||||
"bones_rescued={}] dynobj[valid={} drawn={}] refl[pair={} flat={} gate={:#x}]",
|
"bones_rescued={}] dynobj[valid={} drawn={}] "
|
||||||
|
"lw[ctxs={} ents={} stamp={} fade0={} resc={} fill={} pal={} rows={}] "
|
||||||
|
"refl[pair={} flat={} gate={:#x}]",
|
||||||
frames, scene.items.size(), drawn, g_draws_2d.load(), drawn_2d,
|
frames, scene.items.size(), drawn, g_draws_2d.load(), drawn_2d,
|
||||||
drawn_spline, g_draws_spline.load(),
|
drawn_spline, g_draws_spline.load(),
|
||||||
g_draws_2d_other.load(), g_draws_2d_dropped.load(), g_r.tex_store.size(),
|
g_draws_2d_other.load(), g_draws_2d_dropped.load(), g_r.tex_store.size(),
|
||||||
|
|
@ -13144,8 +13498,10 @@ void LogFrameStats(const FrameScene& scene, uint64_t frames, uint32_t drawn,
|
||||||
shadow_ready, shadow_draws,
|
shadow_ready, shadow_draws,
|
||||||
g_char_attempts.load(), g_char_valid.load(), g_char_drawn.load(),
|
g_char_attempts.load(), g_char_valid.load(), g_char_drawn.load(),
|
||||||
g_char_rows_reused.load(), g_bones_rescued.load(), scene.dynobj_valid,
|
g_char_rows_reused.load(), g_bones_rescued.load(), scene.dynobj_valid,
|
||||||
g_dynobj_drawn.load(), g_refl_pair.load(), g_refl_flat.load(),
|
g_dynobj_drawn.load(), lw_ctxs, lw_ents, g_lw_stamped.load(),
|
||||||
g_refl_gate.load());
|
g_lw_fade0.load(), g_lw_ctx_rescued.load(), g_lw_gap_filled.load(),
|
||||||
|
g_lw_pal_sub.load(), g_lw_rows_served.load(),
|
||||||
|
g_refl_pair.load(), g_refl_flat.load(), g_refl_gate.load());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -14692,8 +15048,12 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
// LivingWorld entity draws at alpha 0 through the whole spawn settle
|
// LivingWorld entity draws at alpha 0 through the whole spawn settle
|
||||||
// (NPCs drop ~1 m to the ground before fading in) and ramps alpha with
|
// (NPCs drop ~1 m to the ground before fading in) and ramps alpha with
|
||||||
// distance; skip invisible items entirely, blend mid-fade ones.
|
// distance; skip invisible items entirely, blend mid-fade ones.
|
||||||
|
// LW-mapped items (lw_alpha >= 0) do not need a validated lighting
|
||||||
|
// capture to honor the fade: the entity alpha is authoritative even
|
||||||
|
// when the capture chain failed; a spawn-settling NPC is invisible
|
||||||
|
// regardless of whether its rows validated this frame.
|
||||||
const bool entity_fade = debug_mode == 0 && item.char_family != 0 &&
|
const bool entity_fade = debug_mode == 0 && item.char_family != 0 &&
|
||||||
char_capture_ok &&
|
(char_capture_ok || item.lw_alpha >= 0.0f) &&
|
||||||
REXCVAR_GET(skate3_native_render_scene_entity_fade);
|
REXCVAR_GET(skate3_native_render_scene_entity_fade);
|
||||||
const float fade_a = entity_fade ? CharFadeAlpha(item) : 1.0f;
|
const float fade_a = entity_fade ? CharFadeAlpha(item) : 1.0f;
|
||||||
// One-frame fade-blink guard: a mesh that rendered ~opaque last frame
|
// One-frame fade-blink guard: a mesh that rendered ~opaque last frame
|
||||||
|
|
@ -14705,8 +15065,13 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
// skip + the mid-fade blend routing) and log it. The last-alpha map
|
// skip + the mid-fade blend routing) and log it. The last-alpha map
|
||||||
// updates from the RAW value, so a persisting alpha 0 (real despawn /
|
// updates from the RAW value, so a persisting alpha 0 (real despawn /
|
||||||
// spawn settle) only gets one repaired frame and then skips normally.
|
// spawn settle) only gets one repaired frame and then skips normally.
|
||||||
|
// LW-mapped items bypass the blink repair entirely (read AND write):
|
||||||
|
// the entity alpha cannot blink; the repair exists for garbage/foreign
|
||||||
|
// CAPTURED rows, and its mesh key is clone-shared, which force-drew
|
||||||
|
// spawn-settling clones OPAQUE every frame their twin was visible (the
|
||||||
|
// "NPC drops out of the sky with no fade" sighting).
|
||||||
bool fade_blink = false;
|
bool fade_blink = false;
|
||||||
if (item.char_family != 0 && debug_mode == 0) {
|
if (item.char_family != 0 && debug_mode == 0 && item.lw_alpha < 0.0f) {
|
||||||
static std::unordered_map<uint32_t, uint8_t> s_fade_opaque; // render thread
|
static std::unordered_map<uint32_t, uint8_t> s_fade_opaque; // render thread
|
||||||
uint8_t& was_opaque = s_fade_opaque[item.mesh];
|
uint8_t& was_opaque = s_fade_opaque[item.mesh];
|
||||||
if (entity_fade && fade_a <= 0.004f && was_opaque) {
|
if (entity_fade && fade_a <= 0.004f && was_opaque) {
|
||||||
|
|
@ -14730,6 +15095,9 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (entity_fade && fade_a <= 0.004f && !fade_blink) {
|
if (entity_fade && fade_a <= 0.004f && !fade_blink) {
|
||||||
|
if (item.lw_alpha >= 0.0f) {
|
||||||
|
g_lw_fade0.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const bool char_fade_blend = char_fade_zwrite(item) && !fade_blink;
|
const bool char_fade_blend = char_fade_zwrite(item) && !fade_blink;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,15 @@ struct DrawItem {
|
||||||
// and the entity store: the owning cModelInstance is resolved backward by
|
// and the entity store: the owning cModelInstance is resolved backward by
|
||||||
// m_arrMeshContext containment (there is no forward mesh->owner pointer).
|
// m_arrMeshContext containment (there is no forward mesh->owner pointer).
|
||||||
uint32_t ctx = 0;
|
uint32_t ctx = 0;
|
||||||
|
// LivingWorld entity store mapping,
|
||||||
|
// stamped at scene build when this ctx belongs to a live LW entity:
|
||||||
|
// lw_alpha = the entity's authoritative opacity (entity+528.x: exactly
|
||||||
|
// the value BindConstants serves this ctx as the shader's output alpha;
|
||||||
|
// -1 = unmapped), lw_entity = the owning entity address (the game's own
|
||||||
|
// per-instance identity, used to key the pose rings so same-mesh clone
|
||||||
|
// reshuffles can never mispair).
|
||||||
|
float lw_alpha = -1.0f;
|
||||||
|
uint32_t lw_entity = 0;
|
||||||
uint32_t mesh; // guest mesh object address (resource cache key)
|
uint32_t mesh; // guest mesh object address (resource cache key)
|
||||||
uint32_t vb_obj; // guest renderengine::VertexBuffer object (draw matching)
|
uint32_t vb_obj; // guest renderengine::VertexBuffer object (draw matching)
|
||||||
uint32_t ib_obj; // guest renderengine::IndexBuffer object (draw matching)
|
uint32_t ib_obj; // guest renderengine::IndexBuffer object (draw matching)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue