Patrick Lidstone
Self-hosted

Development log

The screen over the wire, compressed

· Path OS

A screenshot answers what the screen looks like. It cannot answer what happens when you click there, and that second question is where every rendering and interaction fault ends up. The kernel now runs an RFB 3.8 server and any VNC client can connect to it - Screen Sharing, TigerVNC, RealVNC - with nothing written for the other end.

The protocol is a crate of its own with no sockets and no framebuffer in it, because everything in it is byte layout and state machine and every one of those can be got wrong silently. A rectangle encoder off by one row produces a picture that is nearly right. A message parser that trusts a client's length field produces a kernel that any client can crash. Neither is visible from a log on a machine with no debugger.

The session therefore does no I/O at all - bytes in, bytes out - and the handshake, the encodings, the pixel conversion, the damage logic, the keysym mapping and the authentication are driven from ninety-seven host tests. Hextile is proved by decoding what it produced and comparing pixels, rather than against a hand-written expectation, which is what catches the carried-background rule that no single tile's test can see. There is a test for a client that claims four gigabytes of clipboard and then carries on talking.

Learning to listen

Listening was the bulk of the work. The TCP implementation was a client with "no listen or accept" written into its header as a deliberate refusal, on the grounds that a machine with no processes has nothing to offer a caller. The first half of that stopped being true when the window server did.

It now has a Listen state and a SynRcvd state, and the segment handler takes the source address - which a listener has to learn, and which is now also checked on an established connection, where a segment from a different host carrying the same pair of ports used to be accepted as part of the conversation. A SYN that is never acknowledged puts the port back to listening rather than leaving it failed, because otherwise one port scan takes the screen away until the next reboot. The second half of the old refusal still stands: the port opens only when asked.

The connection buffers moved to the heap on the way, and that was necessary rather than tidy. A connection was nine kilobytes inside the network structure, which is a local in the kernel's entry function, so four interfaces put thirty-six kilobytes on the kernel stack before anything was sent. A server also needs a far bigger send buffer than a client does, because the send buffer is the window: at a kilobyte and a LAN's third of a millisecond the ceiling is about three megabytes a second, and the first frame of this panel is fourteen.

Making it cheap

The first version sent only changed tiles and found them by hashing all fourteen megabytes of the frame on every update, then sent them as Hextile at best and Raw at worst. A caret blink was a full-frame scan to find one tile, and a whole screen over the radio was fourteen megabytes.

Both halves are now cheap. ZRLE is preferred, then Zlib, then Hextile, then Raw, chosen in that order from what the client offered rather than in the client's order - because the cost that matters is the machine's own uplink, not the client's preference. Both compressed encodings need DEFLATE and the tree had only an inflater, in the PNG reader, so the compressor is this crate's own: LZ77 with hash chains over a 32 KiB window, each block written as whichever of stored, fixed or dynamic Huffman is smallest by exact bit count. No dependency was added.

The part of that protocol which is easy to get wrong is that each encoding keeps one zlib stream for the life of the connection, with every rectangle a sync flush of it, the header sent once and the trailer never. A server that restarts the stream per rectangle is rejected by the client's inflater after the first one.

The scan is now driven by what the compositor actually painted rather than by hashing the frame. A still screen costs nothing at all, and the hash that is still taken is per pixel row and tile column rather than a shadow copy of the frame - so one pass answers both which tiles changed and whether the screen scrolled. That second answer is what makes CopyRect possible without keeping the old pixels, and it is guarded: the band must be a verified contiguous shift of at least sixty-four rows, rows that merely did not change do not count towards it, and the client must be known to hold the whole of the last frame. A still screen matching every shift there is would otherwise send a desktop sliding sideways for no reason.

A keyboard driver at the far end

Keys arrive as X11 keysyms and this machine has no keysyms, so a VNC client is exactly as much of a keyboard driver as the PS/2 one is, and has the same two jobs: hold the modifiers itself, and turn the chords into intentions. The mapping is deliberately the same one the PS/2 path makes, because the alternative is a machine whose behaviour depends on whether you are sitting at it.

What is not built

When the server was first written, nothing in it had been run on the hardware. It compiled, the tests passed, and the documentation said which parts rested on a guess - which is a reasonable state to leave a thing in and not the same as a working remote display.

The reason this exists is to let faults be driven rather than described, so the test of it is not whether it connects but whether the next rendering bug is found by clicking on it. That has not happened yet.