skate3recomp-multiplayer/src/skate3_native_scene.cpp

3973 lines
163 KiB
C++

#include "skate3_native_scene.h"
#include "generated/skate3_init.h"
#include <array>
#include <atomic>
#include <algorithm>
#include <bit>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <memory>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
#if defined(_WIN32)
#include <windows.h>
#endif
#include <rex/cvar.h>
#include <rex/graphics/native_guest_renderer.h>
#include <rex/graphics/ultrawide_debug.h>
#include <rex/logging.h>
#if defined(REX_HAS_D3D12) && REX_HAS_D3D12
#include <rex/graphics/d3d12/command_processor.h>
#include <rex/graphics/d3d12/deferred_command_list.h>
#include <rex/graphics/pipeline/texture/info.h>
#include <rex/graphics/pipeline/texture/util.h>
#include <rex/graphics/xenos.h>
#include <d3dcompiler.h>
#if defined(_WIN32)
#include <windows.h>
#endif
#endif
REXCVAR_DEFINE_BOOL(skate3_native_render_scene, false, "Skate 3",
"Render the game scene natively from the hooked MeshContext stream, "
"replacing the emulated GPU output (requires skate3_native_render). "
"Hot-toggles live between the native and emulated renderers (F5).")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_lightmaps, false, "Skate 3",
"Sample guest lightmap textures in the native scene renderer. Off by "
"default: Skate 3 lightpages are composed at runtime on the GPU, so "
"their CPU-side payloads decode black.")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_INT32(skate3_native_render_scene_debug, 0, "Skate 3",
"Native scene debug: 0=normal, 1=clear only, 2=solid color per item, "
"3=limit to 20 items, 4=depth test disabled")
.range(0, 4)
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_INT32(skate3_native_render_scene_msaa, 4, "Skate 3",
"MSAA sample count for the native scene (1 = off, 2/4/8). Distant "
"thin geometry (railings, wires) shimmers without it; mipmaps only "
"fix texture aliasing.")
.range(1, 8)
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_2d, true, "Skate 3",
"Replay the game's 2D/APT (Flash HUD) draws as a native overlay pass "
"on top of the 3D scene")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_splines, true, "Skate 3",
"Replay the game's in-world neon spline draws (waypoint arrows, "
"marker beams; spline_darken/spline_default shaders) inside the "
"native scene pass")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_quadlists, false, "Skate 3",
"Render captured non-indexed quad-list draws. Off by default: every "
"quad-list capture seen so far is a PARTICLE system (disjoint 2-4cm "
"sprites), which renders as floating white squares without the game's "
"sprite textures and blending.")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_INT32(skate3_native_render_scene_mesh_decode_budget, 8, "Skate 3",
"Max new mesh decodes per rendered frame (0 = unlimited). Each decode "
"converts vertices and creates GPU upload resources on the render "
"thread; panning streams dozens of new meshes into view at once and "
"unbounded decoding hitches the frame. Deferred meshes pop in a few "
"frames later.")
.range(0, 100000)
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_INT32(skate3_native_render_scene_texture_decode_budget, 4, "Skate 3",
"Max new texture decodes per rendered frame (0 = unlimited). CPU "
"untile + mip upload is expensive; deferred textures render white "
"for the few frames until their turn.")
.range(0, 100000)
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_snapshot_all_draws, false, "Skate 3",
"Record the draw stream on every recorded frame instead of 2 of every "
"60 (large .draws.bin; for targeted investigations)")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
// Hook layer master switch, defined in skate3_native_render.cpp. The runtime
// toggle refuses to flip the scene on without it: the hooks that feed the
// scene are only installed when it was set at boot.
REXCVAR_DECLARE(bool, skate3_native_render);
namespace skate3::native_scene {
namespace {
// Verified guest structure offsets.
constexpr uint32_t kCtxEffectList = 0x04;
constexpr uint32_t kEffectListPassCount = 0x44;
constexpr uint32_t kCtxDrawCountU16 = 0x38;
constexpr uint32_t kCtxDrawList = 0x48;
constexpr uint32_t kMeshMaterial = 0x24;
constexpr uint32_t kMeshVertexDescriptor = 0x28;
constexpr uint32_t kMeshIndexBuffer = 0x30;
constexpr uint32_t kMeshVertexBuffer = 0x34;
constexpr uint32_t kBufferPhysAddr = 0x18;
constexpr uint32_t kVbBytes = 0x20;
constexpr uint32_t kIbCount = 0x20;
constexpr uint32_t kViewCameraFromView = 0x08;
constexpr uint32_t kViewCamViewProj = 0xA0;
std::mutex g_scene_mutex;
FrameScene g_scene;
uint64_t g_generation = 0;
std::atomic<uint8_t*> g_guest_base{nullptr};
std::atomic<uint64_t> g_frames_rendered{0};
// guid -> guest renderengine::Texture, from the RegisterTexture hook. Keys
// masked of the top bit: material channel guids carry an extra flag bit
// relative to the registered cAssetIDs.
constexpr uint64_t kGuidMask = 0x7FFFFFFFFFFFFFFFull;
std::mutex g_texture_map_mutex;
std::unordered_map<uint64_t, uint32_t> g_texture_map;
// VS constant staging bank (bank id 0x4000, device+1920).
// D3D::SetPending_AluConstants(device, dirty group mask, bank, ptr) is
// called from inside the guest Draw* functions; ptr is the device's
// positional 256-register shadow (partial uploads leave the rest intact).
// Two verified layouts: pre-pass keeps the per-draw transform / bone
// palette at c4; main-pass keeps camera at c4, palette at c7, rigid world
// at c8..c11 (see BankPaletteBase / BankRigidWorld). Because the dynamic
// capture runs right after RenderMesh returns, the bank holds exactly that
// mesh's constants when its draws ran inline.
std::atomic<uint32_t> g_vs_bank{0};
// Pixel constant shadow bank (bank id 0x4400, device+6016): material colors
// (e.g. the CAS hair tint) are staged here per draw.
std::atomic<uint32_t> g_ps_bank{0};
// Dynamic entities (characters, movable props) live entirely in transient
// per-frame arenas: the context, its record, mesh pointers, transforms and
// bone palettes are all recycled before frame end. The complete DrawItem is
// therefore built at RenderMesh hook time and stored here.
std::mutex g_palette_mutex;
std::vector<DrawItem> g_frame_dynitems;
// (ib_obj << 32 | vb_obj) -> indices of pending items in g_frame_dynitems,
// for the post-draw state fixup (deferred / world-path captures draw after
// their capture; the fixup fills palette or world from the actual draw).
std::unordered_multimap<uint64_t, size_t> g_frame_pending_by_buffers;
std::atomic<uint32_t> g_cur_ib{0};
std::atomic<uint32_t> g_cur_vb{0};
std::atomic<uint32_t> g_cur_vb_offset{0};
std::atomic<uint32_t> g_cur_vb_stride{0};
// Guest D3D::CDevice: the fetch-constant shadow lives at device+0x480,
// 6 dwords per fetch group (from recompiled SetPending_FetchConstants).
std::atomic<uint32_t> g_device{0};
// 2D/APT phase depth counters (see On2dPhase). All HUD/menu 2D elements are
// Flash SWFs converted to APT; draws issued inside these brackets are the 2D
// draw stream.
std::atomic<uint32_t> g_phase2d_depth[6];
std::atomic<uint64_t> g_draws_2d{0};
// 2D draws seen through paths the overlay does not replay yet
// (DrawIndexedVertices / DrawVertices in the 2D phase; gameplay HUD uses
// only the BeginVertices inline path, verified by capture).
std::atomic<uint64_t> g_draws_2d_other{0};
std::atomic<uint64_t> g_draws_2d_dropped{0};
// One captured 2D draw (verified layout): BeginVertices
// inline vertices, stride 24 = {float x, y, z, w; float u, v} in 1280x720
// APT movie space; VS c0..c3 = ortho projection rows, c4..c7 = the
// element's 2D transform, c8 = color multiplier; diffuse texture in fetch
// shadow slot 0. Vertex payload is written by the CPU after BeginVertices
// returns, so it is read at frame end (addr) rather than at capture.
// NOTE on render-to-texture: in gameplay the ENTIRE HUD renders inside
// AptRenderingIntegration::UpdateRenderToTexture (bracket bit 3) into a
// screen-sized overlay texture, at true screen coordinates; the game
// composites that overlay over the 3D frame in a later (emulated,
// suppressed) pass. Replaying the draws directly onto the native output IS
// that composite, so bit 3 is diagnostic only, not a routing signal. The
// overlay content is straight-alpha art (verified by decoding the clock
// face/needle/sheen textures offline).
struct Draw2d {
uint32_t prim; // xenos primitive: 4 trilist, 5 tristrip, 13 quadlist
uint32_t count; // vertex count
uint32_t stride; // bytes per vertex at capture (see publish normalization)
uint32_t addr; // guest inline-ring write pointer
uint32_t flags; // bracket bits at capture (layout disambiguation)
uint32_t fetch[6]; // texture fetch constant (shadow slot 0)
float consts[36]; // VS c0..c8
std::vector<uint8_t> verts; // filled at frame end (little-endian dwords)
};
std::mutex g_2d_mutex;
std::vector<Draw2d> g_frame_2d; // capture in submission order
std::vector<Draw2d> g_scene_2d; // published at frame end
uint64_t g_scene_2d_generation = 0;
// One captured in-world neon spline draw (waypoint arrows / marker beams;
// decoded from a live capture + the game's own spline.fx source): a
// DrawVertices triangle strip whose 12-byte float3 "vertices" are only
// parameters: x = control-point index + fractional t, y = U texcoord,
// z = side flag (0/1) selecting the extrusion offset and V texcoord. The
// guest VS (43309A8C) evaluates a uniform cubic B-spline through i_cp[]
// (VS c7..) transformed by world columns c4..c6, adds the world-rotated
// extrusion offset c[151 + z], projects by VP columns c0..c3, and fades by
// clip-z against i_clipvalues (c150) with i_intensity (c149) as the pass
// gain. Two passes per element: spline_darkenPS (straight alpha) then
// spline_defaultPS (additive glow), both depth-tested with no z-write,
// replayed inside the native scene pass with the spline evaluated on the
// CPU at publish time.
struct SplineDraw {
uint32_t pass; // 1 = darken (straight alpha), 2 = default (additive)
uint32_t count; // strip vertex count
uint32_t fetch[6]; // texture fetch slot 3 (the neon gradient)
float consts[153 * 4]; // VS c0..c152 as staged
// capture: raw big-endian float3 params; publish: evaluated little-endian
// {float4 clip_pos, float2 uv, float fade} (28-byte stride).
std::vector<uint8_t> verts;
};
std::vector<SplineDraw> g_frame_spline; // guarded by g_2d_mutex
std::vector<SplineDraw> g_scene_spline;
std::atomic<uint64_t> g_draws_spline{0};
// Current guest shader objects and render-state shadow bank, for the 2D
// recon recording (grouping the stream by shader / reading blend state).
std::atomic<uint32_t> g_cur_ps_obj{0};
std::atomic<uint32_t> g_cur_vs_obj{0};
std::atomic<uint32_t> g_rs_bank{0};
std::atomic<uint32_t> g_cur_viewport[6];
std::atomic<uint32_t> g_cur_scissor[4];
// All four vertex streams (vb, offset, stride); cloth binds its simulated
// vertices on a stream other than 0.
std::atomic<uint32_t> g_cur_streams[4][3];
// Offline-analysis recording (see StartRecording/WriteRecording): the full
// per-draw constant bank stream is the ground truth for what the game's
// shaders saw; the per-frame item lists are what our pipeline made of it.
struct RecordedDraw {
uint32_t frame;
uint32_t func; // 0 = DrawIndexedVertices, 1 = DrawVertices, 2 = BeginVertices
uint32_t ib;
uint32_t vb;
uint32_t vb_offset; // stream-0 OffsetInBytes at bind time
uint32_t vb_stride; // stream-0 stride at bind time
uint32_t streams[4][3]; // all streams: {vb, offset, stride}
uint32_t vfetch[12]; // fetch-constant shadow groups 0-1 (device+0x480)
uint32_t args[4];
float bank[1024]; // VS c0..c255
float ps[256]; // PS c0..c63 (material colors, e.g. CAS hair tint)
// 2D recon fields (SK3DRAW7):
uint32_t flags2d; // bit per active 2D bracket (see On2dPhase)
uint32_t ps_obj; // current guest pixel/vertex shader objects
uint32_t vs_obj;
uint32_t viewport[6]; // last SetViewport payload (raw guest dwords)
uint32_t scissor[4]; // last SetScissorRect payload
uint32_t rstates[256]; // render-state shadow snapshot (blend/depth)
uint32_t vfetch_all[192]; // full 32-slot texture fetch shadow
uint32_t vb_dump; // index into buffers.bin for this draw's payloads
uint32_t ib_dump; // (~0u = none)
};
struct RecordedFrame {
uint64_t generation;
float view_proj[16];
float cam_pos[3];
std::vector<DrawItem> dynitems;
std::vector<DrawItem> items;
};
// Dynamic meshes live in streaming arenas that are recycled DURING a long
// recording; the end-of-window .gsnap holds garbage for them. Dump each
// captured buffer payload once per content fingerprint so offline tools
// decode exactly what the game used.
struct RecordedBuffer {
uint32_t vb_addr;
uint32_t ib_addr;
uint64_t fingerprint;
std::vector<uint8_t> vb;
std::vector<uint8_t> ib;
};
std::mutex g_record_mutex;
std::atomic<bool> g_recording{false};
uint32_t g_record_frame = 0; // index of the NEXT recorded frame
uint32_t g_record_stride = 1; // record every Nth frame
uint32_t g_frames_seen = 0; // frames completed since StartRecording
std::vector<std::unique_ptr<RecordedDraw>> g_recorded_draws;
std::vector<RecordedFrame> g_recorded_frames;
std::vector<RecordedBuffer> g_recorded_buffers;
std::unordered_set<uint64_t> g_recorded_buffer_keys;
size_t g_recorded_buffer_bytes = 0;
// BeginVertices (inline ring) payloads are written by the CPU AFTER the
// call returns; dump them at frame end, when the writes are complete but
// the ring has not yet wrapped. draw_index links the dump back to its
// RecordedDraw.
struct PendingInlineDump {
uint32_t addr;
uint32_t bytes;
size_t draw_index;
};
std::vector<PendingInlineDump> g_pending_inline_dumps;
// One payload dump per (guest address, frame): 2D dynamic buffers hold many
// draws' vertices; the repeated full-buffer dump would be pure duplication.
std::unordered_map<uint64_t, uint32_t> g_frame_dump_ids;
std::atomic<uint64_t> g_vs_uploads{0};
std::atomic<uint64_t> g_palette_snapshots{0};
// Palettes captured at base+1 (the cloth/morph VS layout with an extra
// parameter row before the palette, see RefinePaletteBase).
std::atomic<uint64_t> g_palette_base_plus1{0};
std::atomic<uint64_t> g_skinned_items{0};
std::atomic<uint64_t> g_skinned_skipped{0};
// Rigid items whose transform had to wait for the post-draw fixup (deferred
// draws), and those still pending at frame end (dropped).
std::atomic<uint64_t> g_rigid_pending{0};
std::atomic<uint64_t> g_rigid_dropped{0};
// Model-space props in the world sort lists, excluded from the identity
// world path (their placed copies come from the kind-2 capture).
std::atomic<uint64_t> g_world_props{0};
std::atomic<uint64_t> g_rej_no_dynstate{0};
std::atomic<uint64_t> g_rej_dyn_range{0};
// Render-side silent-skip diagnostics: a scene item that decodes badly or
// loses its bone binding produces no (or wrong) pixels with no other trace.
std::atomic<uint64_t> g_rr_decode_fail{0};
std::atomic<uint64_t> g_rr_no_bones{0};
// Decodes pushed past the per-frame budget (item/texture appears a few
// frames late instead of hitching the render thread).
std::atomic<uint64_t> g_rr_mesh_deferred{0};
std::atomic<uint64_t> g_rr_tex_deferred{0};
std::atomic<uint64_t> g_rej_chain{0};
std::atomic<uint64_t> g_rej_geom{0};
std::atomic<uint64_t> g_rej_draws{0};
std::atomic<uint64_t> g_rej_bbox{0};
bool SceneEnabled() { return REXCVAR_GET(skate3_native_render_scene); }
float LoadGuestF32(uint8_t* base, uint32_t addr) {
const uint32_t bits = REX_LOAD_U32(addr);
return std::bit_cast<float>(bits);
}
uint32_t BSwap32(uint32_t v) {
return (v >> 24) | ((v >> 8) & 0xFF00u) | ((v << 8) & 0xFF0000u) | (v << 24);
}
// Draws completed since startup (any guest draw function). The RenderMesh
// hook samples it around the original call: an unchanged sequence means the
// mesh's draws were deferred by the dispatcher and the constant bank still
// belongs to some earlier mesh; its transform must come from the post-draw
// fixup, never from the bank at submit-exit (a leftover identity matrix at
// c4 validates as a plausible world and renders the prop at the origin).
std::atomic<uint64_t> g_draw_seq{0};
// Where does this bank keep its bone palette? Pre-pass layout: c4 (bone 0's
// affine rows right after viewproj). Main-pass layout: camera position at
// c4, two parameter rows, palette at c7. A camera-position row is easily
// told from a bone rotation row by its norm (hundreds vs ~1). Returns the
// base register, or 0 when neither location holds plausible bone rows.
uint32_t BankPaletteBase(uint8_t* base, uint32_t bank) {
const auto bone_at = [&](uint32_t reg) -> bool {
for (int r = 0; r < 3; ++r) {
float f[4];
for (int i = 0; i < 4; ++i) {
f[i] = LoadGuestF32(base, bank + ((reg + r) * 4 + i) * 4);
if (!(f[i] > -1e7f && f[i] < 1e7f)) return false;
}
const float n = f[0] * f[0] + f[1] * f[1] + f[2] * f[2];
if (!(n > 0.0025f && n < 400.0f)) return false;
if (!(f[3] > -20000.f && f[3] < 20000.f)) return false;
}
return true;
};
if (bone_at(4)) return 4;
if (bone_at(7)) return 7;
return 0;
}
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));
}
// The cloth/morph skinned VS variant (skating-NPC torsos) keeps ONE extra
// parameter row between the viewproj and the bone palette: observed live,
// c4 = (1,0,0,0) with the palette at c5 (pre-pass) / c8 (main-pass). A
// palette read one register early hands every bone [neighbor row, row0,
// row1]; each row is still a perfectly plausible bone row, so the norm
// checks in BankPaletteBase cannot catch it, and the mesh skins to
// component-rotated coordinates ~300 m off in the sky. That was the
// invisible-NPC-torso bug: the torso was captured, non-pending, palette and
// texture resolved, and rendered far outside the view.
//
// Discriminate by skinning a few sample vertices with both candidate bases
// and projecting them with the pass's own viewproj (bank c0..c3,
// column-vector rows): the game drew this mesh with these constants, so
// only the correct base puts the samples inside the clip volume (validated
// offline across every skinned draw of an F10 capture: correct base 1.00,
// wrong base <= ~0.3). Only +1 is tested: a +3 shift (whole-bone
// misalignment) also projects fine and no such layout has been seen.
uint32_t RefinePaletteBase(uint8_t* base, uint32_t bank, uint32_t palette_base,
const DrawItem& item) {
if (item.bw_offset == 0 || item.bi_offset == 0 || item.stride == 0) {
return palette_base;
}
const uint32_t count = item.vb_bytes / item.stride;
if (count < 2) {
return palette_base;
}
float vp[16];
for (int i = 0; i < 16; ++i) {
vp[i] = LoadGuestF32(base, bank + i * 4);
if (!(vp[i] > -1e9f && vp[i] < 1e9f)) return palette_base;
}
constexpr uint32_t kSamples = 6;
const auto score = [&](uint32_t pb) -> int {
int ok = 0;
int n = 0;
for (uint32_t s = 0; s < kSamples; ++s) {
const uint32_t v = item.vb_addr + (s * (count - 1) / (kSamples - 1)) * item.stride;
float p[3];
const uint32_t pa = v + item.pos_offset;
switch (item.pos_fmt) {
case 57:
for (int a = 0; a < 3; ++a) p[a] = LoadGuestF32(base, pa + a * 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 -1;
}
// 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);
uint32_t total = 0;
float q[3] = {0.0f, 0.0f, 0.0f};
for (int k = 0; k < 4; ++k) {
const uint32_t w = (bw >> (24 - 8 * k)) & 0xFF;
if (w == 0) continue;
const uint32_t bone = (bi >> (24 - 8 * k)) & 0xFF;
const uint32_t r0 = pb + 3 * bone;
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] = LoadGuestF32(base, bank + ((r0 + a) * 4 + i) * 4);
}
q[a] += float(w) * (row[0] * p[0] + row[1] * p[1] + row[2] * p[2] + row[3]);
}
}
if (total == 0) continue;
for (int a = 0; a < 3; ++a) q[a] /= float(total);
float clip[4];
for (int r = 0; r < 4; ++r) {
clip[r] = vp[r * 4] * q[0] + vp[r * 4 + 1] * q[1] + vp[r * 4 + 2] * q[2] +
vp[r * 4 + 3];
}
const float aw = std::abs(clip[3]) < 1.0f ? 1.0f : std::abs(clip[3]);
++n;
if (std::abs(clip[0]) <= 1.5f * aw && std::abs(clip[1]) <= 1.5f * aw) {
++ok;
}
}
return n == 0 ? -1 : (ok * 16) / n;
};
const int s_std = score(palette_base);
const int s_plus = score(palette_base + 1);
if (s_plus > s_std) {
g_palette_base_plus1.fetch_add(1, std::memory_order_relaxed);
return palette_base + 1;
}
return palette_base;
}
// Rigid transform validation: the game uses (at least) two VS constant
// layouts, verified from recorded draw streams: the pre-pass layout has a
// row-vector 4x4 world at c4..c7 (rotation rows end in 0, translation in
// c7); the main-pass layout has the camera position at c4 and the world
// 4x4 at c8..c11. Older meshes use a column-vector affine 4x3 at c4..c6.
// Sanity-check rows so a camera-position or parameter block is never
// mistaken for a matrix.
bool TryRow4x4(uint8_t* base, uint32_t bank, uint32_t reg, float* out) {
float f[16];
for (int i = 0; i < 16; ++i) {
f[i] = LoadGuestF32(base, bank + (reg * 4 + i) * 4);
if (!(f[i] > -1e7f && f[i] < 1e7f)) return false;
}
if (f[3] != 0.0f || f[7] != 0.0f || f[11] != 0.0f || f[15] != 1.0f) return false;
for (int r = 0; r < 3; ++r) {
const float n = f[r * 4] * f[r * 4] + f[r * 4 + 1] * f[r * 4 + 1] +
f[r * 4 + 2] * f[r * 4 + 2];
if (!(n > 0.0025f && n < 400.0f)) return false;
}
if (!(f[12] > -20000.f && f[12] < 20000.f && f[13] > -20000.f && f[13] < 20000.f &&
f[14] > -20000.f && f[14] < 20000.f)) {
return false;
}
std::memcpy(out, f, 16 * sizeof(float));
return true;
}
bool TryColAffine(uint8_t* base, uint32_t bank, uint32_t reg, float* out) {
float f[12];
for (int i = 0; i < 12; ++i) {
f[i] = LoadGuestF32(base, bank + (reg * 4 + i) * 4);
if (!(f[i] > -1e7f && f[i] < 1e7f)) return false;
}
for (int r = 0; r < 3; ++r) {
const float n = f[r * 4] * f[r * 4] + f[r * 4 + 1] * f[r * 4 + 1] +
f[r * 4 + 2] * f[r * 4 + 2];
if (!(n > 0.0025f && n < 400.0f)) return false;
if (!(f[r * 4 + 3] > -20000.f && f[r * 4 + 3] < 20000.f)) return false;
}
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
out[i * 4 + j] = f[j * 4 + i];
}
out[i * 4 + 3] = 0.0f;
}
out[12] = f[3];
out[13] = f[7];
out[14] = f[11];
out[15] = 1.0f;
return true;
}
// Fill a rigid item's world matrix from a constant bank, whichever layout
// the staging draw used.
bool BankRigidWorld(uint8_t* base, uint32_t bank, float* out) {
return TryRow4x4(base, bank, 4, out) || TryRow4x4(base, bank, 8, out) ||
TryColAffine(base, bank, 4, out);
}
bool GuestReadableApprox(uint8_t* base, uint32_t addr) {
// The hook layer only walks pointers the game is actively rendering from;
// they are mapped. Reject null/small.
(void)base;
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)
bool GuestTryCopy(void* dst, const void* src, size_t size) {
__try {
std::memcpy(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
// Committed-page check for bulk reads (transient ring memory can be
// partially uncommitted, and resources may be released between the game
// thread capturing an address and the render thread reading it; a blind
// memcpy would fault).
bool GuestRangeReadable(uint8_t* base, uint32_t addr, uint32_t size) {
#if defined(_WIN32)
uint8_t* p = base + addr;
uint8_t* end = p + size;
while (p < end) {
MEMORY_BASIC_INFORMATION info{};
if (VirtualQuery(p, &info, sizeof(info)) == 0 || info.State != MEM_COMMIT ||
(info.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0) {
return false;
}
p = static_cast<uint8_t*>(info.BaseAddress) + info.RegionSize;
}
return true;
#else
(void)base;
(void)addr;
(void)size;
return false;
#endif
}
// Walk one MeshContext into a DrawItem (geometry, material and draw list;
// world left as identity). Returns false if any pointer in the chain is
// implausible. For dynamic entities this MUST run at RenderMesh hook time;
// the whole chain lives in transient per-frame arenas.
// Parse a guest mesh struct (vertex descriptor, buffers, bbox, material
// channels, payload fingerprint) into the item. The draw list and world
// transform are the caller's responsibility (world starts as identity).
bool BuildItemFromMesh(uint8_t* base, uint32_t mesh, DrawItem& item) {
const uint32_t vdesc = REX_LOAD_U32(mesh + kMeshVertexDescriptor);
const uint32_t ib = REX_LOAD_U32(mesh + kMeshIndexBuffer);
const uint32_t vb = REX_LOAD_U32(mesh + kMeshVertexBuffer);
if (!GuestReadableApprox(base, vdesc) || !GuestReadableApprox(base, ib) ||
!GuestReadableApprox(base, vb)) {
g_rej_chain.fetch_add(1, std::memory_order_relaxed);
return false;
}
// Vertex descriptor: find the stream-0 position element and the first
// stream-0 texcoord (D3DDECLUSAGE 5) for the diffuse map.
const uint32_t num_elements = REX_LOAD_U16(vdesc + 8);
if (num_elements == 0 || num_elements > 32) return false;
bool have_pos = false;
bool have_bw = false;
bool have_bi = false;
item.uv_offset = 0;
item.uv_fmt = 0;
item.uv2_offset = 0;
item.uv2_fmt = 0;
item.bw_offset = 0;
item.bi_offset = 0;
item.normal_offset = 0;
item.normal_fmt = 0;
item.skinned = false;
for (uint32_t i = 0; i < num_elements; ++i) {
const uint32_t e = vdesc + 0x10 + i * 16;
const uint32_t stream = REX_LOAD_U16(e);
const uint32_t usage = REX_LOAD_U8(e + 9);
if (stream != 0) continue;
if (usage == 0 && !have_pos) {
item.pos_offset = REX_LOAD_U16(e + 2);
item.pos_fmt = uint8_t(REX_LOAD_U32(e + 4) & 0x3F);
have_pos = true;
} else if (usage == 3 && item.normal_fmt == 0) {
const uint8_t fmt = uint8_t(REX_LOAD_U32(e + 4) & 0x3F);
if (fmt == 16) { // k_10_11_11 packed normal
item.normal_offset = REX_LOAD_U16(e + 2);
item.normal_fmt = fmt;
}
} else if (usage == 5 && item.uv_fmt == 0) {
item.uv_offset = REX_LOAD_U16(e + 2);
item.uv_fmt = uint8_t(REX_LOAD_U32(e + 4) & 0x3F);
} else if (usage == 5 && item.uv2_fmt == 0) {
item.uv2_offset = REX_LOAD_U16(e + 2);
item.uv2_fmt = uint8_t(REX_LOAD_U32(e + 4) & 0x3F);
} else if (usage == 1 && !have_bw) { // blend weights u8x4
item.bw_offset = REX_LOAD_U16(e + 2);
have_bw = (REX_LOAD_U32(e + 4) & 0x3F) == 6;
} else if (usage == 2 && !have_bi) { // blend indices u8x4
item.bi_offset = REX_LOAD_U16(e + 2);
have_bi = (REX_LOAD_U32(e + 4) & 0x3F) == 6;
}
}
if (!have_pos) {
g_rej_geom.fetch_add(1, std::memory_order_relaxed);
return false;
}
item.skinned = have_bw && have_bi;
item.stride = REX_LOAD_U8(vdesc + (num_elements + 1) * 16);
if (item.stride == 0) {
g_rej_geom.fetch_add(1, std::memory_order_relaxed);
return false;
}
// Mesh BBox = two Vector4s at +0x00/+0x10. Reject NaN/absurd bounds.
for (int axis = 0; axis < 3; ++axis) {
const float lo = LoadGuestF32(base, mesh + axis * 4);
const float hi = LoadGuestF32(base, mesh + 0x10 + axis * 4);
if (!(hi - lo < 50000.0f)) {
g_rej_bbox.fetch_add(1, std::memory_order_relaxed);
return false;
}
item.bbox_min[axis] = lo;
item.bbox_max[axis] = hi;
}
item.mesh = mesh;
item.pending = false;
item.cloth_quads = false;
item.vb_obj = vb;
item.ib_obj = ib;
item.vb_addr = REX_LOAD_U32(vb + kBufferPhysAddr) & 0xFFFFFFFC;
item.vb_bytes = REX_LOAD_U32(vb + kVbBytes);
item.ib_addr = REX_LOAD_U32(ib + kBufferPhysAddr) & 0xFFFFFFFC;
item.ib_count = REX_LOAD_U32(ib + kIbCount);
if (item.vb_addr == 0 || item.ib_addr == 0 || item.vb_bytes == 0 ||
item.ib_count == 0 || item.vb_bytes % item.stride != 0) {
g_rej_geom.fetch_add(1, std::memory_order_relaxed);
return false;
}
// Material channels: resolve the "diffuse" and "lightmap" texture guids to
// registered texture objects.
item.diffuse_tex = 0;
item.lightmap_tex = 0;
item.hair = false;
item.unlit = false;
item.tint[0] = item.tint[1] = item.tint[2] = item.tint[3] = 0.0f;
const uint32_t material = REX_LOAD_U32(mesh + kMeshMaterial);
if (GuestReadableApprox(base, material)) {
const uint32_t num_channels = REX_LOAD_U32(material);
const uint32_t channels = REX_LOAD_U32(material + 8);
if (num_channels <= 32 && GuestReadableApprox(base, channels)) {
for (uint32_t i = 0; i < num_channels; ++i) {
const uint32_t chan = channels + i * 0x20;
const uint32_t name = REX_LOAD_U32(chan);
if (!GuestReadableApprox(base, name)) continue;
char text[10] = {};
for (int k = 0; k < 9; ++k) {
text[k] = char(REX_LOAD_U8(name + k));
if (text[k] == '\0') break;
}
uint32_t* slot = nullptr;
if (std::memcmp(text, "diffuse", 8) == 0) {
slot = &item.diffuse_tex;
} else if (std::memcmp(text, "lightmap", 9) == 0) {
slot = &item.lightmap_tex;
} else if (std::memcmp(text, "Attribul", 8) == 0) {
// AttribulatorMaterialName: chan+0x18 is the material name string.
// "character.hair" marks the grayscale hair that needs the
// per-character tint from the pixel constant bank; "sky.*" draws
// fullbright.
const uint32_t s = REX_LOAD_U32(chan + 0x18);
if (GuestReadableApprox(base, s)) {
char mat_name[16] = {};
for (int k = 0; k < 15; ++k) {
mat_name[k] = char(REX_LOAD_U8(s + k));
if (mat_name[k] == '\0') break;
}
item.hair = std::memcmp(mat_name, "character.hair", 15) == 0;
item.unlit = std::memcmp(mat_name, "sky.", 4) == 0;
}
continue;
}
if (slot != nullptr && *slot == 0) {
// Prefer the channel's live stream record (chan+0x1C -> word 0 =
// the renderengine::Texture actually bound): runtime-composed
// customization textures (CAS face/skin, shoes, deck, wheels)
// are never registered under an asset GUID. Validate via the
// fetch-constant type bits before trusting the pointer.
const uint32_t stream = REX_LOAD_U32(chan + 0x1C);
if (GuestReadableApprox(base, stream)) {
const uint32_t tex = REX_LOAD_U32(stream);
if (GuestReadableApprox(base, tex) &&
(REX_LOAD_U32(tex + 7 * 4) & 3u) == 2u) {
*slot = tex;
}
}
if (*slot == 0) {
const uint64_t guid =
(uint64_t(REX_LOAD_U32(chan + 0x10)) << 32) | REX_LOAD_U32(chan + 0x14);
std::lock_guard<std::mutex> lock(g_texture_map_mutex);
auto it = g_texture_map.find(guid & kGuidMask);
if (it != g_texture_map.end()) {
*slot = it->second;
}
}
}
if (item.diffuse_tex != 0 && item.lightmap_tex != 0) {
break;
}
}
}
}
// Payload fingerprint (FNV-1a over bytes sampled across the whole VB/IB)
// so the renderer re-decodes when streaming replaces or fills in the data
// at this address, including middle-of-buffer fills.
uint64_t h = 1469598103934665603ull;
const auto mix = [&h](uint64_t v) {
h = (h ^ v) * 1099511628211ull;
};
mix(item.vb_addr);
mix(item.ib_addr);
mix(item.vb_bytes);
mix(item.ib_count);
if (item.vb_bytes >= 8 && item.ib_count >= 4) {
for (uint32_t k = 0; k < 16; ++k) {
const uint32_t vb_off = uint32_t(uint64_t(item.vb_bytes - 8) * k / 15u) & ~7u;
mix(REX_LOAD_U64(item.vb_addr + vb_off));
const uint32_t ib_off = uint32_t(uint64_t(item.ib_count * 2 - 8) * k / 15u) & ~7u;
mix(REX_LOAD_U64(item.ib_addr + ib_off));
}
}
item.fingerprint = h;
// Transform (identity for world geometry, instance matrix for props;
// characters get their bone array's first matrix, which roughly places
// them until skinning is implemented).
std::memset(item.world, 0, sizeof(item.world));
item.world[0] = item.world[5] = item.world[10] = item.world[15] = 1.0f;
return true;
}
bool BuildItemGeometry(uint8_t* base, uint32_t ctx, DrawItem& item) {
const uint32_t record = REX_LOAD_U32(ctx);
if (!GuestReadableApprox(base, record)) {
g_rej_chain.fetch_add(1, std::memory_order_relaxed);
return false;
}
const uint32_t mesh = REX_LOAD_U32(record);
if (!GuestReadableApprox(base, mesh)) {
g_rej_chain.fetch_add(1, std::memory_order_relaxed);
return false;
}
if (!BuildItemFromMesh(base, mesh, item)) {
return false;
}
// Culled island draw list from the context.
const uint32_t draw_count = REX_LOAD_U16(ctx + kCtxDrawCountU16);
const uint32_t draw_list = REX_LOAD_U32(ctx + kCtxDrawList);
if (draw_count == 0 || draw_count > 512 || !GuestReadableApprox(base, draw_list)) {
g_rej_draws.fetch_add(1, std::memory_order_relaxed);
return false;
}
item.draws.reserve(draw_count);
for (uint32_t i = 0; i < draw_count; ++i) {
const uint32_t d = draw_list + i * 16;
DrawEntry entry{REX_LOAD_U32(d), REX_LOAD_U32(d + 4), REX_LOAD_U32(d + 8),
REX_LOAD_U32(d + 12)};
if (entry.index_count == 0 || entry.index_count > item.ib_count) continue;
item.draws.push_back(entry);
}
if (item.draws.empty()) {
g_rej_draws.fetch_add(1, std::memory_order_relaxed);
return false;
}
return true;
}
} // namespace
bool Enabled() { return SceneEnabled(); }
bool ToggleSceneEnabled() {
if (!REXCVAR_GET(skate3_native_render)) {
REXLOG_WARN(
"native-scene: renderer toggle ignored; the skate3_native_render hook layer "
"is off (set it and restart; it installs the capture hooks the scene needs)");
return false;
}
const bool enabled = !SceneEnabled();
if (enabled) {
// Capture idles while the emulated renderer is active, so anything still
// published is from before the switch away (stale camera). Drop it:
// RenderScene yields to the emulated frame until the capture hooks
// publish a fresh scene (the next frame).
{
std::lock_guard<std::mutex> lock(g_scene_mutex);
g_scene = FrameScene{};
}
{
std::lock_guard<std::mutex> lock(g_2d_mutex);
g_scene_2d.clear();
g_scene_spline.clear();
}
}
REXCVAR_SET(skate3_native_render_scene, enabled);
REXLOG_INFO("native-scene: switched to the {} renderer (runtime toggle)",
enabled ? "NATIVE" : "EMULATED");
return enabled;
}
void OnRegisterTexture(uint64_t guid, uint32_t texture) {
if (texture == 0) {
return;
}
std::lock_guard<std::mutex> lock(g_texture_map_mutex);
g_texture_map[guid & kGuidMask] = texture;
}
void OnVsConstantUpload(uint8_t* base, uint64_t mask, uint32_t bank, uint32_t ptr,
uint32_t device) {
(void)base;
(void)mask;
if (device != 0) {
g_device.store(device, std::memory_order_relaxed);
}
if (!SceneEnabled() || ptr == 0) {
return;
}
if (bank == 0x4400) {
g_ps_bank.store(ptr, std::memory_order_relaxed);
return;
}
if (bank != 0x4000) {
return;
}
g_vs_uploads.fetch_add(1, std::memory_order_relaxed);
g_vs_bank.store(ptr, std::memory_order_relaxed);
}
void On2dPhase(uint32_t bit, bool enter) {
if (bit >= 6) {
return;
}
if (enter) {
g_phase2d_depth[bit].fetch_add(1, std::memory_order_relaxed);
} else {
g_phase2d_depth[bit].fetch_sub(1, std::memory_order_relaxed);
}
}
uint32_t Phase2dFlags() {
uint32_t flags = 0;
for (uint32_t bit = 0; bit < 6; ++bit) {
if (g_phase2d_depth[bit].load(std::memory_order_relaxed) != 0) {
flags |= 1u << bit;
}
}
return flags;
}
void OnSetShader(bool pixel, uint32_t obj) {
(pixel ? g_cur_ps_obj : g_cur_vs_obj).store(obj, std::memory_order_relaxed);
}
void OnRenderStateUpload(uint64_t mask, uint32_t bank, uint32_t ptr) {
(void)mask;
if (ptr == 0) {
return;
}
// Log each distinct bank id once, the AluConstants analog of discovering
// 0x4000/0x4400. Tiny lock-free seen-set (at most a handful of banks).
static std::atomic<uint32_t> seen[8];
for (auto& slot : seen) {
uint32_t cur = slot.load(std::memory_order_relaxed);
if (cur == bank) {
break;
}
if (cur == 0) {
uint32_t expected = 0;
if (slot.compare_exchange_strong(expected, bank, std::memory_order_relaxed)) {
REXLOG_INFO("native-scene: render-state bank id={:#x} ptr={:08X}", bank, ptr);
break;
}
if (expected == bank) {
break;
}
}
}
g_rs_bank.store(ptr, std::memory_order_relaxed);
}
void OnSetViewport(uint8_t* base, uint32_t viewport_ptr) {
if (viewport_ptr == 0) {
return;
}
for (int i = 0; i < 6; ++i) {
g_cur_viewport[i].store(REX_LOAD_U32(viewport_ptr + i * 4), std::memory_order_relaxed);
}
}
void OnSetScissor(uint8_t* base, uint32_t rect_ptr) {
if (rect_ptr == 0) {
return;
}
for (int i = 0; i < 4; ++i) {
g_cur_scissor[i].store(REX_LOAD_U32(rect_ptr + i * 4), std::memory_order_relaxed);
}
}
// The character.hair pixel shader keeps the per-character hair color at PS
// c17 (fixed layout for that shader; verified from recorded PS banks;
// c16/c17 hold the dark ambient/diffuse hair color pair while other
// registers carry lighting globals). Used as captured.
void CaptureHairTint(uint8_t* base, DrawItem& item) {
const uint32_t ps = g_ps_bank.load(std::memory_order_relaxed);
if (ps == 0) {
return;
}
float rgb[3];
for (int i = 0; i < 3; ++i) {
rgb[i] = LoadGuestF32(base, ps + (17 * 4 + i) * 4);
if (!(rgb[i] >= 0.0f && rgb[i] <= 4.0f)) {
return; // implausible bank contents; keep the previous/no tint
}
}
for (int i = 0; i < 3; ++i) {
item.tint[i] = rgb[i];
}
item.tint[3] = 1.0f;
}
// Copy the staged bone palette (and, for hair, the tint) out of the shadow
// banks into the item. The banks are reused draw to draw, hence the copy.
// The base from BankPaletteBase is refined by +1 for the cloth/morph VS
// layout (extra parameter row before the palette); the mesh's own sample
// vertices projected with the bank's viewproj decide (RefinePaletteBase).
void CaptureSkinnedState(uint8_t* base, uint32_t bank, uint32_t palette_base,
DrawItem& item) {
palette_base = RefinePaletteBase(base, bank, palette_base, item);
constexpr uint32_t kPaletteFloats = 84 * 12; // c4..c255 = up to 84 bones
item.bones.resize(kPaletteFloats);
for (uint32_t i = 0; i < kPaletteFloats; ++i) {
const float f = LoadGuestF32(base, bank + (palette_base * 4 + i) * 4);
item.bones[i] = (f > -1e7f && f < 1e7f) ? f : 0.0f;
}
if (item.hair) {
CaptureHairTint(base, item);
}
}
uint64_t DrawSequence() { return g_draw_seq.load(std::memory_order_relaxed); }
uint32_t CaptureDynamicState(uint8_t* base, uint32_t ctx, bool world_path,
bool drew_inside) {
if (!SceneEnabled()) {
return 0;
}
const uint32_t bank = g_vs_bank.load(std::memory_order_relaxed);
if (bank == 0) {
return 0;
}
if (world_path) {
// Cheap pre-check so the world path (~800 calls/frame) only pays the
// full capture for the meshes that need per-draw transforms: skinned
// (LOD pedestrians, jointed props) and rigid MODEL-SPACE props (position
// fmt 32/26). Movable props (vending machines) can reach the frame ONLY
// through the sort lists, rendered as absolute world geometry they
// collapse at the origin. Absolute meshes (fmt 57) stay on the identity
// world path.
const uint32_t record = REX_LOAD_U32(ctx);
if (!GuestReadableApprox(base, record)) return 0;
const uint32_t mesh = REX_LOAD_U32(record);
if (!GuestReadableApprox(base, mesh)) return 0;
const uint32_t vdesc = REX_LOAD_U32(mesh + kMeshVertexDescriptor);
if (!GuestReadableApprox(base, vdesc)) return 0;
const uint32_t num_elements = REX_LOAD_U16(vdesc + 8);
if (num_elements == 0 || num_elements > 32) return 0;
bool have_bw = false;
bool have_bi = false;
uint32_t pos_fmt = 0;
bool have_pos = false;
for (uint32_t i = 0; i < num_elements; ++i) {
const uint32_t e = vdesc + 0x10 + i * 16;
if (REX_LOAD_U16(e) != 0) continue;
const uint32_t usage = REX_LOAD_U8(e + 9);
if (usage == 0 && !have_pos) {
pos_fmt = REX_LOAD_U32(e + 4) & 0x3F;
have_pos = true;
}
if (usage == 1 && (REX_LOAD_U32(e + 4) & 0x3F) == 6) have_bw = true;
if (usage == 2 && (REX_LOAD_U32(e + 4) & 0x3F) == 6) have_bi = true;
}
const bool skinned = have_bw && have_bi;
const bool model_space = pos_fmt == 32 || pos_fmt == 26;
if (!skinned && !model_space) {
return 0;
}
}
DrawItem item;
if (!BuildItemGeometry(base, ctx, item)) {
return 0;
}
// Rigid transform: the bank only holds this mesh's constants if its own
// draws ran inside the submit call. Deferred (multi-pass) rigid props draw
// later; the bank belongs to some earlier mesh, and a leftover identity
// matrix at c4 VALIDATES as a plausible world (verified from recorded
// draw streams: 4 of 6 vending-machine clones captured exact identity and
// rendered invisibly at the origin). Defer those to the post-draw fixup,
// like skinned palettes.
if (!item.skinned) {
if (!drew_inside || !BankRigidWorld(base, bank, item.world)) {
item.pending = true;
g_rigid_pending.fetch_add(1, std::memory_order_relaxed);
}
}
if (item.skinned) {
// Copy the whole possible palette span (c4..c255 = 84 bones) verbatim:
// 3 float4 rows per bone, column-vector affine [R | t], model space
// directly to world space (verified live: bone translations match the
// camera focus). The shader applies rows with explicit dot products,
// sidestepping HLSL matrix packing. Registers beyond the mesh's real
// bone count hold stale bank data but are never indexed. World stays
// identity.
//
// The bank only holds this mesh's palette at c4 if its own draws ran
// inside the call AND used the pre-pass layout. Multi-pass (deferred)
// meshes draw later, and main-pass-layout banks have the camera at c4;
// in both cases leave the palette pending for the post-draw fixup.
const uint32_t effect_list = REX_LOAD_U32(ctx + kCtxEffectList);
uint32_t passes = 1;
if (GuestReadableApprox(base, effect_list)) {
passes = REX_LOAD_U32(effect_list + kEffectListPassCount);
}
const uint32_t palette_base = BankPaletteBase(base, bank);
// World-path captures come from the sort-list hook BEFORE any of the
// mesh's draws ran; the bank belongs to some other mesh; always defer
// to the post-draw fixup. Same when no draw ran inside the submit call
// (deferred mesh): a stale bank can still hold plausible bone rows.
item.pending = world_path || !drew_inside || (passes > 1 && passes < 16) ||
palette_base == 0;
if (!item.pending) {
CaptureSkinnedState(base, bank, palette_base, item);
g_palette_snapshots.fetch_add(1, std::memory_order_relaxed);
}
g_skinned_items.fetch_add(1, std::memory_order_relaxed);
}
if (g_recording.load(std::memory_order_relaxed)) {
// Preserve the buffer payload for offline decode (streaming arenas are
// recycled long before the end-of-window memory snapshot).
std::lock_guard<std::mutex> lock(g_record_mutex);
const uint64_t key = uint64_t(item.vb_addr) * 1099511628211ull ^ item.fingerprint;
if (item.vb_bytes <= (1u << 20) && g_recorded_buffer_bytes < (512u << 20) &&
g_recorded_buffer_keys.insert(key).second) {
RecordedBuffer buf;
buf.vb_addr = item.vb_addr;
buf.ib_addr = item.ib_addr;
buf.fingerprint = item.fingerprint;
buf.vb.resize(item.vb_bytes);
std::memcpy(buf.vb.data(), base + item.vb_addr, item.vb_bytes);
buf.ib.resize(size_t(item.ib_count) * 2);
std::memcpy(buf.ib.data(), base + item.ib_addr, buf.ib.size());
g_recorded_buffer_bytes += buf.vb.size() + buf.ib.size();
g_recorded_buffers.push_back(std::move(buf));
}
}
std::lock_guard<std::mutex> lock(g_palette_mutex);
g_frame_dynitems.push_back(std::move(item));
const size_t index = g_frame_dynitems.size() - 1;
if (g_frame_dynitems[index].pending) {
const DrawItem& d = g_frame_dynitems[index];
g_frame_pending_by_buffers.emplace((uint64_t(d.ib_obj) << 32) | d.vb_obj, index);
}
return uint32_t(index + 1);
}
uint32_t CaptureClothDraw(uint8_t* base, uint32_t r4, uint32_t r5, uint32_t r6,
uint32_t r7, uint32_t* out_key) {
(void)r4;
(void)r7;
if (!SceneEnabled() || r6 != 0x80000000u ||
!REXCVAR_GET(skate3_native_render_scene_quadlists)) {
return 0;
}
// Quad-list draw: stream 0 is a dynamic ping-pong object whose vertex
// fetch block (+0x18 dword0 = base|flags, +0x20 = size in bytes) points at
// the CPU-simulated vertices for this frame: stride 24 = {float3 world
// position (BE), packed normal, float2 uv}, quad-list topology (verified
// offline from recorded payloads). NOTE: every capture examined so far is
// a PARTICLE system (disjoint 2-4cm sprites), which is why rendering is
// gated off by default; see the cvar.
const uint32_t vb_obj = g_cur_vb.load(std::memory_order_relaxed);
if (!GuestReadableApprox(base, vb_obj)) {
return 0;
}
const uint32_t addr = REX_LOAD_U32(vb_obj + 0x18) & 0xFFFFFFFC;
const uint32_t size = REX_LOAD_U32(vb_obj + 0x20);
constexpr uint32_t kStride = 24;
if (addr < 0x10000 || size < kStride * 4 || size > (1u << 20) || size % kStride != 0) {
return 0;
}
// The garment's live vertices start at the buffer head; r5 is the live
// vertex count (verified offline: the zero-fill run starts exactly at r5
// for every garment). Ring slack past it must not be drawn.
uint32_t verts = size / kStride;
if (r5 >= 4 && r5 <= verts) {
verts = r5;
}
const uint32_t quads = verts / 4;
if (quads == 0) {
return 0;
}
const uint32_t start = 0;
const uint32_t garment_key = vb_obj ^ (start * 2654435761u);
DrawItem item{};
item.mesh = garment_key;
item.vb_obj = vb_obj;
item.ib_obj = 0;
item.vb_addr = addr;
item.vb_bytes = verts * kStride;
item.ib_addr = 0;
item.ib_count = quads * 6;
item.diffuse_tex = 0;
item.lightmap_tex = 0;
item.pos_offset = 0;
item.pos_fmt = 57; // float3
item.uv_offset = 16;
item.uv_fmt = 38; // float2
item.uv2_offset = 0;
item.uv2_fmt = 0;
item.bw_offset = 0;
item.bi_offset = 0;
item.stride = kStride;
item.skinned = false;
item.pending = false;
item.cloth_quads = true;
std::memset(item.world, 0, sizeof(item.world));
item.world[0] = item.world[5] = item.world[10] = item.world[15] = 1.0f;
for (int axis = 0; axis < 3; ++axis) {
item.bbox_min[axis] = -20000.0f;
item.bbox_max[axis] = 20000.0f;
}
item.draws.push_back({4, 0, 0, quads * 6});
// Content fingerprint (simulation output changes every frame -> the
// renderer re-decodes each frame).
uint64_t h = 1469598103934665603ull;
const auto mix = [&h](uint64_t v) { h = (h ^ v) * 1099511628211ull; };
mix(addr);
mix(item.vb_bytes);
for (uint32_t k = 0; k < 16; ++k) {
const uint32_t off = uint32_t(uint64_t(item.vb_bytes - 8) * k / 15u) & ~7u;
mix(REX_LOAD_U64(addr + off));
}
item.fingerprint = h;
// Distinct draw ranges within one ring buffer are distinct garments.
*out_key = garment_key;
std::lock_guard<std::mutex> lock(g_palette_mutex);
g_frame_dynitems.push_back(std::move(item));
return uint32_t(g_frame_dynitems.size());
}
namespace {
// Guest shader objects carry their compiled-source debug path at +0x54
// ("D:\P4\xbox2-ww-f\...\spline_darkenPS.updb"); the spline renderer's pixel
// shaders identify the in-world neon guide elements. Cached per object;
// the current pixel/vertex object trackers are checked both ways because the
// hook labels are swapped relative to the real shader types.
uint32_t ClassifySplineShader(uint8_t* base, uint32_t obj) {
if (obj < 0x10000) {
return 0;
}
static std::mutex mu;
static std::unordered_map<uint32_t, uint32_t> cache;
std::lock_guard<std::mutex> lock(mu);
auto it = cache.find(obj);
if (it != cache.end()) {
return it->second;
}
char path[97] = {};
uint32_t kind = 0;
if (GuestTryCopy(path, base + obj + 0x54, 96)) {
path[96] = '\0';
if (std::strstr(path, "spline_darken") != nullptr) {
kind = 1;
} else if (std::strstr(path, "spline_default") != nullptr) {
kind = 2;
}
}
cache.emplace(obj, kind);
return kind;
}
} // namespace
void OnSetIndices(uint32_t ib_obj) { g_cur_ib.store(ib_obj, std::memory_order_relaxed); }
void OnSetStreamSource(uint32_t stream, uint32_t vb_obj, uint32_t offset, uint32_t stride) {
if (stream == 0) {
g_cur_vb.store(vb_obj, std::memory_order_relaxed);
g_cur_vb_offset.store(offset, std::memory_order_relaxed);
g_cur_vb_stride.store(stride, std::memory_order_relaxed);
}
if (stream < 4) {
g_cur_streams[stream][0].store(vb_obj, std::memory_order_relaxed);
g_cur_streams[stream][1].store(offset, std::memory_order_relaxed);
g_cur_streams[stream][2].store(stride, std::memory_order_relaxed);
}
}
void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t r6,
uint32_t r7) {
g_draw_seq.fetch_add(1, std::memory_order_relaxed);
const uint32_t flags2d = Phase2dFlags();
if (flags2d != 0) {
g_draws_2d.fetch_add(1, std::memory_order_relaxed);
}
const uint32_t bank = g_vs_bank.load(std::memory_order_relaxed);
if (bank == 0) {
return;
}
// Live 2D overlay capture: the HUD renders exclusively through the
// BeginVertices inline path (func 2). Vertex payloads are read at frame
// end; everything else (transform constants, texture fetch) is staged now.
if (flags2d != 0 && SceneEnabled() && REXCVAR_GET(skate3_native_render_scene_2d)) {
if (func == 2) {
const uint32_t device = g_device.load(std::memory_order_relaxed);
if (device != 0 && r7 >= 0x10000 && r5 != 0 && r5 <= 65536 && r6 >= 8 &&
r6 <= 256 && (r6 & 3) == 0) {
Draw2d d;
d.prim = r4;
d.count = r5;
d.stride = r6;
d.addr = r7;
d.flags = flags2d;
for (int i = 0; i < 6; ++i) {
d.fetch[i] = REX_LOAD_U32(device + 0x480 + i * 4);
}
for (int i = 0; i < 36; ++i) {
d.consts[i] = LoadGuestF32(base, bank + i * 4);
}
std::lock_guard<std::mutex> lock(g_2d_mutex);
if (g_frame_2d.size() < 4096) {
g_frame_2d.push_back(std::move(d));
} else {
g_draws_2d_dropped.fetch_add(1, std::memory_order_relaxed);
}
}
} else {
g_draws_2d_other.fetch_add(1, std::memory_order_relaxed);
}
}
// In-world neon spline capture (see SplineDraw): a DrawVertices strip of
// 12-byte float3 params on the spline shaders, bound VB object carrying
// the payload fetch block (+0x18 base | flags, +0x20 size).
if (func == 1 && flags2d == 0 && SceneEnabled() &&
REXCVAR_GET(skate3_native_render_scene_splines) && r5 >= 4 && r5 <= 8192) {
uint32_t kind = ClassifySplineShader(base, g_cur_ps_obj.load(std::memory_order_relaxed));
if (kind == 0) {
kind = ClassifySplineShader(base, g_cur_vs_obj.load(std::memory_order_relaxed));
}
const uint32_t vb_obj = g_cur_vb.load(std::memory_order_relaxed);
if (kind != 0 && GuestReadableApprox(base, vb_obj)) {
const uint32_t addr = REX_LOAD_U32(vb_obj + 0x18) & 0xFFFFFFFCu;
const uint32_t size = REX_LOAD_U32(vb_obj + 0x20);
if (addr >= 0x10000 && size >= r5 * 12) {
SplineDraw s;
s.pass = kind;
s.count = r5;
s.verts.resize(size_t(r5) * 12);
if (GuestTryCopy(s.verts.data(), base + addr, s.verts.size())) {
const uint32_t device = g_device.load(std::memory_order_relaxed);
for (int i = 0; i < 6; ++i) {
// The gradient texture lives in fetch shadow slot 3 (24 bytes
// per slot; the spline PS samples tf3).
s.fetch[i] = device != 0 ? REX_LOAD_U32(device + 0x480 + 3 * 24 + i * 4) : 0;
}
for (int i = 0; i < 153 * 4; ++i) {
s.consts[i] = LoadGuestF32(base, bank + i * 4);
}
g_draws_spline.fetch_add(1, std::memory_order_relaxed);
std::lock_guard<std::mutex> lock(g_2d_mutex);
if (g_frame_spline.size() < 64) {
g_frame_spline.push_back(std::move(s));
}
}
}
}
}
const uint32_t cur_ib = g_cur_ib.load(std::memory_order_relaxed);
const uint32_t cur_vb = g_cur_vb.load(std::memory_order_relaxed);
if (g_recording.load(std::memory_order_relaxed)) {
// Sample the full draw stream on a couple of recorded frames out of
// every 60, spread across the window (ground-truth coverage for the
// whole recording), and only for frames that are themselves recorded.
// 2D-phase draws are recorded on EVERY recorded frame; the HUD stream
// is small and is exactly what the 2D reconstruction needs.
std::lock_guard<std::mutex> lock(g_record_mutex);
const bool frame_recorded = (g_frames_seen + 1) % g_record_stride == 0;
const bool all_draws = REXCVAR_GET(skate3_native_render_snapshot_all_draws);
const size_t draw_cap = all_draws ? 200000 : 32768;
if (frame_recorded && (all_draws || flags2d != 0 || (g_record_frame % 60) < 2) &&
g_recorded_draws.size() < draw_cap) {
auto rec = std::make_unique<RecordedDraw>();
rec->func = func;
rec->ib = cur_ib;
rec->vb = cur_vb;
rec->vb_offset = g_cur_vb_offset.load(std::memory_order_relaxed);
rec->vb_stride = g_cur_vb_stride.load(std::memory_order_relaxed);
for (int s = 0; s < 4; ++s) {
for (int k = 0; k < 3; ++k) {
rec->streams[s][k] = g_cur_streams[s][k].load(std::memory_order_relaxed);
}
}
const uint32_t device = g_device.load(std::memory_order_relaxed);
for (int i = 0; i < 12; ++i) {
rec->vfetch[i] = device != 0 ? REX_LOAD_U32(device + 0x480 + i * 4) : 0;
}
rec->args[0] = r4;
rec->args[1] = r5;
rec->args[2] = r6;
rec->args[3] = r7;
for (uint32_t i = 0; i < 1024; ++i) {
rec->bank[i] = LoadGuestF32(base, bank + i * 4);
}
const uint32_t ps_bank = g_ps_bank.load(std::memory_order_relaxed);
for (uint32_t i = 0; i < 256; ++i) {
rec->ps[i] = ps_bank != 0 ? LoadGuestF32(base, ps_bank + i * 4) : 0.0f;
}
rec->frame = g_record_frame;
rec->flags2d = flags2d;
rec->ps_obj = g_cur_ps_obj.load(std::memory_order_relaxed);
rec->vs_obj = g_cur_vs_obj.load(std::memory_order_relaxed);
for (int i = 0; i < 6; ++i) {
rec->viewport[i] = g_cur_viewport[i].load(std::memory_order_relaxed);
}
for (int i = 0; i < 4; ++i) {
rec->scissor[i] = g_cur_scissor[i].load(std::memory_order_relaxed);
}
const uint32_t rs_bank = g_rs_bank.load(std::memory_order_relaxed);
for (int i = 0; i < 256; ++i) {
rec->rstates[i] = rs_bank != 0 ? REX_LOAD_U32(rs_bank + i * 4) : 0;
}
for (int i = 0; i < 192; ++i) {
rec->vfetch_all[i] = device != 0 ? REX_LOAD_U32(device + 0x480 + i * 4) : 0;
}
rec->vb_dump = ~0u;
rec->ib_dump = ~0u;
if (flags2d != 0) {
// 2D payload capture. The 2D pass runs on transient dynamic buffers
// (glyph/shape vertices regenerated per frame); dump them now.
// One dump per (guest address, frame); recording mode, so the
// GuestRangeReadable VAD cost is acceptable.
const auto dump_buffer = [&](uint32_t obj, uint32_t size_off) -> uint32_t {
if (obj == 0) {
return ~0u;
}
const uint32_t addr = REX_LOAD_U32(obj + 0x18) & 0xFFFFFFFCu;
if (addr < 0x10000) {
return ~0u;
}
uint32_t bytes = REX_LOAD_U32(obj + size_off);
if (bytes < 4 || bytes > (8u << 20)) {
return ~0u;
}
const uint64_t key = (uint64_t(g_record_frame) << 32) | addr;
auto it = g_frame_dump_ids.find(key);
if (it != g_frame_dump_ids.end()) {
return it->second;
}
if (g_recorded_buffer_bytes + bytes > (512u << 20) ||
!GuestRangeReadable(base, addr, bytes)) {
return ~0u;
}
RecordedBuffer buf;
buf.vb_addr = addr;
buf.ib_addr = 0;
buf.fingerprint = key;
buf.vb.resize(bytes);
std::memcpy(buf.vb.data(), base + addr, bytes);
const uint32_t dump_id = uint32_t(g_recorded_buffers.size());
g_recorded_buffer_bytes += bytes;
g_recorded_buffers.push_back(std::move(buf));
g_frame_dump_ids.emplace(key, dump_id);
return dump_id;
};
if (func == 0) {
rec->vb_dump = dump_buffer(cur_vb, 0x20);
rec->ib_dump = dump_buffer(cur_ib, 0x1C);
} else if (func == 1) {
rec->vb_dump = dump_buffer(cur_vb, 0x20);
} else if (func == 2 && r7 >= 0x10000 && r5 != 0 && r6 != 0 &&
uint64_t(r5) * r6 <= (4u << 20)) {
// Inline-ring vertices (r5 = count, r6 = stride, r7 = the write
// pointer BeginVertices returned): the CPU writes them after the
// call; dump at frame end.
g_pending_inline_dumps.push_back({r7, r5 * r6, g_recorded_draws.size()});
}
}
if (func == 1 && cur_vb != 0 && flags2d == 0) {
// Non-indexed (cloth) draw: the bound object holds two ping-pong
// vertex fetch blocks whose ring payloads are recycled long before
// frame end; dump both now, keyed back to this draw via rec->ib.
const uint32_t dump_id = uint32_t(g_recorded_buffers.size());
for (uint32_t block = 0; block < 2; ++block) {
const uint32_t fetch0 = REX_LOAD_U32(cur_vb + 0x18 + block * 0x30);
const uint32_t addr = fetch0 & 0xFFFFFFFC;
if (addr < 0x10000) continue;
uint32_t bytes = REX_LOAD_U32(cur_vb + 0x20 + block * 0x30);
if (bytes < 64 || bytes > (256u << 10)) bytes = 128u << 10;
if (g_recorded_buffer_bytes + bytes > (512u << 20)) break;
while (bytes >= 4096 && !GuestRangeReadable(base, addr, bytes)) {
bytes /= 2;
}
if (bytes < 4096) continue;
RecordedBuffer buf;
buf.vb_addr = addr;
buf.ib_addr = 0;
buf.fingerprint = (uint64_t(g_record_frame) << 32) | dump_id | (block << 30);
buf.vb.resize(bytes);
std::memcpy(buf.vb.data(), base + addr, bytes);
g_recorded_buffer_bytes += bytes;
g_recorded_buffers.push_back(std::move(buf));
}
rec->ib = 0x80000000u | dump_id;
}
g_recorded_draws.push_back(std::move(rec));
}
}
if (func != 0) {
return; // palette fixup below matches indexed draws only
}
const uint64_t key = (uint64_t(cur_ib) << 32) | cur_vb;
std::lock_guard<std::mutex> lock(g_palette_mutex);
auto range = g_frame_pending_by_buffers.equal_range(key);
if (range.first == range.second) {
return;
}
// This draw's constants belong to the OLDEST pending item with these
// buffers (clones share mesh assets; the deferred list draws in submit
// order, so FIFO one-shot pairing keeps clones' palettes apart). This is
// the only draw that ever stages a deferred mesh's bones/world.
auto oldest = range.first;
for (auto it = range.first; it != range.second; ++it) {
if (it->second < oldest->second) {
oldest = it;
}
}
DrawItem& d = g_frame_dynitems[oldest->second];
if (d.skinned) {
// Locate the palette for this draw's layout; a bank without plausible
// bone rows (parameter blocks, camera rows) must not be consumed; that
// was the source of screen-wide stretched characters.
const uint32_t palette_base = BankPaletteBase(base, bank);
if (palette_base == 0) {
return;
}
CaptureSkinnedState(base, bank, palette_base, d);
} else {
// Deferred rigid prop: the world matrix is wherever this draw's layout
// keeps it (pre-pass c4..c7, main-pass c8..c11). Not plausible -> wait
// for a later draw with these buffers.
if (!BankRigidWorld(base, bank, d.world)) {
return;
}
}
d.pending = false;
g_frame_pending_by_buffers.erase(oldest);
}
void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
if (!SceneEnabled()) {
return;
}
if (g_recording.load(std::memory_order_relaxed)) {
// Flush this frame's deferred inline-ring payloads (2D BeginVertices
// draws): the CPU has finished writing them by frame end, and the ring
// has not yet been reused.
std::lock_guard<std::mutex> lock(g_record_mutex);
for (const PendingInlineDump& p : g_pending_inline_dumps) {
if (p.draw_index >= g_recorded_draws.size() ||
g_recorded_buffer_bytes + p.bytes > (512u << 20) ||
!GuestRangeReadable(base, p.addr, p.bytes)) {
continue;
}
RecordedBuffer buf;
buf.vb_addr = p.addr;
buf.ib_addr = 0;
buf.fingerprint = (uint64_t(g_recorded_draws[p.draw_index]->frame) << 32) | p.addr;
buf.vb.resize(p.bytes);
std::memcpy(buf.vb.data(), base + p.addr, p.bytes);
g_recorded_draws[p.draw_index]->vb_dump = uint32_t(g_recorded_buffers.size());
g_recorded_buffer_bytes += p.bytes;
g_recorded_buffers.push_back(std::move(buf));
}
g_pending_inline_dumps.clear();
}
// Publish this frame's 2D overlay draws (before any early return: menu
// and empty frames still carry 2D). The inline-ring vertex payloads are
// complete by frame end; convert them to little-endian and expand quad
// lists into triangle lists so the render side stays trivial.
{
std::vector<Draw2d> frame_2d;
{
std::lock_guard<std::mutex> lock(g_2d_mutex);
frame_2d.swap(g_frame_2d);
}
static thread_local std::vector<uint8_t> scratch_2d;
std::vector<Draw2d> published;
published.reserve(frame_2d.size());
for (Draw2d& d : frame_2d) {
const uint32_t bytes = d.count * d.stride;
scratch_2d.resize(bytes);
if (!GuestTryCopy(scratch_2d.data(), base + d.addr, bytes)) {
continue;
}
// Guest dwords are big-endian.
for (size_t i = 0; i + 4 <= scratch_2d.size(); i += 4) {
uint32_t v;
std::memcpy(&v, scratch_2d.data() + i, 4);
v = BSwap32(v);
std::memcpy(scratch_2d.data() + i, &v, 4);
}
// Verified capture-side vertex layouts, all normalized to one 28-byte
// renderer layout {float4 pos, float2 uv, u32 rgba}:
// 24-byte {float4 pos, float2 uv} - APT elements
// 20-byte {float3 pos, float2 uv} - glyph text (bit 4)
// 20-byte {float4 pos, u32 color} - SimpleDraw untextured
// 28-byte {float4 pos, u32 color, float2 uv} - SimpleDraw textured
// (SimpleDraw = bit 5; its DrawParameters ctor orders colours before
// texcoords.)
{
static thread_local std::vector<uint8_t> norm_2d;
norm_2d.resize(size_t(d.count) * 28);
const float one = 1.0f;
const float zero = 0.0f;
const uint32_t white = 0xFFFFFFFFu;
const bool font = (d.flags & 0x10u) != 0;
const bool simple = (d.flags & 0x20u) != 0;
bool ok = true;
for (uint32_t v = 0; v < d.count && ok; ++v) {
uint8_t* dst = norm_2d.data() + size_t(v) * 28;
const uint8_t* src = scratch_2d.data() + size_t(v) * d.stride;
if (d.stride == 24) { // APT: pos4 + uv (with or without the
// SimpleDraw bracket; the compass needle/icons are 24-byte
// quads issued through SimpleDraw::Draw inside the HUD pass,
// same simpledraw_SimpleDrawUVSC shader as plain APT elements;
// requiring !simple here dropped them, a regression from adding
// bracket bit 5)
std::memcpy(dst, src, 24);
std::memcpy(dst + 24, &white, 4);
} else if (d.stride == 20 && font) { // glyphs: pos3 + uv
std::memcpy(dst, src, 12);
std::memcpy(dst + 12, &one, 4);
std::memcpy(dst + 16, src + 12, 8);
std::memcpy(dst + 24, &white, 4);
} else if (d.stride == 20 && simple) { // SimpleDraw: pos4 + color
std::memcpy(dst, src, 16);
std::memcpy(dst + 16, &zero, 4);
std::memcpy(dst + 20, &zero, 4);
// The dword byteswap reversed the color's guest byte order
// (r,g,b,a); restore it for R8G8B8A8_UNORM.
dst[24] = src[19];
dst[25] = src[18];
dst[26] = src[17];
dst[27] = src[16];
} else if (d.stride == 28 && simple) { // SimpleDraw: pos4+color+uv
std::memcpy(dst, src, 16);
std::memcpy(dst + 16, src + 20, 8);
dst[24] = src[19];
dst[25] = src[18];
dst[26] = src[17];
dst[27] = src[16];
} else {
ok = false;
}
}
if (!ok) {
g_draws_2d_other.fetch_add(1, std::memory_order_relaxed);
continue;
}
scratch_2d = norm_2d;
d.stride = 28;
}
if (d.prim == 13 && d.count % 4 == 0) {
// Quad list -> triangle list (v0,v1,v2)(v0,v2,v3).
const uint32_t quads = d.count / 4;
d.verts.resize(size_t(quads) * 6 * d.stride);
static constexpr uint32_t kOrder[6] = {0, 1, 2, 0, 2, 3};
for (uint32_t q = 0; q < quads; ++q) {
for (uint32_t t = 0; t < 6; ++t) {
std::memcpy(d.verts.data() + (size_t(q) * 6 + t) * d.stride,
scratch_2d.data() + (size_t(q) * 4 + kOrder[t]) * d.stride,
d.stride);
}
}
d.prim = 4;
d.count = quads * 6;
} else if (d.prim == 4 || d.prim == 5) {
d.verts.assign(scratch_2d.begin(), scratch_2d.end());
} else {
continue;
}
published.push_back(std::move(d));
}
std::lock_guard<std::mutex> lock(g_2d_mutex);
g_scene_2d = std::move(published);
++g_scene_2d_generation;
}
// Publish this frame's in-world spline draws: evaluate the guest B-spline
// VS on the CPU (see SplineDraw for the decoded algorithm) into final
// clip-space strip vertices so the render side is a passthrough draw.
{
std::vector<SplineDraw> frame_spline;
{
std::lock_guard<std::mutex> lock(g_2d_mutex);
frame_spline.swap(g_frame_spline);
}
std::vector<SplineDraw> published;
published.reserve(frame_spline.size());
for (SplineDraw& s : frame_spline) {
const float* c = s.consts;
const auto row = [&](int r) { return c + r * 4; };
std::vector<uint8_t> out(size_t(s.count) * 28);
float* dst = reinterpret_cast<float*>(out.data());
const uint8_t* src = s.verts.data();
bool ok = true;
for (uint32_t v = 0; v < s.count; ++v, src += 12, dst += 7) {
float p[3];
for (int k = 0; k < 3; ++k) {
uint32_t w;
std::memcpy(&w, src + k * 4, 4);
w = BSwap32(w);
std::memcpy(&p[k], &w, 4);
}
if (!(p[0] >= 0.0f && p[0] < 142.0f)) {
ok = false;
break;
}
const int idx = int(p[0]);
const float t = p[0] - float(idx);
const int side = p[2] >= 0.5f ? 1 : 0;
// Uniform cubic B-spline basis (matches the shader's embedded
// coefficients exactly).
const float t2 = t * t;
const float t3 = t2 * t;
const float wgt[4] = {1.0f - 3.0f * t + 3.0f * t2 - t3,
3.0f * t3 - 6.0f * t2 + 4.0f,
-3.0f * t3 + 3.0f * t2 + 3.0f * t + 1.0f, t3};
// Blend the world-transformed control points (world columns c4..c6,
// translation in .w), /6, plus the world-rotated extrusion offset.
float wp[3] = {0.0f, 0.0f, 0.0f};
for (int k = 0; k < 4; ++k) {
const float* cp = row(7 + idx + k);
for (int a = 0; a < 3; ++a) {
const float* wr = row(4 + a);
wp[a] += wgt[k] *
(wr[0] * cp[0] + wr[1] * cp[1] + wr[2] * cp[2] + wr[3] * cp[3]);
}
}
const float* off = row(151 + side);
for (int a = 0; a < 3; ++a) {
const float* wr = row(4 + a);
wp[a] = wp[a] * (1.0f / 6.0f) +
(wr[0] * off[0] + wr[1] * off[1] + wr[2] * off[2] + wr[3] * off[3]);
}
float clip[4];
for (int a = 0; a < 4; ++a) {
const float* pr = row(a);
clip[a] = pr[0] * wp[0] + pr[1] * wp[1] + pr[2] * wp[2] + pr[3];
}
// Near/far fade against i_clipvalues (clip-space z, pre-divide). A
// zero range degenerates to a step, like the shader's rcp(0) = inf.
const float* cv = row(150);
const auto ramp = [](float z, float start, float range) {
if (range > 1e-20f || range < -1e-20f) {
const float r = (z - start) / range;
return r < 0.0f ? 0.0f : (r > 1.0f ? 1.0f : r);
}
return z - start > 0.0f ? 1.0f : 0.0f;
};
const float fade =
std::min(ramp(clip[2], cv[0], cv[1]), 1.0f - ramp(clip[2], cv[2], cv[3]));
dst[0] = clip[0];
dst[1] = clip[1];
dst[2] = clip[2];
dst[3] = clip[3];
dst[4] = p[1];
dst[5] = p[2];
dst[6] = fade;
}
if (!ok) {
continue;
}
s.verts.swap(out);
published.push_back(std::move(s));
}
std::lock_guard<std::mutex> lock(g_2d_mutex);
g_scene_spline = std::move(published);
}
// Take this frame's hook-time dynamic items regardless of how we exit,
// leaving them in place across an early return (no perspective view, empty
// frame) would desynchronize the indices stored in the records.
std::vector<DrawItem> dynitems;
{
std::lock_guard<std::mutex> lock(g_palette_mutex);
dynitems.swap(g_frame_dynitems);
g_frame_pending_by_buffers.clear();
}
if (count == 0) {
return;
}
g_guest_base.store(base, std::memory_order_relaxed);
// Multiple SceneRenderViews can submit per frame (main, shadow cascades,
// reflections). Pick the perspective one (proj[2][3] == 1 in row-vector
// convention) and only take its items, deduplicated (the same context can
// appear in several of the view's sort lists).
uint32_t view = 0;
uint32_t viewcam = 0;
for (size_t i = 0; i < count; ++i) {
const SubmitRecord& r = records[i];
if (r.kind != 1 || r.c == 0) {
continue;
}
const uint32_t cam = REX_LOAD_U32(r.c + kViewCameraFromView);
if (!GuestReadableApprox(base, cam)) {
continue;
}
const float persp_w = LoadGuestF32(base, cam + 0x60 + (2 * 4 + 3) * 4);
if (persp_w == 1.0f) {
view = r.c;
viewcam = cam;
break;
}
}
if (view == 0) {
return;
}
FrameScene scene;
scene.items.reserve(count);
std::unordered_set<uint32_t> seen;
// Dynamic contexts are submitted several times per frame (once per pass);
// each submission carries that pass's culled island list. Keep the fullest
// one; a shadow-pass list can be missing body parts the main view needs.
std::unordered_map<uint32_t, size_t> dyn_slot;
const auto total_indices = [](const DrawItem& d) {
uint64_t n = 0;
for (const DrawEntry& e : d.draws) n += e.index_count;
return n;
};
for (size_t i = 0; i < count; ++i) {
const SubmitRecord& r = records[i];
// Primary opaque list of the chosen view only; other lists (shadow
// culling, transparents, z-prepass) duplicate the same geometry through
// different MeshContext objects and z-fight.
if (r.kind == 1 && (r.c != view || r.b != 20160)) {
continue;
}
if (r.kind == 2 && r.b != view) {
// World-path capture from another view (shadow cascade): rendering it
// duplicates the entity as a ghost.
seen.insert(r.a);
continue;
}
if (r.kind == 0 || r.kind == 2 || r.kind == 3) {
// Dynamic entity (kind 0), main-view world-path capture (kind 2) or
// quad-list capture (kind 3): the complete item was built at hook time.
seen.insert(r.a);
if (r.c == 0) {
g_rej_no_dynstate.fetch_add(1, std::memory_order_relaxed);
continue;
}
if (r.c > dynitems.size()) {
g_rej_dyn_range.fetch_add(1, std::memory_order_relaxed);
continue;
}
const DrawItem& cand = dynitems[r.c - 1];
if (cand.pending) {
// Deferred mesh whose draw never came: no valid palette/transform.
(cand.skinned ? g_skinned_skipped : g_rigid_dropped)
.fetch_add(1, std::memory_order_relaxed);
continue;
}
auto [slot, inserted] = dyn_slot.try_emplace(r.a, scene.items.size());
if (inserted) {
scene.items.push_back(cand);
} else if (total_indices(cand) > total_indices(scene.items[slot->second])) {
scene.items[slot->second] = cand;
}
continue;
}
if (!seen.insert(r.a).second) {
continue;
}
DrawItem item;
if (BuildItemGeometry(base, r.a, item)) {
if (item.skinned) {
// Skinned meshes reached through the world sort lists (flags,
// banners) have no captured palette, bind-pose garbage; skip.
g_skinned_skipped.fetch_add(1, std::memory_order_relaxed);
continue;
}
if (item.pos_fmt != 57) {
// Model-space prop (vending machines, dispensers): its vertices need
// the per-draw transform, which only the kind-2 hook-time capture
// has; rendered here with identity it collapses at the world
// origin. The capture handles it (or it is dropped when pending).
g_world_props.fetch_add(1, std::memory_order_relaxed);
continue;
}
scene.items.push_back(std::move(item));
}
}
if (scene.items.empty()) {
return;
}
for (int i = 0; i < 16; ++i) {
scene.view_proj[i] = LoadGuestF32(base, viewcam + kViewCamViewProj + i * 4);
}
// Camera position from the view matrix (+0x20, row-vector convention):
// cam = -t * R^T.
{
float view[16];
for (int i = 0; i < 16; ++i) {
view[i] = LoadGuestF32(base, viewcam + 0x20 + i * 4);
}
for (int j = 0; j < 3; ++j) {
scene.cam_pos[j] = -(view[12] * view[j * 4 + 0] + view[13] * view[j * 4 + 1] +
view[14] * view[j * 4 + 2]);
}
}
// The game's projection uses a negative x scale which already yields
// correct D3D NDC orientation; use the view*proj matrix as captured.
// (Negating column 0 here mirrors the image left-right.)
if (g_recording.load(std::memory_order_relaxed)) {
std::lock_guard<std::mutex> lock(g_record_mutex);
if (++g_frames_seen % g_record_stride == 0) {
RecordedFrame rf;
rf.generation = g_generation + 1;
std::memcpy(rf.view_proj, scene.view_proj, sizeof(rf.view_proj));
std::memcpy(rf.cam_pos, scene.cam_pos, sizeof(rf.cam_pos));
rf.dynitems = dynitems;
rf.items = scene.items;
g_recorded_frames.push_back(std::move(rf));
++g_record_frame;
}
}
std::lock_guard<std::mutex> lock(g_scene_mutex);
scene.generation = ++g_generation;
g_scene = std::move(scene);
}
void StartRecording(uint32_t stride) {
std::lock_guard<std::mutex> lock(g_record_mutex);
g_recorded_draws.clear();
g_recorded_frames.clear();
g_recorded_buffers.clear();
g_recorded_buffer_keys.clear();
g_recorded_buffer_bytes = 0;
g_pending_inline_dumps.clear();
g_frame_dump_ids.clear();
g_record_frame = 0;
g_frames_seen = 0;
g_record_stride = stride == 0 ? 1 : stride;
g_recording.store(true, std::memory_order_relaxed);
}
void WriteRecording(const char* dir, const char* stem) {
g_recording.store(false, std::memory_order_relaxed);
std::lock_guard<std::mutex> lock(g_record_mutex);
const std::filesystem::path base_path = std::filesystem::path(dir) / stem;
// Binary draw stream: header "SK3DRAW7", fixed records {u32 frame, u32
// func, u32 ib, u32 vb, u32 vb_offset, u32 vb_stride, u32 streams[4][3],
// u32 vfetch[12], u32 args[4], f32 vs_bank[1024], f32 ps_bank[256],
// u32 flags2d, u32 ps_obj, u32 vs_obj, u32 viewport[6], u32 scissor[4],
// u32 rstates[256], u32 vfetch_all[192], u32 vb_dump, u32 ib_dump}
// (little-endian). func: 0 DrawIndexedVertices, 1 DrawVertices,
// 2 BeginVertices. flags2d: bit0 FrontEndManager::Render2D, bit1
// AptMovieIntegration::Render, bit2 DrawRenderingUnit. vb_dump/ib_dump
// index into buffers.bin records (~0u = none).
{
std::ofstream out(base_path.string() + ".draws.bin", std::ios::binary);
out.write("SK3DRAW7", 8);
for (const auto& d : g_recorded_draws) {
out.write(reinterpret_cast<const char*>(&d->frame), 4);
out.write(reinterpret_cast<const char*>(&d->func), 4);
out.write(reinterpret_cast<const char*>(&d->ib), 4);
out.write(reinterpret_cast<const char*>(&d->vb), 4);
out.write(reinterpret_cast<const char*>(&d->vb_offset), 4);
out.write(reinterpret_cast<const char*>(&d->vb_stride), 4);
out.write(reinterpret_cast<const char*>(d->streams), 48);
out.write(reinterpret_cast<const char*>(d->vfetch), 48);
out.write(reinterpret_cast<const char*>(d->args), 16);
out.write(reinterpret_cast<const char*>(d->bank), 4096);
out.write(reinterpret_cast<const char*>(d->ps), 1024);
out.write(reinterpret_cast<const char*>(&d->flags2d), 4);
out.write(reinterpret_cast<const char*>(&d->ps_obj), 4);
out.write(reinterpret_cast<const char*>(&d->vs_obj), 4);
out.write(reinterpret_cast<const char*>(d->viewport), 24);
out.write(reinterpret_cast<const char*>(d->scissor), 16);
out.write(reinterpret_cast<const char*>(d->rstates), 1024);
out.write(reinterpret_cast<const char*>(d->vfetch_all), 768);
out.write(reinterpret_cast<const char*>(&d->vb_dump), 4);
out.write(reinterpret_cast<const char*>(&d->ib_dump), 4);
}
}
// Captured buffer payloads: header "SK3BUFS1", then records {u32 vb_addr,
// u32 ib_addr, u64 fingerprint, u32 vb_len, u32 ib_len, raw vb, raw ib}.
{
std::ofstream out(base_path.string() + ".buffers.bin", std::ios::binary);
out.write("SK3BUFS1", 8);
for (const RecordedBuffer& b : g_recorded_buffers) {
const uint32_t vb_len = uint32_t(b.vb.size());
const uint32_t ib_len = uint32_t(b.ib.size());
out.write(reinterpret_cast<const char*>(&b.vb_addr), 4);
out.write(reinterpret_cast<const char*>(&b.ib_addr), 4);
out.write(reinterpret_cast<const char*>(&b.fingerprint), 8);
out.write(reinterpret_cast<const char*>(&vb_len), 4);
out.write(reinterpret_cast<const char*>(&ib_len), 4);
out.write(reinterpret_cast<const char*>(b.vb.data()), vb_len);
out.write(reinterpret_cast<const char*>(b.ib.data()), ib_len);
}
}
// Per-frame item dump.
{
std::ofstream out(base_path.string() + ".scene.jsonl");
const auto write_item = [&out](const DrawItem& d) {
out << "{\"mesh\":\"" << std::hex << d.mesh << "\",\"ib_obj\":\"" << d.ib_obj
<< "\",\"vb_obj\":\"" << d.vb_obj << "\",\"vb_addr\":\"" << d.vb_addr
<< "\",\"ib_addr\":\"" << d.ib_addr << "\",\"diffuse\":\"" << d.diffuse_tex
<< "\",\"fp\":\"" << d.fingerprint
<< "\"" << std::dec << ",\"vb_bytes\":" << d.vb_bytes << ",\"ib_count\":"
<< d.ib_count << ",\"stride\":" << int(d.stride) << ",\"pos_fmt\":"
<< int(d.pos_fmt) << ",\"pos_off\":" << d.pos_offset << ",\"bw_off\":"
<< d.bw_offset << ",\"bi_off\":" << d.bi_offset << ",\"skinned\":"
<< (d.skinned ? 1 : 0) << ",\"pending\":" << (d.pending ? 1 : 0)
<< ",\"world\":[";
for (int i = 0; i < 16; ++i) out << (i ? "," : "") << d.world[i];
out << "],\"draws\":[";
for (size_t i = 0; i < d.draws.size(); ++i) {
const DrawEntry& e = d.draws[i];
out << (i ? "," : "") << "[" << e.prim << "," << e.base_vertex << ","
<< e.start_index << "," << e.index_count << "]";
}
out << "],\"bones\":[";
for (size_t i = 0; i < d.bones.size(); ++i) out << (i ? "," : "") << d.bones[i];
out << "]}";
};
for (const RecordedFrame& rf : g_recorded_frames) {
out << "{\"generation\":" << rf.generation << ",\"cam\":[" << rf.cam_pos[0] << ","
<< rf.cam_pos[1] << "," << rf.cam_pos[2] << "],\"view_proj\":[";
for (int i = 0; i < 16; ++i) out << (i ? "," : "") << rf.view_proj[i];
out << "],\"dynitems\":[";
for (size_t i = 0; i < rf.dynitems.size(); ++i) {
if (i) out << ",";
write_item(rf.dynitems[i]);
}
out << "],\"items\":[";
for (size_t i = 0; i < rf.items.size(); ++i) {
if (i) out << ",";
write_item(rf.items[i]);
}
out << "]}\n";
}
}
REXLOG_INFO(
"native-scene: recording written ({} draws, {} frames, {} buffers {} MiB) -> "
"{}.draws.bin/.scene.jsonl/.buffers.bin",
g_recorded_draws.size(), g_recorded_frames.size(), g_recorded_buffers.size(),
g_recorded_buffer_bytes >> 20, base_path.string());
g_recorded_draws.clear();
g_recorded_frames.clear();
g_recorded_buffers.clear();
g_recorded_buffer_keys.clear();
g_recorded_buffer_bytes = 0;
g_pending_inline_dumps.clear();
g_frame_dump_ids.clear();
}
} // namespace skate3::native_scene
#if defined(REX_HAS_D3D12) && REX_HAS_D3D12
namespace skate3::native_scene {
namespace {
using rex::graphics::NativeGuestOutputBackend;
using rex::graphics::NativeGuestOutputRenderContext;
namespace xenos = rex::graphics::xenos;
struct MeshBuffers {
ID3D12Resource* vb = nullptr;
ID3D12Resource* ib = nullptr;
D3D12_VERTEX_BUFFER_VIEW vb_view{};
D3D12_INDEX_BUFFER_VIEW ib_view{};
uint64_t fingerprint = 0;
};
struct GuestTexture {
ID3D12Resource* texture = nullptr;
ID3D12Resource* upload = nullptr; // kept alive; copy recorded in deferred list
uint32_t fetch_words[6] = {}; // big-endian words as read for revalidation
uint32_t srv_slot = 0;
// For failed decodes: frame number for periodic retry (payload may stream
// in after the fetch constant is already valid).
uint64_t retry_after_frame = 0;
bool valid = false;
};
// Host format mapping for the formats Skate 3 uses (mirrors the SDK's
// D3D12 texture cache table). host_swizzle remaps guest data components
// before the fetch-constant swizzle composes on top.
struct HostTextureFormat {
DXGI_FORMAT resource_format = DXGI_FORMAT_UNKNOWN;
DXGI_FORMAT srv_format = DXGI_FORMAT_UNKNOWN;
uint32_t host_swizzle = xenos::XE_GPU_TEXTURE_SWIZZLE_RGBA;
};
bool GetHostTextureFormat(xenos::TextureFormat format, HostTextureFormat& out) {
switch (rex::graphics::GetBaseFormat(format)) {
case xenos::TextureFormat::k_DXT1:
out = {DXGI_FORMAT_BC1_UNORM, DXGI_FORMAT_BC1_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RGBA};
return true;
case xenos::TextureFormat::k_DXT2_3:
out = {DXGI_FORMAT_BC2_UNORM, DXGI_FORMAT_BC2_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RGBA};
return true;
case xenos::TextureFormat::k_DXT4_5:
out = {DXGI_FORMAT_BC3_UNORM, DXGI_FORMAT_BC3_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RGBA};
return true;
case xenos::TextureFormat::k_DXT5A:
out = {DXGI_FORMAT_BC4_UNORM, DXGI_FORMAT_BC4_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RRRR};
return true;
case xenos::TextureFormat::k_DXN:
out = {DXGI_FORMAT_BC5_UNORM, DXGI_FORMAT_BC5_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RGGG};
return true;
case xenos::TextureFormat::k_8_8_8_8:
out = {DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RGBA};
return true;
case xenos::TextureFormat::k_8:
out = {DXGI_FORMAT_R8_UNORM, DXGI_FORMAT_R8_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RRRR};
return true;
case xenos::TextureFormat::k_8_8:
out = {DXGI_FORMAT_R8G8_UNORM, DXGI_FORMAT_R8G8_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RGGG};
return true;
case xenos::TextureFormat::k_5_6_5:
// Red/blue swapped CPU-side while uploading.
out = {DXGI_FORMAT_B5G6R5_UNORM, DXGI_FORMAT_B5G6R5_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RGBB};
return true;
default:
return false;
}
}
UINT ComposeSrvSwizzle(uint32_t fetch_swizzle, uint32_t host_swizzle) {
uint32_t mapping[4];
for (uint32_t c = 0; c < 4; ++c) {
const uint32_t guest = (fetch_swizzle >> (3 * c)) & 7u;
mapping[c] = guest >= 4 ? guest : ((host_swizzle >> (3 * guest)) & 7u);
}
return D3D12_ENCODE_SHADER_4_COMPONENT_MAPPING(mapping[0], mapping[1], mapping[2],
mapping[3]);
}
void SwapGuestEndian(uint8_t* data, uint32_t size, xenos::Endian endian) {
switch (endian) {
case xenos::Endian::k8in16:
for (uint32_t i = 0; i + 2 <= size; i += 2) {
std::swap(data[i], data[i + 1]);
}
break;
case xenos::Endian::k8in32:
for (uint32_t i = 0; i + 4 <= size; i += 4) {
std::swap(data[i], data[i + 3]);
std::swap(data[i + 1], data[i + 2]);
}
break;
case xenos::Endian::k16in32:
for (uint32_t i = 0; i + 4 <= size; i += 4) {
std::swap(data[i], data[i + 2]);
std::swap(data[i + 1], data[i + 3]);
}
break;
default:
break;
}
}
struct RendererState {
ID3D12Device* device = nullptr;
ID3D12RootSignature* root_signature = nullptr;
ID3D12PipelineState* pso = nullptr;
ID3D12PipelineState* pso_nodepth = nullptr;
DXGI_FORMAT rtv_format = DXGI_FORMAT_UNKNOWN;
ID3D12DescriptorHeap* rtv_heap = nullptr; // slot 0 = output, slot 1 = MSAA
ID3D12DescriptorHeap* dsv_heap = nullptr;
ID3D12Resource* depth = nullptr;
uint32_t depth_width = 0;
uint32_t depth_height = 0;
ID3D12Resource* rtv_resource = nullptr;
// MSAA: the scene draws into msaa_color (+ MSAA depth) and a fullscreen
// pass averages the samples into the guest output texture (the deferred
// command list has no ResolveSubresource).
uint32_t msaa = 1;
ID3D12Resource* msaa_color = nullptr;
ID3D12PipelineState* resolve_pso = nullptr;
uint32_t msaa_srv_slot = 0;
bool msaa_srv_allocated = false;
uint32_t rtv_size = 0;
std::unordered_map<uint32_t, MeshBuffers> meshes;
// Buffers replaced by re-decode, kept alive until the GPU has finished the
// submission that last referenced them.
std::vector<std::pair<ID3D12Resource*, uint64_t>> retired;
// Texture SRV staging: CPU-only heap; slots copied into the command
// processor's one-use shader-visible descriptors per draw.
ID3D12DescriptorHeap* srv_heap = nullptr;
uint32_t srv_size = 0;
uint32_t srv_next = 0;
std::unordered_map<uint32_t, GuestTexture> textures;
GuestTexture white;
// Bone palette ring: persistent-mapped upload buffer, one region per
// in-flight frame.
static constexpr uint32_t kBoneRegionSize = 1u << 20;
static constexpr uint32_t kBoneRegions = 4;
ID3D12Resource* bone_ring = nullptr;
uint8_t* bone_ring_cpu = nullptr;
uint32_t bone_ring_offset = 0;
// 2D overlay (HUD/APT replay): alpha-blended depth-less pipeline drawing
// the captured inline vertices from a per-frame upload ring.
ID3D12PipelineState* pso_2d = nullptr;
// In-world neon splines, drawn inside the MSAA scene pass (depth test on,
// no z-write): darken = straight alpha, default = additive glow.
ID3D12PipelineState* pso_spline_darken = nullptr;
ID3D12PipelineState* pso_spline_default = nullptr;
static constexpr uint32_t kUiRegionSize = 1u << 20;
static constexpr uint32_t kUiRegions = 4;
ID3D12Resource* ui_ring = nullptr;
uint8_t* ui_ring_cpu = nullptr;
// Textures resolved from raw fetch-constant words: the 2D path binds its
// textures through the device fetch shadow, not renderengine objects.
// Keyed by an FNV hash of the 6 fetch words.
std::unordered_map<uint64_t, GuestTexture> textures_2d;
bool failed = false;
bool announced = false;
};
RendererState g_r;
// Face-normal shading uses the camera-relative world position: interpolating
// absolute world coordinates (hundreds of meters) destroys ddx/ddy precision
// and produces per-pixel noise.
const char kShaderSource[] = R"(
cbuffer C : register(b0) {
row_major float4x4 world;
row_major float4x4 mvp;
float4 tint;
float4 cam_pos;
// Per-material color multiplier (w > 0 enables): CAS hair is a grayscale
// texture times the character's hair color.
float4 mat_tint;
};
Texture2D<float4> diffuse : register(t0);
Texture2D<float4> lightmap : register(t1);
// Raw bone palette: 3 float4 rows per bone, column-vector affine [R | t],
// applied with explicit dots (StructuredBuffer<float4x4> default packing is
// column-major and would silently transpose the matrices).
StructuredBuffer<float4> bones : register(t2);
SamplerState smp : register(s0);
struct VSOut {
float4 pos : SV_Position;
float3 rpos : TEXCOORD0;
float2 uv : TEXCOORD1;
float2 uv2 : TEXCOORD2;
float3 nrm : TEXCOORD3;
};
VSOut vs_main(float3 p : POSITION, float2 uv : TEXCOORD0, float2 uv2 : TEXCOORD1,
float4 bw : BLENDWEIGHT0, uint4 bi : BLENDINDICES0,
float3 nrm : NORMAL0) {
VSOut o;
float4 mp = float4(p, 1.0);
float3 n = nrm;
// tint.g > 0 marks a skinned item: the bone palette (row-vector matrices)
// maps model space to world space; mvp is then just view*proj.
float wsum = dot(bw, float4(1, 1, 1, 1));
if (tint.g > 0.0 && wsum > 0.001) {
float3 skinned = float3(0, 0, 0);
float3 sn = float3(0, 0, 0);
// Guest blend indices are plain bone numbers (verified live: byte
// streams like 02 00 03 01); bone k = palette rows 3k..3k+2.
[unroll] for (int k = 0; k < 4; ++k) {
uint row = bi[k] * 3u;
skinned += bw[k] * float3(dot(mp, bones[row]), dot(mp, bones[row + 1]),
dot(mp, bones[row + 2]));
sn += bw[k] * float3(dot(n, bones[row].xyz), dot(n, bones[row + 1].xyz),
dot(n, bones[row + 2].xyz));
}
mp = float4(skinned / wsum, 1.0);
n = sn;
} else {
n = mul(n, (float3x3)world);
}
o.pos = mul(mp, mvp);
o.rpos = mul(mp, world).xyz - cam_pos.xyz;
o.uv = uv;
o.uv2 = uv2;
o.nrm = n;
return o;
}
float4 ps_main(VSOut i) : SV_Target {
if (tint.a > 0.0) {
return tint;
}
float4 albedo = diffuse.Sample(smp, i.uv);
// Alpha-tested foliage/fences; opaque formats sample alpha = 1. Skinned
// characters pack gloss in alpha; never clip them.
if (tint.g == 0.0) {
clip(albedo.a - 0.35);
}
// tint.r > 0 marks items with a lightmap bound (2x baked lighting);
// otherwise fall back to derivative face shading.
float3 lit;
if (tint.b > 0.0) {
lit = albedo.rgb; // unlit (sky dome)
} else if (tint.r > 0.0) {
lit = albedo.rgb * lightmap.Sample(smp, i.uv2).rgb * 2.0;
} else {
// Smooth per-vertex normal when the mesh has one; face normal from
// position derivatives otherwise.
float3 n = dot(i.nrm, i.nrm) > 0.01 ? normalize(i.nrm)
: normalize(cross(ddx(i.rpos), ddy(i.rpos)));
float l = abs(dot(n, normalize(float3(0.4, 0.8, 0.3)))) * 0.35 + 0.75;
lit = albedo.rgb * l;
}
if (mat_tint.w > 0.0) {
lit *= mat_tint.rgb;
}
return float4(lit, 1.0);
}
)";
// Fullscreen MSAA resolve: the deferred command list has no
// ResolveSubresource, so average the samples in a pixel shader (SAMPLES is
// injected as a compile-time macro).
const char kResolveShaderSource[] = R"(
Texture2DMS<float4> src : register(t0);
float4 vs_main(uint id : SV_VertexID) : SV_Position {
float2 uv = float2((id << 1) & 2, id & 2);
return float4(uv * float2(2, -2) + float2(-1, 1), 0, 1);
}
float4 ps_main(float4 pos : SV_Position) : SV_Target {
int2 p = int2(pos.xy);
float4 c = 0;
[unroll] for (int k = 0; k < SAMPLES; ++k) {
c += src.Load(p, k);
}
return c / SAMPLES;
}
)";
// 2D/APT overlay shader. Verified against a captured HUD draw stream:
// vertices are {float4 pos, float2 uv} in 1280x720
// APT movie space; the game's VS constants apply as clip = ortho * (world *
// pos) with c0..c3 the ortho rows, c4..c7 the element's transform rows and
// c8 the color multiplier, used exactly as staged.
const char kShader2dSource[] = R"(
cbuffer C : register(b0) {
float4 m[10]; // m[0..3] proj rows, m[4..7] world rows, m[8] color,
// m[9].x = apply D3D9 half-pixel (2D ortho draws only)
};
Texture2D<float4> tex : register(t0);
SamplerState smp : register(s1);
struct VSOut {
float4 pos : SV_Position;
float2 uv : TEXCOORD0;
float4 color : COLOR0;
};
VSOut vs_main(float4 p : POSITION, float2 uv : TEXCOORD0, float4 color : COLOR0) {
float4 wp = float4(dot(p, m[4]), dot(p, m[5]), dot(p, m[6]), dot(p, m[7]));
VSOut o;
o.pos = float4(dot(wp, m[0]), dot(wp, m[1]), dot(wp, m[2]), dot(wp, m[3]));
// D3D9 half-pixel convention: the art bakes half-texel UVs expecting
// pixel centers at integer coordinates; without this the clock-face
// quadrant tiles show their wrapped border rows as dark seam lines.
// Scaled for the 2D ortho; must not apply to 3D (world-space
// SimpleDraw markers), where m[0].x is a projection scale.
if (m[9].x > 0.0) {
o.pos.x -= 0.5 * m[0].x * o.pos.w;
o.pos.y -= 0.5 * m[1].y * o.pos.w;
}
o.uv = uv;
o.color = color;
return o;
}
float4 ps_main(VSOut i) : SV_Target {
return tex.Sample(smp, i.uv) * m[8] * i.color;
}
)";
// In-world neon spline shader (waypoint arrows / marker beams). The guest
// B-spline VS is evaluated on the CPU at publish time, so the VS here is a
// clip-space passthrough; the two PS variants transcribe the game's own
// spline.fx (Skate-Shaders repo): "default" = additive glow with the
// squared-gamma trick, "darken" = straight-alpha backdrop dimming. The
// gradient texture uses the wrap/aniso sampler like the original i_diffuse
// (U runs 0..N along the band).
const char kShaderSplineSource[] = R"(
cbuffer C : register(b0) {
float4 intensity; // i_intensity as staged (x = default gain, y = darken gain)
};
Texture2D<float4> tex : register(t0);
SamplerState smp : register(s0);
struct VSOut {
float4 pos : SV_Position;
float2 uv : TEXCOORD0;
float fade : TEXCOORD1;
};
VSOut vs_main(float4 p : POSITION, float2 uv : TEXCOORD0, float fade : TEXCOORD1) {
VSOut o;
o.pos = p;
o.uv = uv;
o.fade = fade;
return o;
}
// Exact transcription of the Skate 3 spline pixel shaders (disassembled
// ucode, NOT the older Skate 2 spline.fx source: EA added in-shader sqrt
// gamma compensation). default: oC0.rgb = sqrt(rgb^2 * i.x * fade / a);
// darken: oC0.rgb = sqrt(rgb^2 * i.y), oC0.a = sqrt(a * i.y * fade).
// Getting this wrong is visible: a linear 1/a over-brightens the low-alpha
// glow fringe (fuzzy, blown-out edges) and a linear darken alpha weakens the
// backdrop dimming that makes the neon read as solid.
float4 ps_default(VSOut i) : SV_Target {
float4 c = tex.Sample(smp, i.uv);
float3 sq = c.rgb * c.rgb * (intensity.x * i.fade / max(c.a, 1.0 / 255.0));
return float4(sqrt(abs(sq)), 1.0);
}
float4 ps_darken(VSOut i) : SV_Target {
float4 c = tex.Sample(smp, i.uv);
float3 rgb = sqrt(abs(c.rgb * c.rgb * intensity.y));
float a = sqrt(abs(c.a * intensity.y * i.fade));
return float4(rgb, a);
}
)";
float HalfToFloat(uint16_t h) {
const uint32_t sign = (h & 0x8000u) << 16;
uint32_t exp = (h >> 10) & 0x1F;
uint32_t mant = h & 0x3FF;
if (exp == 0) {
if (mant == 0) return std::bit_cast<float>(sign);
// subnormal
while (!(mant & 0x400)) {
mant <<= 1;
--exp;
}
++exp;
mant &= 0x3FF;
} else if (exp == 31) {
return std::bit_cast<float>(sign | 0x7F800000u | (mant << 13));
}
return std::bit_cast<float>(sign | ((exp + 112) << 23) | (mant << 13));
}
ID3D12Resource* CreateUploadBuffer(ID3D12Device* device, size_t size) {
D3D12_HEAP_PROPERTIES heap{};
heap.Type = D3D12_HEAP_TYPE_UPLOAD;
D3D12_RESOURCE_DESC desc{};
desc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
desc.Width = size;
desc.Height = 1;
desc.DepthOrArraySize = 1;
desc.MipLevels = 1;
desc.SampleDesc.Count = 1;
desc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
ID3D12Resource* resource = nullptr;
if (FAILED(device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc,
D3D12_RESOURCE_STATE_GENERIC_READ, nullptr,
IID_PPV_ARGS(&resource)))) {
return nullptr;
}
return resource;
}
uint16_t SwapU16(uint16_t v) { return uint16_t((v >> 8) | (v << 8)); }
uint32_t SwapU32(uint32_t v) {
#if defined(_MSC_VER)
return _byteswap_ulong(v);
#else
return __builtin_bswap32(v);
#endif
}
// Decode guest vertices into {float3 position, float2 uv, float2 uv2,
// unorm4 blend weights, u8x4 blend indices, float3 normal} (48-byte stride).
bool DecodeMesh(ID3D12Device* device, uint8_t* base, const DrawItem& item,
MeshBuffers& out) {
const uint32_t num_verts = item.vb_bytes / item.stride;
if (num_verts == 0) return false;
// This runs on the render thread; the guest payloads were valid on the
// game thread this frame but streaming can decommit them in between.
// Copy them out with the lock-free guarded copy (never VirtualQuery here:
// the VAD lock stalls behind the guest streaming threads while panning).
static thread_local std::vector<uint8_t> vb_scratch;
static thread_local std::vector<uint8_t> ib_scratch;
vb_scratch.resize(item.vb_bytes);
if (!GuestTryCopy(vb_scratch.data(), base + item.vb_addr, item.vb_bytes)) {
return false;
}
if (!item.cloth_quads) {
ib_scratch.resize(size_t(item.ib_count) * 2);
if (!GuestTryCopy(ib_scratch.data(), base + item.ib_addr, size_t(item.ib_count) * 2)) {
return false;
}
}
ID3D12Resource* vb = CreateUploadBuffer(device, size_t(num_verts) * 48);
ID3D12Resource* ib = CreateUploadBuffer(device, size_t(item.ib_count) * 2);
if (!vb || !ib) {
if (vb) vb->Release();
if (ib) ib->Release();
return false;
}
const uint8_t* src_vb = vb_scratch.data();
float* dst = nullptr;
uint32_t garbage = 0;
int min_bi = 255;
int max_bi = -1;
vb->Map(0, nullptr, reinterpret_cast<void**>(&dst));
for (uint32_t v = 0; v < num_verts; ++v) {
const uint8_t* p = src_vb + size_t(v) * item.stride + item.pos_offset;
float x = 0.0f, y = 0.0f, z = 0.0f;
switch (item.pos_fmt) {
case 57: { // k_32_32_32_FLOAT
x = std::bit_cast<float>(SwapU32(*reinterpret_cast<const uint32_t*>(p)));
y = std::bit_cast<float>(SwapU32(*reinterpret_cast<const uint32_t*>(p + 4)));
z = std::bit_cast<float>(SwapU32(*reinterpret_cast<const uint32_t*>(p + 8)));
break;
}
case 32: { // k_16_16_16_16_FLOAT
x = HalfToFloat(SwapU16(*reinterpret_cast<const uint16_t*>(p)));
y = HalfToFloat(SwapU16(*reinterpret_cast<const uint16_t*>(p + 2)));
z = HalfToFloat(SwapU16(*reinterpret_cast<const uint16_t*>(p + 4)));
break;
}
case 26: { // k_16_16_16_16 snorm character dequant
const auto s16 = [&](int off) {
return int16_t(SwapU16(*reinterpret_cast<const uint16_t*>(p + off)));
};
constexpr float kScale = 2.0f / 32767.0f;
x = s16(0) * kScale;
y = s16(2) * kScale + 0.8f;
z = s16(4) * kScale;
break;
}
default:
vb->Unmap(0, nullptr);
vb->Release();
ib->Release();
return false;
}
if (!(x == x && y == y && z == z) ||
x < item.bbox_min[0] - 2.f || x > item.bbox_max[0] + 2.f ||
y < item.bbox_min[1] - 2.f || y > item.bbox_max[1] + 2.f ||
z < item.bbox_min[2] - 2.f || z > item.bbox_max[2] + 2.f) {
++garbage;
}
const auto decode_uv = [&](uint32_t fmt, uint32_t offset, float& u, float& w) {
u = 0.0f;
w = 0.0f;
if (fmt == 0) return;
const uint8_t* q = src_vb + size_t(v) * item.stride + offset;
switch (fmt) {
case 31: // k_16_16_FLOAT
case 32: // k_16_16_16_16_FLOAT (use xy)
u = HalfToFloat(SwapU16(*reinterpret_cast<const uint16_t*>(q)));
w = HalfToFloat(SwapU16(*reinterpret_cast<const uint16_t*>(q + 2)));
break;
case 38: // k_32_32_FLOAT
u = std::bit_cast<float>(SwapU32(*reinterpret_cast<const uint32_t*>(q)));
w = std::bit_cast<float>(SwapU32(*reinterpret_cast<const uint32_t*>(q + 4)));
break;
case 25: { // k_16_16 (snorm; UVs span the full s16 range for [0,1])
const int16_t su = int16_t(SwapU16(*reinterpret_cast<const uint16_t*>(q)));
const int16_t sv = int16_t(SwapU16(*reinterpret_cast<const uint16_t*>(q + 2)));
u = su / 32767.0f;
w = sv / 32767.0f;
break;
}
default:
break;
}
};
dst[v * 12 + 0] = x;
dst[v * 12 + 1] = y;
dst[v * 12 + 2] = z;
decode_uv(item.uv_fmt, item.uv_offset, dst[v * 12 + 3], dst[v * 12 + 4]);
decode_uv(item.uv2_fmt, item.uv2_offset, dst[v * 12 + 5], dst[v * 12 + 6]);
// Blend weights (unorm bytes) and indices (raw bytes). Guest u8x4
// attributes are stored big-endian per 32-bit word; swap so component k
// in the shader matches guest component k.
uint32_t bw = 0;
uint32_t bi = 0;
if (item.skinned) {
bw = SwapU32(*reinterpret_cast<const uint32_t*>(src_vb + size_t(v) * item.stride +
item.bw_offset));
bi = SwapU32(*reinterpret_cast<const uint32_t*>(src_vb + size_t(v) * item.stride +
item.bi_offset));
for (int k = 0; k < 4; ++k) {
const uint8_t weight = uint8_t(bw >> (k * 8));
const uint8_t index = uint8_t(bi >> (k * 8));
if (weight != 0) {
if (index < min_bi) min_bi = index;
if (index > max_bi) max_bi = index;
}
}
}
std::memcpy(&dst[v * 12 + 7], &bw, 4);
std::memcpy(&dst[v * 12 + 8], &bi, 4);
// Vertex normal: k_10_11_11 packed (x 11 bits, y 11 bits, z 10 bits,
// LSB to MSB, signed). Zero when absent -> derivative face-normal
// fallback in the shader.
float nx = 0.0f, ny = 0.0f, nz = 0.0f;
if (item.normal_fmt == 16) {
const uint32_t word = SwapU32(*reinterpret_cast<const uint32_t*>(
src_vb + size_t(v) * item.stride + item.normal_offset));
const int32_t ix = int32_t(word << 21) >> 21;
const int32_t iy = int32_t((word >> 11) << 21) >> 21;
const int32_t iz = int32_t((word >> 22) << 22) >> 22;
nx = float(ix) / 1023.0f;
ny = float(iy) / 1023.0f;
nz = float(iz) / 511.0f;
}
dst[v * 12 + 9] = nx;
dst[v * 12 + 10] = ny;
dst[v * 12 + 11] = nz;
}
vb->Unmap(0, nullptr);
// Blend indices outside the captured palette read garbage rows and mangle
// the vertex. Indices are plain bone numbers, bone k = palette rows 3k.
if (item.skinned && max_bi >= 0) {
const uint32_t palette_bones = uint32_t(item.bones.size() / 12);
if (uint32_t(max_bi) >= palette_bones) {
REXLOG_WARN(
"native-scene: skinned mesh {:08X} blend index range {}..{} exceeds "
"captured palette of {} bones",
item.mesh, min_bi, max_bi, palette_bones);
}
}
if (garbage != 0) {
REXLOG_WARN(
"native-scene: mesh {:08X} decoded {} of {} verts outside bbox "
"({:.1f},{:.1f},{:.1f})..({:.1f},{:.1f},{:.1f}) fmt {} stride {} vb {:08X}",
item.mesh, garbage, num_verts, item.bbox_min[0], item.bbox_min[1], item.bbox_min[2],
item.bbox_max[0], item.bbox_max[1], item.bbox_max[2], item.pos_fmt, item.stride,
item.vb_addr);
}
uint16_t* dst_ib = nullptr;
ib->Map(0, nullptr, reinterpret_cast<void**>(&dst_ib));
if (item.cloth_quads) {
// Quad-list topology with no guest index buffer (live vertex range
// already exact from the draw args): two triangles per quad.
const uint32_t quads = item.ib_count / 6;
for (uint32_t q = 0; q < quads; ++q) {
const uint16_t v = uint16_t(q * 4);
uint16_t* o = dst_ib + q * 6;
o[0] = v;
o[1] = uint16_t(v + 1);
o[2] = uint16_t(v + 2);
o[3] = v;
o[4] = uint16_t(v + 2);
o[5] = uint16_t(v + 3);
}
} else {
const uint16_t* src_ib = reinterpret_cast<const uint16_t*>(ib_scratch.data());
for (uint32_t i = 0; i < item.ib_count; ++i) {
dst_ib[i] = SwapU16(src_ib[i]);
}
}
ib->Unmap(0, nullptr);
out.vb = vb;
out.ib = ib;
out.vb_view = {vb->GetGPUVirtualAddress(), num_verts * 48u, 48u};
out.ib_view = {ib->GetGPUVirtualAddress(), item.ib_count * 2u, DXGI_FORMAT_R16_UINT};
return true;
}
// Decode a guest texture from its 6 fetch-constant words (host-endian),
// the v1-verified path: CPU untile block by block through the 0xA0000000
// physical mirror, endian swap, and create its SRV in the staging heap.
// The 3D path reads the words from renderengine::Texture objects; the 2D
// path passes the device fetch-shadow words directly.
bool EnsureGuestTextureFromWords(const NativeGuestOutputRenderContext& context,
uint8_t* base, const uint32_t words[6],
GuestTexture& out) {
std::memcpy(out.fetch_words, words, 6 * sizeof(uint32_t));
xenos::xe_gpu_texture_fetch_t fetch = {};
fetch.dword_0 = words[0];
fetch.dword_1 = words[1];
fetch.dword_2 = words[2];
fetch.dword_3 = words[3];
fetch.dword_4 = words[4];
fetch.dword_5 = words[5];
if (fetch.type != xenos::FetchConstantType::kTexture || fetch.base_address == 0) {
return false;
}
rex::graphics::TextureInfo info;
if (!rex::graphics::TextureInfo::Prepare(fetch, &info)) {
return false;
}
HostTextureFormat host;
if (!GetHostTextureFormat(info.format, host)) {
return false;
}
if (info.dimension != xenos::DataDimension::k2DOrStacked || info.is_stacked ||
info.width >= 8192 || info.height >= 8192 || info.memory.base_address == 0 ||
info.memory.base_size == 0 || info.memory.base_size > 64u * 1024u * 1024u) {
return false;
}
const rex::graphics::FormatInfo* format_info = info.format_info();
const uint32_t bytes_per_block = format_info->bytes_per_block();
if (bytes_per_block == 0 || (bytes_per_block & (bytes_per_block - 1)) != 0) {
return false;
}
const uint32_t bytes_per_block_log2 = uint32_t(std::countr_zero(bytes_per_block));
const uint32_t width = info.width + 1u;
const uint32_t height = info.height + 1u;
const uint32_t block_w = format_info->block_width;
const uint32_t block_h = format_info->block_height;
const uint32_t host_width = ((width + block_w - 1) / block_w) * block_w;
const uint32_t host_height = ((height + block_h - 1) / block_h) * block_h;
// Upload the guest MIP CHAIN, not just mip 0; sampling mip 0 at distance
// is the source of the grass "TV static" and flickering floor/window
// lines. Power-of-two sizes only (everything the game ships) so BC block
// alignment holds on every level.
const bool pow2 = (width & (width - 1)) == 0 && (height & (height - 1)) == 0;
uint32_t mip_count = 1;
if (pow2 && info.memory.mip_address != 0) {
const uint32_t avail = std::min(info.mip_levels(), info.GetMaxMipLevels());
while (mip_count < avail && (width >> mip_count) >= 4 && (height >> mip_count) >= 4) {
uint32_t ox = 0, oy = 0;
if (info.GetMipLocation(mip_count, &ox, &oy, true) == 0) {
break;
}
++mip_count;
}
}
// Copy the whole guest mip chain out up front with the lock-free guarded
// copy (never VirtualQuery on the render thread: the VAD lock stalls
// behind the guest streaming threads exactly while panning streams
// textures in), truncating the chain at the first unreadable level.
struct MipSrc {
uint32_t addr, scratch_off, size, min_size, pitch_blocks, ox, oy;
};
MipSrc srcs[16] = {};
static thread_local std::vector<uint8_t> tex_scratch;
uint32_t scratch_total = 0;
for (uint32_t m = 0; m < mip_count; ++m) {
uint32_t ox = 0, oy = 0;
const uint32_t mip_addr =
m == 0 ? info.memory.base_address : info.GetMipLocation(m, &ox, &oy, true);
const auto ext = info.GetMipExtent(m, true);
MipSrc& s = srcs[m];
s.addr = mip_addr;
s.pitch_blocks = m == 0 ? info.extent.block_pitch_h : ext.block_pitch_h;
s.size = m == 0 ? info.memory.base_size : ext.all_blocks() * bytes_per_block;
s.min_size = s.size;
if (info.is_tiled) {
// Tiled addressing swizzles across 32x32-BLOCK macro tiles, so a small
// texture with a macro-aligned fetch pitch stores blocks far beyond its
// naive linear size (the 32x32 DXT5 HUD compass icons, pitch 128
// texels: last block at offset 5952 vs base_size 1024; the size guard
// zeroed 48 of 64 blocks, "icons sliced off"). Copy whole macro rows;
// if that over-reaches the committed allocation the copy loop below
// falls back to the reported size.
const uint32_t mh = std::max(height >> m, 1u);
const uint32_t rows = (mh + block_h - 1) / block_h + oy;
const uint32_t padded_rows = (rows + 31u) & ~31u;
s.size = std::max(s.size, padded_rows * s.pitch_blocks * bytes_per_block);
}
s.ox = ox;
s.oy = oy;
s.scratch_off = scratch_total;
scratch_total += s.size;
}
tex_scratch.resize(scratch_total);
uint32_t mips_copied = 0;
for (uint32_t m = 0; m < mip_count; ++m) {
MipSrc& s = srcs[m];
if (!GuestTryCopy(tex_scratch.data() + s.scratch_off,
base + (0xA0000000u | s.addr), s.size)) {
if (s.min_size >= s.size ||
!GuestTryCopy(tex_scratch.data() + s.scratch_off,
base + (0xA0000000u | s.addr), s.min_size)) {
break;
}
s.size = s.min_size;
}
++mips_copied;
}
if (mips_copied == 0) {
return false;
}
mip_count = mips_copied;
// Per-mip upload footprints (D3D12 alignment rules).
struct MipPlan {
uint32_t offset, pitch, cols, rows;
};
MipPlan plans[16] = {};
uint32_t upload_size = 0;
for (uint32_t m = 0; m < mip_count; ++m) {
const uint32_t mw = std::max(width >> m, 1u);
const uint32_t mh = std::max(height >> m, 1u);
MipPlan& p = plans[m];
p.cols = (mw + block_w - 1) / block_w;
p.rows = (mh + block_h - 1) / block_h;
p.pitch = (p.cols * bytes_per_block + (D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u)) &
~(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u);
p.offset = (upload_size + (D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT - 1u)) &
~(D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT - 1u);
upload_size = p.offset + p.pitch * p.rows;
}
ID3D12Device* device = context.d3d12.device;
D3D12_HEAP_PROPERTIES heap{};
heap.Type = D3D12_HEAP_TYPE_DEFAULT;
D3D12_RESOURCE_DESC desc{};
desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
desc.Width = host_width;
desc.Height = host_height;
desc.DepthOrArraySize = 1;
desc.MipLevels = UINT16(mip_count);
desc.Format = host.resource_format;
desc.SampleDesc.Count = 1;
if (FAILED(device->CreateCommittedResource(&heap, D3D12_HEAP_FLAG_NONE, &desc,
D3D12_RESOURCE_STATE_COPY_DEST, nullptr,
IID_PPV_ARGS(&out.texture)))) {
return false;
}
out.upload = CreateUploadBuffer(device, upload_size);
if (!out.upload) {
out.texture->Release();
out.texture = nullptr;
return false;
}
uint8_t* mapping = nullptr;
out.upload->Map(0, nullptr, reinterpret_cast<void**>(&mapping));
const bool swap_rb_565 =
rex::graphics::GetBaseFormat(info.format) == xenos::TextureFormat::k_5_6_5;
for (uint32_t m = 0; m < mip_count; ++m) {
const MipPlan& p = plans[m];
const uint32_t ox = srcs[m].ox;
const uint32_t oy = srcs[m].oy;
const uint32_t src_pitch_blocks = srcs[m].pitch_blocks;
const uint32_t src_size = srcs[m].size;
const uint8_t* guest = tex_scratch.data() + srcs[m].scratch_off;
const uint32_t row_bytes = p.cols * bytes_per_block;
for (uint32_t by = 0; by < p.rows; ++by) {
uint8_t* out_row = mapping + p.offset + size_t(by) * p.pitch;
for (uint32_t bx = 0; bx < p.cols; ++bx) {
uint32_t source_offset;
if (info.is_tiled) {
source_offset = uint32_t(rex::graphics::texture_util::GetTiledOffset2D(
int32_t(bx + ox), int32_t(by + oy), src_pitch_blocks, bytes_per_block_log2));
} else {
source_offset = ((by + oy) * src_pitch_blocks + bx + ox) * bytes_per_block;
}
if (source_offset + bytes_per_block > src_size) {
std::memset(out_row + size_t(bx) * bytes_per_block, 0, bytes_per_block);
continue;
}
std::memcpy(out_row + size_t(bx) * bytes_per_block, guest + source_offset,
bytes_per_block);
}
SwapGuestEndian(out_row, row_bytes, info.endianness);
if (swap_rb_565) {
for (uint32_t i = 0; i + 2 <= row_bytes; i += 2) {
uint16_t value;
std::memcpy(&value, out_row + i, sizeof(value));
value = uint16_t((value & 0x07E0u) | ((value >> 11) & 0x1Fu) |
((value & 0x1Fu) << 11));
std::memcpy(out_row + i, &value, sizeof(value));
}
}
}
}
out.upload->Unmap(0, nullptr);
// Record the upload copies into the deferred command list.
auto* command_processor = context.d3d12.command_processor;
auto& list = command_processor->GetDeferredCommandList();
for (uint32_t m = 0; m < mip_count; ++m) {
const MipPlan& p = plans[m];
D3D12_TEXTURE_COPY_LOCATION dst{};
dst.pResource = out.texture;
dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
dst.SubresourceIndex = m;
D3D12_TEXTURE_COPY_LOCATION src{};
src.pResource = out.upload;
src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
src.PlacedFootprint.Offset = p.offset;
src.PlacedFootprint.Footprint.Format = host.resource_format;
src.PlacedFootprint.Footprint.Width = std::max(host_width >> m, 1u);
src.PlacedFootprint.Footprint.Height = std::max(host_height >> m, 1u);
src.PlacedFootprint.Footprint.Depth = 1;
src.PlacedFootprint.Footprint.RowPitch = p.pitch;
list.D3DCopyTextureRegion(&dst, 0, 0, 0, &src, nullptr);
}
context.d3d12.push_transition_barrier(context.d3d12.command_processor_user_data,
out.texture, D3D12_RESOURCE_STATE_COPY_DEST,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
// SRV in the staging heap with the composed swizzle.
if (g_r.srv_next >= 8192) {
return false;
}
out.srv_slot = g_r.srv_next++;
D3D12_SHADER_RESOURCE_VIEW_DESC srv{};
srv.Format = host.srv_format;
srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srv.Shader4ComponentMapping = ComposeSrvSwizzle(fetch.swizzle, host.host_swizzle);
srv.Texture2D.MipLevels = mip_count;
D3D12_CPU_DESCRIPTOR_HANDLE slot = g_r.srv_heap->GetCPUDescriptorHandleForHeapStart();
slot.ptr += size_t(out.srv_slot) * g_r.srv_size;
device->CreateShaderResourceView(out.texture, &srv, slot);
out.valid = true;
return true;
}
// Fetch-constant read from a renderengine::Texture object (words [7..12]).
bool EnsureGuestTexture(const NativeGuestOutputRenderContext& context, uint8_t* base,
uint32_t tex_ptr, GuestTexture& out) {
uint32_t words[6] = {};
{
uint32_t raw[6];
if (!GuestTryCopy(raw, base + tex_ptr + 7 * 4, sizeof(raw))) {
return false;
}
for (uint32_t i = 0; i < 6; ++i) {
words[i] = SwapU32(raw[i]);
}
}
return EnsureGuestTextureFromWords(context, base, words, out);
}
bool EnsurePipeline(const NativeGuestOutputRenderContext& context) {
if (g_r.failed) return false;
ID3D12Device* device = context.d3d12.device;
g_r.device = device;
if (!g_r.root_signature) {
D3D12_ROOT_PARAMETER params[4] = {};
params[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS;
params[0].Constants.ShaderRegister = 0;
params[0].Constants.Num32BitValues = 44;
params[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL;
D3D12_DESCRIPTOR_RANGE srv_range[2] = {};
srv_range[0].RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
srv_range[0].NumDescriptors = 1;
srv_range[0].BaseShaderRegister = 0;
srv_range[1] = srv_range[0];
srv_range[1].BaseShaderRegister = 1;
params[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
params[1].DescriptorTable.NumDescriptorRanges = 1;
params[1].DescriptorTable.pDescriptorRanges = &srv_range[0];
params[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
params[2] = params[1];
params[2].DescriptorTable.pDescriptorRanges = &srv_range[1];
params[3].ParameterType = D3D12_ROOT_PARAMETER_TYPE_SRV;
params[3].Descriptor.ShaderRegister = 2;
params[3].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;
D3D12_STATIC_SAMPLER_DESC samplers[2] = {};
samplers[0].Filter = D3D12_FILTER_ANISOTROPIC;
samplers[0].MaxAnisotropy = 8;
samplers[0].AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
samplers[0].AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
samplers[0].AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
samplers[0].MaxLOD = D3D12_FLOAT32_MAX;
samplers[0].ShaderRegister = 0;
samplers[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
// s1: bilinear CLAMP for the 2D overlay. The HUD fetch constants carry
// clamp_x/clamp_y = 2 (clamp-to-edge), and the clock face is built from
// MIRRORED quadrant tiles whose outer-edge UVs run past 1.0; wrap
// sampling pulls the opposite edge of the art in as 1px seam lines
// (bright rim row across the middle, dark center column at the edges).
samplers[1] = samplers[0];
samplers[1].Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR;
samplers[1].MaxAnisotropy = 1;
samplers[1].AddressU = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
samplers[1].AddressV = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
samplers[1].AddressW = D3D12_TEXTURE_ADDRESS_MODE_CLAMP;
samplers[1].ShaderRegister = 1;
D3D12_ROOT_SIGNATURE_DESC desc{};
desc.NumParameters = 4;
desc.pParameters = params;
desc.NumStaticSamplers = 2;
desc.pStaticSamplers = samplers;
desc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
if (!context.d3d12.create_root_signature(context.d3d12.command_processor_user_data,
&desc, &g_r.root_signature)) {
REXLOG_ERROR("native-scene: root signature creation failed");
g_r.failed = true;
return false;
}
}
if (!g_r.pso || g_r.rtv_format != context.d3d12.guest_output_format) {
// MSAA level: the requested count, reduced to what the output format
// supports (1 disables and renders directly into the guest output).
const int32_t req = REXCVAR_GET(skate3_native_render_scene_msaa);
uint32_t msaa = req >= 8 ? 8u : req >= 4 ? 4u : req >= 2 ? 2u : 1u;
while (msaa > 1) {
D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS q{
context.d3d12.guest_output_format, msaa, D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE, 0};
if (SUCCEEDED(device->CheckFeatureSupport(D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS,
&q, sizeof(q))) &&
q.NumQualityLevels > 0) {
break;
}
msaa /= 2;
}
g_r.msaa = msaa;
ID3DBlob* vs = nullptr;
ID3DBlob* ps = nullptr;
ID3DBlob* errors = nullptr;
if (FAILED(D3DCompile(kShaderSource, sizeof(kShaderSource) - 1, "native_scene", nullptr,
nullptr, "vs_main", "vs_5_0", 0, 0, &vs, &errors)) ||
FAILED(D3DCompile(kShaderSource, sizeof(kShaderSource) - 1, "native_scene", nullptr,
nullptr, "ps_main", "ps_5_0", 0, 0, &ps, &errors))) {
REXLOG_ERROR("native-scene: shader compile failed: {}",
errors ? static_cast<const char*>(errors->GetBufferPointer()) : "?");
g_r.failed = true;
return false;
}
D3D12_INPUT_ELEMENT_DESC input[6] = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"TEXCOORD", 1, DXGI_FORMAT_R32G32_FLOAT, 0, 20,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"BLENDWEIGHT", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, 28,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"BLENDINDICES", 0, DXGI_FORMAT_R8G8B8A8_UINT, 0, 32,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 36,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}};
D3D12_GRAPHICS_PIPELINE_STATE_DESC pso{};
pso.pRootSignature = g_r.root_signature;
pso.VS = {vs->GetBufferPointer(), vs->GetBufferSize()};
pso.PS = {ps->GetBufferPointer(), ps->GetBufferSize()};
pso.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
pso.SampleMask = UINT_MAX;
pso.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
pso.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
pso.RasterizerState.DepthClipEnable = TRUE;
pso.DepthStencilState.DepthEnable = TRUE;
pso.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL;
pso.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS;
pso.InputLayout = {input, 6};
pso.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
pso.NumRenderTargets = 1;
pso.RTVFormats[0] = context.d3d12.guest_output_format;
pso.DSVFormat = DXGI_FORMAT_D32_FLOAT;
pso.SampleDesc.Count = g_r.msaa;
const HRESULT hr = device->CreateGraphicsPipelineState(&pso, IID_PPV_ARGS(&g_r.pso));
pso.DepthStencilState.DepthEnable = FALSE;
pso.DSVFormat = DXGI_FORMAT_UNKNOWN;
const HRESULT hr2 =
device->CreateGraphicsPipelineState(&pso, IID_PPV_ARGS(&g_r.pso_nodepth));
vs->Release();
ps->Release();
if (errors) errors->Release();
if (FAILED(hr) || FAILED(hr2)) {
REXLOG_ERROR("native-scene: PSO creation failed {:08X}/{:08X}", uint32_t(hr),
uint32_t(hr2));
g_r.failed = true;
return false;
}
if (g_r.msaa > 1) {
char samples[8];
std::snprintf(samples, sizeof(samples), "%u", g_r.msaa);
const D3D_SHADER_MACRO macros[] = {{"SAMPLES", samples}, {nullptr, nullptr}};
ID3DBlob* rvs = nullptr;
ID3DBlob* rps = nullptr;
ID3DBlob* rerrors = nullptr;
if (FAILED(D3DCompile(kResolveShaderSource, sizeof(kResolveShaderSource) - 1,
"native_resolve", macros, nullptr, "vs_main", "vs_5_0", 0, 0,
&rvs, &rerrors)) ||
FAILED(D3DCompile(kResolveShaderSource, sizeof(kResolveShaderSource) - 1,
"native_resolve", macros, nullptr, "ps_main", "ps_5_0", 0, 0,
&rps, &rerrors))) {
REXLOG_ERROR(
"native-scene: resolve shader compile failed: {}",
rerrors ? static_cast<const char*>(rerrors->GetBufferPointer()) : "?");
g_r.failed = true;
return false;
}
D3D12_GRAPHICS_PIPELINE_STATE_DESC rp{};
rp.pRootSignature = g_r.root_signature;
rp.VS = {rvs->GetBufferPointer(), rvs->GetBufferSize()};
rp.PS = {rps->GetBufferPointer(), rps->GetBufferSize()};
rp.BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
rp.SampleMask = UINT_MAX;
rp.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
rp.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
rp.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
rp.NumRenderTargets = 1;
rp.RTVFormats[0] = context.d3d12.guest_output_format;
rp.SampleDesc.Count = 1;
const HRESULT hr3 =
device->CreateGraphicsPipelineState(&rp, IID_PPV_ARGS(&g_r.resolve_pso));
rvs->Release();
rps->Release();
if (rerrors) rerrors->Release();
if (FAILED(hr3)) {
REXLOG_ERROR("native-scene: resolve PSO creation failed {:08X}", uint32_t(hr3));
g_r.failed = true;
return false;
}
}
{
// 2D overlay pipeline: standard alpha blend, no depth, drawn into the
// resolved guest output (sample count 1).
ID3DBlob* uvs = nullptr;
ID3DBlob* ups = nullptr;
ID3DBlob* uerrors = nullptr;
if (FAILED(D3DCompile(kShader2dSource, sizeof(kShader2dSource) - 1, "native_2d",
nullptr, nullptr, "vs_main", "vs_5_0", 0, 0, &uvs,
&uerrors)) ||
FAILED(D3DCompile(kShader2dSource, sizeof(kShader2dSource) - 1, "native_2d",
nullptr, nullptr, "ps_main", "ps_5_0", 0, 0, &ups,
&uerrors))) {
REXLOG_ERROR("native-scene: 2D shader compile failed: {}",
uerrors ? static_cast<const char*>(uerrors->GetBufferPointer())
: "?");
g_r.failed = true;
return false;
}
D3D12_INPUT_ELEMENT_DESC input2d[3] = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 0,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 16,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, 24,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}};
D3D12_GRAPHICS_PIPELINE_STATE_DESC up{};
up.pRootSignature = g_r.root_signature;
up.VS = {uvs->GetBufferPointer(), uvs->GetBufferSize()};
up.PS = {ups->GetBufferPointer(), ups->GetBufferSize()};
auto& rt = up.BlendState.RenderTarget[0];
rt.BlendEnable = TRUE;
// Straight (non-premultiplied) alpha, verified against the decoded
// HUD clock art: the glass-sheen texture is white RGB at low alpha
// (premultiplied blending blows it out into an opaque white blob).
rt.SrcBlend = D3D12_BLEND_SRC_ALPHA;
rt.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
rt.BlendOp = D3D12_BLEND_OP_ADD;
rt.SrcBlendAlpha = D3D12_BLEND_ONE;
rt.DestBlendAlpha = D3D12_BLEND_INV_SRC_ALPHA;
rt.BlendOpAlpha = D3D12_BLEND_OP_ADD;
rt.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
up.SampleMask = UINT_MAX;
up.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
up.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
up.InputLayout = {input2d, 3};
up.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
up.NumRenderTargets = 1;
up.RTVFormats[0] = context.d3d12.guest_output_format;
up.SampleDesc.Count = 1;
const HRESULT hr4 = device->CreateGraphicsPipelineState(&up, IID_PPV_ARGS(&g_r.pso_2d));
uvs->Release();
ups->Release();
if (uerrors) uerrors->Release();
if (FAILED(hr4)) {
REXLOG_ERROR("native-scene: 2D PSO creation failed {:08X}", uint32_t(hr4));
g_r.failed = true;
return false;
}
}
{
// In-world spline pipelines: drawn inside the scene pass (MSAA sample
// count, depth test LESS_EQUAL, no z-write) with the game's own blend
// states (spline.xml): darken = straight alpha, default = additive.
ID3DBlob* svs = nullptr;
ID3DBlob* spd = nullptr;
ID3DBlob* spk = nullptr;
ID3DBlob* serrors = nullptr;
if (FAILED(D3DCompile(kShaderSplineSource, sizeof(kShaderSplineSource) - 1,
"native_spline", nullptr, nullptr, "vs_main", "vs_5_0", 0, 0,
&svs, &serrors)) ||
FAILED(D3DCompile(kShaderSplineSource, sizeof(kShaderSplineSource) - 1,
"native_spline", nullptr, nullptr, "ps_default", "ps_5_0", 0,
0, &spd, &serrors)) ||
FAILED(D3DCompile(kShaderSplineSource, sizeof(kShaderSplineSource) - 1,
"native_spline", nullptr, nullptr, "ps_darken", "ps_5_0", 0,
0, &spk, &serrors))) {
REXLOG_ERROR("native-scene: spline shader compile failed: {}",
serrors ? static_cast<const char*>(serrors->GetBufferPointer())
: "?");
g_r.failed = true;
return false;
}
D3D12_INPUT_ELEMENT_DESC input_spline[3] = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 0,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 16,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"TEXCOORD", 1, DXGI_FORMAT_R32_FLOAT, 0, 24,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}};
D3D12_GRAPHICS_PIPELINE_STATE_DESC sp{};
sp.pRootSignature = g_r.root_signature;
sp.VS = {svs->GetBufferPointer(), svs->GetBufferSize()};
sp.SampleMask = UINT_MAX;
sp.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
sp.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
sp.RasterizerState.DepthClipEnable = TRUE;
sp.DepthStencilState.DepthEnable = TRUE;
sp.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
sp.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS_EQUAL;
sp.InputLayout = {input_spline, 3};
sp.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
sp.NumRenderTargets = 1;
sp.RTVFormats[0] = context.d3d12.guest_output_format;
sp.DSVFormat = DXGI_FORMAT_D32_FLOAT;
sp.SampleDesc.Count = g_r.msaa;
auto& srt = sp.BlendState.RenderTarget[0];
srt.BlendEnable = TRUE;
srt.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
srt.BlendOp = D3D12_BLEND_OP_ADD;
srt.BlendOpAlpha = D3D12_BLEND_OP_ADD;
// darken pass: straight alpha.
sp.PS = {spk->GetBufferPointer(), spk->GetBufferSize()};
srt.SrcBlend = D3D12_BLEND_SRC_ALPHA;
srt.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
srt.SrcBlendAlpha = D3D12_BLEND_SRC_ALPHA;
srt.DestBlendAlpha = D3D12_BLEND_INV_SRC_ALPHA;
const HRESULT hr5 =
device->CreateGraphicsPipelineState(&sp, IID_PPV_ARGS(&g_r.pso_spline_darken));
// default pass: additive glow.
sp.PS = {spd->GetBufferPointer(), spd->GetBufferSize()};
srt.SrcBlend = D3D12_BLEND_ONE;
srt.DestBlend = D3D12_BLEND_ONE;
srt.SrcBlendAlpha = D3D12_BLEND_ONE;
srt.DestBlendAlpha = D3D12_BLEND_ONE;
const HRESULT hr6 =
device->CreateGraphicsPipelineState(&sp, IID_PPV_ARGS(&g_r.pso_spline_default));
svs->Release();
spd->Release();
spk->Release();
if (serrors) serrors->Release();
if (FAILED(hr5) || FAILED(hr6)) {
REXLOG_ERROR("native-scene: spline PSO creation failed {:08X}/{:08X}",
uint32_t(hr5), uint32_t(hr6));
g_r.failed = true;
return false;
}
}
REXLOG_INFO("native-scene: pipelines created (MSAA x{})", g_r.msaa);
g_r.rtv_format = context.d3d12.guest_output_format;
}
if (!g_r.rtv_heap) {
// Slots 0/1 = guest output / MSAA color; 2+ = APT render-to-texture
// targets.
D3D12_DESCRIPTOR_HEAP_DESC heap{D3D12_DESCRIPTOR_HEAP_TYPE_RTV, 8,
D3D12_DESCRIPTOR_HEAP_FLAG_NONE, 0};
if (FAILED(device->CreateDescriptorHeap(&heap, IID_PPV_ARGS(&g_r.rtv_heap)))) {
g_r.failed = true;
return false;
}
g_r.rtv_size = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
heap.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
if (FAILED(device->CreateDescriptorHeap(&heap, IID_PPV_ARGS(&g_r.dsv_heap)))) {
g_r.failed = true;
return false;
}
heap.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
heap.NumDescriptors = 8192;
heap.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
if (FAILED(device->CreateDescriptorHeap(&heap, IID_PPV_ARGS(&g_r.srv_heap)))) {
g_r.failed = true;
return false;
}
g_r.srv_size =
device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
}
if (!g_r.bone_ring) {
g_r.bone_ring = CreateUploadBuffer(
device, size_t(RendererState::kBoneRegionSize) * RendererState::kBoneRegions);
if (!g_r.bone_ring ||
FAILED(g_r.bone_ring->Map(0, nullptr,
reinterpret_cast<void**>(&g_r.bone_ring_cpu)))) {
g_r.failed = true;
return false;
}
}
if (!g_r.ui_ring) {
g_r.ui_ring = CreateUploadBuffer(
device, size_t(RendererState::kUiRegionSize) * RendererState::kUiRegions);
if (!g_r.ui_ring ||
FAILED(g_r.ui_ring->Map(0, nullptr,
reinterpret_cast<void**>(&g_r.ui_ring_cpu)))) {
g_r.failed = true;
return false;
}
}
if (!g_r.white.valid) {
// 1x1 white fallback for items without a resolved diffuse texture.
D3D12_HEAP_PROPERTIES heap_props{};
heap_props.Type = D3D12_HEAP_TYPE_DEFAULT;
D3D12_RESOURCE_DESC desc{};
desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
desc.Width = 1;
desc.Height = 1;
desc.DepthOrArraySize = 1;
desc.MipLevels = 1;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.SampleDesc.Count = 1;
if (FAILED(device->CreateCommittedResource(&heap_props, D3D12_HEAP_FLAG_NONE, &desc,
D3D12_RESOURCE_STATE_COPY_DEST, nullptr,
IID_PPV_ARGS(&g_r.white.texture)))) {
g_r.failed = true;
return false;
}
g_r.white.upload = CreateUploadBuffer(device, 256);
if (!g_r.white.upload) {
g_r.failed = true;
return false;
}
uint8_t* mapping = nullptr;
g_r.white.upload->Map(0, nullptr, reinterpret_cast<void**>(&mapping));
std::memset(mapping, 0xFF, 4);
g_r.white.upload->Unmap(0, nullptr);
auto& list = context.d3d12.command_processor->GetDeferredCommandList();
D3D12_TEXTURE_COPY_LOCATION dst{};
dst.pResource = g_r.white.texture;
dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
D3D12_TEXTURE_COPY_LOCATION src{};
src.pResource = g_r.white.upload;
src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
src.PlacedFootprint.Footprint.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
src.PlacedFootprint.Footprint.Width = 1;
src.PlacedFootprint.Footprint.Height = 1;
src.PlacedFootprint.Footprint.Depth = 1;
src.PlacedFootprint.Footprint.RowPitch = 256;
list.D3DCopyTextureRegion(&dst, 0, 0, 0, &src, nullptr);
context.d3d12.push_transition_barrier(context.d3d12.command_processor_user_data,
g_r.white.texture,
D3D12_RESOURCE_STATE_COPY_DEST,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
g_r.white.srv_slot = g_r.srv_next++;
D3D12_SHADER_RESOURCE_VIEW_DESC srv{};
srv.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srv.Texture2D.MipLevels = 1;
D3D12_CPU_DESCRIPTOR_HANDLE slot = g_r.srv_heap->GetCPUDescriptorHandleForHeapStart();
slot.ptr += size_t(g_r.white.srv_slot) * g_r.srv_size;
device->CreateShaderResourceView(g_r.white.texture, &srv, slot);
g_r.white.valid = true;
}
const uint32_t width = context.guest_output_width;
const uint32_t height = context.guest_output_height;
if (!g_r.depth || g_r.depth_width != width || g_r.depth_height != height) {
if (g_r.depth) {
g_r.depth->Release();
g_r.depth = nullptr;
}
D3D12_HEAP_PROPERTIES heap{};
heap.Type = D3D12_HEAP_TYPE_DEFAULT;
D3D12_RESOURCE_DESC desc{};
desc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
desc.Width = width;
desc.Height = height;
desc.DepthOrArraySize = 1;
desc.MipLevels = 1;
desc.Format = DXGI_FORMAT_D32_FLOAT;
desc.SampleDesc.Count = g_r.msaa;
desc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
D3D12_CLEAR_VALUE clear{};
clear.Format = DXGI_FORMAT_D32_FLOAT;
clear.DepthStencil.Depth = 1.0f;
if (FAILED(g_r.device->CreateCommittedResource(
&heap, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_DEPTH_WRITE, &clear,
IID_PPV_ARGS(&g_r.depth)))) {
g_r.failed = true;
return false;
}
g_r.depth_width = width;
g_r.depth_height = height;
device->CreateDepthStencilView(g_r.depth, nullptr,
g_r.dsv_heap->GetCPUDescriptorHandleForHeapStart());
if (g_r.msaa > 1) {
// MSAA color target (RTV heap slot 1) + its Texture2DMS SRV for the
// fullscreen resolve pass. Lives in RENDER_TARGET state between frames.
if (g_r.msaa_color) {
g_r.retired.emplace_back(g_r.msaa_color,
context.d3d12.command_processor->GetCurrentSubmission());
g_r.msaa_color = nullptr;
}
D3D12_RESOURCE_DESC cdesc = desc;
cdesc.Format = context.d3d12.guest_output_format;
cdesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET;
D3D12_CLEAR_VALUE cclear{};
cclear.Format = cdesc.Format;
cclear.Color[0] = 0.25f;
cclear.Color[1] = 0.35f;
cclear.Color[2] = 0.55f;
cclear.Color[3] = 1.0f;
if (FAILED(g_r.device->CreateCommittedResource(
&heap, D3D12_HEAP_FLAG_NONE, &cdesc, D3D12_RESOURCE_STATE_RENDER_TARGET,
&cclear, IID_PPV_ARGS(&g_r.msaa_color)))) {
g_r.failed = true;
return false;
}
D3D12_CPU_DESCRIPTOR_HANDLE msaa_rtv =
g_r.rtv_heap->GetCPUDescriptorHandleForHeapStart();
msaa_rtv.ptr += g_r.rtv_size;
device->CreateRenderTargetView(g_r.msaa_color, nullptr, msaa_rtv);
if (!g_r.msaa_srv_allocated) {
g_r.msaa_srv_slot = g_r.srv_next++;
g_r.msaa_srv_allocated = true;
}
D3D12_SHADER_RESOURCE_VIEW_DESC srv{};
srv.Format = cdesc.Format;
srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2DMS;
srv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
D3D12_CPU_DESCRIPTOR_HANDLE slot = g_r.srv_heap->GetCPUDescriptorHandleForHeapStart();
slot.ptr += size_t(g_r.msaa_srv_slot) * g_r.srv_size;
device->CreateShaderResourceView(g_r.msaa_color, &srv, slot);
}
}
if (g_r.rtv_resource != context.d3d12.guest_output_resource) {
device->CreateRenderTargetView(context.d3d12.guest_output_resource, nullptr,
g_r.rtv_heap->GetCPUDescriptorHandleForHeapStart());
g_r.rtv_resource = context.d3d12.guest_output_resource;
}
return true;
}
bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_data*/) {
if (!SceneEnabled() || context.backend != NativeGuestOutputBackend::kD3D12) {
return false;
}
// While the game reports menus / pause / loading (presence context 0x8001
// == 0), yield to the emulated output: the native scene neither renders
// the 2D UI (the pause menu was invisible) nor the menu's world backdrop
// materials (everything drew untextured white). The emulated frame is
// complete and correct there; native rendering resumes on unpause.
{
static bool s_in_menus = false;
const bool in_menus = rex::graphics::ultrawide_debug::Skate3GameplayContextValue() == 0;
if (in_menus != s_in_menus) {
s_in_menus = in_menus;
REXLOG_INFO("native-scene: {} (presence context)",
in_menus ? "menus/pause - yielding to emulated output"
: "gameplay - rendering natively");
}
if (in_menus) {
return false;
}
}
uint8_t* base = g_guest_base.load(std::memory_order_relaxed);
if (base == nullptr) {
return false;
}
FrameScene scene;
{
std::lock_guard<std::mutex> lock(g_scene_mutex);
if (g_scene.items.empty()) {
return false;
}
scene = g_scene;
}
if (!EnsurePipeline(context)) {
return false;
}
// Flush any barriers pushed by lazy resource creation (white texture).
context.d3d12.submit_barriers(context.d3d12.command_processor_user_data);
if (!g_r.announced) {
g_r.announced = true;
REXLOG_INFO("native-scene: rendering natively ({} items, {}x{})", scene.items.size(),
context.guest_output_width, context.guest_output_height);
}
auto* command_processor = context.d3d12.command_processor;
auto& list = command_processor->GetDeferredCommandList();
// Free retired buffers whose last-referencing submission has completed.
if (!g_r.retired.empty()) {
const uint64_t completed = command_processor->GetCompletedSubmission();
std::erase_if(g_r.retired, [completed](const auto& entry) {
if (entry.second < completed) {
entry.first->Release();
return true;
}
return false;
});
}
// The scene draws into the MSAA target when enabled (resolved into the
// guest output at the end of the pass), or straight into the guest output.
const bool msaa_on = g_r.msaa > 1 && g_r.msaa_color != nullptr && g_r.resolve_pso != nullptr;
const D3D12_CPU_DESCRIPTOR_HANDLE output_rtv =
g_r.rtv_heap->GetCPUDescriptorHandleForHeapStart();
D3D12_CPU_DESCRIPTOR_HANDLE scene_rtv = output_rtv;
if (msaa_on) {
scene_rtv.ptr += g_r.rtv_size;
} else {
context.d3d12.push_transition_barrier(context.d3d12.command_processor_user_data,
context.d3d12.guest_output_resource,
context.d3d12.guest_output_initial_state,
D3D12_RESOURCE_STATE_RENDER_TARGET);
context.d3d12.submit_barriers(context.d3d12.command_processor_user_data);
}
const int32_t debug_mode = REXCVAR_GET(skate3_native_render_scene_debug);
const bool use_depth = debug_mode != 4;
const D3D12_CPU_DESCRIPTOR_HANDLE dsv = g_r.dsv_heap->GetCPUDescriptorHandleForHeapStart();
const FLOAT clear_color[4] = {0.25f, 0.35f, 0.55f, 1.0f};
list.D3DClearRenderTargetView(scene_rtv, clear_color, 0, nullptr);
if (use_depth) {
list.D3DClearDepthStencilView(dsv, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
list.D3DOMSetRenderTargets(1, &scene_rtv, FALSE, &dsv);
} else {
list.D3DOMSetRenderTargets(1, &scene_rtv, FALSE, nullptr);
}
D3D12_VIEWPORT viewport{0.0f,
0.0f,
float(context.guest_output_width),
float(context.guest_output_height),
0.0f,
1.0f};
list.RSSetViewport(viewport);
D3D12_RECT scissor{0, 0, LONG(context.guest_output_width),
LONG(context.guest_output_height)};
list.RSSetScissorRect(scissor);
list.D3DSetGraphicsRootSignature(g_r.root_signature);
list.D3DSetPipelineState(use_depth ? g_r.pso : g_r.pso_nodepth);
// Our own persistent shader-visible SRV heap for the whole pass. These are
// the last commands of the submission, so displacing the emulated GPU's
// heap binding is safe.
list.SetDescriptorHeaps(g_r.srv_heap, nullptr);
// Reset this frame's bone ring region.
const uint64_t frame_number = g_frames_rendered.load(std::memory_order_relaxed);
const uint32_t bone_region =
uint32_t(frame_number % RendererState::kBoneRegions) *
RendererState::kBoneRegionSize;
g_r.bone_ring_offset = 0;
uint32_t drawn = 0;
uint32_t item_index = 0;
// Per-frame decode budgets: panning/streaming can surface dozens of new
// meshes and textures in one frame, and each decode does CPU conversion +
// CreateCommittedResource on the render thread; unbounded, that is a
// visible hitch. Over-budget work is deferred; at native frame rates the
// pop-in lasts a few tens of milliseconds.
const int32_t mesh_budget = REXCVAR_GET(skate3_native_render_scene_mesh_decode_budget);
const int32_t tex_budget = REXCVAR_GET(skate3_native_render_scene_texture_decode_budget);
uint32_t mesh_decodes = 0;
uint32_t tex_decodes = 0;
for (const DrawItem& item : scene.items) {
const uint32_t index = item_index++;
if (debug_mode == 1) {
break;
}
if (debug_mode == 3 && index >= 20) {
break;
}
auto it = g_r.meshes.find(item.mesh);
if (it != g_r.meshes.end() && it->second.fingerprint != item.fingerprint) {
const uint64_t submission = command_processor->GetCurrentSubmission();
g_r.retired.emplace_back(it->second.vb, submission);
g_r.retired.emplace_back(it->second.ib, submission);
g_r.meshes.erase(it);
it = g_r.meshes.end();
}
if (it == g_r.meshes.end()) {
if (mesh_budget > 0 && mesh_decodes >= uint32_t(mesh_budget)) {
g_rr_mesh_deferred.fetch_add(1, std::memory_order_relaxed);
continue; // decodes on a later frame
}
++mesh_decodes;
MeshBuffers buffers;
if (!DecodeMesh(g_r.device, base, item, buffers)) {
g_rr_decode_fail.fetch_add(1, std::memory_order_relaxed);
static std::unordered_set<uint32_t> logged;
if (logged.size() < 32 && logged.insert(item.mesh).second) {
REXLOG_WARN("native-scene: DecodeMesh FAILED mesh={:08X} vb={:08X} fmt={} stride={}",
item.mesh, item.vb_addr, item.pos_fmt, item.stride);
}
continue;
}
buffers.fingerprint = item.fingerprint;
it = g_r.meshes.emplace(item.mesh, buffers).first;
}
const MeshBuffers& buffers = it->second;
// Resolve guest textures (white fallback). Cached decodes revalidate
// against the live fetch words; streaming reuses texture objects. The
// object addresses were readable on the game thread this frame; NO
// VirtualQuery here; it takes the process VAD lock, which the guest
// streaming threads hammer, and ~2 calls per item stalled the whole
// renderer to 3 fps.
const auto resolve_texture = [&](uint32_t tex_ptr) -> const GuestTexture* {
if (tex_ptr == 0) {
return &g_r.white;
}
auto tit = g_r.textures.find(tex_ptr);
if (tit != g_r.textures.end()) {
uint32_t live[6];
for (uint32_t k = 0; k < 6; ++k) {
live[k] = REX_LOAD_U32(tex_ptr + (7 + k) * 4);
}
const bool retry_failed =
!tit->second.valid && frame_number >= tit->second.retry_after_frame;
if (retry_failed ||
std::memcmp(live, tit->second.fetch_words, sizeof(live)) != 0) {
const uint64_t submission = command_processor->GetCurrentSubmission();
if (tit->second.texture) g_r.retired.emplace_back(tit->second.texture, submission);
if (tit->second.upload) g_r.retired.emplace_back(tit->second.upload, submission);
g_r.textures.erase(tit);
tit = g_r.textures.end();
}
}
if (tit == g_r.textures.end()) {
if (tex_budget > 0 && tex_decodes >= uint32_t(tex_budget)) {
g_rr_tex_deferred.fetch_add(1, std::memory_order_relaxed);
return &g_r.white; // decodes on a later frame
}
++tex_decodes;
GuestTexture gt;
EnsureGuestTexture(context, base, tex_ptr, gt);
// Remember the live words even for failed decodes so they are not
// re-attempted every frame; schedule a periodic retry instead.
if (!gt.valid) {
if (gt.fetch_words[0] == 0 && gt.fetch_words[1] == 0) {
for (uint32_t k = 0; k < 6; ++k) {
gt.fetch_words[k] = REX_LOAD_U32(tex_ptr + (7 + k) * 4);
}
}
gt.retry_after_frame = frame_number + 120;
// Failed decodes render white: log each once (capped) so white
// meshes are always attributable to a specific texture.
static std::unordered_set<uint32_t> logged_failed;
if (logged_failed.size() < 64 && logged_failed.insert(tex_ptr).second) {
REXLOG_INFO(
"native-scene: texture decode FAILED obj={:08X} fetch=[{:08X} {:08X} "
"{:08X} {:08X} {:08X} {:08X}]",
tex_ptr, gt.fetch_words[0], gt.fetch_words[1], gt.fetch_words[2],
gt.fetch_words[3], gt.fetch_words[4], gt.fetch_words[5]);
}
}
// Flush the COPY_DEST -> PIXEL_SHADER_RESOURCE barrier before the
// draw that samples the new texture.
context.d3d12.submit_barriers(context.d3d12.command_processor_user_data);
tit = g_r.textures.emplace(tex_ptr, gt).first;
}
return tit->second.valid ? &tit->second : &g_r.white;
};
const GuestTexture* diffuse = resolve_texture(item.diffuse_tex);
const GuestTexture* lightmap =
item.lightmap_tex != 0 && REXCVAR_GET(skate3_native_render_scene_lightmaps)
? resolve_texture(item.lightmap_tex)
: nullptr;
if (lightmap == &g_r.white) {
lightmap = nullptr;
}
// constants = world + mvp (world * view_proj, row-vector) + tint + cam
// + material tint. tint.a > 0 selects debug solid colors; tint.r > 0
// marks a bound lightmap in the normal path.
float constants[44];
std::memcpy(constants, item.world, sizeof(item.world));
float* mvp = constants + 16;
for (int r = 0; r < 4; ++r) {
for (int c = 0; c < 4; ++c) {
float sum = 0.0f;
for (int k = 0; k < 4; ++k) {
sum += item.world[r * 4 + k] * scene.view_proj[k * 4 + c];
}
mvp[r * 4 + c] = sum;
}
}
// Bone palette upload for skinned items; tint.g flags skinning.
bool bones_bound = false;
if (item.skinned && !item.bones.empty()) {
const uint32_t bytes = uint32_t(item.bones.size() * sizeof(float));
const uint32_t offset = (g_r.bone_ring_offset + 255u) & ~255u;
if (offset + bytes <= RendererState::kBoneRegionSize) {
std::memcpy(g_r.bone_ring_cpu + bone_region + offset, item.bones.data(), bytes);
g_r.bone_ring_offset = offset + bytes;
list.D3DSetGraphicsRootShaderResourceView(
3, g_r.bone_ring->GetGPUVirtualAddress() + bone_region + offset);
bones_bound = true;
}
}
if (!bones_bound) {
if (item.skinned && !item.bones.empty()) {
// Ring exhausted: this item renders bind-pose at identity,
// effectively invisible. Must never happen silently.
g_rr_no_bones.fetch_add(1, std::memory_order_relaxed);
}
list.D3DSetGraphicsRootShaderResourceView(3, g_r.bone_ring->GetGPUVirtualAddress());
}
if (debug_mode >= 2) {
// Stable per-object colors: hash the mesh address, not the (sort-order
// dependent) item index.
const uint32_t hash = (item.mesh >> 4) * 2654435761u;
constants[32] = float((hash >> 0) & 0xFF) / 255.0f;
constants[33] = float((hash >> 8) & 0xFF) / 255.0f;
constants[34] = float((hash >> 16) & 0xFF) / 255.0f;
constants[35] = 1.0f;
} else {
constants[32] = lightmap != nullptr ? 1.0f : 0.0f;
constants[33] = bones_bound ? 1.0f : 0.0f;
constants[34] = item.unlit ? 1.0f : 0.0f;
constants[35] = 0.0f;
}
constants[36] = scene.cam_pos[0];
constants[37] = scene.cam_pos[1];
constants[38] = scene.cam_pos[2];
constants[39] = 0.0f;
std::memcpy(constants + 40, item.tint, 4 * sizeof(float));
list.D3DSetGraphicsRoot32BitConstants(0, 44, constants, 0);
const D3D12_GPU_DESCRIPTOR_HANDLE heap_start =
g_r.srv_heap->GetGPUDescriptorHandleForHeapStart();
D3D12_GPU_DESCRIPTOR_HANDLE srv_gpu = heap_start;
srv_gpu.ptr += size_t(diffuse->srv_slot) * g_r.srv_size;
context.d3d12.set_graphics_root_descriptor_table(
context.d3d12.command_processor_user_data, 1, srv_gpu);
D3D12_GPU_DESCRIPTOR_HANDLE lm_gpu = heap_start;
lm_gpu.ptr +=
size_t((lightmap != nullptr ? lightmap : &g_r.white)->srv_slot) * g_r.srv_size;
context.d3d12.set_graphics_root_descriptor_table(
context.d3d12.command_processor_user_data, 2, lm_gpu);
list.D3DIASetVertexBuffers(0, 1, &buffers.vb_view);
list.D3DIASetIndexBuffer(&buffers.ib_view);
for (const DrawEntry& draw : item.draws) {
if (draw.prim == 4) {
list.D3DIASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
} else if (draw.prim == 6) {
list.D3DIASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
} else {
continue;
}
list.D3DDrawIndexedInstanced(draw.index_count, 1, draw.start_index, draw.base_vertex,
0);
++drawn;
}
}
// Guest-texture resolver shared by the spline pass (pre-resolve, in the
// scene pass) and the HUD pass (post-resolve); both allocate strip/quad
// vertices from the same per-frame ui_ring region.
const auto resolve_2d_texture = [&](const uint32_t fetch[6]) -> const GuestTexture* {
if ((fetch[0] & 0x3u) != 2 || fetch[1] == 0) {
return &g_r.white;
}
uint64_t key = 1469598103934665603ull;
for (int k = 0; k < 6; ++k) {
key ^= fetch[k];
key *= 1099511628211ull;
}
auto it = g_r.textures_2d.find(key);
if (it == g_r.textures_2d.end()) {
GuestTexture gt;
EnsureGuestTextureFromWords(context, base, fetch, gt);
if (!gt.valid) {
static std::unordered_set<uint64_t> logged;
if (logged.size() < 32 && logged.insert(key).second) {
REXLOG_INFO(
"native-scene: 2D texture decode FAILED fetch=[{:08X} {:08X} {:08X} "
"{:08X} {:08X} {:08X}]",
fetch[0], fetch[1], fetch[2], fetch[3], fetch[4], fetch[5]);
}
}
context.d3d12.submit_barriers(context.d3d12.command_processor_user_data);
it = g_r.textures_2d.emplace(key, gt).first;
}
return it->second.valid ? &it->second : &g_r.white;
};
const uint32_t ui_region =
uint32_t(frame_number % RendererState::kUiRegions) * RendererState::kUiRegionSize;
uint32_t ui_offset = 0;
// In-world neon splines (waypoint arrows / marker beams): replayed inside
// the scene pass, depth-tested against the world like the emulated frame,
// in submission order (darken backdrop passes precede the additive glow).
uint32_t drawn_spline = 0;
if (REXCVAR_GET(skate3_native_render_scene_splines) &&
g_r.pso_spline_default != nullptr && g_r.ui_ring_cpu != nullptr) {
std::vector<SplineDraw> scene_spline;
{
std::lock_guard<std::mutex> lock(g_2d_mutex);
scene_spline = g_scene_spline;
}
for (const SplineDraw& s : scene_spline) {
const uint32_t bytes = uint32_t(s.verts.size());
if (bytes == 0 || ui_offset + bytes > RendererState::kUiRegionSize) {
continue;
}
std::memcpy(g_r.ui_ring_cpu + ui_region + ui_offset, s.verts.data(), bytes);
const uint32_t srv_slot = resolve_2d_texture(s.fetch)->srv_slot;
list.D3DSetPipelineState(s.pass == 1 ? g_r.pso_spline_darken
: g_r.pso_spline_default);
// Root constants: i_intensity as staged (c149).
list.D3DSetGraphicsRoot32BitConstants(0, 4, s.consts + 149 * 4, 0);
D3D12_GPU_DESCRIPTOR_HANDLE srv_gpu =
g_r.srv_heap->GetGPUDescriptorHandleForHeapStart();
srv_gpu.ptr += size_t(srv_slot) * g_r.srv_size;
context.d3d12.set_graphics_root_descriptor_table(
context.d3d12.command_processor_user_data, 1, srv_gpu);
D3D12_VERTEX_BUFFER_VIEW vbv{
g_r.ui_ring->GetGPUVirtualAddress() + ui_region + ui_offset, bytes, 28};
list.D3DIASetVertexBuffers(0, 1, &vbv);
list.D3DIASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
list.D3DDrawInstanced(s.count, 1, 0, 0);
ui_offset += bytes;
++drawn_spline;
}
}
if (msaa_on) {
// Resolve: average the MSAA samples into the guest output with a
// fullscreen pass, then restore steady-state resource states.
context.d3d12.push_transition_barrier(context.d3d12.command_processor_user_data,
g_r.msaa_color, D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
context.d3d12.push_transition_barrier(context.d3d12.command_processor_user_data,
context.d3d12.guest_output_resource,
context.d3d12.guest_output_initial_state,
D3D12_RESOURCE_STATE_RENDER_TARGET);
context.d3d12.submit_barriers(context.d3d12.command_processor_user_data);
list.D3DOMSetRenderTargets(1, &output_rtv, FALSE, nullptr);
list.D3DSetPipelineState(g_r.resolve_pso);
D3D12_GPU_DESCRIPTOR_HANDLE msaa_srv =
g_r.srv_heap->GetGPUDescriptorHandleForHeapStart();
msaa_srv.ptr += size_t(g_r.msaa_srv_slot) * g_r.srv_size;
context.d3d12.set_graphics_root_descriptor_table(
context.d3d12.command_processor_user_data, 1, msaa_srv);
list.D3DIASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
list.D3DDrawInstanced(3, 1, 0, 0);
context.d3d12.push_transition_barrier(context.d3d12.command_processor_user_data,
g_r.msaa_color,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
D3D12_RESOURCE_STATE_RENDER_TARGET);
}
// 2D overlay (HUD/APT): replay the frame's captured 2D draws over the
// resolved output, in submission order, with the game's own transform
// constants and textures. In gameplay these draws compose the game's
// full-screen HUD overlay texture at true screen coordinates; drawing
// them here IS the composite the (suppressed) emulated pass used to do.
uint32_t drawn_2d = 0;
if (REXCVAR_GET(skate3_native_render_scene_2d) && g_r.pso_2d != nullptr &&
g_r.ui_ring_cpu != nullptr) {
std::vector<Draw2d> scene_2d;
{
std::lock_guard<std::mutex> lock(g_2d_mutex);
scene_2d = g_scene_2d;
}
if (!scene_2d.empty()) {
list.D3DSetPipelineState(g_r.pso_2d);
// One shared draw routine for both the RTT passes and the screen pass.
// ui_region/ui_offset continue after the spline pass's allocations.
const auto emit_draw = [&](const Draw2d& d) {
const uint32_t bytes = uint32_t(d.verts.size());
if (bytes == 0 || d.stride != 28) {
return;
}
if (ui_offset + bytes > RendererState::kUiRegionSize) {
g_draws_2d_dropped.fetch_add(1, std::memory_order_relaxed);
return;
}
std::memcpy(g_r.ui_ring_cpu + ui_region + ui_offset, d.verts.data(), bytes);
const uint32_t srv_slot = resolve_2d_texture(d.fetch)->srv_slot;
float constants[40];
std::memcpy(constants, d.consts, sizeof(d.consts));
// 2D ortho draws have no translation row in the projection (c3 ==
// (0,0,0,1)); perspective view-proj rows do. Half-pixel applies to
// the former only.
const bool ortho = d.consts[12] == 0.0f && d.consts[13] == 0.0f &&
d.consts[14] == 0.0f && d.consts[15] == 1.0f;
constants[36] = ortho ? 1.0f : 0.0f;
constants[37] = 0.0f;
constants[38] = 0.0f;
constants[39] = 0.0f;
list.D3DSetGraphicsRoot32BitConstants(0, 40, constants, 0);
D3D12_GPU_DESCRIPTOR_HANDLE srv_gpu =
g_r.srv_heap->GetGPUDescriptorHandleForHeapStart();
srv_gpu.ptr += size_t(srv_slot) * g_r.srv_size;
context.d3d12.set_graphics_root_descriptor_table(
context.d3d12.command_processor_user_data, 1, srv_gpu);
D3D12_VERTEX_BUFFER_VIEW vbv{
g_r.ui_ring->GetGPUVirtualAddress() + ui_region + ui_offset, bytes, d.stride};
list.D3DIASetVertexBuffers(0, 1, &vbv);
list.D3DIASetPrimitiveTopology(d.prim == 5 ? D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP
: D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
list.D3DDrawInstanced(d.count, 1, 0, 0);
ui_offset += bytes;
++drawn_2d;
};
list.D3DOMSetRenderTargets(1, &output_rtv, FALSE, nullptr);
list.RSSetViewport(viewport);
list.RSSetScissorRect(scissor);
for (const Draw2d& d : scene_2d) {
emit_draw(d);
}
}
}
context.d3d12.push_transition_barrier(context.d3d12.command_processor_user_data,
context.d3d12.guest_output_resource,
D3D12_RESOURCE_STATE_RENDER_TARGET,
context.d3d12.guest_output_initial_state);
context.d3d12.submit_barriers(context.d3d12.command_processor_user_data);
const uint64_t frames = g_frames_rendered.fetch_add(1) + 1;
if (frames % 600 == 0) {
REXLOG_INFO(
"native-scene: frame {} items={} draws={} draws_2d={} drawn_2d={} "
"splines[{}/{}] "
"2d[other={} dropped={} textures={}] cached_meshes={} textures={} "
"vs_uploads={} palettes={} palette_base_plus1={} skinned={} skinned_skipped={} "
"rigid[pending={} dropped={} worldprops={}] "
"rej[dyn={} range={} chain={} geom={} draws={} bbox={}] "
"rr[decode_fail={} no_bones={} mesh_deferred={} tex_deferred={}]",
frames, scene.items.size(), drawn, g_draws_2d.load(), drawn_2d,
drawn_spline, g_draws_spline.load(),
g_draws_2d_other.load(), g_draws_2d_dropped.load(), g_r.textures_2d.size(),
g_r.meshes.size(), g_r.textures.size(),
g_vs_uploads.load(), g_palette_snapshots.load(), g_palette_base_plus1.load(),
g_skinned_items.load(),
g_skinned_skipped.load(), g_rigid_pending.load(), g_rigid_dropped.load(),
g_world_props.load(),
g_rej_no_dynstate.load(), g_rej_dyn_range.load(),
g_rej_chain.load(), g_rej_geom.load(), g_rej_draws.load(), g_rej_bbox.load(),
g_rr_decode_fail.load(), g_rr_no_bones.load(), g_rr_mesh_deferred.load(),
g_rr_tex_deferred.load());
}
return true;
}
} // namespace
void Install() {
// Registered even when the scene cvar starts off: RenderScene yields to the
// emulated output while disabled, and the runtime toggle (F5) can flip the
// cvar live at any point after boot.
rex::graphics::SetNativeGuestOutputRenderer(&RenderScene, nullptr);
REXLOG_INFO("native-scene: guest output renderer registered (scene {})",
SceneEnabled() ? "on" : "off");
}
} // namespace skate3::native_scene
#else // !REX_HAS_D3D12
namespace skate3::native_scene {
void Install() {}
} // namespace skate3::native_scene
#endif // REX_HAS_D3D12