Files
one-os-dev-wayland-html5-co…/11-pitfalls.md
T

21 KiB
Raw Blame History

11 — Pitfalls, Anti-patterns, and What to Avoid

This is the prominent pitfalls chapter. Greenfield's known weaknesses, anti-patterns, and design pitfalls are catalogued here so the user can avoid making the same mistakes in their own Wayland-to-HTML compositor (Odoo 20 + JS/Python). Each pitfall comes with: what Greenfield does (with file/directory reference), why it's a problem (technical reason), and what the user should do instead (concrete recommendation for their JS/Python + Odoo 20 stack).

Headline numbers

  • 299 TODO|FIXME|HACK|XXX|@deprecated|workaround hits across non-vendored source (after excluding examples/sdk/* upstreams and xtsb/protocol/xproto.xml).
  • 13 packages all pinned to "version": "1.0.0-rc1" — no CHANGELOG.md, no SECURITY.md, no CONTRIBUTING.md, no issue/PR templates.
  • 1 commit visible on master (6c578f4, "Fix typo in code snippet (#161)", 2025-10-15) — implying the public repo is a release snapshot, not the live dev branch.
  • 3 native Node .node addons, all Linux-x64 only.
  • 0 uses of OffscreenCanvas or transferControlToOffscreen anywhere in the compositor.
  • 2 Web Workers used — and only for the WASM H.264 fallback decoder path. The primary WebCodecs path uses none.

Catalogued pitfalls (A–U)

Critical (will bite you on day 1)

A. Native Node addons

What Greenfield does: Three Node-API addons:

  • wayland-server-addon.node (1231 lines C, wraps patched libwayland-server)
  • proxy-encoding-addon.node (290 lines C, wraps GStreamer)
  • proxy-poll-addon.node (127 lines C, wraps libuv uv_poll_t)

All Linux-x64 only, built with CMake + Ninja. ABI-locked to Node major versions.

Why it's a problem:

  • Linux-x64 only — no ARM64 support
  • ABI breaks on Node major upgrades
  • Hard to ship — must rebuild for each target platform
  • Hard to debug — C N-API debugging requires core dumps
  • GStreamer integration is via C, not Python
  • No Python story

What you should do instead: Use pywayland (cffi bindings to libwayland-server), python-xkbcommon, and GStreamer's Python bindings (gi.repository.Gst). One Python process can replace the entire compositor-proxy-cli. See 10-porting-to-odoo-js-python.md for details.

B. rc1 with no policy

What Greenfield does: All 13 packages at 1.0.0-rc1. No CHANGELOG, no SECURITY.md, no CONTRIBUTING.md.

Why it's a problem: Breaking changes can land silently. The maintainer's roadmap is unclear.

What you should do instead: Pin to a specific commit hash if you fork it. Track upstream commits manually. Plan for the eventuality that 1.0.0 final may break APIs.

C. Encoding rigidity

What Greenfield does: Three H.264 GStreamer pipelines (x264/nvh264/ vaapih264) with hand-tuned magic constants:

  • bitrate=12000 vbv-buf-capacity=160 bframes=0 rc-lookahead=0
  • VA-API is "experimental"
  • AMD (vah264enc) absent
  • PNG fallback at hardcoded ≤256×256 threshold
  • Browser-side hardware decode is explicitly disabled in Session.ts:93 with a FIXME "hardware decoding frame offset is wrong"

Why it's a problem:

  • No adaptive bitrate — video plays at 12 Mbps regardless of network conditions
  • No AMD support — limits hardware encoder options
  • Browser decode is software-only — wastes GPU

What you should do instead:

  • Build an adaptive multi-codec encoding router (PNG for ≤64×64 or static, VP8/H.264 for mid-size, H.264/AV1 for video)
  • Drive the router by the EncoderFeedback measurement Greenfield already produces but doesn't consume
  • Use pykcp or similar for congestion-aware bitrate adaptation
  • Enable browser hardware decode (prefer-hardware) — fix the "frame offset is wrong" bug

D. Wire protocol has no schema version field

What Greenfield does: No version prefix on any binary message:

  • EncodedFrame (FRAME channel)
  • RemoteOutOfBandChannel (PROTOCOL channel OOB)
  • The FD-channel in RemoteAppLauncher.ts:612
  • Undocumented magic close codes (4200, 4201, 4403, 4001, 1005)

All break-lockstep — if you change the wire format, both sides must upgrade simultaneously or messages will be misinterpreted.

Why it's a problem:

  • Cannot evolve the protocol without breaking all clients
  • Cannot detect version mismatches
  • Hard to debug — close codes are magic numbers

What you should do instead:

  • Prefix every binary message with u16 protocolVersion
  • Document every close code in a comment
  • Use JSON {"v":1, "type":"..."} for the control plane
  • Build a feature-flag system so old clients can negotiate capabilities

E. Main-thread decode + render

What Greenfield does:

  • VideoDecoder runs on main thread
  • WebGL context bound to HTMLCanvasElement
  • No OffscreenCanvas
  • No Worker in render path
  • 0 uses of OffscreenCanvas or transferControlToOffscreen in the compositor
  • Only 2 Web Workers used (FFmpeg Wasm fallback), shared across surfaces

Why it's a problem:

  • Jank on 1080p surfaces — main thread blocks on decode
  • No way to recover if main thread stalls (input also blocked)
  • Can't use requestAnimationFrame for compositing if main thread is busy

What you should do instead:

  • Render in a Web Worker with OffscreenCanvas from day one
  • Ship EncodedVideoChunk / ArrayBuffer transfers (zero-copy) to the worker
  • Use VideoFrame directly as a WebGL texture source — no copyTo
  • Use multiple workers (one per output) for parallel rendering

High severity

F. xkbcommon Wasm is multi-MB

What Greenfield does: libs/compositor-wasm/xkbcommon/build.sh clones libxkbcommon 1.5.0, embeds the entire xkeyboard-config database via --embed-file. Result: multi-MB WASM module loaded on every page.

Why it's a problem:

  • Slow initial load
  • Layout list parsed via fragile string matching
  • Hard to update keyboard layouts without rebuilding WASM

What you should do instead:

  • Use the browser's navigator.keyboard.getLayoutMap() API for basic layout detection
  • For full xkbcommon support, lazy-load the WASM only when needed
  • Consider server-side keymap compilation (Python python-xkbcommon) and ship only the result string to the browser

G. Clipboard edge cases

What Greenfield does:

  • Hardcoded MIME list ['text/plain', 'text/html', 'image/png']
  • No primary selection (wp_primary_selection_v1 not implemented)
  • Change detection by blob size (false negatives on same-size different content)
  • Focus check at DataOffer.ts:73 is a FIXME
  • Browser-side clipboard polling (no clipboardchange event)

Why it's a problem:

  • Middle-click paste doesn't work (Linux users expect it)
  • Large clipboard content (e.g. copied image) is poorly handled
  • MIME negotiation races

What you should do instead:

  • Implement zwp_primary_selection_device_manager_v1 from day one
  • Use exact MIME matching, not substring
  • For large clipboard content, use chunked transfer
  • Watch for the clipboardchange event in modern browsers (currently behind flags in Chrome)

H. XWayland/xtsb complexity

What Greenfield does:

  • A full XCB reimplementation in TypeScript with its own 5800-line xproto.xml
  • The XWM is a hand-port of wlroots' xwm.c with FIXMEs pointing at specific wlroots commits
  • XWindowManager.ts:746 literally says "this makes xterm close somehow"
  • Title bars painted pixel-by-pixel in XWindowFrame.ts

Why it's a problem:

  • 5800 lines of X11 protocol XML to maintain
  • Hand-ported XWM logic is fragile and out of sync with upstream wlroots
  • Magic workarounds in code

What you should do instead:

  • Use python-xlib on the Python proxy side instead of porting xtsb
  • Consider running XWayland with a minimal window manager (e.g. mutter in kiosk mode) instead of implementing XWM yourself
  • For XWayland apps only (no full X11 desktop needed), the XWM can be much simpler than wlroots'

I. Adaptive bitrate feedback loop is wired but not consumed

What Greenfield does:

  • EncoderFeedback.ts measures client decode duration at 1 Hz
  • 4-byte feedback messages sent from browser to proxy
  • Proxy receives them but does NOT use them to adapt bitrate
  • Channel.ts:181 has a TODO confirming the gap

Why it's a problem:

  • Video plays at fixed 12 Mbps regardless of network conditions
  • No way to recover from congestion
  • Wasted bandwidth on slow networks

What you should do instead:

  • Wire EncoderFeedback to a bitrate adaptation controller
  • Use GstElement.set_property("bitrate", ...) to adjust x264enc/nvh264enc
  • Implement requestKeyUnit plumbing (Greenfield has it end-to-end but the browser never calls it)

J. Single-seat, single-session model

What Greenfield does:

  • One seat per browser session
  • One Node process per browser session (per compositorSessionId)
  • No multi-seat support (e.g. multiple keyboards/mice for collaborative editing)
  • No multi-region isolation

Why it's a problem:

  • Can't support multi-user scenarios (e.g. conference room with multiple keyboards)
  • No way to isolate apps between tenants

What you should do instead:

  • For multi-tenant isolation: separate Python proxy processes per tenant, containerized (Docker + bubblewrap)
  • For multi-seat: implement wl_seat global per seat, route input events by seat

K. Custom web-bitmapbuf protocol — apps must be Greenfield-aware

What Greenfield does:

  • protocol/web-bitmapbuf-unstable-v1.xml is Greenfield's custom extension
  • Lets browser JS apps hand a GPU-backed ImageBitmap directly to the compositor
  • Standard Wayland apps (even browser-resident ones) fall to slow H.264 path

Why it's a problem:

  • Apps must be rebuilt against @gfld/client-protocol to use the fast path
  • Standard Wayland apps can't take advantage of zero-copy GPU textures
  • Browser-resident WASM apps are slow unless rebuilt

What you should do instead:

  • Document clearly that the fast path requires Greenfield-aware apps
  • For native Linux apps (your main use case), this isn't an issue — they go through the H.264 path regardless
  • For browser-resident WASM apps, encourage users to build against @gfld/client-protocol

L. Connection loss handling

What Greenfield does:

  • 10-minute zombie timeout on disconnect (NativeAppContext.ts:152-160)
  • No state resumption on reconnect
  • eagerBufferContents cache in BufferStream.ts:31 is unbounded (TODO at :87 confirms the leak)
  • App kill timers: SIGHUP@10min, SIGKILL@+10s
  • Close code 1001 → immediate SIGHUP; 4001 → immediate SIGTERM

Why it's a problem:

  • User loses all running apps on browser refresh (unless they reconnect within 10 min)
  • Memory leak from unbounded cache
  • No way to survive proxy restart

What you should do instead:

  • Bound the eagerBufferContents cache (e.g. max 100 frames)
  • Persist app state on browser side (localStorage) so users can resume
  • For proxy restart, checkpoint app state to disk (json file)
  • Implement graceful reconnection with state sync (the KCP plumbing exists)

M. No sandboxing

What Greenfield does:

  • spawn(executable, args, { env }) with no bubblewrap/seccomp/namespaces
  • Basic Auth is a single replayable credential compared with !==
  • compositorSessionId is client-generated

Why it's a problem:

  • Native apps have full access to the host filesystem
  • One compromised app can read all other apps' data
  • Basic Auth credentials can be replayed
  • Client-generated session IDs can be spoofed

What you should do instead:

  • Sandbox every spawned app with bubblewrap + seccomp + per-session Linux user namespace
  • Replace Basic Auth with short-lived signed JWT bound to Odoo's session model
  • Server-generated session IDs (use secrets.token_urlsafe(32))

N. Heavy toolchain

What Greenfield does:

  • Yarn 4 + CMake + Ninja + Meson + pkg-config
  • Two different Emscripten versions: 3.1.46 for FFmpeg, 3.1.60 for SDK
  • SDK builds 30+ C libraries from source
  • gtk4 WASM is currently broken
  • Native build needs autoconf libtool automake meson ninja-build bison cmake build-essential gperf

Why it's a problem:

  • Complex build environment setup
  • Long build times
  • Emscripten version skew between libs and SDK
  • gtk4 WASM broken means no GTK4 browser apps

What you should do instead:

  • Stick with a single Emscripten version
  • Pre-build WASM modules and ship them as artifacts (don't rebuild every time)
  • For Python, use pip install for all dependencies (simpler than CMake
    • Ninja)
  • Skip the WASM app SDK entirely if you only care about native Linux apps

Medium / low (but worth fixing)

O. WebCodecs usage

What Greenfield does:

  • WebCodecs IS used (correcting the brief in some agents' notes)
  • BUT HW-decode is disabled
  • VideoEncoder is unused

Why it's a problem:

  • Wastes GPU on software decode
  • Could encode in browser (for upload to other peers) but doesn't

What you should do instead:

  • Enable HW-decode by fixing the "frame offset is wrong" FIXME
  • Consider WebCodecs VideoEncoder for any peer-to-peer video paths

P. WebCodecs hardware decode disabled

What Greenfield does:

  • Session.ts:93 has // FIXME hardware decoding frame offset is wrong
  • hardwareAcceleration: 'prefer-software' is hardcoded

Why it's a problem:

  • Software decode at 1080p60 is ~30% CPU on a modern laptop
  • Battery life impact on mobile
  • Wastes GPU

What you should do instead:

  • Investigate and fix the frame offset bug
  • Enable prefer-hardware for capable browsers
  • Provide a runtime toggle

Q. Unbounded eagerBufferContents cache

What Greenfield does:

  • BufferStream.ts:31 has unbounded cache
  • TODO at :87 confirms the leak

Why it's a problem:

  • Memory leak on long-running sessions
  • Can OOM the browser tab

What you should do instead:

  • Bound the cache to a maximum number of frames (e.g. 100)
  • Use LRU eviction
  • Periodic cleanup of orphaned entries

R. H264NALDecoder.worker.ts is minimal

What Greenfield does:

  • 22 lines of code
  • @ts-ignore'd
  • No error boundary

Why it's a problem:

  • Worker crashes silently
  • No retry on transient errors
  • No metrics on decode failures

What you should do instead:

  • Add error boundary with retry logic
  • Log decode failures to the proxy (for diagnostics)
  • Pool workers for resilience

S. Magic undocumented WebSocket close codes

What Greenfield does:

  • Close codes 4200, 4201, 4403, 4001, 1005 are used without documentation
  • Only codes 4000-4999 (application-level) tear down KCP

Why it's a problem:

  • Hard to debug connection issues
  • Close codes are magic numbers

What you should do instead:

  • Document every close code in code comments
  • Use named constants (e.g. CLOSE_APP_TERMINATED = 4001)
  • Build a close-code → human-readable mapping

T. Generated protocol files committed

What Greenfield does:

  • packages/compositor-proxy/src/protocol/*.js is generated but committed to the repo
  • Can drift from XML source

Why it's a problem:

  • Easy to forget to regenerate
  • PRs may include stale generated code

What you should do instead:

  • Don't commit generated files
  • Use prebuild script to generate them
  • Add a CI check that regenerated files match committed ones

U. Confirmed copy-paste bug in webcodec-buffer-decoder.ts

What Greenfield does:

  • webcodec-buffer-decoder.ts:142-144, 156-157 has: this.opaqueDecoder = undefined twice
  • Should be alphaDecoder the second time
  • Leaks the alpha decoder after errors

Why it's a problem:

  • Memory leak
  • Wasm decoder context not cleaned up properly
  • Can cause decoder pool exhaustion

What you should do instead:

  • Audit all cleanup paths in decoder code
  • Add unit tests for error scenarios

Design wins to copy

Not all of Greenfield's choices are bad. Here are 12 design wins worth copying verbatim:

  1. Generator-driven protocol stubs (libs/compositor-generator/, client-generator/, compositor-proxy-generator/) — single XML source for both sides. Eliminates wire-format drift.
  2. Dual-buffer commit semantics (Surface.ts: state vs pendingState, atomic merge with pixman damage regions). Classic Wayland correctness.
  3. UserShellApi decoupling — shell sees only {events, actions}, never Wayland resources. This is THE integration seam you'll use.
  4. Asynchronous browser compositor — per-client channels, never blocks on a slow client. Essential for multi-app scenarios.
  5. KCP-over-WebSocket for UDP-like latency over TCP (kcp.ts, 1170 lines). Better than plain TCP for real-time video.
  6. WebCodecs primary + WASM fallback + capability probe (Session.ts). Modern browsers get the fast path, older ones still work.
  7. Zero-copy dmabuf → encoder via standard linux-dmabuf-unstable-v1 (westfield-dmabuf.c). Pixels stay on GPU.
  8. Per-surface encoder feedback channel (EncoderFeedback.ts) measuring client decode duration — even though adaptation isn't wired up yet, the telemetry is right.
  9. ReconnectingWebSocket with exponential backoff + message queue. Survives transient network drops.
  10. Minimal shell — 562 lines of TypeScript total in compositor-shell; kiosk variant is 40 lines. Demonstrates that the shell can be tiny.
  11. Honest docs — docs/pages/design/ openly admits WebSocket/TCP is wrong for real-time and waits on WebTransport. Useful as a reference for what trade-offs were considered.
  12. AGPL-3.0 — deliberate copyleft, but be aware before integrating. If you want to keep your modifications proprietary, fork the MIT libs and reimplement the AGPL parts.

Top-5 recommendations for the user's Odoo 20 + JS/Python compositor

  1. Skip native Node addons entirely. Use pywayland (cffi), python-xkbcommon, and GStreamer's gi.repository.Gst Python bindings. One Python process can replace the entire compositor-proxy-cli. See 10-porting-to-odoo-js-python.md for details.

  2. Sandbox every spawned app with bubblewrap + seccomp + per-session Linux user namespace; replace Basic Auth with short-lived signed JWT bound to Odoo's session model.

  3. Render in a Web Worker with OffscreenCanvas from day one; ship EncodedVideoChunk / ArrayBuffer transfers (zero-copy) to the worker; use VideoFrame directly as a WebGL texture source — no copyTo.

  4. Build an adaptive multi-codec encoding router (PNG for ≤64×64 or static, VP8/H.264 for mid-size, H.264/AV1 for video), driven by the EncoderFeedback measurement Greenfield already produces but doesn't consume.

  5. Version your wire protocol from commit #1 — prefix every binary message with u16 protocolVersion; document every close code; use JSON {"v":1, "type":"..."} for the control plane.

License implications of AGPL-3.0

The four packages/* (compositor, compositor-proxy, compositor-proxy-cli, compositor-shell) are AGPL-3.0-or-later. Implications:

  • If you host Greenfield's compositor-proxy as a network service and modify it, you MUST release your modifications under AGPL-3.0+.
  • The browser-side @gfld/compositor is also AGPL. If you modify it (e.g. to integrate with Odoo), your modifications must be AGPL.
  • The lower-level libs/* (common, client-protocol, compositor-protocol, the generators, compositor-wasm, compositor-ffmpeg-h264, xtsb) are MIT or Apache-2.0 — these are safe to use without copyleft obligations.
  • Strategy to avoid AGPL: use the MIT libs as-is, reimplement the AGPL parts from scratch (browser compositor + native proxy). The Python port described in 10-porting-to-odoo-js-python.md is a clean way to do this — if you write your Python proxy from scratch, you can license it however you want.

What Greenfield's codebase reveals about its engineering process

The codebase reveals several engineering process insights:

  1. One-person show — most commits are by Erik De Rijcke. The codebase has the strengths (consistent vision, tight integration) and weaknesses (single point of failure, no code review diversity) of a one-person project.
  2. Long-running project — code references back to 2017-era commits (WebRTC was a previous transport; the kcp.ts MTU comment says "webrtc datachannel MTU"). The architecture has evolved through several major pivots.
  3. rc1 is meaningful — the maintainer has been working toward 1.0 for years and rc1 represents a deliberate freeze. Don't expect 1.0 final any time soon, but also don't expect major architecture changes.
  4. Honest about limitations — docs/pages/design/ explicitly admits what's wrong and what's planned. Use this to inform your own roadmap.