render: guest-structure probes + shadow-mode observers
This commit is contained in:
parent
406f47c79c
commit
cf274e7457
6 changed files with 173 additions and 40 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_v3_shadow.cpp
|
||||||
src/skate3_native_debug_dialog.cpp
|
src/skate3_native_debug_dialog.cpp
|
||||||
src/skate3_native_render.cpp
|
src/skate3_native_render.cpp
|
||||||
src/skate3_native_scene.cpp
|
src/skate3_native_scene.cpp
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,56 @@
|
||||||
|
|
||||||
#include "generated/skate3_init.h"
|
#include "generated/skate3_init.h"
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace skate3::native_scene {
|
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) {
|
float GuestHalfToFloat(uint16_t h) {
|
||||||
const uint32_t sign = uint32_t(h & 0x8000u) << 16;
|
const uint32_t sign = uint32_t(h & 0x8000u) << 16;
|
||||||
const uint32_t exp = (h >> 10) & 0x1F;
|
const uint32_t exp = (h >> 10) & 0x1F;
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,13 @@ struct SkinSampleVert {
|
||||||
bool pos_finite; // every |p| < 1e7 (mid-sim-write garbage detection)
|
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);
|
float GuestHalfToFloat(uint16_t h);
|
||||||
|
|
||||||
// Bind-pose bbox diagonal of the item: the reference size every
|
// Bind-pose bbox diagonal of the item: the reference size every
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#include "skate3_native_render.h"
|
#include "skate3_native_render.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"
|
||||||
|
|
||||||
|
|
@ -563,10 +564,102 @@ extern "C" REX_FUNC(sub_82B82E08) {
|
||||||
extern "C" REX_FUNC(sub_82C9A618) {
|
extern "C" REX_FUNC(sub_82C9A618) {
|
||||||
if (skate3::native_render::Enabled()) {
|
if (skate3::native_render::Enabled()) {
|
||||||
skate3::native_scene::OnRegisterTexture(ctx.r4.u64, ctx.r5.u32);
|
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);
|
__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*)
|
// pegasus::tRModelData::Fixup(void* model, rw::core::arena::ArenaIterator*)
|
||||||
// - the rw-arena LOAD-time pointer resolve, fired once per model while its
|
// - the rw-arena LOAD-time pointer resolve, fired once per model while its
|
||||||
// arena streams in. (The `Unfix(void*, SizeAndAlignment*)` atoms are the
|
// arena streams in. (The `Unfix(void*, SizeAndAlignment*)` atoms are the
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,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_v3_shadow.h"
|
||||||
|
|
||||||
#if defined(REX_HAS_D3D12) && REX_HAS_D3D12
|
#if defined(REX_HAS_D3D12) && REX_HAS_D3D12
|
||||||
#include <rex/graphics/d3d12/command_processor.h>
|
#include <rex/graphics/d3d12/command_processor.h>
|
||||||
|
|
@ -1414,46 +1415,11 @@ bool GuestReadableApprox(uint8_t* base, uint32_t addr) {
|
||||||
return addr >= 0x10000;
|
return addr >= 0x10000;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lock-free guarded bulk copy for render-thread reads of guest payloads.
|
// Guarded bulk copy: skate3::native_scene::GuestTryCopy, moved to
|
||||||
// GuestRangeReadable's VirtualQuery loop takes the process VAD lock, which
|
// native/skate3_native_guest_read.cpp so every guest reader shares the one
|
||||||
// the guest streaming threads hammer exactly while panning streams the world
|
// correctly-built SEH guard. The compiler-trap documentation
|
||||||
// in; every render-thread decode then queues behind them (multi-ms stalls;
|
// (volatile fn-ptr + noinline: plain __try{memcpy}__except compiles to an
|
||||||
// same lock as the 3 fps PERF TRAP). An SEH-guarded memcpy costs nothing in
|
// UNPROTECTED `jmp memcpy`) moved with it.
|
||||||
// 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
|
|
||||||
|
|
||||||
// SEH-guarded single-value guest loads for registry-time walks (the
|
// SEH-guarded single-value guest loads for registry-time walks (the
|
||||||
// loading-screen prewarm): unlike the capture-path walks, whose pointers
|
// 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)) {
|
if (!BuildItemGeometry(base, ctx, item)) {
|
||||||
return 0;
|
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
|
// 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);
|
// 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
|
// `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();
|
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);
|
std::lock_guard<std::mutex> lock(g_scene_mutex);
|
||||||
scene.generation = ++g_generation;
|
scene.generation = ++g_generation;
|
||||||
g_scene = std::make_shared<const FrameScene>(std::move(scene));
|
g_scene = std::make_shared<const FrameScene>(std::move(scene));
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,11 @@ struct DrawEntry {
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DrawItem {
|
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 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)
|
||||||
|
|
@ -288,6 +293,13 @@ struct SubmitRecord {
|
||||||
|
|
||||||
bool Enabled();
|
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
|
// Called from the cProcessArenaAsset::RegisterTexture hook: guid -> guest
|
||||||
// renderengine::Texture object.
|
// renderengine::Texture object.
|
||||||
void OnRegisterTexture(uint64_t guid, uint32_t texture);
|
void OnRegisterTexture(uint64_t guid, uint32_t texture);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue