Running a homebrew and seeing what it prints is the most basic thing
PPSSPPHeadless does, but sceIoWrite() to fd 1 and 2 only ever went into
the log, so it took -l to see any of it - which turns on every log
channel at debug level and buries the output.
The debug-output listener now gets a channel, and headless writes StdOut
and StdErr straight through to the host's, unmodified. With no listener
(the normal app) the old sanitized Log::Printf line is unchanged.
pspautotests writes exclusively to the "emulator:" devctl channel, so
nothing there moves; --compare and --bench suppress the forwarding along
with the debug channel, keeping test console output as it was.
Also fixes a potential one-byte OOB read in the same path when an
unmapped address clamps validSize to 0 with size > 0.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The CSC writes RGB straight into the display buffer, which the hardware
backends can't see on their own - our sceMpegAvcCsc HLE calls
PerformWriteFormattedFromMemory for exactly this reason, and the mpegbase
path didn't. Daxter's intro decoded normally with the screen frozen on the
menu behind it. Invisible under --graphics=software, which reads that memory
directly.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
An allocator that nothing has Init'd yet is a real state, not a broken one -
sceVideocodec keeps one for Media Engine memory that stays empty until a game
plays a video - but DoState asserted on bottom_ when writing, and on reading
treated a block count of zero as corrupt. Both ends handle it now, and the
write loop no longer special-cases the first block.
The stream layout is unchanged, so states written before this still load: they
always had at least one block, and take the same path they always did.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Deleting a savestate from the savedata screen goes through GameInfo::Delete,
not SaveState::DeleteSlot, and it only knew about the .jpg - so the slot's
.name.txt was left behind with nothing to belong to.
Rather than teach the UI the naming scheme a third time, SaveState now answers
what sits beside a state.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
GetSlotCustomName hit the disk for every slot each time the pause screen built
its views, almost always for a file that isn't there. Rescan's listing already
knows, and HasSaveInSlot - which gates whether the name is even shown - reads
the same map, so this can't hide a name the old code would have found.
SetSlotCustomName now rescans, so a rename is visible without depending on the
pause screen happening to rescan on its way back.
Also: clearing a name deletes the file instead of leaving an empty one behind,
and the extension is "name.txt" rather than plain "txt", so an unrelated
"<prefix>_<slot>.txt" in the savestate folder isn't read as a slot name.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The name and value columns were hardcoded at x=17 and x=77. The control has
a fixed width in the dialog and can not be widened, so the 8 hex digits of a
GPR only just fit - and once the register list got a vertical scrollbar, the
~17px it takes off the client width pushed the last digits off the edge.
Derive the value column from the client width each paint instead, pulling it
in far enough for a whole value to fit and clipping anything longer rather
than letting it spill. Same for the category tab labels.
Float registers print with %g rather than %f: in a column that narrow, a
clipped %f of a large value is worse than useless (1e20 would read as
100000000), while %g keeps six significant digits and stays short for the
values you normally see.
imgui 1.92 hands the backend a list of dirty rectangles when its font atlas
grows, but thin3d could only replace a whole mip level, so every new glyph
re-uploaded the entire atlas.
Adds DrawContext::UpdateTextureRegions, taking a batch of regions so each
backend can submit them together:
- Vulkan: packs the regions into the push pool and issues a single
vkCmdCopyBufferToImage from the init command buffer, transitioning only
the level being written.
- OpenGL: new TEXTURE_SUBIMAGE init step. The existing sub-image path is a
render command needing an active render pass and a texture slot, which
doesn't fit here. Rows are packed caller-side since GLES2 lacks
GL_UNPACK_ROW_LENGTH.
- D3D11: UpdateSubresource with a box, no staging texture needed.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
From 947aa9c97 to 98a66d8c8 on the docking branch - the latest release there,
plus the viewport fixes that landed on top of it. The only local changes to
upstream files are the "#undef new" hack at the top of imgui.h and enabling
IMGUI_DISABLE_OBSOLETE_FUNCTIONS in imconfig.h; both re-applied.
1.92 reworked fonts and textures, so the thin3d backend now advertises
ImGuiBackendFlags_RendererHasTextures and creates/updates/destroys imgui's
textures on request instead of building the font atlas itself. ImTextureIDs
below 256 now index those, above it the per-frame temp textures as before.
thin3d can't replace part of a texture yet, so an update re-uploads the whole
thing - fine for an atlas that only changes when a new glyph appears.
Also: AddRect() swapped its thickness and flags parameters in 1.92.8.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Four places were each computing the same sizes and 64-byte-aligned offsets:
the allocation, the recovery sceMpegbase uses, and the readers on both sides.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The output pixel mode decides both the colour packing and the bytes per pixel of
the conversion, and a game sets it once per movie rather than per frame - so a
state resumed mid-movie converted at the default until the next
sceMpegBaseCscInit, which may never come. The gathered PES payloads go in too,
since a state can land between the copy and the decode that consumes it.
The section is optional (minimum version 0), so states written before it still
load.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Same shape as the audio table: one row per open context, with its EDRAM block
and frame buffer allocation. Both are in ME memory, so the addresses shown are
in that space, not in PSP RAM.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
It used to return the path unchanged when the path didn't end in oldExtension,
so a caller that guessed wrong silently went on using the original file - and
"the screenshot next to this savestate" quietly becomes "this savestate".
Every caller had to know to check the extension first, and most didn't.
Now it's [[nodiscard]] bool with an out-param, in the style of ComputePathTo
next door, so the mismatch has to be handled. Changing the signature rather
than the behaviour means no call site can keep the old assumption by accident.
All four callers wanted "skip it" or "fall back", which they now say out loud.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Doing this was written once, for sceMp4, remembering the two UIDs it had
loaded in a static. That static was the only thing stopping a second copy
being loaded on top of the first, and it didn't survive a savestate: resuming
in a fresh process left it at zero while the restored module list already had
the modules in it. Ask the module list instead, which needs no state of its
own and is right after a savestate load and across games.
With the mechanism shared, sceMp3 and sceAtrac get it too. libmp3.prx and
libatrac3plus.prx each import nothing but the kernel and sceAudiocodec, so
both run against what we already have. The sceAtrac checkbox previously led
to an error log and a debug assert saying it wouldn't work, with a note that
we could go and find the file - which is what this does.
This is only ever the path for a game that ships no copy of the library. One
that does loads it directly and never asks sceUtility for it.
Also:
- a module we're really loading no longer reserves its stand-in block as
well. That block only stands in for memory we aren't otherwise taking, so
reserving both charges the game twice, at the top of user memory where
thread stacks come from.
- the module loads at the bottom of the user partition. fromTop is for a
module injected before the game's executable is placed; by the time a game
calls sceUtility nothing moves either way, and the firmware's utility.prx
allocates AV module memory as PSP_SMEM_Low.
- NotifyLoadStatusAtrac read the raw setting rather than the effective
flags, so it could fire for a flag that wasn't in effect.
- the missing-firmware case now says so on screen, naming the library and
pointing at installing a firmware, rather than only logging.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Headless force-enabled HLE for everything the caller didn't name, so a run never
exercised the graduated modules the way the app does. That is right for
pspautotests, which is homebrew PRXes shipping none of the user libraries a
retail disc carries - scePsmfPlayer and friends would have nothing real to run.
It is wrong for a disc, which brings its own copies. Decide from the resolved
boot list: a test batch, or a --vsh run with no file at all, keeps the homebrew
treatment; exactly one non-executable target is a disc and is left alone.
--firmware-from-disc installs the firmware most discs carry into a scratch NAND
beside the memstick and boots against it, which is the version that game shipped
with. Kept per disc so a second run reuses it. It goes after the point where the
NAND root is settled, since that assignment is unconditional and would otherwise
overwrite it - which it silently did until the sceFont check started depending
on the answer.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A survey of disc images found these shipped on a lot of them: LIBDEFLT and
LIBMT19937, LIBADLER, LIBMD5 on about 1/6, LIBSHA256 on 1/7, LIBHEAP on
1/12, LIBSFMT19937 on 1/24. None appears in any firmware dump and none is loadable
through sceUtility, so a game that imports one has to carry it - which makes
running the game's own module the safe default, the same argument psmfplayer
graduated on. Read off real discs with --re-module disc0:/..., all but sceHeap
import nothing at all, and sceHeap only Kernel_Library and ThreadManForUser.
sceFont joins them with a condition. Its module is on the disc like the others -
the most-shipped of all, on a third of the discs - but it reads its fonts from
flash0:/font and has nothing to fall back on, so it is only usable with a
firmware dump installed (our own fallback fonts are just not good enough anyway, it
chokes on them). CheckDisableHLEAvailability puts the HLE back when
those fonts are missing, since ours does have a fallback.
sceParseUri and sceParseHttp look like candidates and aren't: they are in the
firmware and sceUtility can load them, so a game may import them without
shipping one. Their headers say so.
The HLE implementations stay - old savestates index these tables by syscall
opcode - and are marked legacy the way scePsmf is.
Also: Move HLEInit after the filesystems are mounted
It ran before MountFileSystems, so the checks in it deciding whether a library
can run its real module instead of our HLE couldn't ask the PSP filesystem and
had to reach for host paths under the NAND directory. Nothing between
CoreTiming::Init and here touches HLE, and the kernel and the game's modules are
both loaded later, so it can simply move.
Two things fall out. The checks now spell their paths flash0:/... like the rest
of the emulator. And they run after AutoInstallFirmwareFromDisc, so a disc that
carries a firmware counts on the launch that installs it rather than the next
one - verified with an empty NAND and a disc carrying 3.11: the fonts land and
sceFont runs the game's own module the same boot.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The interesting copy of a library is often the one a game ships rather than the
firmware's - and until now getting at it meant extracting the file by hand.
Naming it disc0:/PSP_GAME/USRDIR/MODULES/LIBDEFLT.PRX with the disc as the
positional argument mounts the disc and loads from there, alongside the existing
host paths and flash0: ones. No new option.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
A re-read of the whole thing after the reservation bug, looking for anything else
it got wrong.
The SRC channel's buffer count is read straight out of the state and then used to
index a two-element array, so a corrupt one is a write outside the struct. It is
range-checked now, the same way the channel count above it already was.
A channel holding a buffer with no samples left of it was stuck: the mixer skipped
it without ever clearing the address, so it read as busy for ever and the game had
no way back to sound. The API cannot produce that - a channel is never reserved
for zero samples - but a savestate can claim it, so the mixer now retires such a
channel instead of stepping over it.
Also took the "get rid of this next time we bump" the version-2 resampler section
came with, since this branch is that bump. Nothing was ever in it.
Checked by loading states written by a released build for a game that uses the
mixer channels and one that uses Output2, plus a round trip of the new format.
Every section ends in a marker, so a conversion path that consumed the wrong
number of bytes would fail the load rather than quietly corrupt what follows.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Loading a state written by a released build left every channel unreserved, so
the game's next output came back SCE_ERROR_AUDIO_CHANNEL_NOT_INIT and the audio
never recovered.
The old format's reserved flag, sample count, volumes and format are read at the
top of AudioChannel::DoState and are perfectly good. Only the play position
cannot be reconstructed. The conversion path used clear() to zero the fields that
had no equivalent, which threw the rest away with them - my own cleanup two
commits ago swapped explicit field assignments for that call and did not notice.
The SRC channel had the same problem from the other end: older states carry it as
a ninth entry in the channel array, and that record was read into a throwaway.
What reserve agreed on carries over fine, so it now does.
Both halves were checked by loading a state written by a master build, with and
without the fix. Newer states are untouched and still round-trip.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Another pass looking for gaps, all of it now recorded by audio/blocking/channels
and audio/blocking/oneshot.
sceAudioOneshotOutput was the last unimplemented entry in the module, returning
"library not linked" to anyone who called it. It plays one buffer on a channel it
never reserves, so the channel frees itself when the buffer runs out, and its
argument checks are their own set: any positive sample count, aligned or not, no
upper bound, a negative volume rejected rather than skipped, and no busy check at
all. No game is known to use it; it is implemented because tracing it turned out
to be cheap, not because anything needed it.
The channels test confirms four behaviors: sceAudioChReserve(-1) skips a released
channel that is still playing while a reserve of it succeeds, a second channel
joining a running mixer doesn't lose a block unlike a first, mono counts down in
the same 64-sample steps over the same time as stereo, and the panned blocking
output has no extra delay on the high channels.
Also: the SRC resampler now interpolates into the next buffer at a buffer join
instead of holding the last sample, since the codec reads the two descriptors as
one stream. That only shows up at non-native rates and no test can see it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Two things the last pass looked at and left, now measured on hardware by the new
audio/blocking/vaudio and audio/blocking/overhead.
sceVaudioChRelease is not shaped like the Output2 and SRC releases. It hands the
channel a null pointer first, which waits for a buffer to finish, so it blocks for
one buffer and returns 0 where the others would refuse - and the buffer is played
out instead of being dropped, which is what we were doing. The reservation goes
now rather than when the drain finishes: the caller is parked either way and gets
the same answer at the same time, and the buffers keep playing because the mixer
does not look at the reservation.
The same test turned up two more: a *failed* sceVaudioChReserve still marks vaudio
reserved, so a caller that lost the channel to Output2 is told 0x80000021 next
time until a release clears it; and sceVaudioChRelease ignores that flag entirely
and releases whatever holds the SRC channel, which pspautotests already called the
"wrong release".
The flat 10000 cycles charged to every Output2 and SRC output turns out to be
right only for the refused case. Every other outcome ends up querying the codec
and costs over 100us on hardware, including finding the channel unreserved, so
those now cost 25000. Mixer channels are the other way round - cheap to refuse,
expensive on the one output that starts the DMA - so that charge moved to the DMA
start. F1 2009 behaves identically and Burnout Dominator's mixed output is
bit-identical to before.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The nine channels were one array of one struct, but only index 8 ever used the
two DMA descriptors, the played/fraction position and the waiting-thread vector,
and only 0-7 ever used the buffer slot - so eight copies of a std::vector sat
there dead. They are two different pieces of hardware and now they are two
structs: AudioChannel for the eight the mixer walks, and a single AudioSRCChannel
for the one the codec reads directly.
Found while rereading it:
- __AudioUpdate can now run from inside an output call, and it reschedules when
it wakes a thread. A context switch there lands before the syscall writes its
return value, so the caller loses it and the woken thread gets it instead.
Deferred with hleReSchedule when a syscall is in flight. The same fix applies
to the release path, which had the problem before this branch existed.
- A finished SRC buffer whose waiting thread had given up dropped the completion
entirely, so the next caller waited a buffer too long. Now it walks past dead
waiters and banks the completion if nobody is left.
- An output with a null pointer changed the channel volume, where the hardware
returns before touching it.
- A busy channel came back as an error from the Output2 path and as debug from
the mixer path. It is an ordinary answer a game polls on, so both are debug
now; the old behavior filled the log with 18k error lines in a minute of F1
2009.
- AudioChannel::reset had no callers left: releasing a channel with a thread
parked on it is refused, so there is nobody to wake.
- The two routing modes are globals and were written once per channel. They get
their own small savestate block.
Savestate: the sceAudio section goes to 3, and the SRC channel gets a section of
its own. States from released builds carry it as a ninth channel record, which is
read and discarded.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
I deduced that this was the case, and attempted implementing this path long ago,
but I could never quite get it to work in all games. Set Claude on a quest to
research and implement it, and lo and behold, it works. A bit sobering.
Fixes#12888 and likely more. Additionally, audio latency is likely slightly
improved overall, and memory usage is down by 4.6MB.
Claude says:
The blocking output calls are not a queue that callers line up behind. Each
mixer channel holds exactly one buffer and at most one parked thread; a second
thread arriving while the first is waiting is told the channel is busy and is
expected to skip its turn. The Output2/SRC channel holds two DMA descriptors and
refuses a third caller outright, without waiting at all.
We blocked everyone instead, so a game running a movie thread and a sound-effect
thread over one output made the two alternate - a frame of movie audio, a frame
of effects silence - and the movie played at half rate. That is #12888, seen in
F1 2009 and Colin McRae: DiRT 2. With this, the movie thread keeps the channel
for the whole cutscene and the effects thread is refused, which is what the
hardware trace shows.
The driver also never copies a buffer on the way in: it stores the pointer and
its mixer walks it forward 64 samples at a time out of the game's own memory.
Modelling that fixes#20095 as a side effect, and drops the 4.6MB of per-channel
sample rings we were carrying. The mix event is re-phased to the moment a DMA
starts, since the mixer thread outranks its caller and gets a block in before the
output call returns.
Along the way: the two rest-length calls differ after a null-pointer output,
sceAudioChRelease reports not-reserved rather than not-init,
sceAudioChangeChannelConfig validates the format,
sceAudioChangeChannelVolume validates nothing, and sceAudioOutput2ChangeLength
takes a range of 17..4111. Details in docs/sceAudio.md.
Savestates: AudioChannel goes to version 4. Older ones stored mixed samples that
can't become a pointer and a position again, so they load with the pending audio
dropped and any parked threads released.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The five new [System] keys, in the 33 languages that already had a rendering of
"firmware" to follow - they disagree on it (fastvare, prosjivka, systemprogramvara,
laiteohjelmisto), so each one matches whatever its own file already chose. The
other 13 fall back to English.
Firmware screen: call it "Uninstall firmware", not "Erase firmware"
Translate the rest of the firmware screen strings
"Uninstall firmware", "No firmware installed" and "Launch XMB", in the same 33
languages as the auto-install strings. XMB stays untranslated - it's Sony's
name for it, and pl_PL already wrote it bare.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Most UMDs carry a firmware updater, and having a real firmware in the NAND is
what the LLE modules want. On boot, if the disc's updater is newer than what's
installed - or nothing identifiable is installed, which is what a fonts-only
NAND looks like - unpack it, with a progress bar on the OSD. Controlled by
bAutoUpgradeFirmware, on by default, with a checkbox on the firmware screen.
Off in headless, which shouldn't rewrite the NAND during a test run.
The install itself is now shared with the install screen, and hardened, since
it can run without anyone watching:
- It stages into <NAND>/install-staging and only erases the installed firmware
once the new one is complete on disk, so a failure leaves what's there alone.
- A single entry that didn't unpack fails the whole install. A firmware with
holes still looks installed, so nothing would ever replace it.
- A truncated archive is rejected instead of unpacking to half a firmware that
every stat reports as a clean install. The PSAR header records the length;
it was being clamped to the buffer rather than checked against it.
Also falls back to the version in the archive's own header when the PARAM.SFO
next to the updater is unreadable, which it is on a fair number of discs.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
"Firmware 6.20 is installed. It will be erased and replaced with 6.60." is the
thing worth double-checking before wiping a firmware - installing off whatever
disc is to hand makes going backwards easy to do by accident.
Co-Authored-By: Claude Opus 5 <[email protected]>
He used Claude, never thought of this use case, but it's logical - it
can go reverse engineer the functions to come up with likely names,
making it easier to match the SHA1 hash. (NIDs are truncated SHA1 hashes
of names, except in firmwares later than around 3.50).
SendDebugScreenshot ignores the descriptor entirely and reads the display
framebuffer from the GPU, so filling one in was theatre - and it computed a
pointer from the display address, which is zero whenever the shell has the
display switched off.
Also reset g_screenshotSaved per test, so a test that emits its own screenshot
doesn't stop the next one getting the end-of-run capture, and clear the sceReg
open count on shutdown to match init.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Record the fifth cause (the rejected impose parameter), correct the counts now
that 3.80/3.90 and 2.6x-2.8x are covered, and note that 1.50 through 2.50 never
touch the encrypted XMB index at all.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Same three functions, one more NID each. Those two shells started no plugins at
all and sat on a black screen; both reach an interactive XMB now.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2.60 and 2.71 tag their flash0:/vsh/etc/index.dat 0x495BE403 rather than with
the 0x0B2Bxxx0 the rest of this family uses; 2.80, 2.81 and 2.82 use 0x0B2B05F0.
All five now reach an interactive XMB.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Every impose.prx from 1.50 to 5.55 dispatches on it and returns a field of the
impose context - the same field that decides where the backlight brightness
answer comes from. We rejected it as not a parameter at all, and 3.11's shell
read the error back, blanked the display with sceDisplaySetFrameBuf(0, 0, 0)
and never turned it on again.
2.00, 3.03 and 3.11 now reach an interactive XMB.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The rule was already here in one line and I still added a Claude-Session
trailer to ten commits, because the session's own attribution instructions say
to. Say plainly that this rule wins, and to check git log afterwards.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The old claim of "all of them" measured that the shell renders, which several
versions do while showing only the wallpaper or a full-screen error. Record what
each version actually does, the four causes that were in the way, and how to
find the next one of each kind - including that --timeout is wall-clock, so a
slow-drawing shell can look stuck when it isn't.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The per-model naming only starts around 3.50. Older dumps have plain
memlmd.prx and loadexec.prx, name the wlan firmware after the chip revision
(wlanfirm_magpie.prx became wlanfirm_01g.prx), and don't have lowio.prx at all
until about 3.52 - so every one of those was reported as a failed load on
2.00 through 3.30. Try the emulated model, the model in the path, then those
older spellings, and say plainly when the firmware simply doesn't ship one.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Those generations predate the per-model split and have a single
flash0:/vsh/etc/index.dat. 3.30 through 3.52 now reach an interactive XMB;
3.30 to 3.51 share 3.52's key.
3.03's mesg_led.prx is older than keys330_1, so its table was confirmed against
keys300_1 and keys280_1 instead - both match byte for byte.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
flash0:/vsh/etc/index_XXg.dat is the index of what the XMB shows, and each
firmware generation tags it with a key per PSP model. We only had the 6.6x
triple, so 5.03 through 6.39 decrypted nothing, drew no icons, and gave up with
the red error screen.
The keys come out of each firmware's own mesg_led_XXg.prx, whose tag table is
24-byte entries of tag plus 16-byte key. Extracting the 6.6x triple that way
reproduces the three keys already in this file byte for byte, which is what
establishes the layout.
5.03, 5.50, 5.55, 6.00, 6.20, 6.31 and 6.39 now reach an interactive XMB.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
sceKernelLoadModuleVSH, sceKernelGetModel and sceImposeSetStatus each appear
under a different NID in 3.95/4.05, 6.00/6.20 and 6.31/6.39. Without the load
one those shells started no plugins at all and sat on a black screen.
Identified the same way as the 5.xx set: for each firmware, the modulemgr export
with sceKernelLoadModuleVSH's callee set, and the vshbridge export whose body is
the user-level check 6.61 wraps sceKernelGetModel in - which is also the only
SysMemForKernel import those shells actually call.
3.95 and 4.05 now reach an interactive XMB.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The dumped 0x66 comes from a 6.6x PSP, and 5.50's shell rejects any schema
version above 0x58 as a corrupt registry - it drew its whole XMB and then
replaced it with the "settings are corrupt, press O to repair" dialog.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
We hand every sceRegOpenRegistry the same handle 0, so closing one used to wipe
every open category - including ones a still-open registry handle owns. On
hardware each open gets its own object and they don't interfere.
The VSH's alarm scan does exactly this: it holds /CONFIG/ALARM open, then opens
and closes the registry once per alarm slot, and found its own category gone by
the end. Count the opens and only clear on the last close.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
flash0:/vsh/etc/index_XXg.dat is the index of what the XMB shows, and 5.50 tags
it 0x0B2B11F0, which we had no key for - so the shell decrypted nothing, drew
no icons, and gave up with the red error screen.
The key is read out of that firmware's own mesg_led_02g.prx, whose tag table is
24-byte entries of tag plus 16-byte key. The two neighbouring entries hold
keys330_1 and keys505_a byte for byte, which is how the layout was confirmed.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
sceKernelLoadModuleVSH, sceKernelGetModel, and four of sceImpose_driver's calls
already have implementations; 5.xx just asks for them under different NIDs, so
the 5.50 shell got nothing back. Each one was identified by disassembling that
firmware's own module and comparing the body against 6.61's, where the same
function is exported under a name - the pairs are instruction-for-instruction
identical apart from context-struct offsets.
GetModel was the one that mattered most: unresolved, vshbridge handed the shell
a garbage model number, and it went looking for PSP-3000 resources on a dump
that has none.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
--nand had a field and an ApplyToConfig branch but nothing ever parsed it, so it
was silently ignored.
--screenshot-save only fired when a GE replay finished or a test used the
EMIT_SCREENSHOT devctl, and even then only under --compare. Anything else - a
game, or --vsh - ran to the timeout and wrote nothing. Capture the display at
the end of the run when nothing else did, which is what makes it usable for
looking at what a booting system actually has on screen.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Records that a name written to the memory stick from a PC loses its case when the PSP lists it,
which is what SimulateVFATBug() reproduces. Worth having written down: io/shortname shows the
opposite for files the PSP creates itself, and reading that alone would suggest the hack is wrong
and can go. It can't - the two differ because the PSP sets the FAT lowercase flag and macOS
doesn't, and an emulator can't tell which wrote a given host file.
Cashing in the io/stat and io/shortname recordings.
__IoGetStat began with memset(stat, 0xfe, sizeof(SceIoStat)), which destroyed 24 bytes of the
caller's buffer that a real PSP never touches - it writes only as far as the timestamps and
leaves all six st_private words exactly as it found them. It also wrote a made-up sector number
into st_private[0] on the memory stick. That word carries the LBN on a UMD, which games read to
build disc0:/sce_lbn paths, so it stays for non-FAT and is left alone otherwise.
FAT has no permissions of its own and everything reads back as 0777. We were passing the host's
idea of the file through instead. The existing "all files look executable on FAT" hack for Beats
(issue #14812) was right in substance but lived only in sceIoDread, so sceIoGetstat and
sceIoDread disagreed about the same file where hardware has them agree. Both now go through one
path, which also gets the read-only case right: no write bits means mode 0555 and attr 0x21.
sceIoGetstat on the root of a volume is refused, as on hardware.
sceIoChstat was a logging stub. It now applies the read-only flag, which is what st_mode's write
bits and st_attr's 0x01 both mean on FAT - setting either produces both, and it's reversible.
That needs a new IFileSystem::SetFileWritable, defaulting to "can't" so read-only filesystems and
hosts that can't express it (Android content URIs) are unaffected; the call still succeeds there,
since hardware would have.
GenerateFatShortNames now accounts for capitalisation. FAT keeps a lowercase flag for the base and
another for the extension, but the PSP only honours the base one, so "shrt" becomes SHRT while
"readme.txt" becomes README~1.TXT. We were only adding a counter on collision. The unit test
carries the whole recorded set, including the corrected README~1.MD.
io/shortname stays in tests_next: its d_name column can't match while SimulateVFATBug is
uppercasing lowercase 8.3 names, which is deliberate and load-bearing for homebrew.
The new sysmem tests run the same partition sweep from both privilege levels, which settles
several things that were guesses:
The valid range is 1-6, not 1-9-except-7. sceKernelCreateVpl, CreateFpl, CreateMsgPipe and
AllocPartitionMemory all let 8 and 9 through to the permission check, so a caller asking for
partition 8 got ILLEGAL_PERM where hardware says ILLEGAL_ARGUMENT. Privilege changes the
permission check, not the range - 1, 3 and 4 are refused from user mode and work from kernel mode
in every one of these APIs, which is the evidence the earlier BlockAllocatorFromID change was
missing.
sceKernelAllocPartitionMemory reports an out-of-range partition differently depending on which
entry point was used - ILLEGAL_ARGUMENT through SysMemUserForUser, ILLEGAL_PARTITION through
SysMemForKernel. Both NIDs land on the same function here, and hleIsKernelMode() is precisely
"came in through the kernel NID", so it picks the right one.
sceKernelCreateHeap had four "TODO: Validate error code" comments and no test at all - it's
kernel-only, which is why. All four are now recorded: out-of-range partitions are
ILLEGAL_PARTITION, a size of zero or less is HEAPBLOCK_ALLOC_FAILED before anything is allocated,
a NULL name is refused with ERROR, and flags really are ignored. sceKernelAllocHeapMemoryWithOption
had its validation backwards: the option struct's size field isn't checked at all, while the
alignment must be a power of two from 4 to 0x80.
Not fixed, and split into sysmem/kernel/heapgrow in tests_next: a real heap will hand out a block
larger than the heap itself, so the size isn't a cap. Ours is a fixed allocator over the reserved
block. Worth establishing how far the real one grows before implementing that.
Risk: the range change makes partitions 8 and 9 fail earlier and with a different code than
before. Nothing in tests_good depended on the old behaviour except two expectations that had
drifted from hardware, corrected in the submodule.
The game screen already reports which updater a disc carries, but there was no
way to act on it - the only route to installing one was picking an updater PBP
out of the browser. The unpacker already handles being pointed at a disc, so
this just wires the menu entry to it.
Shown only when the disc actually has one, and not while a game is running:
installing wipes the NAND that game has mounted.
InstallUpdateScreen takes the archive size now, because for a disc the size of
the file it was handed is the game's and says nothing about the firmware.
Co-Authored-By: Claude Opus 5 <[email protected]>
Backwards from 5.01, one release at a time, against all 39 versions that ship
on a disc plus the download-only 6.61. Nothing here is an offset - it's almost
entirely Sony renumbering the kernel *_driver NIDs, which sends an import we
mean to HLE into the real firmware module instead.
Four more NIDs each for sceRtc_driver/sceRtcSetAlarmTick and
sceHprm_driver/sceHprmReadLatch, covering 1.50 up. The rtc one is what parked
every thread on a SceSysconSync semaphore; the hprm one runs once a frame, so
unresolved it was most of the boot log. Also sceImposeGetParam/sceImposeChanges
(1.50 - 2.xx) and sceKernelLoadModuleVSH (1.x, which is how the shell loads its
own plugins - unresolved it got module id 0 and StartModule failed).
sceRtcIsAlarmed had to be implemented too; it returns 0, as in JPCSP. As a null
entry it returned LIBRARY_NOT_YET_LINKED, and the 3.0x-3.5x VSH read that as
"ask the hardware instead" and went back to blocking on syscon.
Two structural findings:
- Up to 4.05, scePaf's heap allocator is a separate heaparea1.prx that paf
imports as scePafHeaparea. Load it when it's there. Its pool pointer needs
the same pre-fill paf's does, at gp - 0x7FCC rather than gp - 0x7E88.
- 1.50's vshmain.prx declares no module attributes at all - PSP_MODULE_VSH_MODE
only appears from 1.52 - so the whole VSH bootstrap was being skipped. Accept
the module name too.
Co-Authored-By: Claude Opus 5 <[email protected]>
The blocker below 6.60 wasn't offsets, it was that Sony renumbered the kernel
*_driver NIDs between versions. A function we HLE under its 6.6x NID is a
stranger on an older build, so the import lands in the real firmware module
instead - and that's where it goes wrong:
- sceRtc_driver sceRtcSetAlarmTick. Without the HLE the VSH's alarm call ran
the real rtc.prx, which called on into syscon.prx and blocked forever on a
SceSysconSync semaphore. That was the whole "stalls with every thread parked"
symptom; the tell was a fourth SceSysconSync waiter a healthy boot lacks.
- sceHprm_driver sceHprmReadLatch, called once a frame - so before this an
older firmware's 12-second boot logged ~20000 lines of one unresolved import.
Three extra NIDs each, found by disassembling the module from both firmwares
and matching on the address of the user-mode export whose NID never changed
(sceRtc/0x7D1FBED3, sceHprm/0x40D2F9F0).
5.55 additionally needed two PRX decryption keys we didn't have (0x4C941AF0
and 0x4C941BF0) - without them none of flash0:/kd decrypted and the shell came
up with no drivers behind it at all.
Checked one release at a time against every version that ships on a disc, plus
6.61. 4.05 and below still die on a null write inside vsh_module.
Co-Authored-By: Claude Opus 5 <[email protected]>
With no kernel modules there's no firmware to speak of - it's the fonts we
pulled off a game's disc - and rows reading "Kernel modules: 0 / XMB: No"
say nothing. Keep the font count and the size.
Co-Authored-By: Claude Opus 5 <[email protected]>
6.60 ships byte-identical paf.prx and vshmain.prx to 6.61 - all 6338 + 669
functions disassemble the same - and boots to an interactive XMB, so let
FirmwareVersionSupportsVSH accept it. That matters because no UMD carries 6.61
(it was download-only), so 6.60 is the best a disc-installed firmware can be.
The two module patches were hardcoded offsets from the module base applied to
any module of the right name, which is quietly wrong on any other build:
- The scePaf heap arena slot moves with every build (0x18CCD8 on 6.00 through
0x18D728 on 6.60/6.61) but sits at gp - 0x7E88 in all of them, so find it
that way. On its own this turns an immediate SIGSEGV inside scePaf into a
clean stall on 6.00 through 6.39 - they still don't reach an XMB, they get
stuck in sceVshBridge_Driver instead.
- The vsh_module alarm-category offset has no such anchor, so check the word
there is the one the patch was derived from. On 6.20 and 6.00 it's ASCII
string data - the unconditional write was corrupting a string table.
Also resolve the per-model kernel drivers (memlmd, loadexec, wlanfirm) to the
model being emulated. They were asked for as _01g, which a firmware unpacked
for a single model doesn't have - and our own updater unpack defaults to 02g.
Co-Authored-By: Claude Opus 5 <[email protected]>
KIRK CMD1 writes header + data_offset + align16(data_size) bytes into outbuf,
and all three come out of the header the decrypter just decrypted, not from the
caller. The SHA1 check doesn't bound them - it only covers the header, so it
passes just as happily for a block that's been cut short.
The PSAR walker has to guess how long an updater's second block is (nothing
records it, so it tries the sizes real updaters use), and a wrong guess sent
KIRK off the end of the buffer: unpacking a firmware crashed roughly half the
time, on every version and disc I tried, depending on the heap layout.
Bound the write against the size the caller gave us, in all six decrypt types.
Co-Authored-By: Claude Opus 5 <[email protected]>
Shows what's actually in PSP/NAND - nothing, a fonts-only partial install, or
a full firmware with its version, build date and region - read from
flash0:/vsh/etc/version.txt, which is present both in a PSAR-unpacked install
and a NAND dumped off hardware.
Also offers to install an updater, erase the NAND, and launch the XMB, the
last one gated on FirmwareVersionSupportsVSH() since the module patches that
get vshmain.prx running are tied to 6.61's offsets.
Installing a firmware now erases flash0/flash1/ipl first - two firmwares can't
be merged, a file the new one doesn't have would linger and still get loaded.
Co-Authored-By: Claude Opus 5 <[email protected]>
The two fudge factors in the reverb path cancelled, which is why the
overall level felt roughly right.
- The send is accumulator * 0x20 >> 16, i.e. sample >> 2. We used >> 1,
driving the reverb 6dB hot.
- The return is (evol * out) >> 11. We used >> 12, i.e. 6dB quiet.
Net level is therefore unchanged, but the reverb now runs at the level
the presets were designed around. That matters because the filter clamps
internally, so a 6dB hot input changes how the feedback path saturates -
worst on the presets with heavy feedback.
Also adds a slider in the imgui.
The reverb presets came from nocash's PS1 table. Six of the nine are
identical on the PSP, but three are not.
Also correct our linear interpolation expression: we were close but
our formula can produce an off by 1 at times.
An ME image is two KL4E streams back to back: the code that runs at
0x88300000, then a blob the image decompresses to ME local RAM at
0x00101000, which is where its data segment lives. Decompressing only the
first left every global unaccounted for.
--re-decrypt now reports how much of the plaintext the first stream used
and writes the remainder to <out>.tail, and accepts an already-plain
KL4E/KL3E file so that tail can be fed straight back in.
--re-decrypt runs pspDecryptPRX() over a file and unpacks the KL4E/KL3E
stream behind it. This opens up flash0:/kd/resource/*.img, the images the
Media Engine actually runs: they are ordinary tagged containers (tag
862648D1, which PrxDecrypter already has a key for) with the ~PSP
signature blanked, so the normal module loader never touches them.
--re-raw-base analyzes --re-module as a flat code image at a given
address rather than as a PRX. The decrypted ME images are raw MIPS with
no ELF around them; the address they were linked for is recoverable from
their own jal targets (0x08300000 for meimg.img).
Also makes PrxDecrypter.h self-contained - PSP_Header is built from _le
types, so it needs Common/Swap.h rather than relying on the includer.
The channel took only 256, 1024 and 2048 samples, so a game that hands it MP3
frames got SCE_KERNEL_ERROR_INVALID_SIZE and no music. Dead or Alive Paradise
does exactly that from its music player: sceVaudioChReserve(1152, 44100, 2),
1152 being the MPEG-1 Layer III frame size.
The format check moves below the sample count check to match: the module
returns 0x80000104 for a bad count before it ever looks at the format, so a
call with both wrong got the wrong error out of us. The two error codes we
already returned are the ones it uses.
Co-Authored-By: Claude Opus 5 <[email protected]>
PPSSPP decided whether a caller was privileged with hleIsKernelMode(), which reports whether the
syscall being executed is itself a kernel-only export. That's a different question from the one
the hardware answers: on a PSP the privilege belongs to the calling module, and a kernel module
reaches sceKernelCreateTlspl through the ordinary ThreadManForUser NID like anything else. So a
kernel module asking for partition 1, 3 or 4 got ILLEGAL_PERM where a real PSP hands it over,
which the new threads/tls/kernel/partition test shows directly.
BlockAllocatorFromID now also accepts a caller whose thread belongs to a kernel module, via a new
__KernelCurThreadIsKernelMode(). It checks the thread's own attribute first and then the owning
module, because a kernel module's main thread isn't necessarily flagged kernel - the attribute
comes from PSP_MAIN_THREAD_ATTR, which needn't set it. That mirrors how sceKernelCreateThread
already works out allowKernel.
This only ever widens access, and only for threads belonging to kernel modules, so games are
unaffected - they run in user modules and see exactly what they saw before.
threads/tls/kernel/partition now records the sweep from a kernel module, which settles the range
question the user-mode recording couldn't: privilege changes the permission check, not the range.
Partitions 1, 3 and 4 are ILLEGAL_PERM from user mode and fine from kernel mode, while 7 and up
are ILLEGAL_ARGUMENT either way. So the check goes back to a plain 1-6 for both, and the
kernel-mode carve-out from the last commit - which would have let 8 and 9 through - is gone.
The hardware doc gains a section on kernel-mode tests: what COMMON_KERNEL does, why the stock
crt0 makes a kernel PRX unloadable, which libraries can't be imported, and how much room there
actually is in the kernel partition.
sceKernelUtilsSha1Block* had the same single global context that MD5 did, so it gets the same
treatment: state, counters and block buffer now live at ctxAddr in the layout hash/sha1ctx
records off hardware. Unlike MD5, SHA-1 does not stream whole blocks through buf, which happens
to be what our sha1_update already does - so no fill-in step is needed there.
The Tlspl partition range from the last commit was too broad a cut. Hardware says only 1-6 exist,
but that recording is from user mode, and BlockAllocatorFromID deliberately maps 8 and 10 to the
user partition for a kernel-mode caller - rejecting them outright would have taken that away.
The tightened range now applies to user mode only and kernel mode keeps what it had.
threads/tls/partition also shows the answer doesn't depend on the compiled SDK version, checked
across 1.00 through 6.06, and that partition 5 is accepted - which no test had covered.
Three fixes, all of them things the new hardware tests turned up.
sceMd5Block* and sceKernelUtilsMd5Block* shared one static md5_context and ignored the context
pointer the caller passed in, with a TODO saying it would do "unless games do several MD5
concurrently". hash/md5ctx shows a real PSP keeps everything in the caller's 96 bytes and happily
runs two digests at once, so do that instead: the state, the counters and the block buffer now
live at ctxAddr in the game's own memory, in the layout the test pins down. Two interleaved
digests come out right, and a context that gets copied mid-digest carries on correctly. As a
side effect the state is now covered by savestates, which a file-static never was.
MersenneTwister masked both halves with 0x80000000 where the low half needs 0x7FFFFFFF, so
sceMt19937UInt and sceKernelUtilsMt19937UInt were returning a sequence that isn't MT19937 at
all - every number differed from hardware from the first draw. hash/mt19937ctx computes the
reference sequence itself and confirms the PSP is plain MT19937; with the mask fixed we match it
for both seeds tested. Init also twists the array immediately, as hardware does, so a context
that has been seeded but not drawn from now holds what a real one would.
sceKernelCreateTlspl accepted partitions up to 9 before falling through to the permission check.
Hardware draws the line at 6 - threads/tls/create records 7, 8, 9 and 10 all returning
ILLEGAL_ARGUMENT - so 8 and 9 were coming back ILLEGAL_PERM. Note this is genuinely different
from sceKernelCreateVpl right above it, which does let 8 and 9 through to ILLEGAL_PERM; the two
had been sharing a check that was only ever right for Vpl.
Risk worth naming: the MT19937 change alters the numbers any game gets from these calls. That's
the point - they were wrong - but a savestate taken mid-sequence will resume with a generator
that behaves differently from the one that made it.