D2R (Diablo II Resurrected) has no profile and its code could not run here: the host-call svcs its cuts emitted only died, the touch-run-v4 rule set needed a missing dm/PRODUCT, and the host files its hooks called were never in this repository. Removed with it: - rewriter: D2R cuts, observation gates and parameter slots, the touch-run-v4 rule set and private gates, the boot list, the cut scope column and the KRES / KASPECT key slots (fixups.tsv keeps the four thread slots; no params.tsv any more); dm/tools/product.sh - host: D2R save edits, add-on content, render-scale controller, time() takeover and Terror Zone clock, crash decoding at D2R offsets, host-call registry and stubs, i18n, UI-screen, shell-app loader, zero-stripped archives, boot profiling hooks with no implementation; D2R headers, tests and scripts; dm/ios/Sources/dm_touch_controls; dm/third_party/libdeflate - graphics: runtime extrapolation, world freeze, iPad UI shift, shop drag image, brightness LUT, visual ruler, bloom / FXAA shader hashes, DM_GFXUSE, the eco-fps setting, the loading-screen gate; shader_map.py, nometal_run.py, nvn_slots.py, nvn_slots.tsv and the PC-texture samples The legacy frame step (frame_step legacy, DM_FRAME_STEP) is retired: the Breath of the Wild and Tears of the Kingdom stubs lose their mode test. Breath of the Wild's interval-controller cuts stay: the framework keeps the controller's interval while the game's VFR override holds. patch_nso.py output is byte-identical for all 15 games on this machine except the two frame-step stubs; all 18 profiles build; the host tests pass (their Makefile now builds the general tests only).
10 KiB
NVN → Metal object mapping
This document lists what each kind of NVN (the Switch's native graphics interface) object and command lands on in Metal in the host's graphics layer. For the overall structure of the graphics layer see README.md.
Scope: the NVN functions games call. The others are connected to a red-reporting stub that, when called, prints [RED] NVN unimplemented function called: <name> in the log.
Two conventions that run through the whole table:
- GPU address = CPU address. NVN memory pools use memory the game allocates itself, and the graphics layer creates zero-copy Metal buffers on that same memory. Any buffer address the game passes in is looked up in the memory pools' interval table and turned into "which
MTLBuffer+ offset" (mtl_addr()). - Record first, replay later. Command buffer calls only write records into the command memory provided by the game (for the format see
nvn/nvn_obj.h); only at submit time doesnvn/nvn_mtl.mreplay them as Metal commands.
1. Objects
| NVN object | Metal implementation | Key points |
|---|---|---|
| Device | MTLDevice (unified memory required) |
The device properties returned by nvnDeviceGetInteger are still given to the game with the Tegra X1's values (g_devinfo in hle_nvn.c) |
| MemoryPool | newBufferWithBytesNoCopy on game memory |
Pages on Apple silicon are 16 KiB and pools are not guaranteed to be aligned to that: the start is rounded down and the end rounded up to page boundaries before the buffer is created, and the buffers of adjacent pools are allowed to overlap (mtl_pool_add(); the approach is verified by metal/probe/p_nocopy.m) |
Buffer address (NVNbufferAddress) |
(MTLBuffer, offset) |
Looked up in the pools' interval table |
| Texture | MTLTexture |
The game's packaged textures are in block-linear layout and are deswizzled at upload; format mapping and deswizzling are in tex/. After the game rewrites pool memory the texture is uploaded again, which is detected by the write-protection dirty page tracking of nvn/nvn_dirty.c. ReadTexels: packaged textures are deswizzled and read directly from pool memory, and textures written by the GPU are read back from the MTLTexture |
| TextureView | newTextureViewWithPixelFormat:textureType:levels:slices: |
— |
| Sampler | MTLSamplerState |
For border color Metal has only three presets (transparent black, opaque black, opaque white); the nearest one to the border color NVN gives is chosen |
| TexturePool / SamplerPool | A host-side table: descriptor number → texture / sampler | The value of a separate texture handle is the descriptor number |
| Program | One MTLFunction per stage |
Keyed by the hash of the shader's control section plus code area and loaded from the offline-translated <key>_b.metallib; nothing is translated at run time. For the translation chain see the README |
| State objects (Blend / Color / ChannelMask / Multisample / Polygon / DepthStencil / VertexAttrib / VertexStream) | Fields of the render pipeline descriptor + MTLDepthStencilState + encoder state |
The game copies these small structures by value, and the host side packs them in place with bit fields. Render pipelines and depth-stencil states are cached by state hash |
| CommandBuffer | A record stream recorded in the game's command memory | Replayed at submit time. The host's record inside the game's NVNcommandBuffer leaves byte +0x10 alone: the game's inlined NVN code writes it directly. When a recording outgrows the game's memory the host lends a segment, released only when the game re-grants the memory the recording started in; GetCommandMemoryUsed counts the bytes in both |
| Queue | Metal command queue | SubmitCommands = replay and submit |
| Sync | The fence value of a submission | FenceSync records the fence value of the most recent submission; SyncWait lets the caller through only after the GPU has actually completed up to that value (the completed value of an MTLSharedEvent). Waiting on a sync object that was never fenced returns a timeout directly |
| Event | A 32-bit value in game memory | The value is written after the GPU completes |
| Window | The game's own swapchain textures ↔ CAMetalLayer |
AcquireTexture returns the index of a swapchain texture, and the wait for vertical sync happens here; QueuePresentTexture only puts the presentation into a queue and returns immediately, and the presentation thread draws that texture onto the drawable using the crop region given by WindowSetCrop. It draws the stored bytes as the Switch's scan-out sends them: an sRGB swapchain texture is sampled through a non-sRGB view (present_raw_src) |
2. Commands
| NVN command | Metal | Notes |
|---|---|---|
| SetRenderTargets | Ends the current render encoder and opens a new render pass | Clears before the first command in the pass are folded into the attachment's load action (load = Clear). A view that changes the format (an RGBA8_SRGB view of an RGBA8 texture) is attached as a Metal view of that format, so writes are encoded as on the Switch |
| ClearColor / ClearDepthStencil | Same as above | Clears in the middle of a pass, and color clears with a partial mask or partial scissor, are not implemented and report red when encountered |
| ClearTexture / ClearTextureui | Clears one texture on its own | Some forms are not implemented and report red when encountered |
| BindProgram and the various state Bind* calls | Recorded into the current state; the pipeline is fetched as needed before the next draw | — |
| BindUniformBuffer(s) / BindStorageBuffer(s) / BindSeparateTextures / BindSeparateSamplers / BindImages | Fills the argument buffer according to the shader's reflection json | For how slot numbers correspond to the constant sections in the shader see "Shader translation chain" in the README |
| BindVertexBuffers | Vertex buffer = (MTLBuffer, offset) |
— |
| SetViewports / SetScissor / SetDepthRange / Stencil* / SetBlendColor / SetPolygonOffsetClamp | The corresponding setting on the encoder | — |
| DrawArrays / DrawElementsInstanced | Draw / indexed draw | Metal has no quad or triangle-fan primitive: non-indexed quads and fans are drawn as triangles through shared index buffers (0,1,2,0,2,3 per quad; 0,i,i+1 per fan triangle) with first as the base vertex; indexed quads / fans and 8-bit indices are expanded on the CPU into a 32-bit (16-bit for 8-bit lists) index list in the frame arena |
| DrawArraysIndirect / DrawElementsIndirect | Indirect draw / indexed indirect draw | NVN's records have the layouts of MTLDrawPrimitivesIndirectArguments / MTLDrawIndexedPrimitivesIndirectArguments and are read by the GPU from the pool. Quads, fans and 8-bit indices: a compute pass before the render pass (ixc_run) converts the GPU-written record into indexed indirect arguments (and, for indexed draws, a 32-bit index list of at most 49,152 indices) |
| DispatchCompute / DispatchComputeIndirect | Compute encoder, with the thread group size taken from the reflection json | The 4 compute shaders embedded in the main program use the hand-written versions in nvn/nvn_builtin.h. Indirect: dispatchThreadgroupsWithIndirectBuffer on the record; the GPU copies its group counts into the driver segment c0 + 0x200 the shader reads |
| CopyBufferToBuffer / UpdateUniformBuffer | Buffer copies inserted in order into the command stream | The data of UpdateUniformBuffer is copied into command memory at record time, because NVN allows the caller to modify the original data after recording |
| CopyBufferToTexture / CopyTextureToBuffer / CopyTextureToTexture | Texture copies | Some forms of texture-to-texture are not implemented and report red when encountered |
| ReportCounter / ResetCounter | The counter's value is written back after the GPU completes | The "samples passed" kind is implemented with Metal's visibility result buffer |
| SignalEvent | The event's value is written, which happens after the GPU completes | — |
| SetTexturePool / SetSamplerPool | Recorded into the command stream, noting the descriptor pool currently in use | — |
The following are empty implementations (g_nops in hle_nvn.c); calling them neither reports red nor has any effect:
SetPatchSize, MultiDrawArraysIndirectCount, MultiDrawElementsIndirectCount, SetDepthBounds, DownsampleTextureView, WaitSync, SetShaderScratchMemory, PushDebugGroupDynamic, PopDebugGroup, InsertDebugMarkerDynamic, WaitEvent, and nvnWindowFinalize.
3. Shader bindings
Switch shaders obtain their resources through a few constant sections maintained by the driver, and during replay the graphics layer fills the same contents into Metal's argument buffer. Each binding in the reflection json carries a kind (NVN_RB_* in nvn/nvn_refl.h):
| Kind | Meaning |
|---|---|
CBUF |
uniform buffer slot r |
TEX / SAMP / SAMPT |
Separate texture slot, separate sampler slot; SAMPT means the sampler is taken from the combined handle of the texture slot |
IMG |
Writable texture (image slot) |
TBUF |
Typed buffer (buffer texture) |
SBUFP |
Device pointer of a storage buffer slot |
UBOP |
Device pointer of a compute-stage uniform slot |
C1 |
The shader's built-in constant section |
DRV0 / DRV2 |
The driver constant sections c[0] (storage buffer address table) and c[2] (texture and sampler handle table), filled in on the spot by the replay layer according to the current bindings |
SUPPORT |
Auxiliary constants used by the translator itself |
A texture slot with nothing bound is given a dummy texture, whose type and pixel class are taken from the reflection.
4. Adding a function that is not implemented yet
- In
nvn/hle_nvn.c, write the implementation with theNVN(name, return type, parameters…)macro. The macro does a signature check againstnvn/nvn_protos.hand registers the implementation in the slot table; if the name is not innvn/names.txt, the program aborts at startup. - If it is a command: add the record format in
nvn/nvn_obj.h, write the recording inhle_nvn.c, and write the replay innvn/nvn_mtl.m. - Add a test in
tests/, thenmake -C dm/gfx/tests run.