What each loader does with obSCEne
Results per loader, from the reports themselves. EMULATORS.md says what each loader is and how to run it.
The table is generated by obscene-tool compat, and
verify.sh fails if it differs from the reports it was built
from.
Results
| host | shadPS4 | PS5PCEM | fpPS4 | kyty | orbistoun | |
|---|---|---|---|---|---|---|
| Records | 36559 | 36575 | 36432 | 194 | 36520 | 36549 |
| Ran to the end | yes | yes | yes | no | yes | yes |
| Output channel | host | sceKernelWrite | write | - | puts | sceKernelWrite |
| Pass / partial / fail / skip | 116/15/358/26 | 456/7/37/15 | 118/68/262/67 | - | 429/11/45/30 | 416/16/56/27 |
| Excluded to keep the loader alive | 0 | 0 | 0 | 0 | 0 | 0 |
| Skipped after dying on a previous run | 0 | 4 | 0 | 19 | 11 | 3 |
| Capabilities established | 7 | 9 | 8 | - | 7 | 6 |
| Checks blocked behind a missing one | 3 | 3 | 0 | - | 12 | 3 |
| Deepest wholly-green section | 20 | 25 | 20 | - | 25 | 4 |
| Census present / absent | 826 / 34505 | 35330 / 0 (void) | 3732 / 31598 | 0 / 0 | 35330 / 0 (void) | 35330 / 0 (void) |
| Probed functions responding / silent | 54 / 0 | 22 / 32 | 0 / 0 | 0 / 0 | 26 / 28 | 31 / 23 |
| Died in | 900-surface/control |
000-boot/number-formatting |
000-boot/number-formatting |
000-boot/number-formatting |
000-boot/number-formatting |
000-boot/number-formatting |
Sections the loaders do not agree on, pass/partial/fail/skip:
| Section | host | shadPS4 | PS5PCEM | fpPS4 | kyty | orbistoun |
|---|---|---|---|---|---|---|
000-boot |
4/0/0/0 | 4/0/0/0 | 4/0/0/0 | 4/0/0/0 | 3/0/1/0 | 2/2/0/0 |
005-generation |
0/1/0/1 | 1/1/0/0 | 1/1/0/0 | 1/1/0/0 | 1/1/0/0 | 1/1/0/0 |
007-responsive |
2/0/0/0 | 0/2/0/0 | 0/0/0/2 | 0/0/0/2 | 0/1/1/0 | 0/2/0/0 |
010-kernel |
3/0/0/0 | 3/0/0/0 | 3/0/0/0 | 2/1/0/0 | 3/0/0/0 | 3/0/0/0 |
015-sync |
11/1/0/2 | 12/1/1/0 | 11/0/1/2 | 12/0/1/1 | 7/2/4/1 | 5/0/7/2 |
017-posix |
4/0/0/1 | 1/0/4/0 | 0/0/0/5 | 0/0/0/5 | 2/0/3/0 | 0/0/4/1 |
018-relational |
18/0/0/0 | 18/0/0/0 | 17/0/1/0 | 16/0/1/1 | 14/0/1/3 | 8/0/2/8 |
020-memory |
4/0/1/4 | 9/0/0/0 | 8/0/0/1 | 9/0/0/0 | 8/0/0/1 | 2/0/3/4 |
030-thread |
3/0/0/0 | 3/0/0/0 | 3/0/0/0 | 3/0/0/0 | 3/0/0/0 | 2/1/0/0 |
035-libc |
30/0/0/0 | 5/1/19/5 | 0/0/0/30 | - | 0/1/15/14 | 10/1/16/3 |
037-math |
13/0/0/0 | 3/0/10/0 | 0/0/0/13 | - | 0/0/13/0 | 0/0/13/0 |
040-file |
5/0/0/2 | 5/0/1/1 | 6/0/1/0 | - | 6/0/1/0 | 2/2/3/0 |
050-time |
1/0/0/0 | 1/0/0/0 | 1/0/0/0 | - | 1/0/0/0 | 0/0/1/0 |
060-module |
3/0/0/0 | 3/0/0/0 | 3/0/0/0 | - | 1/0/1/1 | 2/0/1/0 |
070-user |
0/0/2/0 | 2/0/0/0 | 2/0/0/0 | - | 2/0/0/0 | 0/0/2/0 |
080-video |
1/0/0/2 | 0/0/0/3 | 1/0/0/2 | - | 0/1/0/2 | 2/0/0/1 |
090-audio |
1/1/0/0 | 2/0/0/0 | 2/0/0/0 | - | 2/0/0/0 | 1/1/0/0 |
100-input |
1/1/0/1 | 3/0/0/0 | 3/0/0/0 | - | 2/1/0/0 | 1/1/0/1 |
110-modules |
1/0/0/3 | 2/0/1/1 | 1/0/0/3 | - | 0/0/1/3 | 1/1/1/1 |
120-measure |
4/0/0/0 | 3/1/0/0 | 4/0/0/0 | - | 3/1/0/0 | 1/1/2/0 |
130-layout |
3/0/0/1 | 3/0/0/1 | 0/2/0/2 | - | 0/2/1/1 | 2/1/0/1 |
150-memory-map |
1/1/0/0 | 2/0/0/0 | 0/0/2/0 | - | 0/0/2/0 | 1/0/0/1 |
165-gnm |
0/0/0/2 | 2/0/0/0 | 0/0/0/2 | - | 2/0/0/0 | 1/1/0/0 |
900-surface |
2/10/355/3 | 368/1/1/0 | 47/65/257/1 | - | 368/1/1/0 | 368/1/1/0 |
Real hardware
The table has no hardware column. A retail console is the reference
every loader in the table is measured against, not one of the loaders.
Its results are in HARDWARE.md, each citing a
record in data/hardware/. ./bin/obscene report
and ./bin/obscene deploy capture a run from the
console.

Reading the table
- Not a ranking. A loader that resolves every import to a generic stub scores well on presence and badly on behaviour. A loader that resolves only what it implements scores nothing on presence. A loader that refuses to run scores nothing while still saying something true about the module.
- Void census.
900-surface/controlprobes one symbol that must resolve and one that cannot exist. A loader that resolves everything answers "present" to the impossible one, the control fails, and every count in that section is meaningless. The table marks such a count(void)rather than hiding it. - Blocked checks. "Checks blocked behind a missing one" counts the suite sitting behind a missing capability. Resolving more symbols cannot move it, so it is the number to steer by (D067).
- Per-section tallies. Failing everything in one section and failing one check in each of eight describe different platforms, so the tallies stay per section.
- Stalled runs. A column without an end record is where the run stopped. Sections it did not reach were not attempted and are not absences. fpPS4 binds every unresolved import to a handler that never returns, so its stock run stops at the first missing function.
- Declared dependencies. obSCEne lists the platform
libraries it uses in
DT_NEEDED, as a title does. A firmwareless loader that cannot satisfy a declared dependency may refuse the module; that is the loader's policy, not a malformed module.
Patched loaders
patches/ holds local changes to Kyty and fpPS4 that
return an error or zero where the stock loader stops the process on an
unrecognised input. A report from a patched loader goes to
reports/<loader>-patched.txt and never occupies that
loader's column (D176). A patch that makes an unbound function return
zero also turns an honest failure into an apparent success: under the
patched Kyty, obSCEne reports a ready display for a display that cannot
present a frame.
Behavioural census
910-bulk calls every censused symbol with all arguments
zero and buckets what came back. A function taking an out-pointer or a
handle cannot succeed on zero, so the correct answer for most calls is a
refusal. A library that answers zero to every call is resolving its
imports to stubs. A library with a high refusal rate is validating its
arguments, and matching it means matching real behaviour. A single zero
is not proof of a stub: some functions succeed on zero arguments, and OUTPUT.md states that the bucket cannot separate
the two.
Screenshots
obSCEne draws its report to the screen for the case where no text channel works. A capture usually lands on the summary page; once the report is complete the presenter cycles pages, so the page captured depends on when the run finished.
scripts/screenshot.sh captures with
PrintWindow and PW_RENDERFULLCONTENT, which
renders a window that is covered or never shown; without the flag a
GPU-composited window captures as a blank rectangle. It enumerates
top-level windows by owning process id, prefers a visible one, and falls
back to any window larger than 64x64, because a loader that opens its
window from a render thread has no MainWindowHandle.
shadPS4

The 900-SURFACE bar is mostly green because shadPS4
resolves nearly every censused symbol, including libraries it does not
implement.
PS5PCEM

The header reads GEN 5 (agc). The display takes
whichever video-out pair the platform has, preferring the older when
both appear present (D140). 900-SURFACE is mostly red,
because PS5PCEM resolves what it implements and nothing else.
fpPS4

A detail page showing individual 035-libc checks. The
exclusion list names 035-libc checks individually rather
than the section, because some of them return and some do not. The
header reads GEN UNKNOWN: fpPS4 installs a logging stub for
every unresolved import, so both generations' graphics symbols appear
present, and presence then proves nothing (D140).
Kyty

Kyty re-reads a registered framebuffer from guest memory only when
its GPU submission counter advances. obSCEne draws with the CPU and
submits nothing, so the stock loader shows a black window; the local
patch covers it. Kyty names every import it cannot resolve, which a
stub-everything loader cannot do. Stock Kyty's text channel does not
reach the report: its sceKernelWrite requires an opened
file, and it registers puts under a different library name
from the one this module imports.
orbistoun
orbistoun has no screenshot. obSCEne opens its display against the
initial user, and orbistoun answers both user-service calls with its
Unimplemented placeholder 0x7fff0001, so obSCEne opens no
display:
070-user/initialise fail 0x7fff0001 the user service refused to initialise
070-user/initial-user fail 0x7fff0001 no initial user could be determined
OBS|display|absent|no initial user, even after initialising the service
The loaders that implement sceUserServiceGetInitialUser
each return a different value, so none of them is evidence of what
hardware returns.
Regenerating
Each loader gets its own build directory. The exclusion list lives
beside the module and is per loader, and
scripts/bulk-sweep.sh and scripts/sweep.sh
write different kinds of exclusion into the same file.
# One directory per loader. --resume keeps a list already established.
sh scripts/sweep.sh --build /tmp/obs-shad --generation 4 \
--emulator <emulators>/shadps4/shadPS4.exe --out reports/shadps4.txt
sh scripts/sweep.sh --build /tmp/obs-fp --resume --generation 4 \
--emulator <emulators>/src/fpPS4/fpPS4.exe --out reports/fpps4.txt
# Loaders needing no exclusion walk run directly against a built module.
VM_MODULE=/tmp/obs-orb/obscene.module.elf sh scripts/run-emulator.sh \
--emulator <emulators>/src/PS5PCEM/zig-out/bin/game-run.exe --out reports/ps5pcem.txt
VM_MODULE=/tmp/obs-orb/obscene.module.elf sh scripts/run-emulator.sh \
--emulator <OOPS>/orbistoun/target/release/orbistoun-cli.exe --out reports/orbistoun.txt
# Kyty has its own runner: the report cannot arrive, the unresolved list can.
VM_MODULE=/tmp/obs-orb/obscene.module.elf sh scripts/run-kyty.sh --out reports/kyty.txt
cd tool && cargo run --quiet -- unresolved --root .. /tmp/obscene-kyty/_kyty.txt
cd tool && cargo run --quiet -- compat --into ../docs/COMPATIBILITY.md --write \
"host=../reports/host.txt" "shadPS4=../reports/shadps4.txt" \
"PS5PCEM=../reports/ps5pcem.txt" "fpPS4=../reports/fpps4.txt" \
"kyty=../reports/kyty.txt" "orbistoun=../reports/orbistoun.txt"