PSP Download Index
Install and update homebrew directly on your PlayStation Portable, using the PSPDX standard.
- 📦 Download & update homebrew right on the PSP, no PC needed
- 🧩 Plugins too: install, turn on and off, update
- 🔌 Works on the PSP Street (E1000): online over the USB cable with USBNet
Extract pspdx.zip to your PSP, start PSPDX and browse by category, tag or source. Updates appear on their own. WPA2 needs ARK-5.
PPSSPP: extract the same ZIP to the emulator's Memory Stick directory. PSPDX
carries a free replacement font in itself, so no firmware font or debug
override needs to be installed. A real PSP uses its firmware font first; a
font.pgf beside the EBOOT, as releases up to 1.1.4 shipped it, is used
before the carried one.
- Install: press Cross on an app. Circle keeps the download running in the background, Square cancels.
- Basket: add apps with Triangle, then Download all. Running downloads are listed there too.
- Find: Search on the first page looks through names, authors, tags and descriptions. SELECT sorts any list by newest or by name.
- Display: 60 FPS by default; the first row under the wrench switches to a calmer 30 FPS mode.
- No Wi-Fi (PSP Street): at its first start PSPDX asks whether to connect via Wi-Fi or the USB cable. USB cable installs the USBNet plugin the package carries and turns it on. Run its gateway on a PC, plug in the cable, and in the connection dialog choose [New Connection] → Scan → "Hi-Speed USB" the first time. The row Set up USBNet under the gear asks again later. Its preview appears under the explanation on the right.
Issues? Tell us how it went.
- Catalogs: PSPDX comes with the main catalog and a community list. Add your own in the gear tab.
- Updates: at start, PSPDX fetches the catalogs and asks GitHub about any app no catalog lists.
- Your files stay: an update replaces only the files the release ships; saves and settings next to the EBOOT are kept.
- Memory: every installed app's
.pspdxstays on the Memory Stick.
The PSPDX standard · ids, updates, no .pspdx
PSPDX reads the PSPDX standard: a .pspdx next to each app and catalog.json lists.
- Catalogs: one
catalog.jsonrequest lists every app and shows which have updates. - Downloads: the ZIP comes straight from each app's release, never from the catalog.
- Without a catalog: the saved
.pspdxof an installed app leads back to its repository.
What PSPDX does on top:
- Ids: GitHub →
io.github.<owner>.<repo>; elsewhere the reversed host ofsourceplus the name. A catalog's ownidis kept only when it is well-formed and doesn't clash with an installed app. An installed app keeps the id of its record, whatever a list calls it later; one repository is one row. - Folder: without
installdir→PSP/GAME/<repository name>, cut to 32 characters. An installed app keeps its folder: a list naming another moves nothing, a folder you renamed (record too) stays, and only the author's.pspdxnaming another folder moves it, with everything in it. - Updates: the SHA-256 of
releases[0]differs from the installed ZIP's. Versions and dates are not compared. - ZIP rule: one
.zipon the release, or of several exactly the one withpspin its name; otherwise the app is left out with the reason. - Pinned release:
"release": {"tag": "v1.2"}installs exactly that release, with no updates until the file changes. - No
.pspdxat the source: GitHub's own 404 (not one after a redirect elsewhere). PSPDX installs from the catalog entry, your INBOX file or, typed into Direct Install, the repository's name. A live catalog keeps it updated; without one the latest release is asked at GitHub by the saved.pspdx, same ZIP rule and SHA-256. As soon as the author adds a.pspdx, that file counts. - Catalog text: CR LF and tabs in
descriptionandsummaryare read as meant; an entry with a NUL in its text is dropped, and asha256of zeros is no hash. - Types:
homebrewinstalls underPSP/GAME/;plugininstalls the folder of its.prxtoseplugins/<name>/and, when you turn it on, adds one line to ARK-4'sPLUGINS.TXT;isois listed, not installed yet. schemafield: acatalog.jsonis read as v1 unless it names another PSPDX catalog version (catalog-v2.json); none, a relative path to a copy, or any other value is read as v1 and noted in the log.
cache https://chriopter.github.io/pspdx-catalog/catalog.json
https://github.com/chriopter/pspdx-demo
https://github.com/someone/project@v1.2
- One repository URL per line;
@tagpins a release cachenames a catalog, read first; repositories it doesn't cover are asked directly
"files.associations": { "*.pspdx": "json" },
"json.schemas": [{ "fileMatch": ["*.pspdx"],
"url": "https://chriopter.github.io/pspdx/schema/pspdx-v1.json" }]Sources · catalogs, repositories, INBOX
| Source | Added through | Result |
|---|---|---|
Catalog site URL, catalog.json or text list |
Sources | Browse all listed apps |
GitHub URL or owner/repo |
Direct Install | Add the repository as a source and install its app; without a .pspdx it installs from its latest release, named after the repository |
.pspdx files in PSP/PSPDX/INBOX/ |
Direct Install | Validate and install the selected files; for a repository without its own .pspdx your file is the app |
- Preset sources, in this order:
https://chriopter.github.io/pspdx-catalog/,https://pspdev.github.io/homebrew/ - Presets are carried in the EBOOT; a
presets.txtbeside it, from a release up to 1.1.4, is no longer read - Each preset lands once and is noted in
PSP/PSPDX/presets.seen: removed stays removed, a new one in an update arrives - Sources lists each source with its address, kind, apps and when it last loaded; one that does not load is marked unreachable, one served only from its saved copy offline copy; the rest load as usual
- A line ending in
.pspdx, left from an older version, is skipped with a line in the log - Sources are validated before they land in
PSP/PSPDX/sources.txt - Adding a catalog installs nothing; Direct Install and INBOX do
- Removing a source keeps installed apps, their manifests and state
start -> the built-in list -> in presets.seen? skip
-> append to sources.txt unless there -> note in presets.seen
catalog.json -> ok: browse
| fail
catalog.txt -> ok: check each listed repository directly
| fail
saved copy -> ok: browse the last snapshot
A site with only catalog.txt works without a builder.
Install and update · checks, recovery
flowchart TD
pick(["× on an app, an INBOX file, or Direct Install"]) --> github{"Source on GitHub?"}
github -- "no" --> outside["Only from a catalog entry<br>the ZIP must match its SHA-256<br>an INBOX file waits"]
github -- "yes" --> pspdx{".pspdx at<br>raw.githubusercontent.com"}
pspdx -- "200" --> own["The repository's own .pspdx wins"]
pspdx -- "GitHub's 404" --> stand["The catalog entry, your INBOX file,<br>or the repository name from Direct Install"]
pspdx -- "no answer, 404 elsewhere" --> later(["Not installed, try again later"])
own --> release{"Release: the catalog's,<br>a pinned tag, or GitHub's latest"}
stand --> release
release -- "none, rate limit, 5xx" --> norelease(["No release to install,<br>or GitHub didn't answer"])
release -- "found" --> folder{"PSP/GAME/#lt;dir#gt; free<br>or already this app's?"}
outside --> folder
folder -- "another folder in the way" --> bak(["Offer to move it to #lt;dir#gt;.bak"])
folder -- "yes" --> room{"Room for the ZIP<br>and for unpacking?"}
room -- "no" --> space(["Status line: how many MB are needed"])
room -- "yes" --> check{"Size, SHA-256,<br>ZIP and paths ok?"}
check -- "no" --> refused(["Refused, the stick is untouched"])
check -- "yes" --> installed{"Already installed?"}
installed -- "no" --> fresh["Stage, then rename into PSP/GAME/#lt;dir#gt;"]
installed -- "yes" --> overlay["Swap each shipped file,<br>your other files stay"]
fresh --> state(["Write the record and keep the .pspdx"])
overlay --> state
- On PSP Go, each new installation asks for System Storage or Memory Stick; an absent card cannot be selected. Updates and reinstalls keep the recorded device, including saves and settings.
- Only the folder holding the single
EBOOT.PBPis installed - Updates keep your files: files the ZIP ships are replaced or added, everything else in the folder stays; files an older version shipped and the new one doesn't are left over. Delete removes the whole folder
- An author who moves
installdirmoves the folder with your files in it, then the release is laid over it - Installed from a catalog entry, the saved
.pspdxis the entry's words; from outside GitHub the ZIP must match the entry's SHA-256 - An unmanaged folder in the way is never adopted or overwritten; PSPDX offers to move it to
<dir>.bak - Not enough room: the status line says how many MB are needed, before anything is written
- A transaction journal covers every shipped file, manifest and state; a cut install or update recovers at the next start to the old or the new version
- A leftover that won't delete (a read-only file in
<dir>.old) is retried at every start and blocks only that app; a folder deleted by hand installs fresh - PSPDX never deletes the folder it runs from, whatever record names it
The one case in which PSPDX writes outside PSP/GAME/<dir>/: a plugin's folder under seplugins/ on the device chosen for the install, and, once the plugin is turned on, one line of the PLUGINS.TXT there. Installing and turning on are two steps, and PSPDX asks about the second.
ZIP: the folder of the .prx nearest the top -> <device>/seplugins/<name>/, whole; PLUGINS.TXT is not touched
"Do you want to turn it on?" Yes -> one line after the end of PLUGINS.TXT: always, <device>/seplugins/<name>/<name>.prx, on
Options -> Turn Off / Turn On -> the last field of that line written over itself: off / on
update -> the files the release ships, never the line; refused if the .prx is no longer the one PSPDX installed
Delete -> spaces written over that line, and the files PSPDX installed; the folder too once it is empty
<name>is the.prx's file name without.prx:usbnet.prxgoes toseplugins/usbnet/usbnet.prx, with every file beside and below it in the ZIP. A.prxfurther down is copied along, never loaded; what lies beside the folder stays in the ZIP- Several
.prxin that folder: the.pspdxnames the one to load,"plugin": "main.prx". Without it, or naming a file that is not there, nothing is installed. Nor with a.prxas near the top in a second folder, or more than 64 files - The list is ARK-4's and yours. PSPDX never renames, shortens, deletes or rewrites it and makes no copy of it: it appends its line (the file's own line end, one before it where the list ends without), and afterwards only overwrites bytes of that line with as many bytes. Before each write the list is read again and must be unchanged; afterwards it is read back
- Only PSPDX's own line is written:
always, <path>, onoroffas PSPDX wrote it, or as ARK's plugin managers write it out again, and only when the record says PSPDX added it. Any other line for the same file, another run level, a comment after it,1foron, two lines alike, is yours and is never changed or removed; the row then shows what ARK would do (the last line naming the file decides) and a switch says other lines name it - A plugin that was never turned on leaves no trace in the list. One that was leaves an empty line of spaces after Delete, which ARK skips
- Every file is PSPDX's by the SHA-256 the record took when it was installed. A
.prxyou copied over the installed one is your build and goes on loading: an update is refused and changes nothing; a delete forgets PSPDX's record and leaves the folder and its line, and says so - Any other file in the folder that is not as installed, one you added or settings you changed, is yours: an update does not write it, even where the release ships that name, and a delete leaves it, and the folder with it
- Refused, with the reason, and nothing written: a
seplugins/<name>PSPDX has no record of; a.pspdx-newor.pspdx-oldbeside a file the release ships; a read-only file of the plugin's or list; a list over 64 KB, with a NUL byte in it, or that cannot be read; a.prxnamed outside 5 to 32 characters of[A-Za-z0-9_.-] seplugins/is made only when it is missing and every check has passed; one that is there under any spelling (SEPLUGINS) is used as it is- Everything takes effect when the PSP restarts. Hold START at power-on to start once without plugins
- The files are swapped in one by one under the journal, which lists each with its hash: a copy, a file or an empty folder is removed only when the journal says PSPDX made it, and a file only while its hash agrees. The record notes each write to the list before it is made: one a power cut left half written is finished in the same place at the next start, and where the bytes there are neither the old nor the new ones, nothing is written and the next switch says the list has changed. The record claims a line only once it has been read back, and drops the claim when the list no longer has it
- A plugin PSPDX 1.1 installed as the one file
seplugins/<name>.prxis switched and deleted as it was. Its next update moves it: the folder goes in, the new line is appended where the old one said on, spaces go over the old line, and the old.prxis removed if it is still the one installed and no other line names it - A plugin is never written into PSPDX's own folder. The
usbnet.prxPSPDX carries in itself is installed from memory, through the same installer and with the same refusals, when the USB cable is chosen (see Network); with it PSPDX carries the tag of the pspkit-usbnet release it is of and that release's ZIP hash, which go into the record so the store tells an update the usual way. Ausbnet.prxorusbnet.txta release up to 1.1.4 left beside the EBOOT is not looked at
Runs at startup and on Check for updates, the top row of the stick tab: × is the quick check, □ the full one. An update is a newest release whose SHA-256 differs from the installed ZIP's; versions and dates are not compared, except for a record from before SHA-256 was kept.
flowchart TD
start(["Start, or × Check for updates"]) --> fresh{"In a catalog that answered<br>and is under 24 h old?"}
fresh -- "yes" --> entry["The catalog entry<br>no GitHub request"]
fresh -- "no, or □ full check" --> github{"Source on GitHub?"}
github -- "no" --> record["The catalog entry or the record<br>never asks anywhere else"]
github -- "yes" --> due{"Asked in the last 6 h?"}
due -- "yes, not □" --> saved["The last answer in the record"]
due -- "no, or □" --> pspdx{".pspdx at<br>raw.githubusercontent.com"}
pspdx -- "200" --> api["Latest release<br>api.github.com, 1 of 60 an hour"]
pspdx -- "GitHub's 404" --> listed{"A live catalog lists it?"}
listed -- "yes" --> entry
listed -- "no" --> api
pspdx -- "no answer, 404 elsewhere" --> saved
api -- "rate limit, 5xx" --> backoff["Keep the record<br>asked again after 6 h"]
api -- "release" --> zip["ZIP rule, GitHub's SHA-256"]
entry --> differs
record --> differs
saved --> differs
zip --> differs{"SHA-256 differs<br>from the installed ZIP?"}
differs -- "yes" --> update(["Update to x.y"])
differs -- "no" --> current(["Up to date"])
- A check against a maintained catalog is one request and no API call
- An app without
.pspdxis asked by the saved one: GitHub's 404 on raw.githubusercontent.com, then the API - A pinned release (
"release": {"tag": ...}) is current until its file changes; the newest.pspdxGitHub answers with is saved - A repository added by Direct Install is a source: its
.pspdxand one API call at every start; an app from INBOX is asked every six hours - Rate limit or 5xx: the record stands, the check counts as asked, and Direct Install says GitHub didn't answer
- Catalog older than 24 h: the status line names its host
- Nothing is topped up from GitHub: a
.pspdxwithout author is by the account in its URL, a missing summary or licence stays empty
- Checking never installs; × on Install or Update queues it immediately
- ○ during download or unpack cancels the install and puts the stick back; in a batch it also stops the rest
- INBOX skips conflicts; only successful imports leave INBOX; up to 64 files that install are queued
- A pinned tag in an INBOX file must be the release's exactly (
0.1.3is notv0.1.3); the repository's own.pspdxwins over your file - For an installed app no catalog lists, PSPDX asks GitHub for exactly that tag (one API call)
- Self-updates run last; restart PSPDX afterward
× install · △ options · ○ back · □ basket · START run · L/R tabs (remembering your app page and scroll position); ←/→ tabs in lists, adjacent apps on detail pages
Tabs, as signs to the right, the open one named at the head of the list: Installed, update arrows and a count while updates wait → Homebrews everything published → UMDs coming soon → basket while it holds something → | → gear settings at the right edge; Homebrew, the UMD and the gear keep their place, the stick and the basket hang off them and come and go
△ → Information: version, author, licence, tags, size, id, last check, then summary and description; the analog stick scrolls, ○ back
Network · HTTPS, TLS 1.3, offline
catalog -> one request, gzip asked for, release data for every app
direct -> raw.githubusercontent.com .pspdx, up to 16 KB
-> api.github.com latest release
download -> release ZIP, following GitHub asset redirects
- At startup the PSP system network dialog lets you select or create a Wi-Fi connection
- Over the USB cable: asked once, at the first start, where PSPDX carries the plugin (every release does; a build without it offers no cable), custom firmware can load it and no cable plugin is there yet (a record of it, or the module loaded by the firmware). The answer is kept in
PSP/PSPDX/settings.txt(cable=wifi,cable=usb) and asked again by the row Set up USBNet under the gear, which runs the same question and what follows it; O there leaves everything as it was - USB cable installs that copy through the plugin installer as
io.github.chriopter.pspkitusbnet, turns it on and loads it for the session, so no restart is needed; from then on the store updates it like any plugin. What the installer refuses is said in one line, and the carried copy is then loaded for the session instead, fromPSP/PSPDX/CACHE/usbnet.prx, since the firmware loads a module from a file. Wi-Fi installs and loads nothing; switching back to Wi-Fi later only stores the choice - With the cable chosen, and until a connection has been made once, PSPDX asks two things before the system's dialog opens: Is the gateway running on your PC? Get it from github.com/chriopter/pspkit-usbnet (the cable leads nowhere without the gateway), then Do you want to connect now? with what to pick in the dialog: [New Connection] → Scan → "Hi-Speed USB". No to either leaves PSPDX offline for now
- A plugin that is installed is left alone at start, turned on or off: nothing is asked and nothing loaded
- Cancel to browse offline; checking again or installing can reopen the dialog
- Everything over HTTPS
- A catalog,
.pspdxor API answer has 120 s in all; a response head 30 s - Only a catalog asks for gzip, inflated as it arrives into a 512 KB buffer;
pspdx.logsaysfetch: <n> bytes gzipped, <m> inflated. A ZIP is never asked for compressed - TLS connections to the same host are briefly reused; a closed or expired one is reopened
- Not a perfect TLS 1.3, on purpose: a PSP has no trustworthy clock, no root store, no way to update either, and may have sat in a drawer for years. The goal is that it still connects, not maximum security
- Done by pspkit-https: TLS 1.3 only, ChaCha20-Poly1305 first, all of Mozilla's roots, certificate dates held only against the day it was built. Its README says how and why
- An expired certificate, or one from an unknown issuer, asks "connect anyway?" instead of failing; a bad signature or the wrong hostname always fails
- The real guarantee: the ZIP you install is the one the catalog names (SHA-256), not that this connection is as safe as your browser's
- Packages are not signed; a catalog SHA-256 checks ZIP integrity, not author identity
- Seed: a sweep of the analog stick or smashed buttons when there is none yet,
CRYPTO/seed.binafterwards, renewed with Renew TLS Seed
- Refresh runs in the background; installing pauses network previews, they share the network stack
- Cached browsing works offline; release checks and downloads need a connection
Catalog · updates, previews, cache, memory
hourly: repos.txt -> catalog.txt
new release? -> read .pspdx -> hash ZIP: the 20 newest release ZIPs, each hashed once
-> extract EBOOT media from the newest
unchanged? -> reuse the entry
-> publish catalog.json
catalog.jsoncarries metadata, tags, description, source, install path, up to the 20 newest releases (date, ZIP URL, size, hash, changelog) and media URLs; the console readsreleases[0]- The reference builder lists repos with their own
.pspdx, and a repo without one from a file in itslisted/ - A push or manual run also picks up manifest-only edits; hourly runs wait for a release
- A build with no valid apps leaves the live site in place
- PSPDX compares the SHA-256 of the catalog's newest release with the installed one
- The catalog does not know what is on your PSP
- The update appears after the catalog publishes and the PSP refreshes; the ZIP still comes from the author's release
- The catalog hosts
ICON0.PNG,PIC1.PNG,ICON1.PMFandSND0.AT3separately: no ZIP download for a preview; of several screenshots the first is shown - A picture over 512 px, up to 4096, is scaled down to fit, a row at a time
- Priority: installed EBOOT media -> cached or catalog media -> placeholder
- Direct GitHub lookups fetch no new previews
- Catalogs in
PSP/PSPDX/CACHE/catalogs/, media inCACHE/media/ - Failed fetch: the last usable catalog stays for browsing; installed apps from GitHub ask their saved source, at most every six hours
- Offline: saved records and media only
- Both caches can be deleted without losing installation state
- Up to 64 apps in RAM; the catalog arrives in one 512 KB buffer
- A
.pspdx(up to 16 KB) and a description sit on the heap, only for an app that has them - Information breaks the description into lines once, when it opens, not every frame
Memory Stick · manifests, state, cache
Example on the startup device (ms0: or ef0:); app IDs are illustrative.
Temporary and debug files appear only when used.
ms0:/
├── seplugins/
│ ├── usbnet/ # Installed plugin: usbnet.prx and what its ZIP ships beside it
│ └── PLUGINS.TXT # ARK-4's list; one line per plugin turned on is PSPDX's
└── PSP/
├── GAME/
│ ├── PSPDX/ # Downloader
│ │ ├── EBOOT.PBP # Carries the presets, the fallback font and pspkit-usbnet
│ │ ├── .pspdx # Bundled for offline first start
│ │ └── LICENSES.txt # PSPDX's, the font's and the plugin's, one after another
│ ├── Cathedral/ # Installed homebrew
│ │ ├── EBOOT.PBP
│ │ └── ...
│ │ ├── EBOOT.PBP.pspdx-new # During an update: the new file
│ │ └── EBOOT.PBP.pspdx-old # During an update: the old one
│ ├── .pspdx-stage/ # Temporary first install
│ ├── Cathedral.old/ # Folder being deleted
│ └── Cathedral.bak/ # Unmanaged folder moved aside
└── PSPDX/
├── sources.txt # Subscriptions
├── presets.seen # Presets already offered
├── INBOX/demo.pspdx # Awaiting import
├── INSTALLED/
│ ├── io.github.chriopter.pspdxapp.pspdx
│ ├── io.github.chriopter.pspdxapp.state.json
│ ├── io.github.chriopter.pspcathedral.pspdx
│ └── io.github.chriopter.pspcathedral.state.json
├── CACHE/
│ ├── catalogs/<url-sha1>.json
│ ├── font.pgf # The carried font as a file, only where the firmware's cannot be read
│ ├── usbnet.prx # The carried plugin as a file, only for a session it is not installed in
│ └── media/ # Named after the served file
│ ├── <app-id>-<file>.png
│ ├── <app-id>-<file>.mp4 # or .pmf, as served
│ └── <app-id>-<file>.at3
├── TMP/
│ ├── download.zip
│ ├── transaction.json
│ └── cleanup.txt # Leftovers retried at start
├── LOGS/
│ ├── pspdx.log
│ └── http.txt
├── DEBUG/
│ ├── PSPDX.BMP
│ ├── PSPDX.KEYS
│ ├── PSPDX.NOFONT # A test: start without the firmware's fonts
│ ├── PSPDX.REPLAY
│ ├── PSPDX.TRACE
│ ├── PSPDX_REC/
│ ├── font/ltn8.pgf
│ └── ...
└── CRYPTO/seed.bin
INSTALLED/<app-id>.pspdx keeps the source independently of sources.txt;
for an app installed from a catalog entry, it is the entry's words.
<app-id>.state.json next to it has no outer app-ID key:
| Field | Contents |
|---|---|
source, added_from |
Original repository and import route |
installed |
Version, published_at, SHA-256, install directory (seplugins/<name>/<name>.prx for a plugin, with plugin_sha256 of that file, plugin_files, every installed file with its SHA-256, plugin_line, the path of the line PSPDX added, plugin_write while a write to the list is under way, and plugin_old while a 1.1 file and line are still to be cleared), device (ef0: or ms0:), and pspdx_installdir: the folder the .pspdx named at install |
latest |
Version, published_at, download URL, size, SHA-256, last successful check time and source |
update_check, manifest_url |
Written by older versions; read and ignored |
install or update -> writes installed
catalog or GitHub check -> writes latest, leaves installed alone
browse -> writes nothing
installed 1.0withlatest 1.1means 1.0 is still installed- PSPDX registers its own version at startup and copies its bundled
.pspdx, no request needed added_fromdoes not steer future checks; each operation writes only its app's state
- Recoverable writes may leave
.newor.bakfiles - Records and the download stay on PSPDX's startup device; the transaction journal also records the target device. Recovery waits if that device is unavailable. Legacy records without a device use the startup device.
- Staging stays on the target device under
GAME/and an update's copies beside their files: PSP renames need the same parent - Keep
INSTALLED/;CACHE/is disposable; never delete a pending transaction's files - Only per-app state files load; older combined state files are ignored
- Corrupt records are preserved and block writes; a NUL in a string or a SHA-256 of zeros makes a record corrupt
- A record PSPDX 0.5 kept of itself (
io.github.chriopter.pspdx) is retired at start
Build and run · Docker, PPSSPP
Inside the pspdev/pspdev:latest container, or with $PSPDEV, cmake and
wget on the host:
git submodule update --init --recursive
sh app/lib/pspkit-https/tools/build-wolfssl
make -C app- Output:
app/EBOOT.PBP - Libraries: pspkit-https (wolfSSL), cJSON, intraFont, libpng, zlib, PSP SDK
- CI builds inside
pspdev/pspdev:latest, throughdev/packlike a release, with pspkit-usbnet's newest release compiled in
git tag -a v1.2.0 -m "What changed" && git push origin v1.2.0
-> .github/workflows/release.yml: tag on master -> dev/pack -> GitHub release "1.2.0", notes = the tag's message, pspdx.zip
PACK_ONLY=1 dev/release 1.2.0 -> dev/pack here: the same pspdx.zip, nothing published
dev/pack: pspkit-https at its newest release (else refused), pspkit-usbnet's newestusbnet-psp.zipheld to itsSHA256SUMS-linuxand compiled in (dev/embed),make clean && makeinpspdev/pspdev:latest, the EBOOT a plain ELF carrying the plugin and the font,pspdx.zipwithEBOOT.PBP,.pspdxandLICENSES.txtonly- Needs Docker,
gh, Python, zip
dev/start -> build in Docker -> install on the PPSSPP stick -> launch
dev/start --no-build -> launch the existing build (a mock-catalog build is rebuilt anyway)
dev/start --mock -> the same against the local mock catalog
- Needs Linux, Docker, the PPSSPP Flatpak, Python, a systemd user session and Wayland
dev/startinstallsdev/ppsspp/controls.ini
| PSP | Key |
|---|---|
| × ○ □ △ | S D A W |
| START, SELECT | Enter, Space |
| L, R | Q, E |
| d-pad | arrow keys |
| analog stick | I J K L |
Code layout · app, dev, tools
| Location | Responsibility |
|---|---|
app/main.c |
Startup and the frame loop: the pad, the tabs and the cursor, the sync coming back, calls into session/, and the rig's hooks (scripted keys, screenshots, the film, the cipher benchmark) |
app/text.h |
Every word the screen shows, in one place |
app/gui/ |
Browser, rendering, icons, previews and firmware keyboard |
app/gui/files_view.c |
Data on screen: the two columns, the raw band, its keys, and the film or sound handed to the media thread |
app/session/view.c |
The browser's model, with nothing of the drawing in it: the tabs, the catalog filtered to the open one, the basket, what the action row would fetch, the gear's rows |
app/session/actions.c |
What the session does: installs, one or a run of them, removing, launching, fetching the catalog again, the sweep, the cache and the reset |
app/session/questions.c |
The questions that stand before those: put into words, and answered from the pad, X doing the thing and O leaving it |
app/session/options.c |
The one menu the shell draws: the package's options on triangle and the popups under the gear, their rows built and walked here |
app/update/ |
Manifests, sources, catalogs, INBOX, media cache and synchronization |
app/install/ |
ZIP reader, installation transactions, the in-place PLUGINS.TXT writes and persistent app state |
app/network/ |
The cipher benchmark the rig asks for, and what the kernel does for usbnet.prx |
app/session/cable.c |
Wi-Fi or the USB cable: when to ask, and the carried plugin installed, turned on and loaded |
app/lib/pspkit-https/ |
HTTPS, the entropy pool and the sweep's stick step, as a submodule |
app/audio/, app/video/ |
Audio and video playback |
app/util/ |
Storage paths, PBP access and runtime helpers |
app/util/files.c |
What Data reads off the stick: sources, installs, INBOX, the client's own files, filled into the view gui/files_view.c draws |
dev/tests/ |
Host tests: the parsers, the records and the installer, with power cuts |
dev/ |
Developing PSPDX: start, release, the mock catalog, the emulator settings, and the generators (marks/, render-music.c, sweep-trace.py) |
dev/rig |
The rig: one emulator run with scripted keys, leaving the log and screenshots |
dev/soak/ |
Soak campaigns in the emulator, checked against a model of the client |
dev/localcat/ |
The loopback catalog with its throwaway CA |
dev/testdata/ |
Input traces for the entropy screen |
dev/tools/ |
The EBOOT media scripts and the PSMF wrapper, for any PSP app's ICON1 and SND0 |
Tests · host, emulator soak
sh dev/tests/run- Needs a C compiler, Python, cJSON, zlib, OpenSSL and libpng development files
- Runs the client code and the PNG decoder with address and undefined-behavior sanitizers
- A filesystem adapter simulates power cuts, failed calls, a full or read-only stick and any network answer (
FAULT,FAILAT,STICK_BYTES,RO_MATCH,URL_MAP)
python3 dev/soak/run.py --runs 100 --seed 1
python3 dev/soak/run.py --perf 20
python3 dev/soak/run.py --edge 30
python3 dev/soak/scenarios.py --seed 1 --run 7 # print one input sequencemock catalog -> fixture build with a local CA -> scripted input in PPSSPP -> compare the stick with the model
- Needs Docker, the PPSSPP Flatpak and user systemd services
- Failures land under
dev/soak/results/ - Fixture builds are for local testing only
- Neither replaces real PSP storage, WLAN and power-loss testing
Icons and EBOOT media · glyphs, ICON1, SND0
From the repository root:
sh dev/marks/render.sh # SVG -> PNG
python3 dev/marks/embed.py # PNG -> app/gui/marks_data.h
sh dev/tools/eboot-media/make-icon1.sh demo.mp4 app/assets/icon1.pmf
sh dev/tools/eboot-media/make-snd0.sh theme.wav app/assets/snd0.at3dev/tools/ is for anyone packaging a PSP app, PSPDX included: the two scripts make the ICON1 and SND0 a PBP carries, and dev/tools/mp4-to-psmf.c wraps an MP4 the way the client does.
- Sources in
app/assets/marks/src/ - Rendering needs
rsvg-convertand ImageMagick; packing needs only Python - SVGs, PNGs and the atlas header are committed, so normal builds need no graphics tools
- A new glyph needs matching entries in the generator's
ORDERandenum mark
Video (ICON1.PMF) |
Audio (SND0.AT3) |
|
|---|---|---|
| Needs | ffmpeg, C compiler | ffmpeg, Python, atracdenc |
| Default | 6 s at 144×80 | 18 s ATRAC3 at 132 kbps |
| Knobs | DURATION, FPS |
DURATION, BITRATE 132 or 66 |
Both scripts take a start offset as their third argument.
- PSPDX standard — the
.pspdxandcatalog.jsonfiles PSPDX reads; read the specification - PSPDX Catalog — the main catalog: rebuilds itself every hour and fetches the latest releases; browse it
- Demo app — a minimal homebrew with its own
.pspdx - Abandoned demo — a homebrew without a
.pspdx, listed by the catalog - Releases —
pspdx.zip
Feedback is very welcome!
