From cf274e7457e4177e1701d888e2ff851e582423ae Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Mon, 13 Jul 2026 04:31:55 +0100 Subject: [PATCH] render: guest-structure probes + shadow-mode observers --- CMakeLists.txt | 1 + src/native/skate3_native_guest_read.cpp | 48 +++++++++++++ src/native/skate3_native_guest_read.h | 7 ++ src/skate3_native_render.cpp | 93 +++++++++++++++++++++++++ src/skate3_native_scene.cpp | 52 ++++---------- src/skate3_native_scene.h | 12 ++++ 6 files changed, 173 insertions(+), 40 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 26ee9b6..10915cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/native/skate3_native_guest_read.cpp b/src/native/skate3_native_guest_read.cpp index c3ea786..9832149 100644 --- a/src/native/skate3_native_guest_read.cpp +++ b/src/native/skate3_native_guest_read.cpp @@ -10,8 +10,56 @@ #include "generated/skate3_init.h" +#if defined(_WIN32) +#include +#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; diff --git a/src/native/skate3_native_guest_read.h b/src/native/skate3_native_guest_read.h index ea52b02..3a7b0ca 100644 --- a/src/native/skate3_native_guest_read.h +++ b/src/native/skate3_native_guest_read.h @@ -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 diff --git a/src/skate3_native_render.cpp b/src/skate3_native_render.cpp index bca4a64..10260ba 100644 --- a/src/skate3_native_render.cpp +++ b/src/skate3_native_render.cpp @@ -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 diff --git a/src/skate3_native_scene.cpp b/src/skate3_native_scene.cpp index 58fc99d..19e8fcd 100644 --- a/src/skate3_native_scene.cpp +++ b/src/skate3_native_scene.cpp @@ -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 @@ -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 lock(g_scene_mutex); scene.generation = ++g_generation; g_scene = std::make_shared(std::move(scene)); diff --git a/src/skate3_native_scene.h b/src/skate3_native_scene.h index cd0f3ba..4910de6 100644 --- a/src/skate3_native_scene.h +++ b/src/skate3_native_scene.h @@ -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);