296 lines
9.8 KiB
C++
296 lines
9.8 KiB
C++
// Shared sample-vertex decode + skin + spread helpers (see the header).
|
|
|
|
#include "native/skate3_native_guest_read.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
|
|
#include "generated/skate3_init.h"
|
|
|
|
#if defined(_WIN32)
|
|
#include <windows.h>
|
|
#else
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
|
|
#include <csetjmp>
|
|
#include <mutex>
|
|
|
|
#include <rex/exception_handler.h>
|
|
#endif
|
|
|
|
namespace skate3::native_scene {
|
|
|
|
// Lock-free guarded bulk copy for reads of guest payloads (shared here so
|
|
// every TU uses the one correctly-built guard). GuestRangeReadable's
|
|
// VirtualQuery loop takes the
|
|
// process VAD lock, which the guest streaming threads hammer exactly while
|
|
// panning streams the world in; every render-thread decode then queues
|
|
// behind them (multi-ms stalls; same lock as the 3 fps PERF TRAP). An
|
|
// SEH-guarded memcpy costs nothing in the good case and fails cleanly if
|
|
// streaming decommitted the range between capture and decode. Own function,
|
|
// no C++ objects: SEH cannot share a frame with unwinding.
|
|
#if defined(_WIN32)
|
|
// TWO TRAPS PROVEN FROM CRASH DUMPS OF THE REGISTRY-PREWARM PROBE; both
|
|
// silently delete the guard and let the AV kill the process:
|
|
// 1. clang marks std::memcpy nounwind, concludes the __except is
|
|
// unreachable, and compiles the whole function to `jmp memcpy` (seen in
|
|
// the shipped binary). Calling through a VOLATILE function pointer makes
|
|
// the callee opaque so the SEH scope must be kept.
|
|
// 2. When this function is INLINED into a caller using the C++ EH
|
|
// personality (__CxxFrameHandler3), the __except scope is dropped in the
|
|
// merge, hence the noinline.
|
|
// This means plain `__try { std::memcpy(...) } __except` NEVER protected
|
|
// anything in optimized builds; any future guarded read must go through
|
|
// this function.
|
|
namespace {
|
|
typedef void* (*GuestMemcpyFn)(void*, const void*, size_t);
|
|
volatile GuestMemcpyFn g_guest_memcpy_fn = std::memcpy;
|
|
} // namespace
|
|
__declspec(noinline) bool GuestTryCopy(void* dst, const void* src, size_t size) {
|
|
__try {
|
|
g_guest_memcpy_fn(dst, src, size);
|
|
return true;
|
|
} __except ((GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ||
|
|
GetExceptionCode() == EXCEPTION_IN_PAGE_ERROR)
|
|
? EXCEPTION_EXECUTE_HANDLER
|
|
: EXCEPTION_CONTINUE_SEARCH) { // NOLINT
|
|
return false;
|
|
}
|
|
}
|
|
#else
|
|
// POSIX guard with the same clean-failure semantics as the SEH path: a
|
|
// handler registered in the runtime's exception chain (SIGSEGV/SIGBUS both
|
|
// arrive as Code::kAccessViolation) siglongjmps back here when the fault
|
|
// happened inside the guarded copy on this thread; unrelated faults are
|
|
// declined so the runtime's own handlers (write watches, null traps) keep
|
|
// working. The previous raw std::memcpy crashed the first macOS
|
|
// native-render session (SIGBUS in _platform_memmove under
|
|
// OnRenderModeSceneExit: streaming decommitted the captured range between
|
|
// capture and decode, exactly the case the Windows guard exists for).
|
|
namespace {
|
|
thread_local volatile bool tl_in_guest_copy = false;
|
|
thread_local sigjmp_buf tl_guest_copy_jmp;
|
|
|
|
bool GuestCopyFaultHandler(rex::arch::Exception* ex, void* /*data*/) {
|
|
if (!tl_in_guest_copy ||
|
|
ex->code() != rex::arch::Exception::Code::kAccessViolation) {
|
|
return false; // not ours - keep dispatching down the chain
|
|
}
|
|
tl_in_guest_copy = false;
|
|
// Unwinds the signal frame directly; the kernel-blocked signal is
|
|
// unblocked by the sigsetjmp fault branch (savemask=0 keeps the hot path
|
|
// free of a per-call sigprocmask syscall).
|
|
siglongjmp(tl_guest_copy_jmp, 1);
|
|
}
|
|
|
|
void EnsureGuestCopyHandler() {
|
|
static std::once_flag once;
|
|
std::call_once(once, [] {
|
|
rex::arch::ExceptionHandler::Install(GuestCopyFaultHandler, nullptr);
|
|
});
|
|
}
|
|
} // namespace
|
|
|
|
bool GuestTryCopy(void* dst, const void* src, size_t size) {
|
|
EnsureGuestCopyHandler();
|
|
if (sigsetjmp(tl_guest_copy_jmp, 0) != 0) {
|
|
// Faulted mid-copy and jumped out of the signal handler: the signal is
|
|
// still in this thread's blocked mask (handler entry blocked it, the
|
|
// longjmp skipped the normal return that would restore it). Rare path -
|
|
// the syscall is fine here.
|
|
sigset_t set;
|
|
sigemptyset(&set);
|
|
sigaddset(&set, SIGSEGV);
|
|
sigaddset(&set, SIGBUS);
|
|
pthread_sigmask(SIG_UNBLOCK, &set, nullptr);
|
|
return false;
|
|
}
|
|
tl_in_guest_copy = true;
|
|
std::memcpy(dst, src, size);
|
|
tl_in_guest_copy = false;
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
float GuestHalfToFloat(uint16_t h) {
|
|
const uint32_t sign = uint32_t(h & 0x8000u) << 16;
|
|
const uint32_t exp = (h >> 10) & 0x1F;
|
|
const uint32_t man = h & 0x3FF;
|
|
if (exp == 0) return std::bit_cast<float>(sign); // denorms ~0
|
|
if (exp == 31) return std::bit_cast<float>(sign | 0x7F800000u);
|
|
return std::bit_cast<float>(sign | ((exp + 112) << 23) | (man << 13));
|
|
}
|
|
|
|
float BindDiag(const DrawItem& item) {
|
|
float d2 = 0.0f;
|
|
for (int a = 0; a < 3; ++a) {
|
|
const float d = item.bbox_max[a] - item.bbox_min[a];
|
|
d2 += d * d;
|
|
}
|
|
return std::sqrt(d2);
|
|
}
|
|
|
|
namespace {
|
|
|
|
bool PosFinite(const float p[3]) {
|
|
return p[0] > -1e7f && p[0] < 1e7f && p[1] > -1e7f && p[1] < 1e7f &&
|
|
p[2] > -1e7f && p[2] < 1e7f;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool ReadSkinVertGuest(uint8_t* base, const DrawItem& item, uint32_t vtx,
|
|
SkinSampleVert* out) {
|
|
const uint32_t v = item.vb_addr + vtx * item.stride;
|
|
const uint32_t pa = v + item.pos_offset;
|
|
float* p = out->p;
|
|
switch (item.pos_fmt) {
|
|
case 57:
|
|
for (int a = 0; a < 3; ++a) {
|
|
const uint32_t u = REX_LOAD_U32(pa + a * 4);
|
|
std::memcpy(&p[a], &u, 4);
|
|
}
|
|
break;
|
|
case 32:
|
|
for (int a = 0; a < 3; ++a) {
|
|
p[a] = GuestHalfToFloat(uint16_t(REX_LOAD_U16(pa + a * 2)));
|
|
}
|
|
break;
|
|
case 26: {
|
|
constexpr float kScale = 2.0f / 32767.0f;
|
|
for (int a = 0; a < 3; ++a) {
|
|
p[a] = int16_t(REX_LOAD_U16(pa + a * 2)) * kScale + (a == 1 ? 0.8f : 0.0f);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
out->pos_finite = PosFinite(p);
|
|
if (item.bw_offset != 0 && item.bi_offset != 0) {
|
|
// u8x4 attributes are big-endian per 32-bit word: component k is byte
|
|
// (24 - 8k) of the host-order load.
|
|
const uint32_t bw = REX_LOAD_U32(v + item.bw_offset);
|
|
const uint32_t bi = REX_LOAD_U32(v + item.bi_offset);
|
|
for (int k = 0; k < 4; ++k) {
|
|
out->w[k] = uint8_t((bw >> (24 - 8 * k)) & 0xFF);
|
|
out->bone[k] = uint8_t((bi >> (24 - 8 * k)) & 0xFF);
|
|
}
|
|
} else {
|
|
std::memset(out->w, 0, sizeof(out->w));
|
|
std::memset(out->bone, 0, sizeof(out->bone));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ReadSkinSamplesGuest(uint8_t* base, const DrawItem& item, uint32_t n,
|
|
SkinSampleVert* out) {
|
|
if (item.stride == 0) {
|
|
return false;
|
|
}
|
|
const uint32_t count = item.vb_bytes / item.stride;
|
|
if (count < 2 || n < 2) {
|
|
return false;
|
|
}
|
|
for (uint32_t s = 0; s < n; ++s) {
|
|
if (!ReadSkinVertGuest(base, item, s * (count - 1) / (n - 1), &out[s])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
uint32_t SkinPointHostRows(const SkinSampleVert& sv, const float* rows,
|
|
size_t rows_floats, float q[3]) {
|
|
uint32_t total = 0;
|
|
q[0] = q[1] = q[2] = 0.0f;
|
|
for (int k = 0; k < 4; ++k) {
|
|
const uint32_t w = sv.w[k];
|
|
if (w == 0) continue;
|
|
const uint32_t r0 = 3u * sv.bone[k];
|
|
if ((r0 + 3) * 4 > rows_floats) continue;
|
|
total += w;
|
|
for (int a = 0; a < 3; ++a) {
|
|
const float* row = rows + (r0 + uint32_t(a)) * 4;
|
|
q[a] += float(w) * (row[0] * sv.p[0] + row[1] * sv.p[1] + row[2] * sv.p[2] +
|
|
row[3]);
|
|
}
|
|
}
|
|
if (total != 0) {
|
|
for (int a = 0; a < 3; ++a) q[a] /= float(total);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
uint32_t SkinPointBankRows(uint8_t* base, uint32_t bank, uint32_t pb,
|
|
const SkinSampleVert& sv, float q[3], bool* rows_sane) {
|
|
uint32_t total = 0;
|
|
q[0] = q[1] = q[2] = 0.0f;
|
|
for (int k = 0; k < 4; ++k) {
|
|
const uint32_t w = sv.w[k];
|
|
if (w == 0) continue;
|
|
const uint32_t r0 = pb + 3 * uint32_t(sv.bone[k]);
|
|
if (r0 + 3 > 256) continue;
|
|
total += w;
|
|
for (int a = 0; a < 3; ++a) {
|
|
float row[4];
|
|
for (int i = 0; i < 4; ++i) {
|
|
row[i] = std::bit_cast<float>(REX_LOAD_U32(bank + ((r0 + a) * 4 + i) * 4));
|
|
}
|
|
// A weighted bone's rotation row must be rotation-shaped (norm near
|
|
// the entity scale). The vehicle-flick captures carried WORLD
|
|
// POSITIONS in the rotation slots (norm ~140); those can still
|
|
// project on-screen and pass the geometric gate.
|
|
const float rn = row[0] * row[0] + row[1] * row[1] + row[2] * row[2];
|
|
if (rn < 0.04f || rn > 25.0f) {
|
|
*rows_sane = false;
|
|
}
|
|
q[a] += float(w) * (row[0] * sv.p[0] + row[1] * sv.p[1] + row[2] * sv.p[2] +
|
|
row[3]);
|
|
}
|
|
}
|
|
if (total != 0) {
|
|
for (int a = 0; a < 3; ++a) q[a] /= float(total);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
int SkinnedSpreadHostRows(const SkinSampleVert* sv, uint32_t n, const float* rows,
|
|
size_t rows_floats, int min_n, bool garbage_fails,
|
|
float* out_spread, int* out_n) {
|
|
float qmin[3] = {1e9f, 1e9f, 1e9f};
|
|
float qmax[3] = {-1e9f, -1e9f, -1e9f};
|
|
int weighted = 0;
|
|
for (uint32_t s = 0; s < n; ++s) {
|
|
if (!sv[s].pos_finite) {
|
|
if (garbage_fails) {
|
|
return -1; // NaN/garbage positions mid-sim-write
|
|
}
|
|
continue;
|
|
}
|
|
float q[3];
|
|
if (SkinPointHostRows(sv[s], rows, rows_floats, q) == 0) {
|
|
continue;
|
|
}
|
|
++weighted;
|
|
for (int a = 0; a < 3; ++a) {
|
|
qmin[a] = std::min(qmin[a], q[a]);
|
|
qmax[a] = std::max(qmax[a], q[a]);
|
|
}
|
|
}
|
|
if (out_n) {
|
|
*out_n = weighted;
|
|
}
|
|
if (weighted < min_n) {
|
|
return 0; // nothing to judge
|
|
}
|
|
const float dx = qmax[0] - qmin[0];
|
|
const float dy = qmax[1] - qmin[1];
|
|
const float dz = qmax[2] - qmin[2];
|
|
*out_spread = std::sqrt(dx * dx + dy * dy + dz * dz);
|
|
return 1;
|
|
}
|
|
|
|
} // namespace skate3::native_scene
|