D582 - The clock a guest reads has to repeat, and still move

Status: measured Date: 2026-09-08

The bug, stated as the project's own requirement

Two runs of one build placed the same reservation at 0x7400047e0000 and 0x740004830000, and a pointer PPSA03416 hands to sceKernelAprSubmitCommandBufferAndGetResult alternated between 0x740009100000 and 0x740009200000 across four runs. D181 and D238 require every measurement here to survive a repeat. This one did not, and it is why the buffer at that pointer had to be read by typing an address out of a previous run and hoping (D580).

Two things orbistoun handed the guest were different on every run.

A thread handle was a host heap address

next_handle returned Box::leak, so scePthreadSelf answered 0x23b5e3ade80 one run and 0x22841ab0610 the next. D151 established that a handle must be a real, aligned, zeroed block the guest can dereference - an opaque integer reproduced a fault at a low address - and nothing in that reasoning requires the host allocator to choose where.

So the blocks come from CONTROL_BLOCK_BASE, in the 0x0000_5E2* family docs/ADDRESS_MAP.md keeps for regions of orbistoun's own invention, bump-allocated in order of registration. Handle n is the same address in every run.

The fallback is not silent. A run whose reservation failed uses the host heap again and has lost the property, so handles_repeat says which happened rather than leaving a reader to infer it from addresses that look equally plausible either way.

The clock was the host's, and D256 had decided that deliberately

Not the same clock as the run's call budget. That one exists to make a run reproducible; this one is a value the guest reads and branches on, and pinning it would stop any title that waits for time to pass (D256).

That is right about pinning and it is not the only option. A clock that repeats does not have to be a clock that stands still. One that advances by a fixed step per reading does both, and that is what D256 did not have in front of it - so this supersedes its conclusion rather than its reasoning.

One microsecond per reading: small enough that a guest timing its own work reads a plausible number, large enough that a spin-wait on a millisecond ends in a thousand readings rather than a million. A sleep advances it by what was asked for, so waiting for real durations still works. The wall clock is a fixed, obviously synthetic epoch plus the same elapsed time, so a guest that formats a date gets the same string every run.

Default logical, ORBISTOUN_CLOCK=host for when the question is how long something really took. The default is the one that makes a run comparable, because a measurement that cannot be repeated is not one.

One source, or the fix is the half that looks like a fix

orbistoun-kernel kept its own Instant for GetProcessTime and another for the tick counter, so the platform's process time, its counter and POSIX's monotonic clock were three clocks. All three read clocks::since_start_nanos now. A guest converting between them lands where it expects, and one setting decides whether all of them repeat.

The same hazard bit the sleeps: orbistoun-libc's three were taught to advance the clock and orbistoun-kernel's usleep was not, which is the wiring hazard this project keeps paying for. The libc three go through one slept.

Measured

Two runs of PPSA03416, comparing the mapping sequence (D581):

identical mappings
ORBISTOUN_CLOCK=host 1 of 49
logical clock 19 of 47

The wall is unchanged - 193 imports, the same fault, the same one read of zero bytes - which is the correct outcome for a change that alters what the guest is told and not what it is given.

What is left, and it is not fixable here

The divergence now begins exactly at the first scePthreadCreate, which the mapping record names. Guest threads are real host threads - principle 6, and the reasons are not negotiable - so their interleaving is the host scheduler's, and two runs will not agree about the order two threads reach an allocator.

Reproducing that would mean scheduling guest threads deterministically, which is a different emulator. What is achievable is that everything before the first thread repeats, and it now does, address for address and call for call.

What this does not establish

That the run is deterministic. It is deterministic up to the first thread and not after it, which is a real improvement and not the property. A title that maps everything before spawning would be fully repeatable; this one does not.

Nor that the logical clock is right for every guest. A title that measures how long its own work took now reads a number derived from how many times it asked, not from any real duration. Nothing observed depends on that, and a title that did would need host and would then be unmeasurable - which is a trade to make knowingly rather than a property to rely on.

Nor that these were the only two host values reaching the guest. They are the two that were found by diffing a mapping sequence. The others, if any, would surface the same way.