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.
- [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 0says nothing about them. - New diagnostic: an empty
sdmc:/p3p3ds/gpu_trace.txtwrites 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) tosdmc:/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_TexBindagain.present()draws throughblit(), 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_), soC3D_TexBindwas 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.cppblit):bound_tex_ = nullptrafter 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.
- [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.0between stalls. On the maintainer's console the same build showsui 0.7-0.8.uicovers only the report every 5 s: building the text, the bottom screen and rewritingsdmc:/p3p3ds/report.txt(profile_seconds.txtabsent). [INFERRED] On that SD card onefopen/fcloserewrite 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.cpptarget_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:
.3dsxbuilds, 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.
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
.3dsxunder 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 (
wait0.0-4.3 % of the time) and the worker is busy 56-65 %: the battle is now bound by the game's own code (aot63-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 withsdmc:/p3p3ds/no_gpu_thread.txtto confirm it is the worker, then a frame log of the first 300 vblanks. - Released as 0.3.0.
- [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 %,ge33-36 %,prep17-20 %. [UNVERIFIED] core 2 on hardware. - [VERIFIED] Overlap available (PC production build, battle window vblank 22165-32165, new
--profilecounters): the guest time between asceGeListEnQueueand the game's nextsceGeDrawSyncis 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:AsyncRendererwraps aGeRenderer. The GE command processor stays on the game's thread (list parsing is 1-2 %; signals, callbacks andsceGeGetCmdneed its state) and queues a snapshot of the GE registers and matrices whenstate_versionchanged, 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. NewGeRenderer::drain(), called byGeManager::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 reportedcore2 okandsdmc:/p3p3ds/no_gpu_thread.txtis 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 beforeaptMainLoopand at exit). Report:cores : ... GE on core2|core0andgpu thd: busy B wait W(worker busy share; the game's thread waiting for it, counted inhle);draw:andvtx:are then the worker's steps. - [VERIFIED] PC runner
--ge-thread(software renderer behindAsyncRendereron astd::thread, drained after every list):new_game.txt60M 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-bootstrapPASS,.3dsxbuilds. [UNVERIFIED] speed and stability on the 3DS (Azahar and hardware).
- [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.
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
sceGeListEnQueueminusge(that difference is mostly run filling insync()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 inrecomp/PSPRecomp/tools/codegen_main.cpp; chained calls still pass the shared view). 3DS, the Builder's compile flags and the PC production build also definePSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH(thePSPRECOMP_WATCH_WRITEdebug watch reads an environment variable, always empty on the 3DS; VRAM stores keep their observer on the slow path). - [VERIFIED] 3DS ELF
.text44,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.txt60M replay unchanged (events055626D9..., WAV22532254...),--verify-bootstrapPASS, CTest 27/27 (psprecomp_testsexpects the new parameter name). [UNVERIFIED] speed on the 3DS.
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.
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 %);
sceGeListEnQueue25.7 ms per frame in the battle (was 32.4 ms),vtx9.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.hlerose from 11 to 20 % because filling a run now happens insync()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): onesceGeListEnQueuecosts 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 finedraw: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) andModelRun.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 (newGeRenderer::sync(), called at the end ofGeManager::pumpand 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::continueschecks end address, room, vertex type and material.
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
vtxfell 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 addressesva=/ia=). The unpack of so few vertices was never the main cost; per-draw work is. [WRONG] the earlier reading "26.5 % = vertex unpack" (vtxholds 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), thensceKernelGetSystemTimeLow817/s,sceAudioOutputPannedBlocking689/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):sceGeListEnQueue10.1 % of host time (91 us per call, one per frame: GE list execution, the renderer's own share 3.9 %),sceAudioOutputPannedBlocking5.1 %, Suspend/ResumeIntr 3.6 % together (0.05 us each),__sceSasCore1.0 %. On the 3DS the GE list command processing counts ashle, notge(geis the renderer only). [UNVERIFIED] the same split on the 3DS:sdmc:/p3p3ds/profile_seconds.txtwrites the top three imports per second toprofile_seconds.csv. - PC runner:
--stop-vblank <n>ends a run at a vblank (a sampled scene without the rest of the route);--profilewith--sample-fromcovers only the sampled window and lists the 12 imports with the most time. - Replay
new_game.txt60M dispatches unchanged (events055626D9..., WAV22532254...); CTest 27/27.
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.cppdraw_model: a hit is onememcpyof the shader vertices into the frame's vertex arena instead ofdecode_model_verticesplus 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 ofvtx; H: hits over the last 10 s); SD report linevcache(KiB, sources, hits, misses, volatile, full). An emptysdmc:/p3p3ds/no_vertex_cache.txtturns 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 type0x00015Aequal 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.
The first two defects of the code review below.
- [VERIFIED]
AllegrexContextnow starts with source/target prefixes0xE4(identity) and destination prefix 0 (minimal patch inrecomp/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,minmaxnow exact;exact,minmax_zero,overlapcloser), 0 worse;tests/autotest_baseline.txtupdated.test_interpreter_diffno longer sets0xE4by hand. - [VERIFIED] The runtime records the thread-switch generation at the entry of each import call (
runtime_import_entry_generation, minimal patch inrecomp/PSPRecomp/src/runtime.cpp, nested calls restore the outer value).ThreadManager::resume_pcuses$raonly 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-runssceAudioOutputBlocking(and the other retry waits) instead of continuing at$rawith a stale$v0. - Tests:
test_threadman_syncretry_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) andnew_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-bootstrapPASS. - [VERIFIED] Replay
new_game.txt, 60M dispatches, two runs with the fix: events SHA-256055626D9CA87DD748A479CCEC81D610A4AF8B003835BE2D1480E0843362EB19C, WAV22532254F1E331E3C606702FFDFBC18AF1757168F456CD7A7C0BBE33663C54C1, 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.
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.cppstart_threadassignsAllegrexContext{}, whosevfpu_ctrlis 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 reads1 1 1 1instead of1 2 3 4). The value on hardware is [INFERRED] from PPSSPP (sceKernelThread.cpp, thread context reset: S/T prefix0xE4, CC0x3F, fcr310xE00).test_interpreter_diffsets0xE4by 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_pctreats "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 insceAudioOutputBlockingresumes at$rawith a stale$v0when a priority-32 thread'ssceKernelDelayThread(0)expires in the same post-import hook. AffectssceAudioOutputBlocking/OutputPannedBlocking,sceIoWaitAsync(stale result, op left pending),sceCtrlRead*, module start waits. Must be fixed before NDSP audio. - [INFERRED] Back-face culling: since
72bd103through 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
0x09FF0000with no check against the SysMem heap growing up to0x09FF0000(repro: heap block08ED0000..09FF0000and a new stack09FEC000..09FF0000overlap); stacks are never reclaimed (255 create/delete cycles of 64 KiB stacks fail with0x80020190);sceKernelStartModuleleaks a 256 KiB stack;free_partition_memorynever reuses space;Addrallocations do not check overlap;Highis 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]
sceIoOpenAsyncreturns open errors synchronously; uOFW reports them through the async result (iofilemgr.c,open_iob). - [VERIFIED] GE: finish callbacks run inside
sceGeListEnQueue/UpdateStallAddrinstead of later; SIGNAL handlers are never called;EnQueueHeadruns the list at once. - [VERIFIED] FPU/VFPU:
vsin/vcos/vrotuse the hostsinf/cosf(vcos(1.0)= -4.37e-8 instead of 0);sqrt.s/div.sNaN sign follows the host; the 3DS runs with flush-to-zero and default NaN (libctrusyscalls.c), the PC does not, so PC runs are not a bit-exact reference for denormals and NaN;divuby zero gives LO0xFFFFFFFFwhere hardware gives0x0000FFFFfor dividends up to0xFFFF(the 4cpu_divlines in the autotest baseline);madd/msubaccumulate in signed 64-bit (undefined on overflow; GCC wraps in practice);round.w.srounds half away from zero (P3P has none);vdotzero sign differs between interpreter and AOT. - [VERIFIED]
sceUtilitySavedatabuilds host paths from guest strings without checks (..,/, empty names); delete modes callremove_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(see3DS_PLATFORM.mdsection 3; the old claim aboutAPT_SetAppCpuTimeLimitis [WRONG]). - [VERIFIED] The profiling timers are always on (
platform/3ds/main.cpp): twosteady_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
lwis 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_WATCHby 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; skinned0x014342: 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 thevtxbucket on hardware first. - [INFERRED]
Runtime::functions_holds about 170,000 entries, each with a 16-characterstd::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.ymlpublishes packages without running CTest and does not depend onci.yml. - [VERIFIED] Without
references/pspautotestsor the game,test_sascore(0 of 53 curves),test_hle_graphicsandtest_psp_ebootskip their main checks but report Passed;test_mpegreturns 77 before its reference-free ring-buffer tables run. CI's "3 skipped" hides 3 partial skips. - [VERIFIED]
tools/run_autotests.pybaseline: 10 entries already have mismatches equal to their expected line count and cannot get worse; a change of stop reason is not detected;--updaterecords regressions. - [VERIFIED]
verify_p3p_decoderandp3p_analysis_comparisoncompare against the implementation's own earlier output (change detectors, not correctness checks).replay_check.sh(skillp3p-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 inplatform/pc/builder/pipeline.cpp; mod prefixms0:/PSP/P3Pin both runners. - [VERIFIED]
recomp/PSPRecomp/profiles/vcs/**(227 files, FFmpeg headers) andprofiles/p3p/config/frontier_checkpoint.jsonare not used by the build. - Running CTest from Git Bash picks MSYS
/usr/bin/ctest, which mangles the paths (all BAD_COMMAND); use theCMAKE_CTEST_COMMANDfrombuild/CMakeCache.txt(27/27 pass that way).
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.
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.
- 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 testtest_ge_renderercase 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.
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_vertexper 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.txtreaches 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.txtforces the CPU transform (no skinning/lighting on that path on 3DS).
-
[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,FileVersion0.2.6) andP3P3DS-Builder-0.2.6-linux.tar.gz(2,916,300 bytes);sdk/VERSION0.2.6 2e053cdin both. -
Both CMake projects build without the game (
P3P_HAVE_GAMEoff whenprofiles/p3p/game/eboot.elfis 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 libraryp3p3ds_objects. -
Two tests depended on local state and now do not:
test_hle_graphicsskips its reset-list block without the ELF;test_chase_frontier.pycreates.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 apatches.txtidentical to the game build's. With the game: host CTest 27/27,.3dsxlinks. -
.github/workflows/ci.yml(push to master, pull requests) andrelease.yml(tagv*-> 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 (
FileVersion0.2.5), Linux tar.gz 2.92 MB with staticP3P3DS-Builderandpsp_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.3dsxon hardware; the Linux package on a real Linux machine with devkitPro.
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%withApp: 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.txtswitch ->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:
sceIoGetstatat 83 ms per call (48 calls, 4,007 ms in the slow seconds; 2 calls in 123 steady seconds). PC runnernew_game.txtevents: every getstat isms0:/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 inHostFileSystem::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 intests/test_iofilemgr.cppcompare 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 inpsprecomp::runtime_profile()(per_import), top 3 per second in the CSV. - [VERIFIED] CTest 27/27 (2 skipped as before).
Tooling only; the runtime frontier is unchanged.
platform/pc/builder/: the pipeline is portable (posix_spawnon POSIX,CreateProcessW+ job object on Windows; arguments as a list instead of a quoted command line;.exesuffix only on Windows; devkitPro from$DEVKITPROor/opt/devkitpro; job count fromMemAvailable). On LinuxP3P3DS-Builderis a console program with the--clioptions; the Win32 wizard is unchanged.make_release.pypackages the host platform (-linux.tar.gzon 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 realpsp_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.exelinks; imports only system DLLs;-Wall -Wextrawarning counts unchanged (pipeline 0, wizard 11 pre-existing). - [UNVERIFIED] a real Linux build with devkitPro to a
.3dsx; the Windows.exewas not run; cancellation by Ctrl+C on Linux.
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 PSPSDKguInternal.h/sceGuLight*.c; behaviour cross-checked against PPSSPPGPU/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.txtforces the old CPU transform for comparison. Report:model_drawscounters 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" (
.tmpinput scripts, not yet inprofiles/p3p/input/).
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'sweightsfield 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.
- [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,
svcBreakfromGPUCMD_AddInternal(libctru gpu.c:33, command buffer full) called byC3D_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 usedC3D_GetCmdBufUsage()and crashed the same way on hardware (build 45b5ff0, sameGPUCMD_AddInternalpanic fromC3D_DrawArrays): citro3d updates that value only when a frame is submitted (3ds/citro3d/source/base.c). Now the live fill from libctruGPUCMD_GetBufferis 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.
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::eventbuilt astd::mapand strings for every HLE call (hle_hit) even with diagnostics compiled out, and the GE census did a dozen string-keyed map updates and ansnprintfper draw. Noweventis an inline guard over a stack array, the census is off in the production runtime,sceSasCoremixing 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 withGX_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".
- [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 withscePowerSetClockFrequency(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.
- [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.txtandnew_game.txtwere retimed and NEW GAME reaches "Welcome to the world of P3P.";first_day.txtmust be re-recorded. 27/27 CTest (test_inputexpectations follow the new sampling).
- [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/sceGeGetMtxfrom uOFWsrc/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 underms0:/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.cppcovers 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.
- [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 astools/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.md8.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).
- [VERIFIED] PICA200 GE backend (
platform/3ds/gpu_renderer.cpp, design and measurements indocs/3DS_PLATFORM.md4.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.cppchecksdecode_textureagainstfetch_texelfor 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.
BlankMpegDecoderfills 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.
- [VERIFIED]
platform/3ds/buildsp3p3ds.3dsx(devkitARM GCC 16.1). Core and runtime sources are shared with the host build throughcmake/p3p3ds_sources.cmake;DisplayState::on_vblankis the presentation hook. ARM fixes:std::min/max/clampwith explicit types (uint32_t/int32_tarelongon ARM newlib), including a one-line patch inrecomp/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.mdsections 8.3 and 8.5. - [VERIFIED]
sceIoReadstages 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.md8.3). - [UNVERIFIED] Real hardware (New 2DS XL): memory available to a
.3dsxand speed. Not done: PICA200 renderer, ndsp audio,.cia.
- [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 inrecomp/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 indocs/3DS_PLATFORM.mdsection 8.4. - [VERIFIED] GE feature census (
ge_feature_summaryevent,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-25669B5F7B197972B92A142437A71248A7A5F66CCC51067F430A3924CFDAB57E09A(changed from5D5DDB35...only by the newge_feature_summaryevent). 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).
- [VERIFIED]
platform/pc/autotest.cpp(p3p_autotest <test.prx>): loads a pspautotests program through the realsceKernelLoadModule/sceKernelStartModuleHLE path, runs it on the interpreter fallback, captures its output (harness-onlyemulator:/kemulator:devctl devices fromreferences/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(CTestp3p_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 (fpu3/2026 lines,rounding18/336,roundmode63/308 — P3P never executesctc1/cfc1); VFPU transcendental last bits (vrot356/409 lines differ only by a few ULP of sin/cos; no bit-exact algorithm inpsp/vfpu-docs, PPSSPP's may not be copied); VFPU prefix saturation/consume rules (prefix_sat174/196,prefix_consume); unlowered encodings used only by tests (0xD480000C,vfpu4group0xD0438480,ll/sc,cfc1 $0). Static use in P3P (experiments/p3p-analysis/mnemonic_histogram.cpp):vrot44,vpfxs/vpfxt120 each,vpfxd35,vsin/vcos/vasin41,trunc.w.s4,255,cvt.s.w6,081, novavg/ll/sc/ctc1. - [VERIFIED]
sceUtilityGetSystemParamInt/String(core/src/hle/utility.cpp): 23/23 lines ofutility/systemparam.expected; values live inKernelState::system_params()(default: the reference console, English).
- [VERIFIED]
profiles/p3p/input/new_game.txt(START at vblank 7965, UP at 8300, CROSS at 8400; CROSS confirms in this release): the game checkstitle/sex_select.binandscheduler/scheduler_04.bfthrough 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, CTestp3p_skills_mirror); CLAUDE.md/AGENTS.md section 9 no longer namesdata.cpk, and both gained section 18 (project skills). 23/23 CTest.
- [VERIFIED]
core/include/p3p3ds/input.hpp/core/src/input.cpp:InputSourcesampled by sceCtrl with the virtual vblank count (HostInput::refresh), andInputScript, a vblank-keyed button/stick script (<vblank> <BUTTONS> [hold],<vblank> stick x y). Button bits frompsp/pspsdk/src/ctrl/pspctrl.h=references/uofw/include/ctrl.h. Runner:--input <file>;--gamepadpolls 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_buttonsevents trace each change. - [VERIFIED]
tests/test_input.cpp: parser/semantics, XInput mapping, andsceCtrlReadBufferPositivereturning 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-256399C7DBC...27D4). With the script, two 20M-dispatch replays are byte-identical: events5D5DDB353F63CDF34899BE4DDE272B8493B6507064A1F5380E17D72D4FFFC54C, WAV3E4904A5...2430. 22/22 CTest. - [UNVERIFIED] No window: the vendored SDL3 (
references/static-recomp/rexglue-sdk/thirdparty/sdl3) is an empty submodule andrecomp/PSP-recompilation-project/SDL3.dllhas no headers.
- [VERIFIED]
psp_recomp --no-transfer-records/ CMake-DP3P_AOT_TRANSFER_RECORDS=OFFomit the per-branchrt.record_transfercalls. 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.mdsection 8.2). Default build unchanged (replay SHA-256399C7DBC...27D4as before). 21/21 CTest. - [UNVERIFIED] devkitARM is not installed, so the 3DS harness and ARM11 size measurements were not done (section 8.3).
- [VERIFIED]
platform/pc/pcm_mixer.hpp: every acceptedsceAudiobuffer (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.cppchecks 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, events399C7DBC1288CDB560987DBAE8DEF9B2C55F177FC685055D2F3740B218C627D4). 21/21 CTest. - [UNVERIFIED] No live audio device yet (offline WAV only).
- [VERIFIED]
core/src/hle/mpeg.cpp: all 25 importedsceMpegservices. PSMF program-stream demultiplexer: AVC AUs split at H.264 access-unit delimiters, ATRAC3plus AUs = one 752-byte frame (header0F D0), PES PTS/DTS attached only when a PES payload starts at the AU, AVC attribute =nal_ref_idcof the AU's slice, ringbuffer packets freed once every registered stream consumed them.tests/test_mpeg.cppreplays pspautotestsvideo/mpeg/basic.contest.pmf: 2,905/2,905 lines ofbasic.expectedidentical (180 frames: available sizes, callback offsets/counts, put results, AU sizes/timestamps/attributes, decode results), plus theringbuffer/{memsize,construct,avail,destruct}.expectedtables. Decoding is behindMpegDecoder; 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 sentinel0x2C) and continue when it returns; the guest function may block.sceMpegRingbufferPutuses it for the read callback (which callssceIoRead), as on hardware. - [VERIFIED] Also added:
sceUmdGetDriveStat(0x32when activated,umd/wait/wait.expected; other states [INFERRED]),sceKernelPowerTick(uOFWsysmem/suspend.c),sceDmacMemcpy/TryMemcpy(error codes fromdmac/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-bootstrapPASS. Two--run-until-blocker --max-dispatches 20000000 --ms0 .tmp/ms0replays byte-identical, SHA-256D3FFC30303EC0C2CB739E4C89AA721CF55B37ABD539CED423DEAE51BDAF6898F; 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.
- [VERIFIED]
core/src/hle/ge.cppexecutes 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 age::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 (
frameevent, 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-bootstrapPASS. Two--run-until-blocker --max-dispatches 400000000 --ms0 .tmp/ms0replays byte-identical, SHA-256F983FD7AF8B7B12463A266C311AD2E84042EDAE6CF1254F853CFCD56C3F52B3D; identical frame hash sequences. - [UNVERIFIED] No host window yet: SDL3 is not available offline (vendored
SDL3tree empty), so presentation is frame dumps only.
Immediate blocker: missing sceMpeg::0x682A619B (sceMpegInit) — the opening movie (item 5).
- [VERIFIED]
core/src/hle/sascore.cpp: all 27sceSasCoreimports — 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.cppreplays the call sequences ofpspautotests/tests/audio/sascore/adsrcurve.cppand matches 53/53 hardware envelope traces inadsrcurve.expectedexactly, plus init/volume/pitch/PCM/VAG/ADSR-mode/key-on/off checks from the other.expectedfiles. Envelope-to-amplitude scaling is [INFERRED]. P3P initializes SAS with grain 256, 32 voices, stereo. - [VERIFIED]
UtilsForUsercache maintenance is a real no-op on coherent host memory (same policy asCACHEin codegen); libc clock/time/gettimeofday use the virtual clock with an [INFERRED] fixed 2008-01-01 epoch.sceCtrlfollowsreferences/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,vsocplowered perpsp/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 threadssas thread(32) andSDK 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).
- [VERIFIED]
USRDIR/module/libsuppreacc.prxis an unencrypted ELF PRX (scesupPreAcc_library, 8 exports, imports IoFileMgr/ThreadMan/sceSuspend);libfont.prx/libccc.prxcarry~SCEheaders.core/src/hle/modulemgr.cppreally 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 aSceModmgrStartthread with the module's$gp, caller waits for its end; RESIDENT returns the id, NO_RESIDENT unloads; an entry-less module (this one:e_entry0xFFFFFFFF, no module_start export) starts with status 0.libfont.prx/libccc.prxare 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-256723B58A2EBAF760FEA218CC8E63085E5D94BEBE34A91EDEDD91A59E53443029F. The loaded module's code executed (370 interpreted instructions).
Immediate blocker: missing sceSasCore::__sceSasInit (0x42778A9F), caller 0x08B699C8.
- [VERIFIED]
core/src/vfs.cpp: ISO9660 backend (UMD image read in place, case-insensitive) and host-directory backend.core/src/hle/iofilemgr.cppimplements the 20 importedIoFileMgrForUserservices plusStdioForUserafterreferences/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-bitLseekin$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 mountsdisc0:from--umd(default: the single*.isoin the working directory) andms0:from--ms0(defaultout/ms0). - [VERIFIED] ULUS-10512 has no
data.cpk:PSP_GAME/USRDIRholdsumd0.cpk(5,860 files),umd1.cpk(338),MEMST.CPK,module/{libfont,libsuppreacc,libccc}.prxandsound/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 at0x08806DF0-0x08806FFCand redirected calls):ms0:/PSP/P3P/bind/>mod.cpk>mod1.cpk..mod3.cpk> original.profiles/p3p/config/cwcheat_patches.txtselects it;tools/cwcheat_patches.pyturns it into a patch list thatpsp_recomp --autoapplies after relocation before analysis/codegen and emits asapply_generated_patches(), which the runner and the differential test call, so AOT code and the guest image agree.--mods <dir>aliasesms0:/PSP/P3Pto 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), thendisc0:/PSP_GAME/USRDIR/umd0.cpkandumd1.cpk, and reads each CPK header+TOC. With CriFsV2Lib'sSampleData.cpk(test asset, not game data) asmod.cpkthe 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 ofmod.cpk,umd0.cpk(header+TOC end exactly at first file data0x3A000) andumd1.cpk; 0 unclassified. No file-data read happened before the next blocker, so byte comparison of file contents is not yet exercised. Loosebind/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-256CF6B477539A67E8CE724752552BF59D4688A4FEC3C4BA03C1A1C922A5E61EF43. 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).
- [VERIFIED] All 12 imported
sceAudioservices followreferences/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), plusOutput2*as the SRC channel. Accepted buffers go to anAudioState::PcmSinkas volume-scaled stereo S16 at 44.1 kHz; no host device is attached yet.rest_lengthand SRC blocking are [INFERRED] timing approximations. - [VERIFIED] 14/14 CTest (audio contract cases in
p3p_threadman_sync). Two replays byte-identical, SHA-25614DCF9E7234900BA85349CB11F3FDC38324F85C9F26BDDC24148A65C5552EDA9. Next blocker: missingIoFileMgrForUser::sceIoOpen, caller0x08B40CE8.
- [VERIFIED]
core/src/hle/threadman.cpp+threadman_hle.cppreplace 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 perreferences/uofw/src/kd/usersystemlib/lwmutex.c), callbacks with real guest delivery (sceKernelCheckCallback, *CB waits, return sentinel0x24). uOFW threadman is unreversed, so error codes/edge cases followreferences/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;sceAudioOutputBlockingfollows uOFW's one-queued-buffer model and retries at the stub when the slot frees;sceUmdActivatenotifies the registered UMD callback (hardware:pspautotests/tests/umd/callbacks/umd.expected; argument0x32is [INFERRED]). - [VERIFIED]
test_threadman_sync(CTestp3p_threadman_sync) covers each primitive's errors, blocking/wake/preemption, timeouts (zero and elapsed, remaining written back), delete-wakes, lwmutex handover andnumWaitThreads, 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-bootstrapPASS. Two--run-until-blocker --max-dispatches 5000000replays are byte-identical, SHA-256BA99ED613474E6633FA5DB90D04C1C17BDF34A1FDF3D5C56EF816FF84D2D97DD. The game now passes the old audio blocker, delivers the UMD callback, and initializes CRI middleware: threadsSceWaveMain(7),CriThreadx3 (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
0x09FF0000outside SysMem (which grows up from0x08ED0000); 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.
- [VERIFIED]
core/src/interpreter.cppexecutes any guest PC without a registered AOT entry. Instruction semantics mirrorrecomp/PSPRecomp/tools/codegen_main.cpp(same helpers, same link/delay-slot/condition order).Runtimegained 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 asinterpreter_enter;--no-interpreterrestores the old stop. - [VERIFIED]
test_interpreter_diff(CTestp3p_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.csvgroups and no frontier seeds stops at0x08B1C594with--no-interpreter, but with the interpreter reaches the samesceAudio::0x136CAF51blocker 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 agotointo the stub'sjr ra(P3P's tail callj sceKernelCpuResumeIntrat0x08B73E24), 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-bootstrapPASS,chase_frontier.py --check-only --output .tmp/interp-checkPASS. AUTO replays are byte-identical, SHA-25665D6315C6BDC88B922B1388511AFB6B2441B9EB3F2DDB5488429FC5D3DB4A767; blocker unchanged (missingsceAudioOutputBlocking, 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.
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 asp3p_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 inprofiles/p3p/config/aot_layout.cmakeand checked bytools/verify_aot_layout.py. - [VERIFIED] Clean
p3p_aotbuild, 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.exe79.8 MiB (77.4 MiB text). Incremental regeneration only recompiles changed units (Ninja restat). - [VERIFIED] Minimal
recomp/PSPRecompfixes required for volume (each commentedP3P3DS):codegen_main.cppin-placetext.replace()lowering was O(n^2); aJALto an import stub re-emitted forever (continuewithout advancingpc); direct unit chaining named non-existent units for targets without a block entry (data decoded as code, e.g.0x08B80BE0 -> 0x0C382C34).allegrex_context.hpp_ctscalar helpers now mirror the runtime helpers for indices >= 144 (P3P usesmfvc $zero,255=0x486000FF). - [VERIFIED] 12/12 CTest,
--verify-bootstrapPASS,py -3 -B tools/chase_frontier.py --check-only --output .tmp/auto-checkPASS (independent regeneration byte-identical for all units). Two--run-until-blockerreplays are byte-identical, SHA-25677F38CDCED51D38E1130CEDEC011B7647CDD17B694143716DC2686A41F5DDF05; blocker unchanged: missingsceAudio::0x136CAF51from0x08B650F8, 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.
- [VERIFIED]
build/generated/p3p_generated.cpp(derived from the proprietary EBOOT) is no longer tracked; CMake regenerates it (CMakeLists.txtP3P_GENERATED_CPP). It remains in history since62edefb; purging requires a maintainer-rungit filter-repo, not done here. - [VERIFIED]
logs/p3p_bootstrap_latest.logandprofiles/p3p/config/frontier_checkpoint.jsoncontain only addresses/counters/hashes, no game bytes. - [VERIFIED] at the time: no top-level LICENSE existed and
recomp/PSP-recompilation-project(withfont/jpn0.pgf, a PSP firmware font, andSDL3.dll) was tracked. Both changed later: LICENSE (MIT) was added in55c4f31and the research trees were untracked inb322cfd.p3p_generated.cpp(62edefb..f9c9216),jpn0.pgfandSDL3.dllremain 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.
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.