render: peel the post-process passes into their own translation unit

This commit is contained in:
mchughalex 2026-07-19 10:28:14 +01:00
parent e591f76f21
commit 13669fe589
4 changed files with 2246 additions and 2079 deletions

View file

@ -236,6 +236,7 @@ set(SKATE3_COMMON_SOURCES
src/skate3_native_render.cpp src/skate3_native_render.cpp
src/skate3_native_scene.cpp src/skate3_native_scene.cpp
src/skate3_native_scene_gpu.cpp src/skate3_native_scene_gpu.cpp
src/skate3_native_scene_post.cpp
src/skate3_screenshot.cpp src/skate3_screenshot.cpp
src/skate3_shader_disasm.cpp src/skate3_shader_disasm.cpp
src/skate3_title_update_installer.cpp src/skate3_title_update_installer.cpp

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,897 @@
#pragma once
// GPU-side internals of the native scene renderer, shared by its render
// translation units: skate3_native_scene_gpu.cpp (scene draw + presenter) and
// skate3_native_scene_post.cpp (SSAO/SSR/HDR/menu-blur post passes). Holds
// the RendererState (all GPU-resident resources) and the pipeline-builder
// declarations. Render thread only.
#include "skate3_native_scene_state.h"
#if (defined(REX_HAS_D3D12) && REX_HAS_D3D12) || (defined(REX_HAS_VULKAN) && REX_HAS_VULKAN)
#include <rex/graphics/native_guest_renderer.h>
#include <rex/graphics/native_rhi.h>
#include <rex/graphics/pipeline/texture/info.h>
#include <rex/graphics/pipeline/texture/util.h>
#include <rex/graphics/xenos.h>
namespace skate3::native_scene {
using rex::graphics::NativeGuestOutputBackend;
using rex::graphics::NativeGuestOutputRenderContext;
namespace xenos = rex::graphics::xenos;
namespace nrhi = rex::graphics::nrhi;
// App-local vertex/index binding records (replacing D3D12_VERTEX_BUFFER_VIEW /
// D3D12_INDEX_BUFFER_VIEW): the RHI binds (buffer, offset) directly, no GPU
// virtual addresses.
struct VbBinding {
nrhi::Buffer* buffer = nullptr;
uint64_t offset = 0;
uint32_t size_bytes = 0;
uint32_t stride = 0;
};
struct IbBinding {
nrhi::Buffer* buffer = nullptr;
uint64_t offset = 0;
uint32_t size_bytes = 0;
};
struct MeshBuffers {
nrhi::Buffer* vb = nullptr;
nrhi::Buffer* ib = nullptr;
VbBinding vb_view{};
IbBinding ib_view{};
uint64_t fingerprint = 0;
// Dynamic-payload decode order (DynDecodeJob::seq): the commit drops
// results older than the cached entry so multi-worker reordering cannot
// step the cloth backwards a frame. 0 on static decodes.
uint64_t dyn_seq = 0;
// Double-sided sheet prop (banners/flags): most triangles have an
// opposite-winding twin ~1cm behind, and the two faces map to DIFFERENT
// lightmap atlas cells (lit vs shaded side). Drawn without culling both
// faces z-fight once their depth gap drops below buffer precision;
// distant triangles alternate between the two cells per frame (the "flag
// flicker", stops up close where 1cm still resolves). These meshes draw
// with the backface-culling PSO instead.
bool two_sided_sheet = false;
// ROPA only: the decoded vertex array (num_verts x 14 floats, the scene
// VS layout) retained for draw-time shape blending onto the play clock.
std::vector<float> ropa_verts;
};
struct GuestTexture {
nrhi::Texture* texture = nullptr;
nrhi::Buffer* upload = nullptr; // kept alive; copy recorded in deferred list
// In-place re-decode ping-pong partner (UpdateGuestTexture2DInPlace): the
// GPU may still be copying from the OTHER upload buffer for the previous
// frame's content change, so hot content alternates buffers instead of
// paying two buffer creations per change.
nrhi::Buffer* upload_b = nullptr;
bool upload_flip = false;
uint32_t fetch_words[6] = {}; // big-endian words as read for revalidation
nrhi::TextureView* srv = nullptr;
// 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;
// Payload-content revalidation: a texture first seen while its payload is
// still STREAMING IN decodes to garbage (blocky macro-tile checkers /
// blacked-out walls), and the fetch words never change when the content
// finishes filling in at the same address, so the garbage decode was
// cached forever. Sampled qwords across mip 0, rechecked periodically.
uint32_t payload_addr = 0; // 0xA-mirror guest address of mip 0
uint32_t payload_size = 0;
uint64_t payload_fp = 0;
uint64_t recheck_frame = 0;
// Consecutive failed decodes of this entry (payload still streaming in):
// drives the escalating retry backoff at commit; the first failure
// retries fast (the payload usually lands within a few frames; a fixed
// +120 held freshly streamed textures white for ~half a second), repeat
// failures back off toward the old cadence.
uint8_t fail_count = 0;
// Successful payload rechecks so far: fresh entries re-verify fast
// (2/4/8 frames; a mid-stream garbage decode otherwise stays on screen
// up to the full recheck interval) before settling at the 16-frame
// steady-state cadence.
uint8_t recheck_count = 0;
// Tiled-aware payload probes: absolute 0xA-mirror addresses of up to 16
// of this texture's OWN mip-0 blocks (a 4x4 spread over the block grid,
// resolved through the tiled address swizzle). The old contiguous
// [payload_addr, payload_size) fingerprint was WRONG for streamed world
// textures: they are pitch-packed into shared mip pools and tiled across
// padded macro rows, so the range interleaved NEIGHBOR textures' bytes;
// pool churn kept the fingerprint flapping, the commit-time stability
// verify then rejected every legitimate heal, and freshly promoted mips
// stayed stale/low-res for seconds (the medium-distance texture pop-in;
// one promote re-logged 8x while its heals were refused).
// probe_count == 0 falls back to the range fingerprint (cubes).
// 8x8 = 64 probes: the first 4x4 = 16 grid was too sparse; a mid-stream
// decode whose 16 probed blocks happened to be final (other regions still
// garbage) COMMITTED and never healed, visible as a black/garbage decal
// until the next words change replaced it ("decal flash and replace").
uint32_t probe_addr[64] = {};
uint8_t probe_count = 0;
// SRV recipe remnants (2D textures): srv_format is the revalidation
// compare key and srv_mips gates the single-mip in-place update path.
// The paired t4/t5 descriptor re-creation this recipe once fed is gone;
// pairs bind their two views directly via SetTexturePair (descriptor
// management is backend-internal now).
nrhi::Format srv_format = nrhi::Format::kUnknown;
uint32_t srv_mips = 0;
bool valid = false;
// Store LRU clock: last frame this entry was served/touched. Superseded
// words states (old mip levels, pre-demote detail sets) age out once
// nothing routes to them.
uint64_t last_used_frame = 0;
// A tiled mip's padded macro-row copy faulted and fell back to the
// reported size; blocks beyond it uploaded as ZERO (the half-black
// banner mip: tiled addressing puts the image's bottom rows past
// min_size). The entry serves (better than white) but keeps re-decoding
// until a complete copy lands; the commit prefers complete decodes.
bool incomplete = false;
// Decode-time SampleProbeNearBlack verdict: the payload was near-uniform
// black when this decode was taken (a lightmap page mid-compose). The
// lightmap-slot resolve serves the white fallback instead (tint.r == 0 ->
// the unshadowed-bright window, 59810af semantics) until a heal lands
// real content; other slots ignore it (black diffuse content is legal).
bool near_black = false;
// Same-content confirmations of a near-black verdict (commit dedups of
// forced re-decodes). The forcing covers ONE race, a decode reading a
// page mid-compose while the fingerprint sampled the composed result,
// so a few confirmations prove the content is genuinely uniform and the
// forcing stops (the plain fp poll still heals a later in-place compose).
// Unbounded forcing re-decoded permanently-uniform textures every poll
// forever, each a discarded worker round trip.
uint8_t nb_redecodes = 0;
// Frame of the last decode that landed CHANGED content for this key
// (stamped by the words-key commit and the inline 2D decode). Classifies
// video-plane content changes as mid-playback (recent change -> serve the
// stale frame while the worker re-decodes, keeps 30 fps cadence) vs a
// playback-START edge (quiet for seconds -> the entry holds the PREVIOUS
// video's last frame; serving it flashed old content at every video
// boundary; hold the quad black until fresh content commits).
uint64_t last_change_frame = 0;
};
// THE texture identity key: FNV-1a
// over the six fetch-constant words, console identity semantics for the
// content store. Object resolves, draw-time word bindings, 2D/HUD art, and
// the decode workers all key the same g_r.tex_store entries with it.
inline uint64_t FetchWordsKey(const uint32_t words[6]) {
uint64_t key = 1469598103934665603ull;
for (int k = 0; k < 6; ++k) {
// Clamp modes (dword_0 bits 10-18, clamp_x/y/z) are SAMPLER state, not
// content identity; the same texture object binds with different clamp
// bits per draw (observed: one texture decoded twice under 01004802 vs
// 01024802, bit 17), and keying on them decodes/stores every variant
// separately. The store holds textures + SRVs only; samplers are
// static in the replay pipelines, so clamp-variant entries are exact
// duplicates. Sign bits stay: they select the host SRV format.
key ^= k == 0 ? (words[k] & ~0x0007FC00u) : words[k];
key *= 1099511628211ull;
}
return key;
}
// Base-level pixel area from stored fetch words (word 2 packs width-1 /
// height-1 in 13-bit fields), the material-detail downgrade compare key.
inline uint64_t FetchWordsArea(const uint32_t words[6]) {
return uint64_t((words[2] & 0x1FFFu) + 1u) *
uint64_t(((words[2] >> 13) & 0x1FFFu) + 1u);
}
// Escalating retry backoff (native frames) for failed texture decodes: the
// payload is usually mid-stream and lands within a few frames; the first
// retry is fast (a fixed +120 held freshly streamed textures white for
// ~half a second); repeated failures back off toward the old cadence.
inline uint64_t RetryBackoff(uint8_t fails) {
const uint32_t n = fails > 0 ? uint32_t(fails) - 1 : 0u;
return std::min<uint64_t>(120, 8ull << std::min(n, 4u));
}
inline uint8_t BumpFail(uint8_t v) { return v < 250 ? uint8_t(v + 1) : v; }
// FNV-1a guest-payload fingerprint (SEH-guarded reads; streaming can
// decommit the range). Returns 0 only on unreadable payloads.
// Payloads up to 64 KB hash FULLY: the runtime-composed lightmap atlas
// pages (32 KB DXT1) receive REGIONAL in-place writes as the game composes
// late-streamed cells into a page we already decoded; the old 16-qword
// probe missed writes that fell between its samples, so the revalidation
// pass never re-decoded and the affected cells served the half-composed
// first decode forever (the 2x-bright canopy / dark awning reflective_trans
// glass: one texture, early-composed cells correct, late cells
// stale). Larger payloads (streamed assets whose fetch words change when
// content moves) keep a strided sample, densified 16 -> 64 qwords.
inline uint64_t SamplePayloadFingerprint(uint8_t* base, uint32_t addr, uint32_t size) {
if (addr == 0 || size < 8) {
return 0;
}
uint64_t h = 1469598103934665603ull;
if (size <= 65536) {
uint64_t buf[512];
for (uint32_t off = 0; off < size; off += sizeof(buf)) {
const uint32_t n = std::min<uint32_t>(sizeof(buf), (size - off) & ~7u);
if (n == 0) {
break;
}
if (!GuestTryCopy(buf, base + addr + off, n)) {
return 0;
}
for (uint32_t k = 0; k < n / 8; ++k) {
h = (h ^ buf[k]) * 1099511628211ull;
}
}
return h;
}
for (uint32_t k = 0; k < 64; ++k) {
const uint32_t off = uint32_t(uint64_t(size - 8) * k / 63u) & ~7u;
uint64_t v = 0;
if (!GuestTryCopy(&v, base + addr + off, sizeof(v))) {
return 0;
}
h = (h ^ v) * 1099511628211ull;
}
return h;
}
// Heal-pipeline telemetry (see the 600-frame stats line): verify-fail =
// commit-time stability rejections, demote-hold = mip-0 demotes served
// from the cached full-chain decode without a doomed re-decode.
inline std::atomic<uint64_t> g_heal_verify_fail{0};
inline std::atomic<uint64_t> g_demote_hold{0};
// Sticky/skip serving (the anti-white-flash layer): sticky = draws served
// with the item's previous texture while a fresh object decodes; skipnew =
// first-sight draws suppressed entirely for the in-flight window.
inline std::atomic<uint64_t> g_tex_sticky_served{0};
inline std::atomic<uint64_t> g_skip_new{0};
// Per-mesh pipeline trace (skate3_native_render_scene_trace_mesh). Render
// thread only: the traced mesh address (parsed once per frame), the traced
// mesh's current store keys (so worker-commit events can be matched), and
// the last logged per-ctx state signature (summaries log on change).
inline uint32_t g_trace_mesh_addr = 0;
inline std::unordered_set<uint64_t> g_trace_keys;
inline std::unordered_map<uint32_t, uint64_t> g_trace_sig;
// Words-keyed (event-ad / streamed-artwork) serving: stale = site served
// its previous art while a new-words decode is in flight; none = nothing
// decoded for the site yet (caller shows the baked placeholder).
inline std::atomic<uint64_t> g_ad_stale_served{0};
inline std::atomic<uint64_t> g_ad_placeholder{0};
// Fill GuestTexture::probe_addr with up to 16 of the texture's own mip-0
// blocks: a 4x4 spread over the block grid, each resolved through the same
// tiled/linear addressing the decode uses, clamped to the guarded copy's
// readable range. Probes crossing into the padded macro-row tail are skipped
// rather than clamped; the tail belongs to pool neighbors.
inline void BuildPayloadProbes(const rex::graphics::TextureInfo& info, uint32_t mip0_addr,
uint32_t ox, uint32_t oy, uint32_t pitch_blocks,
uint32_t copy_size, GuestTexture& out) {
out.probe_count = 0;
const rex::graphics::FormatInfo* fi = info.format_info();
const uint32_t bpb = fi->bytes_per_block();
if (mip0_addr == 0 || bpb == 0 || (bpb & (bpb - 1)) != 0) {
return;
}
const uint32_t bpb_log2 = uint32_t(std::countr_zero(bpb));
const uint32_t width = info.width + 1u;
const uint32_t height = info.height + 1u;
const uint32_t cols = (width + fi->block_width - 1) / fi->block_width;
const uint32_t rows = (height + fi->block_height - 1) / fi->block_height;
for (uint32_t gy = 0; gy < 8; ++gy) {
for (uint32_t gx = 0; gx < 8; ++gx) {
const uint32_t bx = cols > 1 ? gx * (cols - 1) / 7 : 0;
const uint32_t by = rows > 1 ? gy * (rows - 1) / 7 : 0;
uint32_t off;
if (info.is_tiled) {
off = uint32_t(rex::graphics::texture_util::GetTiledOffset2D(
int32_t(bx + ox), int32_t(by + oy), pitch_blocks, bpb_log2));
} else {
off = ((by + oy) * pitch_blocks + bx + ox) * bpb;
}
if (off + bpb > copy_size) {
continue;
}
out.probe_addr[out.probe_count++] = (0xA0000000u | mip0_addr) + off;
}
}
}
// Probe-based payload fingerprint: hashes one block-leading qword (or the
// whole block for narrow formats) at each probe address. Returns 0 only when
// a probe is unreadable: same semantics as the range fingerprint. Falls
// back to the range hash when no probes were built (cube maps).
inline uint64_t SampleProbeFingerprint(uint8_t* base, const GuestTexture& t) {
if (t.probe_count == 0) {
return SamplePayloadFingerprint(base, t.payload_addr, t.payload_size);
}
uint64_t h = 1469598103934665603ull;
for (uint32_t i = 0; i < t.probe_count; ++i) {
uint64_t v = 0;
if (!GuestTryCopy(&v, base + t.probe_addr[i], sizeof(v))) {
return 0;
}
h = (h ^ v) * 1099511628211ull;
}
return h;
}
// Near-UNIFORM payload detector, sampled over the same probe grid at decode
// time. A COMPOSED lightmap page or weathering overlay is never one flat
// value across its whole mip-0 (a daylight bake / grime map atlases many
// surfaces), but a page decoded mid-compose is uniform fill: zeroed OR a
// flat grey (the reported "black/grey squares": a zero-only detector
// is structurally blind to grey). A real-but-garbage lightmap binds with
// tint.r > 0 so the CSM min-clamp renders BLACK (the door 59810af's
// white-fallback shader gate cannot see), and since 59810af a garbage macro
// multiplies OVER the decal paint. Consumers act on this only for the
// WHITE-NEUTRAL slots (lightmap/macro); uniform diffuse/spec content is
// legal and unaffected.
inline bool SampleProbeNearBlack(uint8_t* base, const GuestTexture& t) {
if (t.probe_count < 16) {
return false; // range-fallback entries (cubes): not enough coverage
}
uint64_t a = 0, b = 0;
bool have_a = false, have_b = false;
for (uint32_t i = 0; i < t.probe_count; ++i) {
uint64_t v = 0;
if (!GuestTryCopy(&v, base + t.probe_addr[i], sizeof(v))) {
return false;
}
if (!have_a) {
a = v;
have_a = true;
} else if (v != a && !have_b) {
b = v;
have_b = true;
} else if (v != a && v != b) {
return false; // 3+ distinct block values = real composed content
}
}
return true; // <= 2 distinct block values across the whole probe grid
}
// 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 {
nrhi::Format resource_format = nrhi::Format::kUnknown;
nrhi::Format srv_format = nrhi::Format::kUnknown;
uint32_t host_swizzle = xenos::XE_GPU_TEXTURE_SWIZZLE_RGBA;
};
inline bool GetHostTextureFormat(xenos::TextureFormat format, HostTextureFormat& out) {
switch (rex::graphics::GetBaseFormat(format)) {
case xenos::TextureFormat::k_DXT1:
out = {nrhi::Format::kBC1_UNORM, nrhi::Format::kBC1_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RGBA};
return true;
case xenos::TextureFormat::k_DXT2_3:
out = {nrhi::Format::kBC2_UNORM, nrhi::Format::kBC2_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RGBA};
return true;
case xenos::TextureFormat::k_DXT4_5:
out = {nrhi::Format::kBC3_UNORM, nrhi::Format::kBC3_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RGBA};
return true;
case xenos::TextureFormat::k_DXT5A:
out = {nrhi::Format::kBC4_UNORM, nrhi::Format::kBC4_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RRRR};
return true;
case xenos::TextureFormat::k_DXN:
out = {nrhi::Format::kBC5_UNORM, nrhi::Format::kBC5_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RGGG};
return true;
case xenos::TextureFormat::k_8_8_8_8:
out = {nrhi::Format::kR8G8B8A8_UNORM, nrhi::Format::kR8G8B8A8_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RGBA};
return true;
case xenos::TextureFormat::k_8:
out = {nrhi::Format::kR8_UNORM, nrhi::Format::kR8_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RRRR};
return true;
case xenos::TextureFormat::k_8_8:
out = {nrhi::Format::kR8G8_UNORM, nrhi::Format::kR8G8_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RGGG};
return true;
case xenos::TextureFormat::k_5_6_5:
// Red/blue swapped CPU-side while uploading.
out = {nrhi::Format::kB5G6R5_UNORM, nrhi::Format::kB5G6R5_UNORM,
xenos::XE_GPU_TEXTURE_SWIZZLE_RGBB};
return true;
default:
return false;
}
}
// Compose the fetch-constant swizzle over the host-format component remap
// into a per-channel view swizzle (Xenos 0..5 = X,Y,Z,W,0,1, the same
// order as nrhi::Swizzle).
inline void ComposeSrvSwizzle(uint32_t fetch_swizzle, uint32_t host_swizzle,
nrhi::Swizzle out[4]) {
for (uint32_t c = 0; c < 4; ++c) {
const uint32_t guest = (fetch_swizzle >> (3 * c)) & 7u;
const uint32_t v = guest >= 4 ? guest : ((host_swizzle >> (3 * guest)) & 7u);
out[c] = nrhi::Swizzle(v);
}
}
inline 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 {
nrhi::Device* device = nullptr;
nrhi::BindingLayout* layout = nullptr; // main scene binding layout (was the root signature)
nrhi::Pipeline* pso = nullptr;
nrhi::Pipeline* pso_cullback = nullptr; // two_sided_sheet meshes (see MeshBuffers)
nrhi::Pipeline* pso_nodepth = nullptr;
// environment.transparent sub-pass: straight alpha blend, depth test on,
// z-write OFF; items drawn back-to-front after all opaque items.
nrhi::Pipeline* pso_transparent = nullptr;
// Entity-fade variant of the transparent PSO: same straight alpha blend
// but z-write ON. A fading character/vehicle is a solid object at partial
// opacity; z-write-off blending composites every overlapping piece (skin
// under clothes, far-side doors/wheels through the body shell) into an
// x-ray. With depth writes the nearest surface wins and each pixel blends
// once, matching the game's main-pass fade.
nrhi::Pipeline* pso_fade = nullptr;
// Hair sub-passes: transparent blend state with cull BACK / cull FRONT
// (the game's cac_hair/defaulthair two-pass draw order).
nrhi::Pipeline* pso_hair_a = nullptr;
nrhi::Pipeline* pso_hair_b = nullptr;
nrhi::Format rtv_format = nrhi::Format::kUnknown;
nrhi::Texture* depth = nullptr;
uint32_t depth_width = 0;
uint32_t depth_height = 0;
// Cached guest-output texture identity (context.guest_output) for change
// detection: the presenter recreates the output image on resize.
nrhi::Texture* 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 RHI command
// stream has no resolve copy).
uint32_t msaa = 1;
nrhi::Texture* msaa_color = nullptr;
nrhi::Pipeline* resolve_pso = nullptr;
nrhi::TextureView* msaa_srv_slot = nullptr;
bool msaa_srv_allocated = false;
// Popup background blur (see kBlurShaderSource): two intermediates at the
// game's fixed 1152x640 internal resolution + a view of the guest output
// (re-created when the output texture changes). Steady state for
// blur_tex is RENDER_TARGET.
static constexpr uint32_t kBlurWidth = 1152;
static constexpr uint32_t kBlurHeight = 640;
nrhi::Texture* blur_tex[2] = {nullptr, nullptr};
nrhi::TextureView* blur_srv[2] = {nullptr, nullptr};
nrhi::TextureView* output_srv_slot = nullptr;
bool output_srv_allocated = false;
nrhi::Pipeline* pso_blur = nullptr;
nrhi::Pipeline* pso_blur_blit = nullptr;
nrhi::Pipeline* pso_blur_down = nullptr;
// Settings-menu gaussian backdrop (ps_menu_gauss): clean separable
// gaussian on half-output-res ping-pong intermediates, applied at the END
// of the compose so it frosts everything drawn (3D + HUD/2D + FMV).
// Deliberately independent of the popup-blur port above (that chain
// reproduces the console's 1152x640 lattice on purpose). Steady state
// RENDER_TARGET; recreated on output size change.
nrhi::Pipeline* pso_menu_gauss = nullptr;
nrhi::Texture* menu_blur_tex[2] = {nullptr, nullptr};
nrhi::TextureView* menu_blur_srv[2] = {nullptr, nullptr};
uint32_t menu_blur_w = 0;
uint32_t menu_blur_h = 0;
// Native photo grab (photo_grab_native): the blur downsample pass (whose
// 1152x640 blur space happens to BE the game's screenshot-target raster)
// renders the finished frame into blur_tex[0], which is then copied into
// one of these persistently-mapped READBACK buffers; the next armed frame
// CPU-tiles the completed one big-endian into the guest grab target. Two
// buffers so no frame ever waits on the GPU (a full kFull-style drain per
// frame was the old path's stall). Row pitch 1152*4 = 4608 is already
// 256-aligned.
static constexpr uint32_t kGrabRowPitch = kBlurWidth * 4;
nrhi::Buffer* grab_readback[2] = {nullptr, nullptr};
uint8_t* grab_readback_ptr[2] = {nullptr, nullptr};
uint64_t grab_submission[2] = {0, 0};
bool grab_pending[2] = {false, false};
uint32_t grab_write_index = 0;
bool grab_failed = false;
// Window stats for the close log: frames written to guest + CPU tile time.
uint32_t grab_writes = 0;
uint64_t grab_cpu_us = 0;
// Native FMV: overlay-2D pipeline variant whose PS combines the movie
// player's three CPU-filled YUV plane textures (ps_yuv2d; substituted for
// the captured movie quad in the 2D replay, see OnMovieFrame).
nrhi::Pipeline* pso_yuv2d = nullptr;
// Selection outline (see DrawItem::selected): selected items re-render
// into a single-sample R8 mask at OUTPUT resolution (a low-res mask
// stairstepped the contour centerline at 4K) and a fullscreen edge-detect
// pass with fixed UV-fraction tap pitch adds the blue outline onto the
// resolved output (postfx_edgedetectstencil equivalent). Mask steady
// state is RENDER_TARGET.
nrhi::Texture* outline_mask = nullptr;
uint32_t outline_mask_width = 0;
uint32_t outline_mask_height = 0;
nrhi::TextureView* outline_mask_srv = nullptr;
bool outline_mask_srv_allocated = false;
nrhi::Pipeline* pso_outline_mask = nullptr;
nrhi::Pipeline* pso_outline_edge = nullptr;
// Photo-editor postfx chain (photo_fx.hlsl: exact ucode ports, see
// FrameScene::PhotoFx). Own binding layout: CBV b0 (the pass's 256
// captured/baked constant rows) + eight single-texture tables t0..t7 +
// three static samplers. Intermediates at the game's exact half/quarter
// sizes (the DOF tap offsets are baked for them); the full-res stages run
// at output resolution. pfx_quarter persists across frames (accumulation
// input). All pfx color targets idle in RENDER_TARGET state.
static constexpr uint32_t kPfxHalfW = 576, kPfxHalfH = 320;
static constexpr uint32_t kPfxQuarterW = 288, kPfxQuarterH = 160;
nrhi::BindingLayout* pfx_layout = nullptr;
// PSO order: depthpack, visualfx, dof_down, dof_mb, dof, uber, fisheye, blit.
nrhi::Pipeline* pfx_pso[9] = {};
nrhi::Texture* pfx_full[2] = {}; // output-res RGBA8 (visualfx out, uber out)
nrhi::Texture* pfx_half[2] = {}; // 576x320 RGBA8
nrhi::Texture* pfx_quarter = nullptr; // 288x160 RGBA8 accumulation
nrhi::Texture* pfx_depth = nullptr; // output-res packed-depth RGBA8
nrhi::Texture* pfx_lut = nullptr; // 32^3 identity grade LUT (RGBA8)
nrhi::Buffer* pfx_lut_upload = nullptr;
bool pfx_lut_uploaded = false;
nrhi::Buffer* pfx_cb = nullptr; // upload heap, persistently mapped
uint8_t* pfx_cb_ptr = nullptr;
// Fixed SRV views: 0/1 = pfx_full, 2/3 = pfx_half, 4 = quarter, 5 =
// packed depth, 6 = LUT, 7 = the native MSAA/1x depth resource.
nrhi::TextureView* pfx_srv[8] = {};
bool pfx_srv_allocated = false;
uint32_t pfx_width = 0, pfx_height = 0;
bool pfx_ready = false;
bool pfx_failed = false;
// Screen-space ambient occlusion (ssao.hlsl: GTAO over the resolved
// scene; see ApplySsaoPass). Own binding layout: root constants b0 + two
// single-texture tables t0/t1 + point/linear clamp samplers. Full-res
// intermediates: linearized view-Z (R32F) + two AO ping-pong planes
// (R8), idle in RENDER_TARGET state. PSOs rebuild when the MSAA level
// (depth SRV dimension / linearize variant) or the output format
// (composite target) changes.
nrhi::BindingLayout* ao_layout = nullptr;
nrhi::Pipeline* pso_ao_linearize = nullptr;
nrhi::Pipeline* pso_ao_gtao = nullptr;
nrhi::Pipeline* pso_ao_blur = nullptr;
nrhi::Pipeline* pso_ao_luma = nullptr;
nrhi::Pipeline* pso_ao_composite = nullptr;
nrhi::Pipeline* pso_ao_debug = nullptr;
nrhi::Texture* ao_lin_depth = nullptr;
nrhi::Texture* ao_tex[2] = {};
// Scene luminance at the AO raster: the sun-lit protection mask input
// (bright pixels resist AO and skip the march entirely).
nrhi::Texture* ao_luma = nullptr;
nrhi::TextureView* ao_lin_srv = nullptr;
nrhi::TextureView* ao_srv[2] = {};
nrhi::TextureView* ao_luma_srv = nullptr;
// View of the scene depth buffer, re-pointed when depth is rebuilt.
nrhi::TextureView* ao_depth_srv = nullptr;
nrhi::Texture* ao_depth_srv_of = nullptr;
uint32_t ao_width = 0, ao_height = 0; // AO raster (half or full res)
uint32_t ao_lin_width = 0, ao_lin_height = 0; // linear depth (always full)
uint32_t ao_msaa = 0;
nrhi::Format ao_rtv_format = nrhi::Format::kUnknown;
bool ao_hdr = false; // luma/gtao/composite built for the HDR target
bool ao_failed = false;
// Fused-AO handoff (HDR path): the AO pass leaves the finished multiplier
// plane (ao_tex[0]) in PIXEL_SHADER_RESOURCE state for ps_tonemap to
// consume at t2 (replacing the classic full-res composite draw);
// ApplyHdrPost restores it to RENDER_TARGET and clears the flag.
bool ao_plane_in_psr = false;
// HDR pipeline (hdr.hlsl): the scene renders pre-tonemap linear into a
// float intermediate (the MSAA color target and/or hdr_resolved), a bloom
// pyramid extracts real HDR energy, and ps_tonemap applies the game's
// shared tone chain once into the gamma guest output. Binding layout =
// the SSAO shape (root constants b0 + two single-texture tables +
// point/linear clamp samplers). All targets idle in RENDER_TARGET state.
bool hdr_active = false; // latched per pipeline build (PSO formats/variants)
bool hdr_failed = false; // float targets unavailable -> classic path
// Latched scene float format (RGBA16F or the packed R11G11B10 option);
// every HDR-path PSO/target creation reads this, never the cvar.
nrhi::Format hdr_scene_format = nrhi::Format::kR16G16B16A16_FLOAT;
nrhi::BindingLayout* hdr_layout = nullptr;
nrhi::Pipeline* pso_bloom_first = nullptr; // mip-0 extract (threshold+Karis)
nrhi::Pipeline* pso_bloom_down = nullptr;
nrhi::Pipeline* pso_bloom_up = nullptr; // additive ONE/ONE tent upsample
nrhi::Pipeline* pso_tonemap = nullptr;
nrhi::Format hdr_pso_out_format = nrhi::Format::kUnknown;
// 1x float scene plane: the MSAA resolve destination (or the scene target
// itself when MSAA is off); the AO composite, bloom extract and tonemap
// all consume/write it.
nrhi::Texture* hdr_resolved = nullptr;
nrhi::TextureView* hdr_srv = nullptr;
static constexpr uint32_t kBloomMaxLevels = 6;
nrhi::Texture* bloom_tex[kBloomMaxLevels] = {};
nrhi::TextureView* bloom_srv[kBloomMaxLevels] = {};
uint32_t bloom_w[kBloomMaxLevels] = {};
uint32_t bloom_h[kBloomMaxLevels] = {};
uint32_t bloom_levels = 0;
uint32_t bloom_base_w = 0, bloom_base_h = 0; // output size the chain fits
bool targets_hdr = false; // output-sized targets built for the HDR path
// Scene float format the output-sized targets were built with (kUnknown
// when classic); packed-format toggles rebuild them.
nrhi::Format targets_scene_fmt = nrhi::Format::kUnknown;
// Screen-space reflections (ssr.hlsl + scene.hlsl ps_refl_gbuf): a
// half-res reflection G-buffer re-rendered from the frame's reflective
// items (env fams 5/6/13 + water), a half-res screen-space march over the
// full-res linear depth sampling the pre-tonemap HDR plane, and a
// src-alpha composite back onto it between the AO pass and the HDR post
// (so reflections tonemap/bloom like directly-visible scenery). HDR path
// only. Post binding layout = the SSAO shape with 32 root floats + three
// single-texture tables; the G-buffer pass runs under the MAIN layout
// (scene VS + per-item constants). All intermediates idle in
// RENDER_TARGET state.
nrhi::BindingLayout* ssr_layout = nullptr;
nrhi::Pipeline* pso_ssr_gbuf = nullptr; // scene-VS geometry pass
nrhi::Pipeline* pso_ssr_linearize = nullptr; // fallback when SSAO idle
nrhi::Pipeline* pso_ssr_march = nullptr;
nrhi::Pipeline* pso_ssr_composite = nullptr;
nrhi::Texture* ssr_gbuf = nullptr; // half res RGBA16F reflection G-buffer
// Half-res depth for the G-buffer pass (test+write LESS): reflective
// items overlap themselves in screen space (a curved glass tower's far
// side shares texels with its near side inside ONE mesh), so without a
// depth test draw/triangle order decides which surface owns a texel and
// whole visible panes fail the march's scene-depth match. Idles in
// DEPTH_WRITE state.
nrhi::Texture* ssr_gbuf_depth = nullptr;
nrhi::Texture* ssr_tex = nullptr; // half res RGBA16F march output
nrhi::Texture* ssr_lin_depth = nullptr; // full res R32F (own linearize)
nrhi::TextureView* ssr_gbuf_srv = nullptr;
nrhi::TextureView* ssr_srv = nullptr;
nrhi::TextureView* ssr_lin_srv = nullptr;
// Scene-depth SRV for the own linearize, re-pointed on depth rebuilds.
nrhi::TextureView* ssr_depth_srv = nullptr;
nrhi::Texture* ssr_depth_srv_of = nullptr;
uint32_t ssr_width = 0, ssr_height = 0; // half-res raster
uint32_t ssr_lin_width = 0, ssr_lin_height = 0;
uint32_t ssr_msaa = 0; // linearize-variant PSO latch
nrhi::Format ssr_scene_fmt = nrhi::Format::kUnknown; // composite target
bool ssr_failed = false;
// Per-frame handoff: the G-buffer pass drew and left ssr_gbuf in
// PIXEL_SHADER_RESOURCE for ApplySsrPass (which consumes + restores it).
bool ssr_gbuf_ready = false;
std::unordered_map<uint32_t, MeshBuffers> meshes;
// (The old D3D12 bookkeeping, the retired-resource vector, the CPU SRV
// staging heap and its slot allocator/recycling lists, is gone: resource
// destruction is deferred inside the RHI (Device::DestroyDeferred) and
// texture bindings are backend-managed view objects.)
GuestTexture white;
// Water environment CUBE maps (t6): separate cache; same guest object
// addresses decode differently (6 faces, TextureCube SRV).
std::unordered_map<uint32_t, GuestTexture> cube_textures;
GuestTexture white_cube;
// Bone palette ring: persistent-mapped upload buffer, one region per
// in-flight frame.
static constexpr uint32_t kBoneRegionSize = 1u << 20;
// 8-deep (was 4): the Vulkan CP was observed letting the GPU lag more
// than 3 frames under load - 4-deep regions then recycle while still
// referenced (hair shimmer / collapsed garments). If the ring race is
// confirmed, the CP-side throttle is the
// proper fix and this is defense in depth.
static constexpr uint32_t kBoneRegions = 8;
nrhi::Buffer* bone_ring = nullptr;
uint8_t* bone_ring_cpu = nullptr;
uint32_t bone_ring_offset = 0;
// ROPA shape-generation ring (per garment mesh): the last decoded vertex
// arrays keyed by dyn_seq, so the draw can combine the generations under
// the interp pass's kernel weights (the body's own 8-tap boxcar /
// pair-lerp: the stepped OR filter-mismatched shape against the
// interpolated body was the tee jelly). Plus a persistent-mapped upload
// ring the per-frame blended verts are written into (regioned like the
// bone ring).
struct RopaGen {
uint64_t seq = 0;
// Host time at commit: the blend refuses to mix generations recorded
// across a sim-sleep gap (a garment whose cloth sim slept keeps its
// last drapes in the ring; blending one of those, recorded when the
// character stood elsewhere / faced differently, against the fresh
// drape renders the garment rotated/offset from the body).
double t = 0.0;
std::vector<float> verts; // num_verts x 14 floats (scene VS layout)
};
std::unordered_map<uint32_t, std::deque<RopaGen>> ropa_shapes;
static constexpr uint32_t kRopaRegionSize = 1u << 20;
nrhi::Buffer* ropa_ring = nullptr;
uint8_t* ropa_ring_cpu = nullptr;
uint32_t ropa_ring_offset = 0;
// 2D overlay (HUD/APT replay): alpha-blended depth-less pipeline drawing
// the captured inline vertices from a per-frame upload ring.
nrhi::Pipeline* 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.
nrhi::Pipeline* pso_spline_darken = nullptr;
nrhi::Pipeline* pso_spline_default = nullptr;
// Dynamic CSM shadows: casters render
// into a 3-tile (depth, coverage) atlas with MIN blend (depth clear 1,
// "uncoverage" clear 1 -> covered texels write 0), then the game's exact
// Gaussian-coverage + depth-dilation blur runs per tile (5-tap cascade 0,
// 3-tap cascade 1, format-convert-only cascade 2) into the atlas the
// scene pass samples at t5.
nrhi::Pipeline* pso_shadow_caster = nullptr;
nrhi::Pipeline* pso_shadow_blur = nullptr;
nrhi::Texture* shadow_raw = nullptr; // caster pass target
nrhi::Texture* shadow_mid = nullptr; // hblur output
nrhi::Texture* shadow_final = nullptr; // vblur output, sampled
uint32_t shadow_tile = 0; // per-cascade tile size (atlas = 3*tile x tile)
nrhi::TextureView* shadow_srv_raw = nullptr;
nrhi::TextureView* shadow_srv_mid = nullptr;
nrhi::TextureView* shadow_srv_final = nullptr;
// After a shadow pass all three textures sit in PIXEL_SHADER_RESOURCE;
// the next pass transitions them back to RENDER_TARGET first.
bool shadow_in_srv_state = false;
// Shadow-atlas readback, taken on kShadowDumpFrames CONSECUTIVE frames
// once per F11 recording session: the raw and blurred planes (R16G16)
// land in the snapshot dir for offline analysis; the guest's own atlas
// resolve never reaches CPU memory, so this is the only view of what
// receivers actually sample. Consecutive frames make temporal-stability
// diffs possible (silhouette buzz vs animation).
static constexpr uint32_t kShadowDumpFrames = 4;
nrhi::Buffer* shadow_dump_buf[kShadowDumpFrames] = {};
uint8_t* shadow_dump_ptr[kShadowDumpFrames] = {};
uint64_t shadow_dump_submission[kShadowDumpFrames] = {};
uint32_t shadow_dump_enqueued = 0;
uint32_t shadow_dump_written = 0;
bool shadow_dump_done = false;
// Per-frame shadow constant buffer ring (CBV b1). 512-byte slices: the
// first 256 bytes are the original 16-row block, rows 16-18 carry the
// dynamicobject world-shadow transform (dyn_ws* in the scene shader).
static constexpr uint32_t kShadowCbRegions = 8;
static constexpr uint32_t kShadowCbSlice = 512;
nrhi::Buffer* shadow_cb = nullptr;
uint8_t* shadow_cb_cpu = nullptr;
// dynamicobject static world-shadow map (512x512, same convention as the
// game's own: x = light-space depth, sge(x >= ray) = lit): re-rendered
// natively from the frame's STATIC world items with the captured c5/c6/c7
// transform. MIN blend accumulates across frames WITHOUT clearing; the
// per-frame item list is view-culled, and dropping off-screen casters
// would flicker props' shade with the camera. Cleared (re-primed) only
// when the captured transform changes (streaming region switch).
nrhi::Texture* world_shadow = nullptr;
nrhi::TextureView* world_shadow_srv = nullptr;
bool world_shadow_in_srv = false;
bool world_shadow_primed = false;
float world_shadow_rows[12] = {};
static constexpr uint32_t kWorldShadowSize = 512;
static constexpr uint32_t kUiRegionSize = 1u << 20;
static constexpr uint32_t kUiRegions = 8;
nrhi::Buffer* ui_ring = nullptr;
uint8_t* ui_ring_cpu = nullptr;
// Last successfully resolved words-key per streamed-art site
// (mesh << 1 | slot; slot 0 = diffuse override, 1 = decal override).
// Serves the site's previous art while a new-mip-words decode is in
// flight (see resolve_fetch_words). Render thread only.
std::unordered_map<uint64_t, uint64_t> words_sticky;
// (The fam 5/6 masks+normal t4/t5 descriptor-pair cache is gone: draws
// bind both views directly via cmd->SetTexturePair(5, spec, normal);
// pair descriptors are cached inside the backend, so texture replacement
// can never leave a stale descriptor.)
// THE texture content store (console identity
// semantics): fetch-words key -> decode. One
// words state = one entry; both states of a streaming flap A<->B simply
// stay resident under their own keys (what the old lookaside simulated
// with park/take), and superseded states age out via the LRU. Shared by
// the 3D draw path (through the object routes below), the 2D/HUD pass,
// and the draw-time fetch-word overrides (posters/ads). Render thread
// only.
std::unordered_map<uint64_t, GuestTexture> tex_store;
// Texture object -> its last STABLE fetch-words state (seqlock
// double-read at resolve). A route is a lookup aid, never an owner: the
// game freely retargets objects (mip promote/demote, detail demote,
// object reuse) and every retarget is just a different store key; a
// reused object can never serve another binding's art. Render thread
// only.
struct TexRoute {
uint32_t words[6] = {};
uint64_t key = 0;
// Live words carry no mip-0 base (streamer demoted the top level; the
// old pool range is already reused). The route holds the pre-demote
// state, its cached decode carries the full chain, strictly better,
// and payload polls are suspended while held (the probes would read
// the reused pool). A re-promote publishes fresh words and re-routes.
bool demoted = false;
};
std::unordered_map<uint32_t, TexRoute> tex_routes;
// Sticky texture serving (see resolve_texture in draw_item): the last
// ADOPTED (served-live) words state per (mesh << 3 | slot). Serves the
// site's previous art while the current binding's decode is in flight
// (the console's own mip-transition look), and powers the
// detail-downgrade hold: a strict base-area shrink keeps serving the
// previous state's store entry for the hold window. Render thread only.
struct TexStickySite {
uint64_t words_key = 0;
uint64_t area = 0;
// Nonzero = a downgrade is being held, first seen at this frame. The
// site adopts the smaller binding once the downgrade persists past
// skate3_native_render_scene_detail_hold frames.
uint64_t downgrade_since = 0;
};
std::unordered_map<uint64_t, TexStickySite> tex_sticky;
// First frame a texture object resolved white-with-heal-in-flight:
// brand-new items (no sticky fallback) skip drawing for a short window
// instead of flashing white.
std::unordered_map<uint32_t, uint64_t> tex_pending_first;
bool failed = false;
bool announced = false;
};
inline RendererState g_r;
constexpr nrhi::InputElementDesc kSceneInputLayout[7] = {
{"POSITION", 0, 0, nrhi::Format::kR32G32B32_FLOAT, 0},
{"TEXCOORD", 0, 1, nrhi::Format::kR32G32_FLOAT, 12},
{"TEXCOORD", 1, 2, nrhi::Format::kR32G32_FLOAT, 20},
{"BLENDWEIGHT", 0, 3, nrhi::Format::kR8G8B8A8_UNORM, 28},
{"BLENDINDICES", 0, 4, nrhi::Format::kR8G8B8A8_UINT, 32},
{"NORMAL", 0, 5, nrhi::Format::kR32G32B32_FLOAT, 36},
{"TEXCOORD", 2, 6, nrhi::Format::kR32G32_FLOAT, 48}};
// Pipeline / resource-group builders (skate3_native_scene_gpu.cpp). Each is
// idempotent and returns false only on a failure that must abort the native
// path.
bool EnsureRootSignature(const NativeGuestOutputRenderContext& context);
bool EnsureScenePsoFamily(const NativeGuestOutputRenderContext& context);
bool EnsureResolvePso(const NativeGuestOutputRenderContext& context);
bool EnsureBlurPsos(const NativeGuestOutputRenderContext& context);
bool EnsureOutlineEdgePso(const NativeGuestOutputRenderContext& context);
bool Ensure2dPso(const NativeGuestOutputRenderContext& context);
bool EnsureSplinePsos(const NativeGuestOutputRenderContext& context);
bool EnsureShadowPsos(const NativeGuestOutputRenderContext& context);
bool EnsureHeapsAndRings(const NativeGuestOutputRenderContext& context);
bool EnsurePhotoFxPipeline(const NativeGuestOutputRenderContext& context);
bool EnsureShadowResources(const NativeGuestOutputRenderContext& context);
bool EnsureBlurOutlineTargets(const NativeGuestOutputRenderContext& context);
bool EnsureFallbackTextures(const NativeGuestOutputRenderContext& context);
bool EnsureOutputSizedTargets(const NativeGuestOutputRenderContext& context);
// Umbrella builder: runs every group above (plus the base device/queue
// setup) and latches g_r.failed on an unrecoverable failure.
bool EnsurePipeline(const NativeGuestOutputRenderContext& context);
// Shader-desc helper: pairs the HLSL source with its offline-compiled
// SPIR-V blob (Vulkan) by {file, entry, variant}.
nrhi::ShaderDesc MakeShaderDesc(nrhi::ShaderStage stage, const char* file,
const char* hlsl_source, const char* entry,
const nrhi::ShaderMacro* macros,
const char* variant);
// Post passes (skate3_native_scene_post.cpp).
bool EnsureSsaoPipeline(const NativeGuestOutputRenderContext& context);
bool EnsureSsrPipeline(const NativeGuestOutputRenderContext& context);
bool EnsureSsrTargets(const NativeGuestOutputRenderContext& context);
bool EnsureHdrPipeline(const NativeGuestOutputRenderContext& context);
bool ApplySsaoPass(const NativeGuestOutputRenderContext& context,
nrhi::Cmd* cmd, const FrameScene& scene,
const nrhi::Viewport& viewport, const nrhi::Rect& scissor);
bool ApplySsrPass(const NativeGuestOutputRenderContext& context,
nrhi::Cmd* cmd, const FrameScene& scene,
const nrhi::Viewport& viewport, const nrhi::Rect& scissor,
bool ssao_ran);
void ApplyHdrPost(const NativeGuestOutputRenderContext& context,
nrhi::Cmd* cmd, const nrhi::Viewport& viewport,
const nrhi::Rect& scissor, bool loading_native);
bool ApplyMenuBlurPass(const NativeGuestOutputRenderContext& context, nrhi::Cmd* cmd,
float target_sigma, bool output_in_guest_output_state);
// Blur-over-emulated-frames post processor registered by Install().
void PostProcessGuestOutput(const NativeGuestOutputRenderContext& context,
void* user_data);
} // namespace skate3::native_scene
#endif // REX_HAS_D3D12 || REX_HAS_VULKAN

File diff suppressed because it is too large Load diff