render: guest-structure probes + shadow-mode observers

This commit is contained in:
mchughalex 2026-07-13 04:31:55 +01:00
parent 406f47c79c
commit cf274e7457
6 changed files with 173 additions and 40 deletions

View file

@ -229,6 +229,7 @@ set(SKATE3_COMMON_SOURCES
src/skate3_iso_installer.cpp
src/native/skate3_native_diagnostics.cpp
src/native/skate3_native_guest_read.cpp
src/native/skate3_native_v3_shadow.cpp
src/skate3_native_debug_dialog.cpp
src/skate3_native_render.cpp
src/skate3_native_scene.cpp

View file

@ -10,8 +10,56 @@
#include "generated/skate3_init.h"
#if defined(_WIN32)
#include <windows.h>
#endif
namespace skate3::native_scene {
// Lock-free guarded bulk copy for reads of guest payloads (moved verbatim
// from skate3_native_scene.cpp so every TU shares 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
bool GuestTryCopy(void* dst, const void* src, size_t size) {
std::memcpy(dst, src, size);
return true;
}
#endif
float GuestHalfToFloat(uint16_t h) {
const uint32_t sign = uint32_t(h & 0x8000u) << 16;
const uint32_t exp = (h >> 10) & 0x1F;

View file

@ -29,6 +29,13 @@ struct SkinSampleVert {
bool pos_finite; // every |p| < 1e7 (mid-sim-write garbage detection)
};
// SEH-guarded bulk copy of guest memory (host pointers; callers pass
// base + guest_addr). Returns false when the range faults (streaming
// decommit, bogus candidate pointer). ANY guarded guest read must go
// through this function; see the compiler-trap comment at the definition:
// open-coded __try{memcpy}__except compiles to an UNPROTECTED `jmp memcpy`.
bool GuestTryCopy(void* dst, const void* src, size_t size);
float GuestHalfToFloat(uint16_t h);
// Bind-pose bbox diagonal of the item: the reference size every

View file

@ -1,5 +1,6 @@
#include "skate3_native_render.h"
#include "native/skate3_native_v3_shadow.h"
#include "skate3_native_scene.h"
#include "skate3_screenshot.h"
@ -563,10 +564,102 @@ 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);
}
}
// cModelInstance::PackAndMultiplyMatricesForUpload(Matrix44& localToWorld):
// post-call, m_matrices (+0x14) holds exactly the packed 4x3 column-major
// upload palette the VS consumes. r3 = the cModelInstance.
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);
}
}
// 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).
extern "C" REX_FUNC(sub_827A52C8) {
const uint32_t parts = ctx.r3.u32;
const uint32_t count = ctx.r4.u32;
__imp__sub_827A52C8(ctx, base);
if (skate3::native_render::Enabled()) {
skate3::native_v3::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.
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_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.
extern "C" REX_FUNC(sub_827825B0) {
const uint32_t skater = ctx.r3.u32;
__imp__sub_827825B0(ctx, base);
if (skate3::native_render::Enabled()) {
skate3::native_v3::OnRopaStartJobs(base, skater);
}
}
// 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).
extern "C" REX_FUNC(sub_827A6C50) {
const uint32_t rp = ctx.r3.u32;
if (skate3::native_render::Enabled()) {
skate3::native_v3::OnRenderPresentation(base, rp);
}
__imp__sub_827A6C50(ctx, base);
}
extern "C" REX_FUNC(sub_827A6CE8) {
const uint32_t rp = ctx.r3.u32;
if (skate3::native_render::Enabled()) {
skate3::native_v3::OnRenderPresentation(base, rp);
}
__imp__sub_827A6CE8(ctx, base);
}
// pegasus::tRModelData::Fixup(void* model, rw::core::arena::ArenaIterator*)
// - the rw-arena LOAD-time pointer resolve, fired once per model while its
// arena streams in. (The `Unfix(void*, SizeAndAlignment*)` atoms are the

View file

@ -31,6 +31,7 @@
#include "native/skate3_native_diag.h"
#include "native/skate3_native_guest_read.h"
#include "native/skate3_native_v3_shadow.h"
#if defined(REX_HAS_D3D12) && REX_HAS_D3D12
#include <rex/graphics/d3d12/command_processor.h>
@ -1414,46 +1415,11 @@ bool GuestReadableApprox(uint8_t* base, uint32_t addr) {
return addr >= 0x10000;
}
// Lock-free guarded bulk copy for render-thread reads of guest payloads.
// 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.
typedef void* (*GuestMemcpyFn)(void*, const void*, size_t);
volatile GuestMemcpyFn g_guest_memcpy_fn = std::memcpy;
__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
bool GuestTryCopy(void* dst, const void* src, size_t size) {
std::memcpy(dst, src, size);
return true;
}
#endif
// Guarded bulk copy: skate3::native_scene::GuestTryCopy, moved to
// native/skate3_native_guest_read.cpp so every guest reader shares the one
// correctly-built SEH guard. The compiler-trap documentation
// (volatile fn-ptr + noinline: plain __try{memcpy}__except compiles to an
// UNPROTECTED `jmp memcpy`) moved with it.
// SEH-guarded single-value guest loads for registry-time walks (the
// loading-screen prewarm): unlike the capture-path walks, whose pointers
@ -3125,6 +3091,7 @@ uint32_t CaptureDynamicState(uint8_t* base, uint32_t ctx, bool world_path,
if (!BuildItemGeometry(base, ctx, item)) {
return 0;
}
item.ctx = ctx; // identity key for the palette serve / entity store
// The bank only provably holds THIS mesh's constants when the last draw
// that flushed it bound this mesh's buffers (see g_last_draw_ibvb);
// `drew_inside` alone also accepted banks left by another entity's inline
@ -7107,6 +7074,11 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
g_frame_draw_fetch.clear();
}
// v3 shadow-mode readers (Phase 2): compare this frame's authoritative
// guest-structure reads against everything the capture path just built.
// Observers only: must run before the move-publish below.
skate3::native_v3::OnFrameBuilt(base, records, count, scene);
std::lock_guard<std::mutex> lock(g_scene_mutex);
scene.generation = ++g_generation;
g_scene = std::make_shared<const FrameScene>(std::move(scene));

View file

@ -20,6 +20,11 @@ struct DrawEntry {
};
struct DrawItem {
// Guest MeshContext this item was captured from (0 for frame-end world
// items served from the item cache). Identity key for the palette serve
// and the entity store: the owning cModelInstance is resolved backward by
// m_arrMeshContext containment (there is no forward mesh->owner pointer).
uint32_t ctx = 0;
uint32_t mesh; // guest mesh object address (resource cache key)
uint32_t vb_obj; // guest renderengine::VertexBuffer object (draw matching)
uint32_t ib_obj; // guest renderengine::IndexBuffer object (draw matching)
@ -288,6 +293,13 @@ struct SubmitRecord {
bool Enabled();
// The game's per-entity spawn/streaming fade as carried by the item's
// validated character-lighting capture (peds c21.x, defaultcharacter c13.x,
// cacstamp c22.x, vehicle body c20.x, hair strand scale). 1.0 for items
// without a validated capture. Defined in skate3_native_scene.cpp; consumed
// by the render passes.
float CharFadeAlpha(const DrawItem& item);
// Called from the cProcessArenaAsset::RegisterTexture hook: guid -> guest
// renderengine::Texture object.
void OnRegisterTexture(uint64_t guid, uint32_t texture);