mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Vulkan has no quad or line-loop topology. Draws without indices were rewritten already; an indexed quad list went to the GPU with its indices under the triangle list topology, three at a time, joining corners of neighbouring quads, and an indexed loop lacked its closing line. Off by default and read at start; no log of Ocarina has shown such a draw so far, and the new line says when one comes. tests/index_rewrite_check.py checks the index lists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
222 lines
49 KiB
Markdown
222 lines
49 KiB
Markdown
# 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:
|
||
|
||
- **OGD**: NVIDIA open-gpu-doc `classes/3d/clb197.h`, `classes/compute/clb1c0.h` (MIT-style),
|
||
<https://github.com/NVIDIA/open-gpu-doc/tree/master/classes>. Offsets below are method offsets in `clb197.h`. Where a
|
||
method is given as a word index (`0x5A1`), the byte offset is index x 4, so `0x5A1` = OGD `0x1684`.
|
||
- **XL**: the shader translator's source as `tools/build_xlate.py` patches it,
|
||
`build/xlate/ryujinx/src/Ryujinx.Graphics.Shader/`. Paths below are relative to it.
|
||
- **VKSPEC**: Vulkan 1.4 specification, <https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html>;
|
||
**GLSL450**: SPIR-V GLSL.std.450 extended instructions, <https://registry.khronos.org/SPIR-V/specs/unified1/GLSL.std.450.html>.
|
||
|
||
## 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 `VkImage`s 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 `VkImage`s 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. A Switch 2 title (NVN 200 and up) is left out: `AmpereLowering.cs` `Txd` puts SM86's interleaved derivatives in the translator's order already, and transposing again swapped them back (`=1` forces it on). | — |
|
||
| 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. A compare through a linear filter tests the 2x2 texels around the coordinate and blends the four results (percentage-closer filtering), through a nearest one it tests one texel: shadow edges in steps of shadow map texels. Ours: the game's filter, linear with `NX2NAVI_SHADOW_SCALE` above 1 or `NX2NAVI_SHADOW_PCF=1` (`vk_texture.c` `make_sampler`, `shadow_pcf`); the log lists the filters of the compare samplers made ("depth-compare sampler:"). | — |
|
||
| 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. | — |
|
||
| 3.13 | **NaN, infinity and signed zero follow IEEE 754**, and the game's code leans on them: its compiler writes an unordered comparison as `x > c \|\| isnan(x)`, a normalisation of a zero-length vector gives an infinity that a later test catches, and integer masks (0xffffffff) pass through float moves and selects. | SPIR-V without the `SignedZeroInfNanPreserve` execution mode lets the driver assume that no float is NaN or infinite. AMD's compilers act on it; NVIDIA's driver keeps the IEEE results. | The isnan tests of the game and of the translator (patches 0027, 0031) are only safe with the mode. On an RX 9070 XT a select between 1 and the bits 0xffffffff, converted as a signed integer, gave 2^32 instead of -1 and blew shadow casters up (issue #156); RX 6000 cards drew black and white blocks on Navi and on Link next to her (#41, #110); AMD GPUs drew Navi's sparkles without her orb. Ours: `vk_pipeline.c` `spirv_nan_preserve` adds the mode for 32-bit floats (16 and 64 where the module declares them and the driver can) when the module is created, on every driver except NVIDIA's (`NX2NAVI_VK_NAN_PRESERVE`). On AMD's Windows driver for RDNA 1 and 2 ("AMD proprietary shader compiler", `vk_caps.amd_sc`), where the black and white blocks were reported, `spirv_nan_bits` also turns every 32-bit `OpIsNan` into an integer test of its bits and a float select with a NaN-bits constant into a select of the bits, which no float assumption can change (`NX2NAVI_VK_NAN_BITS`). | VKSPEC (SPIR-V environment, precision and operation of SPIR-V instructions) |
|
||
|
||
## 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 `VkBuffer`s over one allocation need alias-aware barriers. | — | — |
|
||
| 4.8 | **Events gate queues**: a command buffer's `WaitEvent` holds its queue until a `SignalEvent` of another queue, or `nvnEventSignal` on the CPU, gives the event the value; the GPU reads what the commands behind the wait use only from then on. Ocarina's worker queues wait for the main queue's signals and the other way round, and the main queue waits each frame for a signal the CPU gives once the game has written the frame's data, before, after or during the main queue's submit call. | One queue in submission order, and the replay copies what a submission reads (VRAM mirror sync, uniform snapshot) when its recording starts. | A wait reached before its signal stops its submission, which goes on when the signal has been replayed (`vk_exec.c`, "event order across the game's queues"). A CPU signal counts for a submission only when it came before those copies; one that comes during the replay stops the submission, and it goes on at once in a submission of its own (`vk_exec_xfer.c`, "events the CPU signals"). Passing the wait on it drew single frames from copies taken before the game had finished writing: the scene's shadowing missing or the scene black, the interface as it was. | — |
|
||
| 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). | — |
|
||
| 4.9 | **The texture pool is read as the GPU goes** (bindless `TEX.B` handles index it), and the GPU runs a submission right after it is submitted, so it sees the pool as of about the submit; Ocarina registers the next submission's textures right after submitting. | A descriptor a pending command buffer may read must not be updated (update-after-bind allows only unused ones), and on RADV a view of memory allocated after a submit is not in that submit's page tables. | A replay part handed to the queue early (`NX2NAVI_SPLIT_DRAWS`) kept reading the set later parts wrote: GPU page fault (`gfxhub`, TCP read) on the 128×128 probe pass of Kokiri Forest's first world frame, about 40 s in, nearly every run on RX 9070 XT. Ours: one pool set per submission from a ring, sealed when a part goes; the next sync that has entries to write takes another set, and no part is split off while every other set belongs to the same submission (`vk_exec_draw.c` `vk_exec_pool_split`, `pool_take_set`). A registration the game makes while a submission is being replayed takes effect after that submission has gone to the queue, and with the replay on its own thread where the game made it among its submits (`vk_backend.c` "registrations in submit order", `NX2NAVI_REG_ORDER`). | — |
|
||
|
||
## 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. | — |
|
||
| 5.13 | **Quads, quad strips, polygons and line loops** are primitive types of their own, indexed or not. | No such topologies: a quad list read as a triangle list takes three indices at a time and joins corners of neighbouring quads; a line loop drawn as a strip lacks its closing line. A quad strip covers what a triangle strip of the same vertices covers, a convex polygon what a fan covers. | Ours: quad strips as triangle strips, polygons as fans (`vk_pipeline.c` `vk_pipeline_topology`); non-indexed quads and loops get indices made by the replay (`vk_exec_draw.c` `op_draw`); indexed ones are drawn with the game's indices unless `NX2NAVI_QUAD_INDEX=1` rewrites them (`draw_idx_rewritten`, `vk_index_rewrite.h`). The first indexed one is logged either way; no log of Ocarina has shown one so far. | — |
|
||
|
||
## 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. `gfx_shader.c` reflects the full declared location span of VS output and FS input arrays,
|
||
not just their base location: an FS `vec4[16]` requires all 16 locations even when it only accesses some elements.
|
||
The same reflected mask feeds recorded-pipeline prewarm, independent stage-library unions and direct pipeline builds.
|
||
Our translator never links stages, so its `InitializeVertexOutputs` would initialise no user output;
|
||
translator patch 0006 initialises the unlinked outputs. Mesh programs: a component of a position or generic vector
|
||
that the program's output map leaves out is written as (0, 0, 0, 1) by the mesh entry function
|
||
(`tools/xlate/MeshSpirv.cs`) and by the fallback's vertex stage (`runtime/mesh_fb.c` `meshfb_vs`); a whole location
|
||
the fragment stage reads and the mesh program does not write is filled in the fragment module of the mesh pipeline
|
||
(`vk_pipeline.c` `spirv_fs_fill_inputs`): (0, 0, 0, 1), or for a mesh program that draws points the point sprite's
|
||
coordinate (s, t, 0, 1): the fragment shaders recorded with two of the game's point-drawing mesh programs read
|
||
location 5, which those programs never write.
|
||
**Implemented; array-span runtime validation pending.**
|
||
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.
|
||
A report reaches guest memory as a CPU write after its submission's fence, a ClearBuffer or ReportValue as a fill the
|
||
GPU makes in stream order, so a report followed by a clear of the same record ended as the report, and a condition
|
||
replayed after the clear read memory from before the fill. A draw that finds the query ring full (every slot still
|
||
waiting for the completion thread, in a hitch at a high frame rate) is left uncounted and the report after it says
|
||
what the rest counted, at least 1, which the game reads as an object all but hidden for that frame;
|
||
`NX2NAVI_OQ_HOLD=1` makes such a report repeat the larger of that and its record's last complete count
|
||
(`vk_exec_xfer.c` `oq_report_count`, `vk_oq_hold.h`), and the log counts them ("missed draws the full occlusion
|
||
query ring left uncounted"). A clear of at most 256 bytes is now also applied in the
|
||
replay to the counter records it covers (`vk_exec_xfer.c` `clear_small`, `NX2NAVI_CLEAR_SMALL_CPU`): earlier reports
|
||
leave the cleared bytes alone, conditions read them from memory. Other small clears stay GPU fills alone, since the
|
||
GPU reads guest memory too and a write ahead of it would reach passes that still want the old value. Whether the
|
||
game clears counter records at all is not known: captures show one buffer fill a frame, of unknown address; the
|
||
periodic `vk_usnap:` log line counts the small clears and the records they covered.
|
||
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 in the translation. `vk_pipeline.c` gives a
|
||
pipeline whose blend state uses a SRC1 factor (`uses_src1`, `GKey.dsb`) a fragment module with the second output
|
||
at `Index 1` (`spirv_dual_source`) where the device has `dualSrcBlend`. **Fixed** (checked in the code on
|
||
2026-11-12, not by a capture).
|
||
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
|
||
NMin/NMax/NClamp (patch 0031), which return the other operand for a NaN as FMNMX does. **FMZ: no fix needed. FMNMX: OK**
|
||
where the driver keeps NaNs (3.13).
|
||
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: recorded with the draw state (`nvn_api_cmd.c`, `OP_BIND_STATE` kind 7) and set as
|
||
dynamic state for draws of the line topology class (`vk_pipeline.c` `vk_pipeline_line_width`), held to the
|
||
device's `lineWidthRange`; 1 px on a device without `wideLines` and for triangles drawn in the line polygon mode.
|
||
**Implemented; not compared with the hardware's wide-line coverage.**
|
||
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. Where the driver does not compare
|
||
the colour format, the dispatch samples a depth twin of the image, copied right before it (`vk_exec_draw.c`,
|
||
"depth twins"; R32_SFLOAT and R16_UNORM). **Fixed for compute passes**; a draw that samples such an image keeps
|
||
the colour view.
|
||
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.** What it can change:
|
||
both differences make the GPU time the game computes shorter than the hardware's (a report written at fence
|
||
completion spans less than top to bottom of the pipeline, and 19.2 MHz ticks read as 614.4 MHz ones are a
|
||
thirty-second), so an engine that lowers its resolution or detail under GPU load would stay at its highest
|
||
here. That is reasoning from the two units, not something measured; no picture difference is tied to it.
|