27 KiB
01 — Layered Architecture Map
This chapter walks Greenfield's architecture layer by layer, from the lowest abstraction (Wayland XML) up to the highest (the user-facing shell). Each layer is described in terms of: responsibility, key abstractions, dependencies, and files of interest. The chapter ends with an ASCII diagram showing how the layers stack and where data flows between them.
The eight layers
┌──────────────────────────────────────────────────────────────────────────┐
│ Layer 8: USER SHELL (Odoo 20 OWL addon, supplied by user) │
│ Hooks: UserShellApi (events + actions) │
│ ─ NOT covered in this chapter (see 08-app-lifecycle.md) │
├──────────────────────────────────────────────────────────────────────────┤
│ Layer 7: DESKTOP / LAYOUT POLICY │
│ Floating vs AlwaysFullScreen, view stacking, focus activation refcount │
│ packages/compositor/src/desktop/Desktop.ts │
├──────────────────────────────────────────────────────────────────────────┤
│ Layer 6: SURFACE LIFECYCLE / ROLES │
│ XdgWmBase, XdgSurface, XdgToplevel, XdgPopup, XdgPositioner, Shell, │
│ ShellSurface, Subcompositor, Subsurface, SurfaceRole │
│ packages/compositor/src/{Xdg*, Shell*, Sub*, SurfaceRole, SurfaceChild} │
├──────────────────────────────────────────────────────────────────────────┤
│ Layer 5: RENDERING PIPELINE │
│ Surface (state stack), View, Output (WebGL canvas), Renderer, Scene, │
│ Shaders (YUV→RGBA, YUVA→RGBA, RGB+A→RGBA), Loop (RAF), Callback │
│ packages/compositor/src/{Surface, View, Output, Loop, Callback, Region} │
│ packages/compositor/src/render/ │
├──────────────────────────────────────────────────────────────────────────┤
│ Layer 4: TRANSPORT (browser side) │
│ Channel (ARQ + Simple), ReconnectingWebSocket, RemoteAppLauncher, │
│ StreamingBuffer, BufferStream, EncodedFrame, EncoderFeedback, decoders │
│ (WebCodecs + WASM fallback) │
│ packages/compositor/src/remote/ │
├──────────────────────────────────────────────────────────────────────────┤
│ Layer 4: TRANSPORT (native side) │
│ Channel (ARQ + Simple), SessionController, kcp.ts, NativeAppContext, │
│ FrameFeedback, SurfaceBufferEncoding, wayland-server.ts │
│ packages/compositor-proxy/src/{Channel, kcp, Session*, Native*, ...} │
├──────────────────────────────────────────────────────────────────────────┤
│ Layer 3: WAYLAND RUNTIME (browser side) │
│ Connection (wire framing), Client, WlObject registry, marshallers │
│ libs/common/src/Connection.ts │
│ libs/compositor-protocol/src/{westfield-runtime-server.ts, generated/*} │
├──────────────────────────────────────────────────────────────────────────┤
│ Layer 3: WAYLAND RUNTIME (native side) │
│ wayland-server.ts + wayland-server-addon.node (N-API → libwayland-server)│
│ MessageInterceptor (per-resource request/event routing) │
│ packages/compositor-proxy/src/{wayland-server.ts, protocol/*_interceptor}│
├──────────────────────────────────────────────────────────────────────────┤
│ Layer 2: PROTOCOL GENERATORS │
│ client-generator, compositor-generator, compositor-proxy-generator │
│ libs/{client,compositor,compositor-proxy}-generator/ │
├──────────────────────────────────────────────────────────────────────────┤
│ Layer 1: WAYLAND PROTOCOL XML │
│ protocol/{wayland, xdg-shell, wayland-drm, web-bitmapbuf-unstable-v1}.xml│
└──────────────────────────────────────────────────────────────────────────┘
Layer 1 — Wayland Protocol XML
Responsibility: declare the protocol surface — what interfaces, requests, events, enums, and bitfield types exist on the wire.
Files of interest:
protocol/wayland.xml— standard upstream Wayland core. 22 interfaces,wl_compositorv4,wl_seatv6,wl_surfacev4 (roughly Wayland 1.20-ish). Kristian Høgsberg copyright. Used by all 3 generators.protocol/xdg-shell.xml— stablexdg_shellv1, 5 interfaces (xdg_wm_base, xdg_surface, xdg_toplevel, xdg_popup, xdg_positioner). Used by all 3 generators.protocol/wayland-drm.xml— legacywl_drmv2, used by native apps (Mesa, XWayland) to advertise dmabuf buffers. Only the proxy generator processes this file — so the proxy can interceptcreate_buffer/create_planar_buffer/create_prime_bufferand turn dmabuf fds into ProxyFDs for the browser's GStreamer encoding pipeline. Browser-side libs do not generatewl_drmstubs.protocol/web-bitmapbuf-unstable-v1.xml— Greenfield's custom extension. Single interfaceweb_bitmapbuf_factoryv1, single requestcreate_buffer(id: new_id<wl_buffer>, bitmap: fd). Lets browser JS apps hand a GPU-backedImageBitmapdirectly to the compositor as awl_buffer— bypassing SHM copy, encoding, and dmabuf import. Thefdarg is marshalled via the samefileDescriptor(...)path aswl_shm.create_pool's fd, but holds anImageBitmap(becomes a transferable on the flush path). Not processed by the proxy generator — native apps never use it.
Dependencies: none. This is the source of truth.
Layer 2 — Protocol Generators
Responsibility: parse the XML with xml2js, walk the interface tree,
emit typed TypeScript bindings for each side.
Key abstractions: ProtocolParser.mjs (shared XML walker) +
ProtocolArguments.mjs (per-side argument-type dispatch table).
Files of interest:
libs/client-generator/{ProtocolParser, ProtocolArguments, index}.mjs— emits TS*Proxyclasses for browser apps that want to be Wayland clients of the browser compositor (viaMessagePort).libs/compositor-generator/{ProtocolParser, ProtocolArguments, index}.mjs— emits TS*Resource+*Requestsclasses for the browser-resident compositor itself (receives requests, dispatches to*Requestsimpls, emits events). Skipswl_display,wl_registry(hand-written in runtime).libs/compositor-proxy-generator/{EndpointProtocolParser, EndpointProtocolArguments, index}.mjs— emits CommonJS*_interface.js(wl_interface struct definitions for the N-API addon)*_interceptor.js(per-opcode interceptors that decide routing) for the Node.js native proxy.
Why three generators? Because each side has a fundamentally different job:
- client-generator → browser apps that want to be Wayland clients of the
browser compositor (via
MessagePort). - compositor-generator → the browser-resident compositor itself (receives
requests, dispatches to
*Requestsimpls, emits events). - compositor-proxy-generator → the Node.js native proxy bridging real
libwayland-client apps to the browser. Emits
wl_interfacestruct definitions for the N-API addon + per-opcode interceptors that decide routing.
Dependencies: xml2js, camelcase, uppercamelcase, meow.
Layer 3 — Wayland Runtime
Responsibility: marshal and unmarshal the actual wire bytes. Implement object lifecycle (creation, dispatch, destruction), new-id tracking, listener dispatch, file descriptor handling.
Browser side (libs/common/src/Connection.ts) — the shared runtime for
both @gfld/client-protocol and @gfld/compositor-protocol:
- Wire format is standard upstream Wayland, byte-for-byte. 8-byte
header:
u32 object_id | u16 opcode | u16 size-incl-header, then per-arg bytes.u/i/f/n/oare 4 bytes;sandaare length-prefixed u32 + payload padded to 4-byte alignment;h(FD) takes zero bytes — FDs travel out-of-band inwireMsg.fds[]. - Reading:
wlObject[opcode](message)— the generated classes use computed-property-key methods[0](message) { ... },[1](message) { ... }so opcode is the array index. - Client side:
Proxy.marshallConstructorcallsdisplay.generateNextId()(1-indexed, recycles ids fromwl_display.delete_id), constructs the new Proxy (self-registers in constructor), patches thenew_idarg'svalue, then flushes. - Server side:
Client.marshallConstructorallocates fromSERVER_OBJECT_ID_BASE = 0xff000000(or recycles locally). When a client-allocated id is destroyed, the server sendswl_display.delete_idso the client can recycle. - Connection registry:
wlObjects: { [id]: WlObject };registerWlObjectthrows on collision. Connection.messagedispatches by literalwlObject[opcode](message). Each handler returns void or a Promise;Connectionawaits the promise before reading the next message — this preserves wire-order while still allowing async implementations. Per-client Connections mean a slow client doesn't block others.
Native side (packages/compositor-proxy/src/wayland-server.ts) — a TS
wrapper around wayland-server-addon.node (the C N-API addon that wraps a
patched libwayland-server):
MessageInterceptor— per-resource request/event dispatch. Eachwl_*_interceptor.jsfile declaresR<opcode>(...args)andE<opcode>(...args)functions that decide whether a message is routed{native: true}(handle in the proxy),{browser: true}(forward to browser compositor), or both.- Special cases:
wl_display.sync→ both native and browser (must complete beforedoneevent),wl_display.get_registry→ both,wl_registry.bind→ native if id is innativeGlobalNames[](e.g.wl_drm,wl_shm), else browser. Fast-sync optimization skips browser round trip when no prior browser-bound request occurred.
File descriptor handling — the Greenfield innovation:
type FD = unknown — it's intentionally opaque. Browser-side FDs are
transferable JS objects:
ArrayBufferViewforwl_shm.create_pool'sfd(SHM backing memory lives in browser, zero-copy transferred viaMessagePort.postMessage(..., transferList)).ImageBitmapforweb_bitmapbuf_factory.create_buffer'sbitmaparg (GPU-backed texture, zero-copy).MessagePortforwl_data_offer.receive'sfd(in-browser pipe).
Native-side (proxy) FDs are real POSIX ints.
NativeWaylandClientSession.onWireMessageEnd converts each to a JSON
ProxyFD = {handle, type, host} (host = proxy's HTTP base URL),
length-prefixed + 4-byte aligned, prepended to the wire message before
sending over WebSocket/KCP. Browser's webFS resolves them by fetching
data over HTTP.
Dependencies: Layer 2's generated bindings + @gfld/common.
Layer 4 — Transport
Responsibility: carry wire bytes between the browser compositor and the native proxy. Multiplex logical channels. Handle reconnection, backpressure, and latency.
Browser side (packages/compositor/src/remote/):
Channel.ts— definesARQChannel(KCP-wrapped, reliable, low-latency) andSimpleChannel(raw WebSocket).ReconnectingWebSocket— vendored frompladaria/reconnecting-websocketwithminDelay=1-5s,factor=1.3,maxDelay=10s,minUptime=5s,connTimeout=4s,retries=∞.RemoteAppLauncher.ts— sets up the 4 logical channels per app client (PROTOCOL, FRAME, XWM, FEEDBACK) and routes incoming messages.RemoteInputOutput.ts— implements theInputOutputinterface for native apps, withmkfifo,mkstempMmap,readFd,writeFdAsStream,closeFdover separate WebSocket endpoints.StreamingBuffer.ts,BufferStream.ts,EncodedFrame.ts,EncoderFeedback.ts— decode pipeline for incoming H.264 / PNG frames.webcodec-buffer-decoder.ts,wasm-buffer-decoder.ts,H264NALDecoder.worker.ts— the actual decoders.
Native side (packages/compositor-proxy/src/):
Channel.ts— mirror image of the browser side.ARQChannelandSimpleChannel.kcp.ts— ~1100-line TypeScript port of the canonical Cikcp.c.SessionController.ts— WS server with 9 endpoint paths:/signal,/channel,/mkfifo,/mkstemp-mmap,/read-fd,/read-fd-as-stream,/write-fd-as-stream,/close-fd,/request-keyframe.Session.ts,SessionProcess.ts,SessionController.ts— process model: one main process, one forked child per browser session.NativeAppContext.ts— per-app state, signaling WS, lifecycle (10-min SIGHUP grace on disconnect, SIGTERM→SIGKILL escalation).FrameFeedback.ts— receives 4-byte feedback messages from the browser, uses them to pacewl_frame.donecallbacks.SurfaceBufferEncoding.ts,ProxyBuffer.ts— encoding pipeline.io/subdirectory — WebSocket server glue.encoding/subdirectory — high-level encoding orchestration.
Wire format: see 04-transport.md for the full byte-level breakdown of
PROTOCOL, FRAME, XWM, and FEEDBACK channels.
Dependencies: Layer 3 + ws (Node) + browser-native WebSocket + ReconnectingWebSocket.
Layer 5 — Rendering Pipeline
Responsibility: take incoming buffer frames (SHM or encoded), turn them into pixels on a browser canvas/WebGL context. Manage surface state, damage tracking, frame callbacks, and the compositing loop.
Key abstractions:
Surface.ts— thewl_surfaceimplementation. Double-buffered state (statevspendingState), atomic merge incommitPending(). Subsurface sync mode cascades through the ancestor chain.SurfaceChild.ts,SurfaceRole.ts,Subsurface.ts,Subcompositor.ts— surface tree / role system.View.ts— represents a surface's position on an output, used for compositing.Output.ts— thewl_outputimplementation; how output is rendered to a WebGL canvas.Shm.ts—wl_shm/ shm pool handling. How shared memory buffers arrive in the browser.BufferContents.ts,BufferImplementation.ts,ImageBitmapBuffer.ts— buffer abstraction hierarchy.Region.ts—wl_region, damage regions (pixman-WASM backed).Callback.ts— frame callback mechanism.Loop.ts— the compositor main loop /requestAnimationFrameintegration.Compositor.ts— thewl_compositorglobal; how it creates surfaces and regions.Session.ts— top-level browser-side session.Globals.ts— how Wayland globals are registered.render/subdirectory — the WebGL renderer:Renderer.ts,Scene.ts,SceneShader.ts,Texture.ts,RenderState.ts,Program.ts,ShaderCompiler.ts,ShaderSources.ts,YUVA2RGBA.ts,YUVA2RGBAShader.ts,YUV2RGBShader.ts,RGBXandAToRGBA.ts,RGBandA2RGBAShader.ts.browser/subdirectory — browser integration glue (canvas, RAF, ResizeObserver, dnd, selection, input).math/subdirectory — matrix/region math (Mat4.ts,Rect.ts).
Pipeline: see 03-html5-rendering.md for the full
commit → damage → render → present pipeline.
Dependencies: Layer 4 + WebGL + WebCodecs + @gfld/compositor-wasm (pixman + xkbcommon).
Layer 6 — Surface Lifecycle / Roles
Responsibility: implement the role-specific surface state machines (xdg-toplevel, xdg-popup, xdg-positioner, wl_shell_surface, subsurface). Manage configure/ack-configure round-trips, popup grabs, window state.
Files of interest:
XdgWmBase.ts— thexdg_wm_baseglobal. How clients advertise they're XDG clients. Includes ping-based unresponsiveness detection.XdgSurface.ts—xdg_surface. The role wrapper around awl_surface.XdgToplevel.ts—xdg_toplevel. Top-level windows. State (maximized, fullscreen, minimized, activated, resizing). Configure/ack-configure round-trip.XdgPopup.ts—xdg_popup. Transient windows, menus, tooltips. Grab semantics.XdgPositioner.ts—xdg_positioner. How popups are positioned relative to parent.Shell.ts,ShellSurface.ts— the legacywl_shell(deprecated but still implemented for old clients).Subcompositor.ts,Subsurface.ts,SurfaceChild.ts,SurfaceRole.ts— subsurface compositing, the surface tree.
State machines: see 08-app-lifecycle.md for the full
XdgToplevel lifecycle, configure/ack round-trip semantics, popup
positioner math, and subsurface sync/desync mode.
Dependencies: Layer 5 + Layer 3 (Wayland runtime).
Layer 7 — Desktop / Layout Policy
Responsibility: arrange surfaces on the output. Decide stacking order, focus policy, and the high-level window management mode (Floating vs AlwaysFullScreen).
Files of interest:
desktop/Desktop.ts— implements bothFloatingDesktopSurfaceandAlwaysFullScreenDesktopSurface, chosen bysession.config.mode. Floating lets the user move/resize windows freely; AlwaysFullScreen enforces one window per output (kiosk mode).
Dependencies: Layer 6 + Layer 5 (for View rendering).
Layer 8 — User Shell
Responsibility: the actual user-facing UI. The taskbar, window decorations, app launcher, keyboard layout picker, etc.
Out of scope for the user — they will build their own shell as an Odoo 20
OWL addon. The integration point is UserShellApi.ts (see
08-app-lifecycle.md).
Greenfield's reference shell (packages/compositor-shell) is a Preact +
Tailwind SPA, 562 lines of TypeScript total. Mentioned here only for
orientation; not analyzed in this bundle.
How data flows across the layers
NATIVE APP (e.g. gtk4-demo, X11 via XWayland)
│
│ libwayland-client → UNIX socket + SCM_RIGHTS FDs
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ NATIVE PROXY (Node.js + C N-API addons) │
│ │
│ Layer 3 (native): wayland-server.ts + wayland-server-addon.node │
│ ↳ patched libwayland-server parses wire bytes │
│ ↳ MessageInterceptor routes each request to {native, browser} │
│ ↳ FDs wrapped as ProxyFD JSON │
│ │
│ Layer 4 (native): Channel.ts + kcp.ts + SessionController │
│ ↳ 4 logical channels: PROTOCOL, FRAME, XWM, FEEDBACK │
│ ↳ KCP-over-WebSocket for ARQ channels │
│ ↳ SimpleChannel for FEEDBACK │
│ │
│ Layer 5 (encoding): SurfaceBufferEncoding + proxy-encoding-addon │
│ ↳ GStreamer H.264 (x264 / nvh264 / vaapih264) on dedicated thread │
│ ↳ PNG fallback for ≤256×256 surfaces │
│ ↳ Zero-copy dmabuf via EGLImage → GstGLMemoryEGL │
│ │
│ Layer 1 (XML) → Layer 2 (proxy-generator) feeds Layer 3 interceptors │
└─────────────────────────────────────────────────────────────────────────┘
│
│ WebSocket (TCP) + KCP-over-WebSocket
│ 9 endpoint paths per session
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ BROWSER COMPOSITOR (TypeScript + WebGL + WebCodecs + WASM) │
│ │
│ Layer 4 (browser): Channel.ts + RemoteAppLauncher + decoders │
│ ↳ ReconnectingWebSocket with exponential backoff │
│ ↳ ARQChannel + SimpleChannel mirror the native side │
│ ↳ WebCodecs VideoDecoder preferred, FFmpeg WASM fallback │
│ ↳ H264NALDecoder.worker.ts (2 workers: opaque + alpha) │
│ │
│ Layer 3 (browser): @gfld/common Connection + compositor-protocol │
│ ↳ Standard Wayland wire format, byte-for-byte │
│ ↳ WlObject registry, opcode dispatch via wlObject[opcode](msg) │
│ ↳ Per-client Connections (slow client never blocks another) │
│ │
│ Layer 5: Surface.ts + View.ts + Output.ts + render/ │
│ ↳ Double-buffered state (state vs pendingState) │
│ ↳ Damage tracking via pixman-WASM regions │
│ ↳ WebGL1 with FBOs for YUV→RGBA, RGB+Alpha→RGBA conversion │
│ ↳ desynchronized:true canvas for low-latency presentation │
│ ↳ Frame callbacks fired on requestAnimationFrame vsync │
│ │
│ Layer 6: XdgWmBase + XdgSurface + XdgToplevel + XdgPopup + ... │
│ ↳ Configure/ack-configure round-trips │
│ ↳ Popup positioner math (anchor + gravity + constraints) │
│ ↳ Subsurface sync/desync mode cascading │
│ │
│ Layer 7: desktop/Desktop.ts │
│ ↳ Floating vs AlwaysFullScreen mode │
│ ↳ View stacking, focus refcount │
│ │
│ Layer 8: UserShellApi (events + actions) │
│ ↳ Events: clientCreated, surfaceCreated, surfaceTitleUpdated, │
│ surfaceActivationUpdated, ... │
│ ↳ Actions: initScene, refreshScene, destroyScene, │
│ closeClient, activateSurface, setUserConfiguration │
│ ↳ THE INTEGRATION SEAM — Odoo 20 OWL shell hooks here │
└─────────────────────────────────────────────────────────────────────────┘
│
│ UserShellApi.events.* fired by compositor
│ UserShellApi.actions.* called by shell
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ USER SHELL (Odoo 20 OWL addon — supplied by user) │
│ │
│ - Application launcher (lists apps from --applications JSON) │
│ - Window management UI (taskbar, decorations, minimize/maximize) │
│ - Keyboard layout picker (RMLVO) │
│ - Per-output canvas (HTMLCanvasElement, one per Output) │
│ - Trigger app launches via fetch('http://proxy:8081/<path>') │
│ - Open signalURL WebSocket to receive CONNECT_CHANNEL messages │
│ │
│ Not analyzed in this bundle (out of scope). │
└─────────────────────────────────────────────────────────────────────────┘
Reading guide for the rest of the bundle
Now that you've seen the layer map, the remaining chapters drill into each layer in detail:
02-wayland-protocol.md— Layers 1, 2, 3 (XML → generators → runtime)03-html5-rendering.md— Layer 5 (rendering pipeline)04-transport.md— Layer 4 (transport, both sides)05-input-handling.md— Layer 5 input subsystem (cross-cuts Layers 3, 4, 5)06-data-clipboard.md— Layer 5 data device (cross-cuts Layers 3, 4, 5)07-native-app-bridge.md— Layer 4 native + Layer 3 native + Layer 1 XML native (proxy-onlywl_drm)08-app-lifecycle.md— Layer 6 + Layer 8 (UserShellApi seam)09-hardware-accel.md— Layer 5 encoding + Layer 4 frame channel + Layer 3 WASM10-porting-to-odoo-js-python.md— how to rebuild all 8 layers in your stack11-pitfalls.md— what Greenfield does wrong, layer by layer12-appendix.md— glossary, file index, external references