Add an NVN API function¶
Goal¶
Implement an NVN entry point that a game calls and libnapple_gfx does not yet answer. Today such a call logs
[napple-gfx] warn: unimplemented NVN entry point called: <name> once, returns 0 and goes on (or aborts when settings.unimplemented_abort is set), and the
count appears in the unimplemented calls field of the [napple-gfx] stats: line and in the diag: lines printed at exit.
Background: gfx.md (object model, interface, threading) and subsystems.md.
How an NVN function is found and called¶
- The guest never runs the SDK's NVN driver. It imports
nvnBootstrapLoader, which the runtime overrides with the host functionhle_bootstrapfromnapple_gfx_hle_table()(gfx/src/core.c; the adaptergfx/adapter/gfx_napple.c: gfx_hle_tableregisters it as a host symbol). The bootstrap loader andnvnDeviceGetProcAddress(gfx/src/api_device.c) both end innvn_lookup(name). nvn_lookup(core.c) hashes the name againstnvn_names[], which is generated at build time fromgfx/src/nvnapi/names.txtbygfx/tools/gen_nvn_tables.sh(one line per API name; the same script writes one assembly stub per name,nvn_unimpl_thunk[], that callsnvn_unimpl_reportand returns 0).- Implementations register themselves: the
NVN(name, ret, args...)macro ingfx/src/internal.hdefines a static functionh_<name>and puts a{name, fn}record in the Mach-O section__DATA,napple_nvn. At first lookupnames_initreads that section and fillsg_impl[]. A name with an implementation returns it; otherwise the lookup returns the unimplemented stub. The result is wrapped for the guest bygfx_wrap_callable. - The
NVNmacro also checks the signature at compile time:static const NVNT_<name> chk_<name> = h_<name>;needs a typedefNVNT_<name>fromgfx/src/nvnapi/nvn_protos.h. For entry points newer than the bundled header the typedefs are hand-maintained ingfx/src/nvnapi/nvn_protos_ext.h(included at the end ofnvn_protos.h).NVN_NOPROTOexists ininternal.hfor an entry without any prototype (no function uses it today).NVNbooleanparameters areuint32_tin the typedefs.
Files to touch¶
| Path | What |
|---|---|
gfx/src/nvnapi/names.txt |
The name must be in this list, or names_init logs NVN implementation <name> is not in the name table and ignores it. (523 names today; g_impl[] and the counters in core.c hold 1024.) |
gfx/src/nvnapi/nvn_protos.h / nvn_protos_ext.h |
The NVNT_<name> function-pointer typedef. Add to nvn_protos_ext.h by hand. |
gfx/src/nvnapi/nvn.h |
Types and enums of the NVN API (read-only reference for argument types). |
gfx/src/api_device.c |
Device, queue, sync, events, nvnDeviceGetProcAddress, nvnDeviceGetInteger. |
gfx/src/api_memory.c |
Memory pools, buffers. |
gfx/src/api_texture.c |
Textures, samplers, pools, builders. |
gfx/src/api_program.c |
Programs and shaders (nvnProgramSetShaders). |
gfx/src/api_state.c |
State objects (blend, color, depth/stencil, polygon, multisample, vertex): setters and getters on guest-sized structs. |
gfx/src/api_cmd.c |
Command buffers: recording of every nvnCommandBuffer* call, recordings, handles. |
gfx/src/api_window.c |
Windows, presentation. |
gfx/src/obj.h |
Opcodes OP_*, payload structs p_*, the state structs (blend_st...) with FITS(T, NVNxxx) size assertions, cmdrec. |
gfx/src/mtl_replay.m |
Replay of recorded commands onto Metal: exec_op and the do_* helpers. |
gfx/tests/ |
t_*.c / t_*.m programs and the shared helper nvn_test.h. |
Steps¶
- Decide the kind of function.
- Object getter or setter (state, builder, sampler): operates on the guest's struct and records nothing. Put it in the matching
api_*.c, modelled onapi_state.c: nvnBlendStateSetBlendTarget/nvnBlendStateGetBlendTarget(ST(type, ptr)/CST(type, ptr)cast the guest object to the packed struct fromobj.h). If you add fields, keepFITS(struct, NVNxxx)true: the guest reserved a fixed size. - Immediate call (device, queue, memory pool, texture builder, sync): does its work on the calling thread; Metal objects are created behind it (
api_device.c,api_memory.c,api_texture.c). - Command-buffer call (
nvnCommandBuffer...): records, never touches Metal. The work happens later in the replay thread. - Accepted and ignored: write
{ (void)c; ... }and log once withLOG_ONCE(NAPPLE_LOG_WARN, ...)if a game could depend on it (api_cmd.c: nvnCommandBufferSetViewportSwizzlesis such a no-op). - Add the name to
names.txtand, if it has no typedef yet, the typedef tonvn_protos_ext.h(pattern:typedef void (*NVNT_nvnCommandBufferSetColorReductionEnable)(NVNcommandBuffer *c, uint32_t e);). - Write the handler with the
NVN(...)macro. Mask booleans withB(x)(internal.h): Switch callers do not zero-extendNVNboolean, only the low 8 bits are valid. - For a recorded command:
- Add an opcode before
OP_COUNTin the enum inobj.hand, if it carries data, ap_<name>payload struct. Payloads hold raw values, addresses and handles only, never pointers into guest-owned objects (recordings outlive their command buffer). - Record it in
api_cmd.c:REC(c, p_type, OP_X)for a fixed payload,REC_N(c, p_type, OP_X, extra_bytes)for a payload followed by an array,rec_op(c, OP_X, n)for raw bytes.rec_opis static inapi_cmd.c, so recorded calls live there. OutsideBeginRecording/EndRecording, or on a null/finalized buffer,rec_opreturns a scratch area and warns once, so a stray call is harmless. - Replay it: add a
case OP_X:to theswitch (h->op)inexec_op(mtl_replay.m). State setters update the replay stateS(viewports inS.vp, scissorsS.sc, blendS.blend[...]...) and setg_pk_dirty = 1when the pipeline key changes; draws, clears and copies call ado_*function (do_draw_arrays,do_clear_color,do_copy_b2b). Anything that ends a render pass callspass_end()first. - Test it (below), then update the graphics notes if the change alters behaviour games can see.
Worked example¶
A recorded command with an array payload, modelled on SetViewports (record) and OP_VIEWPORTS (replay):
/* gfx/src/api_cmd.c */
NVN(nvnCommandBufferSetViewports, void, NVNcommandBuffer *c, int first, int n, const float *v) {
if (n < 0) n = 0; if (n > 16) n = 16;
p_range *p = REC_N(c, p_range, OP_VIEWPORTS, (size_t)n * 16); /* p_range { u32 first, n } then n x 4 floats */
p->first = (uint32_t)first; p->n = (uint32_t)n;
memcpy(p + 1, v, (size_t)n * 16);
}
/* gfx/src/mtl_replay.m, in exec_op() */
case OP_VIEWPORTS: { const p_range *r = (const p_range *)pl; const float *f = (const float *)(r + 1);
for (uint32_t i = 0; i < r->n && r->first + i < 16; i++) memcpy(S.vp[r->first + i], f + 4 * i, 16); break; }
api_cmd.c: nvnCommandBufferSetPrimitiveRestart writes {enable, index} into rec_op(c, OP_PRIM_RESTART, 8); replay case OP_PRIM_RESTART sets
S.prim_restart_en and S.prim_restart_idx) and a draw (nvnCommandBufferDrawArrays fills a p_draw_arrays; replay calls do_draw_arrays) are next to them. A
state object round trip is api_state.c: nvnBlendStateSetBlendTarget plus nvnBlendStateGetBlendTarget, bound by api_cmd.c: nvnCommandBufferBindBlendState (copies the blend_st into an OP_BIND_BLEND record).
nvnCommandBufferSetViewports is already in names.txt; a new function needs its own line there.
How to test¶
- Linux, no Metal:
sh gfx/tests/t_getters_linux.shcompilestests/t_getters_linux.c, which#includessrc/api_state.candsrc/api_texture.cdirectly and calls the statich_nvn*handlers (set then get round trips; the script definesNVN_IMPL_SECTIONso an ELF host accepts the section and leaves unresolved symbols alone). Measured on a Linux box:33 checks, 0 failed. This is the right test for any new state/builder getter or setter. Only those two API files are built this way; the repository has no Linux build of the otherapi_*.cfiles (a recorded command cannot be replayed without Metal). - macOS:
make -C gfx testbuildslibnapple_gfx.aand everytests/t_*.c/t_*.m(the Makefile takes them by wildcard) and runs them withMTL_DEBUG_LAYER=1;make -C gfx testsonly builds them;make -C gfx lib adapterbuilds the library and the runtime adapter. A new test is just a new filetests/t_<name>.c. - Shape of a Metal test (
tests/t_nvnapi.c,t_state.c):napple_gfx_config_default(&cfg); cfg.headless_width = 32; cfg.headless_height = 32; napple_gfx_init(&cfg);, then call entry points throughL(nvnName)(...)(the macro intests/nvn_test.hlooks them up withnapple_gfx_nvn_bootstrap_loader, so a missing name or a missingnames.txtline shows up as a NULL call), record into a command buffer,nvnQueueSubmitCommands,nvnQueueFinish, copy a texture to a buffer in a pool and compare pixels.CHECK(cond, ...)counts failures.nvn_test.halso has a tiny Maxwell assembler (ins_*) andbuild_shaderso no game data is needed. - Prove the call is reached by name: in a test,
L(nvnYourFunction)must be non-NULL and not the unimplemented stub; the init log printsNVN: <n> of <total> entry points implemented, and after a runnapple_gfx_diag_unimplemented()(diag:lines at exit) must not list your name. - Games: run a title that calls it. Useful env (all exist in
gfx/src, full list in environment-variables.md):NAPPLE_NVNTRACE=1prints the op number of each recorded command ([nvntrace] cb <ptr> op <n>),NAPPLE_GFX_CMDTRACE=1logs recording lifetimes,NAPPLE_GFX_LOG=<level>sets the gfx log level,NAPPLE_DRAWLOG*andNAPPLE_SCREENSHOTshow what was drawn (debugging.md).
Common pitfalls¶
- Not in
names.txt. The handler compiles, the section entry exists, and the lookup still never returns it (or logsnot in the name table). Add the line. - Missing or wrong typedef. The compile fails with an incompatible pointer on
chk_<name>: that is the signature check doing its job; fix the typedef innvn_protos_ext.h, not the cast. - Unextended booleans (
B(x)): a game passing a dirty upper half turned conditional state on or off at random. - Command handles outlive command buffers. A game can
EndRecording, finalize the buffer, hand its memory to another one and keep calling the handle (Super Mario Odyssey, Mario Kart 8 Deluxe). Recordings are host copies kept in a graveyard under a byte budget (NAPPLE_REC_BUDGET_MB, default 1024); releasing them by idle time caused exploded geometry (symptoms.md, Rendering). Never store a pointer to guest memory in a payload that replay reads later without checking it. - Command buffers are touched by several threads. The recording lists are under one lock (
g_recs_mu); null and finalized buffers must be tolerated (cbh_get/CBH), as a crash seen one run in three in Animal Crossing showed (symptoms.md, Boot, crashes). - Guest garbage. Games pass odd values once (a pool-relative shader address
0xe00innvnProgramSetShaders). Validate and reject instead of dereferencing. - Replay state is per-
exec_op. SettingS.*withoutg_pk_dirty = 1when the value feeds the pipeline key leaves a stale pipeline; ending the pass is needed before copies that cannot happen inside a render pass. - Window origin and depth mode change coordinates. Viewport, scissor and fragment coordinate follow
nvnDeviceSetWindowOriginMode; do not negate Y in the vertex shader (gfx.md, section 10). - Memory. Writing into guest memory from the host needs
guest_write_prepare(gfx dirty tracking write-protects pages that hold textures); a plainmemcpyinto such a page faults or hitsEFAULT. - Version check. A game compares
nvnDeviceGetInteger(API_MAJOR/MINOR)with its build constant; it comes fromgame.json(nvn_major/nvn_minor) or is detected inmain.c: detect_nvn_version. A mismatch silently skips graphics start-up.