Design
Why obSCEne is shaped the way it is: what it announces before it acts, why it is written from the failure side, how the sections are ordered, and what it refuses to invent.
These were the middle of the README, which made a reader wanting the build instructions scroll past a design essay to reach them. The reasoning is worth keeping and worth keeping here.
See also OUTPUT.md for the report format itself, and WORKFLOW.md for the loop this feeds.
Announce before attempting
The single most important property. Every check prints its identity before making the call:
OBS|try|020-memory/map|libkernel|sceKernelMapDirectMemory
OBS|res|020-memory/map|pass|0x8804000000|
Under an emulator the normal outcome of an unimplemented function is
a hard crash that takes the process with it. When that happens the
stream simply stops - and the last line names the exact call that did
it. A report that ends on a try is not a truncated report,
it is a stack trace with one frame and no debugger required.
Everything else in the program is arranged around keeping that property true.
Checked from the failure side
Much of the suite passes deliberately invalid arguments and expects an error:
int rc = sceKernelClose(-1); /* must fail */This needs no struct layouts - guessing at one corrupts the stack, and the crash lands nowhere near the mistake - and it still proves the function exists, is reachable, validates its arguments, and returns something plausible. That is exactly the set of things an implementation returning a constant gets wrong.
The honest limitation: an implementation that fails everything passes every negative check. Negative checks prove argument validation; only the positive ones prove the function does its job.
That is why 035-libc and 037-math matter
out of proportion to their size. The C runtime is the one library whose
whole interface is ISO C, so every signature is certain and every check
can ask whether it works: calloc must return
zeroed memory, realloc must preserve contents,
qsort must actually sort, snprintf must report
the length it would have written. A stub returning success fails all
four. The maths section is the same idea with no tolerances anywhere -
every value checked against is exactly representable, so
floor(-1.5) must be -2.0 and nothing else. An
epsilon is where a wrong answer hides.
Shifting the balance from checks this project reasoned out to checks a public document settles is the main direction of travel - the current split is in the status block above, and the reasoning is in docs/BACKLOG.md.
018-relational takes the same idea somewhere no document
reaches. It compares results to each other rather than
to an expected value: two live event flags must not share a handle, a
counting semaphore must refuse a third claim against two signals, memory
released must be allocatable again. Those hold whatever the platform's
actual numbers are, so they need no authority to check - which is what
makes them usable on the vendor surface, where no authority exists.
Reading a report
The binary emits one machine-readable format. Colour and grouping are presentation and live in a script, so the guest stays small:
make pretty # colour, grouped by section
./build/obscene-host | obscene-tool prettyThe format contract is docs/OUTPUT.md. Treat it as an interface - parsers exist.
The loop this exists for
make host && ./build/obscene-host > baseline.txt
# ...change the emulator...
make diff BASELINE=baseline.txt improved 037-math/sqrt: skip -> pass
value 900-surface/libc: 0x39 -> 0x4d
7 improved, 0 regressed, 70 unchanged
tally: pass +7, skip -7
Exit 0 means no regressions, 1 means something got worse. A regression is a check that got worse, not a check that is failing - under an early emulator almost everything fails, and the only useful question is whether today beats yesterday.
skip ranks below fail: a check
that stopped running tells you less than one that ran and failed, so
losing coverage counts against you. A check vanishing from the report
entirely counts the same way - otherwise deleting an awkward check would
read as progress.
Sections, base to high level
Order is the whole value of the report: a failure at the top is read before a failure at the bottom, because the bottom depends on the top.
| Section | Establishes | |
|---|---|---|
| 000 | boot | The report itself can be trusted |
| 005 | generation | Which generation this is, inferred from exclusive symbols |
| 010 | kernel | Process identity and the clocks |
| 020 | memory | Reserve, map, use, unmap, release |
| 030 | thread | A thread that actually runs its body |
| 035 | libc | The C runtime, checked for behaviour rather than presence |
| 037 | math | Floating point, where sign and rounding direction break |
| 040 | file | Descriptor handling |
| 050 | time | Sleeping with the clock still monotonic |
| 060 | module | Dynamic linking |
| 070 | user | The identity subsystems are opened against |
| 080 | video | Acquiring the display output |
| 090 | audio | Bringing up audio |
| 100 | input | Acquiring a controller |
| 900 | surface | A census of the whole known surface - presence only |
A check whose prerequisites were not met is skipped, not failed. One broken allocator would otherwise turn everything below it red and bury the one real fault. The same applies to a check whose symbol the loader could not resolve - every platform declaration is weak, so an absent function is skipped rather than jumped to.
Presence and behaviour are different questions
The behavioural sections ask whether a function works, and each one costs a confident signature. The census asks only whether it exists, which costs a name - so it scales to the whole platform, and it is where the honest coverage number comes from.
The census never calls anything, and one section deliberately
does. Every censused name is declared as data rather than as a
function, so the type system forbids the call outright - calling a
function whose signature you do not know is the mistake this project
refuses to make, and forty thousand names would otherwise be forty
thousand chances to make it. 910-bulk steps around that on
purpose, by casting an address rather than redeclaring the name, so the
exception stays inside one expression and every other translation unit
still cannot call these by accident. It is compiled in only under
OBS_BULK and it is the one part of the program expected to
end the process.
Presence is a statement about the loader as much as the
platform. shadPS4 resolves every import through a generic stub
and reports 35,337 of 35,337 present for libraries it
does not implement; PS5PCEM resolves what it implements and names the
other 31,601 absent. The higher number is the less honest one. Read the
census beside 007-responsive, never alone - and see
910-bulk, which exists because a resolved address and an
implementation are not the same claim.
A wrong name in the census is a false negative - visible, harmless, correctable. A wrong arity in a behavioural check corrupts the stack and crashes somewhere unrelated. That asymmetry is why the census can cast a much wider net.
900-surface/control runs first and validates the
test itself, probing one symbol that must resolve and one that
must not. On a platform implementing none of the surface, "everything
absent" and "the presence test is broken" look identical - the control
is what tells them apart.
Nothing here is invented
Every declaration in include/obscene/platform.h is a signature this project is confident about. Where an arity or a struct layout is uncertain, the function is left out rather than guessed at.
The symbol and library strings are ABI identifiers - the import hash is computed from the symbol name, so renaming them for tidiness would stop this testing anything. They stay exactly as the platform spells them. Prose elsewhere avoids vendor branding.