Reference for the four host/guest data streams that move pixels, button/LED
state, and control-plane messages between the QEMU guest and the cdj3k-emu
host process. Audio is covered separately in docs/audio-stack.md; the
subucom SPI wire format is documented in docs/subucom.md.
| Stream | Transport | Direction | Host endpoint |
|---|---|---|---|
| Main LCD | QEMU -display shm (mmap'd file) |
guest → host | {sock_dir}/main.shm |
| Jog LCD | ivshmem-plain BAR2 (mmap'd file) | guest → host | {sock_dir}/jog.shm |
| Subucom ctrl | virtio-serial (Unix socket) | bidirectional | {sock_dir}/ctrl.sock |
| Config (cfg) | virtio-serial (Unix socket) | bidirectional | {sock_dir}/cfg.sock |
sock_dir follows the multi-instance convention
/tmp/cdj3k-emu-{uid}/instance-{id}/ (%TEMP%\cdj3k-emu\instance-{id}\ on
Windows). crates/cdj3k-emu-platform/src/runtime_paths/
is the source of truth for the layout and the length limit on it; the copies in
boot.sh and tools/midi-driver/link.c must agree with it. Authoritative QEMU
args are QemuConfig::build_argv in crates/cdj3k-emu-runtime/src/config.rs.
The sockets are AF_UNIX on Windows too (Windows 10 1803 and later; the host
side is uds_windows, platform::local_socket), so the chardev lines are the
same on every host; the path limit there is 108 bytes. Patches 15-17 give
Windows QEMU memory-backend-file (behind jog.shm), the shm display's file
mapping (main.shm) and ivshmem-plain; ivshmem-doorbell is refused there.
QEMU's monitor is -qmp tcp:localhost:<4445 + slot>. Where the disk adapter
hands a raw USB disk to QEMU as a descriptor (Linux), a second monitor listens
on {sock_dir}/qmp-fd.sock: add-fd needs a Unix socket to carry the
descriptor, and is Unix only (qmp/fd_passing/). The Windows adapter does
not pass descriptors (PASSES_FDS = false); QEMU opens the disk by path.
QEMU's in-tree shm display backend, added by
qemu/patches/06-shm-display-source.patch. Launched with:
-display shm,path={sock_dir}/main.shm
QEMU writes the framebuffer plus a small header into a regular file
(MAP_SHARED). The host mmaps it read-only and polls a generation
counter; pixel data is read in place - no copies through QEMU's UI
ringbuffer.
- Producer:
qemu/patches/06-shm-display-source.patch(new fileui/shm-display.c). - Consumer:
crates/cdj3k-emu-streams/src/main_stream.rs(see top-of-file doc-comment for the layout).
Little-endian. Reader polls generation with Acquire; on bump, the rest
of the header and the dirty pixels are coherent.
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0x00 | u32 | magic | 0x514D5348 (QMSH) |
| 0x04 | u32 | generation | Acquire-load; bumped after each dirty blit |
| 0x08 | u32 | width | 1280 in steady state |
| 0x0C | u32 | height | 720 |
| 0x10 | u32 | stride | bytes per row |
| 0x14 | u32 | format | 1 = RGBA8888 (QEMU swaps XRGB→RGBA host-side) |
| 0x18 | u32 | dirty_x | bounding box of last update |
| 0x1C | u32 | dirty_y | |
| 0x20 | u32 | dirty_w | |
| 0x24 | u32 | dirty_h | |
| 0x40 | u8[] | pixels | stride × height bytes |
Surface dimensions can switch mid-session (e.g. the firmware's 640×480
console handoff to the 1280×720 Xorg surface); the reader re-reads
width/height/stride on every generation bump.
QEMU publishes on each dirty blit (effectively ~60 Hz under firmware
activity). Host poll interval is 500 µs
(POLL_INTERVAL in main_stream.rs). The reader publishes a
DisplayDirty rect carrying an Arc<Mmap> so the UI thread uploads to
the GPU texture without a CPU copy.
The reader checks the magic word every tick. If it disappears (QEMU
exited or replaced the file), the reader drops the mapping and loops on
open_shm with a 1 s backoff until the file reappears with a valid
header. A static display keeps the same generation indefinitely - that
is not an error condition and the reader does not time out on it.
ivshmem-plain PCI BAR2, backed by a shared file. The guest's
deck_shim.so writes XRGB pixels straight into the BAR; the host mmaps
the same file and polls a seqlock counter. Zero copies, zero virtqueue
traffic.
QEMU args (config.rs lines 326-337):
-object memory-backend-file,id=jogshm,mem-path={sock_dir}/jog.shm,size=1M,share=on
-device ivshmem-plain,memdev=jogshm,master=on
- Producer:
guest/deck_shim/core/jog_shm.c(crops the visible 320×240 region from EP122's stretched 1280×240 DRM jog plane). - Consumer:
crates/cdj3k-emu-streams/src/jog_stream.rs.
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0x0000 | u32 | magic | 0x3147_4F4A (JOG1) |
| 0x0004 | u32 | seq | seqlock counter - odd = write in progress |
| 0x0008 | u16 | width | 320 |
| 0x000A | u16 | height | 240 |
| 0x000C | u32 | format | 1 = XRGB8888 (B, G, R, X byte order) |
| 0x1000 | u8[] | pixels | width × height × 4 = 307 200 bytes |
Host polls seq at ~60 Hz (POLL_INTERVAL = 16 ms). Guest publishes
on every DRM flip (~100 Hz upstream). The seqlock read retries up to 4
times if it observes an in-progress write or a torn seq_before / seq_after.
- Seqlock retry as above; if all four attempts tear, the reader sleeps one poll interval and tries again.
- If the magic word disappears (the runtime zeroes it before unlinking the file on cleanup), the reader drops the mapping and re-opens.
- If
seqhasn't advanced forALIVE_TIMEOUT = 2 s, the stream is flagged disconnected so the UI can show a "wait" badge; reads keep trying.
Bidirectional virtio-serial port cdj3k.ctrl exposed as a Unix socket on
the host. Carries the subucom SPI protocol in both directions, 64-byte
frames each way.
QEMU args (config.rs lines 300-324):
-device virtio-serial-device,max_ports=8
-chardev socket,id=vserial_ctrl,path={sock_dir}/ctrl.sock,server=on,wait=off
-device virtserialport,chardev=vserial_ctrl,name=cdj3k.ctrl
- Host endpoint:
crates/cdj3k-emu-streams/src/ctrl_stream.rs. - Frame helpers / CRC:
crates/cdj3k-emu-panel/src/{mosi_frame.rs, miso_frame.rs, crc.rs}. - Guest bridge:
guest/subucom/forwarder.c(connects the kernel character device/dev/subucom_ctrlto the virtio-serial port; module sourceguest/modules/subucom_virt/subucom_virt.c). - Wire-level field layout: see
docs/subucom.md.
- MOSI - guest → host (LEDs).
subucom_virt.koexposes/dev/subucom_ctrl. EP122 writes 64-byte LED-state frames there;forwarder.cpipes each frame onto the virtio-serial port. The host reader (ctrl_stream.rs::stream_loop) decodes them throughMosiFrameand drives the on-screen LED simulation. - MISO - host → guest (buttons/jog/touch). The UI builds 64-byte
MisoFrames representing button bitmasks, jog rotation/touch, and performance-pad state. Each frame is CRC-stamped (CRC-16/X-25 over bytes[0..62], written little-endian into bytes[62..64]) and pushed back through the same socket.forwarder.cwrites them into/dev/subucom_ctrl, where the firmware consumes them as if they came from the real subucom MCU.
MOSI is paced by EP122's SPI loop (~100 Hz). MISO is event-driven: the UI sends a frame on every input change plus an idle keep-alive.
The reader/writer pair connects lazily; on EOF or connect failure
(ECONNREFUSED while QEMU is still bringing the chardev up) it retries
after RECONNECT_DELAY = 2 s. The write half is shared via
Arc<Mutex<Option<UnixStream>>> and is dropped on any I/O error so the
next reconnect re-establishes it.
Second bidirectional virtio-serial port (cdj3k.cfg). Line-based ASCII,
\n-terminated. No framing beyond newlines; no binary.
QEMU args: same virtio-serial-device controller as ctrl, second
chardev/virtserialport pair on it.
- Host:
crates/cdj3k-emu-runtime/src/cfg.rs(top doc-comment is the canonical protocol reference). - Guest:
guest/cfgd/cfgd.c.
| Direction | Line | Meaning |
|---|---|---|
| host → guest | usb attach |
invoke /usr/sbin/usb-external-attach.sh on the guest |
| host → guest | usb detach |
no-op on the guest (EP122 owns unmount) |
| host → guest | set <name> <value> |
write a whitelisted virtio_snd sysfs param |
| host → guest | get <name> |
request the current value of a whitelisted param |
| guest → host | usb_state <0|1> |
emitted by the in-guest USB hooks on mount/unmount |
| guest → host | param <name> <value> |
response to get, or unsolicited push on change |
| guest → host | latency <g>,<h>,<t> |
guest/host/total audio latency, pushed every 3 s |
name and value must not contain spaces or newlines; the host-side
setter rejects those at the API boundary.
Largely idle. latency push every 3 s; everything else is event-driven.
Self-healing on both halves: reader reconnects on EOF after
RECONNECT_DELAY = 500 ms. Writer fast-path uses the long-lived stream
held by the reader thread; if that's missing (e.g. immediately after
spawn) the writer opens a one-shot connection with up to 20 × 100 ms
retries and a 2 s write timeout (COLD_WRITER_* in cfg.rs).
Concatenated from crates/cdj3k-emu-runtime/src/config.rs for the
streams documented here (the audio devices live separately - see
docs/audio-stack.md):
# Main LCD - shm display
-display shm,path={sock_dir}/main.shm
# Jog LCD - ivshmem-plain BAR2
-object memory-backend-file,id=jogshm,mem-path={sock_dir}/jog.shm,size=1M,share=on
-device ivshmem-plain,memdev=jogshm,master=on
# virtio-serial controller for ctrl + cfg
-device virtio-serial-device,max_ports=8
# Subucom MOSI/MISO bridge
-chardev socket,id=vserial_ctrl,path={sock_dir}/ctrl.sock,server=on,wait=off
-device virtserialport,chardev=vserial_ctrl,name=cdj3k.ctrl
# Config / control plane
-chardev socket,id=vserial_cfg,path={sock_dir}/cfg.sock,server=on,wait=off
-device virtserialport,chardev=vserial_cfg,name=cdj3k.cfg
All Unix sockets use server=on,wait=off so they exist before the host
reader connects; readers must tolerate ECONNREFUSED / EOF and retry.
Worst-case figures, one instance.
| Stream | Steady-state | Worst case | Notes |
|---|---|---|---|
| Main LCD | Dirty-rect deltas, typically a few MB/s | ~221 MB/s if the whole 1280×720×4 surface flips every 60 Hz | Zero-copy mmap - no QEMU UI ring traffic |
| Jog LCD | Seqlock-gated; host reads at 60 Hz | 320×240×4 × 60 Hz = ~17.6 MB/s read | Read-only mmap on host; guest writes ~100 Hz raw |
| ctrl.sock | 64 B × ~100 Hz each direction = ~13 KB/s | Same - bounded by 100 Hz SPI loop | virtio-serial, negligible |
| cfg.sock | A handful of ASCII lines per second | Same | Negligible |
The shm and ivshmem paths don't traverse QEMU's UI/virtqueue plumbing, so "bandwidth" here is just memory-bus reads on the host side; nothing competes with the audio path.