The loop

One turn of orbistoun's work, start to finish: what each step does and who performs it. WORKFLOW.md is the command reference - what to type, with which flags.

Every design choice in this repository exists to make one turn cheap, or to make its output worth reading.

Overview

flowchart TD
    A["a title's eboot.bin, in the title library"] --> B["./bin/orbistoun run TITLE"]
    B --> C["build the link plan, or reuse the stored one;<br/>list what the module imports<br/>- by 64-bit hash, not by name"]
    C --> D{"does the hash<br/>have a name?"}
    D -- yes --> G
    D -- no --> E["generate candidates, and read<br/>identifier-shaped strings out<br/>of the module's own bytes"]
    E --> F{"does a candidate<br/>hash to it?"}
    F -- yes --> N["record the name, and add its<br/>words to the vocabulary"]
    N --> G
    F -- no --> G["build a stub for everything<br/>not implemented"]
    G --> H["execute the guest natively"]
    H --> I["every system call lands in orbistoun"]
    I --> J{"implemented?"}
    J -- yes --> K["do the real thing"]
    J -- no --> L["record the call, dump the arguments,<br/>answer 'unimplemented'"]
    K --> M
    L --> M["the guest faults, gives up,<br/>or hits the time limit"]
    M --> O["write the trace;<br/>compare it with last time"]
    O --> P["print FURTHER / same / BACK,<br/>then the ranked findings"]
    P --> Q{"orbistoun-turn runs the<br/>mechanical steps, then stops"}
    Q -- "a thread waits<br/>on one call" --> W["the wait sub-loop:<br/>wait, word, writer, cause"]
    W --> P
    Q -- "a policy patch moved it<br/>and no title went BACK" --> Z["staged as guest-observed;<br/>its measurement filed"]
    Z --> B
    Q -- "wall hit<br/>(architectural / regression)" --> X["a person reads the findings<br/>and decides what next"]
    Q -- "enough is known already" --> R["a person writes the Rust<br/>implementation (known_by: published)"]
    Q -- "not enough is known" --> S["a person writes and runs an<br/>obSCEne probe on real hardware"]
    S --> T["a person reads the transcript<br/>orbistoun-cli probe reports on"]
    T --> R
    R --> U{"trace verdict"}
    U -- FURTHER --> B
    U -- "same / BACK" --> X

    classDef human fill:#7a2f2f,stroke:#d08a8a,color:#fff
    classDef agent fill:#2f4f7a,stroke:#8aafd0,color:#fff
    class X,R,S,T human
    class Q,U,W,Z agent

Naming, stub-building, execution, tracing, the progress verdict, the mechanical sweeps of orbistoun-turn, resolving a waiting thread to the call it waits on, and staging a policy patch that moved a wall run unattended. Deciding what a finding means and writing the implementation are a person's job. Nothing in this repository dispatches a hardware probe, drives obSCEne or Prosperous, or writes Rust from a measurement: orbistoun-probe only reads a transcript, and orbistoun-propose has no implementation proposer.

The first rule: the title is not the bug

Every title the operator provides runs on real hardware, so every wall is orbistoun's (D708). "Debug build", "devkit path", "faithful ENOENT" and "the title's own assertion" are restatements of a symptom orbistoun has not explained, never reasons to stop.

Before instrumenting a fault, check the upstream fact it rests on. A guest that faults after a failed open failed because orbistoun misrouted the path or did not serve the mount, until the file is shown absent from the place the run reads: the resolved data directory, which orbistoun-cli paths prints.

Steps

Once

  1. A title's directory goes into the title library - the one shared titles/ under the data directory that every OOPS tool reads, outside this repository.

Every turn

  1. ./bin/orbistoun run <title-id> rebuilds, refreshes names if they are stale, runs the guest under a time limit, and reports.
  2. orbistoun parses the container, links it, and checks the link plan against the one stored for the title; a plan that differs under the same executable, loader and host CPU is a loader defect and heads the findings (D724). It lists every system function the module imports.
  3. The imports are 64-bit hashes rather than names, because that is how the guest links, so each is looked up in the symbol database.
  4. For hashes nothing names, the name search generates candidates from a grammar and reads identifier-shaped strings out of the module's own bytes.
  5. A candidate is accepted only when it hashes to the import. The hash is the proof, so a reported name is never a guess (see PROVENANCE.md).
  6. A confirmed name is written to symbols/generated.json and its words join the vocabulary, so the next module needs less searching.
  7. orbistoun builds a stub for every declared function it does not implement, from the stub policy. The policy is data: changing an answer costs a relaunch, not a rebuild.
  8. The ELF loader reserves the address space and applies the plan: it places the module, writes the relocations and any instruction rewrites (D725), sets up TLS, attaches the current handlers and stubs to the thunks, and jumps to the entry point. The title's recorded shaders are translated into its cache before that jump (D113), so a shader that will not translate is a finding before a guest instruction runs.
  9. The guest's machine code executes natively - same architecture, no translation - until it calls out.
  10. Every call to a system function lands in orbistoun instead of the platform's operating system.
  11. Implemented functions do the real work. Unimplemented ones record the call, dump the arguments and answer "unimplemented".
  12. The guest faults, exits, or reaches the time limit or call budget. All of these are results, and each writes a trace.
  13. The trace is written to disk keyed by module, so a sweep leaves one per title.
  14. The trace is compared with the previous one for the same module, giving FURTHER, same or BACK. When the two runs applied different link plans, the verdict says so, so a loader change is never read as the effect of an implementation.
  15. The findings are printed, ranked worst first: what went wrong, the evidence, and what to do about it. After them, under "on the host", come the gaps no guest call shows: audio no device played, a flipped buffer the window could not show, pad ports set to a gamepad nothing reads. They rank below every wall, since a silent title still runs, and are always printed, since nothing else names them.
  16. A person reads the top finding. For an unimplemented function, orbistoun-turn reads the structure it was handed and runs the title again with it answering zero: a run that gets further says the answer is what holds the guest back, and the console's answer is the measurement to request (D832). orbistoun-turn has already run the mechanical sweeps against it, and a thread waiting on one call has already been resolved to the call it waits on (see the wait sub-loop). If what is left is an unmeasured interface or structure, the next step is a hardware probe question rather than a guess, filed as a census row batched with the other unmeasured facts of its area (see the capability sub-loop).
  17. A person writes the implementation - from a published source, or from what an obSCEne probe measured on the hardware - and tags it with its known_by. A policy patch the loop proposed needs no person: one that moves its title and no other title back is staged (see staging what the loop proves).
  18. Go to step 2. FURTHER keeps the change; same or BACK does not.

A question answered once by hand becomes a command or a report section the next run answers by itself. The loop has no scratch scripts: a diagnostic worth running twice is orbistoun's code, tested against the wall it first found.

Who does what

Step Who Automatic
2 - run a person types one command yes
3-4 - imports, name lookup orbistoun yes
5-7 - name search, vocabulary widening orbistoun yes
8 - stubs orbistoun yes
9-12 - load, execute, intercept, dump orbistoun yes
13-14 - a trace on every outcome, including a spinning guest orbistoun yes
15 - progress verdict orbistoun yes
16 - ranked findings orbistoun yes
17 - mechanical sweeps (orbistoun-turn) orbistoun yes
17 - resolve a waiting thread to the call it waits on orbistoun yes
18 - stage a policy patch that moved a wall and no title back orbistoun yes
17 - interpret a finding, design a probe a person no
17 - write and run an obSCEne probe on the hardware a person, via pros no; orbistoun-cli probe reads the transcript afterwards
18 - write the implementation a person no
record what a function must answer orbistoun, into learned.toml yes
record what was learned by hand a person, via learn no, deliberately
rank what no measurement covers, corpus-wide orbistoun, via census yes
file a census batch to obSCEne, with predictions a person no
check a resolution against the rows it cites orbistoun, via resolutions yes
record what the title reached compat record, prompted by the run prompted

Step 17: the dispatcher

orbistoun-turn maps each kind of finding the report names to a fixed next step and runs the ones that are mechanical: the argument sweep, the other diagnostic axes, a watchpoint, a naming attempt, a region handed to the guest when a sweep shows one missing. It stops at the steps that are a person's, each with a sentence saying why.

It is a dispatcher, not a chooser. A boot against a wall costs a fraction of a second and a model's answer costs several seconds, so running every candidate experiment is cheaper than choosing between them (D231).

The argument sweep is two-dimensional. It crosses a planted sentinel with what the call is forced to return, because a guest may check the return before it reads the out-parameter, and then neither intervention alone changes anything (D286). It hands back a slot, an offset and the condition it holds under.

A fault on a computed address composes two diagnostics, neither of which reads the guest's code (D276):

fault at an address the guest computed
  -> snapshot the structure it was computed from   (which words are still zero?)
  -> arm up to four of those words                 (who reads them, and from where?)
  -> a named instruction offset, in a named region

The result is an instruction offset, which is a finding; what should have filled the slot is step 18. The variables behind both are in WORKFLOW.md. A waiting thread composes the same two diagnostics another way, in the wait sub-loop.

Step 18: the implementation

Nothing here writes Rust. Each implementation carries a decision - what to leave in the caller's buffer, whether to trust a number the guest supplied, whether refusing beats guessing. A policy entry is different: it is one line of data, and one that earned its place is staged by the loop (D832).

A generated implementation is admissible only as a proposal a person reads, gates and merges. It arrives as an inert patch in a submission, carries an oracle like every other fact, and is merged only by somebody willing to say where the behaviour came from (D322). See TESTING.md.

Walls

There is no separate escape mechanism. A wall is handled by the same verdict and findings:

Reading a turn's output

orbistoun: guest fault: write to 0xfffe0 while executing at 0x400000afc959 (image+0xafc959)
reached    ContainerParsed
reached    ImportsResolved
reached    Mapped
reached    Linked
reached    Entered
outcome    Crashed { signal: "access violation - the guest dereferenced unmapped memory" }

progress
  imports  23 distinct (+0), 222 calls (+0)
  standing 213 of 222 calls answered by an implementation (4% on stubs)
  fault    image+0xafc959
  verdict  same     nothing moved
  abi      222 calls, all on a conforming stack
  faulted  write to 0xfffe0
           rax=0x0 rcx=0xfffe0 rdx=0x100000 rdi=0x4000019e9ca0
           called from 0x400000f5778a <- 0x400000000064 <- 0x4000000000a2

what to do about it
  ! the guest faulted at image+0xafc959, write to 0xfffe0
      write to 0xfffe0 is an address in no region this run mapped
      rax=0x0 rcx=0xfffe0 rdx=0x100000 rdi=0x4000019e9ca0
      just before: libkernel::0x6abac2f3dc6f8cee(0x600000800d38) from 0x400001595d8b
    -> read the calls just before it and the arguments they were given - the value
       that became this address was answered by one of them
  ! libkernel::0xd652cde431670c7e was called 2 times and has no name
      arg0 = 0x7
      arg3 = 0x400001aa8d28 -> image+0x1aa8d28 = 00 00 00 00 00 00 00 00 ...
    -> extend the candidate vocabulary and re-run the name search

Every finding ends in an arrow saying what to do. A finding without one is a defect in orbistoun-report::diagnose.

What you see What it means What to do
A name at the top of the worklist with an enormous count A guest spinning on it Implement it; this is the wall
A yield, sleep or poll at the top, from one call site A thread waiting for something that never comes Read its "waits on" finding: the word it reads, who writes it, and the call that was to complete it
library::0x... at the top The same, not yet named Extend crates/orbistoun-names/data/vendor.toml, re-run
A fault instead of a time-out The guest got somewhere and died Read the faulted block and the calls just before it
Many calls spread thinly The guest is progressing Implement the frequent ones and go again
standing fell while calls rose A stub started answering wrongly Check what the new answers unlocked before believing the progress

The wait sub-loop

A run whose time goes to one waiting call - a yield, a sleep, a poll, a timed wait retried - is not asking that call a question. Its answer is not the wall; something the thread waits for is. The wait sub-loop finds it without reading the guest's code (D832, D276):

flowchart LR
    A["one call dominates,<br/>from one call site"] --> B["the wait: the thread, the site,<br/>and the objects its callee-saved<br/>registers hold there"]
    B --> C["the word: snapshot those objects<br/>across iterations; read-watch<br/>the words that never change"]
    C --> D["the writer: write-watch<br/>the word the loop reads"]
    D --> E{"written?"}
    E -- "never" --> F["the cause: the call handed<br/>that object, or a callback in it"]
    E -- "by a thread that<br/>itself waits" --> B
    F --> G["a 'waits on' finding,<br/>ranked with the rest"]
    B --> H{"moves under the<br/>host clock only?"}
    H -- yes --> I["a clock finding:<br/>the thread's time stands still"]
  1. The wait. The run names the thread, the call site and the call, and what the thread's callee-saved registers held: the objects the waiting loop reads.
  2. The word. Snapshots of those objects at successive iterations name the words that never change; read watchpoints on them name the one the loop reads. After the objects, the words the stopped GPU queue never wrote are watched: each value-storing RELEASE_MEM and memory WRITE_DATA from where the queue stopped on, and those of every submission held back behind it. A loop that reads one waits on GPU work that never ran, and its cause is the queue's wall.
  3. The writer. A write watchpoint on that word says whether anything writes it, and which thread. A writer that is itself waiting moves the question to that thread.
  4. The cause. The word's object is joined to the run's calls: the import handed it, a callback in it, a pointer into it. An unimplemented, placeholder or refused call ranks first.

A wait that moves under ORBISTOUN_CLOCK=host and not under the logical clock is a clock finding, whatever it waits on: the thread's logical time does not advance.

The writer step also serves a frame that samples a texture nothing tracked filled - no frame drawn into it, no file read landed on it, no compute program stored over it. While no thread waits, the first such texture's first word is write-watched the same way, and the run names the instructions that wrote it, or says nothing did while it was watched.

The decision sub-loop

A run that reaches a screen and draws the wrong thing there - a black world under a working HUD, a feature whose work is never submitted - is not stopped. The title decided to skip something, and the wall is the input to that decision. Tracing it back one link per run costs a run per link, so the sub-loop answers with interventions and with what the title tells its own scripts:

  1. Read the decision before running anything. orbistoun-cli callers <eboot> <global> lists every instruction that reads or writes a global, and orbistoun-cli disasm <eboot> <offset> shows the condition around one, both in under a second. Most of a gate - which flags it combines, which functions set them, which script natives touch them - is answered here; a run is spent only on the values static reading cannot know, tallied with step 3 at the gate's own instructions.
  2. What never runs. Execute breakpoints (ORBISTOUN_WATCHPOINT=<addr>:x) on the work the frame lacks say which of its entry points run; the summary names each, hit or not.
  3. Who calls it. The first execute hit reports the top of the stack, so an indirect call - a virtual method, a job, a script VM's native dispatch - names its caller from the return address the static caller scan cannot see.
  4. What the scripts asked. <addr>:trace@<reg> on a dispatch site stays armed and tallies a register at every hit; on a script VM's call <reg> that is every native the scripts called, with counts. A native that should run and does not is the decision, in the title's own words. A tally over a whole run is mostly boot and loading frames: count the phase's own frames first (the report's last-minute line) and open the window at its first flip with ORBISTOUN_WATCH_FROM=<flip>, so the values describe the phase the question is about.
  5. Whether it is the cause. ORBISTOUN_HOLD=<addr>:<value>/<width> keeps a value for the whole run; ?<addr>=<value> writes it only while a guard word holds (an object's vtable), so memory the guest has not built yet is left alone. One run says whether a gate is the cause - the frame changes - before any link is traced.
  6. What the title built. ORBISTOUN_FIND=<text> searches all readable guest memory when the run ends and ORBISTOUN_SAVE=<addr>+<len> writes regions out: tables and scripts a title decrypts or builds only in memory are read there, not from its files.

A cause found this way becomes a fix in the layer that answered wrongly, and the hold that proved it is dropped; a hold is never the fix.

Staging what the loop proves

A policy patch (D296) is data, and one that has earned its place does not wait for a person:

flowchart LR
    A["a proposed policy patch"] --> B["trial on its title"]
    B -- "FURTHER" --> C["run across the corpus"]
    B -- "same / BACK" --> X["stays a candidate<br/>in learned.toml, unread"]
    C -- "no title BACK" --> D["staged in the tracked knowledge,<br/>known_by: guest-observed,<br/>with the run as evidence"]
    C -- "a title BACK" --> X
    D --> E["census row filed:<br/>the hardware asked the same question"]
    E --> F{"measured"}
    F -- agrees --> G["known_by: measured"]
    F -- contradicts --> H["retired; the measurement<br/>replaces it"]

FURTHER on one title is not enough, because a stub answering success moves walls too; the corpus run catches a patch that helps one title by breaking another. A staged patch is labelled for what it is - the guest accepted it - and its measurement is requested in the same step. A patch that writes guest memory still needs a conformance check first (D296). Rust is never staged this way.

The naming sub-loop

Naming is the part of the loop that gets cheaper as it runs.

flowchart LR
    A["every unnamed import hash<br/>in the whole corpus"] --> B["candidates from<br/>the grammar"]
    A --> C["identifier-shaped strings<br/>in every module's own bytes"]
    A --> R["strings in what a guest<br/>passed to an import, as it ran"]
    B --> D["hash each candidate"]
    C --> D
    R --> D
    D --> E{"does it collide<br/>with a wanted import?"}
    E -- yes --> F["proved - the name is right,<br/>and nothing was consulted"]
    E -- no --> G["proves only that the name<br/>was not among those tried"]
    F --> H["split the name into words<br/>and add them to the vocabulary"]
    F --> I["record what proposed it:<br/>derived, static or runtime"]
    H --> B
    I --> J["re-derive records whose<br/>coordinates the new words moved"]
    J --> B

A hash is one-way, so the only method is to propose a name and let the hash accept or refuse it. A hit is proof rather than a lookup, which keeps the symbol database clean-room. Every source is confirmed the same way; the record says which source proposed the name, and the audit tiers sources by what somebody else needs to repeat them (D213).

The search runs over the whole corpus, not one module at a time: one module's strings name another module's imports, and the vendor C library module carries names that titles import without ever spelling them.

Two edges feed back into the grammar:

A model as a vocabulary source

A miss proves only "not in what was tried", so naming more imports needs more words. Words come from the modules, from published standards, and from orbistoun-propose::vocabulary, which asks a local model for words one grammar position at a time.

Nothing a model says is trusted: it proposes words and the hash decides. A wrong suggestion costs a sweep and vanishes, which is why a model may guess here and nowhere else in this project. Words are banked only when a confirmed name was built from them, and promoting a banked word into vendor.toml is a separate, deliberate act. The yield sits in the first round of each position; ask once per position.

The questions sub-loop

Some facts cannot be settled by watching a guest: what a function returns on the hardware when nothing observes the return, or what an argument means when every observed call passes the same value. These are measurements to be taken.

flowchart LR
    A["writing a behaviour down"] --> B["known_by:<br/>published / measured /<br/>guest-observed / assumed"]
    B --> C["everything unsettled listed<br/>as an assumption on that function"]
    C --> D["orbistoun-cli questions --json<br/>- ranked by how often guests call it"]
    D --> E["a conformance probe runs them<br/>on the hardware"]
    E --> F["orbistoun-cli probe TRANSCRIPT"]
    F --> G["known_by becomes 'measured'"]
    G --> C

known_by has no value meaning "already known": every option names something that could contradict it. assumed is a normal state. A written-down assumption can be counted, ranked, probed and retired.

Probing on the hardware

Three components share step E:

The capability sub-loop

The questions sub-loop retires assumptions orbistoun has already written down. The capability sub-loop finds the ones nobody has written down yet: an import no knowledge entry covers, a register field the pipeline refuses, an instruction the translator does not know. It turns each into a measurement, and the measurement into a test.

flowchart LR
    A["orbistoun-cli census<br/>imports with no measured entry,<br/>refusals by reason,<br/>ranked by titles reached"] --> B["group by area:<br/>one library, one GPU layer"]
    B --> C["one obSCEne request per area,<br/>many arms; predictions attached<br/>where a reference has them"]
    C --> D["obSCEne sweep rows"]
    D --> E{"rows settle<br/>every arm?<br/>(resolutions)"}
    E -- no --> F["re-file the arms<br/>the rows missed"]
    F --> C
    E -- yes --> G["knowledge entry,<br/>known_by: measured"]
    D --> L["refuted predictions<br/>into the oracle ledger"]
    G --> H["a fixture test asserting<br/>the measured value"]
    H --> I["the implementation,<br/>step 18"]
    I --> A
  1. orbistoun-cli census lists every import in the corpus with no measured knowledge entry, and every translator and backend refusal by reason, each ranked by how many titles reach it. It is the worklist across titles, as worklist is within one.
  2. Rows are grouped by area - one library, one GPU layer - and each area becomes one request with many arms. A probe that loads a library once and calls twenty functions costs one round trip; twenty requests cost twenty.
  3. Where a reference emulator has an answer, it travels with the request as a prediction (D818): a first arm for the probe and nothing more. The request resolves only on obSCEne's own rows, and an arm the rows miss is re-filed, never assumed. orbistoun-cli resolutions <worklog> --reports <obSCEne repository> checks each resolution against the rows of the logs it cites. A status code no row holds, or a function whose rows say it never bound, sends the request back with those rows quoted (D840).
  4. A resolved row becomes a knowledge entry with known_by: measured, and a fixture test that asserts the value before any implementation uses it (TESTING.md).
  5. Every refuted prediction goes into crates/orbistoun-hle/data/oracle-ledger.toml, so the next census knows which areas' predictions are worth sending.

Conformance titles grade whole layers

Two kinds of first-party guest turn the census from a list into a graded test:

Checkout and binary

The loop does not need this repository. Somebody running a release binary against a title nobody here owns turns the same oracle - the guest itself - and what they produce is worth collecting (D297).

Artifact Where it lands Needs a checkout Sendable
learned.toml measurements data directory no yes
compat entries --dir, default ./compat no yes
traces, run reports data directory no no - they are inputs
symbols/generated.json the repository yes via the name, not the file
an implementation the repository yes as a patch

Everything sendable is the same kind of claim: derived from running a binary the submitter owns, reproducible by anyone with the same title, and falsifiable by a command. A maintainer without the title cannot check a diff, but can accept a measurement as assumed and promote it when somebody who owns the title confirms it. submit export and submit check gather and receive these; see WORKFLOW.md.

Properties of the loop