From 4a2231b14deef064f00c269e5d8c3e64805ae757 Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Thu, 2 Jul 2026 03:04:35 +0100 Subject: [PATCH] diffuse textures, alpha-tested foliage, second UV set, skinned characters --- src/skate3_native_render.cpp | 27 +- src/skate3_native_scene.cpp | 941 ++++++++++++++++++++++++++++++++--- src/skate3_native_scene.h | 27 + 3 files changed, 932 insertions(+), 63 deletions(-) diff --git a/src/skate3_native_render.cpp b/src/skate3_native_render.cpp index b0408be..a863f3e 100644 --- a/src/skate3_native_render.cpp +++ b/src/skate3_native_render.cpp @@ -173,9 +173,10 @@ bool WriteMetadata(const std::filesystem::path& path, return static_cast(out); } -void OnRenderMesh(uint32_t mesh_context, uint32_t vertex_program_state) { +void OnRenderMesh(uint8_t* base, uint32_t mesh_context, uint32_t vertex_program_state) { + const uint32_t dyn = skate3::native_scene::CaptureDynamicState(base, mesh_context); std::lock_guard lock(g_mutex); - g_current_frame.push_back({0, mesh_context, vertex_program_state, 0}); + g_current_frame.push_back({0, mesh_context, vertex_program_state, dyn}); } // SceneRenderView draw-list renderer sub_827FAF50(view, sort_vec, first, count): @@ -286,7 +287,7 @@ void Install() { // r3 = MeshContext*, r4 = renderengine::VertexProgramState*. extern "C" REX_FUNC(sub_82795AD8) { if (skate3::native_render::Enabled()) { - skate3::native_render::OnRenderMesh(ctx.r3.u32, ctx.r4.u32); + skate3::native_render::OnRenderMesh(base, ctx.r3.u32, ctx.r4.u32); } __imp__sub_82795AD8(ctx, base); } @@ -310,3 +311,23 @@ extern "C" REX_FUNC(sub_82B82E08) { } __imp__sub_82B82E08(ctx, base); } + +// cProcessArenaAsset::RegisterTexture(cAssetList*, cAssetID, +// renderengine::Texture*, rw::Resource&): r4 = 64-bit asset guid, +// r5 = texture object. +extern "C" REX_FUNC(sub_82C9A618) { + if (skate3::native_render::Enabled()) { + skate3::native_scene::OnRegisterTexture(ctx.r4.u64, ctx.r5.u32); + } + __imp__sub_82C9A618(ctx, base); +} + +// D3D::SetPending_AluConstants(device, u64 dirty_group_mask, bank, ptr): +// bank 0x4000 = vertex constants. Bone palettes are staged through here +// right before the skinned RenderMesh calls that consume them. +extern "C" REX_FUNC(sub_82B83FE0) { + if (skate3::native_render::Enabled()) { + skate3::native_scene::OnVsConstantUpload(base, ctx.r4.u64, ctx.r5.u32, ctx.r6.u32); + } + __imp__sub_82B83FE0(ctx, base); +} diff --git a/src/skate3_native_scene.cpp b/src/skate3_native_scene.cpp index 0533b24..9e1e338 100644 --- a/src/skate3_native_scene.cpp +++ b/src/skate3_native_scene.cpp @@ -16,13 +16,24 @@ #if defined(REX_HAS_D3D12) && REX_HAS_D3D12 #include #include +#include +#include +#include #include +#if defined(_WIN32) +#include +#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)") .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); +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") @@ -52,6 +63,39 @@ uint64_t g_generation = 0; std::atomic g_guest_base{nullptr}; std::atomic 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 g_texture_map; + +// Bone palette snapshots. The guest uploads palettes as large VS constant +// kicks (4x3 matrices, 3 float4 rows per bone, translation at floats 3/7/11) +// right before the skinned RenderMesh calls that use them; the staging bank +// is reused so the data is copied at hook time. "Sticky": one character's +// palette serves all of its body-part contexts. +struct PaletteSnapshot { + std::vector rows; // raw staged float4 rows (LE) +}; +// Per-dynamic-entity state captured at RenderMesh hook time: the instance +// matrix (already transposed into row-vector convention) and the palette in +// effect. Both live in transient arenas/banks and are stale by frame end. +struct DynamicState { + float world[16]; + uint32_t palette_index; // +1 into g_frame_palettes, 0 = none +}; +std::mutex g_palette_mutex; +std::vector g_frame_palettes; +std::vector g_frame_dynstates; +PaletteSnapshot g_pending_palette; +uint32_t g_pending_palette_index = 0; // index+1 into g_frame_palettes, 0 = none +std::atomic g_vs_uploads{0}; +std::atomic g_vs_uploads_large{0}; +std::atomic g_palette_snapshots{0}; +std::atomic g_skinned_items{0}; +std::atomic g_skinned_skipped{0}; + bool SceneEnabled() { return REXCVAR_GET(skate3_native_render_scene); } float LoadGuestF32(uint8_t* base, uint32_t addr) { @@ -67,10 +111,12 @@ bool GuestReadableApprox(uint8_t* base, uint32_t addr) { } // Walk one MeshContext into a DrawItem. Returns false if any pointer in the -// chain is implausible. use_matrix: world geometry has absolute coordinates -// and its ctx+0x10 points into a transient per-frame matrix arena; reading -// it late picks up recycled garbage, so world items force identity. -bool BuildItem(uint8_t* base, uint32_t ctx, bool use_matrix, DrawItem& item) { +// chain is implausible. dyn: hook-time state snapshot for dynamic entities +// (instance matrix + bone palette; both live in transient arenas and are +// stale by frame end). World geometry passes null and renders with identity +// (absolute coordinates). +bool BuildItem(uint8_t* base, uint32_t ctx, const DynamicState* dyn, + const PaletteSnapshot* palette, DrawItem& item) { const uint32_t record = REX_LOAD_U32(ctx); if (!GuestReadableApprox(base, record)) return false; const uint32_t mesh = REX_LOAD_U32(record); @@ -84,33 +130,53 @@ bool BuildItem(uint8_t* base, uint32_t ctx, bool use_matrix, DrawItem& item) { return false; } - // Vertex descriptor: find the stream-0 position element. + // 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.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 && usage == 0) { + 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; - break; + } 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) return false; + item.skinned = have_bw && have_bi; item.stride = REX_LOAD_U8(vdesc + (num_elements + 1) * 16); if (item.stride == 0) return false; - // Skip the backdrop/ocean/sky class (multi-km bounds). The camera sits - // inside these volumes, their near-plane-crossing triangles rasterize as - // full-screen cover at depth ~0, and the real game renders them with - // dedicated passes. Mesh BBox = two Vector4s at +0x00/+0x10. + // 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 < 1500.0f)) { + if (!(hi - lo < 50000.0f)) { return false; } item.bbox_min[axis] = lo; @@ -143,35 +209,105 @@ bool BuildItem(uint8_t* base, uint32_t ctx, bool use_matrix, DrawItem& item) { } if (item.draws.empty()) return false; - // Payload fingerprint (FNV-1a over sampled VB/IB bytes + sizes) so the - // renderer re-decodes when streaming replaces the data at this address. + // Material channels: resolve the "diffuse" and "lightmap" texture guids to + // registered texture objects. + item.diffuse_tex = 0; + item.lightmap_tex = 0; + 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; + } + if (slot != nullptr && *slot == 0) { + const uint64_t guid = + (uint64_t(REX_LOAD_U32(chan + 0x10)) << 32) | REX_LOAD_U32(chan + 0x14); + std::lock_guard 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); - const uint32_t vb_end = item.vb_addr + item.vb_bytes - 32; - const uint32_t ib_end = item.ib_addr + item.ib_count * 2 - 32; - for (uint32_t off = 0; off < 32; off += 8) { - mix(REX_LOAD_U64(item.vb_addr + off)); - mix(REX_LOAD_U64(vb_end + off)); - mix(REX_LOAD_U64(item.ib_addr + off)); - mix(REX_LOAD_U64(ib_end + off)); + 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). - const uint32_t mtx = use_matrix ? REX_LOAD_U32(ctx + kCtxMatrix) : 0; - if (mtx != 0 && GuestReadableApprox(base, mtx)) { - for (int i = 0; i < 16; ++i) { - item.world[i] = LoadGuestF32(base, mtx + i * 4); + if (dyn != nullptr) { + std::memcpy(item.world, dyn->world, sizeof(item.world)); + if (item.skinned && palette != nullptr && !palette->rows.empty()) { + // Convert the staged 4x3 palette (3 float4 rows per bone, column-vector + // affine) into 4x4 row-vector matrices. The palette maps model space to + // world space directly, so world stays identity. + const uint32_t num_bones = uint32_t(palette->rows.size() / 12); + item.bones.resize(size_t(num_bones) * 16); + for (uint32_t b = 0; b < num_bones; ++b) { + const float* m = palette->rows.data() + size_t(b) * 12; + float* out = item.bones.data() + size_t(b) * 16; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + out[i * 4 + j] = m[j * 4 + i]; + } + out[i * 4 + 3] = 0.0f; + } + out[12] = m[3]; + out[13] = m[7]; + out[14] = m[11]; + out[15] = 1.0f; + } + std::memset(item.world, 0, sizeof(item.world)); + item.world[0] = item.world[5] = item.world[10] = item.world[15] = 1.0f; + g_skinned_items.fetch_add(1, std::memory_order_relaxed); + } else if (item.skinned) { + // A skinned mesh without its palette renders as bind-pose limbs + // sprawling from the model origin; skip it instead. + g_skinned_skipped.fetch_add(1, std::memory_order_relaxed); + return false; } } else { std::memset(item.world, 0, sizeof(item.world)); item.world[0] = item.world[5] = item.world[10] = item.world[15] = 1.0f; + item.skinned = false; } return true; } @@ -180,6 +316,97 @@ bool BuildItem(uint8_t* base, uint32_t ctx, bool use_matrix, DrawItem& item) { bool Enabled() { return SceneEnabled(); } +void OnRegisterTexture(uint64_t guid, uint32_t texture) { + if (texture == 0) { + return; + } + std::lock_guard 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) { + if (!SceneEnabled() || bank != 0x4000 || ptr == 0) { + return; + } + g_vs_uploads.fetch_add(1, std::memory_order_relaxed); + const uint32_t registers = uint32_t(std::popcount(mask)) * 4; + if (registers < 30) { + return; + } + g_vs_uploads_large.fetch_add(1, std::memory_order_relaxed); + + // ptr is the positional 256-register staging bank (partial uploads leave + // the rest of the bank intact). The bone palette is a run of consecutive + // 4x3 affine matrices (3 registers per bone: rotation rows of plausible + // scale, translation in component 3). Find the longest such run. + const auto valid_bone = [&](uint32_t reg) -> bool { + float m[12]; + for (int i = 0; i < 12; ++i) { + m[i] = LoadGuestF32(base, ptr + (reg * 4 + i) * 4); + if (!(m[i] > -1e6f && m[i] < 1e6f)) return false; + } + for (int r = 0; r < 3; ++r) { + const float n = + m[r * 4] * m[r * 4] + m[r * 4 + 1] * m[r * 4 + 1] + m[r * 4 + 2] * m[r * 4 + 2]; + if (!(n > 0.05f && n < 20.0f)) return false; + if (!(m[r * 4 + 3] > -20000.f && m[r * 4 + 3] < 20000.f)) return false; + } + return true; + }; + uint32_t best_start = 0; + uint32_t best_len = 0; + uint32_t reg = 0; + while (reg + 3 <= 256) { + if (!valid_bone(reg)) { + ++reg; + continue; + } + uint32_t run_start = reg; + uint32_t run_len = 0; + while (reg + 3 <= 256 && valid_bone(reg)) { + ++run_len; + reg += 3; + } + if (run_len > best_len) { + best_len = run_len; + best_start = run_start; + } + } + if (best_len < 8) { + return; + } + const uint32_t bones = best_len < 96 ? best_len : 96; + std::lock_guard lock(g_palette_mutex); + g_pending_palette.rows.resize(size_t(bones) * 12); + for (uint32_t i = 0; i < bones * 12; ++i) { + g_pending_palette.rows[i] = LoadGuestF32(base, ptr + (best_start * 4 + i) * 4); + } + g_pending_palette_index = 0; // not yet published + g_palette_snapshots.fetch_add(1, std::memory_order_relaxed); +} + +uint32_t CaptureDynamicState(uint8_t* base, uint32_t ctx) { + const uint32_t mtx = REX_LOAD_U32(ctx + kCtxMatrix); + if (mtx == 0 || !GuestReadableApprox(base, mtx)) { + return 0; + } + DynamicState state; + // Guest matrices store translation in column 3; transpose to row-vector. + for (int r = 0; r < 4; ++r) { + for (int c = 0; c < 4; ++c) { + state.world[r * 4 + c] = LoadGuestF32(base, mtx + (c * 4 + r) * 4); + } + } + std::lock_guard lock(g_palette_mutex); + if (!g_pending_palette.rows.empty() && g_pending_palette_index == 0) { + g_frame_palettes.push_back(g_pending_palette); + g_pending_palette_index = uint32_t(g_frame_palettes.size()); + } + state.palette_index = g_pending_palette_index; + g_frame_dynstates.push_back(state); + return uint32_t(g_frame_dynstates.size()); +} + void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) { if (!SceneEnabled() || count == 0) { return; @@ -217,26 +444,42 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) { std::unordered_set seen; for (size_t i = 0; i < count; ++i) { const SubmitRecord& r = records[i]; - // Dynamic entities (kind 0) are skinned characters/props rendered in - // bind pose without bone matrices; the skater's limbs sprawl across the - // camera. Skip until skinning is implemented; render world geometry only. - if (r.kind == 0) { - continue; - } // 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.c != view || r.b != 20160) { + if (r.kind == 1 && (r.c != view || r.b != 20160)) { continue; } if (!seen.insert(r.a).second) { continue; } + const DynamicState* dyn = nullptr; + const PaletteSnapshot* palette = nullptr; + if (r.kind == 0) { + if (r.c == 0) { + continue; // no hook-time state captured; transform unknown + } + std::lock_guard lock(g_palette_mutex); + if (r.c <= g_frame_dynstates.size()) { + dyn = &g_frame_dynstates[r.c - 1]; + if (dyn->palette_index != 0 && dyn->palette_index <= g_frame_palettes.size()) { + palette = &g_frame_palettes[dyn->palette_index - 1]; + } + } else { + continue; + } + } DrawItem item; - if (BuildItem(base, r.a, /*use_matrix=*/r.kind == 0, item)) { + if (BuildItem(base, r.a, dyn, palette, item)) { scene.items.push_back(std::move(item)); } } + { + std::lock_guard lock(g_palette_mutex); + g_frame_palettes.clear(); + g_frame_dynstates.clear(); + g_pending_palette_index = 0; + } if (scene.items.empty()) { return; } @@ -273,6 +516,7 @@ namespace { using rex::graphics::NativeGuestOutputBackend; using rex::graphics::NativeGuestOutputRenderContext; +namespace xenos = rex::graphics::xenos; struct MeshBuffers { ID3D12Resource* vb = nullptr; @@ -282,6 +526,125 @@ struct MeshBuffers { 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; + } +} + +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) != 0 || (info.Protect & PAGE_NOACCESS) != 0) { + return false; + } + p = static_cast(info.BaseAddress) + info.RegionSize; + } + return true; +#else + (void)base; + (void)addr; + (void)size; + return false; +#endif +} + struct RendererState { ID3D12Device* device = nullptr; ID3D12RootSignature* root_signature = nullptr; @@ -298,6 +661,20 @@ struct RendererState { // Buffers replaced by re-decode, kept alive until the GPU has finished the // submission that last referenced them. std::vector> 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 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; bool failed = false; bool announced = false; }; @@ -314,20 +691,59 @@ cbuffer C : register(b0) { float4 tint; float4 cam_pos; }; -struct VSOut { float4 pos : SV_Position; float3 rpos : TEXCOORD0; }; -VSOut vs_main(float3 p : POSITION) { +Texture2D diffuse : register(t0); +Texture2D lightmap : register(t1); +StructuredBuffer bones : register(t2); +SamplerState smp : register(s0); +struct VSOut { + float4 pos : SV_Position; + float3 rpos : TEXCOORD0; + float2 uv : TEXCOORD1; + float2 uv2 : TEXCOORD2; +}; +VSOut vs_main(float3 p : POSITION, float2 uv : TEXCOORD0, float2 uv2 : TEXCOORD1, + float4 bw : BLENDWEIGHT0, uint4 bi : BLENDINDICES0) { VSOut o; - o.pos = mul(float4(p, 1.0), mvp); - o.rpos = mul(float4(p, 1.0), world).xyz - cam_pos.xyz; + float4 mp = float4(p, 1.0); + // 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) { + float4 skinned = float4(0, 0, 0, 0); + // Guest blend indices are pre-multiplied by 3 (VS constant register + // index; one bone = 3 float4 registers). + [unroll] for (int k = 0; k < 4; ++k) { + skinned += bw[k] * mul(mp, bones[bi[k] / 3u]); + } + mp = float4(skinned.xyz / wsum, 1.0); + } + o.pos = mul(mp, mvp); + o.rpos = mul(mp, world).xyz - cam_pos.xyz; + o.uv = uv; + o.uv2 = uv2; return o; } float4 ps_main(VSOut i) : SV_Target { if (tint.a > 0.0) { return tint; } - float3 n = normalize(cross(ddx(i.rpos), ddy(i.rpos))); - float l = abs(dot(n, normalize(float3(0.4, 0.8, 0.3)))) * 0.6 + 0.35; - return float4(l, l, l, 1.0); + 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.r > 0.0) { + lit = albedo.rgb * lightmap.Sample(smp, i.uv2).rgb * 2.0; + } else { + float3 n = 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; + } + return float4(lit, 1.0); } )"; @@ -379,12 +795,13 @@ uint32_t SwapU32(uint32_t v) { #endif } -// Decode guest vertices into float3 positions. +// Decode guest vertices into {float3 position, float2 uv, float2 uv2, +// unorm4 blend weights, u8x4 blend indices} (36-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; - ID3D12Resource* vb = CreateUploadBuffer(device, size_t(num_verts) * 12); + ID3D12Resource* vb = CreateUploadBuffer(device, size_t(num_verts) * 36); ID3D12Resource* ib = CreateUploadBuffer(device, size_t(item.ib_count) * 2); if (!vb || !ib) { if (vb) vb->Release(); @@ -434,9 +851,50 @@ bool DecodeMesh(ID3D12Device* device, uint8_t* base, const DrawItem& item, z < item.bbox_min[2] - 2.f || z > item.bbox_max[2] + 2.f) { ++garbage; } - dst[v * 3 + 0] = x; - dst[v * 3 + 1] = y; - dst[v * 3 + 2] = z; + 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(q))); + w = HalfToFloat(SwapU16(*reinterpret_cast(q + 2))); + break; + case 38: // k_32_32_FLOAT + u = std::bit_cast(SwapU32(*reinterpret_cast(q))); + w = std::bit_cast(SwapU32(*reinterpret_cast(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(q))); + const int16_t sv = int16_t(SwapU16(*reinterpret_cast(q + 2))); + u = su / 32767.0f; + w = sv / 32767.0f; + break; + } + default: + break; + } + }; + dst[v * 9 + 0] = x; + dst[v * 9 + 1] = y; + dst[v * 9 + 2] = z; + decode_uv(item.uv_fmt, item.uv_offset, dst[v * 9 + 3], dst[v * 9 + 4]); + decode_uv(item.uv2_fmt, item.uv2_offset, dst[v * 9 + 5], dst[v * 9 + 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(src_vb + size_t(v) * item.stride + + item.bw_offset)); + bi = SwapU32(*reinterpret_cast(src_vb + size_t(v) * item.stride + + item.bi_offset)); + } + std::memcpy(&dst[v * 9 + 7], &bw, 4); + std::memcpy(&dst[v * 9 + 8], &bi, 4); } vb->Unmap(0, nullptr); if (garbage != 0) { @@ -458,25 +916,202 @@ bool DecodeMesh(ID3D12Device* device, uint8_t* base, const DrawItem& item, out.vb = vb; out.ib = ib; - out.vb_view = {vb->GetGPUVirtualAddress(), num_verts * 12u, 12u}; + out.vb_view = {vb->GetGPUVirtualAddress(), num_verts * 36u, 36u}; out.ib_view = {ib->GetGPUVirtualAddress(), item.ib_count * 2u, DXGI_FORMAT_R16_UINT}; return true; } +// Decode a guest texture (v1-verified path: fetch constant at renderengine:: +// Texture words [7..12], CPU untile block by block through the 0xA0000000 +// physical mirror, endian swap) and create its SRV in the staging heap. +bool EnsureGuestTexture(const NativeGuestOutputRenderContext& context, uint8_t* base, + uint32_t tex_ptr, GuestTexture& out) { + uint32_t words[6] = {}; + if (!GuestRangeReadable(base, tex_ptr + 7 * 4, 6 * 4)) { + return false; + } + for (uint32_t i = 0; i < 6; ++i) { + words[i] = REX_LOAD_U32(tex_ptr + (7 + i) * 4); + } + std::memcpy(out.fetch_words, words, sizeof(words)); + + 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; + } + const uint32_t guest_data_address = 0xA0000000u | info.memory.base_address; + 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 || + !GuestRangeReadable(base, guest_data_address, info.memory.base_size)) { + 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_columns = + (width + format_info->block_width - 1) / format_info->block_width; + const uint32_t block_rows = + (height + format_info->block_height - 1) / format_info->block_height; + const uint32_t pitch_blocks = info.extent.block_pitch_h; + const uint32_t host_width = block_columns * format_info->block_width; + const uint32_t host_height = block_rows * format_info->block_height; + + 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 = 1; + 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; + } + const uint32_t row_bytes = block_columns * bytes_per_block; + const uint32_t upload_pitch = (row_bytes + (D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u)) & + ~(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1u); + out.upload = CreateUploadBuffer(device, size_t(upload_pitch) * block_rows); + if (!out.upload) { + out.texture->Release(); + out.texture = nullptr; + return false; + } + uint8_t* mapping = nullptr; + out.upload->Map(0, nullptr, reinterpret_cast(&mapping)); + const uint8_t* guest = base + guest_data_address; + const bool swap_rb_565 = + rex::graphics::GetBaseFormat(info.format) == xenos::TextureFormat::k_5_6_5; + for (uint32_t by = 0; by < block_rows; ++by) { + uint8_t* out_row = mapping + size_t(by) * upload_pitch; + for (uint32_t bx = 0; bx < block_columns; ++bx) { + uint32_t source_offset; + if (info.is_tiled) { + source_offset = uint32_t(rex::graphics::texture_util::GetTiledOffset2D( + int32_t(bx), int32_t(by), pitch_blocks, bytes_per_block_log2)); + } else { + source_offset = (by * pitch_blocks + bx) * bytes_per_block; + } + if (source_offset + bytes_per_block > info.memory.base_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 copy into the deferred command list. + auto* command_processor = context.d3d12.command_processor; + auto& list = command_processor->GetDeferredCommandList(); + D3D12_TEXTURE_COPY_LOCATION dst{}; + dst.pResource = out.texture; + dst.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; + D3D12_TEXTURE_COPY_LOCATION src{}; + src.pResource = out.upload; + src.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; + src.PlacedFootprint.Footprint.Format = host.resource_format; + src.PlacedFootprint.Footprint.Width = host_width; + src.PlacedFootprint.Footprint.Height = host_height; + src.PlacedFootprint.Footprint.Depth = 1; + src.PlacedFootprint.Footprint.RowPitch = upload_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 = 1; + 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; +} + 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 param{}; - param.ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS; - param.Constants.ShaderRegister = 0; - param.Constants.Num32BitValues = 40; - param.ShaderVisibility = D3D12_SHADER_VISIBILITY_ALL; + D3D12_ROOT_PARAMETER params[4] = {}; + params[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS; + params[0].Constants.ShaderRegister = 0; + params[0].Constants.Num32BitValues = 40; + 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 sampler{}; + sampler.Filter = D3D12_FILTER_MIN_MAG_LINEAR_MIP_POINT; + sampler.AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP; + sampler.AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP; + sampler.AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP; + sampler.MaxLOD = D3D12_FLOAT32_MAX; + sampler.ShaderRegister = 0; + sampler.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL; D3D12_ROOT_SIGNATURE_DESC desc{}; - desc.NumParameters = 1; - desc.pParameters = ¶m; + desc.NumParameters = 4; + desc.pParameters = params; + desc.NumStaticSamplers = 1; + desc.pStaticSamplers = &sampler; 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)) { @@ -499,9 +1134,17 @@ bool EnsurePipeline(const NativeGuestOutputRenderContext& context) { g_r.failed = true; return false; } - D3D12_INPUT_ELEMENT_DESC input{ - "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, - D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0}; + D3D12_INPUT_ELEMENT_DESC input[5] = { + {"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}}; D3D12_GRAPHICS_PIPELINE_STATE_DESC pso{}; pso.pRootSignature = g_r.root_signature; pso.VS = {vs->GetBufferPointer(), vs->GetBufferSize()}; @@ -514,7 +1157,7 @@ bool EnsurePipeline(const NativeGuestOutputRenderContext& context) { pso.DepthStencilState.DepthEnable = TRUE; pso.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL; pso.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_LESS; - pso.InputLayout = {&input, 1}; + pso.InputLayout = {input, 5}; pso.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; pso.NumRenderTargets = 1; pso.RTVFormats[0] = context.d3d12.guest_output_format; @@ -549,6 +1192,82 @@ bool EnsurePipeline(const NativeGuestOutputRenderContext& context) { 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(&g_r.bone_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(&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; @@ -613,6 +1332,8 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat 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; @@ -666,8 +1387,18 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat 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); const int32_t debug_mode = REXCVAR_GET(skate3_native_render_scene_debug); + // 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; for (const DrawItem& item : scene.items) { @@ -696,7 +1427,62 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat } 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. + 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()) { + 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; + } + // 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. + // tint.a > 0 selects debug solid colors; tint.r > 0 marks a bound + // lightmap in the normal path. float constants[40]; std::memcpy(constants, item.world, sizeof(item.world)); float* mvp = constants + 16; @@ -709,19 +1495,50 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat 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) { + list.D3DSetGraphicsRootShaderResourceView(3, g_r.bone_ring->GetGPUVirtualAddress()); + } + if (debug_mode >= 2) { constants[32] = float((index * 37u) % 255u) / 255.0f; constants[33] = float((index * 73u) % 255u) / 255.0f; constants[34] = float((index * 151u) % 255u) / 255.0f; constants[35] = 1.0f; } else { - constants[32] = constants[33] = constants[34] = constants[35] = 0.0f; + constants[32] = lightmap != nullptr ? 1.0f : 0.0f; + constants[33] = bones_bound ? 1.0f : 0.0f; + constants[34] = 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; list.D3DSetGraphicsRoot32BitConstants(0, 40, 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) { @@ -746,8 +1563,12 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat const uint64_t frames = g_frames_rendered.fetch_add(1) + 1; if (frames % 600 == 0) { - REXLOG_INFO("native-scene: frame {} items={} draws={} cached_meshes={}", frames, - scene.items.size(), drawn, g_r.meshes.size()); + REXLOG_INFO( + "native-scene: frame {} items={} draws={} cached_meshes={} textures={} " + "vs_uploads={} large={} palettes={} skinned={} skinned_skipped={}", + frames, scene.items.size(), drawn, g_r.meshes.size(), g_r.textures.size(), + g_vs_uploads.load(), g_vs_uploads_large.load(), g_palette_snapshots.load(), + g_skinned_items.load(), g_skinned_skipped.load()); } return true; } diff --git a/src/skate3_native_scene.h b/src/skate3_native_scene.h index c2bf69e..23cbd9d 100644 --- a/src/skate3_native_scene.h +++ b/src/skate3_native_scene.h @@ -25,9 +25,21 @@ struct DrawItem { uint32_t ib_addr; // guest address of raw index data (u16 big-endian) uint32_t vb_bytes; uint32_t ib_count; + uint32_t diffuse_tex; // guest renderengine::Texture address (0 = none) + uint32_t lightmap_tex; // guest renderengine::Texture address (0 = none) uint16_t pos_offset; + uint16_t uv_offset; + uint16_t uv2_offset; + uint16_t bw_offset; // blend weights (u8x4), valid when skinned + uint16_t bi_offset; // blend indices (u8x4), valid when skinned uint8_t stride; uint8_t pos_fmt; // xenos vertex format (26 s16x4, 32 half4, 57 float3) + uint8_t uv_fmt; // xenos vertex format of the first texcoord (0 = none) + uint8_t uv2_fmt; // xenos vertex format of the second texcoord (0 = none) + bool skinned; + // Bone palette snapshot (row-vector 4x4 matrices) taken on the game thread + // - the guest bone array lives in a transient per-frame arena. + std::vector bones; // Content fingerprint of the guest payloads. Streaming reuses arena // addresses and pages fill in after the mesh is first submitted, so cached // GPU copies must be revalidated against this every frame. @@ -55,6 +67,21 @@ struct SubmitRecord { bool Enabled(); +// Called from the cProcessArenaAsset::RegisterTexture hook: guid -> guest +// renderengine::Texture object. +void OnRegisterTexture(uint64_t guid, uint32_t texture); + +// Called from the D3D::SetPending_AluConstants hook (guest render thread). +// Large vertex-bank uploads are bone palettes staged for the next skinned +// RenderMesh; the staging bank is reused, so contents are snapshotted here. +void OnVsConstantUpload(uint8_t* base, uint64_t mask, uint32_t bank, uint32_t ptr); + +// Called from the RenderMesh hook for dynamic entities. Snapshots the +// context's instance matrix (ctx+0x10 points into a transient per-frame +// arena; it must be read now, not at frame end) and the pending bone +// palette. Returns a dynamic-state index (+1), 0 on failure. +uint32_t CaptureDynamicState(uint8_t* base, uint32_t ctx); + // Called on the guest render thread at frame end with the frame's records. void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count);