D310 - The window and the guest are different processes, so the shell button had nowhere to go

decided · 2026-08-27 · found by reading the protocol before writing the shell

The shell was scoped as four things: browse the library, boot straight into it, press the shell button in-game, and quit back to it. Two of those are a front-end and two are an ABI contract, and only the first two were buildable - for a reason that had nothing to do with graphics.

Guest code executes in a child process (D032). Request was send-once-then-listen: the worker read a request, ran the guest to completion, and streamed events back. The only in-flight control was Stopper, which is TerminateProcess. So a shell action arriving while a title ran sat unread in the pipe until the run it was meant to interrupt had already finished. Nothing was broken; there was simply no path.

Reading happens on its own thread now, and Request::Shell is applied there rather than forwarded - it needs no reply, so the output stream keeps exactly one writer even while run_guest is mid-sentence. That property is asserted as no event, because a reply would pass a test that only checked the handshake and would corrupt the stream solely under the timing this whole arrangement exists to support.

StdinLock turned out not to be Send - it holds a MutexGuard - so worker mode wraps the unlocked handle. Nothing is given up; this process is its only reader.

The general point is about where a feature's cost actually sits. The scoping conversation put controller, GPU and audio support ahead of the in-game shell. The real prerequisite was a control channel, and it was invisible because it is not a subsystem - it is the absence of one.