Repository navigation
Conversation
|
MGS re-test: the opening's audio was a little choppier with the updated psyz. Bisected on the board: the SPU's new block-a-voice-at-a-time mixer makes MGS's SPU-interrupt clock (voice 23, which the sound driver and its streamed audio run on) irregular, with about a third of the beats re-armed late instead of a tenth. 793d242 puts MGS back on psyz's per-sample mixer. The rasterizer speedups stay, and MGS's boot sequence actually runs faster with them (frame 1320 at vblank 4824 instead of 5426). TPM keeps the block mixer (it doesn't use SPU interrupts). |
ESP-IDF Size Report for 'Tab5 Emu'
FLASH uses app .bin size or json2 flash sum. RAM sums DRAM+IRAM via idf_size. Percentages shown when totals are available. |
New native-port target alongside MGS. No decompilation of the game existed; it is being built in tpm-decomp (SLUS_008.84, USA Rev 1). Until its C runs, components/tpm proves the platform half: it opens SLUS_008.84 + FILE.000 from the card, resolves names through the game's own file table (read out of the executable at 0x800715A0, as the game does) and shows the 44 load screens (320x256 24-bit, cropped to 240 rows; left/right to step). Metadata entries point at <dir>/SLUS_008.84. CONFIG_TPM_ENGINE is reserved for the game C. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The decompilation is public now (esp-cpp/tpm-decomp, tracking main), so point the component's docs at it and vendor it as components/mgs does with mgs_reversing. Nothing compiles from it yet (CONFIG_TPM_ENGINE stays off). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CONFIG_TPM_ENGINE (now on by default) recompiles the user's SLUS_008.84 to C at configure time with tpm-decomp's tools/recomp.py and builds it with the tpm-decomp runtime on psyz's rasterizer, GTE and SPU (the copy in components/mgs). The whole lot is linked into one relocatable object with only the runtime's API left global, since components/mgs links psyz under the same names. src/tpm_engine.cpp is the platform: the disc is the .bin on the card (the executable is read out of its ISO9660 directory), the game runs on core 0 in real time, vblanks hand the display area to a core-1 conversion task, the SPU's samples go to the HAL through a ring and a core-1 task, the HAL's gamepad fills the pad buffers. Without the executable at build time or a .bin on the card, the cart falls back to the load-screen viewer. The app partition grows to 12 MB (image: 7.9 MB). Built; not yet run on the Tab5. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- CONFIG_TAB5_EMU_AUTOSTART_MATCH: auto-start the first game whose name or path contains the text, not just the first entry - CONFIG_TPM_AUTOPRESS: scripted pad presses at given vblanks - CONFIG_TPM_SERIAL_SCREENSHOT_SECONDS: the presented frame as base64 on the console; tools/serial_screenshots.py turns a log into PNGs - a status line every 5 s (vblanks, frames presented, display mode) - tpm-decomp f238b94: VRAM alias follows psyz's pointer on ESP32 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- the runtime's statics in PSRAM (tpm_rt.ld gathers the bundle's .bss into .ext_ram.bss, brackets .bss/.data) and reset before every launch; its big buffers (guest RAM, VRAM, SPU, transfers) from the shared-memory pool, released when the cart quits; the rasterizer out of IRAM. Internal RAM use of the bundle 37.7 KB -> 192 B: the boot-time DMA reserve fits again - the displayed rows are copied at the vblank on the game's thread: the rasterizer draws synchronously there, and VRAM read later from the video task often held the next frame's clear without its polygons - plat_sleep_us for the runtime's throttle - status line: game fps (display-area flips); half-resolution serial screenshots from their own task; a one-shot VRAM dump and a GPU trace of one frame (CONFIG_TPM_SERIAL_FULL_SHOT_VBLANK, CONFIG_TPM_GPU_TRACE_VBLANK) - tpm-decomp 11bd43e Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- rt_gpu_set_async: the rasterizer runs in tpm_gpu on core 1 behind a command queue; the displayed rows are copied by the worker at the vblank marker (plat_display_ready), so the picture is the finished frame - 32-sector read-ahead on the disc image (CD time 13% -> 2%) - status line: the game thread's time split (g_rt_prof); a PC-sampling profiler (CONFIG_TPM_PROFILE_VBLANK); numbered screenshot lines that survive interleaved log output; the screenshot task above the GPU worker - README: status, threads, memory, bring-up aids - tpm-decomp afc58b8 7 -> 12 fps in the Trade Federation ship level (the game's own rate is 30). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tpm-decomp (78b959a..): the recompiler passes only the registers a call needs (a third of the register traffic left, 2.1 -> 1.7 MB of code); the frame limiter's VSync spin skips to the vblank; texture windows (the characters' textures); the cutscenes' vertical display range (the letterbox wrapped around); a libsnd jump table that killed the game on some button presses; FPU triangle gradients; a second GPU worker option. Board side: the presenter honours the display range; the game's thread sleeps instead of spinning while the GPU's queue is full; RGB conversion two pixels a word; per-task CPU load and the GPU worker's rendezvous in the status line; CONFIG_TPM_GPU_WORKERS (default 1: two measured slower here, the cores contending for PSRAM and cache); the sampling profiler also samples the GPU worker (TPMPROFG). psyz: the tpm branch (texture window, desktop worker identity, exact bands). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… bench - tpm-decomp 43bcb36 / psyz tpm: the runtime's own GTE (inline register moves, direct command calls; IRGB / LZCR / SXYP as the hardware), the rasterizer's row loops per span type, pre-modulated 4bpp palettes, exact 32-bit gradient divides, statistics compiled out, word-sized fills; SPU visits active voices only; DMA packets pushed to the GPU queue at once. The level goes from ~12 to ~15-16 fps here. - Disc read-ahead at 512-byte file offsets into a 128-byte-aligned PSRAM buffer: the SD driver no longer bounces through internal DMA memory, whose exhaustion failed reads and stalled the game. - The pad: face buttons by position as in the MGS port (B Cross, A Circle, Y Square, X Triangle), L1/L2/R1/R2 apart, the left stick as the d-pad. - CONFIG_TPM_GPU_BENCH_FILE: a desktop GPU recording replayed through the rasterizer at cart start (TPMBENCH lines). - The frame is converted on the GPU worker at the vblank (on core 0 later, the first room's glass and the pause menu came out wrong). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ed on flips - While the emulator's pause menu is up nothing is presented: the GPU worker still worked through the queued commands and drew game frames over the menu. - tpm-decomp: the plain guest loads/stores always inline (the game thread ~9% faster per frame, the generated code twice the size); psyz: constant texture row shift. - A frame is converted when the game flips (or every fourth vblank). - The second GPU worker sits below the game when enabled (still slower: CONFIG_TPM_GPU_WORKERS stays 1). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pauses the engine the way the emulator's menu does at a given vblank and logs how much of the frame the menu's screenshot would read is lit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On the P4 the cores reach cached PSRAM faster than internal RAM: the game thread's stack (the recompiled code spills guest registers to it all the time), the GPU worker's, g_cpu / g_gte and the CLUT cache now live in PSRAM. The game thread needs ~15% less time per frame; the GPU worker suspends at the end and the cart deletes it (PSRAM stacks). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The GPU worker drops a frame's drawing when the game is a frame ahead of it; stale buffers are not presented. Status line shows game vs drawn fps. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n place; PSRAM at boot logged Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…chmark tpm-decomp: scratchpad accesses check the scratchpad first (the sites in config/scratch_sites.us.txt, passed to recomp.py), a GTE function per command word, calls between a function's entries as local jumps, the GTE dispatch always inlined; psyz: the SPU's voices run through a block in locals. The level benchmark below: 23.8 s -> 20.2 s of game thread (15% less); the level holds ~22 game fps (was 17-20). TPM_BENCH_FROM / TPM_BENCH_TO: run unpaced (with TPM_AUTOPRESS the same work every run) and log the time between two vblanks, less the game thread's waits for the GPU. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The runtime's buffers and the game thread's stack live in one PSRAM arena kept across quits (released before any other cart starts), so a state of raw memory loads at the same addresses. Taking a state, the game's thread checkpoints itself (setjmp) in plat_pause_wait; loading, it waits on a spare stack at the bottom of its own while the runtime's statics, the arena and the live part of its stack are read back, then jumps into the state's checkpoint. The GPU workers stop around both. States load only into the build that wrote them. CONFIG_TPM_TEST_STATE "A:B" saves at vblank A and loads at B; both passes log a guest RAM hash per vblank: the replay after the load matched all 120, also loading a state written before a reset. psyz: the SPU mixer's gauss window in registers, the envelope only where it changes (game thread benchmark 20.2 -> 19.0 s, audio identical). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… voices tpm-decomp c2eacd2 and psyz 0c2cfcb: - recomp.py proves the scratchpad accesses (renderer's base register) and skips their address checks - the GPU queue takes a DMA packet as one entry - libcd's command wait skips ahead to the next interrupt: the level load takes ~4 s of board time instead of ~25 s - frameskip drops a skipped frame's clear with its primitives - silent SPU voices skip interpolation and mixing (bit-exact) Board bench (vblanks 1800..3000): 19.0 -> 17.1 s. In level ~25-26 game fps, both cores busy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tpm-decomp 75a9340: the bundle builds with -ffixed-s11 and RT_RAM_REG=s11, so the generated code reads g_ram from the register instead of loading the pointer before every access. Also GTE commands whose FLAG nothing reads skip it (ed1d607). Board bench (vblanks 1800..3000): 17.1 -> 16.4 s; save-state replay 120/120. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The generated functions are huge and short of registers; the scheduling pass that minds register pressure saves 1% of the game thread (bench 16.4 -> 16.3 s). Without the pass it was 2% slower; inlining RTPS/RTPT into the generated code was 5% slower (more pressure). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Saving lets the GPU workers drain the queue, then leaves out the ranges rt_snap_dead gives (the queue, the transfer buffer past any transfer in progress); the header (v2) lists them and a load skips them. 5.2 -> 3.6 MB, save 2.2 -> 1.5 s, load 0.83 -> 0.58 s; the replay after a load still matches 120/120 vblanks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tpm-decomp 880a0a8: the MDEC is emulated, and the movie player's play function is replaced by a native one (the game's own VLC decoder, the MDEC's decoding, XA-ADPCM into the SPU's CD input), since the original only keeps up at the hardware's exact pace. Start or Cross skips a movie. CONFIG_TPM_DISC_PATH (the disc image on the build machine) recompiles the FMV.CPE overlay; without it the movies are skipped as before. CONFIG_TPM_MOVIES turns them off; builds with CONFIG_TPM_AUTOPRESS skip them for the scripted timeline. Board: the LucasArts and Big Ape logos and the 110 s opening crawl play at 15 fps (full speed), the game thread ~81% busy. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the psyz update the SPU mixes a block a voice at a time by default. MGS clocks its sound driver off the SPU interrupt (voice 23 loops a blank sample, the IRQ address toggled ahead of it), and mixed that way the beats came irregularly: about a third re-armed behind the voice and were caught up, against a tenth sample by sample (old psyz: similar). The opening's streamed audio was choppier. MGS now selects the per-sample reference mixer (still ~10% cheaper than before the update); the rasterizer speedups stay (the boot sequence reached frame 1320 at vblank 4824 instead of 5426). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After another game (MGS) the heap may have no ~5 MB block left; TPM then refused to start. Now it takes the game task's stack alone and the rest piece by piece from the heap, without save states for that session (the next launch tries for the whole block again). The MDEC's input buffer is also halved (tpm-decomp cf507fa). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lock left after MGS Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The tpm component's CMakeLists includes tpm-decomp/recomp/runtime, so configuring without the submodule fails. The README's clone line gets it too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Explain why nothing generated from the game is committed, why the CI firmware has only the load-screen viewer, and that the goal is a matching decompilation replacing the recompiled code function by function. Bump tpm-decomp for the same note in its README. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
Runs Star Wars Episode I: The Phantom Menace (PSX, USA Rev 1, SLUS-00884) natively on the Tab5 (ESP32-P4) as a cart, from the disc image on the microSD card. No decompilation existed, so the game is statically recompiled: esp-cpp/tpm-decomp (new, MIT) translates the executable's MIPS code to C at build time (
tools/recomp.py) and runs it on its own runtime (BIOS, interrupts, DMA, CD-ROM, GPU ports, GTE, MDEC), with psyz's software rasterizer and SPU. Nothing derived from the game is committed; the build reads your ownSLUS_008.84(and, for the movies, your disc image).What works: the whole boot chain with its movies (LucasArts and Big Ape logos, the opening crawl), the title, menus and the levels, with music, speech and sound effects; pause, reset and quit from the emulator menu; save and load states; USB and built-in gamepads.
Gameplay runs at about 27 game fps in the first level (the game's cap is 30) with frameskip 2; movies play at their full 15 fps with their audio; a level loads in about 4 s.
tab5-tpm-save-states-compressed-2.mp4
Recompiled, not (yet) decompiled
Unlike the JK and MGS carts, TPM does not run from decompiled source:
tools/recomp.pytranslates each MIPS instruction of the executable into C mechanically. That output is the game's code in another form (Sony's copyrighted executable), so it is never committed or distributed. It is generated in the build directory from the builder's ownSLUS_008.84(CONFIG_TPM_EXE_PATH) and disc image (CONFIG_TPM_DISC_PATH, for the movie overlay).The goal is a real, matching decompilation, as mgs_reversing is for MGS: hand-written C that rebuilds the byte-identical executable with PSY-Q's GCC 2.8.1. tpm-decomp has started it: the executable is split with splat and rebuilds byte-identical, the PSY-Q functions are identified, and
mainand the file layer are in C. Decompiled functions can replace recompiled ones one at a time, so the port keeps working while the decompilation grows. Once it is complete, TPM can build from source like the MGS cart. Contributions welcome. The READMEs (top level,components/tpm, tpm-decomp) now say all of this.Port layer (this repo)
components/tpm: the cart (launch, pause at the game's next loop back-edge, reset, quit without reboot), LCD present, audio task, disc reads with a 64 KB DMA-able read-ahead, a load-screen viewer when no executable was given.tpm_rt.ld,tpm_rt.keep): only the runtime's API stays global (MGS links its own psyz under the same names), its statics go to PSRAM and are reset on relaunch. Built-O2 -fsched-pressure -fomit-frame-pointer -ffixed-s11: s11 holds guest RAM's address for the generated code..data,.bss, the arena (minus the drained GPU queue and an idle transfer buffer) and the live part of the game stack: 3.6 MB, about 1.5 s to save and 0.6 s to load. A state loads into the firmware build that wrote it.CONFIG_TAB5_EMU_AUTOSTART_MATCHpicks the auto-started game by name; rom_info knows the cart; the factory partition is 12 MB (the recompiled game is large).In the submodules
components/mgs/psyz, now at esp-cpp/psyztpm): rasterizer speedups (per-triangle row loops, pre-modulated 4bpp palettes, exact small divides, texture windows) and SPU speedups (voices mixed per block, envelope work only when it can change, silent voices not mixed), all verified frame- and sample-identical on TPM's desktop runs.Measuring
A deterministic desktop runner (
make -C recomp, thentpm-hostwith a scripted press list) gives the same frames and audio on every machine; every optimisation was checked identical over 3000 frames, with the debug build verifying the recompiler's proofs and the GTE checked in lockstep against psyz's. On the board, an unpaced benchmark over a fixed stretch of the first level times the game thread's work (23.8 s at the start of the performance work, 16.2 s now) and a sampling profiler covers both cores.Known open items
Testing
Played on a Tab5 through the intro movies, the title and the first level; save and load states from the menu (and an automated save-at-2000 / load-at-2400 replay that matches guest RAM for 120 of 120 vblanks); pause, reset and quit. Desktop regressions as above.
🤖 Generated with Claude Code