props, hair, normals, sky, mips, 4x MSAA; NPC torso palette + capture tooling

This commit is contained in:
mchughalex 2026-07-02 22:50:31 +01:00
parent 4a2231b14d
commit d101dc8b78
4 changed files with 1919 additions and 358 deletions

View file

@ -35,9 +35,14 @@ REXCVAR_DEFINE_INT32(skate3_native_render_snapshot_min_meshes, 0, "Skate 3",
.range(0, 100000) .range(0, 100000)
.lifecycle(rex::cvar::Lifecycle::kHotReload); .lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_INT32(skate3_native_render_snapshot_frames, 4, "Skate 3", REXCVAR_DEFINE_INT32(skate3_native_render_snapshot_frames, 4, "Skate 3",
"Number of consecutive frames of RenderMesh records to collect before " "Number of frames of RenderMesh records to collect before writing "
"writing the snapshot") "the snapshot")
.range(1, 64) .range(1, 600)
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_INT32(skate3_native_render_snapshot_stride, 1, "Skate 3",
"Record every Nth frame while collecting (long viewer recordings: "
"e.g. 12 = ~12 recorded frames/sec at 144fps)")
.range(1, 32)
.lifecycle(rex::cvar::Lifecycle::kHotReload); .lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_STRING(skate3_native_render_snapshot_dir, "native_render_snapshots", "Skate 3", REXCVAR_DEFINE_STRING(skate3_native_render_snapshot_dir, "native_render_snapshots", "Skate 3",
"Directory for native-render guest memory snapshots and metadata") "Directory for native-render guest memory snapshots and metadata")
@ -46,9 +51,12 @@ REXCVAR_DEFINE_STRING(skate3_native_render_snapshot_dir, "native_render_snapshot
namespace skate3::native_render { namespace skate3::native_render {
namespace { namespace {
// One per-mesh submission. kind 0 = RenderMesh (dynamic entities: r3 is the // One per-mesh submission. kind 0 = RenderMesh (dynamic entities: a = the
// MeshContext itself, b = VertexProgramState). kind 1 = SceneRenderView draw // MeshContext, b = VertexProgramState, c = dynitem index+1). kind 1 =
// list entry (world geometry: a = MeshContext, b = sort key, c = view). // SceneRenderView sort-list entry (a = MeshContext, b = list offset from the
// view, c = view). kind 2 = world-path capture (skinned / model-space prop:
// a = MeshContext, b = submitting view, c = dynitem index+1). kind 3 =
// quad-list DrawVertices capture (a = synthetic key, c = dynitem index+1).
using RenderMeshRecord = skate3::native_scene::SubmitRecord; using RenderMeshRecord = skate3::native_scene::SubmitRecord;
struct FrameRecords { struct FrameRecords {
@ -60,7 +68,11 @@ std::mutex g_mutex;
std::vector<RenderMeshRecord> g_current_frame; std::vector<RenderMeshRecord> g_current_frame;
std::vector<FrameRecords> g_collected_frames; std::vector<FrameRecords> g_collected_frames;
uint64_t g_frame_index = 0; uint64_t g_frame_index = 0;
uint64_t g_collect_counter = 0;
bool g_collecting = false; bool g_collecting = false;
// F10 immediate mode: capture exactly one frame regardless of the snapshot
// frames/stride cvars.
bool g_immediate = false;
bool g_snapshot_written = false; bool g_snapshot_written = false;
std::atomic<bool> g_announced{false}; std::atomic<bool> g_announced{false};
@ -156,8 +168,9 @@ bool WriteMetadata(const std::filesystem::path& path,
<< "\"phys_mirror_note\":\"guest addr A -> file offset A, plus 0x1000 for A >= " << "\"phys_mirror_note\":\"guest addr A -> file offset A, plus 0x1000 for A >= "
"0xE0000000\"," "0xE0000000\","
<< "\"record_fields\":[\"kind\",\"a\",\"b\",\"c\"]," << "\"record_fields\":[\"kind\",\"a\",\"b\",\"c\"],"
<< "\"kinds\":{\"0\":\"RenderMesh a=ctx b=vps\",\"1\":\"SceneDrawList a=ctx " << "\"kinds\":{\"0\":\"RenderMesh a=ctx b=vps c=dyn\",\"1\":\"SceneDrawList a=ctx "
"b=sort_key c=view\"}}\n"; "b=list_offset c=view\",\"2\":\"WorldPathCapture a=ctx b=view c=dyn\","
"\"3\":\"QuadListDraw a=key c=dyn\"}}\n";
for (const FrameRecords& frame : frames) { for (const FrameRecords& frame : frames) {
out << "{\"type\":\"frame\",\"index\":" << frame.frame_index << ",\"records\":["; out << "{\"type\":\"frame\",\"index\":" << frame.frame_index << ",\"records\":[";
for (size_t i = 0; i < frame.records.size(); ++i) { for (size_t i = 0; i < frame.records.size(); ++i) {
@ -173,15 +186,24 @@ bool WriteMetadata(const std::filesystem::path& path,
return static_cast<bool>(out); return static_cast<bool>(out);
} }
void OnRenderMesh(uint8_t* base, uint32_t mesh_context, uint32_t vertex_program_state) { void OnRenderMesh(uint8_t* base, uint32_t mesh_context, uint32_t vertex_program_state,
const uint32_t dyn = skate3::native_scene::CaptureDynamicState(base, mesh_context); bool drew_inside) {
const uint32_t dyn = skate3::native_scene::CaptureDynamicState(
base, mesh_context, /*world_path=*/false, drew_inside);
std::lock_guard<std::mutex> lock(g_mutex); std::lock_guard<std::mutex> lock(g_mutex);
g_current_frame.push_back({0, mesh_context, vertex_program_state, dyn}); g_current_frame.push_back({0, mesh_context, vertex_program_state, dyn});
} }
// SceneRenderView draw-list renderer sub_827FAF50(view, sort_vec, first, count): // SceneRenderView draw-list renderer sub_827FAF50(view, sort_vec, first, count):
// sort_vec points at an eastl vector whose [0] is the entry array; entries are // sort_vec points at an eastl vector whose [0] is the entry array; entries are
// 8 bytes {u32 sort_key, MeshContext*}. // 8 bytes {u32 sort_key, MeshContext*}.
//
// Skinned entries and rigid MODEL-SPACE props (vending machines and other
// movables that never reach RenderMesh) are captured HERE, before the
// dispatcher draws the list. The captures are transform-pending; the
// post-draw fixup attaches the palette / world matrix at whichever draw
// eventually consumes the mesh's buffers.
void OnSceneDrawList(uint8_t* base, uint32_t view, uint32_t sort_vec, uint32_t first, void OnSceneDrawList(uint8_t* base, uint32_t view, uint32_t sort_vec, uint32_t first,
uint32_t count) { uint32_t count) {
if (count == 0 || count > 100000) { if (count == 0 || count > 100000) {
@ -198,8 +220,17 @@ void OnSceneDrawList(uint8_t* base, uint32_t view, uint32_t sort_vec, uint32_t f
for (uint32_t i = 0; i < count; ++i) { for (uint32_t i = 0; i < count; ++i) {
const uint32_t entry = entries + (first + i) * 8; const uint32_t entry = entries + (first + i) * 8;
const uint32_t mesh_context = REX_LOAD_U32(entry + 4); const uint32_t mesh_context = REX_LOAD_U32(entry + 4);
if (mesh_context != 0) { if (mesh_context == 0) {
g_current_frame.push_back({1, mesh_context, list_offset, view}); continue;
}
g_current_frame.push_back({1, mesh_context, list_offset, view});
const uint32_t dyn =
skate3::native_scene::CaptureDynamicState(base, mesh_context, /*world_path=*/true);
if (dyn != 0) {
// b = the submitting view: shadow-cascade views submit their own
// contexts for the same NPCs; rendering those creates ghost
// duplicates (torso-less: their deferred skin passes never run).
g_current_frame.push_back({2, mesh_context, view, dyn});
} }
} }
} }
@ -211,12 +242,25 @@ void WriteSnapshotLocked(uint8_t* base) {
const std::string stem = SnapshotStem(); const std::string stem = SnapshotStem();
const bool meta_ok = WriteMetadata(dir / (stem + ".meta.jsonl"), g_collected_frames); const bool meta_ok = WriteMetadata(dir / (stem + ".meta.jsonl"), g_collected_frames);
const bool snap_ok = WriteMemorySnapshot(base, dir / (stem + ".gsnap")); const bool snap_ok = WriteMemorySnapshot(base, dir / (stem + ".gsnap"));
skate3::native_scene::WriteRecording(dir.string().c_str(), stem.c_str());
REXLOG_INFO("native-render snapshot: {} ({} frames of records, meta_ok={} snap_ok={})", REXLOG_INFO("native-render snapshot: {} ({} frames of records, meta_ok={} snap_ok={})",
stem, g_collected_frames.size(), meta_ok, snap_ok); stem, g_collected_frames.size(), meta_ok, snap_ok);
g_collected_frames.clear(); g_collected_frames.clear();
g_snapshot_written = true; g_snapshot_written = true;
} }
// Non-indexed cloth patch draws (see native_scene::CaptureClothDraw). The
// synthetic "context" key is the dynamic buffer object, stable per garment.
void OnClothDraw(uint8_t* base, uint32_t r4, uint32_t r5, uint32_t r6, uint32_t r7) {
uint32_t key = 0;
const uint32_t dyn = skate3::native_scene::CaptureClothDraw(base, r4, r5, r6, r7, &key);
if (dyn == 0) {
return;
}
std::lock_guard<std::mutex> lock(g_mutex);
g_current_frame.push_back({3, key, 0, dyn});
}
void OnFrameEnd(uint8_t* base) { void OnFrameEnd(uint8_t* base) {
std::lock_guard<std::mutex> lock(g_mutex); std::lock_guard<std::mutex> lock(g_mutex);
++g_frame_index; ++g_frame_index;
@ -236,29 +280,73 @@ void OnFrameEnd(uint8_t* base) {
if (!g_collecting && min_meshes > 0 && if (!g_collecting && min_meshes > 0 &&
mesh_count >= static_cast<size_t>(min_meshes)) { mesh_count >= static_cast<size_t>(min_meshes)) {
g_collecting = true; g_collecting = true;
g_collect_counter = 0;
skate3::native_scene::StartRecording(
uint32_t(REXCVAR_GET(skate3_native_render_snapshot_stride)));
REXLOG_INFO("native-render snapshot: armed at frame {} ({} meshes)", g_frame_index, REXLOG_INFO("native-render snapshot: armed at frame {} ({} meshes)", g_frame_index,
mesh_count); mesh_count);
} }
} }
// Manual trigger (works repeatedly): create <snapshot_dir>\trigger while // Manual triggers (work repeatedly): press F9 (window recording per the
// the game runs. // snapshot cvars), F10 (IMMEDIATE single-frame capture: full memory
// snapshot + this frame's records/draws, for catching a broken object the
// moment it is on screen), or create <snapshot_dir>\trigger.
#if defined(_WIN32)
if (!g_collecting) {
static bool f9_was_down = false;
const bool f9_down = (GetAsyncKeyState(VK_F9) & 0x8000) != 0;
if (f9_down && !f9_was_down) {
g_collecting = true;
g_collect_counter = 0;
skate3::native_scene::StartRecording(
uint32_t(REXCVAR_GET(skate3_native_render_snapshot_stride)));
REXLOG_INFO("native-render snapshot: F9, armed at frame {} ({} meshes)",
g_frame_index, mesh_count);
}
f9_was_down = f9_down;
static bool f10_was_down = false;
const bool f10_down = (GetAsyncKeyState(VK_F10) & 0x8000) != 0;
if (f10_down && !f10_was_down) {
g_collecting = true;
g_immediate = true;
g_collect_counter = 0;
skate3::native_scene::StartRecording(1);
REXLOG_INFO("native-render snapshot: F10, immediate single-frame capture at frame {}",
g_frame_index);
}
f10_was_down = f10_down;
}
#endif
if (!g_collecting && g_frame_index % 32 == 0) { if (!g_collecting && g_frame_index % 32 == 0) {
const std::filesystem::path trigger = SnapshotDir() / "trigger"; const std::filesystem::path trigger = SnapshotDir() / "trigger";
std::error_code ec; std::error_code ec;
if (std::filesystem::exists(trigger, ec)) { if (std::filesystem::exists(trigger, ec)) {
std::filesystem::remove(trigger, ec); std::filesystem::remove(trigger, ec);
g_collecting = true; g_collecting = true;
g_collect_counter = 0;
skate3::native_scene::StartRecording(
uint32_t(REXCVAR_GET(skate3_native_render_snapshot_stride)));
REXLOG_INFO("native-render snapshot: trigger file, armed at frame {} ({} meshes)", REXLOG_INFO("native-render snapshot: trigger file, armed at frame {} ({} meshes)",
g_frame_index, mesh_count); g_frame_index, mesh_count);
} }
} }
if (g_collecting) { if (g_collecting) {
g_collected_frames.push_back({g_frame_index, std::move(g_current_frame)}); // Immediate (F10) captures skip the arming frame: the scene-side
const auto wanted = // recording was only armed after this frame's BuildFrameScene ran, so
static_cast<size_t>(REXCVAR_GET(skate3_native_render_snapshot_frames)); // the first fully-recorded frame is the next one.
if (g_collected_frames.size() >= wanted) { const auto stride = g_immediate
g_collecting = false; ? uint64_t(2)
WriteSnapshotLocked(base); : static_cast<uint64_t>(REXCVAR_GET(skate3_native_render_snapshot_stride));
if (++g_collect_counter % stride == 0) {
g_collected_frames.push_back({g_frame_index, std::move(g_current_frame)});
const auto wanted = g_immediate
? size_t(1)
: static_cast<size_t>(REXCVAR_GET(skate3_native_render_snapshot_frames));
if (g_collected_frames.size() >= wanted) {
g_collecting = false;
g_immediate = false;
WriteSnapshotLocked(base);
}
} }
} }
@ -285,11 +373,27 @@ void Install() {
// "RenderMesh" per-visible-mesh submission for dynamic entities (characters, // "RenderMesh" per-visible-mesh submission for dynamic entities (characters,
// props). Actual convention (verified via recompiled code + snapshot): // props). Actual convention (verified via recompiled code + snapshot):
// r3 = MeshContext*, r4 = renderengine::VertexProgramState*. // r3 = MeshContext*, r4 = renderengine::VertexProgramState*.
//
// The dynamic state snapshot must be taken AFTER the original call: the
// mesh's VS constants (instance world matrix, bone palette) are only flushed
// through D3D::SetPending_AluConstants from inside DrawIndexedVertices
// (sub_82B7AD68), i.e. during the RenderMesh body. Capturing on entry reads
// the PREVIOUS draw's constants and renders every dynamic entity with the
// previous entity's transform/palette.
// Deferred (multi-pass) meshes draw nothing inside the call, detected via
// the draw sequence counter so their transforms are left for the post-draw
// fixup instead of being read from a stale constant bank.
extern "C" REX_FUNC(sub_82795AD8) { extern "C" REX_FUNC(sub_82795AD8) {
if (skate3::native_render::Enabled()) { const bool enabled = skate3::native_render::Enabled();
skate3::native_render::OnRenderMesh(base, ctx.r3.u32, ctx.r4.u32); const uint32_t mesh_context = ctx.r3.u32;
} const uint32_t vertex_program_state = ctx.r4.u32;
const uint64_t draws_before = skate3::native_scene::DrawSequence();
__imp__sub_82795AD8(ctx, base); __imp__sub_82795AD8(ctx, base);
if (enabled) {
const bool drew_inside = skate3::native_scene::DrawSequence() != draws_before;
skate3::native_render::OnRenderMesh(base, mesh_context, vertex_program_state,
drew_inside);
}
} }
// SceneRenderView sorted draw-list renderer (world geometry): // SceneRenderView sorted draw-list renderer (world geometry):
@ -304,6 +408,7 @@ extern "C" REX_FUNC(sub_827FAF50) {
__imp__sub_827FAF50(ctx, base); __imp__sub_827FAF50(ctx, base);
} }
// Guest D3D Swap: frame boundary. // Guest D3D Swap: frame boundary.
extern "C" REX_FUNC(sub_82B82E08) { extern "C" REX_FUNC(sub_82B82E08) {
if (skate3::native_render::Enabled()) { if (skate3::native_render::Enabled()) {
@ -323,11 +428,90 @@ extern "C" REX_FUNC(sub_82C9A618) {
} }
// D3D::SetPending_AluConstants(device, u64 dirty_group_mask, bank, ptr): // D3D::SetPending_AluConstants(device, u64 dirty_group_mask, bank, ptr):
// bank 0x4000 = vertex constants. Bone palettes are staged through here // bank 0x4000 = vertex constants. Called from inside the Draw* functions;
// right before the skinned RenderMesh calls that consume them. // ptr is the device's positional constant shadow bank.
extern "C" REX_FUNC(sub_82B83FE0) { extern "C" REX_FUNC(sub_82B83FE0) {
if (skate3::native_render::Enabled()) { if (skate3::native_render::Enabled()) {
skate3::native_scene::OnVsConstantUpload(base, ctx.r4.u64, ctx.r5.u32, ctx.r6.u32); skate3::native_scene::OnVsConstantUpload(base, ctx.r4.u64, ctx.r5.u32, ctx.r6.u32,
ctx.r3.u32);
} }
__imp__sub_82B83FE0(ctx, base); __imp__sub_82B83FE0(ctx, base);
} }
// D3DDevice_SetIndices(device, ib) / D3DDevice_SetStreamSource(device,
// stream, vb, offset, stride): track the currently bound guest buffers so
// draws can be matched back to captured skinned items.
extern "C" REX_FUNC(sub_82B79190) {
if (skate3::native_render::Enabled()) {
skate3::native_scene::OnSetIndices(ctx.r4.u32);
}
__imp__sub_82B79190(ctx, base);
}
extern "C" REX_FUNC(sub_82B78FF0) {
if (skate3::native_render::Enabled()) {
skate3::native_scene::OnSetStreamSource(ctx.r4.u32, ctx.r5.u32, ctx.r6.u32,
ctx.r7.u32);
}
__imp__sub_82B78FF0(ctx, base);
}
// D3DDevice_DrawIndexedVertices: post-call, the draw's VS constants are now
// in the shadow bank; refresh any pending skinned item bound to these
// buffers (deferred multi-pass meshes only draw here).
extern "C" REX_FUNC(sub_82B7AD68) {
const bool enabled = skate3::native_render::Enabled();
const uint32_t r4 = ctx.r4.u32;
const uint32_t r5 = ctx.r5.u32;
const uint32_t r6 = ctx.r6.u32;
const uint32_t r7 = ctx.r7.u32;
__imp__sub_82B7AD68(ctx, base);
if (enabled) {
skate3::native_scene::OnDrawDone(base, 0, r4, r5, r6, r7);
}
}
// D3DDevice_DrawVertices, non-indexed draw path: cloth-simulated garments
// (captured live as world-space quad items) and character shadow proxies.
extern "C" REX_FUNC(sub_82B7A970) {
const bool enabled = skate3::native_render::Enabled();
const uint32_t r4 = ctx.r4.u32;
const uint32_t r5 = ctx.r5.u32;
const uint32_t r6 = ctx.r6.u32;
const uint32_t r7 = ctx.r7.u32;
__imp__sub_82B7A970(ctx, base);
if (enabled) {
skate3::native_scene::OnDrawDone(base, 1, r4, r5, r6, r7);
skate3::native_render::OnClothDraw(base, r4, r5, r6, r7);
}
}
// Fourth SetPending_AluConstants caller (symbol mislabeled as
// D3DQuery_GetData: it stages draw constants, so it is a draw variant;
// never observed firing in gameplay). Hooked so the draw-sequence counter
// and recording stay complete if it ever does.
extern "C" REX_FUNC(sub_82B7A458) {
const bool enabled = skate3::native_render::Enabled();
const uint32_t r4 = ctx.r4.u32;
const uint32_t r5 = ctx.r5.u32;
const uint32_t r6 = ctx.r6.u32;
const uint32_t r7 = ctx.r7.u32;
__imp__sub_82B7A458(ctx, base);
if (enabled) {
skate3::native_scene::OnDrawDone(base, 3, r4, r5, r6, r7);
}
}
// D3DDevice_BeginVertices: inline (write-through-ring) vertex path; the
// CPU writes computed vertices directly. Post-call r3 = guest write pointer.
extern "C" REX_FUNC(sub_82B79FC0) {
const bool enabled = skate3::native_render::Enabled();
const uint32_t r4 = ctx.r4.u32;
const uint32_t r5 = ctx.r5.u32;
const uint32_t r6 = ctx.r6.u32;
const uint32_t r7 = ctx.r7.u32;
__imp__sub_82B79FC0(ctx, base);
if (enabled) {
skate3::native_scene::OnDrawDone(base, 2, r4, r5, r6, ctx.r3.u32 != 0 ? ctx.r3.u32 : r7);
}
}

File diff suppressed because it is too large Load diff

View file

@ -21,6 +21,8 @@ struct DrawEntry {
struct DrawItem { struct DrawItem {
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 ib_obj; // guest renderengine::IndexBuffer object (draw matching)
uint32_t vb_addr; // guest address of raw vertex data uint32_t vb_addr; // guest address of raw vertex data
uint32_t ib_addr; // guest address of raw index data (u16 big-endian) uint32_t ib_addr; // guest address of raw index data (u16 big-endian)
uint32_t vb_bytes; uint32_t vb_bytes;
@ -32,13 +34,34 @@ struct DrawItem {
uint16_t uv2_offset; uint16_t uv2_offset;
uint16_t bw_offset; // blend weights (u8x4), valid when skinned uint16_t bw_offset; // blend weights (u8x4), valid when skinned
uint16_t bi_offset; // blend indices (u8x4), valid when skinned uint16_t bi_offset; // blend indices (u8x4), valid when skinned
uint16_t normal_offset; // usage-3 vertex normal (0 fmt = none)
uint8_t stride; uint8_t stride;
uint8_t pos_fmt; // xenos vertex format (26 s16x4, 32 half4, 57 float3) uint8_t pos_fmt; // xenos vertex format (26 s16x4, 32 half4, 57 float3)
uint8_t uv_fmt; // xenos vertex format of the first texcoord (0 = none) uint8_t uv_fmt; // xenos vertex format of the first texcoord (0 = none)
uint8_t uv2_fmt; // xenos vertex format of the second texcoord (0 = none) uint8_t uv2_fmt; // xenos vertex format of the second texcoord (0 = none)
uint8_t normal_fmt; // 16 = k_10_11_11 packed (0 = none, face-normal fallback)
// sky.* materials draw fullbright: per-facet lighting turns the multi-km
// sky dome into visibly shaded rectangular panels.
bool unlit;
bool skinned; bool skinned;
// Bone palette snapshot (row-vector 4x4 matrices) taken on the game thread // Grayscale-tinted material (CAS hair): the shader multiplies the diffuse
// - the guest bone array lives in a transient per-frame arena. // by a per-character color staged in the PIXEL constant bank. Detected via
// the AttribulatorMaterialName channel; tint captured with the palette.
bool hair;
float tint[4]; // rgb + enable flag in w
// The item's per-draw state (bone palette for skinned, world matrix for
// rigid model-space props) was not available at capture time; deferred
// meshes draw AFTER the submit call, and world sort-list captures happen
// before any draw. Cleared by the post-draw (ib,vb) fixup; items still
// pending at frame end are dropped (wrong state renders garbage).
bool pending;
// Cloth patch (non-indexed quad list, CPU-simulated absolute world-space
// float3 verts): the renderer synthesizes quad->triangle indices instead
// of reading a guest index buffer.
bool cloth_quads;
// Bone palette snapshot taken on the game thread: raw staged rows, 3
// float4s per bone (column-vector affine [R | t], model -> world). The
// guest staging bank is reused draw to draw, hence the copy.
std::vector<float> bones; std::vector<float> bones;
// Content fingerprint of the guest payloads. Streaming reuses arena // Content fingerprint of the guest payloads. Streaming reuses arena
// addresses and pages fill in after the mesh is first submitted, so cached // addresses and pages fill in after the mesh is first submitted, so cached
@ -71,16 +94,58 @@ bool Enabled();
// renderengine::Texture object. // renderengine::Texture object.
void OnRegisterTexture(uint64_t guid, uint32_t texture); void OnRegisterTexture(uint64_t guid, uint32_t texture);
// Called from the D3D::SetPending_AluConstants hook (guest render thread). // Called from the D3D::SetPending_AluConstants hook (guest render thread,
// Large vertex-bank uploads are bone palettes staged for the next skinned // invoked from inside the Draw* functions). Records the location of the
// RenderMesh; the staging bank is reused, so contents are snapshotted here. // device's positional 256-register VS constant shadow bank.
void OnVsConstantUpload(uint8_t* base, uint64_t mask, uint32_t bank, uint32_t ptr); void OnVsConstantUpload(uint8_t* base, uint64_t mask, uint32_t bank, uint32_t ptr,
uint32_t device);
// Called from the RenderMesh hook for dynamic entities. Snapshots the // Guest buffer-binding trackers + post-draw state fixup: each
// context's instance matrix (ctx+0x10 points into a transient per-frame // DrawIndexedVertices whose bound IB/VB match a pending item fills that
// arena; it must be read now, not at frame end) and the pending bone // item's bone palette / world matrix from the constant bank (FIFO one-shot
// palette. Returns a dynamic-state index (+1), 0 on failure. // per buffer key; clones share mesh assets and draw in submit order).
uint32_t CaptureDynamicState(uint8_t* base, uint32_t ctx); void OnSetIndices(uint32_t ib_obj);
void OnSetStreamSource(uint32_t stream, uint32_t vb_obj, uint32_t offset, uint32_t stride);
// func: 0 = DrawIndexedVertices (also runs the pending fixup),
// 1 = DrawVertices, 2 = BeginVertices (inline/cloth vertex path).
void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t r6,
uint32_t r7);
// Offline-analysis recording, armed alongside the guest-memory snapshot:
// captures every draw's bound buffers + full VS constant bank, plus the
// per-frame captured items and final scene, every `stride` frames. Written
// as <stem>.scene.jsonl and <stem>.draws.bin next to the .gsnap.
void StartRecording(uint32_t stride);
void WriteRecording(const char* dir, const char* stem);
// Called post-call from the DrawVertices hook for cloth patch draws
// (r6 == 0x80000000 signature): reads the bound dynamic buffer's vertex
// fetch block and builds a world-space quad-list item. Returns a
// dynamic-state index (+1), 0 when the draw is not a cloth patch;
// *out_key receives the garment's stable identity (the buffer object).
uint32_t CaptureClothDraw(uint8_t* base, uint32_t r4, uint32_t r5, uint32_t r6,
uint32_t r7, uint32_t* out_key);
// Total guest draws completed (any draw function). The RenderMesh hook
// samples this around the original call to detect deferred meshes: if no
// draw ran inside, the constant bank still belongs to an earlier mesh and
// the item's transform/palette must come from the post-draw fixup.
uint64_t DrawSequence();
// Called from the RenderMesh hook (dynamic entities) and the world per-mesh
// draw hook (sub_827FAEA8), AFTER the original call returns (the mesh's
// constants are only flushed to the shadow bank by its own draws). Builds
// the complete DrawItem: geometry from the transient per-frame arena plus
// world matrix / bone palette read live from the shadow bank. drew_inside
// says whether any guest draw ran during the original call; when false the
// bank is stale and the transform is left pending for the post-draw fixup.
// With world_path, bails out early for meshes that need no per-draw
// transform (absolute fmt-57 geometry, handled at frame end) and captures
// only skinned meshes and rigid model-space props (vending machines and
// other movables reach the frame ONLY through the sort lists). Returns a
// dynamic-state index (+1), 0 on failure/skip.
uint32_t CaptureDynamicState(uint8_t* base, uint32_t ctx, bool world_path = false,
bool drew_inside = false);
// Called on the guest render thread at frame end with the frame's records. // Called on the guest render thread at frame end with the frame's records.
void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count); void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count);

@ -1 +1 @@
Subproject commit c4b0dfe2c9162340d348d1e86d7b0e1aed353101 Subproject commit c09b8c892d6038236def81e245dce6aef53aab19