
- Title ID
- 0100CD801CE5E000
- Tested build
- 1.1.3
- Nintendo SDK
- 18.3.2
- Engine
- LÖVE 11.4 (love-nx, LuaJIT, SDL)
- Graphics API
- OpenGL
Build this game
Keep [0100CD801CE5E000] in your file names, put the base game, update and DLC in one folder and run the script. Follow the build guide, then the sideloading guide. Needs an iPhone 15 Pro or newer, or an M1 iPad or later.
Technical notes
The project's own record of bringing this game up: what its profile needs and why, what was found and what is still open.
Update 1.1.3 (v851968), SDK 18.3.2. LÖVE 11.4 ("Mysterious Mysteries", love-nx) with LuaJIT 2.1.0-beta3 in its interpreter and SDL: the game is Lua source in the RomFS (main.lua, game.lua, engine/, functions/), its pictures PNG, its sound Ogg Vorbis through OpenAL Soft on the audio renderer. It renders with OpenGL, not NVN: NVIDIA's desktop GL 4.5 through EGL, which the host implements on Metal (dm/gfx/gl/hle_gl.m) on the core profile's paths. Modules main and sdk; sdk is the same build as Absolum's (0100A83022A22000). subsdk0 (NVIDIA's OpenGL driver) and subsdk1 (NVIDIA's Vulkan driver) are never loaded. Plays on the iPhone (IPA about 82 MB), by touch in handheld mode.
On the Mac: the intro, the title, a new run by touch and the tutorial's first steps, at 60 fps with sound; about 0.65 GB resident and 0.2 GB of Metal allocations, flat over three minutes in a run.
What it needs
| Setting | Where | Why |
|---|---|---|
op_mode handheld |
game.tsv |
the Switch build plays by touch there (SDL turns the touch screen into LÖVE touches and mouse presses: tap a card, drag it): on the iPhone the touches outside the gamepad controls, on the Mac the mouse. Docked it reads only the pads. Its window is then the 1280x720 handheld display |
pad_layout touch |
game.tsv |
the virtual gamepad starts in its Tap layout (only the layout button; every touch goes to the game), since the 2D layout's corners cover Run Info, Options and the deck; the layout button brings back 2D and 3D |
gl_capture.tsv |
profile | the 35 shaders LÖVE puts together at run time, which no game file holds; the NSP script captures them on the Mac; see Shaders |
modules main sdk |
game.tsv |
subsdk0 / subsdk1 are NVIDIA's OpenGL and Vulkan drivers, which program the GPU through NvRm: never loaded, the GL layer answers main's EGL imports |
sys_resource 0x0; mem_reserve 0xf0000000, mem_alias 0x10000000, mem_stack 0x10000000, app_pool 0xCD500000 |
game.tsv |
no system resource, so no virtual address memory: nninitStartup gives its malloc a heap of the pool − used memory, which it maps only as it touches it; main's 264 MB bss counts as used memory |
| Node area cut and 4 MB / 32 MB / 1 GB → 2 MB region alignment (SDK 20's) | dm/tools/sdk_patches.py SDK-wide rules, sdk build f118ae1c… |
Absolum's sdk build; the SDK-wide rules give its cuts |
| No NVN shader tables | game.tsv shader_note |
no NVN shaders: gl_shaders.py writes the GL library and its table, not the profile |
Rendering (OpenGL)
LÖVE's graphics module is its own OpenGL backend, so its choices follow what the context reports.
- Telling the API apart:
mainimports the EGL functions andvkGetInstanceProcAddr, SDL's Vulkan loader, which nothing calls. Resolved: the NSP script's provisional profile said Vulkan, and the capture boot crashed in the GL layer;profile_guess.pynow leavesvkGetInstanceProcAddrout of its Vulkan count (Hades and both Galaxies import 99 or 100 Vulkan functions,vkCreateInstanceamong them). - Which GL functions: LÖVE's glad loader asks for 2,133 GL names and stores each pointer straight into a global of its own (no GOT slot per name). The code reaches those globals through GOT entries that hold their addresses, which it often keeps in callee-saved registers across a loop, or picks between with
csel(glEnableVertexAttribArrayorglDisableVertexAttribArray).loader_census.pyfollows registers in a straight line for that (adrp, add, loads through relocated GOT entries,mov,csel): about 150 called. - Resolved: a jump to address 0 right after
glGetFloatv(0xb12)(lr=main+0x2d58c4): a GL function the layer did not have (hereglVertexAttrib4fv, LÖVE's default colours).DM_GL_STUBS=1gives every name the layer lacks a stub of its own that reports its first call instead of a null pointer (a jump to address 0 names nothing);loader_census.pylists what the code calls before the first boot. - Core profile: LÖVE reads
GL_CONTEXT_PROFILE_MASK, which the layer now answers as core. On a core profile it streams vertices through persistently mapped buffers (glBufferStorage+glMapBufferRangeof a 3-frame ring, aglFenceSyncper frame andglClientWaitSyncthree frames later); on a compatibility one it would draw from client memory, which Metal cannot draw from. In the layer the mapping is the sharedMTLBuffer's memory, a fence is the serial of the command buffer being recorded, and a wait on a fence of that command buffer commits it first. The shaders then come as#version 330 core. - Attributes: LÖVE binds VertexPosition, VertexTexCoord, VertexColor and ConstantColor to 0–3 with
glBindAttribLocationbefore each link, keeps VertexColor and ConstantColor as disabled arrays and sets their values withglVertexAttrib4f(the draw colour); the layer maps each Metal input to the game's index at the link. - Uniforms: LÖVE lists them with
glGetActiveUniform(types as GL enums, arrays asname[0]) and reads their initial values back withglGetUniformfv/iv; everyglUniform*form. - Textures: made with
glTexStorage2D(every level up front: the game's images are mipmapped, filled byglGenerateMipmapas a blit) orglTexStorage3D: LÖVE makes a 1x1 default texture of each type, 3D and 2D array among them, and the layer's units hold all four targets (96 of them, asGL_MAX_COMBINED_TEXTURE_IMAGE_UNITSsays). Font glyphs are RG8 with aGL_TEXTURE_SWIZZLE_*of R, R, R, G, applied when the texture is made (a swizzled texture cannot be a render target). - Draws and framebuffers: batched quads share one 16-bit index buffer through
glDrawElementsBaseVertex; instanced draws; canvases bind GL_FRAMEBUFFER (read and draw), check completeness, discard depth and stencil after use (glInvalidateFramebuffer) and clear withglClearBufferfv. Separate read and draw framebuffers,glBlitFramebuffer(colour, same size) andglReadPixels(RGBA8) are there for multisample resolves andCanvas:newImageData, which the game does not use.
Shaders
- Current: LÖVE puts each shader's GLSL together at run time, in Lua, from its own header and built-in code and the game's 19
resources/shaders/*.fs: 35 sources (#version 330 core), all made byGame:start_upbefore the first frame. No game file holds them, so the NSP script captures them on the Mac (DM_GL_CAPTUREat<id>_build/extract/<version>/gl_captureuntil every file ingl_capture.tsvis there) andgl_shaders.pytranslates them; the host finds each by the hash of its text. - Resolved: a GLSL
booluniform is auintin SPIR-V;gl_shaders.pynow reports the declared type, since LÖVE'sShader:sendtakes only a boolean for a bool (the game stopped at its first card otherwise). - A capture run has no shader library: every program then links with no uniforms, and the game's first
Shader:sendraises a Lua error, which quits (below), after every shader is captured.
Audio
- Resolved: silent. The game opens the audio renderer without an event (
OpenAudioRenderer(handle, parameter, work, size), from LÖVE's OpenAL Soft) and paces its own updates; the renderer must still run every 5 ms. The host started its render thread only for the form with an event, so only the game's own updates rendered (catch-up frames that nothing played). Both forms now go through one open that starts the thread (hle_audren.car_open). The log showedaudren: OpenAudioRenderer (no event)and norender thread started.
When the game stops
- On the Switch, Balatro's
love.errhandreturns at once, so any Lua error quits. love-nx's video worker is then deleted twice (love::video::theora::Video→~Worker: its thread releases the worker that the closing video module also deletes), which crashes inmain+0x339c00a second or two into the title. A run that reaches this has hit a Lua error first: look for it. - How it was found: a
printat the top oflove.errhandinmain.lua, in a copy of the RomFS (DM_ROMFS=<copy>), gave the errors:Shader uniform 'distortion_fac' does not exist(the capture run had no shader library, so every program linked with no uniforms), thenbad argument #2 to 'send' (number expected, got boolean)(abooluniform reported asGL_UNSIGNED_INT). Probing the destructors (DM_PROBE) and logging every object the Lua GC released (DM_PROBE=main:0x18b920@x0+0: the module proxies all at once) showed it was a shutdown, not a premature collection.
Rewriter
- The spin-wait search through stores found 12 loops in
maininstead of 9; one is a parser state that leaves on the next pass (main+0x1f4600) (development.md, Power).
Mac routes
- In handheld mode,
DM_KEYS="f1300:T0.35x0.86,f1700:T0.35x0.86,f2100:T0.5x0.68"taps Play at the title, Play in the run setup and the tutorial's Next.
Found on this game
General host changes first needed here; each applies to every game:
- Resolved: Homebrew .NET runtime discovery for the first extraction and shader tools: Homebrew .NET is on PATH but apphosts cannot find the runtime.
- The GL layer's core-profile paths (
GL_CONTEXT_PROFILE_MASK, persistently mapped buffers and fences, attribute bindings and current values, uniform queries, texture storage, 3D and array textures, swizzles, mipmaps, base-vertex and instanced draws, blits) inhle_gl.m, andDM_GL_STUBS=1: The GL core profile and persistently mapped buffers, GL uniforms, pipelines and attributes, GL textures, swizzles and uploads, GL draws and framebuffers, Which GL functions a game calls. loader_census.pyfollowing registers (LÖVE's glad), andvkGetInstanceProcAddrleft out ofprofile_guess.py's Vulkan count: same section.- GL shaders captured at run time (
DM_GL_CAPTURE,gl_capture.tsv, the NSP script'scapture_shaders): development.md, The packing script in detail. - A renderer opened without an event starts the render thread (
ar_open): A renderer opened without an event. - The Tap pad layout (
pad_layout touch,DmGamepad.m): dm/games/README.md.
Open
- The player-facing README says the games outside its 2D list start in the 3D layout; Balatro starts in Tap (its gallery entry says "played by touch").
These notes are republished from the nxnative repository under its MIT license (license and attribution). The original, which may be newer, is the game's notes on GitHub. This site hosts no game files.