From c392d56af005b1aa4e054c576b7cb6081c49fbf2 Mon Sep 17 00:00:00 2001 From: seedds Date: Sat, 24 Oct 2026 01:49:33 +0800 Subject: [PATCH] =?UTF-8?q?Balatro=20(0100CD801CE5E000,=20update=201.1.3,?= =?UTF-8?q?=20L=C3=96VE=2011.4=20"love-nx"=20with=20LuaJIT=20and=20SDL=20o?= =?UTF-8?q?n=20SDK=2018.3.2):=20game=20profile=20and=20the=20GL=20layer=20?= =?UTF-8?q?L=C3=96VE=20needs,=20plus=20four=20general=20changes.=20Plays?= =?UTF-8?q?=20on=20the=20iPhone=20by=20touch;=20on=20the=20Mac=20from=20th?= =?UTF-8?q?e=20intro=20and=20the=20title=20into=20a=20run=20and=20its=20tu?= =?UTF-8?q?torial,=20at=2060=20fps=20with=20sound,=20about=200.65=20GB=20r?= =?UTF-8?q?esident=20and=20flat=20over=20three=20minutes.=20The=20game=20i?= =?UTF-8?q?s=20Lua=20source=20in=20the=20RomFS=20and=20renders=20with=20NV?= =?UTF-8?q?IDIA's=20desktop=20OpenGL=20through=20EGL,=20as=20Digimon=20doe?= =?UTF-8?q?s.=20GL=20layer=20(dm/gfx/gl/hle=5Fgl.m):=20L=C3=96VE=20reads?= =?UTF-8?q?=20GL=5FCONTEXT=5FPROFILE=5FMASK=20and=20takes=20its=20core-pro?= =?UTF-8?q?file=20paths=20only=20when=20it=20says=20core=20(with=20a=20com?= =?UTF-8?q?patibility=20profile=20it=20streams=20vertices=20from=20client?= =?UTF-8?q?=20memory,=20which=20Metal=20cannot=20draw=20from),=20so=20the?= =?UTF-8?q?=20layer=20answers=20core=20and=20implements=20what=20those=20p?= =?UTF-8?q?aths=20use,=20about=20150=20functions=20in=20all=20against=20Di?= =?UTF-8?q?gimon's=2079:=20persistently=20mapped=20buffers=20(glBufferStor?= =?UTF-8?q?age,=20glMapBufferRange:=20the=20shared=20MTLBuffer's=20own=20m?= =?UTF-8?q?emory),=20glBufferSubData=20in=20command=20order,=20fences=20(a?= =?UTF-8?q?=20fence=20is=20the=20serial=20of=20the=20command=20buffer=20be?= =?UTF-8?q?ing=20recorded;=20a=20wait=20on=20one=20still=20recording=20com?= =?UTF-8?q?mits=20it),=20glBindAttribLocation=20(the=20game's=20attribute?= =?UTF-8?q?=20index=20of=20each=20Metal=20input,=20set=20at=20the=20link)?= =?UTF-8?q?=20and=20current=20attribute=20values=20(glVertexAttrib4f:=20L?= =?UTF-8?q?=C3=96VE's=20VertexColor=20and=20ConstantColor=20are=20disabled?= =?UTF-8?q?=20arrays),=20glGetActiveUniform=20and=20the=20uniform=20read-b?= =?UTF-8?q?acks,=20every=20glUniform*=20form,=20glTexStorage2D=20/=203D=20?= =?UTF-8?q?with=20their=20level=20counts,=203D=20and=202D=20array=20textur?= =?UTF-8?q?es=20(L=C3=96VE=20makes=20a=20default=20of=20each=20type=20on?= =?UTF-8?q?=2096=20units),=20GL=5FTEXTURE=5FSWIZZLE=5F*=20(its=20font=20gl?= =?UTF-8?q?yphs=20are=20RG8=20read=20as=20R,=20R,=20R,=20G),=20glGenerateM?= =?UTF-8?q?ipmap=20as=20a=20blit=20(the=20game's=20images=20are=20mipmappe?= =?UTF-8?q?d),=20instanced=20and=20base-vertex=20draws,=20separate=20read?= =?UTF-8?q?=20and=20draw=20framebuffers,=20glBlitFramebuffer=20(colour,=20?= =?UTF-8?q?same=20size),=20glReadPixels,=20glClearBufferfv,=20glFrontFace?= =?UTF-8?q?=20and=20the=20EGL=20calls=20SDL=20makes;=20stack=20arguments?= =?UTF-8?q?=20of=20the=20functions=20with=20more=20than=20eight=20are=2064?= =?UTF-8?q?-bit,=20as=20AAPCS64=20lays=20them=20out.=20DM=5FGL=5FSTUBS=3D1?= =?UTF-8?q?=20gives=20every=20GL=20name=20the=20layer=20lacks=20a=20stub?= =?UTF-8?q?=20of=20its=20own=20that=20reports=20its=20first=20call,=20inst?= =?UTF-8?q?ead=20of=20a=20null=20pointer.=20Shaders:=20L=C3=96VE=20puts=20?= =?UTF-8?q?its=2035=20GLSL=20sources=20together=20at=20run=20time=20in=20L?= =?UTF-8?q?ua,=20so=20the=20NSP=20script=20captures=20them=20on=20the=20Ma?= =?UTF-8?q?c=20as=20it=20does=20Super=20Mario=20Galaxy's=20SPIR-V=20(captu?= =?UTF-8?q?re=5Fshaders=20now=20serves=20both,=20profile=20gl=5Fcapture.ts?= =?UTF-8?q?v;=20profile=5Fguess.py's=20gl=5Fcapture=20rule:=20an=20OpenGL?= =?UTF-8?q?=20game=20with=20no=20GLSL=20source=20in=20its=20RomFS=20or=20m?= =?UTF-8?q?ain),=20and=20gl=5Fshaders.py=20keeps=20a=20bool=20uniform's=20?= =?UTF-8?q?declared=20type=20(a=20uint=20in=20SPIR-V;=20L=C3=96VE's=20Shad?= =?UTF-8?q?er:send=20takes=20only=20a=20boolean=20for=20a=20bool)=20and=20?= =?UTF-8?q?takes=20from=20main=20only=20strings=20that=20start=20with=20'#?= =?UTF-8?q?version=20'=20(Balatro's=20main=20has=20glslang's=20mes?= =?UTF-8?q?sages).=20General:=20profile=5Fguess.py=20leaves=20vkGetInstanc?= =?UTF-8?q?eProcAddr,=20SDL's=20Vulkan=20loader,=20out=20of=20its=20Vulkan?= =?UTF-8?q?=20count=20(Balatro's=20provisional=20profile=20said=20Vulkan;?= =?UTF-8?q?=20Hades=20and=20both=20Galaxies=20import=2099=20or=20100=20Vul?= =?UTF-8?q?kan=20functions);=20OpenAudioRenderer=20without=20an=20event=20?= =?UTF-8?q?now=20starts=20the=20host's=20render=20thread=20too=20(hle=5Fau?= =?UTF-8?q?dren.c=20ar=5Fopen:=20only=20the=20form=20with=20an=20event=20d?= =?UTF-8?q?id,=20and=20such=20a=20game=20was=20silent);=20loader=5Fcensus.?= =?UTF-8?q?py=20follows=20registers=20in=20a=20straight=20line=20(adrp,=20?= =?UTF-8?q?add,=20loads=20through=20relocated=20GOT=20entries,=20mov,=20cs?= =?UTF-8?q?el),=20for=20L=C3=96VE's=20glad,=20whose=20pointers=20are=20glo?= =?UTF-8?q?bals=20its=20code=20keeps=20the=20addresses=20of=20in=20registe?= =?UTF-8?q?rs;=20and=20the=20iPhone=20gamepad=20has=20a=20third=20layout,?= =?UTF-8?q?=20Tap=20(profile=20pad=5Flayout=20touch,=20which=20needs=20op?= =?UTF-8?q?=5Fmode=20handheld;=20DM=5FGAME=5FPAD=5FLAYOUT=20replaces=20DM?= =?UTF-8?q?=5FGAME=5FPAD=5FFULL):=20no=20control=20but=20the=20layout=20bu?= =?UTF-8?q?tton,=20every=20touch=20to=20the=20game,=20offered=20in=20the?= =?UTF-8?q?=20layout=20cycle=20only=20in=20handheld=20mode.=20Profile:=20o?= =?UTF-8?q?p=5Fmode=20handheld=20(the=20Switch=20build=20plays=20by=20touc?= =?UTF-8?q?h=20there),=20pad=5Flayout=20touch,=20gl=5Fcapture.tsv.=20A=20L?= =?UTF-8?q?ua=20error=20makes=20Balatro=20quit=20(its=20Switch=20error=20h?= =?UTF-8?q?andler),=20and=20love-nx's=20video=20worker=20is=20then=20delet?= =?UTF-8?q?ed=20twice=20(main+0x339c00):=20the=20bring-up=20notes=20say=20?= =?UTF-8?q?how=20the=20errors=20were=20found.=20Checked:=20the=20NSP=20scr?= =?UTF-8?q?ipt=20packs=20it=20from=20the=20NSPs=20alone=20(the=20capture?= =?UTF-8?q?=20in=20under=20a=20second,=2035=20shaders=20for=20the=20Mac=20?= =?UTF-8?q?and=20iOS,=20an=2082=20MB=20IPA),=20tool=20tests=20pass,=20prof?= =?UTF-8?q?ile=5Fguess.py=20--check=20changes=20no=20other=20profile;=20Di?= =?UTF-8?q?gimon=20was=20not=20rerun=20(its=20files=20are=20not=20on=20han?= =?UTF-8?q?d).=20README=20(games=20table,=20a=20Mac=20picture=20of=20the?= =?UTF-8?q?=20title),=20AGENTS.md=20(47=20=E2=86=92=2048),=20docs/developm?= =?UTF-8?q?ent.md,=20docs/agents=20and=20the=20READMEs=20of=20dm/games=20a?= =?UTF-8?q?nd=20dm/gfx=20say=20so?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- README.md | 2 +- dm/games/0100CD801CE5E000/bf_want.tsv | 1 + dm/games/0100CD801CE5E000/game.tsv | 48 ++ dm/games/0100CD801CE5E000/gl_capture.tsv | 38 + dm/games/0100CD801CE5E000/pso_manifest.tsv | 1 + dm/games/0100CD801CE5E000/shader_facts.tsv | 1 + dm/games/0100CD801CE5E000/shader_map.tsv | 2 + .../shader_map.tsv.conflicts.tsv | 2 + dm/games/0100CD801CE5E000/shader_pairs.tsv | 1 + dm/games/README.md | 5 +- dm/games/game.py | 7 +- dm/games/profile_guess.py | 23 +- dm/gfx/README.md | 2 +- dm/gfx/gl/hle_gl.m | 687 +++++++++++++++--- dm/gfx/tools/gl_shaders.py | 15 +- dm/host/hle_audren.c | 28 +- dm/ios/Sources/DmGamepad.m | 30 +- dm/ios/tests/test_xcode_workflow.py | 2 +- dm/tools/loader_census.py | 105 ++- docs/agents/02-survey.md | 2 +- docs/agents/03-bringup.md | 6 + docs/agents/04-graphics.md | 15 +- docs/development.md | 15 +- docs/images/balatro.jpg | Bin 0 -> 127912 bytes scripts/build_ipa_from_nsp.py | 70 +- 26 files changed, 900 insertions(+), 210 deletions(-) create mode 100644 dm/games/0100CD801CE5E000/bf_want.tsv create mode 100644 dm/games/0100CD801CE5E000/game.tsv create mode 100644 dm/games/0100CD801CE5E000/gl_capture.tsv create mode 100644 dm/games/0100CD801CE5E000/pso_manifest.tsv create mode 100644 dm/games/0100CD801CE5E000/shader_facts.tsv create mode 100644 dm/games/0100CD801CE5E000/shader_map.tsv create mode 100644 dm/games/0100CD801CE5E000/shader_map.tsv.conflicts.tsv create mode 100644 dm/games/0100CD801CE5E000/shader_pairs.tsv create mode 100644 docs/images/balatro.jpg diff --git a/AGENTS.md b/AGENTS.md index 97c58be..29ba6ea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Notes for agents -- This repository runs Nintendo Switch games natively on Apple platforms: the game's own code runs, rewritten only where Apple's kernel requires it. Forty-seven games are playable on the iPhone: Super Mario Bros. Wonder (update 1.2.1, profile `010015100B514000`), Fire Emblem Engage (update 2.0.0, `0100A6301214E000`), Metroid Dread (update 2.1.0, `010093801237C000`), 13 Sentinels: Aegis Rim (update 1.10, `01003FC01670C000`), Super Mario 3D World + Bowser's Fury (update 1.2.2, `010028600EBDA000`), The Legend of Zelda: Breath of the Wild (update 1.9.0, SDK 20, `01007EF00011E000`), The Legend of Zelda: Link's Awakening (update 1.1.0, SDK 8, `01006BB00C6F0000`) Super Mario Odyssey (update 1.4.1, SDK 7, `0100000000010000`), Hades (update 1.0.38233, SDK 9, `0100535012974000`), Hyrule Warriors: Age of Calamity (update 1.3.0, SDK 9.6, `01002B00111A2000`), Fire Emblem: Three Houses (update 1.2.0, SDK 6.5, `010055D009F78000`), Super Mario Galaxy (update 1.3.1, SDK 18, `010099C022B96000`) The Legend of Zelda: Tears of the Kingdom (update 1.4.3, SDK 20, `0100F2C0115B6000`) Monster Hunter Rise (update 16.0.2, SDK 11.4.3, `0100B04011742000`), Ace Attorney Investigations Collection (update 1.0.0.2, SDK 17.5.3, `010005501E68C000`), Hades II (base game, SDK 19.3.9, `0100A00019DE0000`), Unicorn Overlord (update 1.05, SDK 16.2.4, `010069401ADB8000`), Rhythm Heaven Groove (base game 1.0.0, SDK 21.4.0, `0100D9F01D474000`), Octopath Traveler 0 (update 1.0.5, SDK 19.3.13, `01005270232F2000`), Pokémon Legends: Z-A (update 2.0.2, SDK 19.3.11, `0100F43008C44000`), Cuphead (update 1.3.7, SDK 5.5.0, `0100A5C00D162000`), Super Mario Galaxy 2 (update 1.4.0, SDK 18.3.5, `0100FD8022DAA000`), FINAL FANTASY TACTICS - The Ivalice Chronicles (update 1.4.0, SDK 19.3.9, `010038B015560000`), Absolum (update 1.1.1, SDK 18.3.2, `0100A83022A22000`), Captain Toad: Treasure Tracker (update 1.4.2, SDK 8.2.0, `01009BF0072D4000`) and Brain Memory Training Academy (base game 1.0.0, SDK 16.2.4, `0100F5D01DA3A000`), The Great Ace Attorney Chronicles (base game 1.0.0, SDK 10.4.1, `010036E00FB20000`), STEINS;GATE RE:BOOT (update 1.0.3, SDK 20.5.17, `01008BC024FDE000`), Big Brain Academy: Brain vs. Brain (update 1.2.0, SDK 11.4.3, `0100620012D6E000`), Patrick's Parabox (update 1.0.1, SDK 13.3.7, `0100E44016C22000`), Celeste (update 1.4.1.1, SDK 15.3.2, `01002B30028F6000`), DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition (update 1.0.4, SDK 7.3.2, `01006C300E9F0000`), Monster Train 2 (update 1.1.1, SDK 18.3.5, `010051701FB46000`), Raging Loop (base game 1.0.0, SDK 7.3.2, `010020400E1C2000`), DARK SOULS: REMASTERED (update 1.0.3, SDK 4.5.0, `01004AB00A260000`), Paper Mario: The Thousand-Year Door (update 1.0.1, SDK 15.3.2, `0100ECD018EBE000`), Hollow Knight: Silksong (update 1.0.30000, SDK 20.5.8, `010013C00E930000`), Catherine: Full Body (update 1.0.1a, SDK 8.2.1, `0100BF00112C0000`), Digimon Story Cyber Sleuth: Complete Edition (update 1.0.3, SDK 6.4.0, `010014E00DB56000`), Animal Crossing: New Horizons (update 3.0.3, SDK 20.5.4, `01006F8002326000`), DRAGON QUEST VII Reimagined (update 1.2.0, SDK 19.3.1, `0100A9D01C446000`), FINAL FANTASY RESONANCE (update 1.0.1, SDK 20.5.17, `0100606026F12000`), Luigi's Mansion 3 (update 1.4.0, SDK 7.3.2, `0100DCA0064A6000`), AI: THE SOMNIUM FILES (update 1.0.1, SDK 6.4.2, `010089B00D09C000`), ANIMAL WELL (update 1.0.10, SDK 20.5.7, `010020D01AD24000`), AI: THE SOMNIUM FILES - nirvanA Initiative (update 1.0.2, SDK 13.3.5, `0100713016CCC000`) and Mario Kart 8 Deluxe (update 4.0.0, SDK 4.4.0, `0100152000022000`). Hades renders with Vulkan, which the host implements on Metal (`dm/gfx/vk/hle_vk.m`; `main` imports Vulkan, not NVN). Age of Calamity is Koei Tecmo's KTGL engine: files in RDB archives, shaders as GLSLC outputs (`shader_split.py` `idrk_shaders`). Three Houses is KTGL too; 9 of its shaders exist only as compiles of the game's own GLSLC at run time, captured once on the Mac (its `main` holds GLSL sources and imports `glslcCompile`). Breath of the Wild and Tears of the Kingdom run at 60 fps through a frame step the host decides in whole vsyncs (`FRAME_STEP` in their profiles' `patches.py`, `nvn_fstep.h`, profile keys `fps_target` and `frame_step`); Tears of the Kingdom has a 5 GB guest layout, and its `sdk` build (Breath of the Wild's) has the SDK's address-region node area cut (`sdk_patches.py`): without it the resource heaps ran out of alias region at the first descent from the Great Sky Island and the game played its memory-shortage Blood Moon, which put Link back at the start (its log shows `MSF`). Monster Hunter Rise is Capcom's RE Engine: files in KPKA PAK archives (`shader_split.py` `kpka_jobs`, shaders in SDF files), the base game an XCI, its GPU heap a 64 GB sparse virtual pool (`hle_nvn.c` `vpool_*`, a 4 KB page table the replay reads textures through); it runs at 60 fps (30 on the Switch). Super Mario Galaxy is the Wii game on NERD's Hagi layer (PowerPC code recompiled into `main`), renders with Vulkan, and generates its shaders at run time, which the NSP script captures on the Mac (no SPIR-V file in its RomFS); its pointer is the touch screen in handheld mode (profile key `op_mode`). Ace Attorney Investigations Collection is Unity 2022 IL2CPP: its shaders keep their parameters as blob entries of their own (`shader_split.py` `unity_code_blobs`), and its C# file API goes through the SDK's C library, whose path checks the host answers (`hle_fs.c` `IsPathMounted` and the length limits). Hades II, unlike Hades, renders with NVN; its shaders bind textures at fixed slots up to 34 (slot 34's handle lies in the driver's image table, `dmsh_build.py`), and the two long lines through Melinoë while the staff's Omega attack charges are the game's own aim lines, not a rendering bug. Unicorn Overlord is Vanillaware's engine like 13 Sentinels (shaders compiled into `main`); both need the profile key `alloc_big_align`, which aligns large allocations to 64 KB so the ring heaps the engine cuts from a memory pool fit. Rhythm Heaven Groove is Nintendo's own 2D engine on SDK 21, the newest SDK so far (its DSP command list has no magic, `hle_audren.c`); it plays handheld with Boost (profile keys `op_mode handheld`, `perf_mode boost`), which skips its TV latency calibration and keeps its 1920x1080 draw size. Octopath Traveler 0 is Unreal Engine 5: everything in one pak with an AES-encrypted index whose key is found in `main` (`dm/gfx/tools/uepak.py`), its shaders Oodle-compressed in the pak's shader code archives and decoded once on the Mac by the game's own Oodle (any game with Unreal paks, `DM_OODLE_JOB`), its allocator replaced by the host's because Unreal's reserves 37 GB of address space (a cut in its `patches.py`, `hle_uemalloc.c`), its address region capped by the game's own argument (profile key `game_args`), and the particle shaders that pick a texture slot at run time bound by slot (profile key `tex_dyn_slots`); it runs at 60 fps (30 on the Switch) because the host writes Unreal's `t.MaxFPS` and `rhi.SyncInterval` in game memory (`hle_fpslock.c`, profile key `fps_target`). Pokémon Legends: Z-A is Game Freak's Trinity engine: everything in the packs of `arc/data.trpfs` (FlatBuffers, every file Oodle-compressed), which the shader split decodes with the game's own decoder in the Mac host's tool mode (`shader_split.py` `trinity_shaders`, `hle_oodle.c` `DM_OODLE_SERVE`); its 12 GB StandardAllocator is cut to 4 GB to fit a 5 GB guest layout, and the host refuses maps past the pool as Horizon does (profile key `mem_map_limit`); its characters are skinned with transform feedback (`dmsh_build.py` `vs_tf`, `nvn_mtl.m` `tf_draw`), and its occlusion queries, which decide how characters are animated, need the draw's base instance in the driver segment (c0 + 0x644, `encode_args`). Cuphead is Unity 2017.4 IL2CPP on SDK 5.5.0: Unity 2017's serialized and Shader formats are read by `unityfs.py` / `shader_split.py`, its rewritten modules (80 MB) need the profile key `mem_used` above the default 64 MB, or its `SetHeapSize` fails, and its NVN pool allocator breaks on a zero-size block, so the host never reports a ZCull storage size of 0 (`hle_nvn.c` `zcull_size`). Super Mario Galaxy 2 is Galaxy's Hagi layer with Galaxy's `sdk` build; its movie player reserves 384 MB of virtual address memory, which fits only because a rewriter cut on that `sdk` build shrinks the SDK's address-region node area to 62 MB (`sdk_patches.py`; the profile's alias region is 512 MB). FINAL FANTASY TACTICS - The Ivalice Chronicles is Square Enix's engine on NVN: its shaders are GLSLC outputs in loose technique files, which `shader_split.py` reads in any RomFS file no other reader takes; its UI textures are written by the CPU into uncached pools (profile keys `tex_flush_upload`, `tex_dirty_upload`); and two job-pool workers share one auto-clear event, so a build cut makes a worker woken in another's place pass the wake on (its `patches.py`). Absolum is FNA (XNA 4 in C#) on .NET NativeAOT with SDL3 and FNA3D's NVN driver: its `main` starts its text with 1.7 MB of data the rewriter leaves alone (`nso_code.py`), its shaders are GLSLC outputs in `shaders.nx.bin`, its picture is flipped by a viewport swizzle the host applies, and everything lives in virtual address memory: malloc's 40 GB StandardAllocator is cut to 2.5 GB and the SDK's node area and large-region alignment are cut (its `patches.py` and `sdk_patches.py`), and the .NET GC's region range is 1.5 GB (profile key `game_env`), to fit a 5 GB guest layout. Captain Toad: Treasure Tracker is Nintendo EPD Tokyo's engine (the `al` library over sead / agl, as 3D World and Odyssey) and needed no host change: it plays handheld because its pointer is the touch screen there (the Joy-Con gyro docked; profile key `op_mode`), and agl's post-effect program that samples its own attachment reads a snapshot (profile key `feedback_snap`). Brain Memory Training Academy is Unity 2022.3 IL2CPP with no bundles: its serialized files carry no type trees and nothing lends it a `Shader` tree, so `unityfs.py` has Unity 2022.3's `Shader` layout written out (`KNOWN_TREES`), and like Ace Attorney Investigations Collection it needs the SDK's address-region node area cut on its `sdk` build (`sdk_patches.py`). The Great Ace Attorney Chronicles is Capcom's MT Framework: files in ARC archives of zlib entries, its 460 shaders in one shader package inside one of them (`shader_split.py` `mtspk_shaders`); it copies its textures into a CPU-cached pool after creating them and never flushes (its `main` has no cache maintenance at all), so it needs `tex_flush_upload` and `tex_dirty_upload` like FINAL FANTASY TACTICS. STEINS;GATE RE:BOOT is M2's ADV engine (E-mote characters, PSB archives, H.264 movies through the SDK's movie library): it needed `nn::audio::SetAudioOutVolume` (`hle_audio.c`), and because it decodes every picture into one reused region of a pool that lives for good, the storage-sharing texture table now drops an entry whose storage another texture takes over (`nvn_mtl.m` `alias_take_over`); it draws 1280x720 in both modes by its own choice. Big Brain Academy: Brain vs. Brain is NintendoWare Bezel Engine 1.56: nearly all of RomFS is in Bezel Engine archives of zstd entries, its shaders in their `.bfsha` entries (`shader_split.py` `bea_members`); its `nninitStartup` sets a heap or a 60 GB virtual address region by `IsVirtualAddressMemoryEnabled`, so the host reports the NPDM's system resource (profile key `sys_resource`, 0 here); its activity previews loop the movie decoder with Flush and Start from four threads (`hle_movie.c`); and it plays handheld with Boost (profile keys `op_mode handheld`, `perf_mode boost`), because only handheld does it read the touch screen. Patrick's Parabox is Unity 2020.3 IL2CPP with no bundles: `unityfs.py` has Unity 2020.3's `Shader` layout written out (`KNOWN_TREES`), it needs the SDK's address-region node area cut on its `sdk` build like Brain Memory Training Academy (`sdk_patches.py`), and Unity's player hands the shared fonts straight to FreeType, so a game that imports no `ScalableFontEngineHelper::Decode` gets the TTF itself (`hle_sdk16.c` `font_skip`). Celeste is MonoGame for the Switch, its C# compiled to C++ by Sickhead's BRUTE: its shaders are MonoGame effects (MGFX) in its XNB files and in `main` (`shader_split.py` `mgfx_effect`), and it needed no host change. DRAGON QUEST XI S is Unreal Engine 4.18: legacy paks with LZ5 blocks, decoded on the Mac by the game's own LZ5 (`uepak.py`, `hle_oodle.c`), its shaders inline in the cooked packages (`shader_split.py` `ue_legacy_jobs`), and the slice-writing geometry shader of its colour-grading LUT folded into the vertex shader (`dmsh_build.py` `gs_slice`, `nvn_mtl.m` `prog_fold_gs`); Square Enix's sound driver never waits on the renderer event, so audio catch-up frames wait for queue space (`hle_audren.c`); its movie player plays only once every track holds a decoded output, so audio movie decoders keep 32 (`hle_movie.c`); and its Unreal region is capped at 4064 MB (profile key `game_args`, MB up to 4095) with the node area and large-region alignment cut on its SDK 7.3.2 build (`sdk_patches.py`) for a 5 GB guest layout; it runs at 60 fps (30 on the Switch) because the host writes Unreal's `t.MaxFPS` and `nvn.SyncInterval` in game memory (`hle_fpslock.c`, profile key `fps_target`). Monster Train 2 is Unity 2022.3 IL2CPP on Super Mario Galaxy's SDK 18.3.5 `sdk` build: Unity creates a thread only when `nn::os::GetFreeThreadCount` (`svcGetInfo` type 24, `hle_svc.c`) is at least 1; its MP4 intro needs a movie audio decoder with more output slots than a video one, because Unity's player holds about half a second of audio before it plays (`hle_movie.c` `MD_NOUT`); and its post-processing compute shaders (the colour grading LUT among them) are Unity `ComputeShader` objects, which `shader_split.py` reads (`unity_compute`) with Unity 2022.3's layout written out in `unityfs.py`. It reads no touch, so it stays docked with the 3D pad layout, and it runs at 60 fps (30 on the Switch) with the profile key `present_interval display`, as its animations follow the time that passed. Raging Loop is Unity 2018.3 IL2CPP with no bundles on DRAGON QUEST XI S's `sdk` and `subsdk0` builds: `unityfs.py` has Unity 2018.3's `Shader` layout written out (`KNOWN_TREES`, an older form than the 2020 and 2022 layouts, with the parameters in each subprogram), and it needed no host change; it reads no touch, so it stays docked with the 2D pad layout. DARK SOULS: REMASTERED is FromSoftware's engine on SDK 4.5.0, the oldest SDK so far: its shaders are GLSLC outputs in BND3 archives inside the dvdbnd archives (`shader_split.py` `bdt_shaders`), and SDK 4's GLSLC outputs are read through their section headers (`glslc_section`); it keeps 12 sampler pools with different samplers under the same IDs, so handles a shader reads from a uniform buffer take the base of the pool bound at replay (`nvn_mtl.m` `ubo_tex_id` / `ubo_samp_id`); and it runs at 60 fps (30 on the Switch) with two rewriter cuts, its frame limiter's 30 and its frame update's fixed 1/30 s step (its `patches.py`). Paper Mario: The Thousand-Year Door is Intelligent Systems' engine on NintendoWare, on Celeste's SDK 15.3.2 `sdk` build: nearly every RomFS file is zstd (`.zst`, which `shader_split.py` decompresses by magic); its AngelScript call bridges leave sp misaligned, which a general rewriter rule rounds up (`site_sp_align`); it runs at 60 fps (30 on the Switch) by the host's frame step (`FRAME_STEP`, with the floor of its vsync step cut from 2 to 1); and its three job workers poll the job list with no SVC, which held it at 24 fps on an iPhone until the general spin wait rule (`patch_nso.py` "Spin waits", `hle_thread.c` `dm_spin_wait`). Hollow Knight: Silksong is Unity 6 (6000.0.50f1) IL2CPP, the newest Unity so far: its bundles carry their own type trees, so `unityfs.py` needed no layout; Unity 6 polls `nn::hid::GetDebugPadStates` every frame (`hle_hid.c`), and it takes all its memory itself through `nn::os::AllocateAddressRegion` / `AllocateMemoryPages` with no heap, so it runs in the 5 GB guest layout that `profile_guess.py` now gives every game with an NPDM system resource, with the SDK's address-region node area cut on its `sdk` build (`sdk_patches.py`). Catherine: Full Body is Atlus's GFD engine with CRI middleware (CPK archives, ADX2 sound, Sofdec2 movies through `movie::Decoder`, whose player reads `nv12-uv-stride`, `hle_movie.c`): its 3,128 shaders are GLSLC outputs in `data/shaders_binary`, `main` and two compiles of its own GLSLC at boot, which the host lets run by reporting at least that GLSLC's GPU code version (`hle_glslc.c`); that GLSLC writes every vertex shader as a VertexA part (shader type 0, the position) and a VertexB part, which `dmsh_build.py` merges as Ryujinx does (`vertex_ab`; the screen was black without it), and because the game reuses a copy's staging memory a frame after recording it, the host commits a segment at the first pass boundary after a buffer → texture copy (`nvn_mtl.m` `g_cut_copy`). Digimon Story Cyber Sleuth: Complete Edition is Media.Vision's MVGL engine and renders with NVIDIA's desktop OpenGL 4.5 through EGL, not NVN, which the host implements on Metal (`dm/gfx/gl/hle_gl.m`: the 13 EGL imports and the 79 GL functions its code calls of the 2,956 its glad loader fetches, counted with `dm/tools/loader_census.py`; `profile_guess.py` gives `graphics gl` when `main` imports `eglCreateWindowSurface` and names `glShaderSource`); its shaders are GLSL ES 3.20 source in its MVGL archives (`dm/gfx/tools/mvgl.py`: MDB1 with doboz entries) and in `main`, which `gl_shaders.py` translates offline with glslang and SPIRV-Cross (packing needs `brew install glslang`) and the host finds by the hash of its text. Animal Crossing: New Horizons is Nintendo EPD's engine (sead / agl / gsys) on SDK 20.5.4: its `main` links its own HIPC client for `olsc:u` (cloud backup settings), which the host answers with sessions of its own (`hle_sf.c`), its island lives in device save data (`MountDeviceSaveData`, `hle_fs.c`), it skips clears with conditional rendering (profile key `render_cond`), its 63 MB `main` needed halfword jump tables padded only to their largest entry (`patch_nso.py` `half_count`), and its `SaveProc` builds a new island in grid walks that yield once per cell, so a yield whose loop moved its callee-saved registers since the last one now returns at once instead of backing off, for every game (`hle_thread.c` `thr_yield`; the island showed after 77 s of black screen, now about 10 s). DRAGON QUEST VII Reimagined is Unreal Engine 4.27: IoStore containers beside one pak, its shaders LZ4 blocks in the pak's shader code archives, which `uepak.py` decodes itself (`lz4_shaders`), and its allocator replaced as Octopath Traveler 0's (a cut and an observation gate in its `patches.py`, `hle_uemalloc.c` layout `UE4_27`) in a 2 GB engine region (profile key `game_args`); its audio renderer runs at 32 kHz, resampled by the host on the CPU path (`hle_audren.c`); and Unreal's volumetric lightmap merge sizes its indirection texture with a query on the image it writes, so a texture slot that shares its c2 word with an image slot reads the bound image (`nvn_mtl.m` `tex_slot_h`) and the translator types such a query from the image (`dmsh_xlate` `ImageTypesOf`): without them every character in the opening cutscene glowed white; it runs at 60 fps (30 on the Switch) because the host writes Unreal's `t.MaxFPS` and `rhi.SyncInterval` in game memory (`hle_fpslock.c`, profile key `fps_target`), the same two limits a 60 fps mod for the base game lifts with code patches (`UEngine::SetMaxFPS` returning at once, the name `r.VSync` broken). FINAL FANTASY RESONANCE is Unreal Engine 5.6.1, the newest Unreal so far: its shader libraries are IoStore chunks in its container, not in its pak, two layers of Oodle that `uepak.py` reads through the game's own decoder in the Mac host with no boot (`Toc`, `io_archive`, `OodleHost`); its 94 MB `main` is 95% without unwind entries, so the rewriter analyses long functions in pieces (`patch_nso.py` `pieces`; one 82 MB function took 100 GB of memory) and treats code before the first unwind entry as a function; its allocator is replaced as Octopath Traveler 0's (UE 5.6's 32-slot FMalloc, `hle_uemalloc.c` layout `UE5_6`), and its engine region is 4064 MB (profile key `game_args`), because the engine's own page allocator never gives address space back (2 GB aborted in the first battle); 2,077 particle shaders need `tex_dyn_slots`; it runs at 60 fps (30 on the Switch) by the host's `rhi.SyncInterval` write (`hle_fpslock.c`, profile key `fps_target`). Luigi's Mansion 3 is Next Level Games' own engine on NVN: its RomFS files are in the engine's `.dict` / `.data` archives, and update 1.4.0 ships `.patch` files beside the base ones, which `shader_split.py` applies as the game does (`nlg_shaders`); the game patches texture handles into its recorded command buffers in place, at the command start + 0x10 it took from `GetCommandMemoryUsed`, so single texture binds are recorded in the hardware's layout (`OP_BIND_TEX1`, every game; without it the title showed a stretched icon and a new game a white screen); its G-buffer is two integer targets, which its pixel shaders write through a second, integer-output entry point (`dmsh_build.py` `fs_uint`, every game); and it runs at 60 fps (30 on the Switch) because the host writes its frame limiter's 33.333 ms, a constant in read-only data, as 1000 / `fps_target` less half a millisecond (`hle_fpslock.c`, with `present_interval display`). Its tessellated draws are skipped (Metal 4 has no tessellation stage) and its geometry shaders do not build; neither was drawn in play so far. AI: THE SOMNIUM FILES is Unity 2017.4 IL2CPP (Cuphead's Unity layout) with FMOD: its 168 movies are H.264 MP4s that Unity's player copies into NV12 texture pairs, and its Spike Chunsoft logo movie held the game before the title until a released output slot also went on with the H.264 decoder's drain at the end of a stream (`hle_movie.c`: Unity holds all 16 video outputs by then); it binds its vertex and fragment programs apart, and the precompile prediction reused the ring slot of the combined program the replay had saved, so a draw ran with another pair's shaders until each restore of the replay state took the combination again from the per-stage bindings (`nvn_mtl.m` `prog_resolve`, every game); it plays docked with the 3D pad layout (the investigation's view is on the right stick). ANIMAL WELL embeds all its assets and 46 BNSH shaders in `main`; its base and update Program NCAs have no data section, which `nx_extract` represents as an empty RomFS directory. It needed no host change; its profile uses the 2D pad layout. AI: THE SOMNIUM FILES - nirvanA Initiative is Unity 2020.3 IL2CPP (Patrick's Parabox's Unity) with Addressables bundles and FMOD, and needed no host change: Unity 2020.3 keys a compute shader kernel's variants by keyword set (`shader_split.py` `unity_compute`, `variantMap`); its `nninitStartup` hands pool − used to StandardAllocator as virtual address memory and Unity reserves its own regions beside it, so it has the SDK's address-region node area cut on its `sdk` build (`sdk_patches.py`) and a 1 GB pool, which the NSP script's heap check now sets for such a game (`profile_guess.py` `heap_signals`: the alias region nearly full within seconds and no heap); it plays docked with the 3D pad layout (the investigation's view is on the right stick). Mario Kart 8 Deluxe is Nintendo's sead / agl / gsys engine on SDK 4.4.0, the oldest SDK so far: it lists the six default Miis at boot and builds Mii heads from the console's MiiModel system data, so the host answers the Mii database itself (`hle_mii.c`: the six default Miis, none of the console's own) and mounts a stand-in of that archive with every part empty (`dm_mii_standin`, `hle_fs.c` `MountSystemData`; the firmware is not the host's to ship, so Mii heads draw nothing); its DLC licence is decrypted in a 0x3130-byte heap that also holds the mount cache, now sized as Horizon's RomFS working memory (`romfs_work_size`); it mounts device and account save data, which are now two host saves for a game whose `main` imports both (at boot it deletes `CommonSave:/userdata.dat`); SDK 4's DeviceSink is command 0xf of 0x130 bytes, and the audio renderer's update lock is read off each SDK's own `AudioRenderSystem::Update` (`hle_audren.c` `update_lock`: it was SDK 16's for every SDK before 20); it evaluates its conditional rendering (profile key `render_cond`) and runs at 60 fps. [README.md](README.md) is the player-facing page (games, packing, playing); [docs/development.md](docs/development.md) has the internals, the script's options, building by hand, prewarm lists and per-game notes. +- This repository runs Nintendo Switch games natively on Apple platforms: the game's own code runs, rewritten only where Apple's kernel requires it. Forty-eight games are playable on the iPhone: Super Mario Bros. Wonder (update 1.2.1, profile `010015100B514000`), Fire Emblem Engage (update 2.0.0, `0100A6301214E000`), Metroid Dread (update 2.1.0, `010093801237C000`), 13 Sentinels: Aegis Rim (update 1.10, `01003FC01670C000`), Super Mario 3D World + Bowser's Fury (update 1.2.2, `010028600EBDA000`), The Legend of Zelda: Breath of the Wild (update 1.9.0, SDK 20, `01007EF00011E000`), The Legend of Zelda: Link's Awakening (update 1.1.0, SDK 8, `01006BB00C6F0000`) Super Mario Odyssey (update 1.4.1, SDK 7, `0100000000010000`), Hades (update 1.0.38233, SDK 9, `0100535012974000`), Hyrule Warriors: Age of Calamity (update 1.3.0, SDK 9.6, `01002B00111A2000`), Fire Emblem: Three Houses (update 1.2.0, SDK 6.5, `010055D009F78000`), Super Mario Galaxy (update 1.3.1, SDK 18, `010099C022B96000`) The Legend of Zelda: Tears of the Kingdom (update 1.4.3, SDK 20, `0100F2C0115B6000`) Monster Hunter Rise (update 16.0.2, SDK 11.4.3, `0100B04011742000`), Ace Attorney Investigations Collection (update 1.0.0.2, SDK 17.5.3, `010005501E68C000`), Hades II (base game, SDK 19.3.9, `0100A00019DE0000`), Unicorn Overlord (update 1.05, SDK 16.2.4, `010069401ADB8000`), Rhythm Heaven Groove (base game 1.0.0, SDK 21.4.0, `0100D9F01D474000`), Octopath Traveler 0 (update 1.0.5, SDK 19.3.13, `01005270232F2000`), Pokémon Legends: Z-A (update 2.0.2, SDK 19.3.11, `0100F43008C44000`), Cuphead (update 1.3.7, SDK 5.5.0, `0100A5C00D162000`), Super Mario Galaxy 2 (update 1.4.0, SDK 18.3.5, `0100FD8022DAA000`), FINAL FANTASY TACTICS - The Ivalice Chronicles (update 1.4.0, SDK 19.3.9, `010038B015560000`), Absolum (update 1.1.1, SDK 18.3.2, `0100A83022A22000`), Captain Toad: Treasure Tracker (update 1.4.2, SDK 8.2.0, `01009BF0072D4000`) and Brain Memory Training Academy (base game 1.0.0, SDK 16.2.4, `0100F5D01DA3A000`), The Great Ace Attorney Chronicles (base game 1.0.0, SDK 10.4.1, `010036E00FB20000`), STEINS;GATE RE:BOOT (update 1.0.3, SDK 20.5.17, `01008BC024FDE000`), Big Brain Academy: Brain vs. Brain (update 1.2.0, SDK 11.4.3, `0100620012D6E000`), Patrick's Parabox (update 1.0.1, SDK 13.3.7, `0100E44016C22000`), Celeste (update 1.4.1.1, SDK 15.3.2, `01002B30028F6000`), DRAGON QUEST XI S: Echoes of an Elusive Age – Definitive Edition (update 1.0.4, SDK 7.3.2, `01006C300E9F0000`), Monster Train 2 (update 1.1.1, SDK 18.3.5, `010051701FB46000`), Raging Loop (base game 1.0.0, SDK 7.3.2, `010020400E1C2000`), DARK SOULS: REMASTERED (update 1.0.3, SDK 4.5.0, `01004AB00A260000`), Paper Mario: The Thousand-Year Door (update 1.0.1, SDK 15.3.2, `0100ECD018EBE000`), Hollow Knight: Silksong (update 1.0.30000, SDK 20.5.8, `010013C00E930000`), Catherine: Full Body (update 1.0.1a, SDK 8.2.1, `0100BF00112C0000`), Digimon Story Cyber Sleuth: Complete Edition (update 1.0.3, SDK 6.4.0, `010014E00DB56000`), Animal Crossing: New Horizons (update 3.0.3, SDK 20.5.4, `01006F8002326000`), DRAGON QUEST VII Reimagined (update 1.2.0, SDK 19.3.1, `0100A9D01C446000`), FINAL FANTASY RESONANCE (update 1.0.1, SDK 20.5.17, `0100606026F12000`), Luigi's Mansion 3 (update 1.4.0, SDK 7.3.2, `0100DCA0064A6000`), AI: THE SOMNIUM FILES (update 1.0.1, SDK 6.4.2, `010089B00D09C000`), ANIMAL WELL (update 1.0.10, SDK 20.5.7, `010020D01AD24000`), AI: THE SOMNIUM FILES - nirvanA Initiative (update 1.0.2, SDK 13.3.5, `0100713016CCC000`), Mario Kart 8 Deluxe (update 4.0.0, SDK 4.4.0, `0100152000022000`) and Balatro (update 1.1.3, SDK 18.3.2, `0100CD801CE5E000`). Hades renders with Vulkan, which the host implements on Metal (`dm/gfx/vk/hle_vk.m`; `main` imports Vulkan, not NVN). Age of Calamity is Koei Tecmo's KTGL engine: files in RDB archives, shaders as GLSLC outputs (`shader_split.py` `idrk_shaders`). Three Houses is KTGL too; 9 of its shaders exist only as compiles of the game's own GLSLC at run time, captured once on the Mac (its `main` holds GLSL sources and imports `glslcCompile`). Breath of the Wild and Tears of the Kingdom run at 60 fps through a frame step the host decides in whole vsyncs (`FRAME_STEP` in their profiles' `patches.py`, `nvn_fstep.h`, profile keys `fps_target` and `frame_step`); Tears of the Kingdom has a 5 GB guest layout, and its `sdk` build (Breath of the Wild's) has the SDK's address-region node area cut (`sdk_patches.py`): without it the resource heaps ran out of alias region at the first descent from the Great Sky Island and the game played its memory-shortage Blood Moon, which put Link back at the start (its log shows `MSF`). Monster Hunter Rise is Capcom's RE Engine: files in KPKA PAK archives (`shader_split.py` `kpka_jobs`, shaders in SDF files), the base game an XCI, its GPU heap a 64 GB sparse virtual pool (`hle_nvn.c` `vpool_*`, a 4 KB page table the replay reads textures through); it runs at 60 fps (30 on the Switch). Super Mario Galaxy is the Wii game on NERD's Hagi layer (PowerPC code recompiled into `main`), renders with Vulkan, and generates its shaders at run time, which the NSP script captures on the Mac (no SPIR-V file in its RomFS); its pointer is the touch screen in handheld mode (profile key `op_mode`). Ace Attorney Investigations Collection is Unity 2022 IL2CPP: its shaders keep their parameters as blob entries of their own (`shader_split.py` `unity_code_blobs`), and its C# file API goes through the SDK's C library, whose path checks the host answers (`hle_fs.c` `IsPathMounted` and the length limits). Hades II, unlike Hades, renders with NVN; its shaders bind textures at fixed slots up to 34 (slot 34's handle lies in the driver's image table, `dmsh_build.py`), and the two long lines through Melinoë while the staff's Omega attack charges are the game's own aim lines, not a rendering bug. Unicorn Overlord is Vanillaware's engine like 13 Sentinels (shaders compiled into `main`); both need the profile key `alloc_big_align`, which aligns large allocations to 64 KB so the ring heaps the engine cuts from a memory pool fit. Rhythm Heaven Groove is Nintendo's own 2D engine on SDK 21, the newest SDK so far (its DSP command list has no magic, `hle_audren.c`); it plays handheld with Boost (profile keys `op_mode handheld`, `perf_mode boost`), which skips its TV latency calibration and keeps its 1920x1080 draw size. Octopath Traveler 0 is Unreal Engine 5: everything in one pak with an AES-encrypted index whose key is found in `main` (`dm/gfx/tools/uepak.py`), its shaders Oodle-compressed in the pak's shader code archives and decoded once on the Mac by the game's own Oodle (any game with Unreal paks, `DM_OODLE_JOB`), its allocator replaced by the host's because Unreal's reserves 37 GB of address space (a cut in its `patches.py`, `hle_uemalloc.c`), its address region capped by the game's own argument (profile key `game_args`), and the particle shaders that pick a texture slot at run time bound by slot (profile key `tex_dyn_slots`); it runs at 60 fps (30 on the Switch) because the host writes Unreal's `t.MaxFPS` and `rhi.SyncInterval` in game memory (`hle_fpslock.c`, profile key `fps_target`). Pokémon Legends: Z-A is Game Freak's Trinity engine: everything in the packs of `arc/data.trpfs` (FlatBuffers, every file Oodle-compressed), which the shader split decodes with the game's own decoder in the Mac host's tool mode (`shader_split.py` `trinity_shaders`, `hle_oodle.c` `DM_OODLE_SERVE`); its 12 GB StandardAllocator is cut to 4 GB to fit a 5 GB guest layout, and the host refuses maps past the pool as Horizon does (profile key `mem_map_limit`); its characters are skinned with transform feedback (`dmsh_build.py` `vs_tf`, `nvn_mtl.m` `tf_draw`), and its occlusion queries, which decide how characters are animated, need the draw's base instance in the driver segment (c0 + 0x644, `encode_args`). Cuphead is Unity 2017.4 IL2CPP on SDK 5.5.0: Unity 2017's serialized and Shader formats are read by `unityfs.py` / `shader_split.py`, its rewritten modules (80 MB) need the profile key `mem_used` above the default 64 MB, or its `SetHeapSize` fails, and its NVN pool allocator breaks on a zero-size block, so the host never reports a ZCull storage size of 0 (`hle_nvn.c` `zcull_size`). Super Mario Galaxy 2 is Galaxy's Hagi layer with Galaxy's `sdk` build; its movie player reserves 384 MB of virtual address memory, which fits only because a rewriter cut on that `sdk` build shrinks the SDK's address-region node area to 62 MB (`sdk_patches.py`; the profile's alias region is 512 MB). FINAL FANTASY TACTICS - The Ivalice Chronicles is Square Enix's engine on NVN: its shaders are GLSLC outputs in loose technique files, which `shader_split.py` reads in any RomFS file no other reader takes; its UI textures are written by the CPU into uncached pools (profile keys `tex_flush_upload`, `tex_dirty_upload`); and two job-pool workers share one auto-clear event, so a build cut makes a worker woken in another's place pass the wake on (its `patches.py`). Absolum is FNA (XNA 4 in C#) on .NET NativeAOT with SDL3 and FNA3D's NVN driver: its `main` starts its text with 1.7 MB of data the rewriter leaves alone (`nso_code.py`), its shaders are GLSLC outputs in `shaders.nx.bin`, its picture is flipped by a viewport swizzle the host applies, and everything lives in virtual address memory: malloc's 40 GB StandardAllocator is cut to 2.5 GB and the SDK's node area and large-region alignment are cut (its `patches.py` and `sdk_patches.py`), and the .NET GC's region range is 1.5 GB (profile key `game_env`), to fit a 5 GB guest layout. Captain Toad: Treasure Tracker is Nintendo EPD Tokyo's engine (the `al` library over sead / agl, as 3D World and Odyssey) and needed no host change: it plays handheld because its pointer is the touch screen there (the Joy-Con gyro docked; profile key `op_mode`), and agl's post-effect program that samples its own attachment reads a snapshot (profile key `feedback_snap`). Brain Memory Training Academy is Unity 2022.3 IL2CPP with no bundles: its serialized files carry no type trees and nothing lends it a `Shader` tree, so `unityfs.py` has Unity 2022.3's `Shader` layout written out (`KNOWN_TREES`), and like Ace Attorney Investigations Collection it needs the SDK's address-region node area cut on its `sdk` build (`sdk_patches.py`). The Great Ace Attorney Chronicles is Capcom's MT Framework: files in ARC archives of zlib entries, its 460 shaders in one shader package inside one of them (`shader_split.py` `mtspk_shaders`); it copies its textures into a CPU-cached pool after creating them and never flushes (its `main` has no cache maintenance at all), so it needs `tex_flush_upload` and `tex_dirty_upload` like FINAL FANTASY TACTICS. STEINS;GATE RE:BOOT is M2's ADV engine (E-mote characters, PSB archives, H.264 movies through the SDK's movie library): it needed `nn::audio::SetAudioOutVolume` (`hle_audio.c`), and because it decodes every picture into one reused region of a pool that lives for good, the storage-sharing texture table now drops an entry whose storage another texture takes over (`nvn_mtl.m` `alias_take_over`); it draws 1280x720 in both modes by its own choice. Big Brain Academy: Brain vs. Brain is NintendoWare Bezel Engine 1.56: nearly all of RomFS is in Bezel Engine archives of zstd entries, its shaders in their `.bfsha` entries (`shader_split.py` `bea_members`); its `nninitStartup` sets a heap or a 60 GB virtual address region by `IsVirtualAddressMemoryEnabled`, so the host reports the NPDM's system resource (profile key `sys_resource`, 0 here); its activity previews loop the movie decoder with Flush and Start from four threads (`hle_movie.c`); and it plays handheld with Boost (profile keys `op_mode handheld`, `perf_mode boost`), because only handheld does it read the touch screen. Patrick's Parabox is Unity 2020.3 IL2CPP with no bundles: `unityfs.py` has Unity 2020.3's `Shader` layout written out (`KNOWN_TREES`), it needs the SDK's address-region node area cut on its `sdk` build like Brain Memory Training Academy (`sdk_patches.py`), and Unity's player hands the shared fonts straight to FreeType, so a game that imports no `ScalableFontEngineHelper::Decode` gets the TTF itself (`hle_sdk16.c` `font_skip`). Celeste is MonoGame for the Switch, its C# compiled to C++ by Sickhead's BRUTE: its shaders are MonoGame effects (MGFX) in its XNB files and in `main` (`shader_split.py` `mgfx_effect`), and it needed no host change. DRAGON QUEST XI S is Unreal Engine 4.18: legacy paks with LZ5 blocks, decoded on the Mac by the game's own LZ5 (`uepak.py`, `hle_oodle.c`), its shaders inline in the cooked packages (`shader_split.py` `ue_legacy_jobs`), and the slice-writing geometry shader of its colour-grading LUT folded into the vertex shader (`dmsh_build.py` `gs_slice`, `nvn_mtl.m` `prog_fold_gs`); Square Enix's sound driver never waits on the renderer event, so audio catch-up frames wait for queue space (`hle_audren.c`); its movie player plays only once every track holds a decoded output, so audio movie decoders keep 32 (`hle_movie.c`); and its Unreal region is capped at 4064 MB (profile key `game_args`, MB up to 4095) with the node area and large-region alignment cut on its SDK 7.3.2 build (`sdk_patches.py`) for a 5 GB guest layout; it runs at 60 fps (30 on the Switch) because the host writes Unreal's `t.MaxFPS` and `nvn.SyncInterval` in game memory (`hle_fpslock.c`, profile key `fps_target`). Monster Train 2 is Unity 2022.3 IL2CPP on Super Mario Galaxy's SDK 18.3.5 `sdk` build: Unity creates a thread only when `nn::os::GetFreeThreadCount` (`svcGetInfo` type 24, `hle_svc.c`) is at least 1; its MP4 intro needs a movie audio decoder with more output slots than a video one, because Unity's player holds about half a second of audio before it plays (`hle_movie.c` `MD_NOUT`); and its post-processing compute shaders (the colour grading LUT among them) are Unity `ComputeShader` objects, which `shader_split.py` reads (`unity_compute`) with Unity 2022.3's layout written out in `unityfs.py`. It reads no touch, so it stays docked with the 3D pad layout, and it runs at 60 fps (30 on the Switch) with the profile key `present_interval display`, as its animations follow the time that passed. Raging Loop is Unity 2018.3 IL2CPP with no bundles on DRAGON QUEST XI S's `sdk` and `subsdk0` builds: `unityfs.py` has Unity 2018.3's `Shader` layout written out (`KNOWN_TREES`, an older form than the 2020 and 2022 layouts, with the parameters in each subprogram), and it needed no host change; it reads no touch, so it stays docked with the 2D pad layout. DARK SOULS: REMASTERED is FromSoftware's engine on SDK 4.5.0, the oldest SDK so far: its shaders are GLSLC outputs in BND3 archives inside the dvdbnd archives (`shader_split.py` `bdt_shaders`), and SDK 4's GLSLC outputs are read through their section headers (`glslc_section`); it keeps 12 sampler pools with different samplers under the same IDs, so handles a shader reads from a uniform buffer take the base of the pool bound at replay (`nvn_mtl.m` `ubo_tex_id` / `ubo_samp_id`); and it runs at 60 fps (30 on the Switch) with two rewriter cuts, its frame limiter's 30 and its frame update's fixed 1/30 s step (its `patches.py`). Paper Mario: The Thousand-Year Door is Intelligent Systems' engine on NintendoWare, on Celeste's SDK 15.3.2 `sdk` build: nearly every RomFS file is zstd (`.zst`, which `shader_split.py` decompresses by magic); its AngelScript call bridges leave sp misaligned, which a general rewriter rule rounds up (`site_sp_align`); it runs at 60 fps (30 on the Switch) by the host's frame step (`FRAME_STEP`, with the floor of its vsync step cut from 2 to 1); and its three job workers poll the job list with no SVC, which held it at 24 fps on an iPhone until the general spin wait rule (`patch_nso.py` "Spin waits", `hle_thread.c` `dm_spin_wait`). Hollow Knight: Silksong is Unity 6 (6000.0.50f1) IL2CPP, the newest Unity so far: its bundles carry their own type trees, so `unityfs.py` needed no layout; Unity 6 polls `nn::hid::GetDebugPadStates` every frame (`hle_hid.c`), and it takes all its memory itself through `nn::os::AllocateAddressRegion` / `AllocateMemoryPages` with no heap, so it runs in the 5 GB guest layout that `profile_guess.py` now gives every game with an NPDM system resource, with the SDK's address-region node area cut on its `sdk` build (`sdk_patches.py`). Catherine: Full Body is Atlus's GFD engine with CRI middleware (CPK archives, ADX2 sound, Sofdec2 movies through `movie::Decoder`, whose player reads `nv12-uv-stride`, `hle_movie.c`): its 3,128 shaders are GLSLC outputs in `data/shaders_binary`, `main` and two compiles of its own GLSLC at boot, which the host lets run by reporting at least that GLSLC's GPU code version (`hle_glslc.c`); that GLSLC writes every vertex shader as a VertexA part (shader type 0, the position) and a VertexB part, which `dmsh_build.py` merges as Ryujinx does (`vertex_ab`; the screen was black without it), and because the game reuses a copy's staging memory a frame after recording it, the host commits a segment at the first pass boundary after a buffer → texture copy (`nvn_mtl.m` `g_cut_copy`). Digimon Story Cyber Sleuth: Complete Edition is Media.Vision's MVGL engine and renders with NVIDIA's desktop OpenGL 4.5 through EGL, not NVN, which the host implements on Metal (`dm/gfx/gl/hle_gl.m`: the 13 EGL imports and the 79 GL functions its code calls of the 2,956 its glad loader fetches, counted with `dm/tools/loader_census.py`; `profile_guess.py` gives `graphics gl` when `main` imports `eglCreateWindowSurface` and names `glShaderSource`); its shaders are GLSL ES 3.20 source in its MVGL archives (`dm/gfx/tools/mvgl.py`: MDB1 with doboz entries) and in `main`, which `gl_shaders.py` translates offline with glslang and SPIRV-Cross (packing needs `brew install glslang`) and the host finds by the hash of its text. Animal Crossing: New Horizons is Nintendo EPD's engine (sead / agl / gsys) on SDK 20.5.4: its `main` links its own HIPC client for `olsc:u` (cloud backup settings), which the host answers with sessions of its own (`hle_sf.c`), its island lives in device save data (`MountDeviceSaveData`, `hle_fs.c`), it skips clears with conditional rendering (profile key `render_cond`), its 63 MB `main` needed halfword jump tables padded only to their largest entry (`patch_nso.py` `half_count`), and its `SaveProc` builds a new island in grid walks that yield once per cell, so a yield whose loop moved its callee-saved registers since the last one now returns at once instead of backing off, for every game (`hle_thread.c` `thr_yield`; the island showed after 77 s of black screen, now about 10 s). DRAGON QUEST VII Reimagined is Unreal Engine 4.27: IoStore containers beside one pak, its shaders LZ4 blocks in the pak's shader code archives, which `uepak.py` decodes itself (`lz4_shaders`), and its allocator replaced as Octopath Traveler 0's (a cut and an observation gate in its `patches.py`, `hle_uemalloc.c` layout `UE4_27`) in a 2 GB engine region (profile key `game_args`); its audio renderer runs at 32 kHz, resampled by the host on the CPU path (`hle_audren.c`); and Unreal's volumetric lightmap merge sizes its indirection texture with a query on the image it writes, so a texture slot that shares its c2 word with an image slot reads the bound image (`nvn_mtl.m` `tex_slot_h`) and the translator types such a query from the image (`dmsh_xlate` `ImageTypesOf`): without them every character in the opening cutscene glowed white; it runs at 60 fps (30 on the Switch) because the host writes Unreal's `t.MaxFPS` and `rhi.SyncInterval` in game memory (`hle_fpslock.c`, profile key `fps_target`), the same two limits a 60 fps mod for the base game lifts with code patches (`UEngine::SetMaxFPS` returning at once, the name `r.VSync` broken). FINAL FANTASY RESONANCE is Unreal Engine 5.6.1, the newest Unreal so far: its shader libraries are IoStore chunks in its container, not in its pak, two layers of Oodle that `uepak.py` reads through the game's own decoder in the Mac host with no boot (`Toc`, `io_archive`, `OodleHost`); its 94 MB `main` is 95% without unwind entries, so the rewriter analyses long functions in pieces (`patch_nso.py` `pieces`; one 82 MB function took 100 GB of memory) and treats code before the first unwind entry as a function; its allocator is replaced as Octopath Traveler 0's (UE 5.6's 32-slot FMalloc, `hle_uemalloc.c` layout `UE5_6`), and its engine region is 4064 MB (profile key `game_args`), because the engine's own page allocator never gives address space back (2 GB aborted in the first battle); 2,077 particle shaders need `tex_dyn_slots`; it runs at 60 fps (30 on the Switch) by the host's `rhi.SyncInterval` write (`hle_fpslock.c`, profile key `fps_target`). Luigi's Mansion 3 is Next Level Games' own engine on NVN: its RomFS files are in the engine's `.dict` / `.data` archives, and update 1.4.0 ships `.patch` files beside the base ones, which `shader_split.py` applies as the game does (`nlg_shaders`); the game patches texture handles into its recorded command buffers in place, at the command start + 0x10 it took from `GetCommandMemoryUsed`, so single texture binds are recorded in the hardware's layout (`OP_BIND_TEX1`, every game; without it the title showed a stretched icon and a new game a white screen); its G-buffer is two integer targets, which its pixel shaders write through a second, integer-output entry point (`dmsh_build.py` `fs_uint`, every game); and it runs at 60 fps (30 on the Switch) because the host writes its frame limiter's 33.333 ms, a constant in read-only data, as 1000 / `fps_target` less half a millisecond (`hle_fpslock.c`, with `present_interval display`). Its tessellated draws are skipped (Metal 4 has no tessellation stage) and its geometry shaders do not build; neither was drawn in play so far. AI: THE SOMNIUM FILES is Unity 2017.4 IL2CPP (Cuphead's Unity layout) with FMOD: its 168 movies are H.264 MP4s that Unity's player copies into NV12 texture pairs, and its Spike Chunsoft logo movie held the game before the title until a released output slot also went on with the H.264 decoder's drain at the end of a stream (`hle_movie.c`: Unity holds all 16 video outputs by then); it binds its vertex and fragment programs apart, and the precompile prediction reused the ring slot of the combined program the replay had saved, so a draw ran with another pair's shaders until each restore of the replay state took the combination again from the per-stage bindings (`nvn_mtl.m` `prog_resolve`, every game); it plays docked with the 3D pad layout (the investigation's view is on the right stick). ANIMAL WELL embeds all its assets and 46 BNSH shaders in `main`; its base and update Program NCAs have no data section, which `nx_extract` represents as an empty RomFS directory. It needed no host change; its profile uses the 2D pad layout. AI: THE SOMNIUM FILES - nirvanA Initiative is Unity 2020.3 IL2CPP (Patrick's Parabox's Unity) with Addressables bundles and FMOD, and needed no host change: Unity 2020.3 keys a compute shader kernel's variants by keyword set (`shader_split.py` `unity_compute`, `variantMap`); its `nninitStartup` hands pool − used to StandardAllocator as virtual address memory and Unity reserves its own regions beside it, so it has the SDK's address-region node area cut on its `sdk` build (`sdk_patches.py`) and a 1 GB pool, which the NSP script's heap check now sets for such a game (`profile_guess.py` `heap_signals`: the alias region nearly full within seconds and no heap); it plays docked with the 3D pad layout (the investigation's view is on the right stick). Mario Kart 8 Deluxe is Nintendo's sead / agl / gsys engine on SDK 4.4.0, the oldest SDK so far: it lists the six default Miis at boot and builds Mii heads from the console's MiiModel system data, so the host answers the Mii database itself (`hle_mii.c`: the six default Miis, none of the console's own) and mounts a stand-in of that archive with every part empty (`dm_mii_standin`, `hle_fs.c` `MountSystemData`; the firmware is not the host's to ship, so Mii heads draw nothing); its DLC licence is decrypted in a 0x3130-byte heap that also holds the mount cache, now sized as Horizon's RomFS working memory (`romfs_work_size`); it mounts device and account save data, which are now two host saves for a game whose `main` imports both (at boot it deletes `CommonSave:/userdata.dat`); SDK 4's DeviceSink is command 0xf of 0x130 bytes, and the audio renderer's update lock is read off each SDK's own `AudioRenderSystem::Update` (`hle_audren.c` `update_lock`: it was SDK 16's for every SDK before 20); it evaluates its conditional rendering (profile key `render_cond`) and runs at 60 fps. Balatro is LÖVE 11.4 (love-nx: Lua source in the RomFS, LuaJIT's interpreter, SDL) on NVIDIA's desktop OpenGL through EGL, like Digimon: `main` imports `vkGetInstanceProcAddr` only for SDL's Vulkan loader, which `profile_guess.py` leaves out of its Vulkan count; LÖVE takes its core-profile paths when the GL layer says core (`GL_CONTEXT_PROFILE_MASK`): persistently mapped vertex streams fenced per frame, attribute bindings and current values, uniform queries, texture storage, 3D and array textures, swizzles and mipmaps, about 150 GL functions in all (`hle_gl.m`, counted with `loader_census.py`, which now follows LÖVE's glad through registers; `DM_GL_STUBS=1` names a missing one at its first call); its 35 shaders are GLSL that LÖVE puts together at run time, captured by the NSP script on the Mac (profile `gl_capture.tsv`, `capture_shaders`), with `bool` uniforms keeping their type (`gl_shaders.py`); it opens the audio renderer without an event, which started no render thread before (`hle_audren.c` `ar_open`: it was silent); and it plays by touch in handheld mode (profile keys `op_mode handheld`, `pad_layout touch`: the iPhone gamepad's Tap layout sends every touch to the game). Any Lua error makes it quit, and love-nx then crashes in its video worker's teardown (`main+0x339c00`): look for the Lua error first. [README.md](README.md) is the player-facing page (games, packing, playing); [docs/development.md](docs/development.md) has the internals, the script's options, building by hand, prewarm lists and per-game notes. - Each game's constants and shader tables form a game profile in `dm/games//`, where `` is the base game's title ID: `game.tsv` plus the tables (and `patches.py` when the rewriter has patches for its builds). Mario's profile is `010015100B514000`. Tools pick the profile with `DM_GAME`; `make -C dm/host` and `build_ipa.sh` pick it with `GAME`. There is no default game: set it every time (the tools stop and list the profiles otherwise). Put per-game values in the profile, not in code ([dm/games/README.md](dm/games/README.md)). - Prefer general optimizations and fixes to game-specific rules. Performance work (frame pacing, power, caches, load times) counts here as much as correctness. First look for a change that holds for every game: Horizon / NVN / Vulkan semantics, or what the binary, its SDK version, its imports or its files show. Use a game-specific rule (a profile key, a rewriter cut, a build-ID gate, a per-game branch in the host) only when it is absolutely necessary, that is, when no general rule can do it. - Per-build knowledge is keyed by the NSO build ID: rewriter cuts, observation gates and frame-step stubs in the profile's `patches.py` (a game's own modules) or `dm/tools/sdk_patches.py` (Nintendo's `sdk` modules, which several games ship), which `dm/tools/patch_nso.py` (the general rewriter) loads, the profile's `build` rows (the builds the game was brought up on, and their display versions; the NSP script packs any other version from a derived profile, without those builds' fixes), and host behaviour that uses fixed module addresses checks `module_t.build_id` (from `fixups.tsv`; `dm_main_build()`). When a new game build appears, carry addresses over with `dm/tools/nso_fnmap.py` and check them by disassembly; never let fixed addresses run on a build they were not found in. diff --git a/README.md b/README.md index 2a1ba50..5caa3b8 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Needs iOS / iPadOS 26 or later on an iPhone 15 Pro or newer (A17 Pro chip or lat | ![DRAGON QUEST VII Reimagined](docs/images/dragon-quest-vii-reimagined.jpg)
DRAGON QUEST VII Reimagined
IPA ~5.9 GB with update 1.2.0 | ![FINAL FANTASY RESONANCE](docs/images/final-fantasy-resonance.jpg)
FINAL FANTASY RESONANCE
IPA ~10 GB · 60 fps (30 on Switch) with update 1.0.1 | | ![Luigi's Mansion 3](docs/images/luigis-mansion-3.jpg)
Luigi's Mansion 3
IPA ~7.5 GB · 60 fps (30 on Switch) with update 1.4.0 | ![AI: THE SOMNIUM FILES](docs/images/ai-the-somnium-files.jpg)
AI: THE SOMNIUM FILES
IPA ~8.3 GB | | ![ANIMAL WELL](docs/images/animal-well.png)
ANIMAL WELL
IPA ~54 MB · update 1.0.10 | ![AI: THE SOMNIUM FILES - nirvanA Initiative](docs/images/ai-the-somnium-files-nirvana-initiative.jpg)
AI: THE SOMNIUM FILES - nirvanA Initiative
IPA ~11.6 GB · update 1.0.2 | -| ![Mario Kart 8 Deluxe](docs/images/mario-kart-8-deluxe.jpg)
Mario Kart 8 Deluxe
IPA ~11.9 GB · update 4.0.0 · Mii heads not drawn (no firmware) | | +| ![Mario Kart 8 Deluxe](docs/images/mario-kart-8-deluxe.jpg)
Mario Kart 8 Deluxe
IPA ~11.9 GB · update 4.0.0 · Mii heads not drawn (no firmware) | ![Balatro](docs/images/balatro.jpg)
Balatro
IPA ~82 MB · update 1.1.3 · played by touch | ## Pack an IPA diff --git a/dm/games/0100CD801CE5E000/bf_want.tsv b/dm/games/0100CD801CE5E000/bf_want.tsv new file mode 100644 index 0000000..54caeb8 --- /dev/null +++ b/dm/games/0100CD801CE5E000/bf_want.tsv @@ -0,0 +1 @@ +# generated diff --git a/dm/games/0100CD801CE5E000/game.tsv b/dm/games/0100CD801CE5E000/game.tsv new file mode 100644 index 0000000..d52a394 --- /dev/null +++ b/dm/games/0100CD801CE5E000/game.tsv @@ -0,0 +1,48 @@ +# Game profile of Balatro (Switch, update 1.1.3 = v851968). Plays on the iPhone; on the Mac through the title into a run (touch: the mouse), with sound +# Format: see dm/games/README.md +# LÖVE 11.4 ("Mysterious Mysteries", love-nx) with LuaJIT 2.1.0-beta3 in its interpreter, on SDL, SDK 18.3.2: the game itself is Lua source in the RomFS (main.lua, game.lua, engine/, +# functions/), its pictures PNG, its sound Ogg Vorbis. It renders with NVIDIA's desktop OpenGL 4.5 through EGL (subsdk0, not loaded): main gets its GL functions from LÖVE's glad +# loader (eglGetProcAddress) and calls about 150 of them (dm/tools/loader_census.py), which the host's GL layer implements on Metal (dm/gfx/gl/hle_gl.m), on the core profile's paths. +# main imports vkGetInstanceProcAddr only for SDL's Vulkan loader, which nothing uses (subsdk1 is NVIDIA's Vulkan driver, not loaded either) +# The shaders are GLSL that LÖVE puts together at run time, in Lua, from the game's 19 resources/shaders/*.fs files and its own built-in code: no game file holds them, so the NSP +# script captures them by booting the game on the Mac (gl_capture.tsv lists them) and dm/gfx/tools/gl_shaders.py translates them offline; the host finds each by the hash of its text + +# ---- Identity ---- +title_id 0100CD801CE5E000 +program_id 0100CD801CE5E000 + +# ---- App ---- +# Game name: only a label for profile listings and the Mac window title; the iOS app name is the game name in the base NSP, and the bundle ID comes from the base game title ID (build_ipa.sh / NSP script) +name Balatro +ipa Balatro.ipa +app_category public.app-category.card-games +# handheld: the Switch build plays by touch there (SDL turns the touch screen into LÖVE touches and mouse presses: tap a card, drag it), which on the iPhone are the touches no +# gamepad control takes, on the Mac the mouse; docked it reads only the pads. Its window is then the 1280x720 handheld display +op_mode handheld +# touch: the virtual gamepad starts in its Tap layout, every touch to the game (the pads' corners would cover Run Info, Options and the deck); its layout button brings back 2D / 3D +pad_layout touch + +# ---- Versions ---- +build v851968 1.1.3 main=294d920907e888814f9040550861fdf5 sdk=f118ae1c2e7728471aaf1a0d58484968 +package v851968 +shader_versions v851968 + +# ---- Files ---- +# load order as on Horizon: main, subsdk*, sdk. 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 +modules main sdk +romfs_marker resources/fonts/NotoSansSC-Bold.ttf +zip_store .ogg:.png:.ttf:.metallib + +# ---- Memory ---- +# The NPDM declares no system resource, so the game has 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 +mem_reserve 0xf0000000 +mem_alias 0x10000000 +mem_stack 0x10000000 +app_pool 0xCD500000 +sys_resource 0x0 + +# ---- Shader split ---- +# no NVN shaders: the NVN tables stay empty (gl_shaders.py writes the GL library and its table, not the profile) +shader_note Balatro has no NVN shaders (its GLSL shaders go through the GL layer) diff --git a/dm/games/0100CD801CE5E000/gl_capture.tsv b/dm/games/0100CD801CE5E000/gl_capture.tsv new file mode 100644 index 0000000..a7d500d --- /dev/null +++ b/dm/games/0100CD801CE5E000/gl_capture.tsv @@ -0,0 +1,38 @@ +# GLSL shaders Balatro puts together at run time (the Mac host's DM_GL_CAPTURE, booting to the title screen): LÖVE's built-in ones and those of the game's 19 resources/shaders files, +# all made by Game:start_up before the first frame; .vert / .frag, key = sha256(GLSL)[:16]. The NSP script's GL shader capture runs until every one here is captured; +# gl_shaders.py translates the capture with the RomFS +025a79df0e26f6ac.frag +0e85b14e40ab14af.frag +153a84611e5a520a.frag +1688bb2aa8983a13.frag +1b304327da2df290.vert +220675a48d6335db.vert +2e65932217333729.vert +3bc7e1b81e471167.vert +47edfdacbd78c512.frag +4b655edbb3470c5a.frag +4c23d6b983ddb7d7.vert +5d7917d7f983df9b.frag +6365d3674b5631c3.vert +71d85f3763912406.frag +84f2c2b173b6f948.vert +8feaf8cecebfafbf.frag +916ab7fdd35c3a95.frag +94552b2eaf21f593.frag +955ac2558c587086.frag +98d3ebf757a82902.vert +9e343d31b09c61b7.vert +9f536d3d12f3edf1.vert +a59000fda1e1e00b.vert +a8aada4f37f45489.frag +ba1a64df36dc291b.frag +bb83fe70011ceebb.frag +bfc21d9532c99682.frag +d28b725d43fc106e.vert +e7f9b54e2d2d7fbb.frag +ed2e2b4a7c901145.vert +f20b9c7c0322241d.frag +f2314b010f695639.vert +f6cfca5c3f6901e4.frag +f8034c074d4e8965.vert +fc956bad781f15af.frag diff --git a/dm/games/0100CD801CE5E000/pso_manifest.tsv b/dm/games/0100CD801CE5E000/pso_manifest.tsv new file mode 100644 index 0000000..f2a9403 --- /dev/null +++ b/dm/games/0100CD801CE5E000/pso_manifest.tsv @@ -0,0 +1 @@ +# Prewarm seed list (shipped in the app, next to shader_map.tsv). Rows: Sshader keystage / Rrender pipeline key (pso_skey hex) / Ccompute shader key. diff --git a/dm/games/0100CD801CE5E000/shader_facts.tsv b/dm/games/0100CD801CE5E000/shader_facts.tsv new file mode 100644 index 0000000..e665d36 --- /dev/null +++ b/dm/games/0100CD801CE5E000/shader_facts.tsv @@ -0,0 +1 @@ +# Answer table for translation queries (generated by dm/gfx/tools/shader_split.py, do not edit): key fact JSON. This game has no PC version to pair with: only the thread group / shared memory of compute shaders (taken from the control section); other queries get the translator defaults diff --git a/dm/games/0100CD801CE5E000/shader_map.tsv b/dm/games/0100CD801CE5E000/shader_map.tsv new file mode 100644 index 0000000..3e9790a --- /dev/null +++ b/dm/games/0100CD801CE5E000/shader_map.tsv @@ -0,0 +1,2 @@ +# kind: evidence Balatro has no NVN shaders: the table stays empty (its GLSL shaders go through the GL layer) +package index stage dm_ctl_sha16 source diff --git a/dm/games/0100CD801CE5E000/shader_map.tsv.conflicts.tsv b/dm/games/0100CD801CE5E000/shader_map.tsv.conflicts.tsv new file mode 100644 index 0000000..35c1b4d --- /dev/null +++ b/dm/games/0100CD801CE5E000/shader_map.tsv.conflicts.tsv @@ -0,0 +1,2 @@ +# kind: evidence control-section key conflict rulings: empty +dm_ctl_sha16 chosen_pc class candidates records diff --git a/dm/games/0100CD801CE5E000/shader_pairs.tsv b/dm/games/0100CD801CE5E000/shader_pairs.tsv new file mode 100644 index 0000000..ac32dd3 --- /dev/null +++ b/dm/games/0100CD801CE5E000/shader_pairs.tsv @@ -0,0 +1 @@ +# kind: evidence vertex / pixel shader pairs within BNSH variations (generated by dm/gfx/tools/shader_split.py, do not edit): vertex key pixel key diff --git a/dm/games/README.md b/dm/games/README.md index aa0edfb..d21eb71 100644 --- a/dm/games/README.md +++ b/dm/games/README.md @@ -7,6 +7,7 @@ Each game the port knows has a folder `dm/games//`, named after the base gam - **`shader_facts_manual.tsv`** (optional): hand-written translation facts in `shader_facts.tsv`'s row format, for what the machine code cannot tell (Link's Awakening: a vertex input the integer-input guess gets wrong; Breath of the Wild and Tears of the Kingdom: integer fragment outputs, which a game without a PC version has no other source for). `shader_split.py` merges it over the generated rows; since the table is on the chain, changing it changes only that profile's chain version. - **`pso_manifest.tsv`**: the prewarm seed list. - **`vk_capture.tsv`** (a Vulkan game that generates its shaders at run time, Super Mario Galaxy): the keys of the SPIR-V modules it generates. +- **`gl_capture.tsv`** (an OpenGL game that puts its GLSL together at run time, Balatro): the files (`.vert` / `.frag`) of the shaders it compiles. - **`bf_want.tsv`**. - **`patches.py`** (optional): the rewriter's patches of the game's own builds (`main`), keyed by NSO build ID: `CUTS` (byte cuts, each original asserted), `OBS` (observation gates) and `FRAME_STEP` (frame-step stubs). `dm/tools/patch_nso.py` loads every profile's file and applies those of a module's build, wherever it comes from; a build claimed twice stops the rewrite. Cuts of Nintendo's `sdk` modules, which several games ship, are in `dm/tools/sdk_patches.py`. A game without patches has no file, and a derived profile never has one. `dm/tools/tests/test_patches.py` checks that a file names only its own profile's builds. @@ -57,7 +58,7 @@ None of them has a default game: without one they stop and list the profiles. To | `device_api` | Optional, decided by the SDK version without it: the NVN API version `major.minor` that `nvnDeviceGetInteger` reports, 55.15 for SDK 11 and later (what Mario requires) and 53.313 for SDK 10 and older (`hle_nvn.c` `nvn_api`). Games check it and skip creating their device on a mismatch: Super Mario 3D World + Bowser's Fury (SDK 10) requires major 53 and minor ≥ 313. No profile sets it. `DM_NVN_API` overrides. Not part of the shader chain version (only `shader_*` keys are) | | `op_mode` | The operation mode the game sees at start: `console` (the default, docked) or `handheld`. Super Mario Galaxy and Captain Toad: Treasure Tracker use `handheld`: docked, their pointer is the Joy-Con gyro, which the host does not have; handheld, it is the touch screen, which the iPhone front end feeds with the touches no gamepad control takes (in the 3D layout, outside the right third of the screen, which is the right stick; `DmGamepad.m`). Handheld also renders at 720p. `DM_OPMODE=0` / `1` overrides, also from `Documents/env.txt` | | `perf_mode` | `follow` (the default): the performance mode follows the operation mode (Boost docked, Normal handheld); `boost`: Boost whatever the operation mode (`hle_oe.c`), a combination a Switch 1 never reports, so a profile choice. Rhythm Heaven Groove plays handheld with Boost: docked means a TV to the game, which then asks for a latency calibration, and only Boost gives its 1920x1080 draw size (handheld Normal draws 1280x720) | -| `pad_layout` | The iPhone virtual gamepad's layout on first launch: `simple` (the default) = 2D (left stick, A/B/X/Y, shoulders, − / +), `full` = 3D (adds a D-pad, a right stick and L3 / R3; the right stick is not drawn: a touch landing in the right third of the screen on no control holds it, centred where it lands). The layout button at the top centre switches between them in game and the app remembers the player's choice (`DmGamepad.m`) | +| `pad_layout` | The iPhone virtual gamepad's layout on first launch: `simple` (the default) = 2D (left stick, A/B/X/Y, shoulders, − / +), `full` = 3D (adds a D-pad, a right stick and L3 / R3; the right stick is not drawn: a touch landing in the right third of the screen on no control holds it, centred where it lands), `touch` = Tap (no control but the layout button: every touch goes to the game's touch screen; needs `op_mode handheld`, Balatro). The layout button at the top centre goes to the next layout in game (Tap only in handheld mode) and the app remembers the player's choice (`DmGamepad.m`) | | `feedback_snap` | Fragment shader keys (prefixes) whose draws sample a colour attachment of their own render pass and must read a snapshot instead (`fb_prepare` in `nvn_mtl.m`). Writing and sampling one attachment in a render pass is undefined in Metal: the Mac usually happens to read the old content, the iPhone sometimes reads whole 16×16 blocks as magenta. Each such draw costs a pass split and a copy, so only the shaders seen to break are listed (Super Mario 3D World + Bowser's Fury: its final composite; Captain Toad: Treasure Tracker: the same agl program, whose blocks showed on the Mac too). `DM_NVN_FB_SNAP=0` / `all` / `` overrides, also from `Documents/env.txt` | | `alpha_from_red` | Fragment shader keys (prefixes, up to 48) that read a single-channel render target whose channel mapping is exactly (R, 0, 0, 1) as (R, 0, 0, R) (`tex_alpha_red` in `nvn_mtl.m`). It was a workaround for NintendoWare layout captures: the game rebuilds each capture with its own mapping ((1, 1, 1, R) for one tagged `A8`), and the textures kept (R, 0, 0, 1) only while the host's `nvnTextureCompare` ignored the mapping (fixed, see [04](../../docs/agents/04-graphics.md)). Breath of the Wild no longer lists it; Super Mario Odyssey still lists its 26 layout shaders. `DM_NVN_ALPHA_RED=0` / `` overrides, also from `Documents/env.txt` | | `tex_flush_upload` | `on` / `off` (the default): a texture the game fills from the CPU in place (no packaged layout, no `WriteTexels`) and publishes with `nvnMemoryPoolFlushMappedRange` is uploaded from guest memory like a packaged one, and re-uploaded when its pages change (`ft_add` in `hle_nvn.c`). Tears of the Kingdom streams all its textures this way; without it the 3D scene is black. Off by default because a flushed range in another game could cover an image a shader writes, whose GPU content the upload would replace. Without the key, every game still gets a texture's storage once when the game flushed it before the texture's first use (`nvn_flushed` in `hle_nvn.c`: Breath of the Wild's sky tables); the key is for textures the CPU writes again after that | @@ -73,7 +74,7 @@ None of them has a default game: without one they stop and list the profiles. To These are not profile keys: the tools read them from the game files each time, by rules checked against every profile (`dm/games/profile_guess.py` `shader_sources`). - **Graphics API.** A game whose `main` imports Vulkan functions and not `nvnBootstrapLoader` renders with Vulkan, which the host implements on Metal (`dm/gfx/vk/hle_vk.m`; Hades, Super Mario Galaxy). Its shaders are its SPIR-V files, translated by `dm/gfx/tools/vk_shaders.py [--ios ]` into `/vk/`, and `build_ipa.sh` ships `$MSL_IOS/vk` instead of the NVN chain libraries (`GFXAPI`, which the NSP script passes; by hand the shader libraries there tell). The NVN shader tables stay empty. -- **SPIR-V generated at run time.** A Vulkan game without SPIR-V files in its RomFS generates its shaders at run time (Super Mario Galaxy: Hagi's GX shaders, 3,089 modules created from its pipeline database while it shows "Initializing system..."). The NSP script captures them on the Mac with `DM_VK_CAPTURE=` (`dm/gfx/vk/hle_vk.m`), booting the game in the Mac host until every key the profile's `vk_capture.tsv` lists is there, and `vk_shaders.py` translates the capture folder with the RomFS. +- **SPIR-V generated at run time.** A Vulkan game without SPIR-V files in its RomFS generates its shaders at run time (Super Mario Galaxy: Hagi's GX shaders, 3,089 modules created from its pipeline database while it shows "Initializing system..."). The NSP script captures them on the Mac with `DM_VK_CAPTURE=` (`dm/gfx/vk/hle_vk.m`), booting the game in the Mac host until every key the profile's `vk_capture.tsv` lists is there, and `vk_shaders.py` translates the capture folder with the RomFS. An OpenGL game with no GLSL source in its RomFS or `main` (Balatro: LÖVE puts its shaders together in Lua) is captured the same way with `DM_GL_CAPTURE=` (`dm/gfx/gl/hle_gl.m`) until every file in `gl_capture.tsv` is there, and `gl_shaders.py` translates the capture. - **Shaders compiled at run time.** A game whose `main` holds GLSL sources and calls `glslcCompile` in a module it loads compiles some shaders at run time with its own GLSLC (Fire Emblem: Three Houses: 9 built-in KTGL shaders at boot), so no game file holds them. The NSP script captures them on the Mac with `DM_GLSLC_CAPTURE=` (`dm/host/hle_glslc.c`), booting the game in the Mac host until every capture file the profile's `shader_map.tsv` names is there, and `shader_split.py` reads `/glslc_capture/*.glslc` (beside that version's `RomFS`) or `$DM_GLSLC_CAPTURE`; without the folder it prints a red line, those shaders get no translation and the tables differ from the profile's. - **Unreal paks.** An Unreal Engine game keeps its shaders in the shader code archives of its paks (`*.ushaderbytecode`; a pak is Unreal's when its footer has Unreal's magic, `uepak.py` `unreal_paks`), each shader Oodle-compressed, which only the game's own Oodle decodes. The NSP script (`decode_ue_shaders`) takes the archives out of the paks with `dm/gfx/tools/uepak.py` (the pak index is AES-encrypted with a key it finds in `main`), boots the game in the Mac host with `DM_OODLE_JOB` (`dm/host/hle_oodle.c`: the game's `OodleLZ_Decompress`, found as the one function that references the library's own name string, decodes every shader right before `nnMain`, then the host exits) and writes the decoded archives to `/ue_shaders`, where `shader_split.py` reads them (`DM_UE_SHADERS` overrides; without them it stops). Octopath Traveler 0: 10,596 shaders in 2 s. Unreal Engine 4.26 and later compress shaders with LZ4 unless the project picks another format (DRAGON QUEST VII Reimagined, 4.27: 64,949 shaders, its packages in IoStore containers beside the pak); `uepak.py` decodes those itself (`lz4_shaders`) and the NSP script skips the host run. - **Trinity packs.** Game Freak's Trinity engine keeps nearly every file in Oodle-compressed packs (`arc/data.trpfs`, files starting `ONEPACK`; Pokémon Legends: Z-A). `shader_split.py` decodes them with the game's own `OodleLZ_Decompress`, run by the Mac host in its tool mode (`dm/host/hle_oodle.c`, `DM_OODLE_SERVE`). The split needs `DM_OODLE_HOST` (the Mac host) and `DM_OODLE_MODULES` (the rewritten modules); the NSP script builds the host and sets both when the split runs. diff --git a/dm/games/game.py b/dm/games/game.py index e3dddbf..cfbbe48 100644 --- a/dm/games/game.py +++ b/dm/games/game.py @@ -101,8 +101,8 @@ def header(g): if aba not in ('on', 'off'): sys.exit(f'{g.id}: alloc_big_align must be on or off: {aba}') sf = g.get('shared_font', 'bfttf') # what the shared-font address points at (hle_sdk16.c): bfttf = 8-byte header + TTF, ttf = the TTF alone if sf not in ('bfttf', 'ttf'): sys.exit(f'{g.id}: shared_font must be bfttf or ttf: {sf}') - pl = g.get('pad_layout', 'simple') # the virtual gamepad's first layout (DmGamepad.m; afterwards whatever the player switched to in game is kept): simple = 2D, full = 3D (D-pad, right stick, L3 / R3) - if pl not in ('simple', 'full'): sys.exit(f'{g.id}: pad_layout must be simple or full: {pl}') + pl = g.get('pad_layout', 'simple') # the virtual gamepad's first layout (DmGamepad.m; afterwards whatever the player switched to in game is kept): simple = 2D, full = 3D (D-pad, right stick, L3 / R3), + if pl not in ('simple', 'full', 'touch'): sys.exit(f'{g.id}: pad_layout must be simple, full or touch: {pl}') # touch = Tap (no control: every touch goes to the game's touch screen) fbs = g.words('feedback_snap', '') # fragment shader key prefixes whose same-pass feedback sampling gets a snapshot first (nvn_mtl.m fb_prepare); none by default if any(not re.fullmatch(r'[0-9a-f]{8,16}', k) for k in fbs): sys.exit(f'{g.id}: feedback_snap takes fragment shader keys (8-16 lowercase hex digits): {fbs}') afr = g.words('alpha_from_red', '') # fragment shader key prefixes that read single-channel textures mapped (R, 0, 0, 1) with alpha = red (nvn_mtl.m tex_alpha_red); none by default @@ -123,6 +123,7 @@ def header(g): if gts not in ('submission', 'segment'): sys.exit(f'{g.id}: gpu_timestamps must be submission or segment: {gts}') om = g.get('op_mode', 'console') # the operation mode reported at start (hle_oe.c dm_opmode; DM_OPMODE overrides): console = docked, handheld = touch screen in use (Super Mario Galaxy's pointer) if om not in ('console', 'handheld'): sys.exit(f'{g.id}: op_mode must be console or handheld: {om}') + if pl == 'touch' and om != 'handheld': sys.exit(f'{g.id}: pad_layout touch needs op_mode handheld (the touch screen reaches the game only in handheld mode)') pm = g.get('perf_mode', 'follow') # the performance mode reported (hle_oe.c): follow = the operation mode's (Boost docked, Normal handheld), boost = Boost always if pm not in ('follow', 'boost'): sys.exit(f'{g.id}: perf_mode must be follow or boost: {pm}') api = g.get('device_api', '55.15') # the NVN API major.minor version nvnDeviceGetInteger reports (hle_nvn.c) @@ -152,7 +153,7 @@ def header(g): f'#define DM_GAME_FPS_TARGET {int(ft)}', f'#define DM_GAME_FPS_HOLD {int(fh == "on")}', *([f'#define DM_GAME_SHARED_FONT_TTF {int(sf == "ttf")}'] if g.get('shared_font', '') else []), # without the key the host decides by the SDK version (hle_sdk16.c) - f'#define DM_GAME_PAD_FULL {int(pl == "full")}', + f'#define DM_GAME_PAD_LAYOUT {("simple", "full", "touch").index(pl)}', f'#define DM_GAME_OPMODE {int(om == "console")}', f'#define DM_GAME_PERF_BOOST {int(pm == "boost")}', f'#define DM_GAME_FEEDBACK_SNAP {q(" ".join(fbs))}', diff --git a/dm/games/profile_guess.py b/dm/games/profile_guess.py index 09e67e0..b9c6ee7 100644 --- a/dm/games/profile_guess.py +++ b/dm/games/profile_guess.py @@ -24,6 +24,8 @@ MARKERS = [(b'nvnBootstrapLoader', 'NVN'), (b'vkCreateInstance', 'Vulkan'), (b'e (b'sead::', 'Nintendo sead'), (b'agl::', 'Nintendo agl'), (b'gsys::', 'Nintendo gsys'), (b'Unreal', 'Unreal'), (b'UE4', 'Unreal 4'), (b'Havok', 'Havok'), (b'CRIWARE', 'CRIWARE'), (b'Wwise', 'Wwise'), (b'MonoGame.Framework', 'MonoGame'), (b'FMOD mixer', 'FMOD')] GLSLC_MAGIC = b'\x91\x68\x86\x19' # GLSLC compiler outputs (shader_split.py glslc_shaders) +GL_SRC = rb'\0#version [0-9]' # a GLSL source string as dm/gfx/tools/gl_shaders.py takes it from main (Balatro's main has none, only glslang's messages that start with '#version:') +GL_FILES = ('.mvgl', '.vert', '.frag') # the RomFS files gl_shaders.py reads GLSL from APP_POOL = 0xCD500000 # the application memory pool of a 4 GB Switch (3285 MiB), what every profile but Engage's reports MiB = 1 << 20 @@ -70,7 +72,7 @@ def npdm(path): # (system resource size, program ID) from main.npdm; a non-zer def facts_of(exefs, romfs): """What the profile rules (and the survey report) need from a game's code modules and RomFS""" exefs = Path(exefs) - F = {'mods': {}, 'markers': {}, 'sdkver': set(), 'comps': set(), 'unity': set(), 'glslc': 0, 'glsl_src': 0, 'gl_names': 0, 'sysres': 0, 'program': None} + F = {'mods': {}, 'markers': {}, 'sdkver': set(), 'comps': set(), 'unity': set(), 'glslc': 0, 'glsl_src': 0, 'gl_names': 0, 'gl_src': 0, 'sysres': 0, 'program': None} for p in sorted(exefs.iterdir()): with open(p, 'rb') as f: if f.read(4) != b'NSO0': continue @@ -87,6 +89,7 @@ def facts_of(exefs, romfs): if p.name == 'main': F['unity'] = {v.decode() for v in re.findall(rb'20[12][0-9]\.[0-9]+\.[0-9]+f[0-9]', img)} F['glslc'], F['glsl_src'], F['gl_names'] = img.count(GLSLC_MAGIC), img.count(b'#version '), img.count(b'glShaderSource') + F['gl_src'] = len(re.findall(GL_SRC, img)) F['main_markers'] = {name for pat, name in MARKERS if pat in img} m['kind'] = {'main': 'the game', 'sdk': 'nnSdk', 'rtld': 'rtld'}.get(p.name) or kind(m) F['sysres'], F['program'] = npdm(exefs / 'main.npdm') @@ -167,20 +170,23 @@ GPU_DRIVERS = {'NintendoSDK_OpenGL', 'NintendoSDK_Vulkan'} # ---- Shader sources: decided from the game files at every build, not kept in the profile (scripts/build_ipa_from_nsp.py; shader_split.py reads what those steps write) ---- -def shader_sources(imports, glsl_src, glslc_mods, spirv, unreal_paks, trinity_packs=0, legacy_paks=0, gl_names=0): +def shader_sources(imports, glsl_src, glslc_mods, spirv, unreal_paks, trinity_packs=0, legacy_paks=0, gl_names=0, gl_src=0, gl_files=0): """{name: (value, reason)}: graphics (vulkan when main imports Vulkan functions and not nvnBootstrapLoader; gl when it imports eglCreateWindowSurface and names glShaderSource, so it compiles - GLSL through OpenGL (gl_names: how often main has that name; Absolum imports the same EGL functions for SDL3 but renders with NVN and names no GL function); else nvn), vk_capture (a Vulkan game without SPIR-V files generates its shaders at - run time), glslc_capture (main holds GLSL sources and calls glslcCompile in a module the game loads: it compiles some shaders at run time), ue_decode (Unreal paks keep the shaders Oodle-compressed + GLSL through OpenGL (gl_names: how often main has that name; Absolum imports the same EGL functions for SDL3 but renders with NVN and names no GL function); else nvn; vkGetInstanceProcAddr + alone is SDL's Vulkan loader, which Balatro's LÖVE links and never uses), vk_capture (a Vulkan game without SPIR-V files generates its shaders at run time), gl_capture (an OpenGL game with no + GLSL source where gl_shaders.py looks, gl_src strings in main and gl_files files in the RomFS: it puts its shaders together at run time, LÖVE from its Lua), glslc_capture (main holds GLSL sources and calls glslcCompile in a module the game loads: it compiles some shaders at run time), ue_decode (Unreal paks keep the shaders Oodle-compressed in their shader code archives, which only the game's own Oodle decodes), oodle_packs (Trinity packs, Oodle-compressed file by file, which the shader split decodes with the game's own decoder in the Mac host's tool mode), ue_legacy (legacy Unreal paks, version 4: the shaders are inline in the cooked packages, which the shader split reads through the game's own decoder the same way). unreal_paks counts the version 11 paks, legacy_paks the older ones. Checked against every profile: Hades and Super Mario Galaxy Vulkan, Galaxy alone with no SPIR-V file, Three Houses alone compiling shaders (13 Sentinels holds 106 GLSL sources but does not import glslcCompile; Captain Toad: Treasure Tracker imports it with agl's GLSL sources, but no module defines it), Octopath Traveler 0 - alone with version 11 Unreal paks, DRAGON QUEST XI S alone with legacy ones, Pokémon Legends: Z-A alone with Trinity packs, Digimon Story Cyber Sleuth alone with OpenGL (of the 22 games extracted)""" - vk = sum(1 for s in imports if re.match(r'vk[A-Z]', s)); vulkan = bool(vk) and 'nvnBootstrapLoader' not in imports + alone with version 11 Unreal paks, DRAGON QUEST XI S alone with legacy ones, Pokémon Legends: Z-A alone with Trinity packs, Digimon Story Cyber Sleuth and Balatro alone with OpenGL (of the 23 games + extracted; Hades and both Galaxies import 99 or 100 Vulkan functions, Balatro only vkGetInstanceProcAddr), Balatro alone composing its GLSL""" + vk = sum(1 for s in imports if re.match(r'vk[A-Z]', s) and s != 'vkGetInstanceProcAddr'); vulkan = bool(vk) and 'nvnBootstrapLoader' not in imports gl = not vulkan and 'eglCreateWindowSurface' in imports and bool(gl_names) return {'graphics': ('vulkan', f'main imports {vk} Vulkan functions and not nvnBootstrapLoader: the host\'s Vulkan layer (dm/gfx/vk/hle_vk.m)') if vulkan else ('gl', 'main imports EGL and names glShaderSource: it renders with OpenGL and compiles GLSL, the host\'s GL layer (dm/gfx/gl/hle_gl.m)') if gl else ('nvn', 'main imports nvnBootstrapLoader'), 'vk_capture': (vulkan and not spirv, 'no SPIR-V file in the RomFS: the game generates its shaders at run time (Super Mario Galaxy), captured by booting it on the Mac'), + 'gl_capture': (gl and not gl_src and not gl_files, 'no GLSL source in the RomFS or main: the game puts its shaders together at run time (LÖVE: Balatro), captured by booting it on the Mac'), 'glslc_capture': (not vulkan and not gl and bool(glsl_src) and 'glslcCompile' in imports and bool(glslc_mods), f'main holds {glsl_src} GLSL sources and calls glslcCompile in {", ".join(glslc_mods) or "no module it loads"}: shaders compiled at run time (Fire Emblem: Three Houses), captured by booting it on the Mac'), 'ue_decode': (not vulkan and not gl and bool(unreal_paks), f'{unreal_paks} Unreal pak(s): the shaders are Oodle-compressed in their shader code archives (Octopath Traveler 0), decoded by booting the game on the Mac'), @@ -191,7 +197,7 @@ def shader_sources(imports, glsl_src, glslc_mods, spirv, unreal_paks, trinity_pa def build_sources(exefs, romfs, modules): """shader_sources from an extraction (its ExeFS and RomFS folders) and the modules the profile loads""" exefs = Path(exefs); main = nso(exefs / 'main')[0] - imports = {s for s, defined, _ in nso_syms.dynsyms(main) if not defined}; glsl_src = main.count(b'#version '); gl_names = main.count(b'glShaderSource') + imports = {s for s, defined, _ in nso_syms.dynsyms(main) if not defined}; glsl_src = main.count(b'#version '); gl_names = main.count(b'glShaderSource'); gl_src = len(re.findall(GL_SRC, main)) glslc_mods = [m for m in modules if m != 'main' and glsl_src and 'glslcCompile' in imports and (exefs / m).exists() and any(s == 'glslcCompile' and defined for s, defined, _ in nso_syms.dynsyms(nso(exefs / m)[0]))] spirv = 0 @@ -203,7 +209,8 @@ def build_sources(exefs, romfs, modules): for n in names: if n.endswith('.trpfs'): with open(os.path.join(d, n), 'rb') as f: trinity += f.read(8) == b'ONEPACK\0' - return shader_sources(imports, glsl_src, glslc_mods, spirv, len(uepak.unreal_paks(str(romfs), legacy=False)), trinity, len(uepak.unreal_paks(str(romfs), legacy=True)), gl_names) + gl_files = sum(1 for _, _, names in os.walk(romfs) for n in names if n.lower().endswith(GL_FILES)) if 'eglCreateWindowSurface' in imports else 0 + return shader_sources(imports, glsl_src, glslc_mods, spirv, len(uepak.unreal_paks(str(romfs), legacy=False)), trinity, len(uepak.unreal_paks(str(romfs), legacy=True)), gl_names, gl_src, gl_files) def va_heap(sysres, imports): # virtual address memory (an NPDM system resource) and a heap: the alias / heap split is a starting point the heap check corrects (Wonder, Tears of the Kingdom; Engage's pool is its own measurement) diff --git a/dm/gfx/README.md b/dm/gfx/README.md index 1422e54..cc84244 100644 --- a/dm/gfx/README.md +++ b/dm/gfx/README.md @@ -81,7 +81,7 @@ Binding conventions followed during translation (the NVN driver's layout, the sa | `nvn/names.txt` | The NVN API name list. The per-game tables described above live in the game profile `dm/games//` (named after the base game's title ID; tools pick it with `DM_GAME`, make with `GAME`; there is no default game) | | `tex/` | Texture format mapping, deswizzling, BC decompression, see [tex/README.md](tex/README.md) | | `vk/hle_vk.m` | Vulkan on Metal, for games that render with Vulkan (Hades, Super Mario Galaxy); their SPIR-V shaders are translated by `tools/vk_shaders.py` | -| `gl/hle_gl.m` | OpenGL through EGL on Metal, for games that render with NVIDIA's GL driver (Digimon Story Cyber Sleuth: Complete Edition); their GLSL sources are translated by `tools/gl_shaders.py` (glslang, then SPIRV-Cross) | +| `gl/hle_gl.m` | OpenGL through EGL on Metal, for games that render with NVIDIA's GL driver (Digimon Story Cyber Sleuth: Complete Edition; Balatro, LÖVE on its core-profile paths); their GLSL sources are translated by `tools/gl_shaders.py` (glslang, then SPIRV-Cross) | | `metal/` | A standalone minimal Metal offscreen example, not compiled into the host, see [metal/README.md](metal/README.md) | | `tools/` | Offline tools, see below | | `tests/` | Unit tests | diff --git a/dm/gfx/gl/hle_gl.m b/dm/gfx/gl/hle_gl.m index 9c4e24e..112ba17 100644 --- a/dm/gfx/gl/hle_gl.m +++ b/dm/gfx/gl/hle_gl.m @@ -1,6 +1,9 @@ // OpenGL → Metal layer for games that render with OpenGL through EGL (Digimon Story Cyber Sleuth: Complete Edition: Media.Vision's MVGL engine on NVIDIA's desktop GL 4.5 core -// driver, subsdk0, which the host does not load). main imports the EGL functions, which bind to the ones here through the host's import table (hle.h swhle); it gets every GL -// function through eglGetProcAddress (a glad loader asks for 2,956 names), and its code calls 79 of them. Only those are implemented; every other name gets a null pointer. +// driver, subsdk0, which the host does not load; Balatro: LÖVE 11.4 on SDL, the same driver). main imports the EGL functions, which bind to the ones here through the host's import +// table (hle.h swhle); it gets every GL function through eglGetProcAddress (a glad loader asks for 2,956 names; Digimon's code calls 79 of them, LÖVE's about 150, counted with +// dm/tools/loader_census.py). Only those are implemented; every other name gets a null pointer (DM_GL_STUBS=1: a stub that reports its first call). +// LÖVE takes the core profile's paths (GL_CONTEXT_PROFILE_MASK): its vertex streams are persistently mapped buffers (glBufferStorage, glMapBufferRange) that fences +// (glFenceSync / glClientWaitSync) keep the CPU from rewriting while a frame reads them; with a compatibility profile it would draw from client memory, which Metal cannot. // Model: // · one context, used from the thread that made it current; GL calls are encoded into Metal as they come: one MTLCommandBuffer per frame on this layer's own queue, // committed at eglSwapBuffers (signalling g_ev with its serial for the present path), a render encoder per stretch of draws into one framebuffer; @@ -34,7 +37,7 @@ void dm_vi_layer_size(int *w, int *h); // hle_vi.c: the size of the last sized static int g_log; #define OLOG(...) do { if (g_log) GLOG(__VA_ARGS__); } while (0) -typedef uint32_t GLenum, GLuint, GLbitfield; typedef int32_t GLint, GLsizei; typedef float GLfloat; typedef double GLdouble; typedef char GLchar; typedef intptr_t GLsizeiptr; +typedef uint32_t GLenum, GLuint, GLbitfield; typedef int32_t GLint, GLsizei; typedef float GLfloat; typedef double GLdouble; typedef char GLchar; typedef intptr_t GLsizeiptr, GLintptr; typedef void *EGLDisplay, *EGLConfig, *EGLContext, *EGLSurface; typedef int32_t EGLint; typedef uint32_t EGLBoolean, EGLenum; // GL: one GL function, registered in this layer's table (eglGetProcAddress looks names up there); GLB: the same, run on the thread's big host stack (hle_bigstack.c, register @@ -102,7 +105,19 @@ enum { GL_COMPRESSED_RGBA_BPTC_UNORM = 0x8e8c, GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM, GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT, GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT, GL_COMPRESSED_RGBA_ASTC_4x4 = 0x93b0, GL_COMPRESSED_SRGB8_ALPHA8_ASTC_4x4 = 0x93d0, GL_DEPTH_STENCIL_ATTACHMENT = 0x821a, + GL_CW = 0x900, GL_CCW, GL_POINT_SIZE = 0xb11, GL_POINT_SIZE_RANGE, GL_CULL_FACE_MODE = 0xb45, GL_FRONT_FACE, GL_SCISSOR_BOX = 0xc10, GL_PACK_ALIGNMENT = 0xd05, + GL_MAX_VIEWPORT_DIMS = 0xd3a, GL_COLOR = 0x1800, GL_DEPTH, GL_STENCIL, GL_ZERO = 0, GL_ONE = 1, + GL_TEXTURE_BINDING_3D = 0x806a, GL_TEXTURE_3D = 0x806f, GL_MAX_3D_TEXTURE_SIZE = 0x8073, GL_SAMPLE_BUFFERS = 0x80a8, GL_SAMPLES, GL_CONTEXT_FLAGS = 0x821e, + GL_ALIASED_POINT_SIZE_RANGE = 0x846d, GL_TEXTURE_BINDING_CUBE_MAP = 0x8514, GL_MAX_CUBE_MAP_TEXTURE_SIZE = 0x851c, GL_MAX_DRAW_BUFFERS = 0x8824, GL_MAX_ARRAY_TEXTURE_LAYERS = 0x88ff, + GL_FLOAT_VEC2 = 0x8b50, GL_FLOAT_VEC3, GL_FLOAT_VEC4, GL_INT_VEC2, GL_INT_VEC3, GL_INT_VEC4, GL_BOOL, GL_FLOAT_MAT2 = 0x8b5a, GL_FLOAT_MAT3, GL_FLOAT_MAT4, GL_SAMPLER_2D = 0x8b5e, + GL_SAMPLER_3D, GL_SAMPLER_CUBE, GL_SAMPLER_2D_SHADOW = 0x8b62, GL_TEXTURE_2D_ARRAY = 0x8c1a, GL_TEXTURE_BINDING_2D_ARRAY = 0x8c1d, + GL_READ_FRAMEBUFFER = 0x8ca8, GL_DRAW_FRAMEBUFFER, GL_READ_FRAMEBUFFER_BINDING, GL_RENDERBUFFER_SAMPLES, GL_FRAMEBUFFER_COMPLETE = 0x8cd5, GL_MAX_COLOR_ATTACHMENTS = 0x8cdf, + GL_RENDERBUFFER_WIDTH = 0x8d42, GL_RENDERBUFFER_HEIGHT, GL_RENDERBUFFER_INTERNAL_FORMAT, GL_FRAMEBUFFER_SRGB = 0x8db9, GL_SAMPLER_2D_ARRAY = 0x8dc1, + GL_UNSIGNED_INT_VEC2 = 0x8dc6, GL_UNSIGNED_INT_VEC3, GL_UNSIGNED_INT_VEC4, GL_TEXTURE_SWIZZLE_R = 0x8e42, GL_TEXTURE_SWIZZLE_A = 0x8e45, + GL_ALREADY_SIGNALED = 0x911a, GL_TIMEOUT_EXPIRED, GL_CONDITION_SATISFIED, GL_CONTEXT_PROFILE_MASK = 0x9126, }; +#define GL_MAP_INVALIDATE_BUFFER_BIT 0x8 +#define GL_SYNC_FLUSH_COMMANDS_BIT 0x1 #define GL_COLOR_BUFFER_BIT 0x4000 #define GL_DEPTH_BUFFER_BIT 0x100 #define GL_STENCIL_BUFFER_BIT 0x400 @@ -115,8 +130,7 @@ static void *zalloc(size_t n) { void *p = calloc(1, n); if (!p) die("[gl] out of static id g_dev; static id g_q; static id g_ev; // signalled with a command buffer's serial at its end -static id g_attr0; // (0, 0, 0, 1): the value of a vertex input whose array is disabled (GL's default current attribute) -static id g_tex0, g_cube0; // bound where a sampler's unit has no texture (or an unusable one): opaque black, as GL samples an incomplete texture +static id g_tex0, g_cube0, g_tex3d0, g_arr0; // bound where a sampler's unit has no texture (or an unusable one): opaque black, as GL samples an incomplete texture static id g_samp0; static id g_lib, g_util; // the game's shaders; the layer's own (clears) static uint64_t g_serial = 1; // serial of the command buffer being recorded (g_cb); committed ones have lower serials @@ -126,10 +140,17 @@ static id g_enc; static _Atomic uint64_t g_n_draw, g_n_pass, g_n_up, g_n_blit, g_n_pso, g_n_cb; // ---------- Shader table (/gl/gl_shaders.tsv) ---------- -enum { T_FLOAT, T_VEC2, T_VEC3, T_VEC4, T_INT, T_IVEC2, T_IVEC3, T_IVEC4, T_MAT2, T_MAT3, T_MAT4, T_SAMP2D, T_SAMPCUBE, T_OTHER }; -static const char *const g_tnames[] = {"float", "vec2", "vec3", "vec4", "int", "ivec2", "ivec3", "ivec4", "mat2", "mat3", "mat4", "sampler2D", "samplerCube"}; +enum { T_FLOAT, T_VEC2, T_VEC3, T_VEC4, T_INT, T_IVEC2, T_IVEC3, T_IVEC4, T_UINT, T_UVEC2, T_UVEC3, T_UVEC4, T_BOOL, T_MAT2, T_MAT3, T_MAT4, + T_SAMP2D, T_SAMPCUBE, T_SAMP3D, T_SAMP2DARR, T_SAMP2DSH, T_OTHER }; +static const char *const g_tnames[] = {"float", "vec2", "vec3", "vec4", "int", "ivec2", "ivec3", "ivec4", "uint", "uvec2", "uvec3", "uvec4", "bool", "mat2", "mat3", "mat4", + "sampler2D", "samplerCube", "sampler3D", "sampler2DArray", "sampler2DShadow"}; +static const GLenum g_tenums[] = {GL_FLOAT, GL_FLOAT_VEC2, GL_FLOAT_VEC3, GL_FLOAT_VEC4, GL_INT, GL_INT_VEC2, GL_INT_VEC3, GL_INT_VEC4, GL_UNSIGNED_INT, GL_UNSIGNED_INT_VEC2, + GL_UNSIGNED_INT_VEC3, GL_UNSIGNED_INT_VEC4, GL_BOOL, GL_FLOAT_MAT2, GL_FLOAT_MAT3, GL_FLOAT_MAT4, GL_SAMPLER_2D, GL_SAMPLER_CUBE, GL_SAMPLER_3D, + GL_SAMPLER_2D_ARRAY, GL_SAMPLER_2D_SHADOW, GL_FLOAT}; // glGetActiveUniform's types static int tcode(const char *s) { for (int i = 0; i < T_OTHER; i++) if (!strcmp(s, g_tnames[i])) return i; return T_OTHER; } -static int tcomps(int t) { static const int c[] = {1, 2, 3, 4, 1, 2, 3, 4, 4, 9, 16}; return t < T_SAMP2D ? c[t] : 1; } +static int tcomps(int t) { static const int c[] = {1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 4, 9, 16}; return t < T_SAMP2D ? c[t] : 1; } +static int is_samp(int t) { return t >= T_SAMP2D && t < T_OTHER; } +static int is_intt(int t) { return t >= T_INT && t <= T_BOOL; } typedef struct { char name[48]; uint8_t slot, type; } shattr; // a vertex input typedef struct { char name[48]; uint16_t off, count, stride; uint8_t type; } shuni; // a default block member (count 0 = not an array) typedef struct { char name[48]; uint8_t slot, type; } shsamp; // a combined sampler: texture and sampler slot @@ -186,8 +207,6 @@ static void dev_init_once(void) { g_dev = (__bridge id)mtl_device(); g_q = [g_dev newCommandQueueWithMaxCommandBufferCount:64]; g_q.label = @"gl"; g_ev = [g_dev newSharedEvent]; - const float a0[4] = {0, 0, 0, 1}; - g_attr0 = [g_dev newBufferWithBytes:a0 length:sizeof a0 options:MTLResourceStorageModeShared]; MTLTextureDescriptor *td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:1 height:1 mipmapped:NO]; const uint32_t black = 0xff000000u, blk6[6] = {black, black, black, black, black, black}; g_tex0 = [g_dev newTextureWithDescriptor:td]; @@ -195,6 +214,11 @@ static void dev_init_once(void) { td = [MTLTextureDescriptor textureCubeDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm size:1 mipmapped:NO]; g_cube0 = [g_dev newTextureWithDescriptor:td]; for (int f = 0; f < 6; f++) [g_cube0 replaceRegion:MTLRegionMake2D(0, 0, 1, 1) mipmapLevel:0 slice:f withBytes:&blk6[f] bytesPerRow:4 bytesPerImage:4]; + td = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm width:1 height:1 mipmapped:NO]; + td.textureType = MTLTextureType3D; g_tex3d0 = [g_dev newTextureWithDescriptor:td]; + [g_tex3d0 replaceRegion:MTLRegionMake3D(0, 0, 0, 1, 1, 1) mipmapLevel:0 slice:0 withBytes:&black bytesPerRow:4 bytesPerImage:4]; + td.textureType = MTLTextureType2DArray; g_arr0 = [g_dev newTextureWithDescriptor:td]; + [g_arr0 replaceRegion:MTLRegionMake2D(0, 0, 1, 1) mipmapLevel:0 slice:0 withBytes:&black bytesPerRow:4 bytesPerImage:0]; g_samp0 = [g_dev newSamplerStateWithDescriptor:[MTLSamplerDescriptor new]]; NSError *err = nil; g_util = [g_dev newLibraryWithSource:@(g_util_src) options:nil error:&err]; @@ -268,18 +292,20 @@ static void *name_at(void ***tab, uint32_t *n, uint32_t *cap, uint32_t k, size_t return (*tab)[k]; } -typedef struct { __strong id mb; size_t size; uint64_t use; } buf_t; // use: serial of the last command buffer that read it +// use: serial of the last command buffer that read it; immutable: made by glBufferStorage (LÖVE's persistently mapped streams), never orphaned, the game keeps a pointer into it +typedef struct { __strong id mb; size_t size; uint64_t use; int immutable; } buf_t; NAMES(buf_t, g_bufs, g_nbufs, g_capbufs) static buf_t *buf_of(GLuint n) { return n && n < g_nbufs ? g_bufs[n] : NULL; } -// A texture: target, format, levels given so far (pend[face][level], until the Metal texture is made), sampler parameters +// A texture: target, format, levels given so far (pend[face][level], until the Metal texture is made), sampler parameters. d: depth (3D) or layers (2D array); +// slevels: the level count glTexStorage gave (LÖVE allocates every level up front); swz: GL_TEXTURE_SWIZZLE_R..A (LÖVE's LA8 is RG8 read as R, R, R, G in a core profile) #define MAXLV 16 typedef struct { void *data; size_t pitch, size; uint32_t w, h; } level_t; typedef struct { - GLenum target; int made, fmt, levels; uint32_t w, h; uint64_t use, gen; + GLenum target; int made, fmt, levels, slevels; uint32_t w, h, d; uint64_t use, gen; level_t pend[6][MAXLV]; int defined; // defined: bit per level whose face(s) were given __strong id tex; __strong id smp; - GLint minf, magf, ws, wt, wr, maxlv, cmp; float aniso, border[4]; int smp_dirty; + GLint minf, magf, ws, wt, wr, maxlv, cmp, swz[4]; float aniso, border[4]; int smp_dirty; } tex_t; NAMES(tex_t, g_texs, g_ntexs, g_captexs) static tex_t *tex_of(GLuint n) { return n && n < g_ntexs ? g_texs[n] : NULL; } @@ -288,24 +314,28 @@ typedef struct { int fmt; uint32_t w, h; __strong id tex; uint64_t g NAMES(rb_t, g_rbs, g_nrbs, g_caprbs) static rb_t *rb_of(GLuint n) { return n && n < g_nrbs ? g_rbs[n] : NULL; } -typedef struct { int kind; GLuint name; int level, face; } att_t; // kind 0 none, 1 texture, 2 renderbuffer +typedef struct { int kind; GLuint name; int level, face; } att_t; // kind 0 none, 1 texture, 2 renderbuffer; face: a cube face, an array layer or a 3D slice typedef struct { att_t col, depth, stencil; uint64_t gen; } fb_t; NAMES(fb_t, g_fbs, g_nfbs, g_capfbs) static fb_t *fb_of(GLuint n) { return n && n < g_nfbs ? g_fbs[n] : NULL; } -typedef struct { uint8_t on, size, norm; GLenum type; GLsizei stride; uint64_t off; GLuint buf; } vattr_t; +typedef struct { uint8_t on, size, norm, div; GLenum type; GLsizei stride; uint64_t off; GLuint buf; } vattr_t; // div: glVertexAttribDivisor (0 = per vertex) typedef struct { vattr_t a[16]; GLuint ebo; } vao_t; NAMES(vao_t, g_vaos, g_nvaos, g_capvaos) static vao_t g_vao0; // the default vertex array (GL core has none, the compatibility profile does) typedef struct { GLenum type; char *src; size_t len; char key[17]; const shinfo *si; __strong id fn; int compiled; } shader_t; typedef struct { char name[48]; uint8_t type; uint16_t count; int32_t off[2]; uint16_t stride[2]; int slot[2]; int unit; } puni_t; // [0] vertex, [1] fragment -typedef struct { uint32_t vf[16]; uint16_t stride[16]; uint32_t cpf, dpf, spf; uint8_t blend, mask, bsrc, bdst, bsa, bda, eqc, eqa; } pkey_t; +typedef struct { uint32_t vf[16]; uint16_t stride[16]; uint8_t div[16]; uint32_t cpf, dpf, spf; uint8_t blend, mask, bsrc, bdst, bsa, bda, eqc, eqa; } pkey_t; typedef struct { pkey_t k; void *pso; } pent_t; // pso: a retained id, NULL = failed +// amap: the game's attribute index of each Metal vertex input (glBindAttribLocation before the link, else the input's own number, which glGetAttribLocation returns); +// LÖVE binds VertexPosition, VertexTexCoord, VertexColor and ConstantColor to 0–3 in every program +typedef struct { char name[48]; int index; } abind_t; typedef struct { GLuint vs, fs; int linked; const shinfo *si[2]; __strong id fn[2]; int nu; puni_t *u; uint8_t *ub[2]; uint64_t ub_gen[2]; uint32_t serial; pent_t *pc; int npc, cappc; + abind_t bind[16]; int nbind; int8_t amap[16]; } prog_t; // Shaders and programs share one name space, as in GL typedef struct { int is_prog; union { shader_t s; prog_t p; }; } sp_t; @@ -314,20 +344,37 @@ static shader_t *shader_of(GLuint n) { return n && n < g_nsps && g_sps[n] && !g_ static prog_t *prog_of(GLuint n) { return n && n < g_nsps && g_sps[n] && g_sps[n]->is_prog ? &g_sps[n]->p : NULL; } // ---------- Context state ---------- +// units: as many as GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS says (LÖVE binds a texture of each type to every one at start); fb / read_fb: the draw and read framebuffers; +// va: each attribute's current value (glVertexAttrib4f), what a vertex input reads when its array is disabled: LÖVE's VertexColor and ConstantColor +#define NUNITS 96 static struct { - GLuint array_buf, rb, fb, prog, vao; int unit; - struct { GLuint t2d, tcube; } units[32]; + GLuint array_buf, rb, fb, read_fb, prog, vao; int unit; + struct { GLuint t2d, tcube, t3d, t2da; } units[NUNITS]; float clear_c[4]; double clear_d; int clear_s; - uint8_t cmask, dmask, depth, blend, cull, scissor, stencil, poffset; - GLenum dfunc, cull_face, bsrc, bdst, bsa, bda, eqc, eqa; + uint8_t cmask, dmask, depth, blend, cull, scissor, stencil, poffset, srgb; + GLenum dfunc, cull_face, front, bsrc, bdst, bsa, bda, eqc, eqa; GLenum sfunc, sfail, szfail, spass; GLint sref; GLuint smask, swmask; - GLint vp[4], sc[4]; float pfactor, punits; - int unpack_align, unpack_row; + GLint vp[4], sc[4]; float pfactor, punits, psize; + float va[16][4]; + int unpack_align, unpack_row, pack_align; uint32_t palette[256]; int npalette, color_table, index_shift, index_offset; int swap_interval; -} S = {.cmask = 0xf, .dmask = 1, .dfunc = 0x201, .cull_face = GL_BACK, .bsrc = 1, .bdst = 0, .bsa = 1, .bda = 0, .eqc = GL_FUNC_ADD, .eqa = GL_FUNC_ADD, - .sfunc = 0x207, .sfail = GL_KEEP, .szfail = GL_KEEP, .spass = GL_KEEP, .smask = 0xffffffff, .swmask = 0xffffffff, .clear_d = 1, .unpack_align = 4, .swap_interval = 1}; +} S = {.cmask = 0xf, .dmask = 1, .dfunc = 0x201, .cull_face = GL_BACK, .front = GL_CCW, .bsrc = 1, .bdst = 0, .bsa = 1, .bda = 0, .eqc = GL_FUNC_ADD, .eqa = GL_FUNC_ADD, + .sfunc = 0x207, .sfail = GL_KEEP, .szfail = GL_KEEP, .spass = GL_KEEP, .smask = 0xffffffff, .swmask = 0xffffffff, .clear_d = 1, .unpack_align = 4, .pack_align = 4, + .swap_interval = 1, .psize = 1, + .va = {{0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1}, + {0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1}}}; static vao_t *cur_vao(void) { return S.vao && S.vao < g_nvaos && g_vaos[S.vao] ? g_vaos[S.vao] : &g_vao0; } +static GLuint *unit_slot(int unit, GLenum target) { // a unit's binding of a texture target (a cube face names the cube); NULL for a target this layer does not have + if (target >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && target <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z) target = GL_TEXTURE_CUBE_MAP; + if (unit < 0 || unit >= NUNITS) return NULL; + switch (target) { + case GL_TEXTURE_2D: return &S.units[unit].t2d; case GL_TEXTURE_CUBE_MAP: return &S.units[unit].tcube; + case GL_TEXTURE_3D: return &S.units[unit].t3d; case GL_TEXTURE_2D_ARRAY: return &S.units[unit].t2da; + } + return NULL; +} +static tex_t *bound_tex(GLenum target) { GLuint *s = unit_slot(S.unit, target); return s ? tex_of(*s) : NULL; } // ---------- The window surface ---------- #define NWIN 3 @@ -402,6 +449,7 @@ static int fmt_pick(GLint ifmt, GLenum format, GLenum type, int *cv) { if (format == GL_RGBA) return F_RGBA8; *cv = CV_RGB8; return F_RGBA8; case GL_BGRA: return F_BGRA8; + case GL_RGBA4: case GL_RGB5_A1: case GL_RGB565: return F_RGBA8; // glTexStorage2D's sized formats (no data: the uploads are converted by their type) case GL_SRGB8_ALPHA8: return F_SRGBA8; case GL_RED: case GL_R8: return fl ? F_R32F : half ? F_R16F : F_R8; case GL_RG: case GL_RG8: return fl ? F_RG32F : half ? F_RG16F : F_RG8; @@ -482,7 +530,20 @@ static MTLSamplerAddressMode wrap_of(GLint w) { switch (w) { case GL_CLAMP_TO_EDGE: case GL_CLAMP: return MTLSamplerAddressModeClampToEdge; case GL_MIRRORED_REPEAT: return MTLSamplerAddressModeMirrorRepeat; case GL_CLAMP_TO_BORDER: return MTLSamplerAddressModeClampToBorderColor; default: return MTLSamplerAddressModeRepeat; } } -static void tex_init_params(tex_t *t, GLenum target) { t->target = target; t->minf = GL_NEAREST_MIPMAP_LINEAR; t->magf = GL_LINEAR; t->ws = t->wt = t->wr = GL_REPEAT; t->maxlv = 1000; t->aniso = 1; t->smp_dirty = 1; t->fmt = -1; } +static void tex_init_params(tex_t *t, GLenum target) { + t->target = target; t->minf = GL_NEAREST_MIPMAP_LINEAR; t->magf = GL_LINEAR; t->ws = t->wt = t->wr = GL_REPEAT; t->maxlv = 1000; t->aniso = 1; t->smp_dirty = 1; t->fmt = -1; + for (int c = 0; c < 4; c++) t->swz[c] = GL_RED + c; +} +static int tex_faces(const tex_t *t) { return t->target == GL_TEXTURE_CUBE_MAP ? 6 : 1; } +// The swizzle a texture is made with: its format's own (only where Metal's reading differs from GL's), then GL_TEXTURE_SWIZZLE_R..A over it +static MTLTextureSwizzleChannels tex_swz(const tex_t *t) { + MTLTextureSwizzleChannels f = g_fmt[t->fmt].swz, o; + const MTLTextureSwizzle src[4] = {f.red, f.green, f.blue, f.alpha}; + MTLTextureSwizzle out[4]; + for (int c = 0; c < 4; c++) { GLint p = t->swz[c]; out[c] = p >= GL_RED && p <= GL_ALPHA ? src[p - GL_RED] : p == GL_ONE ? MTLTextureSwizzleOne : MTLTextureSwizzleZero; } + o.red = out[0]; o.green = out[1]; o.blue = out[2]; o.alpha = out[3]; + return o; +} static id tex_sampler(tex_t *t) { if (!t->smp_dirty && t->smp) return t->smp; // sampler states are shared: one per distinct parameter set @@ -509,26 +570,30 @@ static id tex_sampler(tex_t *t) { } static void lv_free(level_t *l) { free(l->data); memset(l, 0, sizeof *l); } static void tex_drop(tex_t *t) { for (int f = 0; f < 6; f++) for (int l = 0; l < MAXLV; l++) lv_free(&t->pend[f][l]); t->tex = nil; t->made = 0; t->defined = 0; t->levels = 0; t->gen++; } -// Make the Metal texture from the levels given so far: as many levels as are given in a row from level 0 (and fit the size). rt: made for a framebuffer attachment +// Make the Metal texture from the levels given so far: as many levels as are given in a row from level 0 (and fit the size), or as glTexStorage said. rt: made for a framebuffer attachment static int tex_make(tex_t *t, int rt) { if (t->made) return t->tex != nil; if (t->fmt < 0 || !t->w || !t->h) return 0; - int faces = t->target == GL_TEXTURE_CUBE_MAP ? 6 : 1, lv = 0; - uint32_t full = 1; for (uint32_t m = umax(t->w, t->h); m > 1; m >>= 1) full++; - while (lv < MAXLV && lv < (int)full && (t->defined >> lv & 1)) lv++; - if (!lv) lv = 1; + int faces = tex_faces(t), lv = 0, vol = t->target == GL_TEXTURE_3D, arr = t->target == GL_TEXTURE_2D_ARRAY; + uint32_t full = 1; for (uint32_t m = umax(umax(t->w, t->h), vol ? t->d : 1); m > 1; m >>= 1) full++; + if (t->slevels) lv = (int)umin(umin((uint32_t)t->slevels, full), MAXLV); + else { while (lv < MAXLV && lv < (int)full && (t->defined >> lv & 1)) lv++; if (!lv) lv = 1; } const fmt_t *m = &g_fmt[t->fmt]; @autoreleasepool { - MTLTextureDescriptor *td = faces == 6 ? [MTLTextureDescriptor textureCubeDescriptorWithPixelFormat:m->pf size:t->w mipmapped:NO] - : [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:m->pf width:t->w height:t->h mipmapped:NO]; + MTLTextureDescriptor *td = [MTLTextureDescriptor new]; + td.textureType = faces == 6 ? MTLTextureTypeCube : vol ? MTLTextureType3D : arr ? MTLTextureType2DArray : MTLTextureType2D; + td.pixelFormat = m->pf; td.width = t->w; td.height = faces == 6 ? t->w : t->h; + if (vol) td.depth = umax(1, t->d); else if (arr) td.arrayLength = umax(1, t->d); td.mipmapLevelCount = lv; - int swz = m->swz.red != MTLTextureSwizzleRed || m->swz.green != MTLTextureSwizzleGreen || m->swz.blue != MTLTextureSwizzleBlue || m->swz.alpha != MTLTextureSwizzleAlpha; + MTLTextureSwizzleChannels sw = tex_swz(t); + int swz = sw.red != MTLTextureSwizzleRed || sw.green != MTLTextureSwizzleGreen || sw.blue != MTLTextureSwizzleBlue || sw.alpha != MTLTextureSwizzleAlpha; int data = 0; for (int f = 0; f < faces; f++) for (int l = 0; l < lv; l++) data |= t->pend[f][l].data != NULL; td.usage = MTLTextureUsageShaderRead | (m->kind != K_BLOCK && !swz ? MTLTextureUsageRenderTarget : 0); // a render target (first made as an attachment, with no data given): private, so Metal can compress it, and its uploads are blits (tex_write). Textures the CPU fills stay // shared: the movie planes, made with no data and filled by glTexSubImage2D every frame, are written in place once the GPU is done with them td.storageMode = m->kind == K_DEPTH || m->kind == K_DS || (rt && !data) ? MTLStorageModePrivate : MTLStorageModeShared; - if (swz) td.swizzle = m->swz; + if (swz) td.swizzle = sw; + if (swz && rt) RED1("texture %ux%u %s with a swizzle is a framebuffer attachment (Metal renders into no swizzled texture)", t->w, t->h, m->name); t->tex = [g_dev newTextureWithDescriptor:td]; if (!t->tex) { GLOG("[RED] texture %ux%u %s (%d levels) could not be made", t->w, t->h, m->name, lv); t->made = 1; return 0; } for (int f = 0; f < faces; f++) for (int l = 0; l < lv; l++) { @@ -543,12 +608,13 @@ static int tex_make(tex_t *t, int rt) { return 1; } // Write one level's (or a part's) bytes into a made texture, in command order: a CPU write when no command buffer that may still read it is pending, -// else a blit from transient memory (in this frame's command buffer, after ending its render pass, when this frame used it; in a command buffer of its own otherwise) -static void tex_write(tex_t *t, int face, int lv, uint32_t x, uint32_t y, uint32_t w, uint32_t h, const void *data, size_t pitch) { +// else a blit from transient memory (in this frame's command buffer, after ending its render pass, when this frame used it; in a command buffer of its own otherwise). +// face: the cube face or array layer; z: the slice of a 3D texture +static void tex_write(tex_t *t, int face, int lv, uint32_t x, uint32_t y, uint32_t z, uint32_t w, uint32_t h, const void *data, size_t pitch) { g_n_up++; size_t rows = fmt_rows(t->fmt, h); if (t->use <= atomic_load(&g_done) && t->tex.storageMode == MTLStorageModeShared) { - [t->tex replaceRegion:MTLRegionMake2D(x, y, w, h) mipmapLevel:lv slice:face withBytes:data bytesPerRow:pitch bytesPerImage:0]; + [t->tex replaceRegion:MTLRegionMake3D(x, y, z, w, h, 1) mipmapLevel:lv slice:face withBytes:data bytesPerRow:pitch bytesPerImage:t->target == GL_TEXTURE_3D ? pitch * rows : 0]; return; } size_t off; id st = arena_alloc(pitch * rows, &off); @@ -558,14 +624,13 @@ static void tex_write(tex_t *t, int face, int lv, uint32_t x, uint32_t y, uint32 else { cb = [g_q commandBuffer]; cb.label = @"gl upload"; } id b = [cb blitCommandEncoder]; [b copyFromBuffer:st sourceOffset:off sourceBytesPerRow:pitch sourceBytesPerImage:pitch * rows sourceSize:MTLSizeMake(w, h, 1) toTexture:t->tex - destinationSlice:face destinationLevel:lv destinationOrigin:MTLOriginMake(x, y, 0)]; + destinationSlice:face destinationLevel:lv destinationOrigin:MTLOriginMake(x, y, z)]; [b endEncoding]; g_n_blit++; if (cb != g_cb) { dm_gfx_enter(); cb_commit_one(cb); dm_gfx_leave(); } // no GPU work goes out while the app is in the background (dm_gfx_suspend) } // glTexImage2D / glCompressedTexImage2D: the level's bytes, converted static void tex_image(GLenum target, GLint level, int f, int cv, uint32_t w, uint32_t h, void *conv, size_t pitch) { - GLuint tn = target == GL_TEXTURE_2D ? S.units[S.unit].t2d : S.units[S.unit].tcube; - tex_t *t = tex_of(tn); + tex_t *t = target == GL_TEXTURE_2D || (target >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && target <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z) ? bound_tex(target) : NULL; int face = target >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && target <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z ? (int)(target - GL_TEXTURE_CUBE_MAP_POSITIVE_X) : 0; if (!t || level < 0 || level >= MAXLV) { RED1("glTexImage2D target %#x level %d with no texture bound", target, level); free(conv); return; } (void)cv; @@ -573,7 +638,7 @@ static void tex_image(GLenum target, GLint level, int f, int cv, uint32_t w, uin if (level == 0 && (t->fmt != f || t->w != w || t->h != h) && (t->made || t->defined)) tex_drop(t); // level 0 respecified: a new texture if (!t->made && !t->defined) { t->fmt = f; t->w = level ? umax(w0, 1) : w; t->h = level ? umax(h0, 1) : h; } if (t->fmt != f) { RED1("texture level %d in format %s, its level 0 in %s", level, g_fmt[f].name, g_fmt[t->fmt].name); free(conv); return; } - if (t->made && level < t->levels && t->tex) { if (conv) tex_write(t, face, level, 0, 0, w, h, conv, pitch); free(conv); return; } + if (t->made && level < t->levels && t->tex) { if (conv) tex_write(t, face, level, 0, 0, 0, w, h, conv, pitch); free(conv); return; } if (t->made) { RED1("texture level %d given after the texture was made with %d levels: ignored", level, t->levels); free(conv); return; } level_t *l = &t->pend[face][level]; lv_free(l); *l = (level_t){conv, pitch, pitch * fmt_rows(f, h), w, h}; @@ -631,7 +696,8 @@ static void pass_begin(void) { } int pc = g_pend_clear.fb == S.fb ? g_pend_clear.bits : 0; if (c) { - MTLRenderPassColorAttachmentDescriptor *a = rp.colorAttachments[0]; a.texture = c; a.level = cl; a.slice = cs; a.storeAction = MTLStoreActionStore; + MTLRenderPassColorAttachmentDescriptor *a = rp.colorAttachments[0]; a.texture = c; a.level = cl; a.storeAction = MTLStoreActionStore; + if (c.textureType == MTLTextureType3D) a.depthPlane = cs; else a.slice = cs; if (pc & GL_COLOR_BUFFER_BIT) { a.loadAction = MTLLoadActionClear; a.clearColor = MTLClearColorMake(g_pend_clear.c[0], g_pend_clear.c[1], g_pend_clear.c[2], g_pend_clear.c[3]); } else a.loadAction = MTLLoadActionLoad; } @@ -726,8 +792,10 @@ static id pso_get(prog_t *p, const pkey_t *k) { int s = vs->a[i].slot; if (k->vf[s]) { vd.attributes[s].format = (MTLVertexFormat)k->vf[s]; vd.attributes[s].offset = 0; vd.attributes[s].bufferIndex = 30 - s; - vd.layouts[30 - s].stride = k->stride[s]; vd.layouts[30 - s].stepFunction = MTLVertexStepFunctionPerVertex; - } else { // a disabled array: the default value (0, 0, 0, 1) + vd.layouts[30 - s].stride = k->stride[s]; + if (k->div[s]) { vd.layouts[30 - s].stepFunction = MTLVertexStepFunctionPerInstance; vd.layouts[30 - s].stepRate = k->div[s]; } + else vd.layouts[30 - s].stepFunction = MTLVertexStepFunctionPerVertex; + } else { // a disabled array: the attribute's current value (glVertexAttrib4f; (0, 0, 0, 1) by default) vd.attributes[s].format = MTLVertexFormatFloat4; vd.attributes[s].offset = 0; vd.attributes[s].bufferIndex = 30 - s; vd.layouts[30 - s].stride = 16; vd.layouts[30 - s].stepFunction = MTLVertexStepFunctionConstant; vd.layouts[30 - s].stepRate = 0; } @@ -797,7 +865,8 @@ static void enc_state(void) { int cull = !S.cull ? MTLCullModeNone : S.cull_face == GL_FRONT ? MTLCullModeFront : S.cull_face == GL_BACK ? MTLCullModeBack : MTLCullModeNone; if (S.cull && S.cull_face == GL_FRONT_AND_BACK) RED1("culling front and back faces not implemented"); if (cull != E.cull) { E.cull = cull; [e setCullMode:(MTLCullMode)cull]; } - if (E.wind != MTLWindingClockwise) { E.wind = MTLWindingClockwise; [e setFrontFacingWinding:MTLWindingClockwise]; } // GL's counter-clockwise, after the y flip + int wind = S.front == GL_CW ? MTLWindingCounterClockwise : MTLWindingClockwise; // GL's winding, reversed by the y flip + if (E.wind != wind) { E.wind = wind; [e setFrontFacingWinding:(MTLWinding)wind]; } float pf = S.poffset ? S.pfactor : 0, pu = S.poffset ? S.punits : 0; if (pf != E.pf || pu != E.pu) { E.pf = pf; E.pu = pu; [e setDepthBias:pu slopeScale:pf clamp:0]; } if (S.stencil && (int)(S.sref & 0xff) != E.sref) { E.sref = S.sref & 0xff; [e setStencilReferenceValue:E.sref]; } @@ -820,12 +889,13 @@ static void bind_uniforms_textures(prog_t *p) { } for (int i = 0; i < p->nu; i++) { puni_t *u = &p->u[i]; - if (u->type != T_SAMP2D && u->type != T_SAMPCUBE) continue; - int unit = u->unit < 0 || u->unit >= 32 ? 0 : u->unit; - tex_t *t = tex_of(u->type == T_SAMPCUBE ? S.units[unit].tcube : S.units[unit].t2d); + if (!is_samp(u->type)) continue; + int unit = u->unit < 0 || u->unit >= NUNITS ? 0 : u->unit; + GLenum target = u->type == T_SAMPCUBE ? GL_TEXTURE_CUBE_MAP : u->type == T_SAMP3D ? GL_TEXTURE_3D : u->type == T_SAMP2DARR ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_2D; + tex_t *t = tex_of(*unit_slot(unit, target)); id tx = nil; id sm = g_samp0; if (t && (t->made || tex_make(t, 0)) && t->tex) { tx = t->tex; sm = tex_sampler(t); t->use = g_serial; } - if (!tx) tx = u->type == T_SAMPCUBE ? g_cube0 : g_tex0; + if (!tx) tx = u->type == T_SAMPCUBE ? g_cube0 : u->type == T_SAMP3D ? g_tex3d0 : u->type == T_SAMP2DARR ? g_arr0 : g_tex0; for (int st = 0; st < 2; st++) { if (u->slot[st] < 0) continue; if (st) { [e setFragmentTexture:tx atIndex:u->slot[st]]; [e setFragmentSamplerState:sm atIndex:u->slot[st]]; } @@ -842,13 +912,13 @@ static int draw_setup(void) { pkey_t k; memset(&k, 0, sizeof k); vao_t *v = cur_vao(); const shinfo *vs = p->si[0]; for (int i = 0; i < vs->na; i++) { - int s = vs->a[i].slot; vattr_t *a = &v->a[s]; + int s = vs->a[i].slot; vattr_t *a = &v->a[p->amap[s]]; if (!a->on) continue; buf_t *b = buf_of(a->buf); - if (!b || !b->mb) { RED1("vertex attribute %d reads client memory or a buffer with no data (not implemented)", s); return 0; } + if (!b || !b->mb) { RED1("vertex attribute %d reads client memory or a buffer with no data (not implemented)", p->amap[s]); return 0; } k.vf[s] = (uint32_t)vfmt_of(a); - k.stride[s] = a->stride ? a->stride : a->size * vtype_bytes(a->type); - if (!k.vf[s] || (k.stride[s] & 3)) { RED1("vertex attribute %d: type %#x size %d stride %d not mapped", s, a->type, a->size, a->stride); return 0; } + k.stride[s] = a->stride ? a->stride : a->size * vtype_bytes(a->type); k.div[s] = a->div; + if (!k.vf[s] || (k.stride[s] & 3)) { RED1("vertex attribute %d: type %#x size %d stride %d not mapped", p->amap[s], a->type, a->size, a->stride); return 0; } } k.cpf = P.cpf; k.dpf = P.dpf; k.spf = P.spf; k.mask = S.cmask; if (S.blend && P.cpf) { k.blend = 1; k.bsrc = bcode(S.bsrc); k.bdst = bcode(S.bdst); k.bsa = bcode(S.bsa); k.bda = bcode(S.bda); k.eqc = beq_of(S.eqc); k.eqa = beq_of(S.eqa); } @@ -858,9 +928,9 @@ static int draw_setup(void) { if ((__bridge void *)pso != E.pso) { E.pso = (__bridge void *)pso; [e setRenderPipelineState:pso]; } enc_state(); for (int i = 0; i < vs->na; i++) { - int s = vs->a[i].slot; vattr_t *a = &v->a[s]; + int s = vs->a[i].slot, gi = p->amap[s]; vattr_t *a = &v->a[gi]; if (a->on) { buf_t *b = buf_of(a->buf); b->use = g_serial; [e setVertexBuffer:b->mb offset:a->off atIndex:30 - s]; } - else [e setVertexBuffer:g_attr0 offset:0 atIndex:30 - s]; + else [e setVertexBytes:S.va[gi] length:16 atIndex:30 - s]; } bind_uniforms_textures(p); g_n_draw++; @@ -887,19 +957,20 @@ static uint32_t idx_convert(int conv, const uint32_t *in, uint32_t n, uint32_t b #undef IX return m; } -GLB(glDrawArrays, void, GLenum mode, GLint first, GLsizei count) { - if (count <= 0) return; +static void draw_arrays(GLenum mode, GLint first, GLsizei count, GLsizei inst) { + if (count <= 0 || inst <= 0) return; @autoreleasepool { int conv; MTLPrimitiveType pt = prim_of(mode, &conv); if (conv < 0 || !draw_setup()) return; - if (!conv) { [g_enc drawPrimitives:pt vertexStart:first vertexCount:count]; return; } + if (!conv) { [g_enc drawPrimitives:pt vertexStart:first vertexCount:count instanceCount:inst]; return; } size_t off; id b = arena_alloc((size_t)count * 6 * 4 + 16, &off); uint32_t n = idx_convert(conv, NULL, count, first, (uint32_t *)((uint8_t *)b.contents + off)); - if (n) [g_enc drawIndexedPrimitives:pt indexCount:n indexType:MTLIndexTypeUInt32 indexBuffer:b indexBufferOffset:off]; + if (n) [g_enc drawIndexedPrimitives:pt indexCount:n indexType:MTLIndexTypeUInt32 indexBuffer:b indexBufferOffset:off instanceCount:inst]; } } -GLB(glDrawElements, void, GLenum mode, GLsizei count, GLenum type, const void *indices) { - if (count <= 0) return; +// base: added to every index (glDrawElementsBaseVertex: LÖVE draws its batched quads with one shared index buffer so) +static void draw_elements(GLenum mode, GLsizei count, GLenum type, const void *indices, GLint base, GLsizei inst) { + if (count <= 0 || inst <= 0) return; @autoreleasepool { int conv; MTLPrimitiveType pt = prim_of(mode, &conv); if (conv < 0) return; @@ -910,7 +981,8 @@ GLB(glDrawElements, void, GLenum mode, GLsizei count, GLenum type, const void *i if (!draw_setup()) return; if (!conv && isz > 1 && eb && io % 4 == 0) { // the game's own index buffer eb->use = g_serial; - [g_enc drawIndexedPrimitives:pt indexCount:count indexType:isz == 4 ? MTLIndexTypeUInt32 : MTLIndexTypeUInt16 indexBuffer:eb->mb indexBufferOffset:io]; + [g_enc drawIndexedPrimitives:pt indexCount:count indexType:isz == 4 ? MTLIndexTypeUInt32 : MTLIndexTypeUInt16 indexBuffer:eb->mb indexBufferOffset:io + instanceCount:inst baseVertex:base baseInstance:0]; return; } // client indices, 8-bit indices, an unaligned offset or a converted mode: 32-bit indices in transient memory @@ -919,9 +991,15 @@ GLB(glDrawElements, void, GLenum mode, GLsizei count, GLenum type, const void *i for (GLsizei i = 0; i < count; i++) tmp[i] = isz == 4 ? ((const uint32_t *)src)[i] : isz == 2 ? ((const uint16_t *)src)[i] : src[i]; uint32_t n = count; if (conv) { n = idx_convert(conv, tmp, count, 0, (uint32_t *)((uint8_t *)b.contents + off)); free(tmp); } - if (n) [g_enc drawIndexedPrimitives:pt indexCount:n indexType:MTLIndexTypeUInt32 indexBuffer:b indexBufferOffset:off]; + if (n) [g_enc drawIndexedPrimitives:pt indexCount:n indexType:MTLIndexTypeUInt32 indexBuffer:b indexBufferOffset:off instanceCount:inst baseVertex:base baseInstance:0]; } } +GLB(glDrawArrays, void, GLenum mode, GLint first, GLsizei count) { draw_arrays(mode, first, count, 1); } +GLB(glDrawArraysInstanced, void, GLenum mode, GLint first, GLsizei count, GLsizei inst) { draw_arrays(mode, first, count, inst); } +GLB(glDrawElements, void, GLenum mode, GLsizei count, GLenum type, const void *indices) { draw_elements(mode, count, type, indices, 0, 1); } +GLB(glDrawElementsBaseVertex, void, GLenum mode, GLsizei count, GLenum type, const void *indices, GLint base) { draw_elements(mode, count, type, indices, base, 1); } +GLB(glDrawElementsInstanced, void, GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei inst) { draw_elements(mode, count, type, indices, 0, inst); } +GLB(glDrawElementsInstancedBaseVertex, void, GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei inst, GLint base) { draw_elements(mode, count, type, indices, base, inst); } // ---------- Clears ---------- // A clear the whole framebuffer takes (no scissor, nothing masked) becomes the next pass's load action; any other is a quad, which keeps the scissor rectangle and the masks @@ -983,7 +1061,14 @@ GLB(glClear, void, GLbitfield mask) { } GL(glClearColor, void, GLfloat r, GLfloat g, GLfloat b, GLfloat a) { S.clear_c[0] = r; S.clear_c[1] = g; S.clear_c[2] = b; S.clear_c[3] = a; } GL(glClearDepth, void, GLdouble d) { S.clear_d = d < 0 ? 0 : d > 1 ? 1 : d; } +GL(glClearDepthf, void, GLfloat d) { S.clear_d = d < 0 ? 0 : d > 1 ? 1 : d; } GL(glClearStencil, void, GLint s) { S.clear_s = s; } +// One buffer cleared to a value of its own, the clear values left as they were (LÖVE clears a canvas so when it is given a colour per canvas); colour attachment 0 only +GLB(glClearBufferfv, void, GLenum buffer, GLint drawbuffer, const GLfloat *v) { + if (buffer == GL_COLOR && drawbuffer == 0) { float keep[4]; memcpy(keep, S.clear_c, sizeof keep); memcpy(S.clear_c, v, sizeof keep); h_glClear(GL_COLOR_BUFFER_BIT); memcpy(S.clear_c, keep, sizeof keep); } + else if (buffer == GL_DEPTH) { double keep = S.clear_d; S.clear_d = v[0] < 0 ? 0 : v[0] > 1 ? 1 : v[0]; h_glClear(GL_DEPTH_BUFFER_BIT); S.clear_d = keep; } + else RED1("glClearBufferfv(%#x, %d) not implemented", buffer, drawbuffer); +} // ---------- State ---------- static uint8_t *cap_of(GLenum cap) { @@ -995,7 +1080,9 @@ static void cap_set(GLenum cap, int on) { uint8_t *c = cap_of(cap); if (c) { *c = on; return; } if (cap == GL_COLOR_TABLE) { S.color_table = on; return; } - if (cap == GL_TEXTURE_2D || cap == 0x809d /* MULTISAMPLE */ || cap == 0x8642 /* PROGRAM_POINT_SIZE */ || cap == 0x8d69 /* PRIMITIVE_RESTART_FIXED_INDEX */ || cap == 0x884f /* TEXTURE_CUBE_MAP_SEAMLESS */) return; + if (cap == GL_FRAMEBUFFER_SRGB) { if (on && !S.srgb) RED1("GL_FRAMEBUFFER_SRGB: writes into sRGB attachments are not converted"); S.srgb = on; return; } + if (cap == GL_TEXTURE_2D || cap == 0x809d /* MULTISAMPLE */ || cap == 0x8642 /* PROGRAM_POINT_SIZE */ || cap == 0x8d69 /* PRIMITIVE_RESTART_FIXED_INDEX */ || cap == 0x884f /* TEXTURE_CUBE_MAP_SEAMLESS */ + || cap == 0x92e0 /* DEBUG_OUTPUT */ || cap == 0x8242 /* DEBUG_OUTPUT_SYNCHRONOUS */) return; static GLenum seen[32]; static int n; for (int i = 0; i < n; i++) if (seen[i] == cap) return; if (n < 32) { seen[n++] = cap; GLOG("[RED] gl%s(%#x) not implemented", on ? "Enable" : "Disable", cap); } @@ -1006,8 +1093,15 @@ GL(glColorMask, void, uint32_t r, uint32_t g, uint32_t b, uint32_t a) { S.cmask GL(glDepthMask, void, uint32_t m) { S.dmask = (m & 0xff) != 0; } GL(glDepthFunc, void, GLenum f) { if (f >= GL_NEVER && f <= GL_NEVER + 7) S.dfunc = f; } GL(glBlendFunc, void, GLenum s, GLenum d) { S.bsrc = S.bsa = s; S.bdst = S.bda = d; } +GL(glBlendFuncSeparate, void, GLenum sc, GLenum dc, GLenum sa, GLenum da) { S.bsrc = sc; S.bdst = dc; S.bsa = sa; S.bda = da; } +GL(glBlendEquation, void, GLenum e) { S.eqc = S.eqa = e; } GL(glBlendEquationSeparate, void, GLenum c, GLenum a) { S.eqc = c; S.eqa = a; } GL(glCullFace, void, GLenum m) { S.cull_face = m; } +GL(glFrontFace, void, GLenum m) { if (m == GL_CW || m == GL_CCW) S.front = m; } +// Points are as big as the vertex shader's point size output says (Metal has no fixed size); wireframe has no Metal counterpart in this layer +GL(glPointSize, void, GLfloat s) { S.psize = s; } +GL(glPolygonMode, void, GLenum face, GLenum mode) { (void)face; if (mode != 0x1b02 /* FILL */) RED1("glPolygonMode(%#x) not implemented (filled)", mode); } +GL(glHint, void, GLenum target, GLenum mode) { (void)target; (void)mode; } GL(glStencilFunc, void, GLenum f, GLint ref, GLuint mask) { if (f >= GL_NEVER && f <= GL_NEVER + 7) S.sfunc = f; S.sref = ref; S.smask = mask; } GL(glStencilMask, void, GLuint m) { S.swmask = m; } GL(glStencilOp, void, GLenum sf, GLenum zf, GLenum zp) { S.sfail = sf; S.szfail = zf; S.spass = zp; } @@ -1017,7 +1111,8 @@ GL(glScissor, void, GLint x, GLint y, GLsizei w, GLsizei h) { S.sc[0] = x; S.sc[ GL(glPixelStorei, void, GLenum p, GLint v) { if (p == GL_UNPACK_ALIGNMENT && (v == 1 || v == 2 || v == 4 || v == 8)) S.unpack_align = v; else if (p == GL_UNPACK_ROW_LENGTH) S.unpack_row = v; - else if (p == 0xd05 /* PACK_ALIGNMENT */ || (p >= 0xcf0 && p <= 0xcf4 && !v)) {} // SWAP_BYTES, LSB_FIRST, SKIP_ROWS, SKIP_PIXELS at their defaults + else if (p == GL_PACK_ALIGNMENT && (v == 1 || v == 2 || v == 4 || v == 8)) S.pack_align = v; + else if (p >= 0xcf0 && p <= 0xcf4 && !v) {} // SWAP_BYTES, LSB_FIRST, SKIP_ROWS, SKIP_PIXELS at their defaults else RED1("glPixelStorei(%#x, %d) not implemented", p, v); } // Colour-index images through a palette (GL's compatibility pixel transfer: glColorTable, GL_INDEX_SHIFT / GL_INDEX_OFFSET); the game's texture loader sets a palette for its @@ -1031,6 +1126,16 @@ GL(glColorTable, void, GLenum target, GLenum ifmt, GLsizei w, GLenum format, GLe } GL(glTexEnvi, void, GLenum t, GLenum p, GLint v) { (void)t; (void)p; (void)v; } GL(glGetError, GLenum, void) { return 0; } +// Everything recorded so far runs to its end (LÖVE when the window changes) +GLB(glFinish, void, void) { if (g_pend_clear.bits) clears_flush(); dm_gfx_enter(); id cb = cb_get(); cb_commit(); [cb waitUntilCompleted]; dm_gfx_leave(); } +// Debug output and labels (LÖVE's debug groups and its GL debug callback, used when the game asks for debug output): none here +GL(glDebugMessageCallback, void, void *cb, const void *user) { (void)cb; (void)user; } +GL(glDebugMessageControl, void, GLenum src, GLenum type, GLenum sev, GLsizei n, const GLuint *ids, uint32_t on) { (void)src; (void)type; (void)sev; (void)n; (void)ids; (void)on; } +GL(glPushDebugGroup, void, GLenum src, GLuint id, GLsizei len, const GLchar *msg) { (void)src; (void)id; (void)len; (void)msg; } +GL(glPopDebugGroup, void, void) {} +GL(glPushGroupMarkerEXT, void, GLsizei len, const GLchar *msg) { (void)len; (void)msg; } +GL(glPopGroupMarkerEXT, void, void) {} +GL(glObjectLabel, void, GLenum id, GLuint name, GLsizei len, const GLchar *label) { (void)id; (void)name; (void)len; (void)label; } // ---------- Queries ---------- static const char *const g_exts[] = {"GL_ARB_ES3_2_compatibility", "GL_ARB_ES3_compatibility", "GL_ARB_texture_compression_bptc", "GL_ARB_texture_compression_rgtc", @@ -1056,21 +1161,37 @@ GL(glGetIntegerv, void, GLenum p, GLint *v) { case GL_NUM_EXTENSIONS: *v = NEXTS; return; case GL_MAJOR_VERSION: *v = 4; return; case GL_MINOR_VERSION: *v = 5; return; - case GL_MAX_TEXTURE_SIZE: case GL_MAX_RENDERBUFFER_SIZE: *v = 16384; return; + case GL_CONTEXT_PROFILE_MASK: *v = 1; return; // CONTEXT_CORE_PROFILE_BIT: what the games asked for (Digimon a 4.5 core context); LÖVE takes its core paths (the file header) + case GL_CONTEXT_FLAGS: *v = 0; return; + case GL_MAX_TEXTURE_SIZE: case GL_MAX_RENDERBUFFER_SIZE: case GL_MAX_CUBE_MAP_TEXTURE_SIZE: *v = 16384; return; + case GL_MAX_3D_TEXTURE_SIZE: case GL_MAX_ARRAY_TEXTURE_LAYERS: *v = 2048; return; + case GL_MAX_VIEWPORT_DIMS: v[0] = v[1] = 16384; return; + case GL_MAX_COLOR_ATTACHMENTS: case GL_MAX_DRAW_BUFFERS: *v = 1; return; // one colour attachment (fb_t) case GL_MAX_VERTEX_ATTRIBS: *v = 16; return; case GL_MAX_TEXTURE_IMAGE_UNITS: *v = 32; return; - case GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS: *v = 96; return; + case GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS: *v = NUNITS; return; case GL_MAX_VERTEX_UNIFORM_VECTORS: case GL_MAX_FRAGMENT_UNIFORM_VECTORS: *v = 1024; return; case GL_MAX_VARYING_VECTORS: *v = 31; return; case GL_MAX_SAMPLES: *v = 8; return; + case GL_SAMPLE_BUFFERS: case GL_SAMPLES: *v = 0; return; // the window surface is not multisampled case GL_VIEWPORT: memcpy(v, S.vp, sizeof S.vp); return; - case GL_FRAMEBUFFER_BINDING: *v = S.fb; return; + case GL_SCISSOR_BOX: memcpy(v, S.sc, sizeof S.sc); return; + case GL_CULL_FACE_MODE: *v = S.cull_face; return; + case GL_FRONT_FACE: *v = S.front; return; + case GL_FRAMEBUFFER_BINDING: *v = S.fb; return; // and DRAW_FRAMEBUFFER_BINDING + case GL_READ_FRAMEBUFFER_BINDING: *v = S.read_fb; return; case GL_RENDERBUFFER_BINDING: *v = S.rb; return; case GL_CURRENT_PROGRAM: *v = S.prog; return; case 0x8894: *v = S.array_buf; return; // ARRAY_BUFFER_BINDING + case 0x8895: *v = cur_vao()->ebo; return; // ELEMENT_ARRAY_BUFFER_BINDING + case 0x85b5: *v = S.vao; return; // VERTEX_ARRAY_BINDING case 0x8069: *v = S.units[S.unit].t2d; return; // TEXTURE_BINDING_2D + case GL_TEXTURE_BINDING_CUBE_MAP: *v = S.units[S.unit].tcube; return; + case GL_TEXTURE_BINDING_3D: *v = S.units[S.unit].t3d; return; + case GL_TEXTURE_BINDING_2D_ARRAY: *v = S.units[S.unit].t2da; return; case 0x84e0: *v = GL_TEXTURE0 + S.unit; return; // ACTIVE_TEXTURE case GL_UNPACK_ALIGNMENT: *v = S.unpack_align; return; + case GL_PACK_ALIGNMENT: *v = S.pack_align; return; } static GLenum seen[32]; static int n; int known = 0; for (int i = 0; i < n; i++) known |= seen[i] == p; @@ -1082,6 +1203,8 @@ GL(glGetFloatv, void, GLenum p, GLfloat *v) { case GL_MAX_TEXTURE_LOD_BIAS: *v = 15; return; case GL_MAX_TEXTURE_MAX_ANISOTROPY: *v = 16; return; case GL_VIEWPORT: for (int i = 0; i < 4; i++) v[i] = (float)S.vp[i]; return; + case GL_POINT_SIZE: *v = S.psize; return; + case GL_POINT_SIZE_RANGE: case GL_ALIASED_POINT_SIZE_RANGE: v[0] = 1; v[1] = 511; return; // Metal's largest point } RED1("glGetFloatv(%#x) not implemented (0)", p); *v = 0; } @@ -1096,11 +1219,13 @@ GL(glBindBuffer, void, GLenum target, GLuint b) { else if (target == GL_ELEMENT_ARRAY_BUFFER) cur_vao()->ebo = b; else RED1("glBindBuffer target %#x not implemented", target); } +static buf_t *bound_buf(GLenum target) { return buf_of(target == GL_ARRAY_BUFFER ? S.array_buf : target == GL_ELEMENT_ARRAY_BUFFER ? cur_vao()->ebo : 0); } // Orphaning: data for a buffer a pending command buffer may still read goes into a new MTLBuffer, so draws already encoded keep the old contents GLB(glBufferData, void, GLenum target, GLsizeiptr size, const void *data, GLenum usage) { (void)usage; - buf_t *b = buf_of(target == GL_ARRAY_BUFFER ? S.array_buf : target == GL_ELEMENT_ARRAY_BUFFER ? cur_vao()->ebo : 0); + buf_t *b = bound_buf(target); if (!b) { RED1("glBufferData with no buffer bound to %#x", target); return; } + if (b->immutable) { RED1("glBufferData on a buffer glBufferStorage made"); return; } if (size <= 0) { b->mb = nil; b->size = 0; return; } @autoreleasepool { if (!b->mb || b->mb.length < (NSUInteger)size || b->use > atomic_load(&g_done) || b->mb.length > (NSUInteger)size * 2 + 4096) @@ -1109,6 +1234,56 @@ GLB(glBufferData, void, GLenum target, GLsizeiptr size, const void *data, GLenum if (data) memcpy(b->mb.contents, data, size); else memset(b->mb.contents, 0, size); } } +// Immutable storage that stays mapped: the game writes into the MTLBuffer's own memory and fences what the GPU still reads (LÖVE's StreamBufferPersistentMapSync) +GLB(glBufferStorage, void, GLenum target, GLsizeiptr size, const void *data, GLbitfield flags) { + (void)flags; buf_t *b = bound_buf(target); + if (!b || size <= 0) { RED1("glBufferStorage(%#x, %ld) with no buffer bound", target, (long)size); return; } + @autoreleasepool { b->mb = [g_dev newBufferWithLength:(size + 15) & ~(GLsizeiptr)15 options:MTLResourceStorageModeShared]; } + b->size = size; b->use = 0; b->immutable = 1; + if (data) memcpy(b->mb.contents, data, size); +} +// In command order: new bytes for a buffer a pending command buffer may still read go into a copy of it, which later draws use (the draws already encoded keep the old one) +GLB(glBufferSubData, void, GLenum target, GLintptr off, GLsizeiptr size, const void *data) { + buf_t *b = bound_buf(target); + if (!b || !b->mb || off < 0 || size < 0 || (size_t)(off + size) > b->size) { RED1("glBufferSubData(%#x, %ld, %ld) outside the bound buffer", target, (long)off, (long)size); return; } + if (b->use > atomic_load(&g_done)) { + if (b->immutable) RED1("glBufferSubData into a buffer glBufferStorage made while the GPU may still read it"); + else { @autoreleasepool { id nb = [g_dev newBufferWithLength:b->mb.length options:MTLResourceStorageModeShared]; memcpy(nb.contents, b->mb.contents, b->size); b->mb = nb; } b->use = 0; } + } + if (size) memcpy((uint8_t *)b->mb.contents + off, data, size); +} +// The mapping is the MTLBuffer's own memory: a persistent or unsynchronized mapping is the game's to fence; invalidating a whole buffer in flight orphans it, as glBufferData does +GLB(glMapBufferRange, void *, GLenum target, GLintptr off, GLsizeiptr len, GLbitfield access) { + buf_t *b = bound_buf(target); + if (!b || !b->mb || off < 0 || len <= 0 || (size_t)(off + len) > b->size) { RED1("glMapBufferRange(%#x, %ld, %ld) outside the bound buffer", target, (long)off, (long)len); return NULL; } + if (b->use > atomic_load(&g_done) && !b->immutable && (access & GL_MAP_INVALIDATE_BUFFER_BIT)) { + @autoreleasepool { b->mb = [g_dev newBufferWithLength:b->mb.length options:MTLResourceStorageModeShared]; } b->use = 0; + } else if (b->use > atomic_load(&g_done) && !(access & (0x40 /* PERSISTENT */ | 0x20 /* UNSYNCHRONIZED */))) + RED1("glMapBufferRange of a buffer the GPU may still read: not waited for"); + return (uint8_t *)b->mb.contents + off; +} +GL(glUnmapBuffer, uint32_t, GLenum target) { (void)target; return 1; } +GL(glFlushMappedBufferRange, void, GLenum target, GLintptr off, GLsizeiptr len) { (void)target; (void)off; (void)len; } // shared memory: nothing to flush + +// ---------- Sync objects ---------- +// A fence is the serial of the command buffer being recorded when it was made, signalled when that command buffer completes (g_ev). LÖVE fences each third of its stream +// buffers at the end of a frame and waits for the fence three frames later, before it writes that third again +typedef struct { uint64_t serial; } sync_t; +GL(glFenceSync, void *, GLenum cond, GLbitfield flags) { (void)cond; (void)flags; sync_t *s = zalloc(sizeof *s); s->serial = g_serial; return s; } +GL(glDeleteSync, void, void *s) { free(s); } +GL(glWaitSync, void, void *s, GLbitfield flags, uint64_t timeout) { (void)s; (void)flags; (void)timeout; } // a GPU-side wait: the command buffers run in order +GLB(glClientWaitSync, GLenum, void *sy, GLbitfield flags, uint64_t timeout_ns) { + sync_t *s = sy; + if (!s) return 0x911d; // WAIT_FAILED + if (g_ev.signaledValue >= s->serial) return GL_ALREADY_SIGNALED; + if (s->serial >= g_serial) { // its command buffer is still being recorded: commit it now (as GL_SYNC_FLUSH_COMMANDS_BIT asks; without it the wait could never end) + if (!timeout_ns && !(flags & GL_SYNC_FLUSH_COMMANDS_BIT)) return GL_TIMEOUT_EXPIRED; + dm_gfx_enter(); cb_commit(); dm_gfx_leave(); + } + if (!timeout_ns) return GL_TIMEOUT_EXPIRED; + uint64_t ms = timeout_ns / 1000000; + return [g_ev waitUntilSignaledValue:s->serial timeoutMS:ms ? ms : 1] ? GL_CONDITION_SATISFIED : GL_TIMEOUT_EXPIRED; +} GL(glGenVertexArrays, void, GLsizei n, GLuint *out) { for (GLsizei i = 0; i < n; i++) out[i] = name_new((void ***)&g_vaos, &g_nvaos, &g_capvaos, sizeof(vao_t)); } GL(glDeleteVertexArrays, void, GLsizei n, const GLuint *v) { for (GLsizei i = 0; i < n; i++) if (v[i] && v[i] < g_nvaos && g_vaos[v[i]]) { free(g_vaos[v[i]]); g_vaos[v[i]] = NULL; if (S.vao == v[i]) S.vao = 0; } } GL(glBindVertexArray, void, GLuint v) { S.vao = v; } @@ -1119,6 +1294,9 @@ GL(glVertexAttribPointer, void, GLuint i, GLint size, GLenum type, uint32_t norm if (i >= 16) { RED1("vertex attribute %u over 15", i); return; } vattr_t *a = &cur_vao()->a[i]; a->size = size == 0x80e1 ? 4 : size; a->type = type; a->norm = (norm & 0xff) != 0; a->stride = stride; a->off = (uintptr_t)ptr; a->buf = S.array_buf; } +GL(glVertexAttribDivisor, void, GLuint i, GLuint d) { if (i < 16) cur_vao()->a[i].div = d > 255 ? 255 : (uint8_t)d; } +GL(glVertexAttrib4f, void, GLuint i, GLfloat x, GLfloat y, GLfloat z, GLfloat w) { if (i < 16) { S.va[i][0] = x; S.va[i][1] = y; S.va[i][2] = z; S.va[i][3] = w; } } +GL(glVertexAttrib4fv, void, GLuint i, const GLfloat *v) { if (i < 16) memcpy(S.va[i], v, sizeof S.va[i]); } // ---------- Textures ---------- GL(glGenTextures, void, GLsizei n, GLuint *out) { for (GLsizei i = 0; i < n; i++) out[i] = name_new((void ***)&g_texs, &g_ntexs, &g_captexs, sizeof(tex_t)); } @@ -1126,22 +1304,23 @@ GL(glDeleteTextures, void, GLsizei n, const GLuint *t) { for (GLsizei i = 0; i < n; i++) { tex_t *x = tex_of(t[i]); if (!x) continue; tex_drop(x); x->smp = nil; free(x); g_texs[t[i]] = NULL; - for (int u = 0; u < 32; u++) { if (S.units[u].t2d == t[i]) S.units[u].t2d = 0; if (S.units[u].tcube == t[i]) S.units[u].tcube = 0; } + for (int u = 0; u < NUNITS; u++) { + if (S.units[u].t2d == t[i]) S.units[u].t2d = 0; if (S.units[u].tcube == t[i]) S.units[u].tcube = 0; + if (S.units[u].t3d == t[i]) S.units[u].t3d = 0; if (S.units[u].t2da == t[i]) S.units[u].t2da = 0; + } } } -GL(glActiveTexture, void, GLenum u) { if (u >= GL_TEXTURE0 && u < GL_TEXTURE0 + 32) S.unit = u - GL_TEXTURE0; } +GL(glActiveTexture, void, GLenum u) { if (u >= GL_TEXTURE0 && u < GL_TEXTURE0 + NUNITS) S.unit = u - GL_TEXTURE0; } GL(glBindTexture, void, GLenum target, GLuint t) { + GLuint *slot = target == GL_TEXTURE_2D || target == GL_TEXTURE_CUBE_MAP || target == GL_TEXTURE_3D || target == GL_TEXTURE_2D_ARRAY ? unit_slot(S.unit, target) : NULL; + if (!slot) { RED1("texture target %#x not implemented", target); return; } tex_t *x = tex_of(t); if (t && !x) { // a name glGenTextures never gave: GL makes the texture on its first bind if (t > 1u << 20) { RED1("glBindTexture of name %u", t); return; } x = name_at((void ***)&g_texs, &g_ntexs, &g_captexs, t, sizeof(tex_t)); } if (x && !x->target) tex_init_params(x, target); - if (target == GL_TEXTURE_2D) S.units[S.unit].t2d = t; else if (target == GL_TEXTURE_CUBE_MAP) S.units[S.unit].tcube = t; else RED1("texture target %#x not implemented", target); -} -static tex_t *bound_tex(GLenum target) { - if (target >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && target <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z) target = GL_TEXTURE_CUBE_MAP; - return tex_of(target == GL_TEXTURE_2D ? S.units[S.unit].t2d : target == GL_TEXTURE_CUBE_MAP ? S.units[S.unit].tcube : 0); + *slot = t; } static void tex_param(GLenum target, GLenum p, float f, GLint i) { tex_t *t = bound_tex(target); @@ -1152,8 +1331,11 @@ static void tex_param(GLenum target, GLenum p, float f, GLint i) { case GL_TEXTURE_MAX_LEVEL: t->maxlv = i; break; case GL_TEXTURE_MAX_ANISOTROPY: t->aniso = f; break; case GL_TEXTURE_COMPARE_MODE: t->cmp = i ? (t->cmp ? t->cmp : 0x203) : 0; break; // COMPARE_REF_TO_TEXTURE, the function defaults to LEQUAL case GL_TEXTURE_COMPARE_FUNC: if (t->cmp) t->cmp = i; break; - case GL_TEXTURE_LOD_BIAS: RED1("GL_TEXTURE_LOD_BIAS %g: Metal samplers have no bias (the shaders pass theirs)", f); break; + case GL_TEXTURE_LOD_BIAS: if (f != 0) RED1("GL_TEXTURE_LOD_BIAS %g: Metal samplers have no bias (the shaders pass theirs)", f); break; // LÖVE sets −(mipmap sharpness), 0 by default case 0x813a: case 0x813b: case 0x813c: break; // MIN_LOD / MAX_LOD / BASE_LEVEL + case GL_TEXTURE_SWIZZLE_R: case GL_TEXTURE_SWIZZLE_R + 1: case GL_TEXTURE_SWIZZLE_R + 2: case GL_TEXTURE_SWIZZLE_A: // fixed once the texture is made (tex_make) + if (t->made && t->swz[p - GL_TEXTURE_SWIZZLE_R] != i) RED1("texture swizzle changed after the texture was made: ignored"); + t->swz[p - GL_TEXTURE_SWIZZLE_R] = i; break; default: RED1("glTexParameter(%#x) not implemented", p); break; } t->smp_dirty = 1; @@ -1184,7 +1366,7 @@ static void teximg_impl(const teximg_args *a) { for (GLsizei r = 0; r < a->h; r++) memcpy((uint8_t *)l->data + (a->y + r) * l->pitch + (size_t)a->x * g_fmt[f].bytes, (uint8_t *)conv + r * pitch, (size_t)a->w * g_fmt[f].bytes); else tex_make(t, 0); } - if (t->made && t->tex && a->level < t->levels) tex_write(t, face, a->level, a->x, a->y, a->w, a->h, conv, pitch); + if (t->made && t->tex && a->level < t->levels) tex_write(t, face, a->level, a->x, a->y, 0, a->w, a->h, conv, pitch); free(conv); return; } @@ -1210,6 +1392,71 @@ GL(glTexSubImage2D, void, GLenum target, GLint level, GLint x, GLint y, GLsizei GL(glCompressedTexImage2D, void, GLenum target, GLint level, GLenum ifmt, GLsizei w, GLsizei h, GLint border, GLsizei size, const void *data) { (void)border; teximg_args a = {target, level, (GLint)ifmt, 0, 0, w, h, 0, 0, data, size, 0, 1}; ((void (*)(const teximg_args *))(const void *)teximg_impl_bs)(&a); } +// glTexStorage2D / 3D: the format, the size and the level count, before any data (LÖVE makes every texture so, the mipmapped ones with all their levels, which glGenerateMipmap +// fills). The Metal texture is made at its first upload, attachment or sampling, as one glTexImage2D gives (tex_make: shared, or private for a render target) +static void tex_storage(GLenum target, GLsizei levels, GLenum ifmt, GLsizei w, GLsizei h, GLsizei d) { + tex_t *t = bound_tex(target); + int cv, f = fmt_pick((GLint)ifmt, 0, 0, &cv); + if (!t || f < 0 || levels <= 0 || w <= 0 || h <= 0 || d <= 0) { RED1("glTexStorage(%#x, %d levels, %#x, %dx%dx%d) not implemented", target, levels, ifmt, w, h, d); return; } + if (t->made || t->defined) tex_drop(t); + t->fmt = f; t->w = w; t->h = h; t->d = d; t->slevels = levels; t->smp_dirty = 1; +} +GL(glTexStorage2D, void, GLenum target, GLsizei levels, GLenum ifmt, GLsizei w, GLsizei h) { tex_storage(target, levels, ifmt, w, h, 1); } +GL(glTexStorage3D, void, GLenum target, GLsizei levels, GLenum ifmt, GLsizei w, GLsizei h, GLsizei d) { tex_storage(target, levels, ifmt, w, h, d); } +// 3D and array textures (LÖVE makes a 1x1x1 one of each at start for its default textures): data through glTexSubImage3D, a layer (or a 3D slice) at a time. +// glTexSubImage3D's last three arguments are on the stack: 64-bit here, so the Apple and AAPCS64 stack layouts agree +typedef struct { GLenum target; GLint level, x, y, z; GLsizei w, h, d; GLenum format, type; const void *pixels; } tex3d_args; +static void tex3d_impl(const tex3d_args *a) __attribute__((used)); +BIGSTUB(tex3d_impl); +static void tex3d_impl(const tex3d_args *a) { + @autoreleasepool { + tex_t *t = bound_tex(a->target); + if (!t || t->fmt < 0 || a->w <= 0 || a->h <= 0 || a->d <= 0 || !a->pixels) return; + if (!t->made) tex_make(t, 0); + if (!t->tex || a->level >= t->levels) return; + int f = t->fmt, cv = f == F_RGBA8 && a->format == GL_RGB ? CV_RGB8 : CV_COPY; + size_t pitch; void *conv = pixels_convert(a->pixels, a->w, a->h * a->d, a->format, a->type, f, cv, &pitch); // the layers one after the other + size_t layer = pitch * fmt_rows(f, a->h); + for (GLsizei k = 0; k < a->d; k++) { + if (t->target == GL_TEXTURE_3D) tex_write(t, 0, a->level, a->x, a->y, a->z + k, a->w, a->h, (uint8_t *)conv + k * layer, pitch); + else tex_write(t, a->z + k, a->level, a->x, a->y, 0, a->w, a->h, (uint8_t *)conv + k * layer, pitch); + } + free(conv); + } +} +GL(glTexSubImage3D, void, GLenum target, GLint level, GLint x, GLint y, GLint z, GLsizei w, GLsizei h, GLsizei d, uint64_t format, uint64_t type, const void *pixels) { + tex3d_args a = {target, level, x, y, z, w, h, d, (GLenum)format, (GLenum)type, pixels}; ((void (*)(const tex3d_args *))(const void *)tex3d_impl_bs)(&a); +} +GL(glTexImage3D, void, GLenum target, GLint level, GLint ifmt, GLsizei w, GLsizei h, GLsizei d, GLint border, GLenum format, uint64_t type, const void *pixels) { + (void)border; + if (level) { RED1("glTexImage3D of level %d not implemented", level); return; } + tex_storage(target, 1, (GLenum)ifmt, w, h, d); + if (pixels) h_glTexSubImage3D(target, 0, 0, 0, 0, w, h, d, format, type, pixels); +} +// Mipmaps from level 0, in command order (after the draws that rendered level 0 this frame): a texture of one level gets its full chain first, level 0 copied over +static void mip_impl(GLenum target) __attribute__((used)); +BIGSTUB(mip_impl); +static void mip_impl(GLenum target) { + @autoreleasepool { + tex_t *t = bound_tex(target); + if (!t || (!t->made && !tex_make(t, 0)) || !t->tex) return; + if (g_fmt[t->fmt].kind != K_COL) { RED1("glGenerateMipmap of a %s texture not implemented", g_fmt[t->fmt].name); return; } + id cb; + if (t->use >= g_serial) { pass_end(); cb = cb_get(); } else { cb = [g_q commandBuffer]; cb.label = @"gl mipmaps"; } + id b = [cb blitCommandEncoder]; + if (t->levels == 1 && umax(t->w, t->h) > 1) { + id old = t->tex; t->made = 0; t->tex = nil; t->defined = 0; t->slevels = MAXLV; + tex_make(t, old.storageMode == MTLStorageModePrivate); + if (!t->tex) { [b endEncoding]; return; } + for (int f = 0; f < tex_faces(t); f++) [b copyFromTexture:old sourceSlice:f sourceLevel:0 toTexture:t->tex destinationSlice:f destinationLevel:0 sliceCount:1 levelCount:1]; + t->gen++; + } + if (t->levels > 1) [b generateMipmapsForTexture:t->tex]; + [b endEncoding]; g_n_blit++; t->use = g_serial; + if (cb != g_cb) { dm_gfx_enter(); cb_commit_one(cb); dm_gfx_leave(); } + } +} +GL(glGenerateMipmap, void, GLenum target) { ((void (*)(GLenum))(const void *)mip_impl_bs)(target); } // ---------- Framebuffers and renderbuffers ---------- GL(glGenFramebuffers, void, GLsizei n, GLuint *out) { for (GLsizei i = 0; i < n; i++) out[i] = name_new((void ***)&g_fbs, &g_nfbs, &g_capfbs, sizeof(fb_t)); } @@ -1217,14 +1464,35 @@ GLB(glDeleteFramebuffers, void, GLsizei n, const GLuint *f) { for (GLsizei i = 0; i < n; i++) { fb_t *x = fb_of(f[i]); if (!x) continue; if (P.open && P.fb == f[i]) pass_end(); if (g_pend_clear.fb == f[i]) g_pend_clear.bits = 0; free(x); g_fbs[f[i]] = NULL; if (S.fb == f[i]) S.fb = 0; } } GLB(glBindFramebuffer, void, GLenum target, GLuint f) { - (void)target; - if (f == S.fb) return; + if (target != GL_DRAW_FRAMEBUFFER) S.read_fb = f; // GL_FRAMEBUFFER binds both + if (target == GL_READ_FRAMEBUFFER || f == S.fb) return; if (g_pend_clear.bits && g_pend_clear.fb == S.fb) clears_flush(); S.fb = f; // the pass ends at the next draw or clear into another framebuffer (pass_ensure) } +GL(glCheckFramebufferStatus, GLenum, GLenum target) { (void)target; return GL_FRAMEBUFFER_COMPLETE; } +// One colour attachment: GL_NONE (LÖVE's depth-only canvases) and attachment 0 are the same draw buffer here +GL(glDrawBuffers, void, GLsizei n, const GLenum *b) { if (n > 1) RED1("glDrawBuffers of %d buffers (one colour attachment is implemented)", n); (void)b; } +GL(glDrawBuffer, void, GLenum b) { (void)b; } +GL(glReadBuffer, void, GLenum b) { (void)b; } +GL(glInvalidateFramebuffer, void, GLenum target, GLsizei n, const GLenum *att) { (void)target; (void)n; (void)att; } // the contents are kept (a store action) +GL(glDiscardFramebufferEXT, void, GLenum target, GLsizei n, const GLenum *att) { (void)target; (void)n; (void)att; } GL(glGenRenderbuffers, void, GLsizei n, GLuint *out) { for (GLsizei i = 0; i < n; i++) out[i] = name_new((void ***)&g_rbs, &g_nrbs, &g_caprbs, sizeof(rb_t)); } GL(glDeleteRenderbuffers, void, GLsizei n, const GLuint *r) { for (GLsizei i = 0; i < n; i++) { rb_t *x = rb_of(r[i]); if (!x) continue; x->tex = nil; free(x); g_rbs[r[i]] = NULL; } } GL(glBindRenderbuffer, void, GLenum target, GLuint r) { (void)target; S.rb = r; } +static void h_glRenderbufferStorage(GLenum target, GLenum ifmt, GLsizei w, GLsizei h); +GLB(glRenderbufferStorageMultisample, void, GLenum target, GLsizei samples, GLenum ifmt, GLsizei w, GLsizei h) { + if (samples > 1) RED1("multisampled renderbuffer (%d samples): made with one sample", samples); + h_glRenderbufferStorage(target, ifmt, w, h); +} +GL(glGetRenderbufferParameteriv, void, GLenum target, GLenum p, GLint *v) { + (void)target; rb_t *r = rb_of(S.rb); + switch (p) { + case GL_RENDERBUFFER_SAMPLES: *v = 0; return; + case GL_RENDERBUFFER_WIDTH: *v = r ? (GLint)r->w : 0; return; + case GL_RENDERBUFFER_HEIGHT: *v = r ? (GLint)r->h : 0; return; + } + RED1("glGetRenderbufferParameteriv(%#x) not implemented", p); *v = 0; +} GLB(glRenderbufferStorage, void, GLenum target, GLenum ifmt, GLsizei w, GLsizei h) { (void)target; rb_t *r = rb_of(S.rb); int cv, f = fmt_pick((GLint)ifmt, GL_RGBA, GL_UNSIGNED_BYTE, &cv); @@ -1236,8 +1504,8 @@ GLB(glRenderbufferStorage, void, GLenum target, GLenum ifmt, GLsizei w, GLsizei } OLOG("renderbuffer %u: %dx%d %s", S.rb, w, h, g_fmt[f].name); } -static void fb_attach(GLenum attachment, int kind, GLuint name, int level, int face) { - fb_t *f = fb_of(S.fb); +static void fb_attach(GLenum target, GLenum attachment, int kind, GLuint name, int level, int face) { + fb_t *f = fb_of(target == GL_READ_FRAMEBUFFER ? S.read_fb : S.fb); if (!f) { RED1("attachment to framebuffer 0 or to one never generated"); return; } att_t a = {kind, name, level, face}; if (!name) a.kind = 0; @@ -1249,11 +1517,83 @@ static void fb_attach(GLenum attachment, int kind, GLuint name, int level, int f f->gen++; } GL(glFramebufferTexture2D, void, GLenum target, GLenum att, GLenum textarget, GLuint t, GLint level) { - (void)target; int face = textarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && textarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z ? (int)(textarget - GL_TEXTURE_CUBE_MAP_POSITIVE_X) : 0; + int face = textarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X && textarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z ? (int)(textarget - GL_TEXTURE_CUBE_MAP_POSITIVE_X) : 0; tex_t *x = tex_of(t); if (x && !x->target) tex_init_params(x, textarget == GL_TEXTURE_2D ? GL_TEXTURE_2D : GL_TEXTURE_CUBE_MAP); - fb_attach(att, 1, t, level, face); + fb_attach(target, att, 1, t, level, face); +} +// A layer of an array texture or a slice of a 3D one +GL(glFramebufferTextureLayer, void, GLenum target, GLenum att, GLuint t, GLint level, GLint layer) { fb_attach(target, att, 1, t, level, layer); } +GL(glFramebufferTexture3D, void, GLenum target, GLenum att, GLenum textarget, GLuint t, GLint level, GLint z) { (void)textarget; fb_attach(target, att, 1, t, level, z); } +GL(glFramebufferRenderbuffer, void, GLenum target, GLenum att, GLenum rbt, GLuint r) { (void)rbt; fb_attach(target, att, 2, r, 0, 0); } +// A framebuffer's colour image: the texture, its level and its slice (a cube face or an array layer) or 3D slice +static id fb_color(GLuint fb, int *level, int *slice) { + *level = *slice = 0; + if (!fb) { if (!g_win.made) { int w, h; dm_vi_layer_size(&w, &h); win_make(w > 0 ? w : 1280, h > 0 ? h : 720); } return g_win.col[g_win.cur]; } + fb_t *f = fb_of(fb); uint64_t g = 0; + if (!f || f->col.kind == 0) return nil; + *level = f->col.level; *slice = f->col.face; + return att_tex(&f->col, &g); +} +// glBlitFramebuffer of colour without scaling, flipping or a format change (LÖVE resolves multisampled canvases so); its last two arguments are on the stack, 64-bit here +typedef struct { GLint s[4], d[4]; GLbitfield mask; } blit_args; +static void blit_impl(const blit_args *a) __attribute__((used)); +BIGSTUB(blit_impl); +static void blit_impl(const blit_args *a) { + @autoreleasepool { + if (a->mask & ~GL_COLOR_BUFFER_BIT) RED1("glBlitFramebuffer of depth or stencil not implemented"); + if (!(a->mask & GL_COLOR_BUFFER_BIT)) return; + if (g_pend_clear.bits) clears_flush(); + int sl, ss, dl, ds; id src = fb_color(S.read_fb, &sl, &ss), dst = fb_color(S.fb, &dl, &ds); + int w = a->s[2] - a->s[0], h = a->s[3] - a->s[1]; + if (!src || !dst || w <= 0 || h <= 0 || w != a->d[2] - a->d[0] || h != a->d[3] - a->d[1] || src.pixelFormat != dst.pixelFormat || src.sampleCount != dst.sampleCount) { + RED1("glBlitFramebuffer with scaling, flipping or a format change not implemented"); return; + } + pass_end(); + id b = [cb_get() blitCommandEncoder]; + [b copyFromTexture:src sourceSlice:ss sourceLevel:sl sourceOrigin:MTLOriginMake(a->s[0], a->s[1], 0) sourceSize:MTLSizeMake(w, h, 1) + toTexture:dst destinationSlice:ds destinationLevel:dl destinationOrigin:MTLOriginMake(a->d[0], a->d[1], 0)]; + [b endEncoding]; g_n_blit++; + } +} +GL(glBlitFramebuffer, void, GLint sx0, GLint sy0, GLint sx1, GLint sy1, GLint dx0, GLint dy0, GLint dx1, GLint dy1, uint64_t mask, uint64_t filter) { + (void)filter; blit_args a = {{sx0, sy0, sx1, sy1}, {dx0, dy0, dx1, dy1}, (GLbitfield)mask}; ((void (*)(const blit_args *))(const void *)blit_impl_bs)(&a); +} +GL(glResolveMultisampleFramebufferAPPLE, void, void) { RED1("glResolveMultisampleFramebufferAPPLE not implemented"); } +// glReadPixels: everything recorded so far runs first (the frame's command buffer is committed and waited for), then the rows are copied out as GL lays them, bottom row first, +// which is how this layer stores them. RGBA / unsigned byte from an RGBA8 or BGRA8 image (LÖVE's Canvas:newImageData and screenshots) +typedef struct { GLint x, y; GLsizei w, h; GLenum format, type; void *data; } readpix_args; +static void readpix_impl(const readpix_args *a) __attribute__((used)); +BIGSTUB(readpix_impl); +static void readpix_impl(const readpix_args *a) { + @autoreleasepool { + if (a->w <= 0 || a->h <= 0 || !a->data) return; + int lv, sl; id src = fb_color(S.read_fb, &lv, &sl); + int bgra = src && src.pixelFormat == MTLPixelFormatBGRA8Unorm; + if (!src || (src.pixelFormat != MTLPixelFormatRGBA8Unorm && src.pixelFormat != MTLPixelFormatRGBA8Unorm_sRGB && !bgra) || a->format != GL_RGBA || a->type != GL_UNSIGNED_BYTE) { + RED1("glReadPixels(%#x, %#x) of a %lu image not implemented", a->format, a->type, src ? (unsigned long)src.pixelFormat : 0ul); return; + } + if (g_pend_clear.bits) clears_flush(); + dm_gfx_enter(); + pass_end(); + size_t pitch = (size_t)a->w * 4; id out = [g_dev newBufferWithLength:pitch * a->h options:MTLResourceStorageModeShared]; + id cb = cb_get(); + id b = [cb blitCommandEncoder]; + [b copyFromTexture:src sourceSlice:sl sourceLevel:lv sourceOrigin:MTLOriginMake(a->x, a->y, 0) sourceSize:MTLSizeMake(a->w, a->h, 1) toBuffer:out destinationOffset:0 + destinationBytesPerRow:pitch destinationBytesPerImage:pitch * a->h]; + [b endEncoding]; + cb_commit(); [cb waitUntilCompleted]; + dm_gfx_leave(); + size_t dp = (pitch + S.pack_align - 1) / S.pack_align * S.pack_align; + for (GLsizei r = 0; r < a->h; r++) { + uint8_t *d = (uint8_t *)a->data + r * dp; const uint8_t *s = (const uint8_t *)out.contents + r * pitch; + if (!bgra) memcpy(d, s, pitch); + else for (GLsizei x = 0; x < a->w; x++) { d[4 * x] = s[4 * x + 2]; d[4 * x + 1] = s[4 * x + 1]; d[4 * x + 2] = s[4 * x]; d[4 * x + 3] = s[4 * x + 3]; } + } + } +} +GL(glReadPixels, void, GLint x, GLint y, GLsizei w, GLsizei h, GLenum format, GLenum type, void *data) { + readpix_args a = {x, y, w, h, format, type, data}; ((void (*)(const readpix_args *))(const void *)readpix_impl_bs)(&a); } -GL(glFramebufferRenderbuffer, void, GLenum target, GLenum att, GLenum rbt, GLuint r) { (void)target; (void)rbt; fb_attach(att, 2, r, 0, 0); } // ---------- Shaders and programs ---------- static void sh_capture(const shader_t *s) { @@ -1336,15 +1676,34 @@ GLB(glLinkProgram, void, GLuint pr) { else { const shsamp *m = &si->t[i - si->nu]; u->type = m->type; u->slot[st] = m->slot; } } } + // the game's attribute indices: glBindAttribLocation's for the inputs it names, the input's own number for the others (or the lowest index no binding took) + const shinfo *vi = p->si[0]; uint32_t taken = 0; + for (int s = 0; s < 16; s++) p->amap[s] = (int8_t)s; + for (int i = 0; i < vi->na; i++) for (int j = 0; j < p->nbind; j++) + if (!strcmp(p->bind[j].name, vi->a[i].name)) { p->amap[vi->a[i].slot] = (int8_t)p->bind[j].index; taken |= 1u << p->bind[j].index; } + for (int i = 0; i < vi->na; i++) { + int bound = 0; for (int j = 0; j < p->nbind; j++) bound |= !strcmp(p->bind[j].name, vi->a[i].name); + if (bound) continue; + int g = vi->a[i].slot; for (int n = 0; n < 16 && (taken >> g & 1); n++) g = (g + 1) % 16; + p->amap[vi->a[i].slot] = (int8_t)g; taken |= 1u << g; + } p->serial = ++serials; p->ub_gen[0]++; p->ub_gen[1]++; p->linked = 1; OLOG("LinkProgram %u: vs %s fs %s, %d uniforms", pr, vs->key, fs->key, p->nu); } +GL(glBindAttribLocation, void, GLuint pr, GLuint index, const GLchar *name) { // takes effect at the next link + prog_t *p = prog_of(pr); if (!p || index >= 16 || !name) return; + int j = 0; while (j < p->nbind && strcmp(p->bind[j].name, name)) j++; + if (j == 16) { RED1("more than 16 attribute bindings in a program"); return; } + if (j == p->nbind) { strlcpy(p->bind[j].name, name, sizeof p->bind[j].name); p->nbind++; } + p->bind[j].index = (int)index; +} GL(glGetProgramiv, void, GLuint pr, GLenum p, GLint *v) { prog_t *x = prog_of(pr); if (!x) { *v = 0; return; } switch (p) { case GL_LINK_STATUS: case GL_VALIDATE_STATUS: *v = 1; return; // as glGetShaderiv's compile status case GL_INFO_LOG_LENGTH: *v = 0; return; case GL_ACTIVE_UNIFORMS: *v = x->nu; return; + case GL_ACTIVE_UNIFORM_MAX_LENGTH: { int m = 0; for (int i = 0; i < x->nu; i++) m = (int)umax(m, (uint32_t)strlen(x->u[i].name) + 4); *v = m; return; } case GL_ATTACHED_SHADERS: *v = (x->vs != 0) + (x->fs != 0); return; case GL_ACTIVE_ATTRIBUTES: *v = x->si[0] ? x->si[0]->na : 0; return; case GL_DELETE_STATUS: *v = 0; return; @@ -1355,9 +1714,26 @@ GL(glDeleteProgram, void, GLuint pr) { prog_t *p = prog_of(pr); if (!p) return; GL(glUseProgram, void, GLuint pr) { S.prog = pr; } GL(glGetAttribLocation, GLint, GLuint pr, const GLchar *name) { prog_t *p = prog_of(pr); if (!p || !p->linked) return -1; - for (int i = 0; i < p->si[0]->na; i++) if (!strcmp(p->si[0]->a[i].name, name)) return p->si[0]->a[i].slot; + for (int i = 0; i < p->si[0]->na; i++) if (!strcmp(p->si[0]->a[i].name, name)) return p->amap[p->si[0]->a[i].slot]; return -1; } +GL(glGetProgramInfoLog, void, GLuint pr, GLsizei size, GLsizei *len, GLchar *log) { (void)pr; if (len) *len = 0; if (log && size > 0) log[0] = 0; } +// The uniforms in the order of the program's table (both stages merged, samplers among them): an array's name ends in [0], as GL reports it (LÖVE's mapActiveUniforms) +GL(glGetActiveUniform, void, GLuint pr, GLuint index, GLsizei size, GLsizei *len, GLint *count, GLenum *type, GLchar *name) { + prog_t *p = prog_of(pr); + if (!p || !p->linked || index >= (GLuint)p->nu) { if (len) *len = 0; if (name && size > 0) name[0] = 0; return; } + const puni_t *u = &p->u[index]; char nm[64]; + snprintf(nm, sizeof nm, "%s%s", u->name, u->count ? "[0]" : ""); + if (name && size > 0) strlcpy(name, nm, (size_t)size); + if (len) *len = name && size > 0 ? (GLsizei)umin((uint32_t)strlen(nm), (uint32_t)size - 1) : 0; + if (count) *count = u->count ? u->count : 1; + if (type) *type = g_tenums[u->type]; +} +GL(glGetShaderPrecisionFormat, void, GLenum stage, GLenum prec, GLint *range, GLint *precision) { // every precision is a 32-bit float or int + (void)stage; int fl = prec <= 0x8df2; // LOW / MEDIUM / HIGH_FLOAT, then the INTs + if (range) { range[0] = fl ? 127 : 31; range[1] = fl ? 127 : 30; } + if (precision) *precision = fl ? 23 : 0; +} GL(glGetUniformLocation, GLint, GLuint pr, const GLchar *name) { prog_t *p = prog_of(pr); if (!p || !p->linked) return -1; char nm[64]; strlcpy(nm, name, sizeof nm); int elem = 0; @@ -1366,47 +1742,86 @@ GL(glGetUniformLocation, GLint, GLuint pr, const GLchar *name) { for (int i = 0; i < p->nu; i++) if (!strcmp(p->u[i].name, nm)) return (elem < 0 || elem >= (p->u[i].count ? p->u[i].count : 1)) ? -1 : i * 4096 + elem; return -1; } -// Write count elements from location loc of the current program: comps floats (or ints) each, into every stage that has the uniform -static void uni_write(GLint loc, GLsizei count, const void *v, int comps, int is_int, int transpose) { +// Write count elements from location loc of the current program: comps values each (a matrix: cols columns of comps / cols), into every stage that has the uniform. +// The default block is laid out as std140: a matrix column takes 16 bytes +static void uni_write(GLint loc, GLsizei count, const void *v, int comps, int is_int, int transpose, int cols) { prog_t *p = prog_of(S.prog); if (!p || !p->linked || loc < 0 || count <= 0) return; int i = loc / 4096, el = loc % 4096; if (i >= p->nu) return; puni_t *u = &p->u[i]; - if (u->type == T_SAMP2D || u->type == T_SAMPCUBE) { if (is_int) u->unit = *(const GLint *)v; return; } - int n = u->count ? u->count : 1; + if (is_samp(u->type)) { if (is_int) u->unit = *(const GLint *)v; return; } + int n = u->count ? u->count : 1, rows = cols ? comps / cols : comps, size = cols ? cols * 16 : 16; if (el + count > n) count = n - el; for (int st = 0; st < 2; st++) { if (u->off[st] < 0) continue; - int stride = u->stride[st] ? u->stride[st] : (u->type == T_MAT4 ? 64 : u->type == T_MAT3 ? 48 : u->type == T_MAT2 ? 32 : 16); + int stride = u->stride[st] ? u->stride[st] : size; for (GLsizei k = 0; k < count; k++) { uint8_t *d = p->ub[st] + u->off[st] + (size_t)(el + k) * stride; - if (d + 16 > p->ub[st] + p->si[st]->ub_size + 16) break; + if (d + size > p->ub[st] + p->si[st]->ub_size + 16) break; const float *s = (const float *)v + (size_t)k * comps; - if (u->type == T_MAT4 && comps == 16) { - if (!transpose) memcpy(d, s, 64); - else for (int c = 0; c < 4; c++) for (int r = 0; r < 4; r++) ((float *)d)[c * 4 + r] = s[r * 4 + c]; - } else if (u->type == T_MAT3 && comps == 9) { for (int c = 0; c < 3; c++) memcpy(d + c * 16, s + c * 3, 12); } - else if (u->type == T_MAT2 && comps == 4) { memcpy(d, s, 8); memcpy(d + 16, s + 2, 8); } + if (cols) { for (int c = 0; c < cols; c++) for (int r = 0; r < rows; r++) ((float *)(d + c * 16))[r] = transpose ? s[r * cols + c] : s[c * rows + r]; } else memcpy(d, s, (size_t)umin(comps, tcomps(u->type)) * 4); } p->ub_gen[st]++; } - (void)is_int; } -GL(glUniform1fv, void, GLint loc, GLsizei n, const GLfloat *v) { uni_write(loc, n, v, 1, 0, 0); } -GL(glUniform2fv, void, GLint loc, GLsizei n, const GLfloat *v) { uni_write(loc, n, v, 2, 0, 0); } -GL(glUniform3fv, void, GLint loc, GLsizei n, const GLfloat *v) { uni_write(loc, n, v, 3, 0, 0); } -GL(glUniform4fv, void, GLint loc, GLsizei n, const GLfloat *v) { uni_write(loc, n, v, 4, 0, 0); } -GL(glUniform1i, void, GLint loc, GLint v) { +// Integer values for a float uniform are converted (GL would reject them; NVIDIA's driver converts), as are float values for an integer one +static void uni_ints(GLint loc, GLsizei n, const GLint *v, int comps) { prog_t *p = prog_of(S.prog); - if (p && p->linked && loc >= 0 && loc / 4096 < p->nu) { - puni_t *u = &p->u[loc / 4096]; - if (u->type == T_FLOAT) { float f = (float)v; uni_write(loc, 1, &f, 1, 0, 0); return; } // GL would reject it; NVIDIA's driver converts + if (p && p->linked && loc >= 0 && loc / 4096 < p->nu && !is_samp(p->u[loc / 4096].type) && !is_intt(p->u[loc / 4096].type)) { + float f[64]; for (GLsizei k = 0; k < n && (k + 1) * comps <= 64; k++) for (int c = 0; c < comps; c++) f[k * comps + c] = (float)v[k * comps + c]; + uni_write(loc, n > 64 / comps ? 64 / comps : n, f, comps, 0, 0, 0); return; } - uni_write(loc, 1, &v, 1, 1, 0); + uni_write(loc, n, v, comps, 1, 0, 0); } -GL(glUniformMatrix4fv, void, GLint loc, GLsizei n, uint32_t transpose, const GLfloat *v) { uni_write(loc, n, v, 16, 0, (transpose & 0xff) != 0); } +static void uni_floats(GLint loc, GLsizei n, const GLfloat *v, int comps) { + prog_t *p = prog_of(S.prog); + if (p && p->linked && loc >= 0 && loc / 4096 < p->nu && is_intt(p->u[loc / 4096].type)) { + GLint iv[64]; for (GLsizei k = 0; k < n && (k + 1) * comps <= 64; k++) for (int c = 0; c < comps; c++) iv[k * comps + c] = (GLint)v[k * comps + c]; + uni_write(loc, n > 64 / comps ? 64 / comps : n, iv, comps, 1, 0, 0); return; + } + uni_write(loc, n, v, comps, 0, 0, 0); +} +GL(glUniform1fv, void, GLint loc, GLsizei n, const GLfloat *v) { uni_floats(loc, n, v, 1); } +GL(glUniform2fv, void, GLint loc, GLsizei n, const GLfloat *v) { uni_floats(loc, n, v, 2); } +GL(glUniform3fv, void, GLint loc, GLsizei n, const GLfloat *v) { uni_floats(loc, n, v, 3); } +GL(glUniform4fv, void, GLint loc, GLsizei n, const GLfloat *v) { uni_floats(loc, n, v, 4); } +GL(glUniform1f, void, GLint loc, GLfloat x) { uni_floats(loc, 1, &x, 1); } +GL(glUniform2f, void, GLint loc, GLfloat x, GLfloat y) { const GLfloat v[] = {x, y}; uni_floats(loc, 1, v, 2); } +GL(glUniform3f, void, GLint loc, GLfloat x, GLfloat y, GLfloat z) { const GLfloat v[] = {x, y, z}; uni_floats(loc, 1, v, 3); } +GL(glUniform4f, void, GLint loc, GLfloat x, GLfloat y, GLfloat z, GLfloat w) { const GLfloat v[] = {x, y, z, w}; uni_floats(loc, 1, v, 4); } +GL(glUniform1i, void, GLint loc, GLint v) { uni_ints(loc, 1, &v, 1); } +GL(glUniform1iv, void, GLint loc, GLsizei n, const GLint *v) { uni_ints(loc, n, v, 1); } +GL(glUniform2iv, void, GLint loc, GLsizei n, const GLint *v) { uni_ints(loc, n, v, 2); } +GL(glUniform3iv, void, GLint loc, GLsizei n, const GLint *v) { uni_ints(loc, n, v, 3); } +GL(glUniform4iv, void, GLint loc, GLsizei n, const GLint *v) { uni_ints(loc, n, v, 4); } +GL(glUniform1uiv, void, GLint loc, GLsizei n, const GLuint *v) { uni_write(loc, n, v, 1, 1, 0, 0); } +GL(glUniform2uiv, void, GLint loc, GLsizei n, const GLuint *v) { uni_write(loc, n, v, 2, 1, 0, 0); } +GL(glUniform3uiv, void, GLint loc, GLsizei n, const GLuint *v) { uni_write(loc, n, v, 3, 1, 0, 0); } +GL(glUniform4uiv, void, GLint loc, GLsizei n, const GLuint *v) { uni_write(loc, n, v, 4, 1, 0, 0); } +#define UMAT(name, c, r) GL(name, void, GLint loc, GLsizei n, uint32_t transpose, const GLfloat *v) { uni_write(loc, n, v, (c) * (r), 0, (transpose & 0xff) != 0, c); } +UMAT(glUniformMatrix2fv, 2, 2) UMAT(glUniformMatrix3fv, 3, 3) UMAT(glUniformMatrix4fv, 4, 4) UMAT(glUniformMatrix2x3fv, 2, 3) UMAT(glUniformMatrix2x4fv, 2, 4) +UMAT(glUniformMatrix3x2fv, 3, 2) UMAT(glUniformMatrix3x4fv, 3, 4) UMAT(glUniformMatrix4x2fv, 4, 2) UMAT(glUniformMatrix4x3fv, 4, 3) +// Values read back from the program's own copy (LÖVE keeps the values a shader initialises its uniforms with): the first stage that has the uniform +static void uni_read(GLuint pr, GLint loc, void *out, int as_int) { + prog_t *p = prog_of(pr); + if (!p || !p->linked || loc < 0 || loc / 4096 >= p->nu) return; + const puni_t *u = &p->u[loc / 4096]; int el = loc % 4096; + if (is_samp(u->type)) { if (as_int) *(GLint *)out = u->unit; else *(GLfloat *)out = (GLfloat)u->unit; return; } + int st = u->off[0] >= 0 ? 0 : 1; if (u->off[st] < 0) return; + int cols = u->type == T_MAT4 ? 4 : u->type == T_MAT3 ? 3 : u->type == T_MAT2 ? 2 : 0, rows = cols ? cols : tcomps(u->type); + const uint8_t *d = p->ub[st] + u->off[st] + (size_t)el * (u->stride[st] ? u->stride[st] : cols ? cols * 16 : 16); + for (int c = 0; c < (cols ? cols : 1); c++) for (int r = 0; r < rows; r++) { + const uint8_t *s = d + c * 16 + r * 4; int k = c * rows + r; + if (as_int == is_intt(u->type)) memcpy((uint8_t *)out + k * 4, s, 4); + else if (as_int) { float f; memcpy(&f, s, 4); ((GLint *)out)[k] = (GLint)f; } + else { GLint i; memcpy(&i, s, 4); ((GLfloat *)out)[k] = (GLfloat)i; } + } +} +GL(glGetUniformfv, void, GLuint pr, GLint loc, GLfloat *v) { uni_read(pr, loc, v, 0); } +GL(glGetUniformiv, void, GLuint pr, GLint loc, GLint *v) { uni_read(pr, loc, v, 1); } +GL(glGetUniformuiv, void, GLuint pr, GLint loc, GLuint *v) { uni_read(pr, loc, v, 1); } // ---------- EGL ---------- #define EGL_TRUE 1 @@ -1437,7 +1852,8 @@ EGL(eglGetDisplay, EGLDisplay, void *native) { (void)native; dev_init(); return EGL(eglInitialize, EGLBoolean, EGLDisplay d, EGLint *major, EGLint *minor) { (void)d; dev_init(); g_egl.init = 1; if (major) *major = 1; if (minor) *minor = 5; return EGL_TRUE; } EGL(eglTerminate, EGLBoolean, EGLDisplay d) { (void)d; return EGL_TRUE; } EGL(eglReleaseThread, EGLBoolean, void) { return EGL_TRUE; } -EGL(eglBindAPI, EGLBoolean, EGLenum api) { if (api != 0x30a2) RED1("eglBindAPI(%#x): only desktop OpenGL (0x30a2) is implemented", api); return EGL_TRUE; } +// SDL binds 0 (its API before it has chosen one) when it makes the window again (Balatro's love.window.setMode): nothing to change +EGL(eglBindAPI, EGLBoolean, EGLenum api) { if (api && api != 0x30a2) RED1("eglBindAPI(%#x): only desktop OpenGL (0x30a2) is implemented", api); return EGL_TRUE; } // One configuration: RGBA8, depth 24 (32F here), stencil 8, a window surface, desktop OpenGL; it matches the game's attributes EGL(eglChooseConfig, EGLBoolean, EGLDisplay d, const EGLint *attr, EGLConfig *cfg, EGLint n, EGLint *num) { (void)d; (void)attr; @@ -1445,6 +1861,35 @@ EGL(eglChooseConfig, EGLBoolean, EGLDisplay d, const EGLint *attr, EGLConfig *cf if (num) *num = 1; return EGL_TRUE; } +// The one configuration's attributes (SDL reads them back after eglChooseConfig): what eglChooseConfig says it is +EGL(eglGetConfigAttrib, EGLBoolean, EGLDisplay d, EGLConfig cfg, EGLint attr, EGLint *v) { + (void)d; (void)cfg; + switch (attr) { + case 0x3020: *v = 32; break; // BUFFER_SIZE + case 0x3021: case 0x3022: case 0x3023: case 0x3024: *v = 8; break; // ALPHA / BLUE / GREEN / RED_SIZE + case 0x3025: *v = 24; break; // DEPTH_SIZE + case 0x3026: *v = 8; break; // STENCIL_SIZE + case 0x3028: *v = 1; break; // CONFIG_ID + case 0x3031: case 0x3032: *v = 0; break; // SAMPLES, SAMPLE_BUFFERS + case 0x302e: *v = 0; break; // NATIVE_VISUAL_ID + case 0x3033: *v = 0x5; break; // SURFACE_TYPE: PBUFFER | WINDOW + case 0x3040: case 0x3042: *v = 0x8; break; // RENDERABLE_TYPE, CONFORMANT: OPENGL_BIT + case 0x3027: *v = 0x3038; break; // CONFIG_CAVEAT: NONE + default: RED1("eglGetConfigAttrib(%#x) not implemented (0)", attr); *v = 0; break; + } + return EGL_TRUE; +} +EGL(eglQueryString, const char *, EGLDisplay d, EGLint name) { + (void)d; + switch (name) { case 0x3053: return "NVIDIA"; case 0x3054: return "1.5"; case 0x3055: return "EGL_KHR_create_context"; case 0x308d: return "OpenGL"; } // VENDOR, VERSION, EXTENSIONS, CLIENT_APIS + return NULL; +} +EGL(eglGetError, EGLint, void) { return 0x3000; } // SUCCESS +EGL(eglQueryAPI, EGLenum, void) { return 0x30a2; } // OPENGL_API +EGL(eglWaitNative, EGLBoolean, EGLint engine) { (void)engine; return EGL_TRUE; } +EGL(eglWaitGL, EGLBoolean, void) { return EGL_TRUE; } +EGL(eglDestroyContext, EGLBoolean, EGLDisplay d, EGLContext c) { (void)d; (void)c; return EGL_TRUE; } +EGL(eglCreatePbufferSurface, EGLSurface, EGLDisplay d, EGLConfig cfg, const EGLint *attr) { (void)d; (void)cfg; (void)attr; RED1("eglCreatePbufferSurface: drawing into it is not implemented"); return (EGLSurface)(uintptr_t)4; } EGL(eglCreateContext, EGLContext, EGLDisplay d, EGLConfig cfg, EGLContext share, const EGLint *attr) { (void)d; (void)cfg; int major = 0, minor = 0; for (int i = 0; attr && attr[i] != 0x3038 && i < 32; i += 2) { if (attr[i] == 0x3098) major = attr[i + 1]; else if (attr[i] == 0x30fb) minor = attr[i + 1]; } @@ -1464,8 +1909,10 @@ EGL(eglCreateWindowSurface, EGLSurface, EGLDisplay d, EGLConfig cfg, void *win, EGL(eglDestroySurface, EGLBoolean, EGLDisplay d, EGLSurface s) { (void)d; (void)s; return EGL_TRUE; } EGL(eglMakeCurrent, EGLBoolean, EGLDisplay d, EGLSurface dr, EGLSurface rd, EGLContext c) { (void)d; (void)rd; - static pthread_t th; static int known; + static pthread_t th; static int known, sized; if (c && dr) { pthread_t me = pthread_self(); if (!known || !pthread_equal(th, me)) { th = me; known = 1; OLOG("context current on a new thread"); } } + // a context's viewport and scissor box start as its first drawable's size (LÖVE reads them back at start) + if (c && dr && !sized && g_win.made) { sized = 1; if (!S.vp[2] && !S.vp[3]) { S.vp[2] = S.sc[2] = g_win.w; S.vp[3] = S.sc[3] = g_win.h; } } return EGL_TRUE; } EGL(eglSwapInterval, EGLBoolean, EGLDisplay d, EGLint n) { (void)d; if (n != S.swap_interval) { GLOG("swap interval %d", n); S.swap_interval = n; } return EGL_TRUE; } @@ -1501,12 +1948,30 @@ static void *gl_lookup(const char *name) { for (unsigned long i = 0; i < sz / sizeof *sec; i++) if (!strcmp(sec[i].name, name)) return sec[i].fn; return NULL; } +// DM_GL_STUBS=1 (bring-up): a GL name the layer does not implement gets a stub of its own instead of a null pointer, which reports its first call and returns 0; a game that calls +// such a function then says which one instead of jumping to address 0. Off by default: a loader may take a name's null pointer to mean "not there" +#define NSTUB 4096 +static const char *g_stub_name[NSTUB]; static uint8_t g_stub_seen[NSTUB]; static int g_nstub; +uint64_t gl_stub_c(uint64_t k, uint64_t x0, uint64_t x1, uint64_t x2) { + if (k < NSTUB && !g_stub_seen[k]++) GLOG("[RED] %s called: not implemented, returns 0 (x0=%#llx x1=%#llx x2=%#llx)", g_stub_name[k], x0, x1, x2); + return 0; +} +__asm__(".text\n.p2align 2\ngl_stub_entry:\n mov x3, x2\n mov x2, x1\n mov x1, x0\n mov x0, x9\n b _gl_stub_c\n" + ".p2align 3\n.globl _gl_stubs\n.private_extern _gl_stubs\n_gl_stubs:\n.set gl_stub_k, 0\n.rept 4096\n mov x9, #gl_stub_k\n b gl_stub_entry\n.set gl_stub_k, gl_stub_k + 1\n.endr\n"); +extern const char gl_stubs[]; // 8 bytes a stub +static void *gl_stub(const char *name) { + static int on = -1; if (on < 0) on = getenv("DM_GL_STUBS") && atoi(getenv("DM_GL_STUBS")); + if (!on || g_nstub == NSTUB) return NULL; + g_stub_name[g_nstub] = name; // the game's own string (its loader's name table) + return (void *)(gl_stubs + 8 * g_nstub++); +} EGL(eglGetProcAddress, void *, const char *name) { static int asked, found; static pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER; void *fn = name && !strncmp(name, "gl", 2) ? gl_lookup(name) : NULL; pthread_mutex_lock(&mu); asked++; if (fn) found++; if (asked % 500 == 0 || (fn && g_log)) GLOG("eglGetProcAddress: %d names asked, %d implemented (last %s)", asked, found, name); + if (!fn && name && !strncmp(name, "gl", 2)) fn = gl_stub(name); pthread_mutex_unlock(&mu); return fn; } diff --git a/dm/gfx/tools/gl_shaders.py b/dm/gfx/tools/gl_shaders.py index 0f048ee..b4fd58c 100644 --- a/dm/gfx/tools/gl_shaders.py +++ b/dm/gfx/tools/gl_shaders.py @@ -35,20 +35,14 @@ def text_of(b): return b[:z] if z >= 0 else b def nso_strings(path): - """'#version' strings of an NSO's read-only segment (lz4 segments, as dm/tools/nso.py decompresses them)""" + """'#version ' strings of an NSO's read-only segment (lz4 segments, as dm/tools/nso.py decompresses them); a main that links glslang (Balatro's LÖVE) also has its messages that + start with '#version:'""" import lz4.block, struct d = open(path, 'rb').read() flags = struct.unpack_from('> 1 & 1: img = lz4.block.decompress(img, uncompressed_size=sz) - out, i = [], 0 - while True: - i = img.find(b'#version', i) - if i < 0: break - if i == 0 or img[i - 1] == 0: - out.append(text_of(img[i:i + 0x10000])) - i += 8 - return out + return [text_of(img[m.start():m.start() + 0x10000]) for m in re.finditer(rb'(?:^|(?<=\0))#version [0-9]', img)] def sources(roots): """(source bytes, stage or None, where) for every shader in the input paths""" @@ -130,6 +124,8 @@ def translate(src, stage, key, tmp): for v in refl.get('inputs', []): f.append(f'a:{v["name"]}={v["location"]}:{v["type"]}') else: for v in refl.get('outputs', []): f.append(f'o:{v["name"]}={v["location"]}') + # a bool uniform is a uint in the SPIR-V block (it has no bool members), but the game asks GL for its declared type: LÖVE's Shader:send takes only a boolean for a GLSL bool + bools = {n for n in re.findall(r'\buniform\s+(?:(?:lowp|mediump|highp)\s+)?bool\s+(\w+)\s*;', pre)} for u in refl.get('ubos', []): t = refl['types'][u['type']] if t['name'] != 'gl_DefaultUniformBlock': return f'uniform block {t["name"]} is not the default block' @@ -138,6 +134,7 @@ def translate(src, stage, key, tmp): arr = mb.get('array', []) if len(arr) > 1: return f'uniform {mb["name"]}: arrays of arrays' ty = glsl_type(mb['type'], refl) + if ty == 'uint' and mb['name'] in bools: ty = 'bool' if ty not in VEC_TYPES: return f'uniform {mb["name"]}: type {ty} (structs are not implemented)' f.append(f'u:{mb["name"]}={mb["offset"]}:{ty}:{arr[0] if arr else 0}:{mb.get("array_stride", 0)}') for t in refl.get('textures', []): diff --git a/dm/host/hle_audren.c b/dm/host/hle_audren.c index d3b632d..2334f29 100644 --- a/dm/host/hle_audren.c +++ b/dm/host/hle_audren.c @@ -38,13 +38,6 @@ HLE_FORCE("_ZN2nn5audio30GetAudioRendererWorkBufferSizeERKNS0_22AudioRendererPar { static int once; if (!once++) LOG("audren: work buffer %#zx", n); } return n; } -HLE_FORCE("_ZN2nn5audio17OpenAudioRendererEPNS0_19AudioRendererHandleERKNS0_22AudioRendererParameterEPvm", "open the renderer: switched to CPU rendering + manual execution, then the SDK in-process renderer", - Result, h_ar_Open0, void **handle, const uint8_t *param, void *work, size_t sz) { - static Result (*orig)(void **, const uint8_t *, void *, size_t); - if (!orig) orig = gsym("_ZN2nn5audio17OpenAudioRendererEPNS0_19AudioRendererHandleERKNS0_22AudioRendererParameterEPvm"); - uint8_t p[P_SIZE]; cpu_manual(p, param); return orig(handle, p, work, sz); -} - // ---------- rendering and output ---------- static void *g_handle[2]; // AudioRendererHandle (renderer object, client state) static _Atomic int g_open; @@ -86,21 +79,28 @@ static void render_main(void *arg) { cq_popped(); // an update waiting for queue space (catch-up pacing) sees g_open cleared } -HLE_FORCE("_ZN2nn5audio17OpenAudioRendererEPNS0_19AudioRendererHandleEPNS_2os11SystemEventERKNS0_22AudioRendererParameterEPvm", "open the renderer (with event): CPU rendering + manual execution, the host starts a render thread", - Result, h_ar_Open, void **handle, void *ev, const uint8_t *param, void *work, size_t sz) { - static Result (*orig)(void **, void *, const uint8_t *, void *, size_t); - if (!orig) orig = gsym("_ZN2nn5audio17OpenAudioRendererEPNS0_19AudioRendererHandleEPNS_2os11SystemEventERKNS0_22AudioRendererParameterEPvm"); +// Both OpenAudioRenderer forms: CPU rendering + manual execution, then the host's render thread. With the event the game waits on it, which each render signals; without it the +// game paces its updates itself (Balatro: LÖVE's OpenAL Soft) and still needs the renderer to run every 5 ms: the form without an event used to start no render thread, so only the +// game's updates rendered (catch-up frames, which nothing played) and the game was silent +static Result ar_open(void **handle, void *ev, const uint8_t *param, void *work, size_t sz) { + static Result (*orig_ev)(void **, void *, const uint8_t *, void *, size_t), (*orig)(void **, const uint8_t *, void *, size_t); + if (!orig_ev) orig_ev = gsym("_ZN2nn5audio17OpenAudioRendererEPNS0_19AudioRendererHandleEPNS_2os11SystemEventERKNS0_22AudioRendererParameterEPvm"); + if (!orig) orig = gsym("_ZN2nn5audio17OpenAudioRendererEPNS0_19AudioRendererHandleERKNS0_22AudioRendererParameterEPvm"); uint8_t p[P_SIZE]; cpu_manual(p, param); - LOG("audren: OpenAudioRenderer rate %d samples per frame %d mix buffers %d submixes %d voices %d sinks %d effects %d splitters %d (original device %d mode %d → CPU / manual) work buffer %p+%#zx", - *(int32_t *)param, *(int32_t *)(param + 4), *(int32_t *)(param + 8), *(int32_t *)(param + 0xc), *(int32_t *)(param + 0x10), *(int32_t *)(param + 0x14), + LOG("audren: OpenAudioRenderer%s rate %d samples per frame %d mix buffers %d submixes %d voices %d sinks %d effects %d splitters %d (original device %d mode %d → CPU / manual) work buffer %p+%#zx", + ev ? "" : " (no event)", *(int32_t *)param, *(int32_t *)(param + 4), *(int32_t *)(param + 8), *(int32_t *)(param + 0xc), *(int32_t *)(param + 0x10), *(int32_t *)(param + 0x14), *(int32_t *)(param + 0x18), *(int32_t *)(param + 0x24), *(int32_t *)(param + P_DEVICE), *(int32_t *)(param + P_MODE), work, sz); - Result r = orig(handle, ev, p, work, sz); + Result r = ev ? orig_ev(handle, ev, p, work, sz) : orig(handle, p, work, sz); if (r) { LOG("[RED] audren: SDK OpenAudioRenderer failed %#x", r); return r; } if (atomic_load(&g_open)) { LOG("[RED] audren: a second renderer (the host drives only one)"); return r; } g_handle[0] = handle[0]; g_handle[1] = handle[1]; atomic_store(&g_open, 1); g_render_tt = dm_guest_thread_spawn(render_main, NULL, 9, 0x10000); return r; } +HLE_FORCE("_ZN2nn5audio17OpenAudioRendererEPNS0_19AudioRendererHandleEPNS_2os11SystemEventERKNS0_22AudioRendererParameterEPvm", "open the renderer (with event): CPU rendering + manual execution, the host starts a render thread", + Result, h_ar_Open, void **handle, void *ev, const uint8_t *param, void *work, size_t sz) { return ar_open(handle, ev, param, work, sz); } +HLE_FORCE("_ZN2nn5audio17OpenAudioRendererEPNS0_19AudioRendererHandleERKNS0_22AudioRendererParameterEPvm", "open the renderer (no event): CPU rendering + manual execution, the host starts a render thread", + Result, h_ar_Open0, void **handle, const uint8_t *param, void *work, size_t sz) { return ar_open(handle, NULL, param, work, sz); } HLE_FORCE("_ZN2nn5audio18CloseAudioRendererENS0_19AudioRendererHandleE", "close the renderer: stop the host render thread first, then hand over to the SDK", void, h_ar_Close, void *obj, void *state) { static void (*orig)(void *, void *); if (!orig) orig = gsym("_ZN2nn5audio18CloseAudioRendererENS0_19AudioRendererHandleE"); // Join the render thread first (it checks g_open every round and at most every 10 ms while waiting for the output): it may be inside exec(), which uses the renderer the SDK is about to free. diff --git a/dm/ios/Sources/DmGamepad.m b/dm/ios/Sources/DmGamepad.m index 381c78f..eb35e2f 100644 --- a/dm/ios/Sources/DmGamepad.m +++ b/dm/ios/Sources/DmGamepad.m @@ -1,5 +1,6 @@ // Virtual gamepad (Nintendo button positions) + physical controllers (GameController). Both are merged and handed to the host through dm_input_set (hle_hid.c, bit layout in dm_input.h). -// Two layouts; one tap on the layout button at the top centre (labelled with the current layout, "2D" / "3D") switches them, and the choice is kept in NSUserDefaults (each game is its own app, so per game); the first launch uses the game profile's pad_layout (DM_GAME_PAD_FULL). +// Two layouts, three in handheld mode; one tap on the layout button at the top centre (labelled with the current layout, "2D" / "3D" / "Tap") goes to the next, and the choice is kept in NSUserDefaults (each game is its own app, so per game); the first launch uses the game profile's pad_layout (DM_GAME_PAD_LAYOUT). +// Tap (touch): no control but the layout button, every other touch goes to the game's touch screen; offered only while the game is in handheld mode, the only one in which it reads the touch screen (Balatro, played by touch). // 2D (simple): top corners ZL above L (one column), ZR above R likewise, teardrops with the tip toward the screen corner; − and + side by side at the top centre; the left stick in the bottom-left corner (fixed ring, tilt measured from the ring centre, small dead zone); // the A (right) B (bottom) X (top) Y (left) diamond in the bottom-right corner, sliding a finger between keys switches keys. Each bottom corner holds a single control, so it sits close in (reaching halfway into the side safe-area inset). // 3D (full, Pro Controller style): everything in 2D (left stick and diamond keep their place and size), plus a D-pad (inward of and below the left stick, 8-way), a right stick that is not drawn @@ -88,7 +89,8 @@ static const CGFloat K_RSWIPE = 1.0 / 3, // right stick area: this fraction of the screen width from the right edge (not in u) K_CLICK_X = 102, K_CLICK_R = 17; // L3 / R3: centre from the outer edge (level with ZL / ZR), radius static const int k_btns[] = {B_ZL, B_L, B_ZR, B_R, B_MINUS, B_PLUS, B_LS, B_RS}; // keys locked at touch-down (L3 / R3 only in the 3D layout) -static NSString *const k_layout_key = @"DmPadLayout"; // NSUserDefaults: 1 = 3D, 0 = 2D +static NSString *const k_layout_key = @"DmPadLayout"; // NSUserDefaults: 0 = 2D, 1 = 3D, 2 = Tap (DM_GAME_PAD_LAYOUT's numbers) +int dm_opmode(void); // hle_oe.c: 0 = handheld @interface DmGamepad : UIView @end @@ -104,7 +106,7 @@ static __weak DmGamepad *g_view; CAShapeLayer *_ring, *_knob; // the left stick (the right one is not drawn) CAShapeLayer *_swap; CATextLayer *_swapLabel; BOOL _swapHeld; BOOL _stickShown; // the left stick was held at the last refresh - BOOL _full; // the 3D layout is current + BOOL _full, _tap; // the 3D layout / the Tap layout is current CALayer *_controls; // holds every control layer: hidden while a physical controller is connected (the view itself stays, for game touches) BOOL _padOff; // a physical controller is connected: every touch goes to the game UITouch *_game[16]; // touches on the game's touch screen by finger slot @@ -146,7 +148,9 @@ static BOOL fullOnly(int b) { return b == B_LS || b == B_RS || b >= B_LEFT; } } _swap = shape(_controls); _swapLabel = label(_swap, @""); NSNumber *saved = [NSUserDefaults.standardUserDefaults objectForKey:k_layout_key]; - _full = saved ? saved.boolValue : DM_GAME_PAD_FULL; + int l = saved ? (int)saved.integerValue : DM_GAME_PAD_LAYOUT; + if (l == 2 && dm_opmode() != 0) l = 0; // Tap only in handheld mode (env.txt's DM_OPMODE may have changed it) + _full = l == 1; _tap = l == 2; [self applyLayout]; NSNotificationCenter *nc = NSNotificationCenter.defaultCenter; [nc addObserver:self selector:@selector(padsChanged) name:GCControllerDidConnectNotification object:nil]; @@ -158,15 +162,16 @@ static BOOL fullOnly(int b) { return b == B_LS || b == B_RS || b >= B_LEFT; } } - (void)applyLayout { // show / hide the controls for the current layout [CATransaction begin]; [CATransaction setDisableActions:YES]; - for (int b = 0; b < B_N; b++) _key[b].hidden = fullOnly(b) && !_full; - _swapLabel.string = _full ? @"3D" : @"2D"; + for (int b = 0; b < B_N; b++) _key[b].hidden = _tap || (fullOnly(b) && !_full); + _ring.hidden = _knob.hidden = _tap; + _swapLabel.string = _tap ? @"Tap" : _full ? @"3D" : @"2D"; [CATransaction commit]; } -- (void)setFull:(BOOL)full { // switch layouts: release every touch first (no key stays held), save, lay out again - if (full == _full) return; +- (void)nextLayout { // 2D → 3D (→ Tap in handheld mode) → 2D: release every touch first (no key stays held), save, lay out again + int l = _tap ? 0 : _full ? (dm_opmode() == 0 ? 2 : 0) : 1; [self reset]; - _full = full; - [NSUserDefaults.standardUserDefaults setObject:@(full) forKey:k_layout_key]; + _full = l == 1; _tap = l == 2; + [NSUserDefaults.standardUserDefaults setObject:@(l) forKey:k_layout_key]; [self applyLayout]; [self setNeedsLayout]; } @@ -278,9 +283,10 @@ static CGFloat dist(CGPoint a, CGPoint b) { return hypot(a.x - b.x, a.y - b.y); BOOL game = NO; for (UITouch *t in ts) { if (_padOff) { game |= [self gameBegin:t]; continue; } - CGPoint p = [t locationInView:self]; int hit = [self btnAt:p]; + CGPoint p = [t locationInView:self]; int hit = _tap ? -1 : [self btnAt:p]; if (hit >= 0) { [_owner setObject:@(C_BTN | (uint64_t)hit << 8) forKey:t]; continue; } if ([self inSwap:p]) { [_owner setObject:@(C_SWAP) forKey:t]; continue; } + if (_tap) { game |= [self gameBegin:t]; continue; } // diamond, D-pad, left stick: take the one with the smallest distance / grab radius, if it is <= 1 CGFloat best = 1; int kind = C_NONE, idx = 0; CGFloat f = dist(p, self.faceCenter) / (self.faceGap + self.faceR * 1.6); if (f <= best) { best = f; kind = C_FACE; } @@ -318,7 +324,7 @@ static CGFloat dist(CGPoint a, CGPoint b) { return hypot(a.x - b.x, a.y - b.y); } if (game) [self pushGame]; [self recompute]; - if (swap) [self setFull:!_full]; + if (swap) [self nextLayout]; } - (void)touchesEnded:(NSSet *)ts withEvent:(UIEvent *)e { [self endTouches:ts tapped:YES]; } - (void)touchesCancelled:(NSSet *)ts withEvent:(UIEvent *)e { [self endTouches:ts tapped:NO]; } diff --git a/dm/ios/tests/test_xcode_workflow.py b/dm/ios/tests/test_xcode_workflow.py index 7975465..8ee3dfc 100644 --- a/dm/ios/tests/test_xcode_workflow.py +++ b/dm/ios/tests/test_xcode_workflow.py @@ -41,7 +41,7 @@ class XcodePipelineTests(unittest.TestCase): profile = SimpleNamespace(id=registered.id, dir=root / 'profile' / registered.id, get=registered.get, words=registered.words, builds=lambda: {'v0': {'display': '1.0.0', 'main': 'other-build'}}) - sources = {key: (False, '') for key in ('glslc_capture', 'vk_capture', 'ue_decode', 'oodle_packs', 'ue_legacy')} + sources = {key: (False, '') for key in ('glslc_capture', 'vk_capture', 'gl_capture', 'ue_decode', 'oodle_packs', 'ue_legacy')} sources['graphics'] = (graphics, 'fixture') fam = {'base': base, 'updates': [] if derived else [(327680, update)], 'dlc': []} version = 'v0' if derived else 'v327680' diff --git a/dm/tools/loader_census.py b/dm/tools/loader_census.py index 7af7e72..7ba177b 100644 --- a/dm/tools/loader_census.py +++ b/dm/tools/loader_census.py @@ -28,6 +28,84 @@ def addimm(w): return (w & 31, (w >> 5) & 31, (w >> 10) & 0xfff) if (w & 0xffc00 def ldr64(w): return (w & 31, (w >> 5) & 31, ((w >> 10) & 0xfff) * 8) if (w & 0xffc00000) == 0xf9400000 else None def is_call(w): return (w & 0xfc000000) == 0x94000000 or (w & 0xfffffc1f) == 0xd63f0000 # bl, blr def is_str_x0(w, base): return (w & 0xffc003ff) == 0xf9000000 | (base << 5) and ((w >> 10) & 0xfff) == 0 # str x0, [base] +def str_x0(w): return ((w >> 5) & 31, ((w >> 10) & 0xfff) * 8) if (w & 0xffc0001f) == 0xf9000000 else None # str x0, [base, #off] → (base, off) + +def got_values(img): + """{address: the address it holds once relocated} for the image's relative relocations (RELA R_AARCH64_RELATIVE, and RELR, whose addend is in place)""" + out = {} + mod0 = struct.unpack_from('> 32) * 24 + 6) + if shndx: out[off] = value + add + where = 0 + for k in range(tags.get(35, 0) // 8): # DT_RELR / DT_RELRSZ + e = struct.unpack_from('> (b + 1) & 1: a = where + b * 8; out[a] = struct.unpack_from('> 16) & 31] + ([(w >> 5) & 31] if csel else []) + if all(k in val for k in src): val[w & 31] = tuple(x for k in src for x in val[k]) + else: val.pop(w & 31, None) + continue + if (w & 0xfc000000) == 0x94000000 or (w & 0xfffffc1f) == 0xd63f0000: # bl, blr: x0–x18 and x30 are the callee's + for k in [*range(19), 30]: val.pop(k, None) + continue + if (w & 0xfc000000) == 0x14000000: # b: the next block is reached from elsewhere, where x19–x28 still hold what the function hoisted into them (a loop's GL pointers) + for k in [*range(19), 29, 30]: val.pop(k, None) + continue + if w == 0xd65f03c0 or (w & 0xfffffc1f) == 0xd61f0000: # ret, br: the function ends + val.clear(); continue + if (w & 0x0a000000) == 0x08000000: # loads and stores + if (w & 0x3a000000) == 0x28000000: # pairs: a load writes both, pre / post index writes the base + if (w >> 22) & 1: val.pop(w & 31, None); val.pop((w >> 10) & 31, None) + if (w >> 23) & 1 or not (w >> 24) & 1: val.pop((w >> 5) & 31, None) + else: + if (w & 0x3b000000) == 0x39000000: load = (w >> 22) & 3 != 0 # unsigned offset + else: + load = (w >> 22) & 3 != 0 or (w & 0x3f000000) in (0x08000000, 0x18000000, 0x58000000, 0x98000000) + if (w & 0x3b200000) == 0x38000000 and (w >> 10) & 1: val.pop((w >> 5) & 31, None) # pre / post index + if load: val.pop(w & 31, None) + continue + if (w & 0x1c000000) == 0x14000000 and (w & 0xfff00000) != 0xd5300000: continue # branches, compares, system (but mrs) write no register + val.pop(w & 31, None) # data processing and the rest: Rd def main(): a = sys.argv[1:] @@ -40,27 +118,28 @@ def main(): e = img.find(b'\0', p, p + 256) try: return img[p:e].decode('ascii') if e > p else None except UnicodeDecodeError: return None - slot, loader = {}, set() + slot, loader, page = {}, set(), {} for i in range(4, n - 3): + r = adrp(words[i], i * 4); a = addimm(words[i]) + if r: page[r[0]] = r[1] # the last address each register was given by adrp (+ add): LÖVE's glad keeps a few globals' addresses in x19–x28 across its loader + elif a and a[1] in page: page[a[0]] = page[a[1]] + a[2] if not is_call(words[i]): continue name = None for k in range(i - 4, i - 1): # the name: adrp + add into x0 or x1 just before the call r = adrp(words[k], k * 4); s = addimm(words[k + 1]) if r and s and r[0] in (0, 1) and s[1] == r[0] and s[0] == r[0]: name = cstr(r[1] + s[2]) - r = adrp(words[i + 1], (i + 1) * 4); l = ldr64(words[i + 2]) - if not (name and r and l and l[1] == r[0] and is_str_x0(words[i + 3], l[0])): continue - slot[r[1] + l[2]] = name + if not name: continue + r = adrp(words[i + 1], (i + 1) * 4); l = ldr64(words[i + 2]); s = str_x0(words[i + 1]); s2 = str_x0(words[i + 2]) + if r and l and l[1] == r[0] and is_str_x0(words[i + 3], l[0]): at = r[1] + l[2] # through the GOT: adrp + ldr of the slot's address, str x0 to it (Digimon) + elif r and s2 and s2[0] == r[0]: at = r[1] + s2[1] # straight into the global: adrp + str x0, [page, #off] (LÖVE's glad: Balatro) + elif s and s[0] in page and s[0] not in (0, 1): at = page[s[0]] + s[1] # str x0, [base, #off], base a page set earlier + else: continue + if name not in slot.setdefault(at, []): slot[at].append(name) # a slot two names were stored into keeps both loader.update(range((i - 4) * 4, (i + 4) * 4, 4)) + slot = {at: ' / '.join(sorted(names, key=len)) for at, names in slot.items()} refs, sites = collections.Counter(), collections.defaultdict(list) - for i in range(n - 1): - r = adrp(words[i], i * 4) - if not r or i * 4 in loader: continue - for j in range(i + 1, min(i + 6, n)): - l = ldr64(words[j]) - if l and l[1] == r[0]: - s = r[1] + l[2] - if s in slot: refs[slot[s]] += 1; sites[slot[s]].append(i * 4) - break + for i, at in known_loads(words, got_values(img)): + if at in slot and i * 4 not in loader: refs[slot[at]] += 1; sites[slot[at]].append(i * 4) fams = collections.Counter(nm[:2] if nm.startswith('gl') else nm[:3] for nm in slot.values()) print(f'{len(slot)} slots stored by a loader ({", ".join(f"{k}* {v}" for k, v in fams.most_common(6))}); {len(refs)} loaded by other code:') for nm in sorted(refs): print(f' {refs[nm]:4d} {nm}') diff --git a/docs/agents/02-survey.md b/docs/agents/02-survey.md index 5a20327..647e2ce 100644 --- a/docs/agents/02-survey.md +++ b/docs/agents/02-survey.md @@ -48,7 +48,7 @@ The tables below are what to check by hand afterwards. ## NVN surface -- First check that the game draws with NVN at all. Importing `nvnBootstrapLoader` is not enough: Digimon Story Cyber Sleuth: Complete Edition imports it and EGL, makes an NVN device for a helper beside its movie player, and renders everything with NVIDIA's desktop OpenGL (`eglBindAPI(EGL_OPENGL_API)`, a glad loader asking `eglGetProcAddress` for 2,956 names). Read the graphics setup that calls `nvnBootstrapLoader` / `eglGetDisplay` / `vkCreateInstance`, and count which loaded entry points the code really reaches: for a glad or NVN C loader that stores each pointer in a global, the GOT slots of those globals that code outside the loader loads (Digimon: 79 GL functions, 30 NVN ones, all in that helper). `profile_guess.py` decides `graphics gl` when `main` imports `eglCreateWindowSurface` and names `glShaderSource`; the host's GL layer is `dm/gfx/gl/hle_gl.m` ([04](04-graphics.md)). +- First check that the game draws with NVN at all. Importing `nvnBootstrapLoader` is not enough: Digimon Story Cyber Sleuth: Complete Edition imports it and EGL, makes an NVN device for a helper beside its movie player, and renders everything with NVIDIA's desktop OpenGL (`eglBindAPI(EGL_OPENGL_API)`, a glad loader asking `eglGetProcAddress` for 2,956 names). Read the graphics setup that calls `nvnBootstrapLoader` / `eglGetDisplay` / `vkCreateInstance`, and count which loaded entry points the code really reaches: for a glad or NVN C loader that stores each pointer in a global, the GOT slots of those globals that code outside the loader loads (Digimon: 79 GL functions, 30 NVN ones, all in that helper; Balatro's LÖVE: about 150 GL functions, whose globals its code reaches through registers, which `dm/tools/loader_census.py` follows). `profile_guess.py` decides `graphics gl` when `main` imports `eglCreateWindowSurface` and names `glShaderSource`; the host's GL layer is `dm/gfx/gl/hle_gl.m` ([04](04-graphics.md)). - Find how the game obtains NVN entry points. Some games write the slots with `adrp`-to-slot code; Mario's loader (`main+0x1070910`) calls `GetProcAddress` per name and stores through the GOT (`dm/tools/nvn_slots_got.py `). The output lists every fetched function and every call site. - Mario fetched 534 functions and called 236 of them; 79 were new to the host (CommandBuffer 31, Texture 15, Buffer 10, Sampler 10). Names the host doesn't know at all go into `dm/gfx/nvn/names.txt` → `gen_protos.py`. diff --git a/docs/agents/03-bringup.md b/docs/agents/03-bringup.md index 2a46a94..290f25c 100644 --- a/docs/agents/03-bringup.md +++ b/docs/agents/03-bringup.md @@ -202,6 +202,11 @@ Each entry: symptom → root cause → fix (where the fix lives). Check these fi - **SDK 6.4.0 (Digimon Story Cyber Sleuth: Complete Edition, Media.Vision's MVGL engine on OpenGL), one boot each**: - **`UNIMPL eglGetDisplay`, then `Break` right after `nv::InitializeGraphics` and `vi::OpenDefaultDisplay`** (`main+0x5ed9d0`) → the game renders with NVIDIA's desktop OpenGL through EGL; its NVN device (made just before) serves only a helper beside the movie player. The host's GL layer (`dm/gfx/gl/hle_gl.m`) answers the EGL imports and `eglGetProcAddress` ([04](04-graphics.md)). - **SIGBUS in `__vfprintf` under `aoc_load`, from `h_aoc_Count`** (a store to `0x…67ff0`, right below a page of the guest thread's stack) → the first add-on content query lists `DM_AOC_DIR` and logs, a few KB of stack, and the game asks from a thread with little left. `aoc::CountAddOnContent` and `ListAddOnContent` run on the big host stack (`HLE_FORCE_BIG`, `hle_misc.c`). +- **SDK 18.3.2 (Balatro, LÖVE 11.4 on OpenGL with SDL and LuaJIT), one boot each**: + - **The NSP script's provisional profile said Vulkan, and the capture boot crashed in the GL layer** → `main` imports `vkGetInstanceProcAddr` for SDL's Vulkan loader, which nothing uses; the game draws with EGL and GL. `profile_guess.py` now leaves that import out of its Vulkan count ([04](04-graphics.md)). + - **A jump to address 0 right after `glGetFloatv(0xb12)`** (`lr=main+0x2d58c4`) → a GL function the layer did not have (here `glVertexAttrib4fv`, LÖVE's default colours). `DM_GL_STUBS=1` hands out a stub per missing name that reports its first call; `loader_census.py` lists what the code calls before the first boot. + - **A crash in `main+0x339c00` a second or two into the title, in love-nx's video module teardown** (`love::video::theora::Video` → `~Worker`; its thread released the worker that the closing module deletes too) → the Lua state was closing: on the Switch, Balatro's `love.errhand` returns at once, so any Lua error quits. A `print` at the top of `love.errhand` in a copy of the RomFS (`DM_ROMFS=`) gave the error: `Shader uniform 'distortion_fac' does not exist` (the capture run had no shader library, so every program linked with no uniforms), then `bad argument #2 to 'send' (number expected, got boolean)` (a `bool` uniform reported as `GL_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. + - **Silent** (the log has `audren: OpenAudioRenderer (no event)` and no `render thread started`) → see [Audio](#audio). - **SDK 8 (Link's Awakening, 8.2.0)**: - **Abort in `os::WaitSystemEvent` under `oe::PopNotificationMessage`** → the blocking pop waits on the system event `oe::Initialize` would have created, and the host's `Initialize` is a no-op. The host now implements it on its notification queue (a condition variable, then the `TryPop` path). - **SIGSEGV in `vi::SetLayerCrop`** → the SDK's version dereferences vi service objects the host never made; `SetLayerCrop`, `DestroyLayer`, `CloseDisplay` and `vi::Finalize` are host no-ops. @@ -284,6 +289,7 @@ Each entry: symptom → root cause → fix (where the fix lives). Check these fi - **The CPU processor drops the final mix.** `dsp::SendCommandBuffer`, `dsp::Wait` and `dsp::Signal` are stubs in the application build, and DeviceSink (command 0x13) is a no-op in `CommandListProcessor::Process`. Take the output after `Process` from the mix buffers the DeviceSink command names (layout in `hle_audren.c`). - **Generation needs the renderer's lock.** `AudioRenderSystem::Update` holds a mutex in `AudioRenderSystem`; `GenerateCommand` does not take it, so the host does (the auto-mode system thread does the same in `SendCommandToDsp`). Which mutex depends on the SDK: `nn::os::LockMutex(MutexType *)` at +0x1d0 in SDK 4.4 and at +0x1d8 in 6.4–8.2, `SdkMutexType::Lock` at +0x1d8 in 10.4–11.4, +0x1f0 in 13.3–16.2 and +0x1f8 in 17.5 (the `sdk` builds of 24 games on hand), inlined in 19 and 20 (+0x2d0, the Span layout above). The host took SDK 16's for every SDK before 20, which wrote a lock word into another field and excluded nothing; Mario Kart 8 Deluxe (4.4) was the first game it was plainly wrong for. `update_lock` in `hle_audren.c` reads it off the SDK's own `Update`: the register the prologue adds the offset to and moves into x0 before its first `bl`, whose PLT slot must hold `LockMutex` or `SdkMutexType::Lock`; anything else keeps SDK 16's. Checked with sound on Big Brain Academy (11.4, +0x1d8), DRAGON QUEST XI S (7.3, `MutexType` +0x1d8) and Super Mario Odyssey (7.3, which is silent for its first minute with either lock). - **At least one rendered frame between two updates.** An effect's "needs init" parameter state is set only in the update where the game flags it new; the next update copies the game's value over it. Mario updates twice in a row at start-up; with a real audio device the first frame came after both, and the light limiter later crashed on a null state pointer (`ApplyLightLimiterEffect`, `sdk+0x228494`). The `AudioRenderSystem::Update` hook renders a catch-up frame first when nothing was rendered since the last update. `DM_AUDREN_TRACE=1` logs every update with the number of frames rendered since the previous one. +- **A renderer opened without an event** (Balatro: LÖVE's OpenAL Soft on `OpenAudioRenderer(handle, parameter, work, size)`, the form with no system event). The game paces its own updates, and the renderer must still run every 5 ms; the host used to start its render thread only for the form with an event, so only the game's updates rendered (catch-up frames that nothing played) and the game was silent. Both forms now go through one open that starts the thread (`hle_audren.c` `ar_open`). - **A sound driver that never waits on the renderer event** (DRAGON QUEST XI S, Square Enix's `SQEX::Sd::Driver::Core::CoreAudioOut`, its loop at `main+0x442c5c0`). It appends a wave buffer, mixes the next block on the same thread, calls `RequestUpdate` and takes the next released buffer. It waits on the renderer event only when no buffer is released. Every update there finds no frame rendered since the last one and renders a catch-up frame, which releases the buffer just appended, so the loop never waits. When a full catch-up queue discarded the frame, the loop ran 11,000 updates a second on one core, the sound ran about 50 times faster than the output. An update with a full queue now waits (at most 50 ms) for the render thread to play one queued frame, so catch-up frames keep real time. The render thread then only plays queued frames, and the driver's updates render every frame. A queued frame does not signal the renderer event (the SDK signals it in each render): signalled, an ordinary game woke, updated with nothing rendered since and rendered the next catch-up frame, so every frame became one (Big Brain Academy: Brain vs. Brain, about 38,000 catch-up renders instead of 37). `audren: rendered 12000 frames, N of them not silent` shows whether a game's sound reaches the output. - **Crackling on iOS only** → RemoteIO asks for as many frames per callback as the audio session's I/O buffer duration, about 21 ms by default (1024 frames at 48 kHz), while the renderer keeps 4 blocks × 5 ms = 20 ms in flight. Every callback came up 1.3 ms short and was padded with zeros, about 47 gaps per second. Reproduced on the Mac by forcing 1024-frame callbacks (`DM_DBG_AU_FRAMES=1024` with `DM_AUDIO=1`: 1999 underruns in 2000 callbacks). Fix: `AVAudioSession` `setPreferredIOBufferDuration:0.005` (and 48 kHz) before the game opens audio and again on return to the foreground (`front_ios_audio_session` in `front_ios.m`); the device then calls back with 240 frames and nothing underruns. When checking a new device, read the host's `audio #N device callbacks` line, logged every 2000 callbacks: frames per callback, queued blocks, underruns. - **Several outputs at once.** Engage opens one AudioOut from Wwise (`main+0x177d924`, no release event, the game polls). On the phone it opened a second one later; the Mac runs never did. Unity's FMOD also has an output (`main+0x4aec0c` / `0x4aec4c`). The host logs each output as `audio #N`: the open with its call site, Start / Stop / Close (the first 32 per output), and the callback line every 2000 of that output's callbacks. Combined numbers mislead: one fully fed output plus one fed by nobody look like "every other callback underruns, 2.0 blocks queued". diff --git a/docs/agents/04-graphics.md b/docs/agents/04-graphics.md index 9b49a9f..708e0ea 100644 --- a/docs/agents/04-graphics.md +++ b/docs/agents/04-graphics.md @@ -403,12 +403,25 @@ Media.Vision's MVGL engine on SDK 6.4.0 renders with **NVIDIA's desktop OpenGL 4 - **Shaders are GLSL ES 3.20 source**, compiled at run time with `glShaderSource` / `glCompileShader`: 1,682 `.shad` files in the MVGL archives (615 distinct) and 37 strings in `main` (post effects, FXAA, shadows, the movie's YUV). `gl_shaders.py` translates them offline: glslang (`-V -R`: relaxed Vulkan rules gather the loose uniforms into one `gl_DefaultUniformBlock`, whose member offsets the table lists) → SPIR-V → SPIRV-Cross. The host finds each shader by the hash of its text and its stage. Varyings are matched by name, as GL links them: `[[user(locnN)]]` becomes `[[user()]]`, so a vertex and a fragment shader translated apart still meet. `--fixup-clipspace` maps GL's clip z to Metal's. - **What NVIDIA's compiler accepts and glslang does not** (fix-ups on the preprocessed text): the GLSL ES 1.0 functions `textureCube` / `texture2D` in ES 3.20 code (89 fragment shaders; with `-V` glslang reads `textureCube` as Vulkan's texture type, so a macro cannot rename it), `out vec4 gl_Position` redeclared through a macro (the built-in sprite shader), and `textureOffset` with an offset that is not a constant (FXAA), which becomes a plain sample at P + offset ÷ level 0 size. A string in `main` says nothing of its stage, so it is translated as each stage that compiles (one fragment shader defines `vPosition` as `gl_Position`). - **Orientation**: the layer renders as GL does, bottom row first, through the NVN layer's lower-left origin (`mtl_set_origin_lower_left`): flipped viewports, reversed winding, a flipped present. Scissor rectangles and `gl_FragCoord` need no change, and render targets read back with GL's texture coordinates. -- **State**: uniforms are CPU copies per stage, set as bytes at a draw after they change; pipelines are cached per program by vertex layout, attachment formats, blend and colour mask; disabled vertex arrays read GL's default (0, 0, 0, 1) through a constant-step buffer. The game's attribute index is the Metal one: `glGetAttribLocation` returns the shader's own attribute numbers. +- **State**: uniforms are CPU copies per stage, set as bytes at a draw after they change; pipelines are cached per program by vertex layout, attachment formats, blend and colour mask; disabled vertex arrays read the attribute's current value (GL's default (0, 0, 0, 1) until `glVertexAttrib4f`) through a constant-step buffer. Without `glBindAttribLocation` the game's attribute index is the Metal one: `glGetAttribLocation` returns the shader's own attribute numbers (Balatro below binds its own). - **Textures** are made on first use from the levels given so far (the loader defines them level by level). An upload into a texture a pending command buffer may still read is a blit in command order; any other is a CPU write. Metal reads a channel a format lacks as 0 (alpha 1) as GL does, so only luminance / alpha formats get a swizzle: Metal refuses a swizzle on a texture that can be a render target (`Texture swizzling is not compatable with MTLTextureUsageRenderTarget`, the first story scene). - **Buffers**: `glBufferData` on a buffer the GPU may still read gets a new `MTLBuffer`, as GL drivers orphan it. **Clears**: a whole-framebuffer clear becomes the next pass's load action; a scissored or masked one is a quad. - **Present**: three window textures through the NVN layer's present path (`mtl_ext_tex`, as the Vulkan swapchain), paced by the swap interval in display-link ticks. The game sets the interval to 60 ÷ its frame rate before each swap: 1 at the title and in the DigiLab, 2 from the story's opening on. - **Movies**: the USM movies' H.264 is decoded through the SDK's movie library (the host's decoder) into NV12 buffers (3,110,400 bytes a 1920x1080 frame); the game uploads the planes into a `GL_RED` and a `GL_RG` texture (two `glTexSubImage2D` a frame) and converts YUV in its own shader (in `main`, `SamplerY` / `SamplerUV`). +### Seen in Balatro (LÖVE 11.4 on OpenGL) + +LÖVE ("love-nx", with SDL and LuaJIT) renders with the same NVIDIA desktop GL 4.5 through EGL; its graphics module is LÖVE's own OpenGL backend, so its choices follow what the context reports. + +- **Telling the API apart**: `main` imports the EGL functions and `vkGetInstanceProcAddr`, SDL's Vulkan loader, which nothing calls (`subsdk1` is NVIDIA's Vulkan driver). `profile_guess.py` leaves `vkGetInstanceProcAddr` out of its Vulkan count: Hades and both Galaxies import 99 or 100 Vulkan functions, `vkCreateInstance` among them. +- **Which GL functions**: LÖVE's glad stores each pointer straight into a global of its own (no GOT slot per name), and 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` (`glEnableVertexAttribArray` or `glDisableVertexAttribArray`). `loader_census.py` follows registers in a straight line for that (adrp, add, loads through relocated GOT entries, `mov`, `csel`): 2,133 names asked, about 150 called. `DM_GL_STUBS=1` gives 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). +- **Core profile**: LÖVE reads `GL_CONTEXT_PROFILE_MASK`; on a core profile it streams vertices through persistently mapped buffers (`glBufferStorage` + `glMapBufferRange` of a 3-frame ring, a `glFenceSync` per frame and `glClientWaitSync` three frames later), on a compatibility one from client memory, which Metal cannot draw from. The layer answers core: the mapping is the shared `MTLBuffer`'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 `glBindAttribLocation` before each link, keeps VertexColor and ConstantColor as disabled arrays and sets their values with `glVertexAttrib4f` (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 as `name[0]`) and reads their initial values back with `glGetUniformfv` / `iv`. A GLSL `bool` is a `uint` in the SPIR-V block; `gl_shaders.py` keeps the declared type, as LÖVE's `Shader:send` takes only a boolean for a bool. +- **Textures**: made with `glTexStorage2D` (every level up front: the game's images are mipmapped, filled by `glGenerateMipmap` as a blit) or `glTexStorage3D`: 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, as `GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS` says). Font glyphs are RG8 with a `GL_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`; canvases bind GL_FRAMEBUFFER (read and draw), check completeness, discard depth and stencil after use (`glInvalidateFramebuffer`) and clear with `glClearBufferfv`. `glBlitFramebuffer` (colour, same size) and `glReadPixels` (RGBA8) are there for multisample resolves and `Canvas:newImageData`, which the game does not use. +- **Shaders are put together at run time**: LÖVE builds each source in Lua from its header, its built-in code and the game's `.fs` file; the 35 are all made before the first frame. The NSP script captures them on the Mac (`gl_capture.tsv`). A capture run has no library: every program then links with no uniforms, the game's first `Shader:send` raises a Lua error and, on the Switch, its error handler quits ([03](03-bringup.md)). + ### Seen in DRAGON QUEST VII Reimagined Unreal Engine 4.27's desktop renderer with forward shading (one RG11B10 colour pass draws every object, lit per pixel from the light grid, the volumetric lightmap and the screen-space shadow mask), HEXA DRIVE's toon lighting on top. 62,901 shaders, LZ4 in the shader code archives (section 1); the game has no PC version, so the facts table holds only compute group sizes. diff --git a/docs/development.md b/docs/development.md index 4f3154d..f8eff55 100644 --- a/docs/development.md +++ b/docs/development.md @@ -58,7 +58,8 @@ What the script does: 3. It rewrites the code modules, checking each build ID against the profile. A version the profile does not register (the base game alone when the profile was brought up on an update, another update, or files whose build IDs differ), or one whose shader tables need a version that is not in the folder, is built from a profile derived from it in `_build/profile//`: the profile's keys, that version's build row, and shader tables, chain header, `c1_full.tsv` and `cb_extent.tsv` made from that version's own files (as for a provisional profile, without the heap check). The rewriter's cuts and the host's fixes for a registered build do not reach a module of another build; the script says which modules those are. A provisional profile (a game without one in `dm/games`) then gets `mem_used` from the rewritten modules, and, when the game has virtual address memory and sets a heap, a heap check: the Mac host is built into `_build/tools/dmhost` and the game boots in it until its first heap request (a few seconds, log `_build/logs/heap_check.log`), which sizes the heap region (see [dm/games/README.md](../dm/games/README.md), Adding a game). 4. For a game whose `main` holds GLSL sources and calls `glslcCompile` (Three Houses), it captures the shaders the game compiles at run time, which no game file holds. It builds the Mac host into `_build/tools/dmhost` and boots the game in it, headless and silent, with `DM_GLSLC_CAPTURE` pointing at `_build/extract//glslc_capture`. The save, temporary storage and log all stay in `_build/`. It stops the game as soon as every capture file named in the profile's shader map is there (Three Houses: 9, about a second after launch), and gives up after 30 s. A folder that already holds them all is reused. A Vulkan game without SPIR-V files in its RomFS (Super Mario Galaxy) gets the same kind of run: the game boots in the Mac host with `DM_VK_CAPTURE` pointing at `_build/extract//vk_capture` until every SPIR-V key in the profile's `vk_capture.tsv` is there (Galaxy: 3,089, about 7 s after launch), and gives up after 120 s. -5. It splits out the shaders and checks them against the profile's tables (a Vulkan profile: it translates the RomFS's SPIR-V and the capture with `vk_shaders.py` instead). + So does an OpenGL game with no GLSL source in its RomFS or `main` (Balatro: LÖVE puts its shaders together at run time, in Lua): `DM_GL_CAPTURE` at `_build/extract//gl_capture` until every file in the profile's `gl_capture.tsv` is there (Balatro: 35, made before its first frame). Without the profile's tables (a provisional profile) a capture stops once nothing has been added for 20 s. +5. It splits out the shaders and checks them against the profile's tables (a Vulkan profile: it translates the RomFS's SPIR-V and the capture with `vk_shaders.py` instead; an OpenGL one: the GLSL sources and the capture with `gl_shaders.py`). 6. It builds the iOS shader libraries, with the Mac's own Metal compiler and spirv-cross when they differ from those in `dmsh_chain.lock` (`dm/gfx/tools/own_compilers.py`; one line in the output names them). If a registered profile's `dmsh_chain.h` does not hold the version the source tree gives (a chain change left it behind), the script writes it again before any host is built, so it shows as changed in the checkout (`dmsh_build.py` would stop otherwise). Then the script makes the profile's `c1_full.tsv` and `cb_extent.tsv` from this build's Mac outputs when they are missing or name another chain. These two tables are not committed and git ignores them; `build_ipa.sh` stops without a current `c1_full.tsv`. Each line in the output names the file. 7. It calls `build_ipa.sh`. Xcode builds the app in the game's own build folder (`_build/ipa/build/ios/pkg/dd`: `build_ipa.sh` sets `SYMROOT` and `OBJROOT`, which a custom build location in Xcode's settings does not override), so a repeat build compiles only what changed and two games built at once never write the same app. Then `dm/ios/tools/pack_ipa.py` packs it with the shader libraries, the add-on content and the RomFS straight from where they are. It reads and deflates files on all cores and writes at about the T7's write speed (300 MB/s): Breath of the Wild's 16 GB IPA takes 57 s, Info-ZIP's copy and zip took 2 min 50 s. @@ -632,6 +633,16 @@ The game's own work and its frame rate stay as they are; the host avoids work of - **Host changes, all general:** the Mii database and the MiiModel stand-in; the add-on content cache sized as Horizon's RomFS working memory (its licence is decrypted in a 0x3130-byte heap); device save data as a save of its own for a game that mounts account save data too (every boot started a new save); SDK 4's DeviceSink layout and the audio renderer's update lock read off each SDK's `Update` (it was SDK 16's for every SDK before 20); and the zero cube for a one-layer 2D array in a cube slot ([03](agents/03-bringup.md), [04](agents/04-graphics.md)). - **Mac input:** the work folder's `run/ctl.sh` drives a run started with `DM_CTL`; `run/race.sh` boots from a copy of a save past the Mii pick into a 50cc Grand Prix (Mario, Mushroom Cup) with frame-timed keys and holds accelerate (`DM_KEYS=f960:L+R~0.3,f1260:A,f1500:A,f1740:A,f2000:A,f2240:A,f2400:A,f2560:A,f2800:A,f3040:A,f4300:A~70`). From a fresh save, L+R at the title opens "Select a Mii" (Right, A takes the first default Mii). +**Balatro (`0100CD801CE5E000`)** +- **Status:** brought up on the Mac from update 1.1.3 (`v851968`): 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. Plays on the iPhone (IPA about 82 MB). +- **Engine:** LÖVE 11.4 ("love-nx") with LuaJIT 2.1.0-beta3 in its interpreter and SDL, on SDK 18.3.2. 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**: NVIDIA's desktop GL 4.5 through EGL (`subsdk0`, not loaded; `subsdk1` is NVIDIA's Vulkan driver, and `main` imports `vkGetInstanceProcAddr` only for SDL's Vulkan loader, which `profile_guess.py` now leaves out of its Vulkan count). LÖVE's glad loader asks for 2,133 GL names and its code calls about 150 of them (`loader_census.py`). +- **GL layer:** LÖVE takes its core-profile paths when `GL_CONTEXT_PROFILE_MASK` says core (the layer now answers it): its vertex streams are persistently mapped buffers (`glBufferStorage`, `glMapBufferRange`) fenced per frame (`glFenceSync`, `glClientWaitSync`); with a compatibility profile it would draw from client memory. Also new for it: attribute bindings (`glBindAttribLocation`: LÖVE fixes VertexPosition, VertexTexCoord, VertexColor and ConstantColor to 0–3) and current attribute values (`glVertexAttrib4f`: VertexColor and ConstantColor are disabled arrays), `glGetActiveUniform` and the uniform read-backs, every `glUniform*` form, texture storage with its levels, 3D and array textures (LÖVE makes a default of each), texture swizzles (its font glyphs are RG8 read as R, R, R, G), `glGenerateMipmap` (the game's images are mipmapped), instanced and base-vertex draws, separate read and draw framebuffers, blits and `glReadPixels` ([04](agents/04-graphics.md)). +- **Shaders:** 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 by `Game:start_up` before the first frame. No game file holds them, so the NSP script captures them on the Mac (`gl_capture.tsv`) and `gl_shaders.py` translates them. A GLSL `bool` uniform is a `uint` in SPIR-V; `gl_shaders.py` now reports the declared type, since LÖVE's `Shader:send` takes only a boolean for a bool (the game stopped at its first card otherwise). +- **Sound:** the game opens the audio renderer without an event, and the host started its render thread only for the form with one, so only the game's own updates rendered, and nothing played them: silent. Both forms now start it (`hle_audren.c`). +- **Input and display:** `op_mode handheld`: the Switch build plays by touch there (SDL turns the touch screen into LÖVE touches and mouse presses), the iPhone's touches outside the gamepad controls, the Mac's mouse; docked it reads only the pads. Its window is the 1280x720 handheld display. `pad_layout touch`: the virtual gamepad starts in its new 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. +- **When the game stops:** on the Switch, Balatro's error handler quits at once, and love-nx's video worker is then deleted twice (its thread releases the worker that the closing video module also deletes), which crashes in `main+0x339c00`. A Mac run that reaches this has hit a Lua error first: a copy of the RomFS with a `print` at the top of `love.errhand` in `main.lua` shows it (a capture run without the shader library stops at the first `Shader:send` so, after every shader is captured). +- **Mac input:** 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. + ## Tools (`dm/tools`) | Tool | What it does | @@ -643,4 +654,4 @@ The game's own work and its frame rate stay as they are; the host avoids work of | `nso_code.py` | Function ranges from `.eh_frame` and the data blocks in a text segment, which the rewriter and the census leave out | | `nso_syms.py` | Compares the SDK import surface of two games, or of one game (`-` for the reference) with the host hooks | | `nvn_slots_got.py` | Builds the NVN fetch table and call sites (GOT-indirect loaders) | -| `loader_census.py` | Which of the functions a name loader fetches (glad's GL loader over `eglGetProcAddress`, NVN's C proc loader) the code really calls: the GOT slots the loader stores, loaded by other code | +| `loader_census.py` | Which of the functions a name loader fetches (glad's GL loader over `eglGetProcAddress`, NVN's C proc loader) the code really calls: the GOT slots or globals the loader stores, loaded by other code (followed through registers, relocated GOT entries and `csel`, for LÖVE's glad) | diff --git a/docs/images/balatro.jpg b/docs/images/balatro.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5755d212d428c5a3044dab3ef76cee04ce2f1394 GIT binary patch literal 127912 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

J6jzR@mTd&r%V^JK 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.spv (dm/gfx/vk/hle_vk.m); GL: <16 hex>.vert / .frag (dm/gfx/gl/hle_gl.m sh_capture) +CAPTURES = {'vk': dict(name='Vulkan', env='DM_VK_CAPTURE', table='vk_capture.tsv', what='SPIR-V shaders it generates', key=lambda n: n[:-4] if n.endswith('.spv') and len(n) == 20 else None), + 'gl': dict(name='GL', env='DM_GL_CAPTURE', table='gl_capture.tsv', what='GLSL shaders it puts together', + key=lambda n: n if re.fullmatch(r'[0-9a-f]{16}\.(vert|frag)', n) else None)} -def vk_wanted(g): # the SPIR-V keys the profile's vk_capture.tsv lists; None without the table (a provisional profile) - try: keys = {l.strip() for l in g.table('vk_capture.tsv').read_text().splitlines() if l.strip() and not l.startswith('#')} +def capture_wanted(g, api): # the keys the profile's capture table lists; None without the table (a provisional profile) + try: keys = {l.strip() for l in g.table(CAPTURES[api]['table']).read_text().splitlines() if l.strip() and not l.startswith('#')} except OSError: return None return keys or None -def vk_done(d): # keys of the whole SPIR-V files in a capture folder (the host writes each under a temporary name and renames it) +def capture_done(d, api): # keys of the whole files in a capture folder try: names = os.listdir(d) except OSError: return set() - return {n[:-4] for n in names if n.endswith('.spv') and len(n) == 20} + return {k for k in map(CAPTURES[api]['key'], names) if k} -def capture_vk(repo, d, patched, work, env, logs, g): - """A Vulkan game without SPIR-V files (Super Mario Galaxy): it generates its SPIR-V shaders at run time (Hagi's GX shaders, from its pipeline database), so no game file holds them. - The game boots in the Mac host (headless and silent, no shader library) with DM_VK_CAPTURE (dm/gfx/vk/hle_vk.m), which writes each module to /vk_capture, where vk_shaders.py - reads them with the RomFS; the modules are the same on every run. The run stops once every key the profile's vk_capture.tsv lists is there; without the table, once none has been added for 20 s. - A folder that already holds every listed key is reused""" - want, out = vk_wanted(g), d / 'vk_capture' - PROG.stage('Vulkan shader capture') - if want and vk_done(out) >= want: +def capture_shaders(repo, d, patched, work, env, logs, g, api): + """Shaders no game file holds, made at run time: a Vulkan game without SPIR-V files (Super Mario Galaxy: Hagi's GX shaders, from its pipeline database) or an OpenGL game whose GLSL is put + together at run time (Balatro: LÖVE adds its header and its built-in code to each of the game's shader files, in Lua). The game boots in the Mac host (headless and silent, no shader + library) with DM_VK_CAPTURE (dm/gfx/vk/hle_vk.m) or DM_GL_CAPTURE (dm/gfx/gl/hle_gl.m), which writes each shader to /vk_capture or gl_capture, where vk_shaders.py / + gl_shaders.py read them with the RomFS; the shaders are the same on every run. The run stops once every key the profile's capture table lists is there; without the table, once none has + been added for 20 s. A folder that already holds every listed key is reused""" + C = CAPTURES[api]; want, out = capture_wanted(g, api), d / f'{api}_capture' + PROG.stage(f'{C["name"]} shader capture') + if want and capture_done(out, api) >= want: say(f'reusing the captured run-time shaders: {out}'); PROG.end('reused'); return out PROG.set(step='building the Mac host') host = build_mac_host(repo, work / 'tools/dmhost', env, logs, g) - cap, rd, log = d / 'vk_capture.partial', work / 'tmp/vk_run', logs / 'vk_capture.log' + cap, rd, log = d / f'{api}_capture.partial', work / f'tmp/{api}_run', logs / f'{api}_capture.log' rmtree(cap); rmtree(rd); cap.mkdir(parents=True) for x in ('save', 'temp', 'msl'): (rd / x).mkdir(parents=True) - # everything the run writes stays in _build (DM_ROOT is already there): a fresh save and temporary storage, an empty shader library folder (pipelines fail, the game still creates every module) - henv = dict(env, DM_ROMFS=str(d / 'RomFS'), DM_SAVE=str(rd / 'save'), DM_TEMP=str(rd / 'temp'), DM_MSL_DIR=str(rd / 'msl'), DM_VK_CAPTURE=str(cap)) + # everything the run writes stays in _build (DM_ROOT is already there): a fresh save and temporary storage, an empty shader library folder (pipelines fail, the game still creates every shader) + henv = dict(env, DM_ROMFS=str(d / 'RomFS'), DM_SAVE=str(rd / 'save'), DM_TEMP=str(rd / 'temp'), DM_MSL_DIR=str(rd / 'msl'), **{C['env']: str(cap)}) PROG.set(step='booting the game', done=0, total=len(want) if want else None, unit='shaders') - say(f'booting the game in the Mac host to capture the SPIR-V shaders it generates at run time → {out}') + say(f'booting the game in the Mac host to capture the {C["what"]} at run time → {out}') t0 = time.time(); last = {'n': 0, 't': t0} def done(): # every listed key is there; without the table, none has been added for 20 s - got = vk_done(cap); n = len(got & want if want else got); PROG.set(done=n) + got = capture_done(cap, api); n = len(got & want if want else got); PROG.set(done=n) if n != last['n']: last['n'], last['t'] = n, time.time() return bool((want and got >= want) or (not want and n and time.time() - last['t'] > 20)) - rc = boot_mac(host, patched, henv, rd, log, VK_CAPTURE_SECS, done) - got = vk_done(cap) + rc = boot_mac(host, patched, henv, rd, log, CAPTURE_SECS, done) + got = capture_done(cap, api) if rc is not None and not (want and got >= want): host_stopped(log, rc, time.time() - t0) if not got or (want and not got >= want): PROG.end(ok=False) PROG.say(f'---- last lines of {log} ----\n' + ''.join(collections.deque(open(log, errors='replace'), maxlen=40)).rstrip('\n')) raise BuildError(f'captured only {len(got)} run-time shaders' + (f', the profile needs {len(want)}' if want else '') + f' (host output: {log})') - if want and got - want: say(f'[RED] captured shaders the profile\'s vk_capture.tsv does not list: {len(got - want)}') + if want and got - want: say(f'[RED] captured shaders the profile\'s {C["table"]} does not list: {len(got - want)}') swap_in(cap, out); rmtree(rd) PROG.end(f'{len(got)} shaders in {hms(time.time() - t0)}') return out @@ -1586,13 +1594,14 @@ def main(args=None): src = pg.build_sources(game / 'ExeFS', game / 'RomFS', g.words('modules')) gfx = src['graphics'][0] # nvn, vulkan (Hades: the host's Vulkan layer, SPIR-V shaders) or gl (Digimon Story Cyber Sleuth: the host's GL layer, GLSL sources) glslc = gfx == 'nvn' and src['glslc_capture'][0] # shaders the game compiles at run time, captured in the Mac host (capture_glslc) - vkcap = gfx == 'vulkan' and src['vk_capture'][0] # SPIR-V shaders the game generates at run time, captured in the Mac host (capture_vk) + vkcap = gfx == 'vulkan' and src['vk_capture'][0] # SPIR-V shaders the game generates at run time, captured in the Mac host (capture_shaders) + glcap = gfx == 'gl' and src['gl_capture'][0] # GLSL the game puts together at run time (LÖVE), captured in the Mac host (capture_shaders) uedec = gfx == 'nvn' and src['ue_decode'][0] # Unreal Engine shaders, Oodle-compressed in the paks' shader code archives, decoded in the Mac host (decode_ue_shaders) if gfx == 'vulkan': say(f'Vulkan game ({src["graphics"][1]}): vk_shaders.py translates the shaders instead of the NVN shader chain') if gfx == 'gl': say(f'OpenGL game ({src["graphics"][1]}): gl_shaders.py translates the shaders instead of the NVN shader chain') - for on, k in ((glslc, 'glslc_capture'), (vkcap, 'vk_capture'), (uedec, 'ue_decode'), (gfx == 'nvn' and src['oodle_packs'][0], 'oodle_packs'), (gfx == 'nvn' and src['ue_legacy'][0], 'ue_legacy')): + for on, k in ((glslc, 'glslc_capture'), (vkcap, 'vk_capture'), (glcap, 'gl_capture'), (uedec, 'ue_decode'), (gfx == 'nvn' and src['oodle_packs'][0], 'oodle_packs'), (gfx == 'nvn' and src['ue_legacy'][0], 'ue_legacy')): if on: say(src[k][1]) - PROG.add(glslc + vkcap + uedec + (gfx == 'nvn')) + PROG.add(glslc + vkcap + glcap + uedec + (gfx == 'nvn')) aoc = extract_aoc(nx, keys, repo, work, app, fam, logs) base = next((dirs[v] for v in versions if picks[v][1] is None), dirs[versions[0]]) # the folder the base game was extracted to alone # the app name and bundle ID both come from the base NSP: the game name in its control data, and a bundle ID from its title ID; a setting / the command line overrides them @@ -1625,10 +1634,11 @@ def main(args=None): if uedec: decode_ue_shaders(repo, {v: dirs[v] for v in sv}, pkg_ver, patched, work, env, logs, g) sh = work / 'shader' if gfx == 'vulkan': # the packed version's merged RomFS holds the SPIR-V files the game loads; a capture holds those it generates - cap = capture_vk(repo, game, patched, work, env, logs, g) if vkcap else None + cap = capture_shaders(repo, game, patched, work, env, logs, g, 'vk') if vkcap else None ios = build_vk_shaders(py, repo, [game / 'RomFS'] + ([cap] if cap else []), sh, env, logs, g, reuse=not (provisional or derived or ALLOW_DAMAGED), mac=mac) - elif gfx == 'gl': # the GLSL sources in the packed version's archives and in its main (the game compiles them at run time; the host finds each by the hash of its text) - ios = build_gl_shaders(py, repo, [game / 'RomFS', game / 'ExeFS/main'], sh, env, logs, g, reuse=not (provisional or derived or ALLOW_DAMAGED), mac=mac) + elif gfx == 'gl': # the GLSL sources in the packed version's archives and in its main (the game compiles them at run time; the host finds each by the hash of its text), and a capture of those it puts together + cap = capture_shaders(repo, game, patched, work, env, logs, g, 'gl') if glcap else None + ios = build_gl_shaders(py, repo, [game / 'RomFS', game / 'ExeFS/main'] + ([cap] if cap else []), sh, env, logs, g, reuse=not (provisional or derived or ALLOW_DAMAGED), mac=mac) else: # Trinity packs and legacy Unreal paks: the split decodes them with the packed version's own decoder (Oodle, LZ5), in the Mac host built for the profile oodle = (lambda: {'DM_OODLE_HOST': str(build_mac_host(repo, work / 'tools/dmhost', env, logs, g)), 'DM_OODLE_MODULES': str(patched)}) if src['oodle_packs'][0] or src['ue_legacy'][0] else None