D526 - A positioned read took the guest into the GPU, and the wall finally moved

guest-observed - 2026-09-03 (A/B by unregistering, three samples)

sceKernelPread was undeclared. descriptor::read_at - the positioned read itself - has been built the whole time, used by POSIX pread. One declaration and one wrapper later:

without   188 distinct, read of 0xa0 at image+0x1389269
with      197 distinct, read of 0x50 at image+0xf56e09

The fault moved. It had been at the same instruction since D515.

What it opened

sceAgcCreateShader                    sceAgcDriverAddEqEvent
sceAgcDcbResetQueue                   sceAgcDriverSetHsOffchipParam
sceAgcDcbSetIndexSize                 sceAgcDriverSetTFRing
sceAgcDcbWaitUntilSafeForRendering
sceKernelClockGettime   _sigprocmask   sceKernelWaitEqueue

The graphics pipeline. Shader creation, command-buffer setup, driver ring configuration. Reading its own asset files is what stood between this title and its renderer.

sceKernelWaitEqueue appearing is worth noting on its own: D524 built the queue's identity and deliberately built no delivery, because nothing waited. Something waits now.

The wall the last five ticks orbited is behind us

The new fault's call chain is image+0xf269e7 - the return address of the call at 0xf269e2, which is the memory-manager initialiser. So 0xf23970 returns now; the module no longer invokes the scope hook before the memory manager exists.

That is retrospective support for the reading D519 left open and D521 narrowed to one: the module was on a path it would not take on hardware, and what put it there was upstream - a file call that did not answer. It is support, not proof. Nothing measured the console; what is measured is that answering two file calls made the symptom go away.

ORBISTOUN_RETURN is a return-value experiment, not an implementation switch

The first A/B here forced sceKernelPread to the placeholder and reported 197 either way - which read as "the answer does not matter". It is wrong, and the reason is worth writing down:

forcing a return does not stop the implementation running. The bytes still landed in the guest's buffer; only the count was replaced. For a function whose effect is a side effect, this simulates nothing.

D525's A/B on sceKernelStat was valid for the opposite reason - a guest branches on whether stat succeeded, so replacing the return replaced the thing being tested. The distinction is between a call whose answer is the product and a call whose side effect is.

The honest A/B is to unregister the implementation and rebuild, which is what the numbers above come from.

What is shared and what is not

The positioned read is descriptor::read_at, shared with POSIX pread - it reads at an offset without moving the descriptor's position, which is the whole point and is why it is not a seek, a read and a seek back. Failure differs, as it does for stat (D525): the vendor EBADF sign-extended, which obSCEne measured for read, lseek and write against a bad descriptor (D439), never -1.

The test breaks on the position, not the bytes: a seek/read/seek-back passes the byte assertion and fails the next one. Watched failing.

And a correction: I read a capped list as a complete one

The run report prints six findings and ends with ... and 23 more. I read the six as the whole set and said "three stubs left" twice. The trace has 28 unimplemented functions.

The tool was honest - it says how many it withheld, on the line after. This is check 4 turned on its reader rather than on a filter: a printed list is a fact about the printing. The real work list is the trace, and reading it is what found sceKernelPread at all.