Cat Pranks

Cat Pranks

Wayward Griffin

waygriff is a local display proxy. It captures frames from an X11 session and displays them in a Wayland window on the same computer. Mouse and keyboard inputs are forwarded in the other direction.

https://github.com/catpranks/WaywardGriffin

Unsolved problem in web platform engineering

Suppose you want to write a video player for your web page. Since it's 2025 and you're a Web Platform enthusiast, you reach for the latest and greatest APIs: Streams and WebCodecs. You skim through the docs and realize how well the pieces fit together. Even the official Chrome blog recommends this approach. You just need some classes to represent demuxer, decoder and renderer nodes, start each one in a worker, connect them with streams. Then it's a small matter of playing Factorio with stream buffer sizes until everything flows just right.

You spend the next few hours yelling at Gemini to produce a prototype... TypeScript is so cool, maybe you'll learn it one day. It takes you some effort and a few exploratory examples (thanks, chatbot) to piece together VideoDecoder dequeue events and ReadableStream pull callbacks. Your robot pal is no help, he only rememebers half the spec and is more than happy to suggest polling every 5ms. It all makes sense in the end though. Those web standards and browser people are so thoughtful.

You reload the page for the last time and it stalls after rendering only a few frames. VideoDecoder.decodeQueueSize is staying at 1 and not producing any frames. After a few seconds, two A VideoFrame was garbage collected without being closed. Applications should call close() on frames when done with them to prevent stalls messages show up on the console. You did close your VideoFrames manually, didn't you? Yes, of course you did.

You mutter an earnest wish for amdgpu maintainers to step on a Lego and spend the next hour mutating your program in increasingly nonsensical ways hoping to reveal the root cause. One hint is the DataCloneError: A VideoFrame could not be cloned because it was closed error you get reading the frame if you close it before enqueueing.

... that's right, there is no transfer mechanism in Streams. VideoFrame is both transferrable and clonable. The sending worker adds a reference to the stream's buffer. The receiving worker clones the VideoFrame, producing a second handle to the same hardware resource. The original handle goes into GC where it hogs a resource from the decoder buffer pool until you manually click the trash button in devtools.

Naturally, the streams repo has an open issue from 2021 and un-merged PR from 2023 for exactly this problem.