Port Portal: Source engine NROs on NVN, shaders from game.zip

- profile 01007BB017812000 (1.0.3, SDK 13.3.0): plays on the Mac at 60 fps
- provisional profiles add the subsdks the NROs import (profile_guess nro_modules)
- an NRO importing nvnBootstrapLoader makes an NVN game; NROs calling glslcCompile get the run-time GLSLC capture
- shader_split reads zip archive members (Portal's nxcontent/nvsb GLSLC outputs)
- ro::LookupModuleSymbol for a name a loaded module lacks is not red (Horizon fails it too)
- AGENTS.md: a Mac gameplay test is enough to count a game as playable (65)
This commit is contained in:
seedds committed 2026-10-26 01:28:35 +08:00
1 parent 6d4202b562
commit a8e76bf601
21 files changed
+812 -25

No files matched your search

+52
View File
@@ -0,0 +1,52 @@
# Portal (`01007BB017812000`)
Update 1.0.3 (`v196608`) over the base game, SDK 13.3.0. Valve's Source engine as NVIDIA Lightspeed Studios ported it to the Switch: `main` is a 0.5 MB launcher with SDL, and the engine is 28 NRO modules (`nro/*.nro` in the RomFS, registered by `.nrr/launcher_main.nrr`) loaded with `nn::ro`. It renders with NVN, its D3D9 layer linked into `materialsystem.nro`. Modules `main`, `subsdk0` (GLSLC), `subsdk2` (NintendoSDK_movie) and `sdk`; `subsdk1` is NVIDIA's OpenGL driver and is never loaded.
**Current:** plays on the Mac at 60 fps: the opening movie, the title menu (its 3D background map), New Game, test chamber 00 with its portals (the view through them included), the pause menu and a saved game, with sound; no red line. About 1.5 GB footprint in the chamber. The unsigned IPA is built (2,823,721,518 bytes, about 2.6 GB); not yet tested on an iPhone.
## What it needs
| Setting | Where | Why |
|---|---|---|
| `modules main subsdk0 subsdk2 sdk` | [`game.tsv`](game.tsv) | `main` imports from no subsdk; the NROs do: `video_nx.nro` plays the movies through the movie module and both it and `materialsystem.nro` call GLSLC ([Modules only the NROs import from](../../../docs/reference/services.md#modules-only-the-nros-import-from)) |
| 4 GB layout: `mem_reserve 0x100000000`, `mem_alias 0x10000000`, `mem_stack 0x10000000`, `app_pool 0xCD500000`, `sys_resource 0x0` | `game.tsv` | no system resource, so no virtual address memory; the game sets a heap in the 4 GB layout. The rewritten modules fit the default 64 MB of used memory |
| `pad_layout full` | `game.tsv` | first-person: move with the left stick, look with the right |
| `shader_groups /game.zip/=game` | `game.tsv` | the shipped programs come from `game.zip`; the run-time captures go to `other` |
## Rendering
- **Resolved: the NSP script took it for an OpenGL game.** `main` imports 20 EGL functions and names `glShaderSource` (SDL's EGL backend and GL renderer), but draws nothing; `materialsystem.nro` and `video_nx.nro` import `nvnBootstrapLoader`. The first capture boot stopped with `[nvn-mtl] FATAL cannot read the shader map` from an NVN submit in `nro8` ([NVN in NROs under an SDL main that names GL](../../../docs/reference/rendering.md#nvn-in-nros-under-an-sdl-main-that-names-gl)).
- About 140 draws a frame at the title and in chamber 00; GPU p50 about 6 ms on an M4 Pro, NVN submit and present under 3 ms.
## Shaders
- The D3D9 layer links each Source shader combination (`hl2/shaders/fxc/*.vcs`, D3D9 bytecode) into one program and keeps the GLSLC output of every program in the game's precache list (`portal/glbaseshaders.cfg`, 219 entries) in `nxcontent/nvsb/<hash>.nvsb`: a 0x60-byte `NVSB` header (version 0x65, the file's length at +8), then one GLSLC output holding a vertex and a pixel shader. The game compiles the rest with `glslcCompile`: within 30 s of boot, 10 outputs (the movie's and the engine's own).
- **Resolved:** the split found none of the 219: they are deflated members of `game.zip` ([GLSLC outputs in a zip archive, and those NROs compile at run time](../../../docs/reference/shaders.md#glslc-outputs-in-a-zip-archive-and-those-nros-compile-at-run-time)). The map has 255 keys, 165 pixel and 90 vertex, 242 from the `.nvsb` files and 13 from the capture, with 229 vertex and pixel pairs.
- The game writes the programs it has linked to its save, `portal/glshaders/glshaders.cfg` (`-glshadersSaveFS` in its boot parameters): 219 entries after chamber 00, the precache list. More entries there mean programs compiled at run time on that route, which a fresh capture boot does not reach; on the iPhone each would give `[RED] shader … is not in the shader map`.
## Files
- The whole game is `game.zip` (2.7 GB, 41,416 members, Deflate or stored) beside the NROs; the Source file system reads it itself. The profile's `romfs_marker` is `game.zip`.
- The boot parameters are `nxcontent/rom_boot_params.txt` in the zip (`-game portal -glshadersSaveFS -nodev -nosteam +mat_queue_mode 2 +mat_picmip 0`); host arguments after `--` (a `+map` among them) change nothing.
- Only the merged update is registered. The base game's build has no profile row.
## Movies
- The opening movie (1280x720 H.264 with AAC) plays through the host's movie decoder from `video_nx.nro`, which shows the frames with NVN and plays the sound through its own `AudioOut`.
## Mac routes
- Run the command the NSP script prints for `--platform mac` (`DM_NVN_SHMAP` names this profile's `shader_map.tsv`), with `DM_CTL=<file>` to drive it ([05-debugging.md](../../../docs/agents/05-debugging.md)).
- From a fresh save: the title menu at about 25 s; `A` (New Game), `A` (Chapter 1) loads test chamber 00 in about 15 s. The portal opens in the vault after GLaDOS's first lines (about 30 s); the right stick turns, the left stick walks through it. `Plus` opens the pause menu; `Down`, `A`, `A` saves (`portal/save/*.sav` in the save).
## Found on this game
- [Modules only the NROs import from](../../../docs/reference/services.md#modules-only-the-nros-import-from): the provisional profile adds the subsdks the NROs need.
- [NVN in NROs under an SDL main that names GL](../../../docs/reference/rendering.md#nvn-in-nros-under-an-sdl-main-that-names-gl): the graphics API follows the NROs' imports.
- [GLSLC outputs in a zip archive, and those NROs compile at run time](../../../docs/reference/shaders.md#glslc-outputs-in-a-zip-archive-and-those-nros-compile-at-run-time): the split reads zip members; the run-time capture also runs for NROs that call `glslcCompile`.
- [`ro::LookupModuleSymbol` for a name the module does not define](../../../docs/reference/services.md#rolookupmodulesymbol-for-a-name-the-module-does-not-define): the engine's `BuiltDebug` probe is answered as on Horizon, without a red line.
## Open
- iPhone test of the IPA.
- Later chambers (energy balls, turrets, water, the escape) are untested; watch `glshaders.cfg` and the log for programs outside the shipped 219.