Skip to content

Latest commit

 

History

History
412 lines (291 loc) · 84.8 KB

CURRENT_STATE.md

File metadata and controls

412 lines (291 loc) · 84.8 KB

P3P3DS current execution state

This is the current source of truth, newest entries first. Evidence concerns ULUS-10512; each section says whether it was measured on the PC runner, in Azahar or on a real New 3DS.

Logos at start: stale texture binding after a present (2026-10-12)

  • [VERIFIED] (Azahar, maintainer's 0.2.6 frame dumps of 2026-10-05, build c4060fc) the logos were already missing in 0.2.8-era builds: the top screen at vblank 300 is all black, while the PC runner shows the ATLUS logo there (displayed frames 70-140 = about vblank 236-420). Not a regression of the GE thread. The maintainer's 0.3.0 report at vblank 207 was taken before the logos (the loading screen with the blue diamond, frames 0-60), so its tex 0 up 0 says nothing about them.
  • New diagnostic: an empty sdmc:/p3p3ds/gpu_trace.txt writes the first 1,200 presents and every screen-space draw (target, CPU upload, texture and its decoded contents, blend, alpha and depth test, scissor, first vertices) to sdmc:/p3p3ds/gpu_trace.log (GpuRenderer::set_trace).
  • [VERIFIED] Trace in Azahar: per frame five clear-mode sprites, an opaque black quad (z 50000), the logo (through-mode strip, CLUT4 512x256, decoded contents identical to the PC: 9,320 coloured texels, alpha 255, modulate, blend SRC_ALPHA / 1-SRC_ALPHA, no tests, colour mask 0) and a fading black overlay; all emitted with correct vertices. The untextured loading diamond is visible, the textured logo is not.
  • [VERIFIED] Cause, by experiment in Azahar (frame dumps every 30 vblanks): disabling blending, or replacing the colour or alpha combiner, each brought the logo back, and every one of these experiments also forced C3D_TexBind again. present() draws through blit(), which binds the render target's texture to unit 0; the next draw (a clear) made the state valid again without binding a texture; the logo then used the same texture as in the previous frame (tex == bound_tex_), so C3D_TexBind was skipped and the GPU sampled the black render target. Introduced with the GPU state cache in 3c1f54c (first released in 0.2.8).
  • Fix (platform/3ds/gpu_renderer.cpp blit): bound_tex_ = nullptr after its bind. Azahar frame dumps now show the loading diamond (vblank 120-180), ATLUS (240-360), CRIWARE (420-540) and the title (690). [INFERRED] Any frame whose first textured draw reused the texture of the previous frame's last textured draw was affected, not only the logos; candidates: fades and transitions (issue #3). [UNVERIFIED] on hardware. CTest 27/27. Not released yet.

Report written off the game's thread; new targets cleared (2026-10-11)

  • [VERIFIED] User report attached to issue #3 (New 2DS XL, 0.3.0 build f23bc87, 3 minutes from start): speed 36%, ui 58.6 (last 10 s) / 62.2 (run average), aot 8, idle 31, fps 30.0 between stalls. On the maintainer's console the same build shows ui 0.7-0.8. ui covers only the report every 5 s: building the text, the bottom screen and rewriting sdmc:/p3p3ds/report.txt (profile_seconds.txt absent). [INFERRED] On that SD card one fopen/fclose rewrite takes about 3 s, and the game stood still for it every 5 s.
  • platform/3ds/main.cpp: the periodic report is posted to a writer thread on core 0 below the game's priority (a newer report replaces one not yet written); the final report at exit is still written directly after the thread stops. The report shows the longest rewrite so far: linear free : N KiB sd max M ms.
  • platform/3ds/gpu_renderer.cpp target_for: a new render target is filled black (C3D_FrameBufClear, queued before the frame's draws) instead of keeping what the previous application left in VRAM. [UNVERIFIED] whether this removes the garbage at start and in transitions (issue #3): the first draw into a target already starts from the guest framebuffer, so the garbage may come from elsewhere.
  • Verified: .3dsx builds, CTest 27/27. Released as 0.3.1.
  • [VERIFIED] Hardware, same user (issue #3, build be4e522, 4.5 minutes): speed 97% (last 10 s and run average 99 %), ui 0.0, sd max 2575 ms: one rewrite of report.txt still takes 2.6 s on that card, now without stopping the game. The user reports the freezes gone and transitions now black on hardware as in Azahar (no more garbage). [VERIFIED] the garbage was uninitialized VRAM of a render target.
  • Open after 0.3.1: the ATLUS and CRIWARE logos are still not shown (black); fixed below.

Hardware: GE on core 2, battles at 16-25 fps (2026-10-11)

Maintainer, New 2DS XL, build 7eabbb8 (the report shows build b073247: the build id is taken when CMake configures, not at each build), 7.6 minutes from start through the first days and the first battle. Files: report and five photos of the screens (not tracked).

  • [VERIFIED] Core 2 is usable by the .3dsx under the Homebrew Launcher and runs in parallel: cores : core2 ok, parallel x1.98, GE on core2.
  • [VERIFIED] Speed (maintainer's observation and bottom-screen photos): dorm 28-30 fps, dialogue scenes 30 fps most of the time, battle 16-25 fps. Photos: Yukari's Evoker 21.9 fps / 66 % (aot 63 hle 15 ge 13, gpu thd: busy 57 wait 4.3), the protagonist's Evoker 25.9 fps / 86 % (aot 70, busy 65 wait 0.4), battle command menu 19.0 fps / 63 % (aot 66, busy 56 wait 0.2). Final report: run average 90 % of real time, 24,583 vblanks, vtx: cache hit 100%, runs : 6591k draws in 193k runs, linear free 1,501 KiB. For comparison 0.2.7 on hardware: battle 5-7.5 fps, 18-25 %.
  • The game's thread rarely waits for the worker (wait 0.0-4.3 % of the time) and the worker is busy 56-65 %: the battle is now bound by the game's own code (aot 63-70 %), not by the renderer. [INFERRED] Next gains in battle come from the game code (AOT output, HLE call cost), not from the GE.
  • [VERIFIED] New defect: the logos at start are replaced by a gray screen with stripes of old video memory (tiled pictures left over from the Homebrew Launcher, same look as issue #3 on hardware); after them the game continues normally. Report at that moment: vblank 207, draws 322, tex 0 up 0, present gpu 54 cpu 99 same 53. Not seen with the GE on the main thread (0.2.9). [UNVERIFIED] cause: a render target presented on the GPU path (gpu_seq > cpu_seq) before it holds the picture, i.e. the order of CPU-write notes and draws differs once they are queued for the worker. Next step: the same start with sdmc:/p3p3ds/no_gpu_thread.txt to confirm it is the worker, then a frame log of the first 300 vblanks.
  • Released as 0.3.0.

GE back end on core 2 (2026-10-11)

  • [VERIFIED] Azahar run of build 9f2dc29 (maintainer, 2026-10-10): cores : core2 ok, parallel x2.00. Same run: Evoker scenes 16.3 fps / 54 % and 13.5 fps / 35 %; in the battle proper (Orpheus attacking) 9.3 fps / 34 %, ge 33-36 %, prep 17-20 %. [UNVERIFIED] core 2 on hardware.
  • [VERIFIED] Overlap available (PC production build, battle window vblank 22165-32165, new --profile counters): the guest time between a sceGeListEnQueue and the game's next sceGeDrawSync is 47.1 % of the host time (5,000 windows, one per frame); GE finish callbacks take 0.1 %. [INFERRED] On the 3DS the renderer (about 36 % of the battle) can mostly run beside the game: about 1.4x, battle 34 % -> about 47 %, Evoker 54 % -> about 75 % speed.
  • core/include/p3p3ds/ge/async_renderer.hpp: AsyncRenderer wraps a GeRenderer. The GE command processor stays on the game's thread (list parsing is 1-2 %; signals, callbacks and sceGeGetCmd need its state) and queues a snapshot of the GE registers and matrices when state_version changed, the CLUT when its hash changed, and each draw / transfer / sync; a worker replays the queue (single-producer single-consumer byte ring, release/acquire indices) into the real renderer. New GeRenderer::drain(), called by GeManager::sync (sceGeDrawSync / sceGeListSync): the game's thread waits there until the worker is done, as the PSP GE would be. Without a worker every call goes straight through.
  • 3DS (platform/3ds/main.cpp): the worker runs on core 2 when the start probe reported core2 ok and sdmc:/p3p3ds/no_gpu_thread.txt is absent; everything that touches citro3d goes through it (present posted per vblank, CPU writes to EDRAM posted at most once per 64 KiB block between other queued work, frame dumps read once the worker is idle, the queue drained before aptMainLoop and at exit). Report: cores : ... GE on core2|core0 and gpu thd: busy B wait W (worker busy share; the game's thread waiting for it, counted in hle); draw: and vtx: are then the worker's steps.
  • [VERIFIED] PC runner --ge-thread (software renderer behind AsyncRenderer on a std::thread, drained after every list): new_game.txt 60M dispatches gives the same events and WAV as the direct path (055626D9..., 22532254...); the first battle route with rasterization from vblank 21000 to 24000 gives byte-identical event dumps (7D4CF4A5..., 11,918 frame hashes, 1,517 of them rendered battle frames). test_ge_geometry: 6,000 random draws, state and CLUT changes, transfers, syncs and posted calls through the smallest queue (many wraps) replay exactly as direct calls. CTest 27/27, --verify-bootstrap PASS, .3dsx builds. [UNVERIFIED] speed and stability on the 3DS (Azahar and hardware).

Old 3DS: stops at start, out of memory (2026-10-11)

  • [VERIFIED] Maintainer, original Old 3DS, release 0.2.9 (build fa6e196), photo of the bottom screen: APPLICATION region 65,536 KiB, heap 9,468 KiB after the code is loaded (34 KiB in use), linear free 10,713 KiB; the runtime then fails to allocate 33,554,432 bytes (the 32 MiB PSP RAM) and stops with "Out of memory creating the runtime (std::bad_alloc)". The failure is the intended clean stop, not a crash.
  • This confirms on hardware the estimate in NEXT_STEPS.md ("Old 3DS / 2DS"): about 112 MB of application memory are needed (code now about 41 MB after the AOT view change, 32 MiB PSP RAM, about 10-15 MB of function tables), the Old 3DS gives 64 MB by default and at most 96 MB. Even with 96 MB it would also run its CPU at 268 MHz without L2 cache, about a third of the New 3DS. [INFERRED] It needs a different approach (compile only hot code ahead of time, interpret the rest), not tuning.

Game code: local AOT memory view (2026-10-10)

The game's own code (aot) is the largest share of the battle (45 % in Azahar, 0.2.9) and stays on the main thread whatever happens to the GE.

  • [VERIFIED] GE list parsing is not worth optimising: the battle window (vblank 22165-32165, PC production build) executes 26.4 M GE commands = about 5,300 per frame, about 49 us per frame on the PC (9 ns per command); [INFERRED] about 1-2 % of the time on the 3DS, not 8 % as estimated from sceGeListEnQueue minus ge (that difference is mostly run filling in sync() and the finish callback's guest code). New: [PROFILE] ge commands= on the PC runner (HostProfile::ge_commands).
  • Generated units took the AOT memory view by reference and the compiler reloaded its base and limits after every guest store (a store through uint8_t* may alias it). Now each unit entry copies it into a local (const GuestMemory::AotFastView aot_mem = aot_mem_ref;, minimal patch in recomp/PSPRecomp/tools/codegen_main.cpp; chained calls still pass the shared view). 3DS, the Builder's compile flags and the PC production build also define PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH (the PSPRECOMP_WATCH_WRITE debug watch reads an environment variable, always empty on the 3DS; VRAM stores keep their observer on the slow path).
  • [VERIFIED] 3DS ELF .text 44,567,648 -> 40,914,544 bytes (-8.2 %, about 3.5 MiB more free memory). PC production battle window 4.31 / 3.94 s -> 3.83 / 3.67 s (two runs each, noisy). new_game.txt 60M replay unchanged (events 055626D9..., WAV 22532254...), --verify-bootstrap PASS, CTest 27/27 (psprecomp_tests expects the new parameter name). [UNVERIFIED] speed on the 3DS.

Second CPU core: availability probe (2026-10-10)

Before moving the GE back end to another core (estimate: at most about 1.6x in battle, since the game code and GE list parsing stay on the main thread [INFERRED]), the runner checks at start whether the .3dsx can create a thread on core 2 (else core 1 with APT_SetAppCpuTimeLimit(30)) and whether it really runs in parallel: the same busy loop alone, then on both threads at once; parallel = 2 x alone / together (2.0: a real second core, 1.0: time-shared, as an emulator may do). Shown as cores : in the report and on the loading screen. [UNVERIFIED] the result on hardware and in Azahar.

Runs of contiguous model draws (2026-10-10)

For the per-draw cost of the battle (about 2,000 model draws per frame, median 4 vertices).

  • [VERIFIED] (Azahar, maintainer, build 988c0af): the protagonist's Evoker scene 15.8-16 fps / 52-53 % speed (was 14-15 fps / 45-48 %), Yukari's Evoker 38 % speed (was 31 %); sceGeListEnQueue 25.7 ms per frame in the battle (was 32.4 ms), vtx 9.6 % (was 30.1 %); runs : 2117k draws in 62k runs (34 draws per run), vertex cache 202 sources, 100 % hits. Screenshots show the models intact. hle rose from 11 to 20 % because filling a run now happens in sync() at the end of the list, outside the renderer's sampled scope. [UNVERIFIED] on hardware. Released as 0.2.9.

  • Azahar run of the cheaper-profiling build (maintainer, 2026-10-10): Yukari's Evoker 11.3 fps / 31 % speed (10.9 / 29 % before), hle 21 -> 11 %; the protagonist's Evoker 14-15 fps / 45-48 %, ge 40, aot 40, idle 7; draw: prep 6.6 vtx 30.1, fill 15.6. Per second (profile_seconds.csv): one sceGeListEnQueue costs a median 6.1 ms where ge < 15 % and 33.1 ms where ge >= 35 % (correlation with ge 0.75): list execution is synchronous, so the renderer's per-draw work is inside it, and on the PSP it would overlap the CPU. [INFERRED] the fine draw: shares are inflated in Azahar (every timing read is an SVC, which the emulator may use for its own events); fps and speed are the reliable figures.

  • [VERIFIED] PC draw log: consecutive non-indexed model draws read consecutive guest memory (vertex address +72 bytes = 4 vertices x 18 per draw).

  • core/include/p3p3ds/ge/model_batch.hpp: append_triangles (strip/fan unrolling shared by both paths) and ModelRun. gpu_renderer.cpp: a non-indexed, non-morph model draw whose vertices start where the open run ends (same vertex type and material) only appends its indices; the run's block (up to 1,024 vertices reserved) is filled once when the run ends, through the vertex cache (one lookup and one copy per run). A run ends at the next draw that does not continue it, before any other arena allocation, when the batch is submitted, before a block transfer, and when GE execution returns to the guest (new GeRenderer::sync(), called at the end of GeManager::pump and before a finish callback), so vertices are read before the CPU can change them. Indexed and morphing draws keep the per-draw path. Report: runs : N draws in M runs.

  • [VERIFIED] test_ge_geometry: 400 random draws (lists, strips, fans; random breaks of the vertex address) give, corner by corner in guest memory, the same triangles through runs as converted one by one, with fewer than half as many runs as draws; ModelRun::continues checks end address, room, vertex type and material.

Battle: what the vertex cache did, cheaper profiling, where HLE time goes (2026-10-09)

Maintainer's Azahar run of the vertex-cache build (first Shadow battle, report "last 10 s"): 7-10 fps, speed 29-31 %, aot 33 hle 21 ge 36 idle 8; draw: prep 6.6 vtx 23.7 unif 1.3 sub 0.5; vtx: fill 12.6 cache hit 100%; vcache 1,727 KiB, 3,442 sources, 941k hits, 4k misses, 0 volatile. Dorm 18-21 fps (cache not involved: through mode, hit 0 %; no Azahar dorm figure of 0.2.8 to compare with).

  • [VERIFIED] The cache works (100 % hits) but vtx fell only from 26.5 to 23.7 %: the battle is about 1,975 draws per displayed frame with a median of 4 vertices (mean 6.4; PC draw log of two battle frames, --draw-log, which now also prints the vertex and index addresses va=/ia=). The unpack of so few vertices was never the main cost; per-draw work is. [WRONG] the earlier reading "26.5 % = vertex unpack" (vtx holds all per-draw vertex work).
  • [VERIFIED] 3DS profiling read steady_clock (an SVC plus 64-bit software divisions) twice per HLE call and per GE primitive. Now: raw system ticks (RuntimeProfile::clock / HostProfile::clock, converted to seconds only for the report), every HLE call still timed (waits and presents inside calls make sampling unreliable), GE primitives timed one in 8 (SampledProfileScope). [UNVERIFIED] the gain and the new split on the 3DS.
  • [VERIFIED] HLE calls in the battle window (PC production build, vblank 22165-32165 of first_battle.txt, histogram difference): 4.16 M calls = 24,960 per game second, 87.6 % sceKernelCpuSuspendIntr / sceKernelCpuResumeIntr (10,929 pairs per game second, about 360 per frame), then sceKernelGetSystemTimeLow 817/s, sceAudioOutputPannedBlocking 689/s, lwmutex lock/unlock 404/s each.
  • [VERIFIED] HLE time by import in that window on the PC (--profile, new per-import list; inclusive times): sceGeListEnQueue 10.1 % of host time (91 us per call, one per frame: GE list execution, the renderer's own share 3.9 %), sceAudioOutputPannedBlocking 5.1 %, Suspend/ResumeIntr 3.6 % together (0.05 us each), __sceSasCore 1.0 %. On the 3DS the GE list command processing counts as hle, not ge (ge is the renderer only). [UNVERIFIED] the same split on the 3DS: sdmc:/p3p3ds/profile_seconds.txt writes the top three imports per second to profile_seconds.csv.
  • PC runner: --stop-vblank <n> ends a run at a vblank (a sampled scene without the rest of the route); --profile with --sample-from covers only the sampled window and lists the 12 imports with the most time.
  • Replay new_game.txt 60M dispatches unchanged (events 055626D9..., WAV 22532254...); CTest 27/27.

Model vertex cache for battle speed (2026-10-08)

For the battle's vertex unpack (26.5 % of time in Azahar, 0.2.8). Not yet measured in Azahar or on hardware.

  • core/include/p3p3ds/ge/vertex_cache.hpp: converted model vertices kept across frames, keyed by vertex address, vertex type (GE 0x12), referenced index range and, for vertices without a colour, the material colour (0x55/0x58). An entry holds a copy of the exact guest bytes and is used only while they are identical (memcmp, no hash), so CPU writes to vertex buffers are always seen. A source that changes on 3 uses in a row (vertices animated by the game's CPU code) is left uncached for 120 frames. Budget 2 MiB of the main heap; over budget, sources unused in the current and previous frame are dropped (at most once per frame), otherwise the draw is unpacked as before.
  • platform/3ds/gpu_renderer.cpp draw_model: a hit is one memcpy of the shader vertices into the frame's vertex arena instead of decode_model_vertices plus the repack loop; batching, the arena and GPU data lifetime are unchanged (the GPU reads only this frame's arena). Morphing draws are never cached.
  • Measuring: bottom-screen line vtx: fill F cache hit H% (F: share of time getting model vertices into the arena, part of vtx; H: hits over the last 10 s); SD report line vcache (KiB, sources, hits, misses, volatile, full). An empty sdmc:/p3p3ds/no_vertex_cache.txt turns the cache off for A/B runs.
  • [VERIFIED] test_ge_geometry: reuse only for identical bytes, reconversion after a one-byte change, material in the key, volatile window, budget refusal and eviction, and cached decoded vertices of the battle type 0x00015A equal to a fresh decode after a vertex is rewritten. CTest 27/27. 3DS objects build with devkitARM.
  • [UNVERIFIED] the speed gain; [INFERRED] most battle models are static in guest memory (skinning is done by the shader from bone matrices, the census found no morphing), so a high hit rate is expected.

Fixed: VFPU prefixes of new threads, retry lost after preemption (2026-10-08)

The first two defects of the code review below.

  • [VERIFIED] AllegrexContext now starts with source/target prefixes 0xE4 (identity) and destination prefix 0 (minimal patch in recomp/PSPRecomp/include/psprecomp/allegrex_context.hpp), so every new PSP thread and the root context start like a PSP thread. Hardware evidence: pspautotests baseline 9/48 programs exact instead of 6/48 (cpu/vfpu/callout, convert_scaled, minmax now exact; exact, minmax_zero, overlap closer), 0 worse; tests/autotest_baseline.txt updated. test_interpreter_diff no longer sets 0xE4 by hand.
  • [VERIFIED] The runtime records the thread-switch generation at the entry of each import call (runtime_import_entry_generation, minimal patch in recomp/PSPRecomp/src/runtime.cpp, nested calls restore the outer value). ThreadManager::resume_pc uses $ra only when the calling thread is still the one inside the call; a thread loaded during the call keeps its saved pc, so a thread woken from a retry wait and preempted in the same post-import hook re-runs sceAudioOutputBlocking (and the other retry waits) instead of continuing at $ra with a stale $v0.
  • Tests: test_threadman_sync retry_survives_preemption (offsets -4..+4 around the collision of the slot-free time and a higher-priority deadline; asserts the preempt-after-wake case is really reached) and new_thread_vfpu_prefixes. Both fail on the previous code (stashed fix: the stub check and both prefix checks fail) and pass now. CTest 27/27, --verify-bootstrap PASS.
  • [VERIFIED] Replay new_game.txt, 60M dispatches, two runs with the fix: events SHA-256 055626D9CA87DD748A479CCEC81D610A4AF8B003835BE2D1480E0843362EB19C, WAV 22532254F1E331E3C606702FFDFBC18AF1757168F456CD7A7C0BBE33663C54C1, both identical to each other and to the build before the fix: on this route (title, NEW GAME, protagonist selection) neither defect changed P3P's behaviour. [UNVERIFIED] effect on later scenes (battle, the audio threads under load) and on the 3DS.

Code review: open defects and hypotheses (2026-10-08)

Read-only review of HEAD fa45224; nothing in the runtime changed. Repro programs and notes are local (.tmp/review_{codegen,hle,3ds,tests}/, not committed). None of these is fixed yet; README "Known issues" lists the user-visible ones.

Correctness (ranked by likely impact):

  • Fixed (section above). [VERIFIED] New PSP threads start with VFPU prefixes 0 instead of 0xE4: core/src/hle/threadman.cpp start_thread assigns AllegrexContext{}, whose vfpu_ctrl is zero (allegrex_context.hpp). Prefix 0 makes every lane read X, so the first prefixed VFPU operation of each thread reads a broadcast X (host demo: C000.q reads 1 1 1 1 instead of 1 2 3 4). The value on hardware is [INFERRED] from PPSSPP (sceKernelThread.cpp, thread context reset: S/T prefix 0xE4, CC 0x3F, fcr31 0xE00). test_interpreter_diff sets 0xE4 by hand and cannot see it. [UNVERIFIED] which P3P threads are affected.
  • Fixed (section above). [VERIFIED] A retry wait is skipped when the thread is preempted right after being dispatched: ThreadManager::resume_pc treats "pc is an import stub" as "inside an HLE call, resume at $ra", but a thread woken from a retry wait also sits at the stub and must re-run the call. Repro: a priority-40 thread in sceAudioOutputBlocking resumes at $ra with a stale $v0 when a priority-32 thread's sceKernelDelayThread(0) expires in the same post-import hook. Affects sceAudioOutputBlocking/OutputPannedBlocking, sceIoWaitAsync (stale result, op left pending), sceCtrlRead*, module start waits. Must be fixed before NDSP audio.
  • [INFERRED] Back-face culling: since 72bd103 through mode and transform mode use opposite rules (software_renderer.cpp, gpu_renderer.cpp); PPSSPP uses one screen-space rule from the CULL bit in both modes (GPU/GLES/StateMappingGLES.cpp, GPU/Software/TransformUnit.cpp). The split may compensate an unidentified Y flip. Test: count through-mode draws with CULL on in --draw-log, then compare frame hashes of the input scripts with one uniform rule.
  • [VERIFIED] SysMem and thread stacks: stacks bump down from 0x09FF0000 with no check against the SysMem heap growing up to 0x09FF0000 (repro: heap block 08ED0000..09FF0000 and a new stack 09FEC000..09FF0000 overlap); stacks are never reclaimed (255 create/delete cycles of 64 KiB stacks fail with 0x80020190); sceKernelStartModule leaks a 256 KiB stack; free_partition_memory never reuses space; Addr allocations do not check overlap; High is allocated low. [UNVERIFIED] P3P's own peak and alloc/free pattern.
  • [VERIFIED] Mutexes (and lwmutexes) are not released when the owning thread exits (repro: deadlock); PPSSPP releases them.
  • [VERIFIED] Event flag outBits: not written on a real timeout and written on a zero timeout; hardware does the opposite (pspautotests/tests/threads/events/wait/wait.expected).
  • [VERIFIED] sceIoOpenAsync returns open errors synchronously; uOFW reports them through the async result (iofilemgr.c, open_iob).
  • [VERIFIED] GE: finish callbacks run inside sceGeListEnQueue/UpdateStallAddr instead of later; SIGNAL handlers are never called; EnQueueHead runs the list at once.
  • [VERIFIED] FPU/VFPU: vsin/vcos/vrot use the host sinf/cosf (vcos(1.0) = -4.37e-8 instead of 0); sqrt.s/div.s NaN sign follows the host; the 3DS runs with flush-to-zero and default NaN (libctru syscalls.c), the PC does not, so PC runs are not a bit-exact reference for denormals and NaN; divu by zero gives LO 0xFFFFFFFF where hardware gives 0x0000FFFF for dividends up to 0xFFFF (the 4 cpu_div lines in the autotest baseline); madd/msub accumulate in signed 64-bit (undefined on overflow; GCC wraps in practice); round.w.s rounds half away from zero (P3P has none); vdot zero sign differs between interpreter and AOT.
  • [VERIFIED] sceUtilitySavedata builds host paths from guest strings without checks (.., /, empty names); delete modes call remove_all, and an empty game+save name would remove every save.

Speed and memory (3DS):

  • [VERIFIED] Only Core 0 is used; GE work runs synchronously on the main thread. Core 2 needs exheader flag 0x2000 (see 3DS_PLATFORM.md section 3; the old claim about APT_SetAppCpuTimeLimit is [WRONG]).
  • [VERIFIED] The profiling timers are always on (platform/3ds/main.cpp): two steady_clock::now() (an SVC plus 64-bit divisions) per HLE call and per GE PRIM. [INFERRED] 1-5 % of the time, and they inflate the hle/ge shares.
  • [VERIFIED] AOT guest memory access: a lw is 10 ARM instructions with 4 loads (view base and limit reloaded after every guest store through possible aliasing); a local copy of the fast view cuts unit 0049 by 2.7 %, PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH by 1.1 %. AOT units build with -Os -fno-schedule-insns2; only size, never run time, was measured for these flags.
  • [INFERRED] Vertex unpack: the battle's main vertex types (0x00015A: s16 position/normal, u16 UV, 4444 colour; skinned 0x014342: u8 weights) are formats the PICA200 reads natively except u16 (sign fix-up in the shader) and packed colours. Copying the raw bytes and folding the scale factors into the matrices may remove most of the 26.5 % instead of caching unpacked vertices. Time the steps inside the vtx bucket on hardware first.
  • [INFERRED] Runtime::functions_ holds about 170,000 entries, each with a 16-character std::string (one past the short-string limit, so a heap allocation each): roughly 10 MiB of the 10.7 MiB of free heap could be freed by storing a unit index.
  • [INFERRED] Field drops: every file read, including ms0: and SD mods, holds the caller for 200 us + 0.5 us per byte of virtual time (UMD speed, IoManager::transfer_us), and the 3DS pacing then sleeps that difference. At 89-91 % speed, 30 fps would show as 27, but the dorm shows 21-26, so frames are lost in virtual time too. Test: dorm route on the PC runner with near-zero read latency, compare game frames per virtual second.
  • [INFERRED] Virtual-time policy: guest code costs 0 us, so slow scenes play in slow motion (never frame-skipped), and a future NDSP output fed by the virtual clock will underrun; a thread spinning on memory without HLE calls never lets deadlines expire (hang risk; the PSP would preempt).

Tests and CI:

  • [VERIFIED] release.yml publishes packages without running CTest and does not depend on ci.yml.
  • [VERIFIED] Without references/pspautotests or the game, test_sascore (0 of 53 curves), test_hle_graphics and test_psp_eboot skip their main checks but report Passed; test_mpeg returns 77 before its reference-free ring-buffer tables run. CI's "3 skipped" hides 3 partial skips.
  • [VERIFIED] tools/run_autotests.py baseline: 10 entries already have mismatches equal to their expected line count and cannot get worse; a change of stop reason is not detected; --update records regressions.
  • [VERIFIED] verify_p3p_decoder and p3p_analysis_comparison compare against the implementation's own earlier output (change detectors, not correctness checks). replay_check.sh (skill p3p-run-triage) ignores the runner's exit status, so two identical early crashes print "REPLAY IDENTICAL".
  • [VERIFIED] Game-specific values outside profiles/p3p/config/ (CLAUDE.md section 8): unit count 237, load base and unit span in platform/pc/builder/pipeline.cpp; mod prefix ms0:/PSP/P3P in both runners.
  • [VERIFIED] recomp/PSPRecomp/profiles/vcs/** (227 files, FFmpeg headers) and profiles/p3p/config/frontier_checkpoint.json are not used by the build.
  • Running CTest from Git Bash picks MSYS /usr/bin/ctest, which mangles the paths (all BAD_COMMAND); use the CMAKE_CTEST_COMMAND from build/CMakeCache.txt (27/27 pass that way).

Battle speed: per-draw GPU work cut, 5.8 -> 9.9 fps in Azahar (2026-10-06)

Measured by the maintainer in Azahar (New 3DS mode), Evoker scene of the first battle, "last 10 s" of the bottom-screen report; the maintainer finds Azahar close to the console. [UNVERIFIED] on hardware.

Build Change fps speed GE share
72bd103 (culling fix, baseline) 5.8-5.9 19 % 66 %
3c1f54c only changed state sent per draw (not measured separately)
cea9aae uniforms rebuilt only on change; one-pass vertex unpack 6.5-6.6 22 % 62 %
03c2aec consecutive model draws with identical state batched 7.7-7.8 24-25 % 55 %
65d261b fast path for draws with unchanged GE state 9.9 33 % 42 %
  • [VERIFIED] (Azahar) Before: each of about 2,000 draws per frame re-sent the vertex buffer configuration (about 40 command words), texture unit, TexEnv, blend/depth/cull and scissor; the command buffer filled about four times per frame. After 3c1f54c: no early submits in battle, about 25-33 command words per screen draw.
  • [VERIFIED] (Azahar) Batching: 2.07 M model draws became 55 k GPU draws (PC draw log: 4,076 draws in two battle frames form about 100 runs of identical state). Fast path: 1.81 M of the 2.07 M draws.
  • Remaining split in battle (65d261b, Azahar): aot 29, hle 19, ge 42 (vertex unpack 26.5, preparation 4.4, uniforms 1.0, submit 0.5), idle 8 %. The game's own code (aot + hle, 48 % of the time at 33 % speed) alone limits the battle to roughly 70 % speed; next targets: vertex unpack per draw (cache unpacked models across frames) and the AOT/HLE cost.
  • Report on the bottom screen: line draw: (prep, vtx, unif, sub: % of time per draw step, sampled every 16th draw with the cost of the tick call removed), flush cmd/arena (early submits), w/draw (command words per screen draw), merge Nk>Mk fKk (model draws > GPU batches, fast-path draws).
  • Unattended runs: sdmc:/p3p3ds/autotest/ (input.txt route, savedata.txt slot, stop.txt vblank, dump_every.txt, ms0/), for measurements without anyone at the controls.

Hardware run of build 72bd103: first battle renders correctly, 5-7.5 fps (2026-10-05)

Maintainer, New 2DS XL, 572 s wall, quit with START+SELECT in the battle; three photos (Evoker scene, protagonist on the roof).

  • [VERIFIED] The culling fix works on hardware: the protagonist has her face (closed eyes in the Evoker scene, open eyes with the Evoker drawn), hair and uniform without spikes; the roof, its walls, the green floor and the starry sky are drawn. The maintainer: "everything works".
  • [VERIFIED] Speed in the Evoker scene: 7.5 game frames/s, last 10 s 25 % of real time (aot 21, hle 8, ge 63, idle 0 %; GE: hash 0.3, GPU wait 9.8-10.5 %). At quit (battle): 5.2 frames/s, last 10 s 18 % (aot 17, hle 6, ge 70, idle 5 %; GPU wait 20.4 %). Run: 4.19 M draws, 17.98 M triangles, 3,458 frame flushes, 665 render-target binds.
  • [INFERRED] The GE share is still CPU work per draw (vertex unpack, state and uniform updates; about 1,000 draws per frame), and the GPU wait doubled in the battle proper: the GPU now draws the full scene (walls and floor were culled before). Next: measure the split of GE time.

First battle on the PC; inside-out models were a culling bug (2026-10-05)

  • PC route through the first battle: the maintainer's 4/9 evening save, own room, cursor on the bed right of the pillow, "Sleep", then CROSS through the Dark Hour scenes, the Evoker, Orpheus and the battle to the hospital (local, ignored: profiles/p3p/input/local/first_battle.txt; about 55 M dispatches to the Evoker scene).
  • [VERIFIED] GE census of that run: 7.8 M transform-mode draws, all lit; 2.0 M skinned (u8 weights, 1-6 per vertex; weight sums 1.000 in every logged vertex); no morphing, no texture-coordinate generation; the fog enable bit (0x1F) is set on 7.2 M draws, but no fog is visible in the game (maintainer) [UNVERIFIED whether the fog parameters have any effect; not a known issue]. The dorm field before it is through mode only (2.7 M draws): the battle is the first use of transform, skinning and lighting.
  • [VERIFIED] The PC software renderer (CPU reference) showed the same picture as the 3DS photos of build 029f3c8: spiky collar and sleeves, a face without eyes, black surroundings. So the shader was not the cause.
  • [VERIFIED] Cause: back-face culling. The eye mesh (texture with the eyes, one bone) projects onto the face but every one of its triangles was culled. With the culled winding inverted for transform mode (through mode unchanged: its rule was measured on the logos in Azahar) the PC frames show the face with eyes, the hair and uniform without spikes, and the roof, sky, moon, the Shadow, Orpheus and Thanatos. The old rule drew the back faces of closed models (inside out) and culled the walls and floor turned to the camera.
  • Fixed in both renderers: software renderer and PICA C3D_CullFace (platform/3ds/gpu_renderer.cpp). Unit test test_ge_renderer case 6: CULL = 1 keeps triangles counter-clockwise on screen (y down) in transform mode; it fails with the old rule. [UNVERIFIED] on hardware until the next test.
  • PC runner --draw-log <file> --draw-log-from <frame>: one line per GE draw (vertex type, framebuffer, texture, blend, alpha and depth test, culling, screen bounds) for the two displayed frames after that frame index, and for skinned draws the bone matrices and per vertex the weights and screen position.

Hardware run of build 029f3c8: shader draws characters, battle still wrong and slow (2026-10-04)

Maintainer, New 2DS XL, 448 s wall, quit during the first battle; two photos of the Evoker scene.

  • [VERIFIED] Field: own room 30 fps, dorm lounge 25, 2nd floor 21, 3rd floor 26; speed about 89-91 %. Not better than before the shader: field draws are mostly through mode (2D), which still takes the CPU path.
  • [VERIFIED] Battle: the protagonist (Evoker scene) is now drawn, textured and lit; the Evoker is in her hand. Distorted: some limbs and parts of the hair are stretched or misplaced ("exploded"), the face area is blank, the surroundings stay black except a few lit beams.
  • [VERIFIED] Battle speed: 7-11 game frames/s at the Shadow. Report, last 10 s: speed 19-26 %, aot 21 %, hle 7-8 %, ge 62-65 %, idle 6 %; GE: texture hashing 0.3 %, conversion 0.0 %, GPU wait 7.2-8.4 %. Run: 3.67 M draws, 2,759 frame flushes, 6 CPU-to-target uploads.
  • [INFERRED] The GE share is CPU work per draw outside hashing, uploads and GPU wait: unpacking model vertices (ge::decode_model_vertex per vertex), the screen path of the many 2D draws, citro3d state and uniform updates. To measure before changing: a split of GE time (vertex unpack, screen path, state).
  • [UNVERIFIED] causes of the distortion, to check one by one against the PC software renderer (same decode, CPU skinning/lighting reference) once the PC route reaches the battle (profiles/p3p/input/local/first_battle.txt reaches the own room): (1) the shader's float24 precision for bone blending; (2) a mismatch between the GPU path and the CPU reference (bone row layout, weight count, uniform cache across draws); (3) PSP features still missing on both paths (texture-coordinate generation / environment mapping, morph + skin combinations). The black surroundings may be lit geometry whose material or light setup the shader handles differently from the PSP, or a texture mode not yet supported.
  • Comparison switch for the maintainer: an empty sdmc:/p3p3ds/cpu_vertices.txt forces the CPU transform (no skinning/lighting on that path on 3DS).

GitHub Actions: CI and Builder releases (2026-10-04)

  • [VERIFIED] First automated release v0.2.6 (commit 2e053cd): all jobs green, release published with P3P3DS-Builder-0.2.6.zip (2,353,068 bytes, FileVersion 0.2.6) and P3P3DS-Builder-0.2.6-linux.tar.gz (2,916,300 bytes); sdk/VERSION 0.2.6 2e053cd in both.

  • Both CMake projects build without the game (P3P_HAVE_GAME off when profiles/p3p/game/eboot.elf is missing): host runtime, tools, Builder and the tests that do not run game code; the New 3DS runtime objects and libraries plus the CWCheat patch list (now its own custom command), the runner objects in the object library p3p3ds_objects.

  • Two tests depended on local state and now do not: test_hle_graphics skips its reset-list block without the ELF; test_chase_frontier.py creates .tmp/ (absent in a fresh clone; it passed only when another test had created it first).

  • [VERIFIED] Clean worktree without the game (Windows MinGW): build OK, CTest 24/24 (3 skipped: pspautotests absent), also with .tmp/ removed; 3DS build without the game: main.o, gpu_renderer.o, ge_shbin.o, both libraries and a patches.txt identical to the game build's. With the game: host CTest 27/27, .3dsx links.

  • .github/workflows/ci.yml (push to master, pull requests) and release.yml (tag v* -> packages for Windows and Linux, GitHub release; manual run = dry run). README "Continuous integration and releases".

  • [VERIFIED] First runs on GitHub: CI green on all three jobs (Linux GCC 14 CTest 24/24 with 3 skipped, about 1 min; Windows MSYS2 24/24, about 3 min; New 3DS runtime about 1 min). Release dry run (manual, ref master) green; publish skipped as designed. Packages: Windows zip 2.35 MB (FileVersion 0.2.5), Linux tar.gz 2.92 MB with static P3P3DS-Builder and psp_recomp (no PT_INTERP/PT_DYNAMIC), stripped runtime objects, sdk/VERSION = commit.

  • [VERIFIED] End to end: the CI-built Windows Builder turned the maintainer's ISO into p3p3ds.3dsx (50.8 MB) in 259 s with the local devkitPro (--jobs 10). [UNVERIFIED] that .3dsx on hardware; the Linux package on a real Linux machine with devkitPro.

Frame drops at dialog text and character loads: Mod Support stat (2026-10-04)

Azahar 2126.1.2 (New 3DS mode, build b5f79144 plus this change's diagnostics), maintainer playing the opening.

  • [VERIFIED] Not Azahar: at a drop its status bar showed Speed: 100% with App: 17 FPS. Not the game logic: it renders 1 frame per 2 vblanks in drops too (17.7 frames at 35 vblanks/s); the vblank rate falls, i.e. the runtime falls behind real time.
  • [VERIFIED] Not the recompiled code: per-second split (new sdmc:/p3p3ds/profile_seconds.txt switch -> profile_seconds.csv), 19 slow seconds (< 50 vblanks) vs 123 steady: AOT share lower in slow seconds (34.6 % vs 39.9 %), HLE share up (32 % vs 18.8 %) with fewer HLE calls.
  • [VERIFIED] Cause: sceIoGetstat at 83 ms per call (48 calls, 4,007 ms in the slow seconds; 2 calls in 123 steady seconds). PC runner new_game.txt events: every getstat is ms0:/PSP/P3P/bind/... (CWCheat Mod Support), all missing, including voice files (bind/data/sound/voice/v450001.afs) loaded with dialog lines. Each lookup did several SD operations in HostFileSystem::resolve.
  • Fix: HostFileSystem(root, fixed_contents=true) for the read-only mod folder lists each directory once and answers lookups from memory (sizes only for found files). Regression checks in tests/test_iofilemgr.cpp compare it with uncached lookups. [UNVERIFIED] the drop is gone in Azahar / on hardware: the 3DS build with the fix was not run.
  • Diagnostics kept: io (VFS read time) in the report split; per-import HLE time in psprecomp::runtime_profile() (per_import), top 3 per second in the CSV.
  • [VERIFIED] CTest 27/27 (2 skipped as before).

P3P3DS Builder on Linux (2026-10-04)

Tooling only; the runtime frontier is unchanged.

  • platform/pc/builder/: the pipeline is portable (posix_spawn on POSIX, CreateProcessW + job object on Windows; arguments as a list instead of a quoted command line; .exe suffix only on Windows; devkitPro from $DEVKITPRO or /opt/devkitpro; job count from MemAvailable). On Linux P3P3DS-Builder is a console program with the --cli options; the Win32 wizard is unchanged. make_release.py packages the host platform (-linux.tar.gz on Linux).
  • [VERIFIED] Linux GCC 16.2 (nixpkgs): builds static, CTest 27/27 (2 skipped as before). Stub-devkitPro run (real decryption, SHA-256 be2abbd4..., and real psp_recomp; stub compiler/linker/packagers, paths with spaces): forced compiler failure reported with its log, resumed build compiled 174 of 238 units, linked through the response file and packaged.
  • [VERIFIED] MinGW-w64 GCC 15.3 cross build of P3P3DS-Builder.exe links; imports only system DLLs; -Wall -Wextra warning counts unchanged (pipeline 0, wizard 11 pre-existing).
  • [UNVERIFIED] a real Linux build with devkitPro to a .3dsx; the Windows .exe was not run; cancellation by Ctrl+C on Linux.

Vertex shader: GPU transform, skinning and lighting (2026-10-03)

For the black, slow battle (section below). Not yet run on hardware or in Azahar.

  • Core (core/src/ge/geometry.cpp): vertex decoding reads guest memory through a host pointer instead of per-component accessors; morph targets are blended with MORPH_WEIGHT 0x2C-0x33 (the vertex stride now counts every morph target); skinning with the 8 bone matrices; GE vertex lighting (4 lights: directional/point/spot, ambient/diffuse/specular/powered diffuse, attenuation, material colours from the vertex colour per MATERIAL_COLOR 0x53, scene ambient, NORMAL_REVERSE). Register numbers from PSPSDK guInternal.h/sceGuLight*.c; behaviour cross-checked against PPSSPP GPU/Software/Lighting.cpp (no code copied). This is the CPU reference and is used by the PC software renderer: lit draws on the PC now get lighting. [INFERRED] float arithmetic instead of the hardware's fixed point; separate specular colour added to the primary colour.
  • 3DS (platform/3ds/shaders/ge.v.pica, gpu_renderer.cpp): transform-mode triangles, strips and fans are drawn as model vertices; the CPU only unpacks the referenced vertices and copies indices (C3D_DrawElements), the PICA200 shader does bone blending, WORLD, VIEW x PROJ x viewport (one matrix) and lighting. Through mode, sprites and clear mode keep the CPU transform. Uniforms are only written when they change. sdmc:/p3p3ds/cpu_vertices.txt forces the old CPU transform for comparison. Report: model_draws counters in the GPU stats.
  • [VERIFIED] CTest 27/27 on the PC (first-day replays unchanged except lit draws); shader assembles with picasso; 3DS build links. [UNVERIFIED] picture and speed on hardware; flat shading of transformed triangles is now smooth on the GPU path.
  • PC route to the first battle: maintainer save 4/9 (slot 1) -> LOAD GAME -> sign-in sheet -> "Go back to your room" (.tmp input scripts, not yet in profiles/p3p/input/).

Hardware run of build 06818ae (0.2.5): first battle reached, scene black (2026-10-03)

Maintainer, New 2DS XL, 602 s wall, quit with START+SELECT during the first battle.

  • [VERIFIED] The first Shadow no longer crashes: the battle starts and runs. The command-buffer fix (live fill from GPUCMD_GetBuffer) holds: 2,706 frame flushes, no panic.
  • [VERIFIED] Picture in battle (maintainer's description): everything is black except the protagonist's gun (Evoker) and the Shadow; the party members are black silhouettes; of Orpheus only his instrument (lyre) is visible. The scene reads as "a floating gun on a black background".
  • [VERIFIED] Speed in battle: 5-10 game frames/s. Report: last 10 s speed 21 %, split aot 28 %, hle 9 %, ge 49 %, present 0 %, idle 6 %; in GE texture hashing 0.6 %, conversion 0.1 %, GPU wait 3.7 %; ui 8.2 %. Run average 49 % of real time (aot 32, hle 11, ge 36, idle 11, ui 8.6). 5.9 M draws in 602 s (build 472599b: 4.7 M in 1,250 s), 23.6 M triangles, 294 textures cached, cpu->rtt 4.
  • [INFERRED] Black models: the vertex path (core/src/ge/geometry.cpp, decode_screen_vertices) implements neither lighting (GE enable 0x17) nor skinning (bone weights; the layout's weights field is decoded but positions are not blended with the bone matrices) nor morphing. A lit vertex without its own colour takes the material ambient colour (registers 0x55/0x58), which for lit materials is typically dark, and the texture is modulated by it: black. The gun and the Shadow are probably drawn unlit or with vertex colours. Skinned party models would additionally sit in bind/bone space. The first-day census counted 12,684 lit draws; battles light almost everything. To verify on the PC runner: census of a battle (lighting, skinning, morph counts) and frames of the software renderer, which shares this vertex path.
  • [INFERRED] GE at 49 % in battle is CPU vertex work: all transform (and later lighting/skinning) runs on the ARM11 per vertex; hashing, uploads and GPU wait are small. Moving transform, lighting and skinning to the PICA200 vertex shader is the matching fix.
  • [VERIFIED] ui 8 %: redrawing the bottom-screen report once per second (libctru console) plus the SD report every 5 s costs 8 % of the time, and the maintainer saw the bottom screen flicker. Now the report is drawn every 5 s together with the SD report, row by row in place (no screen clear, no scrolling). [UNVERIFIED] new ui share on hardware.

Hardware run of build 472599b: about 30 fps, crash at the first Shadow fixed (2026-10-03)

  • [VERIFIED] Maintainer, New 2DS XL: saves work; mostly a steady 30 game frames/s, outside battle sometimes down to 20, at the first Shadow down to 15. Report after 1,250 s: 88 % of real time; run average aot 44 %, hle 14 %, ge 21 % (texture hashing 3.3 %, conversion 0.6 %, GPU wait 7.4 %), present 0 %, idle (pacing sleep) 19 %, ui 2.2 %; 32,966 presents skipped as unchanged. Last 10 s before the crash (the Shadow): speed 33 %, ge 49 %.
  • [VERIFIED] Crash: Luma exception screen, svcBreak from GPUCMD_AddInternal (libctru gpu.c:33, command buffer full) called by C3D_DrawArrays (addresses resolved with arm-none-eabi-addr2line on the build's ELF). Cause: since unchanged presents are skipped, a frame stays open across vblanks, and the flush rule (every 1,500th draw counted since start) did not bound one frame. The first fix used C3D_GetCmdBufUsage() and crashed the same way on hardware (build 45b5ff0, same GPUCMD_AddInternal panic from C3D_DrawArrays): citro3d updates that value only when a frame is submitted (3ds/citro3d/source/base.c). Now the live fill from libctru GPUCMD_GetBuffer is checked before each draw (submit above 75 %). [UNVERIFIED] on hardware past the Shadow. Same build: 30 fps with drops to 25 in the field, 15 at the Shadow.
  • [INFERRED] The GE share at the Shadow is not hashing or texture conversion; the report now also counts CPU-written framebuffers re-uploaded to GPU targets (cpu->rtt) and frame flushes, to locate it.

Speed work 1: host costs outside the game code (2026-10-02)

Measured on the PC with a host build configured like the 3DS one (-DP3P_PRODUCTION_RUNTIME=ON, build directory build/prod, AOT flags of the 3DS build) and the new statistical profiler (--sample <file> [--sample-from <vblank>], report with tools/profile_symbols.py). Same scripted route (title to the faculty office, 89.4 M dispatches, no rasterization) before and after; the run ends at the same guest PC.

  • [VERIFIED] Host time of the route: 24.0 s before, 11.2 s after. Before, 29 % of the samples in gameplay were in the Windows heap (ntdll/ucrtbase): call sites of Runtime::event built a std::map and strings for every HLE call (hle_hit) even with diagnostics compiled out, and the GE census did a dozen string-keyed map updates and an snprintf per draw. Now event is an inline guard over a stack array, the census is off in the production runtime, sceSasCore mixing uses the inline memory accessors, the per-syscall deadline scan is skipped until the earliest deadline, and the GE command loop looks its list up once per list instead of once per command.
  • [VERIFIED] Memory leak: every display list ever enqueued stayed in GeManager::lists_ (17,084 lists on this route). Now the 64 most recent completed lists are kept. [INFERRED] over the 40-minute hardware session this was on the order of 15 MB of heap.
  • 3DS renderer: C3D_FrameEnd(0) flushed the data cache over the whole 12 MiB linear heap at each call (3ds/citro3d/source/renderqueue.c); now only this frame's vertices are flushed and the command list with GX_CMDLIST_FLUSH. A present that would show the same picture again is skipped (queued draws stay in the open frame). [UNVERIFIED] effect on hardware.
  • 3DS report: time split over the last 10 s and the run average (aot, hle, ge, present, idle = pacing sleep, ui), and in GE: texture hashing, texture conversion, GPU wait. Earlier reports counted pacing sleep and presentation inside "hle".

Maintainer session in Azahar: saves work, Dark Hour stop, speed (2026-10-02)

  • [VERIFIED] In-game saving works: two saves made at the dorm (slots DATA00 and DATA01, "4/7 (Tu) Evening Dorm" and "4/9 (Th) Evening Dorm"), each with P3PSAVE.BIN (88,500 bytes), PARAM.SFO, ICON0.PNG and PIC1.PNG; the maintainer reports saving and loading as working. The bottom-screen menu showed P3P's internal slot names (DATA0a, DATA10, ...) and mojibake: SAVEDATA_DETAIL is UTF-8 with the hero's name in full-width letters and a gender sign. Now: slots numbered 01, 02, ..., the detail line shown in the list, full-width forms converted to ASCII.
  • [VERIFIED] Stop at the first Shadow (battle transition): missing scePower::0xEBD177D6 (scePowerSetClockFrequency350, name from recomp/PSP-recompilation-project/src/rt/nid_names.h). Implemented with scePowerSetClockFrequency (0x737486F2): argument check of pspautotests power/freq.expected (below 19 MHz -> 0x800001FE), clock not modelled. [UNVERIFIED] past that point.
  • [VERIFIED] Speed in Azahar (build bfa6474, 1,398 s wall): name entry 74 % / 11.9 game fps, faculty office 66 % / 16.5 fps, Velvet Room 61 % / 11.9 fps, Dark Hour rooftop 13 vblanks/s / 6.5 fps at that moment; average 56 %. Time split HLE 60 % (GE rendering 16 %), AOT + dispatch 39 %; 19.6 M HLE calls (about 14,000/s). [INFERRED] the sceCtrl fix lets scenes that ran at 20 frames/s run at 30, i.e. up to 1.5x more work per guest second, so lower speed than the earlier hardware run (87 %, build f6c9611) is expected; Azahar is not a timing reference for hardware.
  • PC runner: --render-from <vblank> skips rasterization before that vblank (display lists still run): a run to the school took 176 s instead of about 540 s.

sceCtrl sampling time: menus at 30 fps (2026-10-02)

  • [VERIFIED] The main menu ran at 20 game frames/s in virtual time (15.8/s in Azahar): P3P's loop is WaitVblankStartMultiCB(2) then sceCtrlReadBufferPositive, and the read waited for the next vblank. Hardware samples a little after vblank start (references/pspautotests/tests/ctrl/vblank.expected: an interrupt handler still sees the previous sample, blocking reads return 10-15 lines into the frame); reads now wait for vblank + 700 us ([INFERRED] point in that window). Main menu: 30 frames/s virtual. The title now appears at vblank 6627 (110.5 s) instead of 7965 (133 s); title_start.txt and new_game.txt were retimed and NEW GAME reaches "Welcome to the world of P3P."; first_day.txt must be re-recorded. 27/27 CTest (test_input expectations follow the new sampling).

Real New 3DS, savedata, pacing (2026-10-02)

  • [VERIFIED] Maintainer's New 3DS (hardware), build f6c9611: 40 minutes of play (2,381 s guest in 2,734 s wall, 87 % of real time) from the title through NEW GAME into the Dark Hour; the APPLICATION region is 126,976 KiB, the heap 71,220 KiB with 60,284 KiB in use. No sound. Time split: AOT + dispatch 55 %, HLE 42 % (GE rendering 10 %). Stop at the first Shadow: missing sceGe_user::0xDC93CFEF (sceGeGetCmd, battle transition effect).
  • [VERIFIED] sceGeGetCmd / sceGeGetMtx from uOFW src/kd/ge/ge.c. [UNVERIFIED] on hardware past that point.
  • [VERIFIED] sceUtilitySavedata (core/src/hle/savedata.cpp): P3P's LOAD GAME uses mode 4 (LISTLOAD) with gameName ULUS10512, saveNameList DATA00.., P3PSAVE.BIN (0x159B4 bytes) and a key. Implemented: status sequence of the pspautotests transcripts, AUTO/LOAD/SAVE, LISTLOAD/LISTSAVE/LISTDELETE through a host dialog (3DS: bottom-screen menu; PC: --savedata latest|cancel|N), delete, SIZES; saves under ms0:/PSP/SAVEDATA/<game><slot>/ with the data file unencrypted plus PARAM.SFO and icons. PC: LOAD GAME with no saves returns to the main menu (result 0x80110307). tests/test_savedata.cpp covers a save/load round trip, cancel, no data, SIZES. [UNVERIFIED] an in-game save and load on the 3DS; saves are not exchangeable with PSP/PPSSPP (no encryption).
  • [VERIFIED] 3DS runner: real-time pacing (the guest clock no longer runs ahead: 60 vblanks/s in Azahar instead of up to 95); the fps line now counts game frames (framebuffer changes, 31.7/s on the title screen) next to vblanks/s.
  • Old 3DS: not feasible as designed: about 112 MB of application memory are needed (44 MB code, 32 MB PSP RAM), Old 3DS offers 64 MB (96 MB in its largest mode), and its CPU runs at 268 MHz without L2 cache.

P3P3DS Builder: ISO -> .3dsx on the user's PC (2026-10-02)

  • [VERIFIED] platform/pc/builder/ (P3P3DS-Builder.exe + sdk/): Windows wizard with progress, time estimate and Persona 3 trivia; decrypts EBOOT.BIN in C++ (same SHA-256 as tools/prepare_game.py), generates and compiles the 237 units with the user's devkitARM, links prebuilt runtime objects, packages the .3dsx, optionally copies it and the ISO to the SD card. Clean build from the release package: 263 s (12 jobs); the result reaches the title screen in Azahar. Release package: make_release.py, 2.3 MB zip without game data. Details: docs/3DS_PLATFORM.md 8.5-8.6. 26/26 CTest (new: p3p_psp_eboot).
  • [UNVERIFIED] The builder on a PC other than the maintainer's (devkitPro detection, older CPUs).

New 3DS: GPU renderer, title screen and main menu (2026-10-01)

  • [VERIFIED] PICA200 GE backend (platform/3ds/gpu_renderer.cpp, design and measurements in docs/3DS_PLATFORM.md 4.4). In Azahar the logos, the title screen and the main menu render correctly; logo positions are pixel-identical to the PC software frames. Speed 92 % of real time, 45 fps (was 6 %, 2 fps with CPU rendering). Frame dumps: sdmc:/p3p3ds/dump_every.txt.
  • [VERIFIED] Vertex/texel decoding moved to core/src/ge/geometry.cpp, shared by both renderers: PC first-day frames unchanged (7,199 frame hashes identical to the run before the move); tests/test_ge_geometry.cpp checks decode_texture against fetch_texel for all non-DXT formats. 25/25 CTest.
  • [VERIFIED] The opening movie (black: no decoder) is skipped with START (PC check); the 3DS runner presses START while a CPU-written picture is shown and movie frames are decoded. BlankMpegDecoder fills rows instead of 130k checked stores per picture.
  • [VERIFIED] Maintainer check in Azahar (2026-10-02): NEW GAME and the following scenes run and render without visible problems; no sound (no ndsp backend yet); loading a save crashes, also after starting a new game.
  • Blocker: LOAD GAME / saving call sceUtilitySavedataInitStart (0x50C4CD57), not implemented. [UNVERIFIED] hardware.

First New 3DS build boots in Azahar (2026-10-01)

  • [VERIFIED] platform/3ds/ builds p3p3ds.3dsx (devkitARM GCC 16.1). Core and runtime sources are shared with the host build through cmake/p3p3ds_sources.cmake; DisplayState::on_vblank is the presentation hook. ARM fixes: std::min/max/clamp with explicit types (uint32_t/int32_t are long on ARM newlib), including a one-line patch in recomp/PSPRecomp/include/psprecomp/allegrex_context.hpp.
  • [VERIFIED] In Azahar (New 3DS mode) the game runs after three memory fixes (heap split, code size, direct-table window): 314 vblanks in 111.9 s wall, about 5 % of real time. Details, numbers and what is not verified: docs/3DS_PLATFORM.md sections 8.3 and 8.5.
  • [VERIFIED] sceIoRead stages data in 256 KiB chunks instead of one host buffer of the requested size (P3P issues reads of tens of MiB).
  • [VERIFIED] Azahar screenshots by the maintainer: ATLUS and CRIWARE logos render correctly on the top screen; the opening movie is black (no video decoder, as on PC). Profile: software rendering 91 % of wall time, game code and dispatch a few percent (docs/3DS_PLATFORM.md 8.3).
  • [UNVERIFIED] Real hardware (New 2DS XL): memory available to a .3dsx and speed. Not done: PICA200 renderer, ndsp audio, .cia.

First game day with scripted input; profile and GE census (2026-10-01)

  • [VERIFIED] With vblank-keyed input scripts the game runs from NEW GAME through the protagonist selection, the Dark Hour dorm lobby (3D), the game's own name-entry keyboard, Mitsuru's introduction, the protagonist's room (the navigation cursor follows DOWN/RIGHT/LEFT input: .tmp/room3/ frames 13420 vs 13590), going to bed, the next morning with Yukari, and Gekkoukan High's "1st Floor, Main Lobby" with 3D students. Runs of 1,270 s and 2,291 s virtual time (26,318 / 46,731 frames) ended only at the dispatch budget: no missing service, no deadlock, audio produced throughout (0 late, 0 clipped). The repeated-message loops seen in long runs ("You are tired today...", "You should go to the Faculty Office.") are the game answering scripted CROSS taps on an exit, not hangs: every frame differs (cursor animation) and moving the cursor away works. Checked visually; key frames copied to .tmp/showcase/ (not committed).
  • [UNVERIFIED] profiles/p3p/input/first_day.txt: the lobby-movement tail after the added dismiss tap (vblank 44450) was not re-run; the steps before it were verified in the exploration runs above.
  • [VERIFIED] --profile (HLE timer: minimal patch in recomp/PSPRecomp/src/runtime.cpp; interpreter/renderer timers in core): 20M dispatches to the title menu take 46.4 s wall; HLE 70.4 % (6.1 M calls, includes GE rendering started from list submission and the scheduler hook), software rendering 37.6 % (15,411 draws), interpreter 0.000 s (13 entries); AOT code + dispatch is the remainder (~30 %). Numbers in docs/3DS_PLATFORM.md section 8.4.
  • [VERIFIED] GE feature census (ge_feature_summary event, GeManager::count_features): up to the title menu all 15,411 draws are through-mode; 9,497 of 9,550 textured draws ask for linear filtering (renderer: nearest only), 53 line-strip draws are not rasterized, textures are CLUT4/CLUT8 (+53 RGBA8888). The 3D sections were run before the census existed; their feature mix is not yet measured.
  • [VERIFIED] Replay with title_start.txt: two runs byte-identical, SHA-256 69B5F7B197972B92A142437A71248A7A5F66CCC51067F430A3924CFDAB57E09A (changed from 5D5DDB35... only by the new ge_feature_summary event). 24/24 CTest.
  • [VERIFIED] Maintainer check (2026-10-01): the first-day screens match the original game without graphical artifacts, and the first-day audio (--wav) is the original music and sound, without artifacts. No emulator reference captures are planned.

Immediate blocker for live play: no window/live input (SDL3 absent). Next: a renderer pass driven by the census of the 3D sections (linear filtering, line primitives, lighting/skinning if used).

pspautotests CPU/FPU/VFPU against hardware (2026-10-01)

  • [VERIFIED] platform/pc/autotest.cpp (p3p_autotest <test.prx>): loads a pspautotests program through the real sceKernelLoadModule/sceKernelStartModule HLE path, runs it on the interpreter fallback, captures its output (harness-only emulator:/kemulator: devctl devices from references/pspautotests/common/common.c) and diffs it with the hardware .expected. SysMem is configured as a full 24 MiB user partition for the test ([INFERRED] 0x08800000-0x0A000000; newlib requests a 0x1500000-byte heap). The interpreter shares its instruction helpers with the AOT generator, so mismatches here are semantics both paths share — exactly what the AOT-vs-interpreter differential cannot see. Test PRXs run interpreted only; no AOT units are generated for them.
  • [VERIFIED] tools/run_autotests.py + tests/autotest_baseline.txt (CTest p3p_autotest_baseline, fails on any regression): 6/48 programs match hardware exactly (cpu_alu, cpu_branch2, lsu, matrix, minmax_tie, systemparam). Known gaps, now measurable: FPU rounding/NaN/branch-hazard details (fpu 3/2026 lines, rounding 18/336, roundmode 63/308 — P3P never executes ctc1/cfc1); VFPU transcendental last bits (vrot 356/409 lines differ only by a few ULP of sin/cos; no bit-exact algorithm in psp/vfpu-docs, PPSSPP's may not be copied); VFPU prefix saturation/consume rules (prefix_sat 174/196, prefix_consume); unlowered encodings used only by tests (0xD480000C, vfpu4 group 0xD0438480, ll/sc, cfc1 $0). Static use in P3P (experiments/p3p-analysis/mnemonic_histogram.cpp): vrot 44, vpfxs/vpfxt 120 each, vpfxd 35, vsin/vcos/vasin 41, trunc.w.s 4,255, cvt.s.w 6,081, no vavg/ll/sc/ctc1.
  • [VERIFIED] sceUtilityGetSystemParamInt/String (core/src/hle/utility.cpp): 23/23 lines of utility/systemparam.expected; values live in KernelState::system_params() (default: the reference console, English).

New Game start (2026-10-01)

  • [VERIFIED] profiles/p3p/input/new_game.txt (START at vblank 7965, UP at 8300, CROSS at 8400; CROSS confirms in this release): the game checks title/sex_select.bin and scheduler/scheduler_04.bf through the mod chain and shows the protagonist selection scene with the dialog "> Welcome to the world of P3P." (frames 3810-6941, .tmp/ng1/), then waits for input. 40M dispatches, 300 s virtual, no blocker.
  • Also: LICENSE (MIT) added; .agents/skills/ mirrors .claude/skills/ (tools/sync_agent_skills.py, CTest p3p_skills_mirror); CLAUDE.md/AGENTS.md section 9 no longer names data.cpk, and both gained section 18 (project skills). 23/23 CTest.

Host input and leaving the title screen (2026-10-01)

  • [VERIFIED] core/include/p3p3ds/input.hpp / core/src/input.cpp: InputSource sampled by sceCtrl with the virtual vblank count (HostInput::refresh), and InputScript, a vblank-keyed button/stick script (<vblank> <BUTTONS> [hold], <vblank> stick x y). Button bits from psp/pspsdk/src/ctrl/pspctrl.h = references/uofw/include/ctrl.h. Runner: --input <file>; --gamepad polls XInput pad 0 live (platform/pc/host_input.hpp, mapping A/B/X/Y = Cross/Circle/Square/Triangle) — live polling is [UNVERIFIED]: no pad was attached and the run is not paced to wall time. Keyboard: a default key map exists as data only (no window to receive keys). ctrl_buttons events trace each change.
  • [VERIFIED] tests/test_input.cpp: parser/semantics, XInput mapping, and sceCtrlReadBufferPositive returning scripted buttons at vblanks 1-4.
  • [VERIFIED] profiles/p3p/input/title_start.txt (7965 START 6): the game leaves "PRESS ANY BUTTON" and shows the main menu NEW GAME / LOAD GAME / CONFIG / DATA INSTALL (frame 3690, cursor on LOAD GAME), then without further input returns to the title (frame 4140). Checked visually in .tmp/in1/. Without a script the run is unchanged (SHA-256 399C7DBC...27D4). With the script, two 20M-dispatch replays are byte-identical: events 5D5DDB353F63CDF34899BE4DDE272B8493B6507064A1F5380E17D72D4FFFC54C, WAV 3E4904A5...2430. 22/22 CTest.
  • [UNVERIFIED] No window: the vendored SDL3 (references/static-recomp/rexglue-sdk/thirdparty/sdl3) is an empty submodule and recomp/PSP-recompilation-project/SDL3.dll has no headers.

AOT without transfer-record diagnostics (2026-10-01)

  • [VERIFIED] psp_recomp --no-transfer-records / CMake -DP3P_AOT_TRANSFER_RECORDS=OFF omit the per-branch rt.record_transfer calls. PC host: text 81.54 to 73.68 MB, clean build 642 to 583 s, 20M dispatches 43.9 to 36.3 s, with identical frame hashes, WAV and stop PC (table: docs/3DS_PLATFORM.md section 8.2). Default build unchanged (replay SHA-256 399C7DBC...27D4 as before). 21/21 CTest.
  • [UNVERIFIED] devkitARM is not installed, so the 3DS harness and ARM11 size measurements were not done (section 8.3).

PCM output on PC (2026-10-01)

  • [VERIFIED] platform/pc/pcm_mixer.hpp: every accepted sceAudio buffer (AudioState PcmSink, volume-scaled) is mixed at its virtual start time into a 44.1 kHz stereo S16 timeline; --wav <file> streams it to a WAV. tests/test_pcm_mixer.cpp checks placement, overlap sum/clamp, late buffers and the WAV header.
  • [VERIFIED] 20M-dispatch run: 52,428 buffers, 146 s of audio, 0 late, 0 clipped, peak 7,523. Silent through the logos and the movie (ATRAC3plus has no decoder yet), then non-silent from ~115 s (title screen): RMS 260-1,180 per second, first-difference/signal energy 0.012 (low-frequency content, not noise). The PCM is produced by the game's own CRI code; the maintainer listened to the first-day recording and confirmed it (see above). Two replays: event dump and WAV byte-identical (WAV SHA-256 74A7CC1EBFA509472B8A2A4A4C6F896CB64E0C0946906B785E207D5BF8CA2E4F, events 399C7DBC1288CDB560987DBAE8DEF9B2C55F177FC685055D2F3740B218C627D4). 21/21 CTest.
  • [UNVERIFIED] No live audio device yet (offline WAV only).

Opening movie, title screen and attract loop (2026-10-01)

  • [VERIFIED] core/src/hle/mpeg.cpp: all 25 imported sceMpeg services. PSMF program-stream demultiplexer: AVC AUs split at H.264 access-unit delimiters, ATRAC3plus AUs = one 752-byte frame (header 0F D0), PES PTS/DTS attached only when a PES payload starts at the AU, AVC attribute = nal_ref_idc of the AU's slice, ringbuffer packets freed once every registered stream consumed them. tests/test_mpeg.cpp replays pspautotests video/mpeg/basic.c on test.pmf: 2,905/2,905 lines of basic.expected identical (180 frames: available sizes, callback offsets/counts, put results, AU sizes/timestamps/attributes, decode results), plus the ringbuffer/{memsize,construct,avail,destruct}.expected tables. Decoding is behind MpegDecoder; the default outputs black pictures and silence [UNVERIFIED substitute] (plan: docs/NEXT_STEPS.md).
  • [VERIFIED] ThreadManager::call_guest: an HLE service can call a guest function on the calling thread (return sentinel 0x2C) and continue when it returns; the guest function may block. sceMpegRingbufferPut uses it for the read callback (which calls sceIoRead), as on hardware.
  • [VERIFIED] Also added: sceUmdGetDriveStat (0x32 when activated, umd/wait/wait.expected; other states [INFERRED]), sceKernelPowerTick (uOFW sysmem/suspend.c), sceDmacMemcpy/TryMemcpy (error codes from dmac/dmactest.expected; synchronous copy [INFERRED]); tests/test_misc_hle.cpp. GE finish callbacks (interrupt context, cannot block) run to completion with a 1,000,000-dispatch runaway cap instead of 32.
  • [VERIFIED] The game now plays its 100 s opening movie (P3OPMV_P3P.pmsf, 2,407 ringbuffer puts, black frames), then shows the title screen ("PRESS ANY BUTTON", frame 3600 at virtual 132.9 s), and without input loops through its attract sequence (logos, movie, title) indefinitely: a 200M-dispatch run reached 38,314 frames (virtual 24 min, 7 min wall) with no blocker. Checked visually from frame dumps.
  • [VERIFIED] 20/20 CTest, --verify-bootstrap PASS. Two --run-until-blocker --max-dispatches 20000000 --ms0 .tmp/ms0 replays byte-identical, SHA-256 D3FFC30303EC0C2CB739E4C89AA721CF55B37ABD539CED423DEAE51BDAF6898F; the title frame dump is bit-identical across three runs.

Immediate blocker: none in the attract loop. Next: host controller input (to leave the title screen), host PCM output, a PC window.

GE display-list execution and first visible frames (2026-10-01)

  • [VERIFIED] core/src/hle/ge.cpp executes GE lists (JUMP/BJUMP/CALL/RET, SIGNAL 1-3, FINISH/END, BASE/OFFSET/ORIGIN, bone/world/view/proj/texgen matrices, LOADCLUT, TRANSFERSTART, PRIM) and hands the decoded register file to a ge::GeRenderer (core/include/p3p3ds/ge/renderer.hpp). ge::SoftwareRenderer (core/src/ge/software_renderer.cpp) is the target-agnostic reference backend: it rasterizes into guest EDRAM (points/lines are not drawn; sprites, triangles, strips, fans; through and transform mode; 16/32-bit and CLUT4/8/16/32 textures, swizzle; texture functions; alpha/depth tests; blending; clear mode; masks; block transfer). DXT textures render magenta, BEZIER/SPLINE stop explicitly. Rasterization rules are [INFERRED] (not compared to hardware captures).
  • [VERIFIED] tests/test_ge_renderer.cpp (sprite coverage + scissor, linear vs swizzled CLUT4 sampled exactly, 50% alpha blend, clear mode, block transfer). CPU writes to render targets no longer halt the run (only --stop-on-any-vram-write).
  • [VERIFIED] The runner hashes every displayed framebuffer (frame event, FNV-1a, non-black pixel count) and writes BMPs with --frames-dir <dir> [--frame-every N] (platform/pc/frame_dump.hpp). In one run (virtual 12.6 s, 227 frames, ~5 s wall) the game shows a loading indicator, the ATLUS logo (frames ~70-140) and the CRIWARE logo (frames ~150-220); checked visually from .tmp/frames. No PPSSPP screenshot comparison was possible offline.
  • [VERIFIED] 18/18 CTest, --verify-bootstrap PASS. Two --run-until-blocker --max-dispatches 400000000 --ms0 .tmp/ms0 replays byte-identical, SHA-256 F983FD7AF8B7B12463A266C311AD2E84042EDAE6CF1254F853CFCD56C3F52B3D; identical frame hash sequences.
  • [UNVERIFIED] No host window yet: SDL3 is not available offline (vendored SDL3 tree empty), so presentation is frame dumps only.

Immediate blocker: missing sceMpeg::0x682A619B (sceMpegInit) — the opening movie (item 5).

SAS, controller, cache/time services and remaining VFPU (2026-10-01)

  • [VERIFIED] core/src/hle/sascore.cpp: all 27 sceSasCore imports — VAG ADPCM / PCM / noise voices, pitch, volumes, ADSR state machine (32-sample key-on delay, linear/bent/exponential/direct curves), pause/end flags, grain mixing into stereo S16 (__sceSasCore, WithMix). Reverb parameters are stored but not mixed [UNVERIFIED]. tests/test_sascore.cpp replays the call sequences of pspautotests/tests/audio/sascore/adsrcurve.cpp and matches 53/53 hardware envelope traces in adsrcurve.expected exactly, plus init/volume/pitch/PCM/VAG/ADSR-mode/key-on/off checks from the other .expected files. Envelope-to-amplitude scaling is [INFERRED]. P3P initializes SAS with grain 256, 32 voices, stereo.
  • [VERIFIED] UtilsForUser cache maintenance is a real no-op on coherent host memory (same policy as CACHE in codegen); libc clock/time/gettimeofday use the virtual clock with an [INFERRED] fixed 2008-01-01 epoch. sceCtrl follows references/uofw/src/kd/ctrl/ctrl.c (sampling mode returns the previous mode, idle threshold range, Read* waits for the next vblank sample, Peek* immediate) with a host input state (no buttons, centred sticks by default).
  • [VERIFIED] VFPU: vcrs.t, vi2uc, vbfy1, vsgn, vsocp lowered per psp/vfpu-docs/inst-vfpu-desc.yaml; verify_p3p_decoder: 912,808 lowerable, 0 decoded-but-unlowered, 251 BREAK.
  • [VERIFIED] 17/17 CTest; two replays byte-identical, SHA-256 3E279BA9F8328472AC1B1D7E04B89FFF1DDBB463305EAF40CFD5F2EF47D79223. New threads sas thread (32) and SDK WRAP (33) start.

Immediate blocker: GE display-list command the setup-only GE model rejects (GE unsupported command, list near 0x08D647C4) — real GE execution (item 6).

Game PRX modules (2026-10-01)

  • [VERIFIED] USRDIR/module/libsuppreacc.prx is an unencrypted ELF PRX (scesupPreAcc_library, 8 exports, imports IoFileMgr/ThreadMan/sceSuspend); libfont.prx/libccc.prx carry ~SCE headers. core/src/hle/modulemgr.cpp really loads unencrypted PRX code (SysMem block, relocation at the block base), serves its imports through dynamic HLE wrappers, binds the EBOOT's imports of its libraries to its exports (7 bound stubs) and runs it via the interpreter fallback. Start follows uOFW _StartModule: module_start in a SceModmgrStart thread with the module's $gp, caller waits for its end; RESIDENT returns the id, NO_RESIDENT unloads; an entry-less module (this one: e_entry 0xFFFFFFFF, no module_start export) starts with status 0. libfont.prx/libccc.prx are accepted as HLE modules by file name [INFERRED]; any other encrypted module stops explicitly.
  • [VERIFIED] test_modulemgr (real UMD; skips without it) and 16/16 CTest. Two replays byte-identical, SHA-256 723B58A2EBAF760FEA218CC8E63085E5D94BEBE34A91EDEDD91A59E53443029F. The loaded module's code executed (370 interpreted instructions).

Immediate blocker: missing sceSasCore::__sceSasInit (0x42778A9F), caller 0x08B699C8.

IoFileMgr, VFS and mod chain (2026-10-01)

  • [VERIFIED] core/src/vfs.cpp: ISO9660 backend (UMD image read in place, case-insensitive) and host-directory backend. core/src/hle/iofilemgr.cpp implements the 20 imported IoFileMgrForUser services plus StdioForUser after references/uofw/src/kd/iofilemgr/iofilemgr.c: one pending async op per fd, PollAsync=1 while pending, WaitAsync[CB] blocks (retry at the stub) and writes the 64-bit result, ASYNC_BUSY/NO_ASYNC_OP, 64-bit Lseek in $a2:$a3/$t0, Getstat, Dopen/Dread/Dclose. Synchronous calls hold the caller for [INFERRED] UMD time (200 us + bytes/2 us) on the virtual clock. The runner mounts disc0: from --umd (default: the single *.iso in the working directory) and ms0: from --ms0 (default out/ms0).
  • [VERIFIED] ULUS-10512 has no data.cpk: PSP_GAME/USRDIR holds umd0.cpk (5,860 files), umd1.cpk (338), MEMST.CPK, module/{libfont,libsuppreacc,libccc}.prx and sound/pmsf/*.pmsf. The CLAUDE.md section 9 wording "data.cpk" is [WRONG] for this release; the chain applies to the archives the game binds.
  • [VERIFIED] On PSP the community chain is the CWCheat "Mod Support" code in p3p/p3p-patches/ULUS10512.ini (115 constant writes: strings plus a code cave at 0x08806DF0-0x08806FFC and redirected calls): ms0:/PSP/P3P/bind/ > mod.cpk > mod1.cpk..mod3.cpk > original. profiles/p3p/config/cwcheat_patches.txt selects it; tools/cwcheat_patches.py turns it into a patch list that psp_recomp --auto applies after relocation before analysis/codegen and emits as apply_generated_patches(), which the runner and the differential test call, so AOT code and the guest image agree. --mods <dir> aliases ms0:/PSP/P3P to the mods directory (3DS target: sdmc:/p3p3ds/mods/). Mods remain data.
  • [VERIFIED] Observed: the patched game opens ms0:/PSP/P3P/mod.cpk, mod1-3.cpk (absent: 0x80010002), then disc0:/PSP_GAME/USRDIR/umd0.cpk and umd1.cpk, and reads each CPK header+TOC. With CriFsV2Lib's SampleData.cpk (test asset, not game data) as mod.cpk the game binds and reads it first. experiments/cpk-check (CriFsV2Lib sources compiled offline against the local .NET 10 pack) classifies all 6 traced reads (--io-trace) as CPK metadata of mod.cpk, umd0.cpk (header+TOC end exactly at first file data 0x3A000) and umd1.cpk; 0 unclassified. No file-data read happened before the next blocker, so byte comparison of file contents is not yet exercised. Loose bind/ files were not exercised.
  • [VERIFIED] test_iofilemgr (synthetic ISO + host dirs) covers sync/async semantics, errors, stat/dir listing and alias precedence. 15/15 CTest; two replays byte-identical, SHA-256 CF6B477539A67E8CE724752552BF59D4688A4FEC3C4BA03C1A1C922A5E61EF43. Diagnostics now cap high-frequency event types per type (Runtime::event_type_limits, counts in the dump) instead of halting at 50,000 events.

Immediate blocker: missing ModuleMgrForUser::sceKernelLoadModule (0x977DE386), caller 0x08807EB8 (loading USRDIR/module/*.prx).

sceAudio channel contracts (2026-10-01)

  • [VERIFIED] All 12 imported sceAudio services follow references/uofw/src/kd/audio/audio.c / include/audio.h: reserve/release/data length/volume/config/rest length, Output/OutputPanned (non-blocking, OUTPUT_BUSY) and their blocking forms (retry at the stub), plus Output2* as the SRC channel. Accepted buffers go to an AudioState::PcmSink as volume-scaled stereo S16 at 44.1 kHz; no host device is attached yet. rest_length and SRC blocking are [INFERRED] timing approximations.
  • [VERIFIED] 14/14 CTest (audio contract cases in p3p_threadman_sync). Two replays byte-identical, SHA-256 14DCF9E7234900BA85349CB11F3FDC38324F85C9F26BDDC24148A65C5552EDA9. Next blocker: missing IoFileMgrForUser::sceIoOpen, caller 0x08B40CE8.

ThreadMan with waits and virtual time (2026-10-01)

  • [VERIFIED] core/src/hle/threadman.cpp + threadman_hle.cpp replace the DIRTY_FIRST_FRAME stubs (LwMutex lock/unlock/try, DelayThread, GetSystemTimeLow, event flags, CpuSuspend/ResumeIntr) with kernel objects and wait queues: threads (create/start/exit/delete/wait-end/sleep/wakeup/suspend/resume/priority/refer), semaphores, event flags, mutexes, lwmutexes (user workarea per references/uofw/src/kd/usersystemlib/lwmutex.c), callbacks with real guest delivery (sceKernelCheckCallback, *CB waits, return sentinel 0x24). uOFW threadman is unreversed, so error codes/edge cases follow references/pspautotests/tests/threads/**/*.expected; PPSSPP only cross-checked.
  • [VERIFIED] Scheduling: strict priority, FIFO within a level, immediate preemption when a waker readies a higher-priority thread. Virtual clock: +1 us per HLE call (kSyscallCostUs, [INFERRED] policy), idle jumps to the earliest deadline, deadlock (all threads waiting without a deadline) stops with a named list. VBlank waits use a 16,683 us period on that clock; sceAudioOutputBlocking follows uOFW's one-queued-buffer model and retries at the stub when the slot frees; sceUmdActivate notifies the registered UMD callback (hardware: pspautotests/tests/umd/callbacks/umd.expected; argument 0x32 is [INFERRED]).
  • [VERIFIED] test_threadman_sync (CTest p3p_threadman_sync) covers each primitive's errors, blocking/wake/preemption, timeouts (zero and elapsed, remaining written back), delete-wakes, lwmutex handover and numWaitThreads, recursion/overflow, sleep/wakeup counting, wait-end status, callback frames (check + CB wait, non-zero return deletes), deadlock detection and blocking audio retry.
  • [VERIFIED] 14/14 CTest, --verify-bootstrap PASS. Two --run-until-blocker --max-dispatches 5000000 replays are byte-identical, SHA-256 BA99ED613474E6633FA5DB90D04C1C17BDF34A1FDF3D5C56EF816FF84D2D97DD. The game now passes the old audio blocker, delivers the UMD callback, and initializes CRI middleware: threads SceWaveMain (7), CriThread x3 (10/14/17), CRI ADX Audio (28), CRI ADX File (29), CRI Wave out (30). UIDs moved because semaphores/flags now take real UIDs.
  • [INFERRED] Thread stacks still bump down from 0x09FF0000 outside SysMem (which grows up from 0x08ED0000); they can only collide if both regions meet. [WRONG] as a safety statement: nothing checks that they meet (code review 2026-10-08, top of this file).

Immediate blocker: missing sceAudio::0x95FD0C2D (sceAudioChangeChannelConfig), caller 0x08B64F90, user_main UID 2.

Interpreter fallback (2026-10-01)

  • [VERIFIED] core/src/interpreter.cpp executes any guest PC without a registered AOT entry. Instruction semantics mirror recomp/PSPRecomp/tools/codegen_main.cpp (same helpers, same link/delay-slot/condition order). Runtime gained a fallback hook (set_runtime_fallback_hook): with it installed the outer loop uses exact per-PC lookup, and a unit entered at a non-entry PC returns to the dispatcher (Runtime::unregistered_entry) instead of stopping. The first entry at each PC is logged as interpreter_enter; --no-interpreter restores the old stop.
  • [VERIFIED] test_interpreter_diff (CTest p3p_interpreter_differential): from identical CPU/RAM/VRAM state, AOT(+fallback) and pure interpretation must agree on all GPR/HI/LO/FPR/FCR31/VFPU state, RAM, VRAM, last transfer and stop class after N recorded transfers (Runtime::transfer_budget). Default: 400 fully-lowerable analyzer functions x 3 register seeds (every odd seed starts at an interior unregistered PC) = 1,200 cases, 0 failures. Extended run: 500 functions x 4 seeds, budget 1,000: 2,000 cases, 69,093 transfers, 1,016 fallback entries, 0 failures.
  • [VERIFIED] Real-execution check: a MANUAL build with only the 114 original p3p_functions.csv groups and no frontier seeds stops at 0x08B1C594 with --no-interpreter, but with the interpreter reaches the same sceAudio::0x136CAF51 blocker as AUTO (39 interpreter entries, 17 PCs, 2,534 instructions). Its 206 semantic events (all HLE calls, arguments, results) are identical to the AUTO run.
  • [VERIFIED] That comparison exposed a codegen bug, now fixed in codegen_main.cpp: a fixed transfer to an import stub inside the same CFG/unit became a goto into the stub's jr ra (P3P's tail call j sceKernelCpuResumeIntr at 0x08B73E24), silently skipping the HLE. Import checks now precede local labels, MANUAL mode gets the stub set, and AUTO no longer emits the 221 stub bodies (751,453 PCs / 170,803 entries).
  • [VERIFIED] 13/13 CTest, --verify-bootstrap PASS, chase_frontier.py --check-only --output .tmp/interp-check PASS. AUTO replays are byte-identical, SHA-256 65D6315C6BDC88B922B1388511AFB6B2441B9EB3F2DDB5488429FC5D3DB4A767; blocker unchanged (missing sceAudioOutputBlocking, UID 5).
  • [UNVERIFIED] pspautotests CPU/FPU PRX were not run: their harness needs newlib stdio over sceIo/host0:, semaphores and thread creation. Planned once ThreadMan and IoFileMgr exist.

Whole-.text AOT generation (2026-10-01)

Default build is now P3P_AOT_MODE=AUTO: psp_recomp --auto lowers the whole analyzer CFG instead of the 21-seed manifest. MANUAL remains for the legacy frontier tooling.

  • [VERIFIED] psp_recomp profiles/p3p/game/eboot.elf --auto <dir> 0x08804000 16384: 21,965 function seeds (same list as p3p_report_functions_auto.csv), 751,674 emitted instruction PCs, 171,024 block entries, 237 units x 16 KiB + registry, 123 MB of C++, generated in ~40 s. Layout is pinned in profiles/p3p/config/aot_layout.cmake and checked by tools/verify_aot_layout.py.
  • [VERIFIED] Clean p3p_aot build, GCC 16.1 -O2, -j10, i5-12400F / 16 GB: 780 s wall, 4.8 GiB peak RSS (all compilers). Largest unit (generated_unit_0049): 21.3 s / 570 MiB at -O2, 13.6 s at -O1. Objects: 238 files, 101.6 MiB; p3p_pc_bootstrap.exe 79.8 MiB (77.4 MiB text). Incremental regeneration only recompiles changed units (Ninja restat).
  • [VERIFIED] Minimal recomp/PSPRecomp fixes required for volume (each commented P3P3DS): codegen_main.cpp in-place text.replace() lowering was O(n^2); a JAL to an import stub re-emitted forever (continue without advancing pc); direct unit chaining named non-existent units for targets without a block entry (data decoded as code, e.g. 0x08B80BE0 -> 0x0C382C34). allegrex_context.hpp _ct scalar helpers now mirror the runtime helpers for indices >= 144 (P3P uses mfvc $zero,255 = 0x486000FF).
  • [VERIFIED] 12/12 CTest, --verify-bootstrap PASS, py -3 -B tools/chase_frontier.py --check-only --output .tmp/auto-check PASS (independent regeneration byte-identical for all units). Two --run-until-blocker replays are byte-identical, SHA-256 77F38CDCED51D38E1130CEDEC011B7647CDD17B694143716DC2686A41F5DDF05; blocker unchanged: missing sceAudio::0x136CAF51 from 0x08B650F8, UID 5. 171,026 registered entries.
  • [INFERRED] Manual seed 0x08B13400 (p3p_functions.csv) is an interior loop PC, not a function entry: it is covered as an instruction but not a registered block entry in AUTO mode. Such PCs are the interpreter fallback's job.

Repository hygiene (2026-09-30)

  • [VERIFIED] build/generated/p3p_generated.cpp (derived from the proprietary EBOOT) is no longer tracked; CMake regenerates it (CMakeLists.txt P3P_GENERATED_CPP). It remains in history since 62edefb; purging requires a maintainer-run git filter-repo, not done here.
  • [VERIFIED] logs/p3p_bootstrap_latest.log and profiles/p3p/config/frontier_checkpoint.json contain only addresses/counters/hashes, no game bytes.
  • [VERIFIED] at the time: no top-level LICENSE existed and recomp/PSP-recompilation-project (with font/jpn0.pgf, a PSP firmware font, and SDL3.dll) was tracked. Both changed later: LICENSE (MIT) was added in 55c4f31 and the research trees were untracked in b322cfd. p3p_generated.cpp (62edefb..f9c9216), jpn0.pgf and SDL3.dll remain in the pushed history; removing them needs a maintainer-run history rewrite (agents may not, CLAUDE.md section 17). logs/ is ignored, so the log cited above is a local file.

Older entries

The frontier-chasing log of September 2026 (UMD, audio and thread-entry blockers before whole-.text AOT, the frontier checker workflow) was removed on 2026-10-06; it is in the git history of this file.