From 13669fe589a3f66abae116d57d45b16ccb5d8521 Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Sun, 19 Jul 2026 10:28:14 +0100 Subject: [PATCH] render: peel the post-process passes into their own translation unit --- CMakeLists.txt | 1 + src/skate3_native_scene_gpu.cpp | 2087 +----------------------- src/skate3_native_scene_gpu_internal.h | 897 ++++++++++ src/skate3_native_scene_post.cpp | 1340 +++++++++++++++ 4 files changed, 2246 insertions(+), 2079 deletions(-) create mode 100644 src/skate3_native_scene_gpu_internal.h create mode 100644 src/skate3_native_scene_post.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c9699a..686a5ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -236,6 +236,7 @@ set(SKATE3_COMMON_SOURCES src/skate3_native_render.cpp src/skate3_native_scene.cpp src/skate3_native_scene_gpu.cpp + src/skate3_native_scene_post.cpp src/skate3_screenshot.cpp src/skate3_shader_disasm.cpp src/skate3_title_update_installer.cpp diff --git a/src/skate3_native_scene_gpu.cpp b/src/skate3_native_scene_gpu.cpp index b730d69..46201b0 100644 --- a/src/skate3_native_scene_gpu.cpp +++ b/src/skate3_native_scene_gpu.cpp @@ -55,6 +55,7 @@ #endif #endif #include "skate3_native_scene_state.h" +#include "skate3_native_scene_gpu_internal.h" // Cvars defined in skate3_native_scene.cpp (and SDK cvars re-declared there). REXCVAR_DECLARE(bool, async_shader_compilation); @@ -145,822 +146,6 @@ REXCVAR_DECLARE(std::string, skate3_native_render_snapshot_dir); namespace skate3::native_scene { namespace { -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 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(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. -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(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. -std::atomic g_heal_verify_fail{0}; -std::atomic 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. -std::atomic g_tex_sticky_served{0}; -std::atomic 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). -uint32_t g_trace_mesh_addr = 0; -std::unordered_set g_trace_keys; -std::unordered_map 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). -std::atomic g_ad_stale_served{0}; -std::atomic 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. -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). -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. -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; -}; - -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). -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); - } -} - -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 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 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 verts; // num_verts x 14 floats (scene VS layout) - }; - std::unordered_map> 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 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 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 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 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 tex_pending_first; - bool failed = false; - bool announced = false; -}; - -RendererState g_r; - // Retire a guest texture's GPU resources AND its view. Destruction is // deferred inside the RHI against the CURRENT submission; the `submission` // parameter is kept for call-site compatibility and ignored. @@ -3054,14 +2239,8 @@ bool EnsureGuestCubeTexture(const NativeGuestOutputRenderContext& context, uint8 // PSO family and the shadow-caster PSO. `location` = the element's // declaration order in the scene VS input struct (the Vulkan input // location; see nrhi::InputElementDesc). -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}}; + +} // namespace // ---- EnsurePipeline helpers ------------------------------------------------ // Split from the former ~1,000-line EnsurePipeline: one resource @@ -4361,1073 +3540,6 @@ bool EnsureOutputSizedTargets(const NativeGuestOutputRenderContext& context) { return true; } -// ---- Screen-space ambient occlusion (ssao.hlsl: GTAO) -------------------- -// Layout + PSOs, built lazily on the first enabled frame and rebuilt when -// the MSAA level (linearize shader variant) or the output format (composite -// target) changes. Failure disables the effect only. -bool EnsureSsaoPipeline(const NativeGuestOutputRenderContext& context) { - if (g_r.ao_failed) { - return false; - } - // Composite target: the float HDR plane (pre-tonemap multiply) or the - // gamma guest output (classic). - const nrhi::Format ao_out_fmt = g_r.hdr_active - ? g_r.hdr_scene_format - : context.guest_output->format(); - if (g_r.pso_ao_linearize != nullptr && g_r.ao_msaa == g_r.msaa && - g_r.ao_rtv_format == ao_out_fmt && g_r.ao_hdr == g_r.hdr_active) { - return true; - } - nrhi::Device* device = context.device; - const auto fail = [&](const char* what) { - REXLOG_ERROR("native-scene: ssao pipeline setup failed ({})", what); - g_r.ao_failed = true; - return false; - }; - if (g_r.ao_layout == nullptr) { - nrhi::BindingLayoutDesc ld; - ld.param_count = 3; - ld.params[0] = {nrhi::BindingParamKind::kConstants, /*b*/ 0, 16, - nrhi::Visibility::kAll}; - ld.params[1] = {nrhi::BindingParamKind::kTextureTable, /*t*/ 0, 1, - nrhi::Visibility::kPixel}; - ld.params[2] = {nrhi::BindingParamKind::kTextureTable, 1, 1, - nrhi::Visibility::kPixel}; - ld.static_sampler_count = 2; - ld.static_samplers[0] = {/*s*/ 0, nrhi::Filter::kPoint, - nrhi::AddressMode::kClamp, 1}; - ld.static_samplers[1] = {1, nrhi::Filter::kLinear, - nrhi::AddressMode::kClamp, 1}; - ld.allow_input_layout = false; - g_r.ao_layout = device->CreateBindingLayout(ld); - if (g_r.ao_layout == nullptr) { - return fail("binding layout"); - } - } - nrhi::Pipeline** psos[6] = {&g_r.pso_ao_linearize, &g_r.pso_ao_gtao, - &g_r.pso_ao_blur, &g_r.pso_ao_luma, - &g_r.pso_ao_composite, &g_r.pso_ao_debug}; - for (nrhi::Pipeline** p : psos) { - if (*p != nullptr) { - device->DestroyDeferred(*p); - *p = nullptr; - } - } - const bool msaa = g_r.msaa > 1; - const nrhi::ShaderMacro msaa_defs[] = {{"AO_MSAA", "1"}, {nullptr, nullptr}}; - // HDR=1 on the luma pass (tonemap the pre-tonemap scene before the - // protection luma) and the march (linear-space intensity exponent). - const nrhi::ShaderMacro hdr_defs[] = {{"HDR", "1"}, {nullptr, nullptr}}; - nrhi::Shader* vs = device->CreateShader( - MakeShaderDesc(nrhi::ShaderStage::kVertex, "ssao.hlsl", - kSsaoShaderSource, "vs_main", nullptr, "")); - const char* ps_entries[6] = {"ps_linearize", "ps_gtao", "ps_blur", - "ps_luma", "ps_composite", "ps_debug"}; - nrhi::Shader* ps[6] = {}; - bool ok = vs != nullptr; - for (int i = 0; i < 6; ++i) { - const bool msaa_entry = i == 0 && msaa; - const bool hdr_entry = (i == 1 || i == 3) && g_r.hdr_active; - ps[i] = device->CreateShader(MakeShaderDesc( - nrhi::ShaderStage::kPixel, "ssao.hlsl", kSsaoShaderSource, - ps_entries[i], - msaa_entry ? msaa_defs : (hdr_entry ? hdr_defs : nullptr), - msaa_entry ? "AO_MSAA=1" : (hdr_entry ? "HDR=1" : ""))); - ok = ok && ps[i] != nullptr; - } - if (!ok) { - device->DestroyDeferred(vs); - for (nrhi::Shader* s : ps) { - device->DestroyDeferred(s); - } - return fail("shader compile"); - } - nrhi::GraphicsPipelineDesc pso; - pso.layout = g_r.ao_layout; - pso.vs = vs; - pso.cull = nrhi::CullMode::kNone; - pso.sample_count = 1; - pso.ps = ps[0]; - pso.rtv_format = nrhi::Format::kR32_FLOAT; - g_r.pso_ao_linearize = device->CreateGraphicsPipeline(pso); - pso.ps = ps[1]; - pso.rtv_format = nrhi::Format::kR8_UNORM; - g_r.pso_ao_gtao = device->CreateGraphicsPipeline(pso); - pso.ps = ps[2]; - g_r.pso_ao_blur = device->CreateGraphicsPipeline(pso); - pso.ps = ps[3]; - g_r.pso_ao_luma = device->CreateGraphicsPipeline(pso); - // Composite: dst.rgb * src.a; the AO plane carries the finished - // multiplier, and the RGB-only write mask keeps the output's alpha - // untouched. - pso.ps = ps[4]; - pso.rtv_format = ao_out_fmt; - pso.blend.enable = true; - pso.blend.src = nrhi::BlendFactor::kZero; - pso.blend.dst = nrhi::BlendFactor::kSrcAlpha; - pso.blend.op = nrhi::BlendOp::kAdd; - pso.blend.src_alpha = nrhi::BlendFactor::kZero; - pso.blend.dst_alpha = nrhi::BlendFactor::kOne; - pso.blend.op_alpha = nrhi::BlendOp::kAdd; - pso.blend.write_mask = 0x7; - g_r.pso_ao_composite = device->CreateGraphicsPipeline(pso); - pso.blend = {}; - pso.ps = ps[5]; - g_r.pso_ao_debug = device->CreateGraphicsPipeline(pso); - device->DestroyDeferred(vs); - for (nrhi::Shader* s : ps) { - device->DestroyDeferred(s); - } - if (g_r.pso_ao_linearize == nullptr || g_r.pso_ao_gtao == nullptr || - g_r.pso_ao_blur == nullptr || g_r.pso_ao_luma == nullptr || - g_r.pso_ao_composite == nullptr || g_r.pso_ao_debug == nullptr) { - return fail("pso"); - } - g_r.ao_msaa = g_r.msaa; - g_r.ao_rtv_format = ao_out_fmt; - g_r.ao_hdr = g_r.hdr_active; - REXLOG_INFO("native-scene: ssao pipeline created (MSAA x{}, {})", g_r.msaa, - g_r.hdr_active ? "HDR" : "classic"); - return true; -} - -// GTAO chain over the resolved scene (see ssao.hlsl): linearize depth, -// horizon-based AO, depth-aware separable blur, multiply-composite onto the -// guest output (which must be in RENDER_TARGET state). Returns true when it -// drew; binding layout/state was changed and the caller restores the main -// pass's bindings. -bool ApplySsaoPass(const NativeGuestOutputRenderContext& context, - nrhi::Cmd* cmd, const FrameScene& scene, - const nrhi::Viewport& viewport, const nrhi::Rect& scissor) { - // The published projection must be the live perspective matrix (row-vector - // m23 == 1; all zeros until the first publish). - const float* pr = scene.proj; - if (pr[11] != 1.0f || pr[0] == 0.0f || pr[5] == 0.0f || pr[14] == 0.0f) { - return false; - } - if (!EnsureSsaoPipeline(context)) { - return false; - } - // The luma pass samples the scene through this view; the composite writes - // the same plane (HDR: the float scene plane pre-tonemap, classic: the - // guest output). - const bool hdr = g_r.hdr_active && g_r.hdr_resolved != nullptr; - nrhi::Texture* const scene_plane = - hdr ? g_r.hdr_resolved : context.guest_output; - nrhi::TextureView* const scene_srv = - hdr ? g_r.hdr_srv : g_r.output_srv_slot; - if (scene_srv == nullptr) { - return false; - } - nrhi::Device* device = context.device; - const uint32_t width = context.guest_output_width; - const uint32_t height = context.guest_output_height; - // AO raster: half the output resolution by default: for a low-frequency - // term the depth-aware blur + bilinear upsample hide it, at 1/4 the march - // cost (full-res GTAO at 4K x 300+ uncapped fps pegged the GPU). The - // linearized depth stays full res: the march samples it at arbitrary UVs - // either way, and it keeps silhouette depth exact. - const bool ao_full = - REXCVAR_GET(skate3_native_render_scene_ssao_full_res); - const uint32_t aw = ao_full ? width : std::max(1u, (width + 1) / 2); - const uint32_t ah = ao_full ? height : std::max(1u, (height + 1) / 2); - // Intermediates (steady state RENDER_TARGET), rebuilt on either size. - if (g_r.ao_width != aw || g_r.ao_height != ah || - g_r.ao_lin_width != width || g_r.ao_lin_height != height || - g_r.ao_lin_depth == nullptr || g_r.ao_luma == nullptr) { - nrhi::Texture** res[4] = {&g_r.ao_lin_depth, &g_r.ao_tex[0], - &g_r.ao_tex[1], &g_r.ao_luma}; - nrhi::TextureView** views[4] = {&g_r.ao_lin_srv, &g_r.ao_srv[0], - &g_r.ao_srv[1], &g_r.ao_luma_srv}; - for (int i = 0; i < 4; ++i) { - if (*views[i] != nullptr) { - device->DestroyDeferred(*views[i]); - *views[i] = nullptr; - } - if (*res[i] != nullptr) { - device->DestroyDeferred(*res[i]); - *res[i] = nullptr; - } - nrhi::TextureDesc desc; - desc.width = i == 0 ? width : aw; - desc.height = i == 0 ? height : ah; - desc.mip_levels = 1; - desc.format = - i == 0 ? nrhi::Format::kR32_FLOAT : nrhi::Format::kR8_UNORM; - desc.usage = nrhi::kTextureUsageRenderTarget; - desc.initial_state = nrhi::ResourceState::kRenderTarget; - *res[i] = device->CreateTexture(desc); - if (*res[i] == nullptr) { - g_r.ao_failed = true; - return false; - } - nrhi::TextureViewDesc vd; - vd.mip_levels = 1; - *views[i] = device->CreateTextureView(*res[i], vd); - if (*views[i] == nullptr) { - g_r.ao_failed = true; - return false; - } - } - g_r.ao_width = aw; - g_r.ao_height = ah; - g_r.ao_lin_width = width; - g_r.ao_lin_height = height; - } - // Scene-depth SRV, re-pointed when the depth buffer is rebuilt (resize / - // MSAA change). D32 textures view as R32_FLOAT automatically. - if (g_r.ao_depth_srv == nullptr || g_r.ao_depth_srv_of != g_r.depth) { - if (g_r.ao_depth_srv != nullptr) { - device->DestroyDeferred(g_r.ao_depth_srv); - g_r.ao_depth_srv = nullptr; - } - nrhi::TextureViewDesc sd; - if (g_r.msaa > 1) { - sd.dimension = nrhi::ViewDimension::k2DMS; - } else { - sd.dimension = nrhi::ViewDimension::k2D; - sd.mip_levels = 1; - } - g_r.ao_depth_srv = device->CreateTextureView(g_r.depth, sd); - if (g_r.ao_depth_srv == nullptr) { - g_r.ao_failed = true; - return false; - } - g_r.ao_depth_srv_of = g_r.depth; - } - - // b0 rows (see ssao.hlsl cbuffer C): size, projection, tuning, blur axis. - // size = the AO raster the march/blur passes run at; every shader use is - // UV-relative, so half res needs no shader-side changes. - float c[16]; - c[0] = float(aw); - c[1] = float(ah); - c[2] = 1.0f / float(aw); - c[3] = 1.0f / float(ah); - c[4] = std::fabs(pr[0]); // |m00|: the guest projection's x scale is - // negative; AO is mirror-invariant - c[5] = std::fabs(pr[5]); // |m11| - c[6] = pr[10]; // m22 - c[7] = pr[14]; // m32 - c[8] = float(REXCVAR_GET(skate3_native_render_scene_ssao_radius)); - c[9] = float(REXCVAR_GET(skate3_native_render_scene_ssao_intensity)); - // Distance fade 150..250 view units: fog owns the far field and the - // horizon-search precision degrades with depth anyway. - c[10] = 150.0f; - c[11] = 1.0f / 100.0f; - c[12] = 1.0f; // blur axis (horizontal first) - c[13] = 0.0f; - c[14] = float(REXCVAR_GET(skate3_native_render_scene_ssao_luma_protect)); - c[15] = 0.0f; - - const nrhi::Viewport ao_vp{0.0f, 0.0f, float(aw), float(ah), 0.0f, 1.0f}; - const nrhi::Rect ao_sc{0, 0, int32_t(aw), int32_t(ah)}; - - cmd->SetBindingLayout(g_r.ao_layout); - cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList); - cmd->SetViewport(viewport); - cmd->SetScissor(scissor); - cmd->SetRootConstants(0, 16, c, 0); - - // 1) Scene depth -> linear view Z (sample 0 when MSAA). Full res. - cmd->Barrier(g_r.depth, nrhi::ResourceState::kDepthWrite, - nrhi::ResourceState::kPixelShaderResource); - cmd->FlushBarriers(); - cmd->SetRenderTargets(g_r.ao_lin_depth, nullptr); - cmd->SetPipeline(g_r.pso_ao_linearize); - cmd->SetTexture(1, g_r.ao_depth_srv); - cmd->Draw(3, 0); - cmd->Barrier(g_r.depth, nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kDepthWrite); - cmd->Barrier(g_r.ao_lin_depth, nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kPixelShaderResource); - cmd->FlushBarriers(); - - // 1b) Scene luminance -> ao_luma (AO raster): the sun-lit protection - // mask. The scene plane pauses as a sampled source for one draw. - cmd->Barrier(scene_plane, nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kPixelShaderResource); - cmd->FlushBarriers(); - cmd->SetViewport(ao_vp); - cmd->SetScissor(ao_sc); - cmd->SetRenderTargets(g_r.ao_luma, nullptr); - cmd->SetPipeline(g_r.pso_ao_luma); - cmd->SetTexture(1, scene_srv); - cmd->Draw(3, 0); - cmd->Barrier(g_r.ao_luma, nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kPixelShaderResource); - cmd->Barrier(scene_plane, nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - cmd->FlushBarriers(); - - // 2) GTAO -> ao_tex[0], at the AO raster size. - cmd->SetRenderTargets(g_r.ao_tex[0], nullptr); - cmd->SetPipeline(g_r.pso_ao_gtao); - cmd->SetTexture(1, g_r.ao_lin_srv); - cmd->SetTexture(2, g_r.ao_luma_srv); - cmd->Draw(3, 0); - cmd->Barrier(g_r.ao_tex[0], nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kPixelShaderResource); - cmd->FlushBarriers(); - - // 3) Depth-aware blur, horizontal -> ao_tex[1]. - cmd->SetRenderTargets(g_r.ao_tex[1], nullptr); - cmd->SetPipeline(g_r.pso_ao_blur); - cmd->SetTexture(1, g_r.ao_srv[0]); - cmd->SetTexture(2, g_r.ao_lin_srv); - cmd->Draw(3, 0); - cmd->Barrier(g_r.ao_tex[1], nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kPixelShaderResource); - cmd->Barrier(g_r.ao_tex[0], nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - cmd->FlushBarriers(); - - // 4) Vertical -> ao_tex[0]. - const float dir_v[2] = {0.0f, 1.0f}; - cmd->SetRootConstants(0, 2, dir_v, 12); - cmd->SetRenderTargets(g_r.ao_tex[0], nullptr); - cmd->SetTexture(1, g_r.ao_srv[1]); - cmd->SetTexture(2, g_r.ao_lin_srv); - cmd->Draw(3, 0); - cmd->Barrier(g_r.ao_tex[0], nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kPixelShaderResource); - cmd->FlushBarriers(); - - // 5) Consume the finished multiplier plane. Classic: a full-res composite - // (or debug replace) draw onto the scene plane, s1 bilinearly - // upsampling. HDR: no draw at all: ps_tonemap samples the plane at t2 - // and multiplies pre-tonemap (identical math, one less full-res pass); - // the plane stays in PIXEL_SHADER_RESOURCE for ApplyHdrPost, which - // restores it. - if (hdr) { - g_r.ao_plane_in_psr = true; - } else { - cmd->SetViewport(viewport); - cmd->SetScissor(scissor); - cmd->SetRenderTargets(scene_plane, nullptr); - cmd->SetPipeline(REXCVAR_GET(skate3_native_render_scene_ssao_debug) - ? g_r.pso_ao_debug - : g_r.pso_ao_composite); - cmd->SetTexture(1, g_r.ao_srv[0]); - cmd->Draw(3, 0); - cmd->Barrier(g_r.ao_tex[0], nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - } - - // Restore intermediate steady states. - cmd->Barrier(g_r.ao_tex[1], nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - cmd->Barrier(g_r.ao_lin_depth, nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - cmd->Barrier(g_r.ao_luma, nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - cmd->FlushBarriers(); - return true; -} - -// ---- Screen-space reflections (ssr.hlsl + scene.hlsl ps_refl_gbuf) -------- -// Layout + PSOs, built lazily on the first SSR frame and rebuilt when the -// MSAA level (linearize variant) or the scene float format (composite -// target) changes. Failure disables the effect only, never g_r.failed. -bool EnsureSsrPipeline(const NativeGuestOutputRenderContext& context) { - if (g_r.ssr_failed || !g_r.hdr_active) { - return false; - } - if (g_r.pso_ssr_march != nullptr && g_r.ssr_msaa == g_r.msaa && - g_r.ssr_scene_fmt == g_r.hdr_scene_format) { - return true; - } - nrhi::Device* device = context.device; - const auto fail = [&](const char* what) { - REXLOG_ERROR("native-scene: ssr pipeline setup failed ({})", what); - g_r.ssr_failed = true; - return false; - }; - if (g_r.ssr_layout == nullptr) { - // The SSAO layout shape with a wider constant block (the march needs - // the world->view rotation rows): root constants b0 (32 floats) + three - // single-texture tables + point/linear clamp samplers. - nrhi::BindingLayoutDesc ld; - ld.param_count = 4; - ld.params[0] = {nrhi::BindingParamKind::kConstants, /*b*/ 0, 32, - nrhi::Visibility::kAll}; - ld.params[1] = {nrhi::BindingParamKind::kTextureTable, /*t*/ 0, 1, - nrhi::Visibility::kPixel}; - ld.params[2] = {nrhi::BindingParamKind::kTextureTable, 1, 1, - nrhi::Visibility::kPixel}; - ld.params[3] = {nrhi::BindingParamKind::kTextureTable, 2, 1, - nrhi::Visibility::kPixel}; - ld.static_sampler_count = 2; - ld.static_samplers[0] = {/*s*/ 0, nrhi::Filter::kPoint, - nrhi::AddressMode::kClamp, 1}; - ld.static_samplers[1] = {1, nrhi::Filter::kLinear, - nrhi::AddressMode::kClamp, 1}; - ld.allow_input_layout = false; - g_r.ssr_layout = device->CreateBindingLayout(ld); - if (g_r.ssr_layout == nullptr) { - return fail("binding layout"); - } - } - for (nrhi::Pipeline** p : {&g_r.pso_ssr_gbuf, &g_r.pso_ssr_linearize, - &g_r.pso_ssr_march, &g_r.pso_ssr_composite}) { - if (*p != nullptr) { - device->DestroyDeferred(*p); - *p = nullptr; - } - } - const bool msaa = g_r.msaa > 1; - const nrhi::ShaderMacro msaa_defs[] = {{"SSR_MSAA", "1"}, - {nullptr, nullptr}}; - nrhi::Shader* scene_vs = device->CreateShader( - MakeShaderDesc(nrhi::ShaderStage::kVertex, "scene.hlsl", kShaderSource, - "vs_main", nullptr, "")); - nrhi::Shader* ps_gbuf = device->CreateShader( - MakeShaderDesc(nrhi::ShaderStage::kPixel, "scene.hlsl", kShaderSource, - "ps_refl_gbuf", nullptr, "")); - nrhi::Shader* vs = device->CreateShader( - MakeShaderDesc(nrhi::ShaderStage::kVertex, "ssr.hlsl", kSsrShaderSource, - "vs_main", nullptr, "")); - nrhi::Shader* ps_lin = device->CreateShader(MakeShaderDesc( - nrhi::ShaderStage::kPixel, "ssr.hlsl", kSsrShaderSource, "ps_linearize", - msaa ? msaa_defs : nullptr, msaa ? "SSR_MSAA=1" : "")); - nrhi::Shader* ps_march = device->CreateShader( - MakeShaderDesc(nrhi::ShaderStage::kPixel, "ssr.hlsl", kSsrShaderSource, - "ps_march", nullptr, "")); - nrhi::Shader* ps_comp = device->CreateShader( - MakeShaderDesc(nrhi::ShaderStage::kPixel, "ssr.hlsl", kSsrShaderSource, - "ps_composite", nullptr, "")); - const bool shaders_ok = scene_vs != nullptr && ps_gbuf != nullptr && - vs != nullptr && ps_lin != nullptr && - ps_march != nullptr && ps_comp != nullptr; - if (!shaders_ok) { - for (nrhi::Shader* s : - {scene_vs, ps_gbuf, vs, ps_lin, ps_march, ps_comp}) { - device->DestroyDeferred(s); - } - return fail("shader compile"); - } - // Reflection G-buffer pass: the scene VS + input layout under the MAIN - // binding layout (the caller re-stages each reflective item's main-pass - // root constants / textures), half-res single-sample float target with - // its OWN half-res depth (test+write: the MSAA scene depth cannot pair - // with a 1x RTV) so the nearest reflective surface owns each texel; - // occlusion by non-reflective geometry is then resolved in the march by - // comparing the stored G-buffer depth against the scene linear depth. - { - nrhi::GraphicsPipelineDesc pso; - pso.layout = g_r.layout; - pso.vs = scene_vs; - pso.ps = ps_gbuf; - pso.input_elements = kSceneInputLayout; - pso.input_element_count = 7; - pso.vertex_stride = 56; - pso.cull = nrhi::CullMode::kNone; - pso.sample_count = 1; - pso.depth_clip = true; - pso.depth.test_enable = true; - pso.depth.write_enable = true; - pso.depth.func = nrhi::CompareFunc::kLess; - pso.dsv_format = nrhi::Format::kD32_FLOAT; - pso.rtv_format = nrhi::Format::kR16G16B16A16_FLOAT; - g_r.pso_ssr_gbuf = device->CreateGraphicsPipeline(pso); - } - nrhi::GraphicsPipelineDesc pso; - pso.layout = g_r.ssr_layout; - pso.vs = vs; - pso.cull = nrhi::CullMode::kNone; - pso.sample_count = 1; - pso.ps = ps_lin; - pso.rtv_format = nrhi::Format::kR32_FLOAT; - g_r.pso_ssr_linearize = device->CreateGraphicsPipeline(pso); - pso.ps = ps_march; - pso.rtv_format = nrhi::Format::kR16G16B16A16_FLOAT; - g_r.pso_ssr_march = device->CreateGraphicsPipeline(pso); - // Composite: straight src-alpha blend onto the float scene plane (the - // shader outputs confidence x reflectivity x intensity in alpha), RGB - // write mask: the plane's alpha stays untouched. - pso.ps = ps_comp; - pso.rtv_format = g_r.hdr_scene_format; - pso.blend.enable = true; - pso.blend.src = nrhi::BlendFactor::kSrcAlpha; - pso.blend.dst = nrhi::BlendFactor::kInvSrcAlpha; - pso.blend.op = nrhi::BlendOp::kAdd; - pso.blend.src_alpha = nrhi::BlendFactor::kZero; - pso.blend.dst_alpha = nrhi::BlendFactor::kOne; - pso.blend.op_alpha = nrhi::BlendOp::kAdd; - pso.blend.write_mask = 0x7; - g_r.pso_ssr_composite = device->CreateGraphicsPipeline(pso); - for (nrhi::Shader* s : {scene_vs, ps_gbuf, vs, ps_lin, ps_march, ps_comp}) { - device->DestroyDeferred(s); - } - if (g_r.pso_ssr_gbuf == nullptr || g_r.pso_ssr_linearize == nullptr || - g_r.pso_ssr_march == nullptr || g_r.pso_ssr_composite == nullptr) { - return fail("pso"); - } - g_r.ssr_msaa = g_r.msaa; - g_r.ssr_scene_fmt = g_r.hdr_scene_format; - REXLOG_INFO("native-scene: ssr pipeline created (MSAA x{})", g_r.msaa); - return true; -} - -// Half-res SSR intermediates (reflection G-buffer + march output), steady -// state RENDER_TARGET, rebuilt on resize. -bool EnsureSsrTargets(const NativeGuestOutputRenderContext& context) { - const uint32_t sw = std::max(1u, (context.guest_output_width + 1) / 2); - const uint32_t sh = std::max(1u, (context.guest_output_height + 1) / 2); - if (g_r.ssr_width == sw && g_r.ssr_height == sh && g_r.ssr_gbuf != nullptr && - g_r.ssr_tex != nullptr) { - return true; - } - nrhi::Device* device = context.device; - if (g_r.ssr_gbuf_depth != nullptr) { - device->DestroyDeferred(g_r.ssr_gbuf_depth); - g_r.ssr_gbuf_depth = nullptr; - } - { - nrhi::TextureDesc dd; - dd.width = sw; - dd.height = sh; - dd.format = nrhi::Format::kD32_FLOAT; - dd.usage = nrhi::kTextureUsageDepthStencil; - dd.initial_state = nrhi::ResourceState::kDepthWrite; - dd.clear_depth = 1.0f; - g_r.ssr_gbuf_depth = device->CreateTexture(dd); - if (g_r.ssr_gbuf_depth == nullptr) { - g_r.ssr_failed = true; - return false; - } - } - nrhi::Texture** res[2] = {&g_r.ssr_gbuf, &g_r.ssr_tex}; - nrhi::TextureView** views[2] = {&g_r.ssr_gbuf_srv, &g_r.ssr_srv}; - for (int i = 0; i < 2; ++i) { - if (*views[i] != nullptr) { - device->DestroyDeferred(*views[i]); - *views[i] = nullptr; - } - if (*res[i] != nullptr) { - device->DestroyDeferred(*res[i]); - *res[i] = nullptr; - } - nrhi::TextureDesc desc; - desc.width = sw; - desc.height = sh; - desc.mip_levels = 1; - desc.format = nrhi::Format::kR16G16B16A16_FLOAT; - desc.usage = nrhi::kTextureUsageRenderTarget; - desc.initial_state = nrhi::ResourceState::kRenderTarget; - *res[i] = device->CreateTexture(desc); - if (*res[i] == nullptr) { - g_r.ssr_failed = true; - return false; - } - nrhi::TextureViewDesc vd; - vd.mip_levels = 1; - *views[i] = device->CreateTextureView(*res[i], vd); - if (*views[i] == nullptr) { - g_r.ssr_failed = true; - return false; - } - } - g_r.ssr_width = sw; - g_r.ssr_height = sh; - return true; -} - -// SSR march + composite (see ssr.hlsl): consumes the reflection G-buffer -// the in-pass draw left in PIXEL_SHADER_RESOURCE, marches the full-res -// linear depth (reusing the SSAO linearize plane when AO ran this frame) -// while sampling the pre-tonemap HDR plane, and blends the result back onto -// it. Returns true when it drew; the binding layout changed and the -// caller's post_ran block restores the main-pass bindings. -bool ApplySsrPass(const NativeGuestOutputRenderContext& context, - nrhi::Cmd* cmd, const FrameScene& scene, - const nrhi::Viewport& viewport, const nrhi::Rect& scissor, - bool lin_depth_ready) { - if (!g_r.ssr_gbuf_ready) { - return false; - } - // Consume the handoff up front: every exit below must leave the G-buffer - // back in its RENDER_TARGET steady state. - g_r.ssr_gbuf_ready = false; - nrhi::Device* device = context.device; - const uint32_t width = context.guest_output_width; - const uint32_t height = context.guest_output_height; - const auto bail = [&] { - cmd->Barrier(g_r.ssr_gbuf, nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - cmd->FlushBarriers(); - return false; - }; - // Full-res linear view-Z: the SSAO plane when AO linearized this frame - // (it idles back in RENDER_TARGET after the AO pass), else the own - // linearize below. - lin_depth_ready = lin_depth_ready && g_r.ao_lin_depth != nullptr && - g_r.ao_lin_srv != nullptr && g_r.ao_lin_width == width && - g_r.ao_lin_height == height; - if (!lin_depth_ready) { - if (g_r.ssr_lin_depth == nullptr || g_r.ssr_lin_width != width || - g_r.ssr_lin_height != height) { - if (g_r.ssr_lin_srv != nullptr) { - device->DestroyDeferred(g_r.ssr_lin_srv); - g_r.ssr_lin_srv = nullptr; - } - if (g_r.ssr_lin_depth != nullptr) { - device->DestroyDeferred(g_r.ssr_lin_depth); - g_r.ssr_lin_depth = nullptr; - } - nrhi::TextureDesc desc; - desc.width = width; - desc.height = height; - desc.mip_levels = 1; - desc.format = nrhi::Format::kR32_FLOAT; - desc.usage = nrhi::kTextureUsageRenderTarget; - desc.initial_state = nrhi::ResourceState::kRenderTarget; - g_r.ssr_lin_depth = device->CreateTexture(desc); - if (g_r.ssr_lin_depth == nullptr) { - g_r.ssr_failed = true; - return bail(); - } - nrhi::TextureViewDesc vd; - vd.mip_levels = 1; - g_r.ssr_lin_srv = device->CreateTextureView(g_r.ssr_lin_depth, vd); - if (g_r.ssr_lin_srv == nullptr) { - g_r.ssr_failed = true; - return bail(); - } - g_r.ssr_lin_width = width; - g_r.ssr_lin_height = height; - } - // Scene-depth SRV, re-pointed when the depth buffer is rebuilt. - if (g_r.ssr_depth_srv == nullptr || g_r.ssr_depth_srv_of != g_r.depth) { - if (g_r.ssr_depth_srv != nullptr) { - device->DestroyDeferred(g_r.ssr_depth_srv); - g_r.ssr_depth_srv = nullptr; - } - nrhi::TextureViewDesc sd; - if (g_r.msaa > 1) { - sd.dimension = nrhi::ViewDimension::k2DMS; - } else { - sd.dimension = nrhi::ViewDimension::k2D; - sd.mip_levels = 1; - } - g_r.ssr_depth_srv = device->CreateTextureView(g_r.depth, sd); - if (g_r.ssr_depth_srv == nullptr) { - g_r.ssr_failed = true; - return bail(); - } - g_r.ssr_depth_srv_of = g_r.depth; - } - } - nrhi::Texture* const lin_tex = - lin_depth_ready ? g_r.ao_lin_depth : g_r.ssr_lin_depth; - nrhi::TextureView* const lin_srv = - lin_depth_ready ? g_r.ao_lin_srv : g_r.ssr_lin_srv; - - // b0 rows (see ssr.hlsl cbuffer C): dest size, projection, the world -> - // "AO view space" rotation, tuning. view = view_proj * proj^-1 (row- - // vector); the projection's per-axis signs fold into the rotation so - // normals land in the same mirrored space ViewPos reconstructs positions - // in; reflection directions are mirror-SENSITIVE, unlike every AO term. - const float* pr = scene.proj; - float c[32] = {}; - c[0] = float(g_r.ssr_width); - c[1] = float(g_r.ssr_height); - c[2] = 1.0f / float(g_r.ssr_width); - c[3] = 1.0f / float(g_r.ssr_height); - c[4] = std::fabs(pr[0]); - c[5] = std::fabs(pr[5]); - c[6] = pr[10]; - c[7] = pr[14]; - const float sxs = pr[0] < 0.0f ? -1.0f : 1.0f; - const float sys = pr[5] < 0.0f ? -1.0f : 1.0f; - const float inv00 = 1.0f / pr[0]; - const float inv11 = 1.0f / pr[5]; - for (int r = 0; r < 3; ++r) { - // proj^-1 columns: x/y scale by 1/m00 / 1/m11, view z = clip w (the - // view_proj row's w column); the AO-space axis signs multiply in. - c[8 + r * 4 + 0] = scene.view_proj[r * 4 + 0] * inv00 * sxs; - c[8 + r * 4 + 1] = scene.view_proj[r * 4 + 1] * inv11 * sys; - c[8 + r * 4 + 2] = scene.view_proj[r * 4 + 3]; - } - c[20] = float(REXCVAR_GET(skate3_native_render_scene_ssr_steps)); - c[21] = float(REXCVAR_GET(skate3_native_render_scene_ssr_thickness)); - c[22] = float(REXCVAR_GET(skate3_native_render_scene_ssr_intensity)); - c[23] = float(REXCVAR_GET(skate3_native_render_scene_ssr_debug)); - c[24] = 250.0f; // max ray distance (view units; the screen clip governs) - - cmd->SetBindingLayout(g_r.ssr_layout); - cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList); - cmd->SetRootConstants(0, 32, c, 0); - - // 1) Own linearize when AO did not produce the plane this frame. - if (!lin_depth_ready) { - cmd->Barrier(g_r.depth, nrhi::ResourceState::kDepthWrite, - nrhi::ResourceState::kPixelShaderResource); - cmd->FlushBarriers(); - cmd->SetViewport(viewport); - cmd->SetScissor(scissor); - cmd->SetRenderTargets(g_r.ssr_lin_depth, nullptr); - cmd->SetPipeline(g_r.pso_ssr_linearize); - cmd->SetTexture(1, g_r.ssr_depth_srv); - cmd->Draw(3, 0); - cmd->Barrier(g_r.depth, nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kDepthWrite); - } - cmd->Barrier(lin_tex, nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kPixelShaderResource); - - // 2) March (half res): the HDR plane pauses as a sampled source. - cmd->Barrier(g_r.hdr_resolved, nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kPixelShaderResource); - cmd->FlushBarriers(); - const nrhi::Viewport ssr_vp{0.0f, 0.0f, float(g_r.ssr_width), - float(g_r.ssr_height), 0.0f, 1.0f}; - const nrhi::Rect ssr_sc{0, 0, int32_t(g_r.ssr_width), - int32_t(g_r.ssr_height)}; - cmd->SetViewport(ssr_vp); - cmd->SetScissor(ssr_sc); - cmd->SetRenderTargets(g_r.ssr_tex, nullptr); - cmd->SetPipeline(g_r.pso_ssr_march); - cmd->SetTexture(1, g_r.ssr_gbuf_srv); - cmd->SetTexture(2, lin_srv); - cmd->SetTexture(3, g_r.hdr_srv); - cmd->Draw(3, 0); - cmd->Barrier(g_r.ssr_tex, nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kPixelShaderResource); - cmd->Barrier(g_r.hdr_resolved, nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - cmd->FlushBarriers(); - - // 3) Composite (full res, src-alpha blend onto the HDR plane). Dest-size - // constants switch to the full raster; the rest carries over. - c[0] = float(width); - c[1] = float(height); - c[2] = 1.0f / float(width); - c[3] = 1.0f / float(height); - cmd->SetRootConstants(0, 4, c, 0); - cmd->SetViewport(viewport); - cmd->SetScissor(scissor); - cmd->SetRenderTargets(g_r.hdr_resolved, nullptr); - cmd->SetPipeline(g_r.pso_ssr_composite); - cmd->SetTexture(1, g_r.ssr_srv); - cmd->SetTexture(2, g_r.ssr_gbuf_srv); - cmd->SetTexture(3, lin_srv); - cmd->Draw(3, 0); - - // Restore intermediate steady states. - cmd->Barrier(g_r.ssr_tex, nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - cmd->Barrier(lin_tex, nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - cmd->Barrier(g_r.ssr_gbuf, nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - cmd->FlushBarriers(); - return true; -} - -// ---- HDR post chain (hdr.hlsl: bloom pyramid + the extracted tonemap) ---- -// Layout + PSOs, built when the HDR path activates and rebuilt when the -// guest output format (the tonemap target) changes. Failure falls back to -// the classic in-material tonemap instead of killing the native renderer. -bool EnsureHdrPipeline(const NativeGuestOutputRenderContext& context) { - if (!g_r.hdr_active || g_r.hdr_failed) { - return false; - } - if (g_r.pso_tonemap != nullptr && - g_r.hdr_pso_out_format == context.guest_output->format()) { - return true; - } - nrhi::Device* device = context.device; - const auto fail = [&](const char* what) { - REXLOG_WARN( - "native-scene: HDR pipeline setup failed ({}), falling back to the " - "classic tonemap path", - what); - g_r.hdr_failed = true; - return false; - }; - if (g_r.hdr_layout == nullptr) { - // The SSAO layout shape plus a third single-texture table (t2 = the - // fused AO multiplier plane): root constants b0 + three single-texture - // tables + point/linear clamp samplers (hdr.hlsl's frozen Vulkan plan). - nrhi::BindingLayoutDesc ld; - ld.param_count = 4; - ld.params[0] = {nrhi::BindingParamKind::kConstants, /*b*/ 0, 16, - nrhi::Visibility::kAll}; - ld.params[1] = {nrhi::BindingParamKind::kTextureTable, /*t*/ 0, 1, - nrhi::Visibility::kPixel}; - ld.params[2] = {nrhi::BindingParamKind::kTextureTable, 1, 1, - nrhi::Visibility::kPixel}; - ld.params[3] = {nrhi::BindingParamKind::kTextureTable, 2, 1, - nrhi::Visibility::kPixel}; - ld.static_sampler_count = 2; - ld.static_samplers[0] = {/*s*/ 0, nrhi::Filter::kPoint, - nrhi::AddressMode::kClamp, 1}; - ld.static_samplers[1] = {1, nrhi::Filter::kLinear, - nrhi::AddressMode::kClamp, 1}; - ld.allow_input_layout = false; - g_r.hdr_layout = device->CreateBindingLayout(ld); - if (g_r.hdr_layout == nullptr) { - return fail("binding layout"); - } - } - for (nrhi::Pipeline** p : {&g_r.pso_bloom_first, &g_r.pso_bloom_down, - &g_r.pso_bloom_up, &g_r.pso_tonemap}) { - if (*p != nullptr) { - device->DestroyDeferred(*p); - *p = nullptr; - } - } - const nrhi::ShaderMacro first_defs[] = {{"BLOOM_FIRST", "1"}, - {nullptr, nullptr}}; - nrhi::Shader* vs = device->CreateShader( - MakeShaderDesc(nrhi::ShaderStage::kVertex, "hdr.hlsl", kHdrShaderSource, - "vs_main", nullptr, "")); - nrhi::Shader* ps_first = device->CreateShader(MakeShaderDesc( - nrhi::ShaderStage::kPixel, "hdr.hlsl", kHdrShaderSource, - "ps_bloom_down", first_defs, "BLOOM_FIRST=1")); - nrhi::Shader* ps_down = device->CreateShader( - MakeShaderDesc(nrhi::ShaderStage::kPixel, "hdr.hlsl", kHdrShaderSource, - "ps_bloom_down", nullptr, "")); - nrhi::Shader* ps_up = device->CreateShader( - MakeShaderDesc(nrhi::ShaderStage::kPixel, "hdr.hlsl", kHdrShaderSource, - "ps_bloom_up", nullptr, "")); - nrhi::Shader* ps_tone = device->CreateShader( - MakeShaderDesc(nrhi::ShaderStage::kPixel, "hdr.hlsl", kHdrShaderSource, - "ps_tonemap", nullptr, "")); - const bool shaders_ok = vs != nullptr && ps_first != nullptr && - ps_down != nullptr && ps_up != nullptr && - ps_tone != nullptr; - if (!shaders_ok) { - for (nrhi::Shader* s : {vs, ps_first, ps_down, ps_up, ps_tone}) { - device->DestroyDeferred(s); - } - return fail("shader compile"); - } - nrhi::GraphicsPipelineDesc pso; - pso.layout = g_r.hdr_layout; - pso.vs = vs; - pso.cull = nrhi::CullMode::kNone; - pso.sample_count = 1; - pso.rtv_format = nrhi::Format::kR16G16B16A16_FLOAT; - pso.ps = ps_first; - g_r.pso_bloom_first = device->CreateGraphicsPipeline(pso); - pso.ps = ps_down; - g_r.pso_bloom_down = device->CreateGraphicsPipeline(pso); - // Progressive upsample: additive ONE/ONE onto the next-larger level. - pso.ps = ps_up; - pso.blend.enable = true; - pso.blend.src = nrhi::BlendFactor::kOne; - pso.blend.dst = nrhi::BlendFactor::kOne; - pso.blend.op = nrhi::BlendOp::kAdd; - pso.blend.src_alpha = nrhi::BlendFactor::kOne; - pso.blend.dst_alpha = nrhi::BlendFactor::kOne; - pso.blend.op_alpha = nrhi::BlendOp::kAdd; - g_r.pso_bloom_up = device->CreateGraphicsPipeline(pso); - pso.blend = {}; - pso.ps = ps_tone; - pso.rtv_format = context.guest_output->format(); - g_r.pso_tonemap = device->CreateGraphicsPipeline(pso); - for (nrhi::Shader* s : {vs, ps_first, ps_down, ps_up, ps_tone}) { - device->DestroyDeferred(s); - } - if (g_r.pso_bloom_first == nullptr || g_r.pso_bloom_down == nullptr || - g_r.pso_bloom_up == nullptr || g_r.pso_tonemap == nullptr) { - return fail("pso"); - } - g_r.hdr_pso_out_format = context.guest_output->format(); - REXLOG_INFO("native-scene: HDR post pipeline created"); - return true; -} - -// Bloom pyramid + tonemap over the float scene plane into the guest output. -// Runs after the AO composite (bloom sees the occluded scene). Transitions -// the guest output kGuestOutput -> kRenderTarget (the state every later -// consumer expects); the caller restores the main binding layout after. -void ApplyHdrPost(const NativeGuestOutputRenderContext& context, - nrhi::Cmd* cmd, const nrhi::Viewport& viewport, - const nrhi::Rect& scissor, bool loading_native) { - nrhi::Device* device = context.device; - const uint32_t width = context.guest_output_width; - const uint32_t height = context.guest_output_height; - // Bloom chain intermediates: QUARTER res halving down to the level cap or - // an 8-px floor, RGBA16F, steady state RENDER_TARGET. Quarter start = - // 1/4 the pyramid cost of a half-res chain; the extraction pass widens - // its 13-tap spread (p1.x = 2) so the 4x reduction still integrates the - // whole source block. Creation failure only loses bloom (the tonemap - // still runs). - if (g_r.bloom_base_w != width || g_r.bloom_base_h != height || - (g_r.bloom_levels > 0 && g_r.bloom_tex[0] == nullptr)) { - for (uint32_t i = 0; i < RendererState::kBloomMaxLevels; ++i) { - if (g_r.bloom_srv[i] != nullptr) { - device->DestroyDeferred(g_r.bloom_srv[i]); - g_r.bloom_srv[i] = nullptr; - } - if (g_r.bloom_tex[i] != nullptr) { - device->DestroyDeferred(g_r.bloom_tex[i]); - g_r.bloom_tex[i] = nullptr; - } - } - g_r.bloom_levels = 0; - uint32_t w = std::max(1u, (width + 1) / 2); - uint32_t h = std::max(1u, (height + 1) / 2); - for (uint32_t i = 0; i < RendererState::kBloomMaxLevels; ++i) { - w = std::max(1u, (w + 1) / 2); - h = std::max(1u, (h + 1) / 2); - if (w < 8 || h < 8) { - break; - } - nrhi::TextureDesc desc; - desc.width = w; - desc.height = h; - desc.mip_levels = 1; - desc.format = nrhi::Format::kR16G16B16A16_FLOAT; - desc.usage = nrhi::kTextureUsageRenderTarget; - desc.initial_state = nrhi::ResourceState::kRenderTarget; - g_r.bloom_tex[i] = device->CreateTexture(desc); - if (g_r.bloom_tex[i] == nullptr) { - break; - } - nrhi::TextureViewDesc vd; - vd.mip_levels = 1; - g_r.bloom_srv[i] = device->CreateTextureView(g_r.bloom_tex[i], vd); - if (g_r.bloom_srv[i] == nullptr) { - device->DestroyDeferred(g_r.bloom_tex[i]); - g_r.bloom_tex[i] = nullptr; - break; - } - g_r.bloom_w[i] = w; - g_r.bloom_h[i] = h; - ++g_r.bloom_levels; - } - g_r.bloom_base_w = width; - g_r.bloom_base_h = height; - if (g_r.bloom_levels == 0) { - REXLOG_WARN("native-scene: bloom chain allocation failed (bloom off)"); - } - } - const bool bloom = REXCVAR_GET(skate3_native_render_scene_bloom) && - !loading_native && g_r.bloom_levels > 0 && - g_r.pso_bloom_first != nullptr && - g_r.pso_bloom_down != nullptr && - g_r.pso_bloom_up != nullptr; - - cmd->SetBindingLayout(g_r.hdr_layout); - cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList); - // The scene plane is the sampled source for the whole chain. - cmd->Barrier(g_r.hdr_resolved, nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kPixelShaderResource); - cmd->FlushBarriers(); - - float c[16]; - const auto set_consts = [&](uint32_t dw, uint32_t dh, uint32_t sw, - uint32_t sh, float intensity, float tap_scale) { - c[0] = float(dw); - c[1] = float(dh); - c[2] = 1.0f / float(dw); - c[3] = 1.0f / float(dh); - c[4] = float(sw); - c[5] = float(sh); - c[6] = 1.0f / float(sw); - c[7] = 1.0f / float(sh); - c[8] = float(REXCVAR_GET(skate3_native_render_scene_bloom_threshold)); - c[9] = float(REXCVAR_GET(skate3_native_render_scene_bloom_knee)); - c[10] = intensity; - // Debug view (ps_tonemap p0.w); the classic ssao_debug cvar maps onto - // the fused AO view when the plane rode along this frame. - c[11] = float(REXCVAR_GET(skate3_native_render_scene_hdr_debug)); - if (c[11] == 0.0f && g_r.ao_plane_in_psr && - REXCVAR_GET(skate3_native_render_scene_ssao_debug)) { - c[11] = 3.0f; - } - c[12] = tap_scale; - c[13] = c[14] = c[15] = 0.0f; - cmd->SetRootConstants(0, 16, c, 0); - }; - - if (bloom) { - // Downsample chain (level 0 extracts with threshold + Karis weighting; - // its 4x reduction doubles the tap spread to cover the source block). - for (uint32_t i = 0; i < g_r.bloom_levels; ++i) { - const uint32_t sw = i == 0 ? width : g_r.bloom_w[i - 1]; - const uint32_t sh = i == 0 ? height : g_r.bloom_h[i - 1]; - set_consts(g_r.bloom_w[i], g_r.bloom_h[i], sw, sh, 0.0f, - i == 0 ? 2.0f : 1.0f); - const nrhi::Viewport vp{0.0f, 0.0f, float(g_r.bloom_w[i]), - float(g_r.bloom_h[i]), 0.0f, 1.0f}; - const nrhi::Rect sc{0, 0, int32_t(g_r.bloom_w[i]), - int32_t(g_r.bloom_h[i])}; - cmd->SetViewport(vp); - cmd->SetScissor(sc); - cmd->SetRenderTargets(g_r.bloom_tex[i], nullptr); - cmd->SetPipeline(i == 0 ? g_r.pso_bloom_first : g_r.pso_bloom_down); - cmd->SetTexture(1, i == 0 ? g_r.hdr_srv : g_r.bloom_srv[i - 1]); - cmd->Draw(3, 0); - cmd->Barrier(g_r.bloom_tex[i], nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kPixelShaderResource); - cmd->FlushBarriers(); - } - // Progressive tent upsample, additive onto each larger level. - for (int32_t i = int32_t(g_r.bloom_levels) - 2; i >= 0; --i) { - cmd->Barrier(g_r.bloom_tex[i], nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - cmd->FlushBarriers(); - set_consts(g_r.bloom_w[i], g_r.bloom_h[i], g_r.bloom_w[i + 1], - g_r.bloom_h[i + 1], 0.0f, 1.0f); - const nrhi::Viewport vp{0.0f, 0.0f, float(g_r.bloom_w[i]), - float(g_r.bloom_h[i]), 0.0f, 1.0f}; - const nrhi::Rect sc{0, 0, int32_t(g_r.bloom_w[i]), - int32_t(g_r.bloom_h[i])}; - cmd->SetViewport(vp); - cmd->SetScissor(sc); - cmd->SetRenderTargets(g_r.bloom_tex[i], nullptr); - cmd->SetPipeline(g_r.pso_bloom_up); - cmd->SetTexture(1, g_r.bloom_srv[i + 1]); - cmd->Draw(3, 0); - cmd->Barrier(g_r.bloom_tex[i], nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kPixelShaderResource); - cmd->FlushBarriers(); - } - } - - // Tonemap into the guest output (the single application of the game's - // shared tone chain; bloom energy joins pre-tonemap). - cmd->Barrier(context.guest_output, nrhi::ResourceState::kGuestOutput, - nrhi::ResourceState::kRenderTarget); - cmd->FlushBarriers(); - set_consts(width, height, width, height, - bloom ? float(REXCVAR_GET( - skate3_native_render_scene_bloom_intensity)) - : 0.0f, - 1.0f); - cmd->SetViewport(viewport); - cmd->SetScissor(scissor); - cmd->SetRenderTargets(context.guest_output, nullptr); - cmd->SetPipeline(g_r.pso_tonemap); - cmd->SetTexture(1, g_r.hdr_srv); - cmd->SetTexture(2, bloom ? g_r.bloom_srv[0] : g_r.white.srv); - // t2 = the fused AO multiplier plane (white = no AO this frame). - cmd->SetTexture(3, g_r.ao_plane_in_psr ? g_r.ao_srv[0] : g_r.white.srv); - cmd->Draw(3, 0); - - // Restore steady states. - cmd->Barrier(g_r.hdr_resolved, nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - if (g_r.ao_plane_in_psr) { - cmd->Barrier(g_r.ao_tex[0], nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - g_r.ao_plane_in_psr = false; - } - if (bloom) { - for (uint32_t i = 0; i < g_r.bloom_levels; ++i) { - cmd->Barrier(g_r.bloom_tex[i], - nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - } - } - cmd->FlushBarriers(); -} - bool EnsurePipeline(const NativeGuestOutputRenderContext& context) { if (g_r.failed) return false; nrhi::Device* device = context.device; @@ -5494,191 +3606,7 @@ bool EnsurePipeline(const NativeGuestOutputRenderContext& context) { return true; } -// ---- Settings-menu backdrop blur (shared native + emulated-post paths) ---- -// Clean separable gaussian over the finished guest output; deliberately -// shares nothing with the game's -// popup chain (that one reproduces the console's 1152x640 lattice on -// purpose). Downsample to half res (plain bilinear = 2x2 box), gaussian H, -// gaussian V, bilinear stretch back. Eased in/out (~60 ms) so open/close -// doesn't pop; the smoothing state is shared across the native and -// emulated-post paths (only one runs per frame). Returns true while the -// effect is active (target > 0 or still easing out). -bool ApplyMenuBlurPass(const NativeGuestOutputRenderContext& context, nrhi::Cmd* cmd, - float target_sigma, bool output_in_guest_output_state) { - static float s_menu_sigma = 0.0f; - static int64_t s_menu_ns = 0; - const int64_t menu_now_ns = std::chrono::duration_cast( - PerfClock::now().time_since_epoch()) - .count(); - float menu_dt = s_menu_ns != 0 ? float(menu_now_ns - s_menu_ns) * 1e-9f : 1.0f; - if (menu_dt > 0.25f) { - s_menu_sigma = 0.0f; - menu_dt = 0.0f; - } - s_menu_ns = menu_now_ns; - const float menu_a = 1.0f - std::exp(-menu_dt / 0.06f); - s_menu_sigma += (target_sigma - s_menu_sigma) * menu_a; - const bool active = target_sigma > 0.0f || s_menu_sigma > 0.05f; - if (s_menu_sigma <= 0.05f || g_r.pso_menu_gauss == nullptr || - g_r.pso_blur_blit == nullptr || g_r.menu_blur_tex[0] == nullptr || - g_r.output_srv_slot == nullptr) { - return active; - } - const float w = float(g_r.menu_blur_w); - const float h = float(g_r.menu_blur_h); - // Sigma is authored in 1080p pixels; halve again for the half-res space. - const float sigma_half = std::max( - 0.3f, s_menu_sigma * (float(context.guest_output_height) / 1080.0f) * 0.5f); - const float radius = std::min(48.0f, std::ceil(sigma_half * 3.0f)); - const nrhi::Viewport half_vp{0.0f, 0.0f, w, h, 0.0f, 1.0f}; - const nrhi::Rect half_sc{0, 0, int32_t(g_r.menu_blur_w), int32_t(g_r.menu_blur_h)}; - const nrhi::Viewport full_vp{0.0f, - 0.0f, - float(context.guest_output_width), - float(context.guest_output_height), - 0.0f, - 1.0f}; - const nrhi::Rect full_sc{0, 0, int32_t(context.guest_output_width), - int32_t(context.guest_output_height)}; - const nrhi::ResourceState output_entry_state = output_in_guest_output_state - ? nrhi::ResourceState::kGuestOutput - : nrhi::ResourceState::kRenderTarget; - // The binding layout must be latched on this cmd before any draw; on the - // emulated-post path this is a FRESH frame state where no layout has been - // set, and nrhi draws silently no-op without one (EnsureDrawState). - // Harmless on the native path (same layout, re-latch). - cmd->SetBindingLayout(g_r.layout); - cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList); - // Downsample: guest output -> menu[0]. - cmd->Barrier(context.guest_output, output_entry_state, - nrhi::ResourceState::kPixelShaderResource); - cmd->FlushBarriers(); - cmd->SetRenderTargets(g_r.menu_blur_tex[0], nullptr); - cmd->SetViewport(half_vp); - cmd->SetScissor(half_sc); - cmd->SetPipeline(g_r.pso_blur_blit); - cmd->SetTexture(1, g_r.output_srv_slot); - cmd->Draw(3, 0); - // H: menu[0] -> menu[1]. - cmd->Barrier(g_r.menu_blur_tex[0], nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kPixelShaderResource); - cmd->FlushBarriers(); - cmd->SetRenderTargets(g_r.menu_blur_tex[1], nullptr); - cmd->SetPipeline(g_r.pso_menu_gauss); - const float h_consts[8] = {1.0f / w, 0.0f, sigma_half, radius, 1.0f, 1.0f, 1.0f, 1.0f}; - cmd->SetRootConstants(0, 8, h_consts, 0); - cmd->SetTexture(1, g_r.menu_blur_srv[0]); - cmd->Draw(3, 0); - // V: menu[1] -> menu[0]. - cmd->Barrier(g_r.menu_blur_tex[1], nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kPixelShaderResource); - cmd->Barrier(g_r.menu_blur_tex[0], nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - cmd->FlushBarriers(); - cmd->SetRenderTargets(g_r.menu_blur_tex[0], nullptr); - const float v_consts[8] = {0.0f, 1.0f / h, sigma_half, radius, 1.0f, 1.0f, 1.0f, 1.0f}; - cmd->SetRootConstants(0, 8, v_consts, 0); - cmd->SetTexture(1, g_r.menu_blur_srv[1]); - cmd->Draw(3, 0); - // Stretch back over the full output. - cmd->Barrier(g_r.menu_blur_tex[0], nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kPixelShaderResource); - cmd->Barrier(context.guest_output, nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - cmd->FlushBarriers(); - cmd->SetRenderTargets(context.guest_output, nullptr); - cmd->SetViewport(full_vp); - cmd->SetScissor(full_sc); - cmd->SetPipeline(g_r.pso_blur_blit); - cmd->SetTexture(1, g_r.menu_blur_srv[0]); - cmd->Draw(3, 0); - // Restore steady states (the intermediates stay in RENDER_TARGET; the - // output goes back to the state the caller's path expects). - cmd->Barrier(g_r.menu_blur_tex[0], nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - cmd->Barrier(g_r.menu_blur_tex[1], nrhi::ResourceState::kPixelShaderResource, - nrhi::ResourceState::kRenderTarget); - if (output_in_guest_output_state) { - cmd->Barrier(context.guest_output, nrhi::ResourceState::kRenderTarget, - nrhi::ResourceState::kGuestOutput); - cmd->FlushBarriers(); - } - return active; -} - -// Minimal resource bring-up for the emulated-output post-processor path: the -// full EnsurePipeline never runs while the native renderer is disabled or -// yielding, so the blur ensures just what it needs (binding layout, the blur -// PSOs, the half-res intermediates, and a sampled view of the output). -bool EnsureMenuBlurStandalone(const NativeGuestOutputRenderContext& context) { - if (g_r.failed) { - return false; - } - if (!EnsureRootSignature(context)) { - return false; - } - if (g_r.pso_menu_gauss == nullptr) { - static bool s_attempted = false; - if (s_attempted) { - return false; // creation failed once; don't hammer every frame - } - s_attempted = true; - EnsureBlurPsos(context); - if (g_r.pso_menu_gauss == nullptr) { - return false; - } - } - EnsureBlurOutlineTargets(context); - if (g_r.menu_blur_tex[0] == nullptr) { - return false; - } - if (g_r.rtv_resource != context.guest_output) { - if (g_r.output_srv_slot) { - context.device->DestroyDeferred(g_r.output_srv_slot); - g_r.output_srv_slot = nullptr; - } - nrhi::TextureViewDesc vd; - vd.mip_levels = 1; - g_r.output_srv_slot = context.device->CreateTextureView(context.guest_output, vd); - g_r.output_srv_allocated = g_r.output_srv_slot != nullptr; - g_r.rtv_resource = context.guest_output; - } - return g_r.output_srv_slot != nullptr; -} - -// Registered with the command processors: invoked at the end of the EMULATED -// guest-output refresh (native-rendered frames apply the blur inline in -// RenderScene). Covers boot/startup frames before the native scene takes -// over and manual emulated mode. -void PostProcessGuestOutput(const NativeGuestOutputRenderContext& context, void* /*user_data*/) { - const float target = g_settings_menu_blur.load(std::memory_order_relaxed) - ? float(REXCVAR_GET(skate3_menu_blur_sigma)) - : 0.0f; - // Diagnostics (throttled): the emulated-path blur has several silent - // early-outs; log which leg each invocation takes. - const bool log_this = g_post_blur_log_count.load(std::memory_order_relaxed) < 8; - const bool ensured = EnsureMenuBlurStandalone(context); - if (log_this) { - g_post_blur_log_count.fetch_add(1, std::memory_order_relaxed); - REXLOG_INFO( - "native-scene: emulated-post blur invoked (target={:.1f} ensured={} pso={} tex={} " - "srv={} failed={})", - target, ensured, (void*)g_r.pso_menu_gauss, (void*)g_r.menu_blur_tex[0], - (void*)g_r.output_srv_slot, g_r.failed); - } - if (!ensured) { - if (target <= 0.0f) { - rex::graphics::RequestNativeGuestOutputPostProcess(false); - } - return; - } - if (!ApplyMenuBlurPass(context, context.cmd, target, - /*output_in_guest_output_state=*/true)) { - // Fully eased out: stop the per-frame post-process invocations until the - // menu opens again. - rex::graphics::RequestNativeGuestOutputPostProcess(false); - } -} +namespace { // ---- Warm decode (loading-screen prewarm + gameplay warmup) -------------- // Decodes, within `deadline`, every GPU resource `item`'s draw would @@ -5713,9 +3641,9 @@ void WarmItemResources(const NativeGuestOutputRenderContext& context, uint8_t* b // Ropa meshes are excluded: the cloth sim rewrites their payload every // frame BY DESIGN (fingerprint never converges), so revalidating them // here just destroys+redecodes the base mesh once per settle frame - - // the churn that kept the settle pass extending itself (log 1624: - // mesh 40D30140 re-fingerprinting 4x in 50ms). The draw path serves - // ropa content from the ropa ring / shape generations regardless. + // the churn that kept the settle pass extending itself (observed: one + // mesh re-fingerprinting 4x in 50 ms). The draw path serves ropa + // content from the ropa ring / shape generations regardless. if (within()) { if (g_warm_mesh_log_budget.load(std::memory_order_relaxed) > 0 && g_warm_mesh_log_budget.fetch_sub(1, std::memory_order_relaxed) > 0) { @@ -11499,3 +9427,4 @@ void Install() {} } // namespace skate3::native_scene #endif // REX_HAS_D3D12 || REX_HAS_VULKAN + diff --git a/src/skate3_native_scene_gpu_internal.h b/src/skate3_native_scene_gpu_internal.h new file mode 100644 index 0000000..386186c --- /dev/null +++ b/src/skate3_native_scene_gpu_internal.h @@ -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 +#include +#include +#include +#include + +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 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(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(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 g_heal_verify_fail{0}; +inline std::atomic 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 g_tex_sticky_served{0}; +inline std::atomic 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 g_trace_keys; +inline std::unordered_map 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 g_ad_stale_served{0}; +inline std::atomic 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 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 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 verts; // num_verts x 14 floats (scene VS layout) + }; + std::unordered_map> 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 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 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 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 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 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 diff --git a/src/skate3_native_scene_post.cpp b/src/skate3_native_scene_post.cpp new file mode 100644 index 0000000..fb4af81 --- /dev/null +++ b/src/skate3_native_scene_post.cpp @@ -0,0 +1,1340 @@ +// Native scene renderer, post-process passes (render thread): SSAO (GTAO), +// screen-space reflections, the HDR bloom/tonemap chain, and the settings-menu +// backdrop blur (including the blur-over-emulated-frames post processor). +// Shared GPU state lives in skate3_native_scene_gpu_internal.h. + +#include "skate3_native_scene.h" + +#include "generated/skate3_init.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#endif + +#include +#include +#include +#include + +#include "native/skate3_native_diag.h" +#include "native/skate3_native_entity.h" +#include "native/skate3_native_guest_read.h" +#include "native/skate3_native_lw.h" +#include "native/skate3_native_palette.h" +// Offline-compiled SPIR-V for the native shaders (compiled from the HLSL +// sources with DXC): the Vulkan RHI backend consumes these blobs; the D3D12 +// backend runtime-compiles the embedded HLSL as before. +#include "native/shaders/spirv/skate3_native_shaders_spirv.h" + +#if (defined(REX_HAS_D3D12) && REX_HAS_D3D12) || (defined(REX_HAS_VULKAN) && REX_HAS_VULKAN) +#include +#include +#include +#include +#if defined(_WIN32) +#include +#endif +#endif +#include "skate3_native_scene_gpu_internal.h" + +// Cvars defined in skate3_native_scene.cpp. +REXCVAR_DECLARE(double, skate3_menu_blur_sigma); +REXCVAR_DECLARE(bool, skate3_native_render_scene_bloom); +REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_intensity); +REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_knee); +REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_threshold); +REXCVAR_DECLARE(bool, skate3_native_render_scene_hdr); +REXCVAR_DECLARE(int32_t, skate3_native_render_scene_hdr_debug); +REXCVAR_DECLARE(bool, skate3_native_render_scene_hdr_packed); +REXCVAR_DECLARE(bool, skate3_native_render_scene_ssao_debug); +REXCVAR_DECLARE(bool, skate3_native_render_scene_ssao_full_res); +REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_intensity); +REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_luma_protect); +REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_radius); +REXCVAR_DECLARE(int32_t, skate3_native_render_scene_ssr_debug); +REXCVAR_DECLARE(double, skate3_native_render_scene_ssr_intensity); +REXCVAR_DECLARE(int32_t, skate3_native_render_scene_ssr_steps); +REXCVAR_DECLARE(double, skate3_native_render_scene_ssr_thickness); + +#if (defined(REX_HAS_D3D12) && REX_HAS_D3D12) || (defined(REX_HAS_VULKAN) && REX_HAS_VULKAN) + +#include "skate3_native_shaders.h" + +namespace skate3::native_scene { + +// ---- Screen-space ambient occlusion (ssao.hlsl: GTAO) -------------------- +// Layout + PSOs, built lazily on the first enabled frame and rebuilt when +// the MSAA level (linearize shader variant) or the output format (composite +// target) changes. Failure disables the effect only. +bool EnsureSsaoPipeline(const NativeGuestOutputRenderContext& context) { + if (g_r.ao_failed) { + return false; + } + // Composite target: the float HDR plane (pre-tonemap multiply) or the + // gamma guest output (classic). + const nrhi::Format ao_out_fmt = g_r.hdr_active + ? g_r.hdr_scene_format + : context.guest_output->format(); + if (g_r.pso_ao_linearize != nullptr && g_r.ao_msaa == g_r.msaa && + g_r.ao_rtv_format == ao_out_fmt && g_r.ao_hdr == g_r.hdr_active) { + return true; + } + nrhi::Device* device = context.device; + const auto fail = [&](const char* what) { + REXLOG_ERROR("native-scene: ssao pipeline setup failed ({})", what); + g_r.ao_failed = true; + return false; + }; + if (g_r.ao_layout == nullptr) { + nrhi::BindingLayoutDesc ld; + ld.param_count = 3; + ld.params[0] = {nrhi::BindingParamKind::kConstants, /*b*/ 0, 16, + nrhi::Visibility::kAll}; + ld.params[1] = {nrhi::BindingParamKind::kTextureTable, /*t*/ 0, 1, + nrhi::Visibility::kPixel}; + ld.params[2] = {nrhi::BindingParamKind::kTextureTable, 1, 1, + nrhi::Visibility::kPixel}; + ld.static_sampler_count = 2; + ld.static_samplers[0] = {/*s*/ 0, nrhi::Filter::kPoint, + nrhi::AddressMode::kClamp, 1}; + ld.static_samplers[1] = {1, nrhi::Filter::kLinear, + nrhi::AddressMode::kClamp, 1}; + ld.allow_input_layout = false; + g_r.ao_layout = device->CreateBindingLayout(ld); + if (g_r.ao_layout == nullptr) { + return fail("binding layout"); + } + } + nrhi::Pipeline** psos[6] = {&g_r.pso_ao_linearize, &g_r.pso_ao_gtao, + &g_r.pso_ao_blur, &g_r.pso_ao_luma, + &g_r.pso_ao_composite, &g_r.pso_ao_debug}; + for (nrhi::Pipeline** p : psos) { + if (*p != nullptr) { + device->DestroyDeferred(*p); + *p = nullptr; + } + } + const bool msaa = g_r.msaa > 1; + const nrhi::ShaderMacro msaa_defs[] = {{"AO_MSAA", "1"}, {nullptr, nullptr}}; + // HDR=1 on the luma pass (tonemap the pre-tonemap scene before the + // protection luma) and the march (linear-space intensity exponent). + const nrhi::ShaderMacro hdr_defs[] = {{"HDR", "1"}, {nullptr, nullptr}}; + nrhi::Shader* vs = device->CreateShader( + MakeShaderDesc(nrhi::ShaderStage::kVertex, "ssao.hlsl", + kSsaoShaderSource, "vs_main", nullptr, "")); + const char* ps_entries[6] = {"ps_linearize", "ps_gtao", "ps_blur", + "ps_luma", "ps_composite", "ps_debug"}; + nrhi::Shader* ps[6] = {}; + bool ok = vs != nullptr; + for (int i = 0; i < 6; ++i) { + const bool msaa_entry = i == 0 && msaa; + const bool hdr_entry = (i == 1 || i == 3) && g_r.hdr_active; + ps[i] = device->CreateShader(MakeShaderDesc( + nrhi::ShaderStage::kPixel, "ssao.hlsl", kSsaoShaderSource, + ps_entries[i], + msaa_entry ? msaa_defs : (hdr_entry ? hdr_defs : nullptr), + msaa_entry ? "AO_MSAA=1" : (hdr_entry ? "HDR=1" : ""))); + ok = ok && ps[i] != nullptr; + } + if (!ok) { + device->DestroyDeferred(vs); + for (nrhi::Shader* s : ps) { + device->DestroyDeferred(s); + } + return fail("shader compile"); + } + nrhi::GraphicsPipelineDesc pso; + pso.layout = g_r.ao_layout; + pso.vs = vs; + pso.cull = nrhi::CullMode::kNone; + pso.sample_count = 1; + pso.ps = ps[0]; + pso.rtv_format = nrhi::Format::kR32_FLOAT; + g_r.pso_ao_linearize = device->CreateGraphicsPipeline(pso); + pso.ps = ps[1]; + pso.rtv_format = nrhi::Format::kR8_UNORM; + g_r.pso_ao_gtao = device->CreateGraphicsPipeline(pso); + pso.ps = ps[2]; + g_r.pso_ao_blur = device->CreateGraphicsPipeline(pso); + pso.ps = ps[3]; + g_r.pso_ao_luma = device->CreateGraphicsPipeline(pso); + // Composite: dst.rgb * src.a; the AO plane carries the finished + // multiplier, and the RGB-only write mask keeps the output's alpha + // untouched. + pso.ps = ps[4]; + pso.rtv_format = ao_out_fmt; + pso.blend.enable = true; + pso.blend.src = nrhi::BlendFactor::kZero; + pso.blend.dst = nrhi::BlendFactor::kSrcAlpha; + pso.blend.op = nrhi::BlendOp::kAdd; + pso.blend.src_alpha = nrhi::BlendFactor::kZero; + pso.blend.dst_alpha = nrhi::BlendFactor::kOne; + pso.blend.op_alpha = nrhi::BlendOp::kAdd; + pso.blend.write_mask = 0x7; + g_r.pso_ao_composite = device->CreateGraphicsPipeline(pso); + pso.blend = {}; + pso.ps = ps[5]; + g_r.pso_ao_debug = device->CreateGraphicsPipeline(pso); + device->DestroyDeferred(vs); + for (nrhi::Shader* s : ps) { + device->DestroyDeferred(s); + } + if (g_r.pso_ao_linearize == nullptr || g_r.pso_ao_gtao == nullptr || + g_r.pso_ao_blur == nullptr || g_r.pso_ao_luma == nullptr || + g_r.pso_ao_composite == nullptr || g_r.pso_ao_debug == nullptr) { + return fail("pso"); + } + g_r.ao_msaa = g_r.msaa; + g_r.ao_rtv_format = ao_out_fmt; + g_r.ao_hdr = g_r.hdr_active; + REXLOG_INFO("native-scene: ssao pipeline created (MSAA x{}, {})", g_r.msaa, + g_r.hdr_active ? "HDR" : "classic"); + return true; +} + +// GTAO chain over the resolved scene (see ssao.hlsl): linearize depth, +// horizon-based AO, depth-aware separable blur, multiply-composite onto the +// guest output (which must be in RENDER_TARGET state). Returns true when it +// drew; binding layout/state was changed and the caller restores the main +// pass's bindings. +bool ApplySsaoPass(const NativeGuestOutputRenderContext& context, + nrhi::Cmd* cmd, const FrameScene& scene, + const nrhi::Viewport& viewport, const nrhi::Rect& scissor) { + // The published projection must be the live perspective matrix (row-vector + // m23 == 1; all zeros until the first publish). + const float* pr = scene.proj; + if (pr[11] != 1.0f || pr[0] == 0.0f || pr[5] == 0.0f || pr[14] == 0.0f) { + return false; + } + if (!EnsureSsaoPipeline(context)) { + return false; + } + // The luma pass samples the scene through this view; the composite writes + // the same plane (HDR: the float scene plane pre-tonemap, classic: the + // guest output). + const bool hdr = g_r.hdr_active && g_r.hdr_resolved != nullptr; + nrhi::Texture* const scene_plane = + hdr ? g_r.hdr_resolved : context.guest_output; + nrhi::TextureView* const scene_srv = + hdr ? g_r.hdr_srv : g_r.output_srv_slot; + if (scene_srv == nullptr) { + return false; + } + nrhi::Device* device = context.device; + const uint32_t width = context.guest_output_width; + const uint32_t height = context.guest_output_height; + // AO raster: half the output resolution by default: for a low-frequency + // term the depth-aware blur + bilinear upsample hide it, at 1/4 the march + // cost (full-res GTAO at 4K x 300+ uncapped fps pegged the GPU). The + // linearized depth stays full res: the march samples it at arbitrary UVs + // either way, and it keeps silhouette depth exact. + const bool ao_full = + REXCVAR_GET(skate3_native_render_scene_ssao_full_res); + const uint32_t aw = ao_full ? width : std::max(1u, (width + 1) / 2); + const uint32_t ah = ao_full ? height : std::max(1u, (height + 1) / 2); + // Intermediates (steady state RENDER_TARGET), rebuilt on either size. + if (g_r.ao_width != aw || g_r.ao_height != ah || + g_r.ao_lin_width != width || g_r.ao_lin_height != height || + g_r.ao_lin_depth == nullptr || g_r.ao_luma == nullptr) { + nrhi::Texture** res[4] = {&g_r.ao_lin_depth, &g_r.ao_tex[0], + &g_r.ao_tex[1], &g_r.ao_luma}; + nrhi::TextureView** views[4] = {&g_r.ao_lin_srv, &g_r.ao_srv[0], + &g_r.ao_srv[1], &g_r.ao_luma_srv}; + for (int i = 0; i < 4; ++i) { + if (*views[i] != nullptr) { + device->DestroyDeferred(*views[i]); + *views[i] = nullptr; + } + if (*res[i] != nullptr) { + device->DestroyDeferred(*res[i]); + *res[i] = nullptr; + } + nrhi::TextureDesc desc; + desc.width = i == 0 ? width : aw; + desc.height = i == 0 ? height : ah; + desc.mip_levels = 1; + desc.format = + i == 0 ? nrhi::Format::kR32_FLOAT : nrhi::Format::kR8_UNORM; + desc.usage = nrhi::kTextureUsageRenderTarget; + desc.initial_state = nrhi::ResourceState::kRenderTarget; + *res[i] = device->CreateTexture(desc); + if (*res[i] == nullptr) { + g_r.ao_failed = true; + return false; + } + nrhi::TextureViewDesc vd; + vd.mip_levels = 1; + *views[i] = device->CreateTextureView(*res[i], vd); + if (*views[i] == nullptr) { + g_r.ao_failed = true; + return false; + } + } + g_r.ao_width = aw; + g_r.ao_height = ah; + g_r.ao_lin_width = width; + g_r.ao_lin_height = height; + } + // Scene-depth SRV, re-pointed when the depth buffer is rebuilt (resize / + // MSAA change). D32 textures view as R32_FLOAT automatically. + if (g_r.ao_depth_srv == nullptr || g_r.ao_depth_srv_of != g_r.depth) { + if (g_r.ao_depth_srv != nullptr) { + device->DestroyDeferred(g_r.ao_depth_srv); + g_r.ao_depth_srv = nullptr; + } + nrhi::TextureViewDesc sd; + if (g_r.msaa > 1) { + sd.dimension = nrhi::ViewDimension::k2DMS; + } else { + sd.dimension = nrhi::ViewDimension::k2D; + sd.mip_levels = 1; + } + g_r.ao_depth_srv = device->CreateTextureView(g_r.depth, sd); + if (g_r.ao_depth_srv == nullptr) { + g_r.ao_failed = true; + return false; + } + g_r.ao_depth_srv_of = g_r.depth; + } + + // b0 rows (see ssao.hlsl cbuffer C): size, projection, tuning, blur axis. + // size = the AO raster the march/blur passes run at; every shader use is + // UV-relative, so half res needs no shader-side changes. + float c[16]; + c[0] = float(aw); + c[1] = float(ah); + c[2] = 1.0f / float(aw); + c[3] = 1.0f / float(ah); + c[4] = std::fabs(pr[0]); // |m00|: the guest projection's x scale is + // negative; AO is mirror-invariant + c[5] = std::fabs(pr[5]); // |m11| + c[6] = pr[10]; // m22 + c[7] = pr[14]; // m32 + c[8] = float(REXCVAR_GET(skate3_native_render_scene_ssao_radius)); + c[9] = float(REXCVAR_GET(skate3_native_render_scene_ssao_intensity)); + // Distance fade 150..250 view units: fog owns the far field and the + // horizon-search precision degrades with depth anyway. + c[10] = 150.0f; + c[11] = 1.0f / 100.0f; + c[12] = 1.0f; // blur axis (horizontal first) + c[13] = 0.0f; + c[14] = float(REXCVAR_GET(skate3_native_render_scene_ssao_luma_protect)); + c[15] = 0.0f; + + const nrhi::Viewport ao_vp{0.0f, 0.0f, float(aw), float(ah), 0.0f, 1.0f}; + const nrhi::Rect ao_sc{0, 0, int32_t(aw), int32_t(ah)}; + + cmd->SetBindingLayout(g_r.ao_layout); + cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList); + cmd->SetViewport(viewport); + cmd->SetScissor(scissor); + cmd->SetRootConstants(0, 16, c, 0); + + // 1) Scene depth -> linear view Z (sample 0 when MSAA). Full res. + cmd->Barrier(g_r.depth, nrhi::ResourceState::kDepthWrite, + nrhi::ResourceState::kPixelShaderResource); + cmd->FlushBarriers(); + cmd->SetRenderTargets(g_r.ao_lin_depth, nullptr); + cmd->SetPipeline(g_r.pso_ao_linearize); + cmd->SetTexture(1, g_r.ao_depth_srv); + cmd->Draw(3, 0); + cmd->Barrier(g_r.depth, nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kDepthWrite); + cmd->Barrier(g_r.ao_lin_depth, nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kPixelShaderResource); + cmd->FlushBarriers(); + + // 1b) Scene luminance -> ao_luma (AO raster): the sun-lit protection + // mask. The scene plane pauses as a sampled source for one draw. + cmd->Barrier(scene_plane, nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kPixelShaderResource); + cmd->FlushBarriers(); + cmd->SetViewport(ao_vp); + cmd->SetScissor(ao_sc); + cmd->SetRenderTargets(g_r.ao_luma, nullptr); + cmd->SetPipeline(g_r.pso_ao_luma); + cmd->SetTexture(1, scene_srv); + cmd->Draw(3, 0); + cmd->Barrier(g_r.ao_luma, nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kPixelShaderResource); + cmd->Barrier(scene_plane, nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + cmd->FlushBarriers(); + + // 2) GTAO -> ao_tex[0], at the AO raster size. + cmd->SetRenderTargets(g_r.ao_tex[0], nullptr); + cmd->SetPipeline(g_r.pso_ao_gtao); + cmd->SetTexture(1, g_r.ao_lin_srv); + cmd->SetTexture(2, g_r.ao_luma_srv); + cmd->Draw(3, 0); + cmd->Barrier(g_r.ao_tex[0], nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kPixelShaderResource); + cmd->FlushBarriers(); + + // 3) Depth-aware blur, horizontal -> ao_tex[1]. + cmd->SetRenderTargets(g_r.ao_tex[1], nullptr); + cmd->SetPipeline(g_r.pso_ao_blur); + cmd->SetTexture(1, g_r.ao_srv[0]); + cmd->SetTexture(2, g_r.ao_lin_srv); + cmd->Draw(3, 0); + cmd->Barrier(g_r.ao_tex[1], nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kPixelShaderResource); + cmd->Barrier(g_r.ao_tex[0], nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + cmd->FlushBarriers(); + + // 4) Vertical -> ao_tex[0]. + const float dir_v[2] = {0.0f, 1.0f}; + cmd->SetRootConstants(0, 2, dir_v, 12); + cmd->SetRenderTargets(g_r.ao_tex[0], nullptr); + cmd->SetTexture(1, g_r.ao_srv[1]); + cmd->SetTexture(2, g_r.ao_lin_srv); + cmd->Draw(3, 0); + cmd->Barrier(g_r.ao_tex[0], nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kPixelShaderResource); + cmd->FlushBarriers(); + + // 5) Consume the finished multiplier plane. Classic: a full-res composite + // (or debug replace) draw onto the scene plane, s1 bilinearly + // upsampling. HDR: no draw at all: ps_tonemap samples the plane at t2 + // and multiplies pre-tonemap (identical math, one less full-res pass); + // the plane stays in PIXEL_SHADER_RESOURCE for ApplyHdrPost, which + // restores it. + if (hdr) { + g_r.ao_plane_in_psr = true; + } else { + cmd->SetViewport(viewport); + cmd->SetScissor(scissor); + cmd->SetRenderTargets(scene_plane, nullptr); + cmd->SetPipeline(REXCVAR_GET(skate3_native_render_scene_ssao_debug) + ? g_r.pso_ao_debug + : g_r.pso_ao_composite); + cmd->SetTexture(1, g_r.ao_srv[0]); + cmd->Draw(3, 0); + cmd->Barrier(g_r.ao_tex[0], nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + } + + // Restore intermediate steady states. + cmd->Barrier(g_r.ao_tex[1], nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + cmd->Barrier(g_r.ao_lin_depth, nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + cmd->Barrier(g_r.ao_luma, nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + cmd->FlushBarriers(); + return true; +} + +// ---- Screen-space reflections (ssr.hlsl + scene.hlsl ps_refl_gbuf) -------- +// Layout + PSOs, built lazily on the first SSR frame and rebuilt when the +// MSAA level (linearize variant) or the scene float format (composite +// target) changes. Failure disables the effect only, never g_r.failed. +bool EnsureSsrPipeline(const NativeGuestOutputRenderContext& context) { + if (g_r.ssr_failed || !g_r.hdr_active) { + return false; + } + if (g_r.pso_ssr_march != nullptr && g_r.ssr_msaa == g_r.msaa && + g_r.ssr_scene_fmt == g_r.hdr_scene_format) { + return true; + } + nrhi::Device* device = context.device; + const auto fail = [&](const char* what) { + REXLOG_ERROR("native-scene: ssr pipeline setup failed ({})", what); + g_r.ssr_failed = true; + return false; + }; + if (g_r.ssr_layout == nullptr) { + // The SSAO layout shape with a wider constant block (the march needs + // the world->view rotation rows): root constants b0 (32 floats) + three + // single-texture tables + point/linear clamp samplers. + nrhi::BindingLayoutDesc ld; + ld.param_count = 4; + ld.params[0] = {nrhi::BindingParamKind::kConstants, /*b*/ 0, 32, + nrhi::Visibility::kAll}; + ld.params[1] = {nrhi::BindingParamKind::kTextureTable, /*t*/ 0, 1, + nrhi::Visibility::kPixel}; + ld.params[2] = {nrhi::BindingParamKind::kTextureTable, 1, 1, + nrhi::Visibility::kPixel}; + ld.params[3] = {nrhi::BindingParamKind::kTextureTable, 2, 1, + nrhi::Visibility::kPixel}; + ld.static_sampler_count = 2; + ld.static_samplers[0] = {/*s*/ 0, nrhi::Filter::kPoint, + nrhi::AddressMode::kClamp, 1}; + ld.static_samplers[1] = {1, nrhi::Filter::kLinear, + nrhi::AddressMode::kClamp, 1}; + ld.allow_input_layout = false; + g_r.ssr_layout = device->CreateBindingLayout(ld); + if (g_r.ssr_layout == nullptr) { + return fail("binding layout"); + } + } + for (nrhi::Pipeline** p : {&g_r.pso_ssr_gbuf, &g_r.pso_ssr_linearize, + &g_r.pso_ssr_march, &g_r.pso_ssr_composite}) { + if (*p != nullptr) { + device->DestroyDeferred(*p); + *p = nullptr; + } + } + const bool msaa = g_r.msaa > 1; + const nrhi::ShaderMacro msaa_defs[] = {{"SSR_MSAA", "1"}, + {nullptr, nullptr}}; + nrhi::Shader* scene_vs = device->CreateShader( + MakeShaderDesc(nrhi::ShaderStage::kVertex, "scene.hlsl", kShaderSource, + "vs_main", nullptr, "")); + nrhi::Shader* ps_gbuf = device->CreateShader( + MakeShaderDesc(nrhi::ShaderStage::kPixel, "scene.hlsl", kShaderSource, + "ps_refl_gbuf", nullptr, "")); + nrhi::Shader* vs = device->CreateShader( + MakeShaderDesc(nrhi::ShaderStage::kVertex, "ssr.hlsl", kSsrShaderSource, + "vs_main", nullptr, "")); + nrhi::Shader* ps_lin = device->CreateShader(MakeShaderDesc( + nrhi::ShaderStage::kPixel, "ssr.hlsl", kSsrShaderSource, "ps_linearize", + msaa ? msaa_defs : nullptr, msaa ? "SSR_MSAA=1" : "")); + nrhi::Shader* ps_march = device->CreateShader( + MakeShaderDesc(nrhi::ShaderStage::kPixel, "ssr.hlsl", kSsrShaderSource, + "ps_march", nullptr, "")); + nrhi::Shader* ps_comp = device->CreateShader( + MakeShaderDesc(nrhi::ShaderStage::kPixel, "ssr.hlsl", kSsrShaderSource, + "ps_composite", nullptr, "")); + const bool shaders_ok = scene_vs != nullptr && ps_gbuf != nullptr && + vs != nullptr && ps_lin != nullptr && + ps_march != nullptr && ps_comp != nullptr; + if (!shaders_ok) { + for (nrhi::Shader* s : + {scene_vs, ps_gbuf, vs, ps_lin, ps_march, ps_comp}) { + device->DestroyDeferred(s); + } + return fail("shader compile"); + } + // Reflection G-buffer pass: the scene VS + input layout under the MAIN + // binding layout (the caller re-stages each reflective item's main-pass + // root constants / textures), half-res single-sample float target with + // its OWN half-res depth (test+write: the MSAA scene depth cannot pair + // with a 1x RTV) so the nearest reflective surface owns each texel; + // occlusion by non-reflective geometry is then resolved in the march by + // comparing the stored G-buffer depth against the scene linear depth. + { + nrhi::GraphicsPipelineDesc pso; + pso.layout = g_r.layout; + pso.vs = scene_vs; + pso.ps = ps_gbuf; + pso.input_elements = kSceneInputLayout; + pso.input_element_count = 7; + pso.vertex_stride = 56; + pso.cull = nrhi::CullMode::kNone; + pso.sample_count = 1; + pso.depth_clip = true; + pso.depth.test_enable = true; + pso.depth.write_enable = true; + pso.depth.func = nrhi::CompareFunc::kLess; + pso.dsv_format = nrhi::Format::kD32_FLOAT; + pso.rtv_format = nrhi::Format::kR16G16B16A16_FLOAT; + g_r.pso_ssr_gbuf = device->CreateGraphicsPipeline(pso); + } + nrhi::GraphicsPipelineDesc pso; + pso.layout = g_r.ssr_layout; + pso.vs = vs; + pso.cull = nrhi::CullMode::kNone; + pso.sample_count = 1; + pso.ps = ps_lin; + pso.rtv_format = nrhi::Format::kR32_FLOAT; + g_r.pso_ssr_linearize = device->CreateGraphicsPipeline(pso); + pso.ps = ps_march; + pso.rtv_format = nrhi::Format::kR16G16B16A16_FLOAT; + g_r.pso_ssr_march = device->CreateGraphicsPipeline(pso); + // Composite: straight src-alpha blend onto the float scene plane (the + // shader outputs confidence x reflectivity x intensity in alpha), RGB + // write mask: the plane's alpha stays untouched. + pso.ps = ps_comp; + pso.rtv_format = g_r.hdr_scene_format; + pso.blend.enable = true; + pso.blend.src = nrhi::BlendFactor::kSrcAlpha; + pso.blend.dst = nrhi::BlendFactor::kInvSrcAlpha; + pso.blend.op = nrhi::BlendOp::kAdd; + pso.blend.src_alpha = nrhi::BlendFactor::kZero; + pso.blend.dst_alpha = nrhi::BlendFactor::kOne; + pso.blend.op_alpha = nrhi::BlendOp::kAdd; + pso.blend.write_mask = 0x7; + g_r.pso_ssr_composite = device->CreateGraphicsPipeline(pso); + for (nrhi::Shader* s : {scene_vs, ps_gbuf, vs, ps_lin, ps_march, ps_comp}) { + device->DestroyDeferred(s); + } + if (g_r.pso_ssr_gbuf == nullptr || g_r.pso_ssr_linearize == nullptr || + g_r.pso_ssr_march == nullptr || g_r.pso_ssr_composite == nullptr) { + return fail("pso"); + } + g_r.ssr_msaa = g_r.msaa; + g_r.ssr_scene_fmt = g_r.hdr_scene_format; + REXLOG_INFO("native-scene: ssr pipeline created (MSAA x{})", g_r.msaa); + return true; +} + +// Half-res SSR intermediates (reflection G-buffer + march output), steady +// state RENDER_TARGET, rebuilt on resize. +bool EnsureSsrTargets(const NativeGuestOutputRenderContext& context) { + const uint32_t sw = std::max(1u, (context.guest_output_width + 1) / 2); + const uint32_t sh = std::max(1u, (context.guest_output_height + 1) / 2); + if (g_r.ssr_width == sw && g_r.ssr_height == sh && g_r.ssr_gbuf != nullptr && + g_r.ssr_tex != nullptr) { + return true; + } + nrhi::Device* device = context.device; + if (g_r.ssr_gbuf_depth != nullptr) { + device->DestroyDeferred(g_r.ssr_gbuf_depth); + g_r.ssr_gbuf_depth = nullptr; + } + { + nrhi::TextureDesc dd; + dd.width = sw; + dd.height = sh; + dd.format = nrhi::Format::kD32_FLOAT; + dd.usage = nrhi::kTextureUsageDepthStencil; + dd.initial_state = nrhi::ResourceState::kDepthWrite; + dd.clear_depth = 1.0f; + g_r.ssr_gbuf_depth = device->CreateTexture(dd); + if (g_r.ssr_gbuf_depth == nullptr) { + g_r.ssr_failed = true; + return false; + } + } + nrhi::Texture** res[2] = {&g_r.ssr_gbuf, &g_r.ssr_tex}; + nrhi::TextureView** views[2] = {&g_r.ssr_gbuf_srv, &g_r.ssr_srv}; + for (int i = 0; i < 2; ++i) { + if (*views[i] != nullptr) { + device->DestroyDeferred(*views[i]); + *views[i] = nullptr; + } + if (*res[i] != nullptr) { + device->DestroyDeferred(*res[i]); + *res[i] = nullptr; + } + nrhi::TextureDesc desc; + desc.width = sw; + desc.height = sh; + desc.mip_levels = 1; + desc.format = nrhi::Format::kR16G16B16A16_FLOAT; + desc.usage = nrhi::kTextureUsageRenderTarget; + desc.initial_state = nrhi::ResourceState::kRenderTarget; + *res[i] = device->CreateTexture(desc); + if (*res[i] == nullptr) { + g_r.ssr_failed = true; + return false; + } + nrhi::TextureViewDesc vd; + vd.mip_levels = 1; + *views[i] = device->CreateTextureView(*res[i], vd); + if (*views[i] == nullptr) { + g_r.ssr_failed = true; + return false; + } + } + g_r.ssr_width = sw; + g_r.ssr_height = sh; + return true; +} + +// SSR march + composite (see ssr.hlsl): consumes the reflection G-buffer +// the in-pass draw left in PIXEL_SHADER_RESOURCE, marches the full-res +// linear depth (reusing the SSAO linearize plane when AO ran this frame) +// while sampling the pre-tonemap HDR plane, and blends the result back onto +// it. Returns true when it drew; the binding layout changed and the +// caller's post_ran block restores the main-pass bindings. +bool ApplySsrPass(const NativeGuestOutputRenderContext& context, + nrhi::Cmd* cmd, const FrameScene& scene, + const nrhi::Viewport& viewport, const nrhi::Rect& scissor, + bool lin_depth_ready) { + if (!g_r.ssr_gbuf_ready) { + return false; + } + // Consume the handoff up front: every exit below must leave the G-buffer + // back in its RENDER_TARGET steady state. + g_r.ssr_gbuf_ready = false; + nrhi::Device* device = context.device; + const uint32_t width = context.guest_output_width; + const uint32_t height = context.guest_output_height; + const auto bail = [&] { + cmd->Barrier(g_r.ssr_gbuf, nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + cmd->FlushBarriers(); + return false; + }; + // Full-res linear view-Z: the SSAO plane when AO linearized this frame + // (it idles back in RENDER_TARGET after the AO pass), else the own + // linearize below. + lin_depth_ready = lin_depth_ready && g_r.ao_lin_depth != nullptr && + g_r.ao_lin_srv != nullptr && g_r.ao_lin_width == width && + g_r.ao_lin_height == height; + if (!lin_depth_ready) { + if (g_r.ssr_lin_depth == nullptr || g_r.ssr_lin_width != width || + g_r.ssr_lin_height != height) { + if (g_r.ssr_lin_srv != nullptr) { + device->DestroyDeferred(g_r.ssr_lin_srv); + g_r.ssr_lin_srv = nullptr; + } + if (g_r.ssr_lin_depth != nullptr) { + device->DestroyDeferred(g_r.ssr_lin_depth); + g_r.ssr_lin_depth = nullptr; + } + nrhi::TextureDesc desc; + desc.width = width; + desc.height = height; + desc.mip_levels = 1; + desc.format = nrhi::Format::kR32_FLOAT; + desc.usage = nrhi::kTextureUsageRenderTarget; + desc.initial_state = nrhi::ResourceState::kRenderTarget; + g_r.ssr_lin_depth = device->CreateTexture(desc); + if (g_r.ssr_lin_depth == nullptr) { + g_r.ssr_failed = true; + return bail(); + } + nrhi::TextureViewDesc vd; + vd.mip_levels = 1; + g_r.ssr_lin_srv = device->CreateTextureView(g_r.ssr_lin_depth, vd); + if (g_r.ssr_lin_srv == nullptr) { + g_r.ssr_failed = true; + return bail(); + } + g_r.ssr_lin_width = width; + g_r.ssr_lin_height = height; + } + // Scene-depth SRV, re-pointed when the depth buffer is rebuilt. + if (g_r.ssr_depth_srv == nullptr || g_r.ssr_depth_srv_of != g_r.depth) { + if (g_r.ssr_depth_srv != nullptr) { + device->DestroyDeferred(g_r.ssr_depth_srv); + g_r.ssr_depth_srv = nullptr; + } + nrhi::TextureViewDesc sd; + if (g_r.msaa > 1) { + sd.dimension = nrhi::ViewDimension::k2DMS; + } else { + sd.dimension = nrhi::ViewDimension::k2D; + sd.mip_levels = 1; + } + g_r.ssr_depth_srv = device->CreateTextureView(g_r.depth, sd); + if (g_r.ssr_depth_srv == nullptr) { + g_r.ssr_failed = true; + return bail(); + } + g_r.ssr_depth_srv_of = g_r.depth; + } + } + nrhi::Texture* const lin_tex = + lin_depth_ready ? g_r.ao_lin_depth : g_r.ssr_lin_depth; + nrhi::TextureView* const lin_srv = + lin_depth_ready ? g_r.ao_lin_srv : g_r.ssr_lin_srv; + + // b0 rows (see ssr.hlsl cbuffer C): dest size, projection, the world -> + // "AO view space" rotation, tuning. view = view_proj * proj^-1 (row- + // vector); the projection's per-axis signs fold into the rotation so + // normals land in the same mirrored space ViewPos reconstructs positions + // in; reflection directions are mirror-SENSITIVE, unlike every AO term. + const float* pr = scene.proj; + float c[32] = {}; + c[0] = float(g_r.ssr_width); + c[1] = float(g_r.ssr_height); + c[2] = 1.0f / float(g_r.ssr_width); + c[3] = 1.0f / float(g_r.ssr_height); + c[4] = std::fabs(pr[0]); + c[5] = std::fabs(pr[5]); + c[6] = pr[10]; + c[7] = pr[14]; + const float sxs = pr[0] < 0.0f ? -1.0f : 1.0f; + const float sys = pr[5] < 0.0f ? -1.0f : 1.0f; + const float inv00 = 1.0f / pr[0]; + const float inv11 = 1.0f / pr[5]; + for (int r = 0; r < 3; ++r) { + // proj^-1 columns: x/y scale by 1/m00 / 1/m11, view z = clip w (the + // view_proj row's w column); the AO-space axis signs multiply in. + c[8 + r * 4 + 0] = scene.view_proj[r * 4 + 0] * inv00 * sxs; + c[8 + r * 4 + 1] = scene.view_proj[r * 4 + 1] * inv11 * sys; + c[8 + r * 4 + 2] = scene.view_proj[r * 4 + 3]; + } + c[20] = float(REXCVAR_GET(skate3_native_render_scene_ssr_steps)); + c[21] = float(REXCVAR_GET(skate3_native_render_scene_ssr_thickness)); + c[22] = float(REXCVAR_GET(skate3_native_render_scene_ssr_intensity)); + c[23] = float(REXCVAR_GET(skate3_native_render_scene_ssr_debug)); + c[24] = 250.0f; // max ray distance (view units; the screen clip governs) + + cmd->SetBindingLayout(g_r.ssr_layout); + cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList); + cmd->SetRootConstants(0, 32, c, 0); + + // 1) Own linearize when AO did not produce the plane this frame. + if (!lin_depth_ready) { + cmd->Barrier(g_r.depth, nrhi::ResourceState::kDepthWrite, + nrhi::ResourceState::kPixelShaderResource); + cmd->FlushBarriers(); + cmd->SetViewport(viewport); + cmd->SetScissor(scissor); + cmd->SetRenderTargets(g_r.ssr_lin_depth, nullptr); + cmd->SetPipeline(g_r.pso_ssr_linearize); + cmd->SetTexture(1, g_r.ssr_depth_srv); + cmd->Draw(3, 0); + cmd->Barrier(g_r.depth, nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kDepthWrite); + } + cmd->Barrier(lin_tex, nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kPixelShaderResource); + + // 2) March (half res): the HDR plane pauses as a sampled source. + cmd->Barrier(g_r.hdr_resolved, nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kPixelShaderResource); + cmd->FlushBarriers(); + const nrhi::Viewport ssr_vp{0.0f, 0.0f, float(g_r.ssr_width), + float(g_r.ssr_height), 0.0f, 1.0f}; + const nrhi::Rect ssr_sc{0, 0, int32_t(g_r.ssr_width), + int32_t(g_r.ssr_height)}; + cmd->SetViewport(ssr_vp); + cmd->SetScissor(ssr_sc); + cmd->SetRenderTargets(g_r.ssr_tex, nullptr); + cmd->SetPipeline(g_r.pso_ssr_march); + cmd->SetTexture(1, g_r.ssr_gbuf_srv); + cmd->SetTexture(2, lin_srv); + cmd->SetTexture(3, g_r.hdr_srv); + cmd->Draw(3, 0); + cmd->Barrier(g_r.ssr_tex, nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kPixelShaderResource); + cmd->Barrier(g_r.hdr_resolved, nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + cmd->FlushBarriers(); + + // 3) Composite (full res, src-alpha blend onto the HDR plane). Dest-size + // constants switch to the full raster; the rest carries over. + c[0] = float(width); + c[1] = float(height); + c[2] = 1.0f / float(width); + c[3] = 1.0f / float(height); + cmd->SetRootConstants(0, 4, c, 0); + cmd->SetViewport(viewport); + cmd->SetScissor(scissor); + cmd->SetRenderTargets(g_r.hdr_resolved, nullptr); + cmd->SetPipeline(g_r.pso_ssr_composite); + cmd->SetTexture(1, g_r.ssr_srv); + cmd->SetTexture(2, g_r.ssr_gbuf_srv); + cmd->SetTexture(3, lin_srv); + cmd->Draw(3, 0); + + // Restore intermediate steady states. + cmd->Barrier(g_r.ssr_tex, nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + cmd->Barrier(lin_tex, nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + cmd->Barrier(g_r.ssr_gbuf, nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + cmd->FlushBarriers(); + return true; +} + +// ---- HDR post chain (hdr.hlsl: bloom pyramid + the extracted tonemap) ---- +// Layout + PSOs, built when the HDR path activates and rebuilt when the +// guest output format (the tonemap target) changes. Failure falls back to +// the classic in-material tonemap instead of killing the native renderer. +bool EnsureHdrPipeline(const NativeGuestOutputRenderContext& context) { + if (!g_r.hdr_active || g_r.hdr_failed) { + return false; + } + if (g_r.pso_tonemap != nullptr && + g_r.hdr_pso_out_format == context.guest_output->format()) { + return true; + } + nrhi::Device* device = context.device; + const auto fail = [&](const char* what) { + REXLOG_WARN( + "native-scene: HDR pipeline setup failed ({}), falling back to the " + "classic tonemap path", + what); + g_r.hdr_failed = true; + return false; + }; + if (g_r.hdr_layout == nullptr) { + // The SSAO layout shape plus a third single-texture table (t2 = the + // fused AO multiplier plane): root constants b0 + three single-texture + // tables + point/linear clamp samplers (hdr.hlsl's frozen Vulkan plan). + nrhi::BindingLayoutDesc ld; + ld.param_count = 4; + ld.params[0] = {nrhi::BindingParamKind::kConstants, /*b*/ 0, 16, + nrhi::Visibility::kAll}; + ld.params[1] = {nrhi::BindingParamKind::kTextureTable, /*t*/ 0, 1, + nrhi::Visibility::kPixel}; + ld.params[2] = {nrhi::BindingParamKind::kTextureTable, 1, 1, + nrhi::Visibility::kPixel}; + ld.params[3] = {nrhi::BindingParamKind::kTextureTable, 2, 1, + nrhi::Visibility::kPixel}; + ld.static_sampler_count = 2; + ld.static_samplers[0] = {/*s*/ 0, nrhi::Filter::kPoint, + nrhi::AddressMode::kClamp, 1}; + ld.static_samplers[1] = {1, nrhi::Filter::kLinear, + nrhi::AddressMode::kClamp, 1}; + ld.allow_input_layout = false; + g_r.hdr_layout = device->CreateBindingLayout(ld); + if (g_r.hdr_layout == nullptr) { + return fail("binding layout"); + } + } + for (nrhi::Pipeline** p : {&g_r.pso_bloom_first, &g_r.pso_bloom_down, + &g_r.pso_bloom_up, &g_r.pso_tonemap}) { + if (*p != nullptr) { + device->DestroyDeferred(*p); + *p = nullptr; + } + } + const nrhi::ShaderMacro first_defs[] = {{"BLOOM_FIRST", "1"}, + {nullptr, nullptr}}; + nrhi::Shader* vs = device->CreateShader( + MakeShaderDesc(nrhi::ShaderStage::kVertex, "hdr.hlsl", kHdrShaderSource, + "vs_main", nullptr, "")); + nrhi::Shader* ps_first = device->CreateShader(MakeShaderDesc( + nrhi::ShaderStage::kPixel, "hdr.hlsl", kHdrShaderSource, + "ps_bloom_down", first_defs, "BLOOM_FIRST=1")); + nrhi::Shader* ps_down = device->CreateShader( + MakeShaderDesc(nrhi::ShaderStage::kPixel, "hdr.hlsl", kHdrShaderSource, + "ps_bloom_down", nullptr, "")); + nrhi::Shader* ps_up = device->CreateShader( + MakeShaderDesc(nrhi::ShaderStage::kPixel, "hdr.hlsl", kHdrShaderSource, + "ps_bloom_up", nullptr, "")); + nrhi::Shader* ps_tone = device->CreateShader( + MakeShaderDesc(nrhi::ShaderStage::kPixel, "hdr.hlsl", kHdrShaderSource, + "ps_tonemap", nullptr, "")); + const bool shaders_ok = vs != nullptr && ps_first != nullptr && + ps_down != nullptr && ps_up != nullptr && + ps_tone != nullptr; + if (!shaders_ok) { + for (nrhi::Shader* s : {vs, ps_first, ps_down, ps_up, ps_tone}) { + device->DestroyDeferred(s); + } + return fail("shader compile"); + } + nrhi::GraphicsPipelineDesc pso; + pso.layout = g_r.hdr_layout; + pso.vs = vs; + pso.cull = nrhi::CullMode::kNone; + pso.sample_count = 1; + pso.rtv_format = nrhi::Format::kR16G16B16A16_FLOAT; + pso.ps = ps_first; + g_r.pso_bloom_first = device->CreateGraphicsPipeline(pso); + pso.ps = ps_down; + g_r.pso_bloom_down = device->CreateGraphicsPipeline(pso); + // Progressive upsample: additive ONE/ONE onto the next-larger level. + pso.ps = ps_up; + pso.blend.enable = true; + pso.blend.src = nrhi::BlendFactor::kOne; + pso.blend.dst = nrhi::BlendFactor::kOne; + pso.blend.op = nrhi::BlendOp::kAdd; + pso.blend.src_alpha = nrhi::BlendFactor::kOne; + pso.blend.dst_alpha = nrhi::BlendFactor::kOne; + pso.blend.op_alpha = nrhi::BlendOp::kAdd; + g_r.pso_bloom_up = device->CreateGraphicsPipeline(pso); + pso.blend = {}; + pso.ps = ps_tone; + pso.rtv_format = context.guest_output->format(); + g_r.pso_tonemap = device->CreateGraphicsPipeline(pso); + for (nrhi::Shader* s : {vs, ps_first, ps_down, ps_up, ps_tone}) { + device->DestroyDeferred(s); + } + if (g_r.pso_bloom_first == nullptr || g_r.pso_bloom_down == nullptr || + g_r.pso_bloom_up == nullptr || g_r.pso_tonemap == nullptr) { + return fail("pso"); + } + g_r.hdr_pso_out_format = context.guest_output->format(); + REXLOG_INFO("native-scene: HDR post pipeline created"); + return true; +} + +// Bloom pyramid + tonemap over the float scene plane into the guest output. +// Runs after the AO composite (bloom sees the occluded scene). Transitions +// the guest output kGuestOutput -> kRenderTarget (the state every later +// consumer expects); the caller restores the main binding layout after. +void ApplyHdrPost(const NativeGuestOutputRenderContext& context, + nrhi::Cmd* cmd, const nrhi::Viewport& viewport, + const nrhi::Rect& scissor, bool loading_native) { + nrhi::Device* device = context.device; + const uint32_t width = context.guest_output_width; + const uint32_t height = context.guest_output_height; + // Bloom chain intermediates: QUARTER res halving down to the level cap or + // an 8-px floor, RGBA16F, steady state RENDER_TARGET. Quarter start = + // 1/4 the pyramid cost of a half-res chain; the extraction pass widens + // its 13-tap spread (p1.x = 2) so the 4x reduction still integrates the + // whole source block. Creation failure only loses bloom (the tonemap + // still runs). + if (g_r.bloom_base_w != width || g_r.bloom_base_h != height || + (g_r.bloom_levels > 0 && g_r.bloom_tex[0] == nullptr)) { + for (uint32_t i = 0; i < RendererState::kBloomMaxLevels; ++i) { + if (g_r.bloom_srv[i] != nullptr) { + device->DestroyDeferred(g_r.bloom_srv[i]); + g_r.bloom_srv[i] = nullptr; + } + if (g_r.bloom_tex[i] != nullptr) { + device->DestroyDeferred(g_r.bloom_tex[i]); + g_r.bloom_tex[i] = nullptr; + } + } + g_r.bloom_levels = 0; + uint32_t w = std::max(1u, (width + 1) / 2); + uint32_t h = std::max(1u, (height + 1) / 2); + for (uint32_t i = 0; i < RendererState::kBloomMaxLevels; ++i) { + w = std::max(1u, (w + 1) / 2); + h = std::max(1u, (h + 1) / 2); + if (w < 8 || h < 8) { + break; + } + nrhi::TextureDesc desc; + desc.width = w; + desc.height = h; + desc.mip_levels = 1; + desc.format = nrhi::Format::kR16G16B16A16_FLOAT; + desc.usage = nrhi::kTextureUsageRenderTarget; + desc.initial_state = nrhi::ResourceState::kRenderTarget; + g_r.bloom_tex[i] = device->CreateTexture(desc); + if (g_r.bloom_tex[i] == nullptr) { + break; + } + nrhi::TextureViewDesc vd; + vd.mip_levels = 1; + g_r.bloom_srv[i] = device->CreateTextureView(g_r.bloom_tex[i], vd); + if (g_r.bloom_srv[i] == nullptr) { + device->DestroyDeferred(g_r.bloom_tex[i]); + g_r.bloom_tex[i] = nullptr; + break; + } + g_r.bloom_w[i] = w; + g_r.bloom_h[i] = h; + ++g_r.bloom_levels; + } + g_r.bloom_base_w = width; + g_r.bloom_base_h = height; + if (g_r.bloom_levels == 0) { + REXLOG_WARN("native-scene: bloom chain allocation failed (bloom off)"); + } + } + const bool bloom = REXCVAR_GET(skate3_native_render_scene_bloom) && + !loading_native && g_r.bloom_levels > 0 && + g_r.pso_bloom_first != nullptr && + g_r.pso_bloom_down != nullptr && + g_r.pso_bloom_up != nullptr; + + cmd->SetBindingLayout(g_r.hdr_layout); + cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList); + // The scene plane is the sampled source for the whole chain. + cmd->Barrier(g_r.hdr_resolved, nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kPixelShaderResource); + cmd->FlushBarriers(); + + float c[16]; + const auto set_consts = [&](uint32_t dw, uint32_t dh, uint32_t sw, + uint32_t sh, float intensity, float tap_scale) { + c[0] = float(dw); + c[1] = float(dh); + c[2] = 1.0f / float(dw); + c[3] = 1.0f / float(dh); + c[4] = float(sw); + c[5] = float(sh); + c[6] = 1.0f / float(sw); + c[7] = 1.0f / float(sh); + c[8] = float(REXCVAR_GET(skate3_native_render_scene_bloom_threshold)); + c[9] = float(REXCVAR_GET(skate3_native_render_scene_bloom_knee)); + c[10] = intensity; + // Debug view (ps_tonemap p0.w); the classic ssao_debug cvar maps onto + // the fused AO view when the plane rode along this frame. + c[11] = float(REXCVAR_GET(skate3_native_render_scene_hdr_debug)); + if (c[11] == 0.0f && g_r.ao_plane_in_psr && + REXCVAR_GET(skate3_native_render_scene_ssao_debug)) { + c[11] = 3.0f; + } + c[12] = tap_scale; + c[13] = c[14] = c[15] = 0.0f; + cmd->SetRootConstants(0, 16, c, 0); + }; + + if (bloom) { + // Downsample chain (level 0 extracts with threshold + Karis weighting; + // its 4x reduction doubles the tap spread to cover the source block). + for (uint32_t i = 0; i < g_r.bloom_levels; ++i) { + const uint32_t sw = i == 0 ? width : g_r.bloom_w[i - 1]; + const uint32_t sh = i == 0 ? height : g_r.bloom_h[i - 1]; + set_consts(g_r.bloom_w[i], g_r.bloom_h[i], sw, sh, 0.0f, + i == 0 ? 2.0f : 1.0f); + const nrhi::Viewport vp{0.0f, 0.0f, float(g_r.bloom_w[i]), + float(g_r.bloom_h[i]), 0.0f, 1.0f}; + const nrhi::Rect sc{0, 0, int32_t(g_r.bloom_w[i]), + int32_t(g_r.bloom_h[i])}; + cmd->SetViewport(vp); + cmd->SetScissor(sc); + cmd->SetRenderTargets(g_r.bloom_tex[i], nullptr); + cmd->SetPipeline(i == 0 ? g_r.pso_bloom_first : g_r.pso_bloom_down); + cmd->SetTexture(1, i == 0 ? g_r.hdr_srv : g_r.bloom_srv[i - 1]); + cmd->Draw(3, 0); + cmd->Barrier(g_r.bloom_tex[i], nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kPixelShaderResource); + cmd->FlushBarriers(); + } + // Progressive tent upsample, additive onto each larger level. + for (int32_t i = int32_t(g_r.bloom_levels) - 2; i >= 0; --i) { + cmd->Barrier(g_r.bloom_tex[i], nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + cmd->FlushBarriers(); + set_consts(g_r.bloom_w[i], g_r.bloom_h[i], g_r.bloom_w[i + 1], + g_r.bloom_h[i + 1], 0.0f, 1.0f); + const nrhi::Viewport vp{0.0f, 0.0f, float(g_r.bloom_w[i]), + float(g_r.bloom_h[i]), 0.0f, 1.0f}; + const nrhi::Rect sc{0, 0, int32_t(g_r.bloom_w[i]), + int32_t(g_r.bloom_h[i])}; + cmd->SetViewport(vp); + cmd->SetScissor(sc); + cmd->SetRenderTargets(g_r.bloom_tex[i], nullptr); + cmd->SetPipeline(g_r.pso_bloom_up); + cmd->SetTexture(1, g_r.bloom_srv[i + 1]); + cmd->Draw(3, 0); + cmd->Barrier(g_r.bloom_tex[i], nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kPixelShaderResource); + cmd->FlushBarriers(); + } + } + + // Tonemap into the guest output (the single application of the game's + // shared tone chain; bloom energy joins pre-tonemap). + cmd->Barrier(context.guest_output, nrhi::ResourceState::kGuestOutput, + nrhi::ResourceState::kRenderTarget); + cmd->FlushBarriers(); + set_consts(width, height, width, height, + bloom ? float(REXCVAR_GET( + skate3_native_render_scene_bloom_intensity)) + : 0.0f, + 1.0f); + cmd->SetViewport(viewport); + cmd->SetScissor(scissor); + cmd->SetRenderTargets(context.guest_output, nullptr); + cmd->SetPipeline(g_r.pso_tonemap); + cmd->SetTexture(1, g_r.hdr_srv); + cmd->SetTexture(2, bloom ? g_r.bloom_srv[0] : g_r.white.srv); + // t2 = the fused AO multiplier plane (white = no AO this frame). + cmd->SetTexture(3, g_r.ao_plane_in_psr ? g_r.ao_srv[0] : g_r.white.srv); + cmd->Draw(3, 0); + + // Restore steady states. + cmd->Barrier(g_r.hdr_resolved, nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + if (g_r.ao_plane_in_psr) { + cmd->Barrier(g_r.ao_tex[0], nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + g_r.ao_plane_in_psr = false; + } + if (bloom) { + for (uint32_t i = 0; i < g_r.bloom_levels; ++i) { + cmd->Barrier(g_r.bloom_tex[i], + nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + } + } + cmd->FlushBarriers(); +} + + +// ---- Settings-menu backdrop blur (shared native + emulated-post paths) ---- +// Clean separable gaussian over the finished guest output; deliberately +// shares nothing with the game's +// popup chain (that one reproduces the console's 1152x640 lattice on +// purpose). Downsample to half res (plain bilinear = 2x2 box), gaussian H, +// gaussian V, bilinear stretch back. Eased in/out (~60 ms) so open/close +// doesn't pop; the smoothing state is shared across the native and +// emulated-post paths (only one runs per frame). Returns true while the +// effect is active (target > 0 or still easing out). +bool ApplyMenuBlurPass(const NativeGuestOutputRenderContext& context, nrhi::Cmd* cmd, + float target_sigma, bool output_in_guest_output_state) { + static float s_menu_sigma = 0.0f; + static int64_t s_menu_ns = 0; + const int64_t menu_now_ns = std::chrono::duration_cast( + PerfClock::now().time_since_epoch()) + .count(); + float menu_dt = s_menu_ns != 0 ? float(menu_now_ns - s_menu_ns) * 1e-9f : 1.0f; + if (menu_dt > 0.25f) { + s_menu_sigma = 0.0f; + menu_dt = 0.0f; + } + s_menu_ns = menu_now_ns; + const float menu_a = 1.0f - std::exp(-menu_dt / 0.06f); + s_menu_sigma += (target_sigma - s_menu_sigma) * menu_a; + const bool active = target_sigma > 0.0f || s_menu_sigma > 0.05f; + if (s_menu_sigma <= 0.05f || g_r.pso_menu_gauss == nullptr || + g_r.pso_blur_blit == nullptr || g_r.menu_blur_tex[0] == nullptr || + g_r.output_srv_slot == nullptr) { + return active; + } + const float w = float(g_r.menu_blur_w); + const float h = float(g_r.menu_blur_h); + // Sigma is authored in 1080p pixels; halve again for the half-res space. + const float sigma_half = std::max( + 0.3f, s_menu_sigma * (float(context.guest_output_height) / 1080.0f) * 0.5f); + const float radius = std::min(48.0f, std::ceil(sigma_half * 3.0f)); + const nrhi::Viewport half_vp{0.0f, 0.0f, w, h, 0.0f, 1.0f}; + const nrhi::Rect half_sc{0, 0, int32_t(g_r.menu_blur_w), int32_t(g_r.menu_blur_h)}; + const nrhi::Viewport full_vp{0.0f, + 0.0f, + float(context.guest_output_width), + float(context.guest_output_height), + 0.0f, + 1.0f}; + const nrhi::Rect full_sc{0, 0, int32_t(context.guest_output_width), + int32_t(context.guest_output_height)}; + const nrhi::ResourceState output_entry_state = output_in_guest_output_state + ? nrhi::ResourceState::kGuestOutput + : nrhi::ResourceState::kRenderTarget; + // The binding layout must be latched on this cmd before any draw; on the + // emulated-post path this is a FRESH frame state where no layout has been + // set, and nrhi draws silently no-op without one (EnsureDrawState). + // Harmless on the native path (same layout, re-latch). + cmd->SetBindingLayout(g_r.layout); + cmd->SetPrimitiveTopology(nrhi::PrimitiveTopology::kTriangleList); + // Downsample: guest output -> menu[0]. + cmd->Barrier(context.guest_output, output_entry_state, + nrhi::ResourceState::kPixelShaderResource); + cmd->FlushBarriers(); + cmd->SetRenderTargets(g_r.menu_blur_tex[0], nullptr); + cmd->SetViewport(half_vp); + cmd->SetScissor(half_sc); + cmd->SetPipeline(g_r.pso_blur_blit); + cmd->SetTexture(1, g_r.output_srv_slot); + cmd->Draw(3, 0); + // H: menu[0] -> menu[1]. + cmd->Barrier(g_r.menu_blur_tex[0], nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kPixelShaderResource); + cmd->FlushBarriers(); + cmd->SetRenderTargets(g_r.menu_blur_tex[1], nullptr); + cmd->SetPipeline(g_r.pso_menu_gauss); + const float h_consts[8] = {1.0f / w, 0.0f, sigma_half, radius, 1.0f, 1.0f, 1.0f, 1.0f}; + cmd->SetRootConstants(0, 8, h_consts, 0); + cmd->SetTexture(1, g_r.menu_blur_srv[0]); + cmd->Draw(3, 0); + // V: menu[1] -> menu[0]. + cmd->Barrier(g_r.menu_blur_tex[1], nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kPixelShaderResource); + cmd->Barrier(g_r.menu_blur_tex[0], nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + cmd->FlushBarriers(); + cmd->SetRenderTargets(g_r.menu_blur_tex[0], nullptr); + const float v_consts[8] = {0.0f, 1.0f / h, sigma_half, radius, 1.0f, 1.0f, 1.0f, 1.0f}; + cmd->SetRootConstants(0, 8, v_consts, 0); + cmd->SetTexture(1, g_r.menu_blur_srv[1]); + cmd->Draw(3, 0); + // Stretch back over the full output. + cmd->Barrier(g_r.menu_blur_tex[0], nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kPixelShaderResource); + cmd->Barrier(context.guest_output, nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + cmd->FlushBarriers(); + cmd->SetRenderTargets(context.guest_output, nullptr); + cmd->SetViewport(full_vp); + cmd->SetScissor(full_sc); + cmd->SetPipeline(g_r.pso_blur_blit); + cmd->SetTexture(1, g_r.menu_blur_srv[0]); + cmd->Draw(3, 0); + // Restore steady states (the intermediates stay in RENDER_TARGET; the + // output goes back to the state the caller's path expects). + cmd->Barrier(g_r.menu_blur_tex[0], nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + cmd->Barrier(g_r.menu_blur_tex[1], nrhi::ResourceState::kPixelShaderResource, + nrhi::ResourceState::kRenderTarget); + if (output_in_guest_output_state) { + cmd->Barrier(context.guest_output, nrhi::ResourceState::kRenderTarget, + nrhi::ResourceState::kGuestOutput); + cmd->FlushBarriers(); + } + return active; +} + +// Minimal resource bring-up for the emulated-output post-processor path: the +// full EnsurePipeline never runs while the native renderer is disabled or +// yielding, so the blur ensures just what it needs (binding layout, the blur +// PSOs, the half-res intermediates, and a sampled view of the output). +bool EnsureMenuBlurStandalone(const NativeGuestOutputRenderContext& context) { + if (g_r.failed) { + return false; + } + if (!EnsureRootSignature(context)) { + return false; + } + if (g_r.pso_menu_gauss == nullptr) { + static bool s_attempted = false; + if (s_attempted) { + return false; // creation failed once; don't hammer every frame + } + s_attempted = true; + EnsureBlurPsos(context); + if (g_r.pso_menu_gauss == nullptr) { + return false; + } + } + EnsureBlurOutlineTargets(context); + if (g_r.menu_blur_tex[0] == nullptr) { + return false; + } + if (g_r.rtv_resource != context.guest_output) { + if (g_r.output_srv_slot) { + context.device->DestroyDeferred(g_r.output_srv_slot); + g_r.output_srv_slot = nullptr; + } + nrhi::TextureViewDesc vd; + vd.mip_levels = 1; + g_r.output_srv_slot = context.device->CreateTextureView(context.guest_output, vd); + g_r.output_srv_allocated = g_r.output_srv_slot != nullptr; + g_r.rtv_resource = context.guest_output; + } + return g_r.output_srv_slot != nullptr; +} + +// Registered with the command processors: invoked at the end of the EMULATED +// guest-output refresh (native-rendered frames apply the blur inline in +// RenderScene). Covers boot/startup frames before the native scene takes +// over and manual emulated mode. +void PostProcessGuestOutput(const NativeGuestOutputRenderContext& context, void* /*user_data*/) { + const float target = g_settings_menu_blur.load(std::memory_order_relaxed) + ? float(REXCVAR_GET(skate3_menu_blur_sigma)) + : 0.0f; + // Diagnostics (throttled): the emulated-path blur has several silent + // early-outs; log which leg each invocation takes. + const bool log_this = g_post_blur_log_count.load(std::memory_order_relaxed) < 8; + const bool ensured = EnsureMenuBlurStandalone(context); + if (log_this) { + g_post_blur_log_count.fetch_add(1, std::memory_order_relaxed); + REXLOG_INFO( + "native-scene: emulated-post blur invoked (target={:.1f} ensured={} pso={} tex={} " + "srv={} failed={})", + target, ensured, (void*)g_r.pso_menu_gauss, (void*)g_r.menu_blur_tex[0], + (void*)g_r.output_srv_slot, g_r.failed); + } + if (!ensured) { + if (target <= 0.0f) { + rex::graphics::RequestNativeGuestOutputPostProcess(false); + } + return; + } + if (!ApplyMenuBlurPass(context, context.cmd, target, + /*output_in_guest_output_state=*/true)) { + // Fully eased out: stop the per-frame post-process invocations until the + // menu opens again. + rex::graphics::RequestNativeGuestOutputPostProcess(false); + } +} + +} // namespace skate3::native_scene + +#endif // REX_HAS_D3D12 || REX_HAS_VULKAN