Files
NX2navi/docs/maxwell.md
chrg6 ee6fbaf0bb Zero shader locals with DLSS and DLAA on too, which removes the speckled floor tiles there.
With DLAA on, function-local variables stayed undefined, and NVIDIA's driver filled the game's clustered-light reads
with other warp lanes' values: floors around Link lit in 4x8-pixel tiles. The setting was also part of every shader
cache key, so shaders prepared with DLSS off missed with it on. Zeroing is now on for every setting; DLSS still finds
the game's FXAA by its zeroed hash and keeps its camera motion. A log line now says when DLSS replaces the game's
scaling draw.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 13:31:06 -05:00

40 KiB
Raw Blame History

Maxwell / NVN behaviour a port must reproduce

Reference for diagnosing graphics bugs. Each item: what Maxwell (GM20B, 3D class B197, compute B1C0) and NVN do, what Vulkan / AMD (RDNA4, our GPU0) do instead, what goes wrong when the difference is ignored and what our runtime does, and the source. Items are ordered by how likely they are to cause visible artifacts. "Ours:" marks what our runtime does (grep-level look, see section 6; our code is cited by file and function).

Source shorthands:

1. Render targets

# Maxwell / NVN Vulkan / AMD Effect / ours Source
1.1 Colour target the fragment shader does not write keeps its old texel, even with blending enabled. Undefined value written/blended. Zero write mask and no blend for unwritten outputs. The same holds per component: the shader header's omap names the components a target gets, and a shader that writes RGB leaves the alpha alone; AMD writes its export registers' leftovers there (NaN and saturated pixels the bloom spreads). Ours: vk_pipeline.c target_wmask masks the target and the components outside the translator's frag_out_map. —
1.2 Unwritten vertex outputs read as (0, 0, 0, 1) in the next stage, including the unwritten components of a partly written output (VS writes xyz, FS divides by w). FS input with no VS output: interface mismatch, value undefined; an unwritten component of a written output is undefined (AMD: whatever the register holds, often 0). The translator initialises exactly the components the next stage reads, but only when stages are linked (XL Translation/Translator.cs:212-227, NextInputAttributesComponents); position starts (0,0,0,1) (:259). Symptom: sun shadow lookups divided by w = 0, scene dim, no shadows. Ours: missing outputs padded with (0, 0, 0, 1), unlinked outputs initialised by translator patch 0006 (section 6, check 2). XL
1.3 Clears honour the scissor and the clear's own channel / stencil write mask, and may come between draws inside a pass. SET_CLEAR_SURFACE_CONTROL (0x10f8) has RESPECT_STENCIL_MASK, USE_CLEAR_RECT, USE_SCISSOR0, USE_VIEWPORT_CLIP0. vkCmdClearAttachments takes a rect but ignores the colour write mask; load-op clears are whole-attachment. A triangle drawn with the clear's mask reproduces it. NX2navi uses mid-pass, partial-mask and partial-scissor clears. Hardware recipe: CLEAR_BUFFERS (0x674 = 0x19d0) carries the colour channel mask (bits 2-5), depth (0), stencil (1), target id (6-9) and layer (10-20) in one word; with ClearBufferFlags (0x43E = 0x10f8) set once to StencilMask | Scissor, every clear is cut by scissor 0 and its stencil bits by the stencil front write mask register, and a colour clear does not program ColorWriteMask (0x680). A stencil clear writes StencilFrontMask (0x4E7) and ClearStencil (0x368) just before the clear and restores the shadowed state after. Multisample raster is switched off around a colour clear. Arrays: a macro repeats the clear for every layer. No pass concept: clears sit between draws. Ours: masked colour and stencil clears are skipped (check 3). OGD 0x10f8, 0x19d0
1.4 Conditional rendering (SET_RENDER_ENABLE_A..C 0x1550-0x1558, NVN SetRenderEnableConditional) compares two 64-bit counters at addr and addr+16 on the GPU, in command order. NX2navi wraps nearly every clear in it. A TIMESTAMP report writes 0 to the counter field. VK_EXT_conditional_rendering tests one 32-bit word against zero. Ignoring it leaves light-volume depth uncleared: black 3D scene. Ours: evaluated on the CPU at record time; a report from an unfinished submission counts as 0 (check 5). OGD
1.5 Depth mode is runtime state: nvnDeviceSetDepthMode near = -w (GL, NVN default) or 0. Register SET_VIEWPORT_Z_CLIP (0x0d7c) NEGATIVE_W_TO_POSITIVE_W / ZERO_TO_POSITIVE_W. Vulkan clips z < 0 unless VK_EXT_depth_clip_control negativeOneToOne. The wrong mode either clips everything (black window) or puts all depth in 0.5..1 (depth of field blurs the screen). Ours: negativeOneToOne from NVN state. The hardware is programmed in two halves: SetDepthMode (0x35F = 0xd7c) selects the pre-transform clip range, and the viewport transform maps z to [near, far] itself: scale z = 0.5 * (far - near) and offset z = 0.5 * (near + far) for -1..1, scale far - near and offset near for 0..1. Same maths as Vulkan negativeOneToOne. OGD 0x0d7c
1.6 Depth clamp / clip: SET_VIEWPORT_CLIP_CONTROL (0x193c) has separate PIXEL_MIN_Z/PIXEL_MAX_Z CLIP or CLAMP and geometry-clip modes. depthClampEnable also disables z clipping (unless VK_EXT_depth_clip_enable). RADV had a dynamic depth clamp driver bug causing vertex explosions. One depth-clamp switch is enough: VIEW_VOLUME_CLIP_CONTROL (0x64F = 0x193c) is written as 0x101A with clamp on and 0x181D with clamp off (0x181D is the default at setup). Bits 3-4 (DepthClampNear/Far) are set in both values, bits 0-2 and 11 differ. There is no separate depth-clip toggle. The viewport near/far registers (Viewport array words 2-3) carry the clamp range and are swapped when far < near. Vulkan clamp to [min, max] of the viewport depth matches. OGD 0x193c
1.7 Depth formats: D24S8 is native; D24 quantises to 2^-24 everywhere in 0..1. Polygon offset units are in D24 steps for D24 targets; SET_DEPTH_BIAS_CONTROL (0x1110) DEPTH_FORMAT_DEPENDENT. RX 9070 has no D24S8 attachment; D32F has finer steps near 0 and a per-primitive exponent-based r. EQUAL / LEQUAL tests between a pre-pass and a later pass, and decal biases, behave differently. Depth-bias units need rescaling when D24 is emulated as D32F; the register holds twice the API value. A D16 load-clear of 0.5 stores 32767 while a fragment depth of 0.5 stores 32768, so depth EQUAL against cleared depth shows tiles. Ours: D24S8 → D32_SFLOAT_S8, bias not rescaled (check 4). PolygonOffsetUnits (0x56F = 0x15bc) is written as constantFactor * 2.0f, the clamp to 0x61F, the factor to 0x55B. OGD 0x1110, 0x15bc, 0x156c
1.8 Position invariance: identical position math gives identical depth across programs (pre-pass then EQUAL / LEQUAL). Only for outputs declared Invariant. The translator decorates position Invariant (XL CodeGen/Spirv/Declarations.cs:571) and marks float ops NoContraction unless QueryHostReducedPrecision (XL CodeGen/Spirv/Instructions.cs:2047). Without it ground z-fights, and foliage drawn with depth EQUAL vanishes when the pre-pass differs. Ours: translator default (all NoContraction). XL
1.9 Sampling a texture that is also a bound render target: Maxwell's texture cache is not coherent with ROP writes; NVN requires nvnCommandBufferBarrier(INVALIDATE_TEXTURE) between writing and sampling (methods INVALIDATE_TEXTURE_DATA_CACHE 0x1338, ..._NO_WFI 0x1288). Reading a different level/layer, or read-only depth while testing, is fine. Same image as attachment and sampled in one pass is a feedback loop: undefined unless VK_EXT_attachment_feedback_loop_layout; on AMD DCC/HTILE-compressed data is not readable by the sampler without decompression. Without barriers on attachment feedback loops, glow effects come out pixelated. On GCN, HTILE-compressed depth samples as black textures; AMD_DEBUG=nohyperz (HTILE off) avoids it. Ours: an NVN barrier ends the pass and inserts a full memory barrier; no detection of a sampled texture that is a current attachment (check 10). Barrier recipe: the ordering part and the cache part are separate. Ordering: Tiles = TiledCacheBarrier (0x3DF), Fragments = FragmentBarrier (0x378), Primitives = WaitForIdle (0x44), Full = gpfifo SetReference + new gpfifo entry with NoPrefetch, so later command and indirect-parameter fetch happens after everything finished. Tiles and Fragments need an implicit WFI before a cache invalidate. Caches (each opt-in): texture data InvalidateTextureDataCache (0x3DD, WFI) or ..NoWfi (0x4A2); shader InvalidateShaderCaches (0x369: instruction, global data, constant); descriptors InvalidateTextureHeaderCacheNoWfi (0x50A) + InvalidateSamplerCacheNoWfi (0x509); zcull 0x4B2; L2 gpfifo MemOpB L2FlushDirty + L2SysmemInvalidate. The texture-cache invalidate is its own flag; ordering alone does not refresh it. A queue flush that ends with image + shader + descriptor + L2 invalidation makes CPU updates visible between flushes: CPU writes into textures and descriptors reach the GPU at the next submit boundary, not mid-submit (2.2). OGD
1.10 Duplicate attachments: the same texture bound at two colour slots with write mask 0 on one does not interfere (ROP writes only enabled channels). Tile-based or compressed attachments may store the unwritten copy over the written one. A G-buffer with c1 = c2, c3 = c4 shows scattered squares in albedo; leaving the duplicate slot unattached fixes it. —
1.11 Per-target write masks: RtColorMask[i], or entry 0 for all when RtColorMaskShared. SET_SINGLE_CT_WRITE_CONTROL (0x0f90). A colour target with format 0 is disabled. Per-attachment colorWriteMask. Writing 0 to a target's format disables it. OGD 0x0f90
1.12 sRGB render target views: NVN encodes on write when the view format is sRGB over a linear texture. Maxwell also has a global SET_SRGB_WRITE (0x15b8). Format of the attachment view decides; needs MUTABLE_FORMAT. Attaching the base texture stores linear values: picture darker. Ours: RT views take the view format (view_format). OGD 0x15b8
1.13 Compressed and 3D targets: agl's GPU BC4 encoder renders blocks through an RGBA16UI view, one plane of a 32×32×32 BC4 3D texture per pass. NVN renders a 3D texture slice by slice. Needs BLOCK_TEXEL_VIEW_COMPATIBLE + EXTENDED_USAGE, 2D_ARRAY_COMPATIBLE. Shadow textures are drawn this way. A wrong block depth on 3D slices gives wrong textures on terrain. Already fixed here. —
1.14 Draw with no render target (fragment shader only writes an SSBO, 1×1 viewport) runs normally. Needs an attachment-less pass with explicit render area. Skipping such draws loses cave ambient light and leaves a bloom halo. Ours: area of viewport 0 (vk_exec.c resolve_rt). —
1.15 Window origin: NVN default lower-left (GL); NX2navi switches to upper-left. Y flip and scissor flip follow. Vulkan upper-left; negative viewport height to flip. The scissor flips against the screen height when Y-negate is on. —
1.16 Multiple viewports / scissors (16), selected by ViewportIndex output. Needs multiViewport and shaderOutputViewportIndex. Ours: one viewport, one scissor; differing scissors logged (nvn_api_cmd.c). OGD 0x192c
1.17 Fragment output saturation per target: SET_PS_SATURATE (0x13a8) clamps output n to 0..1 or -1..1 in fixed function. NVN does not expose it (GLSLC emits .SAT in code). None. Not emulated; listed so a clamp difference is not chased in the wrong place. OGD 0x13a8
1.18 New memory reads as zero (fresh pool pages); feedback effects never clear their state texture. New image content undefined. New textures need a zero fill. —
1.19 MSAA: nvnCommandBufferDownsample resolves; depth-stencil downsamples are copies the game does itself. vkCmdResolveImage colour only; integer formats pick one sample. Losing the stencil in a half-size depth copy removes ambient occlusion. —

2. Textures and samplers

# Maxwell / NVN Vulkan / AMD Effect / ours Source
2.1 Automatic block height: NVN truncates height_in_blocks * 1.5 / 8 before picking the GOB power of two; rounding up instead is wrong. Wrong H reads padding: a 160×160 BC3 texture draws half black. Allocation size is the same, so size checks do not catch it. Mip levels shrink the block height while the level fits in half of it. Opaque optimal tiling. Checked against 40 SDK-measured cases. Ours: truncating form already (runtime/nvn_tex.c, the block-height pick). The rest of the layout: height in block rows (rounded up), x 1.5, then / 8, then the largest power of two <= that, capped at 16 GOBs; 3D uses depth + depth / 2 directly as the GOB count; video images are fixed at 2 GOBs. Per level, the shift drops while unit << (shift - 1) >= level dimension with unit 8 rows / 1 slice; width tile is always 0. Array layers are padded to the block shrunk to the full height / depth. nvn_tex.c matches every one of these. —
2.2 Textures filled by the CPU with no NVN flush (NX2navi terrain: two 121-layer ASTC 8×8 arrays, 42 MB each; BC4 material textures read back with ReadTexels and copied into a 5120×5120 roughness atlas). The CPU and GPU share memory, so no flush is needed on the Switch. Host images are copies; must detect CPU writes. Write-protecting texture pages catches the writes; without an upload, zero ASTC decodes magenta, zero BC black, and a roughness atlas built from never-uploaded BC4 leaves stone unlit by torches. Small CPU writes into GPU-used pages that are not invalidated give slow rain/snow particles and particle corruption after a teleport. Ours: GetWriteWatch generations (vk_memory.c, mem_written_pages). —
2.3 Same storage, new texture object every frame (transient targets, deferred handles). Same storage with a different descriptor reinterprets the same block-linear bytes. Two VkImages over one allocation with optimal tiling do not share a defined layout (AMD tiling and DCC metadata are per image); a new image starts UNDEFINED. Sharing host textures by (pool, offset, description), or redirecting to the live texture at an address, keeps the bytes together. Wrong alias/overlap ordering in a texture cache blacks out whole areas for a frame on camera changes. Ours: GPU-only pool textures are VkImages bound onto pool memory (check 7). —
2.4 Deferred texture handles (BindTexturesDeferred): the GPU reads the handle from memory when the command runs; a game may record command blocks once and write handles later. n/a Handles copied at record time are zero: 3D scene black. —
2.5 Border colours: any RGBA (TSC words 4-7, plus sRGB border fields). The grass interaction map clamps to (0.5, 0.5, 0.5, 0.5), its "untouched" value. Three fixed colours unless VK_EXT_custom_border_color (+ customBorderColorWithoutFormat); integer formats need INT_CUSTOM. Emulating it in the shader works; with transparent black, grass away from the player lies flat and shows no cut. Ours: custom border when supported, else transparent black (check 11). —
2.6 sRGB decode is a TIC bit (G80_TIC_2_SRGB_CONVERSION) separate from the format; TSC has its own SRGB_CONVERSION (word 0 bit 13). NVN exposes sRGB as formats. Format decides. NVN-level translation by format is enough. —
2.7 Swizzles per texture and per view (TextureBuilderSetSwizzle, TextureViewSetSwizzle); layout captures may rebuild a texture on the same storage with (1, 1, 1, R) or alpha 1. Default per-format mapping R8 → (R, 0, 0, 1). VkComponentMapping on the view. Without it: font atlas white boxes, HUD dark boxes, a black stamina wheel. Ours: mapped (vk_exec_draw.c get_view). —
2.8 Sample dimension comes from the instruction: a non-array sample of an array texture reads layer 0; shaders may sample a cube as a cube array. View type must match the declared type. A 2D array bound as 2D gives one pale colour (washed-out scene); cubes created as 6-layer arrays with cube / cube-array views work. Ours: views per resource type (get_view). —
2.9 LOD: TSC min/max LOD clamp, LOD bias, TWO_D_NO_MIPMAP texture type, a min filter without mips stays on the base level; TIC carries its own max anisotropy and aniso quality fields. minLod/maxLod, mipLodBias limited by maxSamplerLodBias. Ours: non-mip filter → maxLod = minLod + 0.25, bias clamped (vk_texture.c make_sampler). —
2.10 Texture gather rounding: Maxwell sub-texel precision differs from AMD/Intel; edge texels picked by TLD4 differ by one. Implementation-defined subTexelPrecisionBits. A sub-pixel offset on gathers on non-NVIDIA hosts fixes shadow artifacts at object edges. Ours: the translator biases gather coordinates by the host's subTexelPrecisionBits on every non-NVIDIA GPU (vk_caps.c → job gather_bias → XL TexturePass.InsertCoordGatherBias); without it RADV drew dashed dark lines along wall and floor edges that changed with the camera angle. NX2NAVI_GATHER_BIAS=0 turns it off. —
2.11 Gather offsets: TLD4.PTP takes four per-texel offsets, possibly from registers. ConstOffsets must be constant. Ours: QueryHostSupportsNonConstantTextureOffset() => false makes the translator emulate register offsets (tools/xlate/Program.cs). XL Translation/Optimizations (TexturePass)
2.13 TXD gradient register order: NX2navi's terrain shaders fill the gradient registers of a TXD per coordinate (du/dx, du/dy, dv/dx, dv/dy). Vulkan's Grad takes dPdx (du/dx, dv/dx) and dPdy (du/dy, dv/dy). The translator's EmitTxd takes the first two registers as dPdx and the last two as dPdy, so the mip level came from the wrong derivatives and was too sharp on terrain faces turned across the screen: patches that followed the faces crawled when the camera moved (found from an F9 frame: a gradient sample's coordinate and gradients traced back to DPdx and DPdy of the interpolants). Ours: gfx_shader.c patch_txd_transpose swaps the two inner components of every two-component gradient pair in the loaded SPIR-V (the shader cache is untouched). NX2NAVI_TXD_TRANSPOSE=0 turns it off. Only 2D gradients are handled; 3D and cube ones are left as translated. —
2.12 Depth compare (TSC depth compare + TIC_4_DEPTH_TEXTURE), min/max reduction filtering. compareEnable; reduction mode needs samplerFilterMinmax and is not allowed with compare. Ours: reduction dropped when compare is on (vk_texture.c make_sampler). A depth-compare instruction through a sampler whose compare is off still compares (the game's shadow reads do this); in Vulkan that is undefined, NVIDIA's driver compares and AMD's returns the depth. Ours: such a read through a bound sampler gets the slot's sampler with the compare on (vk_sampler_compare_by_id_locked; the game's function, else LEQUAL; NX2NAVI_SHADOW_COMPARE=0 off). Reads through the sampler pool (bindless) still take the game's sampler as it is. —
2.13 ASTC / BC: native ASTC; zero ASTC block = error colour on hosts. AMD and NVIDIA desktop GPUs lack ASTC: decode to RGBA8. Ours: native upload where the device samples ASTC, else a compute-shader decode (vk_astc.c, NX2NAVI_ASTC), CPU decode (tex_astc.c) as the fallback. runtime/vk_astc.c
2.14 Linear CPU-written textures (FLAGS_LINEAR, small ramps, glyph caches): CPU writes visible to the GPU directly. Must re-upload or use linear images over host memory. Without them a sky drawn from 64×1 RGBA16F ramps is black. —

3. Shader math

# Maxwell Vulkan / AMD Effect / ours Source
3.1 FMNMX and .SAT with NaN return the other operand / 0 (IEEE minNum). Games rely on this to stop NaN spreading (one NaN pixel blown up by bloom or temporal passes flickers or fills the screen). GLSL.std.450 FMin/FMax/FClamp: result undefined when an operand is NaN (NMin/NMax are the NaN-dropping forms). AMD hardware min/max usually drops NaN; the compiler may still assume no NaN. The translator emits FMin/FMax/FClamp (XL CodeGen/Spirv/Instructions.cs:336, 956, 972; saturate = FClamp 0..1, Instructions/InstEmitAluHelper.cs:419-423). isnan selects give the Maxwell result. GLSL450
3.2 FMZ multiply mode (FMUL/FFMA/... field Fmz, .FMZ): 0 × anything = 0, including Inf/NaN (DX9 rule). IEEE: 0 × Inf = NaN. The translator decodes Fmz (XL Decoders/InstDecoders.cs:306-309, 1901) but no emitter reads it. Ours: same. Census needed (check 13). XL
3.3 Denormals: per-instruction .FTZ. AMD flushes fp32 denormals by default; VK_KHR_shader_float_controls sets it per shader. The translator ignores .FTZ. Low risk. XL (no Ftz use in Instructions/)
3.4 Warp size 32: SHFL, VOTE, S2R LANEID, ballots are 32-bit. AMD RDNA runs wave32 or wave64 per shader (the driver chooses; RADV fragment default wave64); VK_EXT_subgroup_size_control requiredSubgroupSize = 32 fixes it. The translator adapts lane masks to QueryHostSubgroupSize() (default 32, XL IGpuAccessor.cs:218; Instructions/InstEmitWarp.cs:72, 106, InstEmitMove.cs:175-185). Ours: not overridden, no required size (check 6). https://docs.mesa3d.org/envvars.html (RADV_PERFTEST=pswave32)
3.5 Discard (KIL) keeps the quad's derivatives valid (demote semantics). OpKill terminates; later derivatives in the quad undefined. The translator emits OpKill (XL CodeGen/Spirv/Instructions.cs:508). —
3.6 Implicit-LOD sampling in divergent control flow works on Maxwell (TEX unit uses quad neighbours). Undefined in non-uniform control flow; AMD whole-quad mode usually works. — VKSPEC (derivatives)
3.7 Integer division does not exist (compiled to XMAD/IMAD sequences); MUFU.RCP(0) = +Inf, LG2(0) = -Inf. Same IEEE edge cases through 1/x, Log2. Low risk. —
3.8 FP64 in some NX2navi cutscene shaders. No FP64 on some hosts. Ours: softfloat64 job flag lowers FP64 to FP32 (tools/xlate/Program.cs QueryHostSupportsShaderFloat64). —
3.9 Integer vertex inputs read as raw bits (bone indices R8UI). Float-declared inputs convert integers. Without it characters stretch; re-translating with uint inputs fixes it. Ours: attribute types passed to QueryGraphicsState (Program.cs). —
3.10 BFE / BFI fields past bit 31 (as PTX bfe / bfi): a field that runs past bit 31 is cut there; one that starts past it extracts 0 (signed: bit 31 repeated) and inserts nothing. Terrain vertex shaders (42 variants) have taken the position from a register, ((2y + x) * 8) & 0xff with 5-bit x and y, so up to 248. OpBitField*Extract / OpBitFieldInsert undefined when offset or offset + count exceeds 32; AMD's bitfield instructions take offset and width modulo 32. The translator passes the fields through. Terrain walls and ground went missing on the Steam Deck and a Linux RX 570 (RADV). Ours: clamped where the field is not a constant inside the word (translator patch 0012). RX 9070 on Windows drew the terrain the same before and after. PTX ISA bfe, bfi
3.11 SHF by 0 gives the unshifted word (C for SHF.L, A for SHF.R). The translator's funnel shifts the other word by 32: undefined in SPIR-V; AMD shifts take the low 5 bits and give A | C. Gives terrain gaps at a distance. Ours: a select on a zero amount (translator patch 0013). In the shaders checked, every register SHF pairs its word with 0, so the result was right already. —
3.12 Registers read before any write on a path (paths the structurizer adds included) have no defined value in the game's code; the NVIDIA driver has been reported to give 0. An OpVariable without an initializer loads an undefined value; Mesa folds undefined values freely. Ours: every function-local variable starts at 0 (OpConstantNull, job flag zero_locals, translator patch 0016) on every driver; NX2NAVI_ZERO_LOCALS=0 turns it off. On NVIDIA an undefined read takes another warp lane's value: Ocarina's clustered-light loop lit its floors in 4x8-pixel tiles (one warp each) and darkened Link. It was off while DLAA was on, because zeroing moves the SPIR-V hash of the game's FXAA shader that vk_dlaa.c vk_dlaa_skip_draw matches (FXAA_FS holds the zeroed hash too); with it off the tiles showed under DLSS and DLAA, and the shader cache keys, which include the flag, changed with the upscaler, so shaders prepared with one setting missed under the other. Checked on an RTX 5090: zeroed, DLAA keeps its camera motion (NX2NAVI_MV_CHECK) and the floor is clean. —

4. Memory model

# Maxwell / Tegra Vulkan / AMD discrete Effect / ours Source
4.1 One memory: the CPU reads GPU-written buffers and textures after a fence; the GPU sees CPU writes after FlushMappedRange or, in practice, immediately (NX2navi skips flushes). Block-linear layout is defined, so a buffer copy into texture storage is a texture upload. Images live in VRAM with private tiling; buffer bytes copied into image memory are not texels. Small CPU writes (2.2); copies through two buffers over one allocation need alias visibility barriers. Ours: GPU-written ranges of CPU-visible pools marked at fence completion; GPU-only pools nothing (check 8). —
4.2 Counter reports (ReportCounter, SET_REPORT_SEMAPHORE_A..D 0x1b00): {counter, timestamp}; TIMESTAMP puts 0 in the counter; SAMPLES_PASSED counts visible samples; ZCULL stats. Occlusion queries are separate objects; results copied to a buffer. The timestamp's counter of 0 matters for conditional clears. Ours: every report writes {0, time}; occlusion counts are always 0 (check 5). Hardware recipe: SET_REPORT_SEMAPHORE_OFFSET (0x6C0 = 0x1b00) with payload 0 and OPERATION=Counter, STRUCTURE_SIZE=FourWords: TIMESTAMP = unit Crop, counter Payload, written when the ROP has retired every earlier draw; SAMPLES_PASSED = unit Crop, counter 0x15; ZCULL stats = four one-word reports at +0, +4, +8, +12. TIMESTAMP_TOP is not a 3D report: a gpfifo Semaphore release of 16 bytes with ReleaseWfiDisable, written when the command processor reaches it, without waiting for earlier work. So TIMESTAMP - TIMESTAMP_TOP around a span is its GPU duration. Layout: two 64-bit words, counter then timestamp. Each report is followed by a tiled-cache flush. ResetCounter (0x54C = 0x1530) zeroes SAMPLES_PASSED; it is the reset for the timestamp types too. Unit: ns = ticks * 625 / 384, i.e. 614.4 MHz GPU ticks (1.63 ns), not the 19.2 MHz system tick (check 21). OGD 0x1b00
4.3 Semaphores / events written by the GPU in order; CPU polls them. Writes on a completion thread after the fence. Ours: vk_completion_add, submit order. —
4.4 Inline uniform updates (UpdateUniformBuffer) happen in command order between draws; reading the buffer at record time gives the last value. Must snapshot. Snapshot at encode time; a wave simulation diverged when two writes reordered. —
4.5 GPU-written index buffers (8-bit indices written earlier in the same submission). No uint8 index type without VK_EXT_index_type_uint8; CPU widening reads stale bytes. Widening on the GPU avoids it. Ours: CPU widening at record time; device-local byte indices skipped (check 9). —
4.6 Compute shared memory: 48 KiB per block on GM20B. AMD 64 KiB. Ours: QueryComputeSharedMemorySize from job (Program.cs). OGD clb1c0.h
4.7 Virtual memory pools map physical pool pages; the same bytes reachable at two GPU addresses. Two VkBuffers over one allocation need alias-aware barriers. — —
4.8 Command list calls: a CallCommands / CallList is not a GPU subroutine. A call is stored as a control entry; at submit the CPU walks the entries and recurses into the called list, emitting gpfifo entries that point at the command words, which the GPU fetches later (prefetch can be switched off per entry). So list structure is fixed at submit, command words are read when fetched. n/a Ours: OP_CALL is walked at replay, CallCommands inlines the recording at record time (vk_exec.c walk, nvn_api_cmd.c inline_recording). Equivalent for control flow; deferred bind handles are the data read late (2.4). —

5. Fixed function

# Maxwell / NVN Vulkan / AMD Effect / ours Source
5.1 Provoking vertex: last (GL convention, NVN default; SET_PROVOKING_VERTEX 0x1684, NVN 55 SetProvokingVertexMode). Flat-interpolated outputs (instance/particle ids, material indices, flat normals) come from the last vertex. First vertex unless VK_EXT_provoking_vertex with provokingVertexMode = LAST_VERTEX. Ours: last-vertex mode where provokingVertexLast is available (vk_pipeline.c create_graphics; check 1). ProvokingVertexLast (0x5A1) is set to 1 at queue setup and per rasterizer state, with last as the default: last is the hardware-side default. OGD 0x1684; runtime/nvn_ext.c
5.2 Primitive restart: any index value, compared against the raw index for any index size; can apply to lists (SET_DA_PRIMITIVE_RESTART_TOPOLOGY_CHANGE) and to non-indexed draws (..._VERTEX_ARRAY 0x0de8). Restart only at all-ones of the index type; lists need primitiveTopologyListRestart. NX2navi terrain strips use 0xffff. Ours: non-all-ones restart not replayed, logged. PrimitiveRestartEnable (0x591 = 0x1644) + PrimitiveRestartIndex (0x592 = 0x1648), index written only when enabled; any value; PrimitiveRestartWithDrawArrays (0x37A = 0x0de8) is set to 0 at setup. OGD 0x1644, 0x1648
5.3 Point size: SET_POINT_SIZE (0x1518) when the shader does not write it; GLSLC clamps a written size to a driver constant. Point size must be written by the shader. The translator writes the PointSize from graphics state at VS start (XL Translation/EmitterContext.cs:84-88); ours reads the SetPointSize state from the support buffer (translator patch 0008). —
5.4 Line width, smooth lines (SET_LINE_WIDTH_FLOAT 0x13b0, SET_ANTI_ALIASED_LINE). wideLines, VK_EXT_line_rasterization. Ours: 1 px, logged. OGD
5.5 Polygon offset units are twice the API value in the register; NVN API takes GL units. depthBiasConstantFactor in format-dependent r. The register value is constantFactor * 2.0f (1.7). Ours takes NVN API units (right) but D32F instead of D24 (1.7). OGD 0x15bc, 0x187c
5.6 Dual-source blending: SRC1 factors read the shader's second output. Needs fragment output Location 0, Index 1. The translator declares Index only when DualSourceBlendEnable (XL Translation/Translator.cs:196, CodeGen/Spirv/Declarations.cs:608-617). Ours: SRC1 factors mapped (vk_pipeline.c blend_factor) but the translator is always given false (Program.cs QueryGraphicsState) (check 12). —
5.7 Alpha-to-coverage with dither (SET_ALPHA_TO_COVERAGE_DITHER_CONTROL 0x12e0), alpha test (fixed function on Maxwell). A2C without dither; no alpha test. The translator can emulate alpha test and A2C dither in the shader from graphics state; ours passes AlphaTestOp.Always, A2C false to the translator (Program.cs QueryGraphicsState). OGD 0x12e0, 0x16b4
5.8 Logic ops on colour targets. logicOp feature; ignored on float targets. Ours: in the pipeline key. OGD 0x19c4
5.9 Vertex formats: *SCALED 8/16-bit, RGB10A2 SNORM/SCALED, 3-component formats, 32-bit scaled. No 32-bit SCALED; 3-component formats often unsupported. Ours: 32-bit scaled read as integers (logged), 3-component widened (vk_pipeline.c vertex format table). —
5.10 Transform feedback, rasterizer discard. VK_EXT_transform_feedback. Ours: rasterizer discard without XFB = no draw (nvn_api_cmd.c). OGD 0x0744
5.11 Depth bounds, sample mask, conservative raster. Optional features. Ours: ignored, logged (nvn_ext.c). OGD 0x0fa4
5.12 Blend factors on targets without alpha (RGBX emulated as RGBA). Destination alpha reads the stored value. DST_ALPHA factors need replacing on no-alpha targets. —

6. Checks for our runtime

Grep-level look on 2026-10-22, statuses updated 2026-10-26; "saw" is what the grep and short reads showed, not a test result.

  1. Provoking vertex. Vulkan draws flat outputs from the first vertex, NVN from the last. vk_pipeline.c enables provokingVertexLast and chains VkPipelineRasterizationProvokingVertexStateCreateInfoEXT{LAST_VERTEX} in create_graphics; without the feature it logs that flat outputs come from the first vertex. Fixed.

  2. Unwritten vertex outputs. spirv_add_outputs (vk_pipeline.c) adds outputs initialised to (0, 0, 0, 1) for FS inputs the VS lacks. Our translator never links stages, so its InitializeVertexOutputs would initialise no user output; translator patch 0006 initialises the unlinked outputs. Fixed.

  3. Masked clears. vk_exec.c skips colour clears with a partial channel mask (W_CLEAR_MASK); clear_ds skips stencil clears with a partial mask (W_STENCIL_MASK). NX2navi issues such clears. Check the log for colour clear with channel mask and stencil clear with mask. The hardware recipe (1.3) shows what the replay must do: colour clear = scissor 0 + channel mask, stencil clear = value & mask merged under the front write mask; so a draw with colorWriteMask / stencilWriteMask set to the clear mask is the exact equivalent. Missing.

  4. D24 → D32F. vk_formats.c maps D24S8 to D32_SFLOAT_S8_UINT; vk_exec_draw.c record_fixed passes NVN units unscaled. Depth EQUAL passes and decal offsets can z-fight. Look at: per-pass depth-test op histogram, EQUAL passes against D24 targets. N/A for NX2navi: a census of the field and fight saves (2026-10-22) saw depth targets 0x34 (DEPTH32F) and 0x32 (DEPTH16) only; biased draws (units -5 and 1, factor -0.5 and -0.2) are on DEPTH32F.

  5. Counters and conditional rendering. vk_exec_xfer.c (as of 2026-10-22): every report writes {0, time}; SAMPLES_PASSED is always 0; a condition is read on the CPU at record time and an unfinished report reads 0. Grep the NVN log for counter types other than TIMESTAMP; any occlusion-driven visibility (flares, culling) is off. Fixed 2026-10-22: SAMPLES_PASSED counts come from Vulkan occlusion queries between RESET and REPORT (vk_exec_xfer.c, ~4 queries per frame in the field, ~21 in a fight); the game's CPU reads them (guest f_70010c3298) to cull objects.

  6. Subgroup size. The translator assumes 32; RDNA4 may run fragment shaders in wave64. vk_caps.c and vk_pipeline.c pin_subgroup32 pin stages to 32 lanes with VkPipelineShaderStageRequiredSubgroupSizeCreateInfo where subgroupSizeControl allows it, and otherwise tell the translator the GPU's own size (NX2NAVI_VK_SUBGROUP_PIN=0 forces that). Fixed.

  7. Images aliased onto GPU-only pool memory. vk_texture.c bind_pool_memory binds each VkImage into its pool allocation; textures are keyed by address plus descriptor, and each new image starts UNDEFINED (init_image). Two descriptors over the same storage, or bytes written through a buffer, do not see each other's data on AMD. Check: count distinct keys per address per frame. Risk (the alias/overlap ordering class of 2.3).

  8. GPU writes into CPU-visible pools. note_gpu_write (vk_exec_xfer.c) stamps pages when the fence completes, so a texture sampled later in the same submission keeps its old upload; GPU-only pools are skipped. At most one buffer copy per frame has been seen in NX2navi, so low priority. Risk.

  9. Index buffers. vk_exec_draw.c op_draw_idx: byte indices widened on the CPU at record time, skipped in device-local memory; restart_for: a non-all-ones restart index logged and dropped. Census (2026-10-22): 130-170 byte-index draws/frame widened on the CPU, 0 device-local, 0 non-all-ones restarts. OK; VK_KHR_index_type_uint8 (supported) would drop the CPU cost.

  10. Feedback loops. Census (2026-10-22): 425/frame (field) to 667 (fight), G-buffer material/decal shaders sampling the 1600x900 targets of the open pass. The terrain G-buffer pass reads its own albedo, normal and R32F targets at the fragment's pixel; on the RX 9070 the reads returned stale data as ~16x14 px dark rectangles on the ground, and a character next to Link was not drawn. vk_exec_draw.c (feedback loops) now detects such a draw: with VK_EXT_attachment_feedback_loop_layout and dynamicRenderingLocalRead the target moves to the feedback layout and a by-region barrier precedes the draw; without them the pass restarts behind a full barrier (NX2NAVI_FEEDBACK). Fixed.

  11. Border colours. vk_texture.c make_sampler: custom colour only with customBorderColorWithoutFormat, else transparent black; integer formats use the float path. Confirm the RX 9070 reports the feature (grass map needs 0.5). OK if supported.

  12. Dual-source blend. vk_pipeline.c blend_factor maps SRC1 factors; Program.cs QueryGraphicsState always passes dualSourceBlendEnable = false, so no Index 1 output exists. Log pipelines with SRC1 factors. Mismatch if used.

  13. FMZ / NaN. Count FMUL/FFMA/FMUL32I/FFMA32I words with the Fmz field set (Decoders/InstDecoders.cs:1901 bit positions) in build/v121/shaders/; FMin/FMax/FClamp NaN results are undefined (3.1). Census (2026-10-22): FMZ in 1 instruction of 12397 shaders, FTZ in 12241 shaders, FMNMX in 7457. The translator emits GLSL.std.450 FMin/FMax (GenerateMinimum), not NMin/NMax. FMZ: no fix needed. FMNMX: OK (RX 9070 test: FMin/FMax(NaN, 1) = 1 both orders, FClamp(NaN, 0, 1) = 0).

  14. Point size / line width. Census: SetLineWidth 4.5 ~1/frame (2 line draws into a 144x144 R8 target), SetPointSize 5 ~14/frame; 4 point VS rely on it. Point size: translator patch 0008 reads it from the support buffer, fixed. Line width: 1 px, non-default values logged (nvn_ext.c), mismatch, minor.

  15. Block height. nvn_tex.c truncates like NVN (2.1); every other step of the layout matches. OK.

  16. Depth mode and clamp. vk_pipeline.c create_graphics (negativeOneToOne from nvn_depth_minus_w, depthClampEnable from NVN state). Matches the hardware z maths (1.5) and clamp switch (1.6). OK.

  17. Depth bias argument order. vk_exec_draw.c record_fixed (units, clamp, factor); the register takes the same three, units doubled (1.7). OK.

  18. Sampler LOD. vk_texture.c make_sampler. OK.

  19. Viewport / scissor arrays. Single viewport (vk_exec_draw.c record_fixed), extra scissors logged. Census: 0 differing. OK.

  20. Depth-compare sampling in compute (VUID-vkCmdDispatch-None-06479): one compute shader samples a 4x4 R32F CPU-written placeholder through an R32_SFLOAT view with compare on, ~1/frame. Needs a D32 copy. Minor.

  21. Report timestamps: unit and instant. write_counter (vk_exec_xfer.c) writes host ns * 12 / 625 (19.2 MHz ticks) at fence completion for every report; nvnDeviceGetTimestampInNanoseconds converts back with the same factor (nvn_ext.c). On the hardware, GPU ticks convert at 625/384 ns (614.4 MHz), TIMESTAMP is written when the ROP has retired the earlier work, TIMESTAMP_TOP when the command processor reaches it (4.2). Two consequences for the engine's GPU time (dynamic resolution and speed follow it): (a) if the game's consumer scales raw report ticks with its own constant rather than the NVN conversion call, our 19.2 MHz unit is 32x off from the hardware's; find that consumer before fixing the unit; (b) the delta needs vkCmdWriteTimestamp2 with TOP_OF_PIPE for TIMESTAMP_TOP and BOTTOM_OF_PIPE for TIMESTAMP, per submission segment, converted to whichever unit (a) shows. Mismatch (instant); unit unverified.