From 868a0be71bf66c521b1fe85fb42e8c240841597b Mon Sep 17 00:00:00 2001
From: Cursor Agent <cursoragent@cursor.com>
Date: Tue, 30 Dec 2025 03:23:51 +0000
Subject: [PATCH 01/12] Refactor frame processing and shared buffer handling

Remove frame padding stripping logic.
Update frame processing to use stride and raw data.
Improve shared buffer consumer with readInto and slot size.
Increase shared buffer slot size.
Update WebGPU renderer to use stride for bytesPerRow.
Use Arc for decoded frame data to avoid cloning.

Co-authored-by: richiemcilroy1 <richiemcilroy1@gmail.com>
---
 apps/desktop/src-tauri/src/editor_window.rs   | 61 ++++--------
 apps/desktop/src-tauri/src/frame_ws.rs        |  1 +
 apps/desktop/src/utils/frame-worker.ts        | 96 +++++++++++--------
 apps/desktop/src/utils/shared-frame-buffer.ts | 96 ++++++++++++++++++-
 apps/desktop/src/utils/socket.ts              |  2 +-
 apps/desktop/src/utils/webgpu-renderer.ts     |  7 +-
 crates/rendering/src/decoder/ffmpeg.rs        | 12 ++-
 crates/rendering/src/frame_pipeline.rs        |  3 +-
 8 files changed, 182 insertions(+), 96 deletions(-)

diff --git a/apps/desktop/src-tauri/src/editor_window.rs b/apps/desktop/src-tauri/src/editor_window.rs
index 66ea67b975..4fedba9655 100644
--- a/apps/desktop/src-tauri/src/editor_window.rs
+++ b/apps/desktop/src-tauri/src/editor_window.rs
@@ -11,31 +11,6 @@ use crate::{
     frame_ws::{WSFrame, create_watch_frame_ws},
 };
 
-fn strip_frame_padding(frame: RenderedFrame) -> Result<(Vec<u8>, u32), &'static str> {
-    let expected_stride = frame
-        .width
-        .checked_mul(4)
-        .ok_or("overflow computing expected_stride")?;
-
-    if frame.padded_bytes_per_row == expected_stride {
-        Ok((frame.data, expected_stride))
-    } else {
-        let capacity = expected_stride
-            .checked_mul(frame.height)
-            .ok_or("overflow computing buffer capacity")?;
-        let mut stripped = Vec::with_capacity(capacity as usize);
-        for row in 0..frame.height {
-            let start = row
-                .checked_mul(frame.padded_bytes_per_row)
-                .ok_or("overflow computing row start")? as usize;
-            let end = start + expected_stride as usize;
-            stripped.extend_from_slice(&frame.data[start..end]);
-        }
-
-        Ok((stripped, expected_stride))
-    }
-}
-
 pub struct EditorInstance {
     inner: Arc<cap_editor::EditorInstance>,
     pub ws_port: u16,
@@ -58,15 +33,15 @@ async fn do_prewarm(app: AppHandle, path: PathBuf) -> PendingResult {
         Box::new(move |frame| {
             let width = frame.width;
             let height = frame.height;
-            if let Ok((data, stride)) = strip_frame_padding(frame)
-                && let Err(e) = frame_tx.send(Some(WSFrame {
-                    data,
-                    width,
-                    height,
-                    stride,
-                    created_at: Instant::now(),
-                }))
-            {
+            let stride = frame.padded_bytes_per_row;
+            let data = frame.data;
+            if let Err(e) = frame_tx.send(Some(WSFrame {
+                data,
+                width,
+                height,
+                stride,
+                created_at: Instant::now(),
+            })) {
                 debug!("Frame receiver dropped during prewarm: {e}");
             }
         }),
@@ -220,15 +195,15 @@ impl EditorInstances {
                     Box::new(move |frame| {
                         let width = frame.width;
                         let height = frame.height;
-                        if let Ok((data, stride)) = strip_frame_padding(frame)
-                            && let Err(e) = frame_tx.send(Some(WSFrame {
-                                data,
-                                width,
-                                height,
-                                stride,
-                                created_at: Instant::now(),
-                            }))
-                        {
+                        let stride = frame.padded_bytes_per_row;
+                        let data = frame.data;
+                        if let Err(e) = frame_tx.send(Some(WSFrame {
+                            data,
+                            width,
+                            height,
+                            stride,
+                            created_at: Instant::now(),
+                        })) {
                             debug!("Frame receiver dropped in get_or_create: {e}");
                         }
                     }),
diff --git a/apps/desktop/src-tauri/src/frame_ws.rs b/apps/desktop/src-tauri/src/frame_ws.rs
index 93146003f1..1d30d1c42d 100644
--- a/apps/desktop/src-tauri/src/frame_ws.rs
+++ b/apps/desktop/src-tauri/src/frame_ws.rs
@@ -3,6 +3,7 @@ use tokio::sync::{broadcast, watch};
 use tokio_util::sync::CancellationToken;
 
 fn pack_frame_data(mut data: Vec<u8>, stride: u32, height: u32, width: u32) -> Vec<u8> {
+    data.reserve_exact(12);
     data.extend_from_slice(&stride.to_le_bytes());
     data.extend_from_slice(&height.to_le_bytes());
     data.extend_from_slice(&width.to_le_bytes());
diff --git a/apps/desktop/src/utils/frame-worker.ts b/apps/desktop/src/utils/frame-worker.ts
index 5b61309b6f..70948b3ac3 100644
--- a/apps/desktop/src/utils/frame-worker.ts
+++ b/apps/desktop/src/utils/frame-worker.ts
@@ -98,6 +98,7 @@ interface PendingFrameWebGPU {
 	data: Uint8ClampedArray;
 	width: number;
 	height: number;
+	strideBytes: number;
 }
 
 type PendingFrame = PendingFrameCanvas2D | PendingFrameWebGPU;
@@ -125,14 +126,13 @@ let lastRawFrameData: Uint8ClampedArray | null = null;
 let lastRawFrameWidth = 0;
 let lastRawFrameHeight = 0;
 
-let webgpuFrameBuffer: Uint8ClampedArray | null = null;
-let webgpuFrameBufferSize = 0;
-
 let frameDropCount = 0;
 let lastFrameDropLogTime = 0;
 
 let consumer: Consumer | null = null;
 let useSharedBuffer = false;
+let sharedReadBuffer: Uint8Array | null = null;
+let sharedReadBufferSize = 0;
 
 let pendingRenderFrame: PendingFrame | null = null;
 let _rafId: number | null = null;
@@ -156,7 +156,13 @@ function renderLoop() {
 		pendingRenderFrame = null;
 
 		if (frame.mode === "webgpu" && webgpuRenderer) {
-			renderFrameWebGPU(webgpuRenderer, frame.data, frame.width, frame.height);
+			renderFrameWebGPU(
+				webgpuRenderer,
+				frame.data,
+				frame.width,
+				frame.height,
+				frame.strideBytes,
+			);
 		} else if (frame.mode === "canvas2d" && offscreenCanvas && offscreenCtx) {
 			if (
 				offscreenCanvas.width !== frame.width ||
@@ -210,6 +216,8 @@ function cleanup() {
 	offscreenCtx = null;
 	consumer = null;
 	useSharedBuffer = false;
+	sharedReadBuffer = null;
+	sharedReadBufferSize = 0;
 	pendingRenderFrame = null;
 	lastImageData = null;
 	cachedImageData = null;
@@ -220,8 +228,6 @@ function cleanup() {
 	lastRawFrameData = null;
 	lastRawFrameWidth = 0;
 	lastRawFrameHeight = 0;
-	webgpuFrameBuffer = null;
-	webgpuFrameBufferSize = 0;
 	frameDropCount = 0;
 	lastFrameDropLogTime = 0;
 }
@@ -298,6 +304,7 @@ async function initCanvas(canvas: OffscreenCanvas): Promise<void> {
 				lastRawFrameData,
 				lastRawFrameWidth,
 				lastRawFrameHeight,
+				lastRawFrameWidth * 4,
 			);
 			self.postMessage({
 				type: "frame-rendered",
@@ -337,21 +344,24 @@ async function initCanvas(canvas: OffscreenCanvas): Promise<void> {
 
 type DecodeResult = FrameQueuedMessage | DecodedFrame | ErrorMessage;
 
-async function processFrame(buffer: ArrayBuffer): Promise<DecodeResult> {
-	const data = new Uint8Array(buffer);
-	if (data.length < 12) {
+async function processFrameBytes(bytes: Uint8Array): Promise<DecodeResult> {
+	if (bytes.byteLength < 12) {
 		return {
 			type: "error",
 			message: "Received frame too small to contain metadata",
 		};
 	}
 
-	const metadataOffset = data.length - 12;
-	const meta = new DataView(buffer, metadataOffset, 12);
+	const metadataOffset = bytes.byteOffset + bytes.byteLength - 12;
+	const meta = new DataView(bytes.buffer, metadataOffset, 12);
 	const strideBytes = meta.getUint32(0, true);
 	const height = meta.getUint32(4, true);
 	const width = meta.getUint32(8, true);
-	const frameData = new Uint8ClampedArray(buffer, 0, metadataOffset);
+	const frameData = new Uint8ClampedArray(
+		bytes.buffer,
+		bytes.byteOffset,
+		bytes.byteLength - 12,
+	);
 
 	if (!width || !height) {
 		return {
@@ -375,25 +385,6 @@ async function processFrame(buffer: ArrayBuffer): Promise<DecodeResult> {
 		};
 	}
 
-	let processedFrameData: Uint8ClampedArray;
-	if (strideBytes === expectedRowBytes) {
-		processedFrameData = frameData.subarray(0, expectedLength);
-	} else {
-		if (!strideBuffer || strideBufferSize < expectedLength) {
-			strideBuffer = new Uint8ClampedArray(expectedLength);
-			strideBufferSize = expectedLength;
-		}
-		for (let row = 0; row < height; row += 1) {
-			const srcStart = row * strideBytes;
-			const destStart = row * expectedRowBytes;
-			strideBuffer.set(
-				frameData.subarray(srcStart, srcStart + expectedRowBytes),
-				destStart,
-			);
-		}
-		processedFrameData = strideBuffer.subarray(0, expectedLength);
-	}
-
 	if (renderMode === "webgpu" && webgpuRenderer) {
 		if (pendingRenderFrame !== null) {
 			frameDropCount++;
@@ -409,21 +400,35 @@ async function processFrame(buffer: ArrayBuffer): Promise<DecodeResult> {
 			}
 		}
 
-		if (!webgpuFrameBuffer || webgpuFrameBufferSize < expectedLength) {
-			webgpuFrameBuffer = new Uint8ClampedArray(expectedLength);
-			webgpuFrameBufferSize = expectedLength;
-		}
-		webgpuFrameBuffer.set(processedFrameData);
-
 		pendingRenderFrame = {
 			mode: "webgpu",
-			data: webgpuFrameBuffer,
+			data: frameData.subarray(0, availableLength),
 			width,
 			height,
+			strideBytes,
 		};
 		return { type: "frame-queued", width, height };
 	}
 
+	let processedFrameData: Uint8ClampedArray;
+	if (strideBytes === expectedRowBytes) {
+		processedFrameData = frameData.subarray(0, expectedLength);
+	} else {
+		if (!strideBuffer || strideBufferSize < expectedLength) {
+			strideBuffer = new Uint8ClampedArray(expectedLength);
+			strideBufferSize = expectedLength;
+		}
+		for (let row = 0; row < height; row += 1) {
+			const srcStart = row * strideBytes;
+			const destStart = row * expectedRowBytes;
+			strideBuffer.set(
+				frameData.subarray(srcStart, srcStart + expectedRowBytes),
+				destStart,
+			);
+		}
+		processedFrameData = strideBuffer.subarray(0, expectedLength);
+	}
+
 	if (!lastRawFrameData || lastRawFrameData.length < expectedLength) {
 		lastRawFrameData = new Uint8ClampedArray(expectedLength);
 	}
@@ -469,9 +474,14 @@ async function processFrame(buffer: ArrayBuffer): Promise<DecodeResult> {
 async function pollSharedBuffer(): Promise<void> {
 	if (!consumer || !useSharedBuffer) return;
 
-	const buffer = consumer.read(50);
-	if (buffer) {
-		const result = await processFrame(buffer);
+	if (!sharedReadBuffer || sharedReadBufferSize < consumer.getSlotSize()) {
+		sharedReadBuffer = new Uint8Array(consumer.getSlotSize());
+		sharedReadBufferSize = sharedReadBuffer.byteLength;
+	}
+
+	const size = consumer.readInto(sharedReadBuffer, 50);
+	if (size != null && size > 0) {
+		const result = await processFrameBytes(sharedReadBuffer.subarray(0, size));
 		if (result.type === "decoded") {
 			self.postMessage(result, { transfer: [result.bitmap] });
 		} else if (result.type === "frame-queued") {
@@ -495,6 +505,8 @@ self.onmessage = async (e: MessageEvent<IncomingMessage>) => {
 	if (e.data.type === "init-shared-buffer") {
 		consumer = createConsumer(e.data.buffer);
 		useSharedBuffer = true;
+		sharedReadBuffer = null;
+		sharedReadBufferSize = 0;
 
 		if (workerReady) {
 			pollSharedBuffer();
@@ -536,7 +548,7 @@ self.onmessage = async (e: MessageEvent<IncomingMessage>) => {
 	}
 
 	if (e.data.type === "frame") {
-		const result = await processFrame(e.data.buffer);
+		const result = await processFrameBytes(new Uint8Array(e.data.buffer));
 		if (result.type === "decoded") {
 			self.postMessage(result, { transfer: [result.bitmap] });
 		} else if (result.type === "frame-queued") {
diff --git a/apps/desktop/src/utils/shared-frame-buffer.ts b/apps/desktop/src/utils/shared-frame-buffer.ts
index 7b0f82737f..8b5d94d0ae 100644
--- a/apps/desktop/src/utils/shared-frame-buffer.ts
+++ b/apps/desktop/src/utils/shared-frame-buffer.ts
@@ -18,7 +18,7 @@ const CONTROL_SLOT_SIZE = 4;
 const CONTROL_METADATA_OFFSET = 5;
 const CONTROL_DATA_OFFSET = 6;
 const CONTROL_VERSION = 7;
-const CONTROL_READER_ACTIVE = 8;
+const _CONTROL_READER_ACTIVE = 8;
 
 const META_FRAME_SIZE = 0;
 const META_FRAME_NUMBER = 1;
@@ -235,7 +235,9 @@ export function createProducer(init: SharedFrameBufferInit): Producer {
 
 export interface Consumer {
 	read(timeoutMs?: number): ArrayBuffer | null;
+	readInto(target: Uint8Array, timeoutMs?: number): number | null;
 	isShutdown(): boolean;
+	getSlotSize(): number;
 }
 
 function advanceReadIndexCAS(
@@ -373,8 +375,100 @@ export function createConsumer(buffer: SharedArrayBuffer): Consumer {
 			return null;
 		},
 
+		readInto(target: Uint8Array, timeoutMs: number = 100): number | null {
+			const MAX_CAS_RETRIES = 3;
+
+			for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
+				const shutdownFlag = Atomics.load(controlView, CONTROL_SHUTDOWN);
+				if (shutdownFlag) {
+					return null;
+				}
+
+				const readIdx = Atomics.load(controlView, CONTROL_READ_INDEX);
+				const slotMetaIdx =
+					(metadataOffset + readIdx * METADATA_ENTRY_SIZE) / 4;
+
+				let state = Atomics.load(metadataView, slotMetaIdx + META_SLOT_STATE);
+
+				if (state !== SLOT_STATE.READY) {
+					const waitResult = Atomics.wait(
+						metadataView,
+						slotMetaIdx + META_SLOT_STATE,
+						state,
+						timeoutMs,
+					);
+					if (waitResult === "timed-out") {
+						return null;
+					}
+
+					const shutdownCheck = Atomics.load(controlView, CONTROL_SHUTDOWN);
+					if (shutdownCheck) {
+						return null;
+					}
+
+					state = Atomics.load(metadataView, slotMetaIdx + META_SLOT_STATE);
+					if (state !== SLOT_STATE.READY) {
+						continue;
+					}
+				}
+
+				const exchangedState = Atomics.compareExchange(
+					metadataView,
+					slotMetaIdx + META_SLOT_STATE,
+					SLOT_STATE.READY,
+					SLOT_STATE.READING,
+				);
+				if (exchangedState !== SLOT_STATE.READY) {
+					continue;
+				}
+
+				const frameSize = Atomics.load(
+					metadataView,
+					slotMetaIdx + META_FRAME_SIZE,
+				);
+				const slotDataOffset = dataOffset + readIdx * slotSize;
+
+				if (
+					!Number.isInteger(frameSize) ||
+					frameSize < 0 ||
+					frameSize > slotSize ||
+					slotDataOffset < 0 ||
+					slotDataOffset + frameSize > buffer.byteLength ||
+					frameSize > target.byteLength
+				) {
+					Atomics.store(
+						metadataView,
+						slotMetaIdx + META_SLOT_STATE,
+						SLOT_STATE.EMPTY,
+					);
+					const nextIdx = (readIdx + 1) % slotCount;
+					advanceReadIndexCAS(controlView, readIdx, nextIdx);
+					return null;
+				}
+
+				target.set(new Uint8Array(buffer, slotDataOffset, frameSize), 0);
+
+				Atomics.store(
+					metadataView,
+					slotMetaIdx + META_SLOT_STATE,
+					SLOT_STATE.EMPTY,
+				);
+
+				const nextIdx = (readIdx + 1) % slotCount;
+				advanceReadIndexCAS(controlView, readIdx, nextIdx);
+
+				return frameSize;
+			}
+
+			return null;
+		},
+
 		isShutdown(): boolean {
 			return Atomics.load(controlView, CONTROL_SHUTDOWN) === 1;
 		},
+
+		getSlotSize(): number {
+			return slotSize;
+		},
 	};
 }
diff --git a/apps/desktop/src/utils/socket.ts b/apps/desktop/src/utils/socket.ts
index 4484b5b043..f5a2899d55 100644
--- a/apps/desktop/src/utils/socket.ts
+++ b/apps/desktop/src/utils/socket.ts
@@ -12,7 +12,7 @@ import {
 const SAB_SUPPORTED = isSharedArrayBufferSupported();
 const FRAME_BUFFER_CONFIG: SharedFrameBufferConfig = {
 	slotCount: 4,
-	slotSize: 8 * 1024 * 1024,
+	slotSize: 16 * 1024 * 1024,
 };
 
 export type FrameData = {
diff --git a/apps/desktop/src/utils/webgpu-renderer.ts b/apps/desktop/src/utils/webgpu-renderer.ts
index b97d3ba359..fb7078b05d 100644
--- a/apps/desktop/src/utils/webgpu-renderer.ts
+++ b/apps/desktop/src/utils/webgpu-renderer.ts
@@ -153,6 +153,7 @@ export function renderFrameWebGPU(
 	data: Uint8ClampedArray,
 	width: number,
 	height: number,
+	bytesPerRow: number = width * 4,
 ): void {
 	const { device, context, pipeline, sampler, bindGroupLayout, canvas } =
 		renderer;
@@ -184,7 +185,7 @@ export function renderFrameWebGPU(
 		return;
 	}
 
-	const requiredBytes = width * height * 4;
+	const requiredBytes = bytesPerRow * height;
 	if (data.byteLength < requiredBytes) {
 		console.error(
 			`WebGPU renderFrame: buffer too small. Expected at least ${requiredBytes} bytes, got ${data.byteLength}`,
@@ -197,8 +198,8 @@ export function renderFrameWebGPU(
 
 	device.queue.writeTexture(
 		{ texture: renderer.frameTexture },
-		textureData.buffer as unknown as GPUAllowSharedBufferSource,
-		{ bytesPerRow: width * 4, rowsPerImage: height },
+		textureData,
+		{ bytesPerRow, rowsPerImage: height },
 		{ width, height },
 	);
 
diff --git a/crates/rendering/src/decoder/ffmpeg.rs b/crates/rendering/src/decoder/ffmpeg.rs
index b49ee06110..4186ad676b 100644
--- a/crates/rendering/src/decoder/ffmpeg.rs
+++ b/crates/rendering/src/decoder/ffmpeg.rs
@@ -33,16 +33,18 @@ struct ProcessedFrame {
 impl ProcessedFrame {
     fn to_decoded_frame(&self) -> DecodedFrame {
         match self.format {
-            PixelFormat::Rgba => DecodedFrame::new((*self.data).clone(), self.width, self.height),
-            PixelFormat::Nv12 => DecodedFrame::new_nv12(
-                (*self.data).clone(),
+            PixelFormat::Rgba => {
+                DecodedFrame::new_with_arc(Arc::clone(&self.data), self.width, self.height)
+            }
+            PixelFormat::Nv12 => DecodedFrame::new_nv12_with_arc(
+                Arc::clone(&self.data),
                 self.width,
                 self.height,
                 self.y_stride,
                 self.uv_stride,
             ),
-            PixelFormat::Yuv420p => DecodedFrame::new_yuv420p(
-                (*self.data).clone(),
+            PixelFormat::Yuv420p => DecodedFrame::new_yuv420p_with_arc(
+                Arc::clone(&self.data),
                 self.width,
                 self.height,
                 self.y_stride,
diff --git a/crates/rendering/src/frame_pipeline.rs b/crates/rendering/src/frame_pipeline.rs
index 49084728ee..a5eabfaa43 100644
--- a/crates/rendering/src/frame_pipeline.rs
+++ b/crates/rendering/src/frame_pipeline.rs
@@ -44,7 +44,8 @@ impl PendingReadback {
 
         let buffer_slice = self.buffer.slice(..);
         let data = buffer_slice.get_mapped_range();
-        let data_vec = data.to_vec();
+        let mut data_vec = Vec::with_capacity(data.len() + 12);
+        data_vec.extend_from_slice(&data);
 
         drop(data);
         self.buffer.unmap();

From 0e60c967daf4c61db92f23c756356e7473767776 Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Tue, 30 Dec 2025 15:16:27 +0000
Subject: [PATCH 02/12] Fix type casting for buffer sources in rendering utils

---
 apps/desktop/src/utils/socket.ts          | 2 +-
 apps/desktop/src/utils/webgpu-renderer.ts | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/apps/desktop/src/utils/socket.ts b/apps/desktop/src/utils/socket.ts
index f5a2899d55..8d0c5bd60d 100644
--- a/apps/desktop/src/utils/socket.ts
+++ b/apps/desktop/src/utils/socket.ts
@@ -262,7 +262,7 @@ export function createImageDataWS(
 					}
 
 					const imageData = new ImageData(
-						new Uint8ClampedArray(frameData.buffer),
+						new Uint8ClampedArray(frameData.buffer as ArrayBuffer),
 						width,
 						height,
 					);
diff --git a/apps/desktop/src/utils/webgpu-renderer.ts b/apps/desktop/src/utils/webgpu-renderer.ts
index fb7078b05d..306fed630f 100644
--- a/apps/desktop/src/utils/webgpu-renderer.ts
+++ b/apps/desktop/src/utils/webgpu-renderer.ts
@@ -198,7 +198,7 @@ export function renderFrameWebGPU(
 
 	device.queue.writeTexture(
 		{ texture: renderer.frameTexture },
-		textureData,
+		textureData.buffer as unknown as GPUAllowSharedBufferSource,
 		{ bytesPerRow, rowsPerImage: height },
 		{ width, height },
 	);

From 5f70bf867ef10a64b18f27e88e0beaf11ba55b7a Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Tue, 30 Dec 2025 16:08:45 +0000
Subject: [PATCH 03/12] Add frame timing metadata to frame pipeline and WS

---
 apps/desktop/src-tauri/src/camera_legacy.rs   |   2 +
 apps/desktop/src-tauri/src/editor_window.rs   |  10 +-
 apps/desktop/src-tauri/src/frame_ws.rs        |  42 ++++++-
 .../src-tauri/src/screenshot_editor.rs        |   2 +
 apps/desktop/src/utils/frame-worker.ts        | 107 +++++++++++++-----
 apps/desktop/src/utils/socket.ts              |   6 +-
 crates/rendering/src/frame_pipeline.rs        |  13 ++-
 crates/rendering/src/lib.rs                   |   2 +
 8 files changed, 147 insertions(+), 37 deletions(-)

diff --git a/apps/desktop/src-tauri/src/camera_legacy.rs b/apps/desktop/src-tauri/src/camera_legacy.rs
index ee99b50fb1..ac980a7d1e 100644
--- a/apps/desktop/src-tauri/src/camera_legacy.rs
+++ b/apps/desktop/src-tauri/src/camera_legacy.rs
@@ -64,6 +64,8 @@ pub async fn create_camera_preview_ws() -> (Sender<FFmpegVideoFrame>, u16, Cance
                     width: frame.width(),
                     height: frame.height(),
                     stride: frame.stride(0) as u32,
+                    frame_number: 0,
+                    target_time_ns: 0,
                     created_at: Instant::now(),
                 })
                 .ok();
diff --git a/apps/desktop/src-tauri/src/editor_window.rs b/apps/desktop/src-tauri/src/editor_window.rs
index 4fedba9655..5dc0618eb5 100644
--- a/apps/desktop/src-tauri/src/editor_window.rs
+++ b/apps/desktop/src-tauri/src/editor_window.rs
@@ -4,8 +4,6 @@ use tokio::sync::{RwLock, watch};
 use tokio_util::sync::CancellationToken;
 use tracing::debug;
 
-use cap_rendering::RenderedFrame;
-
 use crate::{
     create_editor_instance_impl,
     frame_ws::{WSFrame, create_watch_frame_ws},
@@ -34,12 +32,16 @@ async fn do_prewarm(app: AppHandle, path: PathBuf) -> PendingResult {
             let width = frame.width;
             let height = frame.height;
             let stride = frame.padded_bytes_per_row;
+            let frame_number = frame.frame_number;
+            let target_time_ns = frame.target_time_ns;
             let data = frame.data;
             if let Err(e) = frame_tx.send(Some(WSFrame {
                 data,
                 width,
                 height,
                 stride,
+                frame_number,
+                target_time_ns,
                 created_at: Instant::now(),
             })) {
                 debug!("Frame receiver dropped during prewarm: {e}");
@@ -196,12 +198,16 @@ impl EditorInstances {
                         let width = frame.width;
                         let height = frame.height;
                         let stride = frame.padded_bytes_per_row;
+                        let frame_number = frame.frame_number;
+                        let target_time_ns = frame.target_time_ns;
                         let data = frame.data;
                         if let Err(e) = frame_tx.send(Some(WSFrame {
                             data,
                             width,
                             height,
                             stride,
+                            frame_number,
+                            target_time_ns,
                             created_at: Instant::now(),
                         })) {
                             debug!("Frame receiver dropped in get_or_create: {e}");
diff --git a/apps/desktop/src-tauri/src/frame_ws.rs b/apps/desktop/src-tauri/src/frame_ws.rs
index 1d30d1c42d..31c9e4addd 100644
--- a/apps/desktop/src-tauri/src/frame_ws.rs
+++ b/apps/desktop/src-tauri/src/frame_ws.rs
@@ -2,11 +2,20 @@ use std::time::Instant;
 use tokio::sync::{broadcast, watch};
 use tokio_util::sync::CancellationToken;
 
-fn pack_frame_data(mut data: Vec<u8>, stride: u32, height: u32, width: u32) -> Vec<u8> {
-    data.reserve_exact(12);
+fn pack_frame_data(
+    mut data: Vec<u8>,
+    stride: u32,
+    height: u32,
+    width: u32,
+    frame_number: u32,
+    target_time_ns: u64,
+) -> Vec<u8> {
+    data.reserve_exact(24);
     data.extend_from_slice(&stride.to_le_bytes());
     data.extend_from_slice(&height.to_le_bytes());
     data.extend_from_slice(&width.to_le_bytes());
+    data.extend_from_slice(&frame_number.to_le_bytes());
+    data.extend_from_slice(&target_time_ns.to_le_bytes());
     data
 }
 
@@ -16,6 +25,8 @@ pub struct WSFrame {
     pub width: u32,
     pub height: u32,
     pub stride: u32,
+    pub frame_number: u32,
+    pub target_time_ns: u64,
     #[allow(dead_code)]
     pub created_at: Instant,
 }
@@ -50,7 +61,14 @@ pub async fn create_watch_frame_ws(
         {
             let frame_opt = camera_rx.borrow().clone();
             if let Some(frame) = frame_opt {
-                let packed = pack_frame_data(frame.data, frame.stride, frame.height, frame.width);
+                let packed = pack_frame_data(
+                    frame.data,
+                    frame.stride,
+                    frame.height,
+                    frame.width,
+                    frame.frame_number,
+                    frame.target_time_ns,
+                );
 
                 if let Err(e) = socket.send(Message::Binary(packed)).await {
                     tracing::error!("Failed to send initial frame to socket: {:?}", e);
@@ -83,7 +101,14 @@ pub async fn create_watch_frame_ws(
                     }
                     let frame_opt = camera_rx.borrow().clone();
                     if let Some(frame) = frame_opt {
-                        let packed = pack_frame_data(frame.data, frame.stride, frame.height, frame.width);
+                        let packed = pack_frame_data(
+                            frame.data,
+                            frame.stride,
+                            frame.height,
+                            frame.width,
+                            frame.frame_number,
+                            frame.target_time_ns,
+                        );
 
                         if let Err(e) = socket.send(Message::Binary(packed)).await {
                             tracing::error!("Failed to send frame to socket: {:?}", e);
@@ -168,7 +193,14 @@ pub async fn create_frame_ws(frame_tx: broadcast::Sender<WSFrame>) -> (u16, Canc
                 incoming_frame = camera_rx.recv() => {
                     match incoming_frame {
                         Ok(frame) => {
-                            let packed = pack_frame_data(frame.data, frame.stride, frame.height, frame.width);
+                            let packed = pack_frame_data(
+                                frame.data,
+                                frame.stride,
+                                frame.height,
+                                frame.width,
+                                frame.frame_number,
+                                frame.target_time_ns,
+                            );
 
                             if let Err(e) = socket.send(Message::Binary(packed)).await {
                                 tracing::error!("Failed to send frame to socket: {:?}", e);
diff --git a/apps/desktop/src-tauri/src/screenshot_editor.rs b/apps/desktop/src-tauri/src/screenshot_editor.rs
index 58f7a5f8f6..90bffe7b60 100644
--- a/apps/desktop/src-tauri/src/screenshot_editor.rs
+++ b/apps/desktop/src-tauri/src/screenshot_editor.rs
@@ -360,6 +360,8 @@ impl ScreenshotEditorInstances {
                                     width: frame.width,
                                     height: frame.height,
                                     stride: frame.padded_bytes_per_row,
+                                    frame_number: frame.frame_number,
+                                    target_time_ns: frame.target_time_ns,
                                     created_at: Instant::now(),
                                 }));
                             }
diff --git a/apps/desktop/src/utils/frame-worker.ts b/apps/desktop/src/utils/frame-worker.ts
index 70948b3ac3..f132283ca6 100644
--- a/apps/desktop/src/utils/frame-worker.ts
+++ b/apps/desktop/src/utils/frame-worker.ts
@@ -86,11 +86,17 @@ type IncomingMessage =
 	| InitSharedBufferMessage
 	| CleanupMessage;
 
+interface FrameTiming {
+	frameNumber: number;
+	targetTimeNs: bigint;
+}
+
 interface PendingFrameCanvas2D {
 	mode: "canvas2d";
 	imageData: ImageData;
 	width: number;
 	height: number;
+	timing: FrameTiming;
 }
 
 interface PendingFrameWebGPU {
@@ -99,6 +105,7 @@ interface PendingFrameWebGPU {
 	width: number;
 	height: number;
 	strideBytes: number;
+	timing: FrameTiming;
 }
 
 type PendingFrame = PendingFrameCanvas2D | PendingFrameWebGPU;
@@ -138,6 +145,10 @@ let pendingRenderFrame: PendingFrame | null = null;
 let _rafId: number | null = null;
 let rafRunning = false;
 
+let playbackStartTime: number | null = null;
+let playbackStartFrameNumber = 0;
+let lastRenderedFrameNumber = -1;
+
 function renderLoop() {
 	_rafId = null;
 
@@ -153,32 +164,68 @@ function renderLoop() {
 
 	const frame = pendingRenderFrame;
 	if (frame) {
-		pendingRenderFrame = null;
+		const now = performance.now();
+		const frameNum = frame.timing.frameNumber;
+
+		const isSeek =
+			playbackStartTime !== null &&
+			lastRenderedFrameNumber >= 0 &&
+			(frameNum < lastRenderedFrameNumber ||
+				frameNum > lastRenderedFrameNumber + 10);
+
+		if (playbackStartTime === null || isSeek) {
+			playbackStartTime = now;
+			playbackStartFrameNumber = frameNum;
+		}
 
-		if (frame.mode === "webgpu" && webgpuRenderer) {
-			renderFrameWebGPU(
-				webgpuRenderer,
-				frame.data,
-				frame.width,
-				frame.height,
-				frame.strideBytes,
-			);
-		} else if (frame.mode === "canvas2d" && offscreenCanvas && offscreenCtx) {
-			if (
-				offscreenCanvas.width !== frame.width ||
-				offscreenCanvas.height !== frame.height
-			) {
-				offscreenCanvas.width = frame.width;
-				offscreenCanvas.height = frame.height;
+		let shouldRender = true;
+
+		if (frame.timing.targetTimeNs > 0n) {
+			const elapsedMs = now - playbackStartTime;
+			const startFrameTimeNs =
+				(BigInt(playbackStartFrameNumber) * 1_000_000_000n) / 60n;
+			const adjustedTargetNs = frame.timing.targetTimeNs - startFrameTimeNs;
+			const targetMs = Number(adjustedTargetNs / 1_000_000n);
+			const diffMs = targetMs - elapsedMs;
+
+			if (diffMs > 8) {
+				shouldRender = false;
+			} else if (diffMs < -33) {
+				if (frameNum <= lastRenderedFrameNumber && !isSeek) {
+					shouldRender = false;
+				}
 			}
-			offscreenCtx.putImageData(frame.imageData, 0, 0);
 		}
 
-		self.postMessage({
-			type: "frame-rendered",
-			width: frame.width,
-			height: frame.height,
-		} satisfies FrameRenderedMessage);
+		if (shouldRender) {
+			pendingRenderFrame = null;
+			lastRenderedFrameNumber = frameNum;
+
+			if (frame.mode === "webgpu" && webgpuRenderer) {
+				renderFrameWebGPU(
+					webgpuRenderer,
+					frame.data,
+					frame.width,
+					frame.height,
+					frame.strideBytes,
+				);
+			} else if (frame.mode === "canvas2d" && offscreenCanvas && offscreenCtx) {
+				if (
+					offscreenCanvas.width !== frame.width ||
+					offscreenCanvas.height !== frame.height
+				) {
+					offscreenCanvas.width = frame.width;
+					offscreenCanvas.height = frame.height;
+				}
+				offscreenCtx.putImageData(frame.imageData, 0, 0);
+			}
+
+			self.postMessage({
+				type: "frame-rendered",
+				width: frame.width,
+				height: frame.height,
+			} satisfies FrameRenderedMessage);
+		}
 	}
 
 	_rafId = requestAnimationFrame(renderLoop);
@@ -230,6 +277,9 @@ function cleanup() {
 	lastRawFrameHeight = 0;
 	frameDropCount = 0;
 	lastFrameDropLogTime = 0;
+	playbackStartTime = null;
+	playbackStartFrameNumber = 0;
+	lastRenderedFrameNumber = -1;
 }
 
 function initWorker() {
@@ -345,22 +395,25 @@ async function initCanvas(canvas: OffscreenCanvas): Promise<void> {
 type DecodeResult = FrameQueuedMessage | DecodedFrame | ErrorMessage;
 
 async function processFrameBytes(bytes: Uint8Array): Promise<DecodeResult> {
-	if (bytes.byteLength < 12) {
+	if (bytes.byteLength < 24) {
 		return {
 			type: "error",
 			message: "Received frame too small to contain metadata",
 		};
 	}
 
-	const metadataOffset = bytes.byteOffset + bytes.byteLength - 12;
-	const meta = new DataView(bytes.buffer, metadataOffset, 12);
+	const metadataOffset = bytes.byteOffset + bytes.byteLength - 24;
+	const meta = new DataView(bytes.buffer, metadataOffset, 24);
 	const strideBytes = meta.getUint32(0, true);
 	const height = meta.getUint32(4, true);
 	const width = meta.getUint32(8, true);
+	const frameNumber = meta.getUint32(12, true);
+	const targetTimeNs = meta.getBigUint64(16, true);
+	const timing: FrameTiming = { frameNumber, targetTimeNs };
 	const frameData = new Uint8ClampedArray(
 		bytes.buffer,
 		bytes.byteOffset,
-		bytes.byteLength - 12,
+		bytes.byteLength - 24,
 	);
 
 	if (!width || !height) {
@@ -406,6 +459,7 @@ async function processFrameBytes(bytes: Uint8Array): Promise<DecodeResult> {
 			width,
 			height,
 			strideBytes,
+			timing,
 		};
 		return { type: "frame-queued", width, height };
 	}
@@ -450,6 +504,7 @@ async function processFrameBytes(bytes: Uint8Array): Promise<DecodeResult> {
 			imageData: cachedImageData,
 			width,
 			height,
+			timing,
 		};
 
 		return { type: "frame-queued", width, height };
diff --git a/apps/desktop/src/utils/socket.ts b/apps/desktop/src/utils/socket.ts
index 8d0c5bd60d..85646420e7 100644
--- a/apps/desktop/src/utils/socket.ts
+++ b/apps/desktop/src/utils/socket.ts
@@ -227,9 +227,9 @@ export function createImageDataWS(
 
 		if (directCanvas && directCtx) {
 			const data = new Uint8Array(buffer);
-			if (data.length >= 12) {
-				const metadataOffset = data.length - 12;
-				const meta = new DataView(buffer, metadataOffset, 12);
+			if (data.length >= 24) {
+				const metadataOffset = data.length - 24;
+				const meta = new DataView(buffer, metadataOffset, 24);
 				const strideBytes = meta.getUint32(0, true);
 				const height = meta.getUint32(4, true);
 				const width = meta.getUint32(8, true);
diff --git a/crates/rendering/src/frame_pipeline.rs b/crates/rendering/src/frame_pipeline.rs
index a5eabfaa43..290e85bd3b 100644
--- a/crates/rendering/src/frame_pipeline.rs
+++ b/crates/rendering/src/frame_pipeline.rs
@@ -10,6 +10,8 @@ pub struct PendingReadback {
     padded_bytes_per_row: u32,
     width: u32,
     height: u32,
+    frame_number: u32,
+    frame_rate: u32,
 }
 
 impl PendingReadback {
@@ -44,17 +46,22 @@ impl PendingReadback {
 
         let buffer_slice = self.buffer.slice(..);
         let data = buffer_slice.get_mapped_range();
-        let mut data_vec = Vec::with_capacity(data.len() + 12);
+        let mut data_vec = Vec::with_capacity(data.len() + 24);
         data_vec.extend_from_slice(&data);
 
         drop(data);
         self.buffer.unmap();
 
+        let target_time_ns =
+            (self.frame_number as u64 * 1_000_000_000) / self.frame_rate.max(1) as u64;
+
         Ok(RenderedFrame {
             data: data_vec,
             padded_bytes_per_row: self.padded_bytes_per_row,
             width: self.width,
             height: self.height,
+            frame_number: self.frame_number,
+            target_time_ns,
         })
     }
 }
@@ -163,6 +170,8 @@ impl PipelinedGpuReadback {
             padded_bytes_per_row,
             width: uniforms.output_size.0,
             height: uniforms.output_size.1,
+            frame_number: uniforms.frame_number,
+            frame_rate: uniforms.frame_rate,
         });
 
         Ok(())
@@ -310,6 +319,8 @@ pub struct RenderedFrame {
     pub width: u32,
     pub height: u32,
     pub padded_bytes_per_row: u32,
+    pub frame_number: u32,
+    pub target_time_ns: u64,
 }
 
 // impl FramePipelineEncoder {
diff --git a/crates/rendering/src/lib.rs b/crates/rendering/src/lib.rs
index 48f2b20889..8458d5c5ed 100644
--- a/crates/rendering/src/lib.rs
+++ b/crates/rendering/src/lib.rs
@@ -464,6 +464,7 @@ pub struct ProjectUniforms {
     pub output_size: (u32, u32),
     pub cursor_size: f32,
     pub frame_rate: u32,
+    pub frame_number: u32,
     display: CompositeVideoFrameUniforms,
     camera: Option<CompositeVideoFrameUniforms>,
     camera_only: Option<CompositeVideoFrameUniforms>,
@@ -1557,6 +1558,7 @@ impl ProjectUniforms {
             scene,
             interpolated_cursor,
             frame_rate: fps,
+            frame_number,
             prev_cursor: prev_interpolated_cursor,
             display_parent_motion_px: display_motion_parent,
             motion_blur_amount: user_motion_blur,

From b40d0be7dc038217ceca061f2ce8dea77a5d4b6c Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Tue, 30 Dec 2025 16:41:00 +0000
Subject: [PATCH 04/12] Add frame state reset and buffer borrowing for WebGPU

---
 apps/desktop/src/routes/editor/Editor.tsx     |   2 +
 apps/desktop/src/utils/frame-worker.ts        |  78 ++++++++++---
 apps/desktop/src/utils/shared-frame-buffer.ts | 103 ++++++++++++++++++
 apps/desktop/src/utils/socket.ts              |   4 +
 4 files changed, 172 insertions(+), 15 deletions(-)

diff --git a/apps/desktop/src/routes/editor/Editor.tsx b/apps/desktop/src/routes/editor/Editor.tsx
index 4e6d98121c..b02cd55672 100644
--- a/apps/desktop/src/routes/editor/Editor.tsx
+++ b/apps/desktop/src/routes/editor/Editor.tsx
@@ -96,6 +96,7 @@ function Inner() {
 		setEditorState,
 		previewResolutionBase,
 		dialog,
+		canvasControls,
 	} = useEditorContext();
 
 	const isExportMode = () => {
@@ -208,6 +209,7 @@ function Inner() {
 	const updateConfigAndRender = throttle(async (time: number) => {
 		const config = serializeProjectConfiguration(project);
 		await commands.updateProjectConfigInMemory(config);
+		canvasControls()?.resetFrameState();
 		renderFrame(time);
 	}, 1000 / FPS);
 	createEffect(
diff --git a/apps/desktop/src/utils/frame-worker.ts b/apps/desktop/src/utils/frame-worker.ts
index f132283ca6..705c902807 100644
--- a/apps/desktop/src/utils/frame-worker.ts
+++ b/apps/desktop/src/utils/frame-worker.ts
@@ -32,6 +32,10 @@ interface CleanupMessage {
 	type: "cleanup";
 }
 
+interface ResetFrameStateMessage {
+	type: "reset-frame-state";
+}
+
 interface ReadyMessage {
 	type: "ready";
 }
@@ -84,7 +88,8 @@ type IncomingMessage =
 	| InitCanvasMessage
 	| ResizeMessage
 	| InitSharedBufferMessage
-	| CleanupMessage;
+	| CleanupMessage
+	| ResetFrameStateMessage;
 
 interface FrameTiming {
 	frameNumber: number;
@@ -106,6 +111,7 @@ interface PendingFrameWebGPU {
 	height: number;
 	strideBytes: number;
 	timing: FrameTiming;
+	releaseCallback?: () => void;
 }
 
 type PendingFrame = PendingFrameCanvas2D | PendingFrameWebGPU;
@@ -209,6 +215,9 @@ function renderLoop() {
 					frame.height,
 					frame.strideBytes,
 				);
+				if (frame.releaseCallback) {
+					frame.releaseCallback();
+				}
 			} else if (frame.mode === "canvas2d" && offscreenCanvas && offscreenCtx) {
 				if (
 					offscreenCanvas.width !== frame.width ||
@@ -255,6 +264,12 @@ function stopRenderLoop() {
 
 function cleanup() {
 	stopRenderLoop();
+	if (
+		pendingRenderFrame?.mode === "webgpu" &&
+		pendingRenderFrame.releaseCallback
+	) {
+		pendingRenderFrame.releaseCallback();
+	}
 	if (webgpuRenderer) {
 		disposeWebGPU(webgpuRenderer);
 		webgpuRenderer = null;
@@ -394,7 +409,10 @@ async function initCanvas(canvas: OffscreenCanvas): Promise<void> {
 
 type DecodeResult = FrameQueuedMessage | DecodedFrame | ErrorMessage;
 
-async function processFrameBytes(bytes: Uint8Array): Promise<DecodeResult> {
+async function processFrameBytes(
+	bytes: Uint8Array,
+	releaseCallback?: () => void,
+): Promise<DecodeResult> {
 	if (bytes.byteLength < 24) {
 		return {
 			type: "error",
@@ -440,6 +458,12 @@ async function processFrameBytes(bytes: Uint8Array): Promise<DecodeResult> {
 
 	if (renderMode === "webgpu" && webgpuRenderer) {
 		if (pendingRenderFrame !== null) {
+			if (
+				pendingRenderFrame.mode === "webgpu" &&
+				pendingRenderFrame.releaseCallback
+			) {
+				pendingRenderFrame.releaseCallback();
+			}
 			frameDropCount++;
 			const now = performance.now();
 			if (now - lastFrameDropLogTime > 1000) {
@@ -460,6 +484,7 @@ async function processFrameBytes(bytes: Uint8Array): Promise<DecodeResult> {
 			height,
 			strideBytes,
 			timing,
+			releaseCallback,
 		};
 		return { type: "frame-queued", width, height };
 	}
@@ -529,20 +554,37 @@ async function processFrameBytes(bytes: Uint8Array): Promise<DecodeResult> {
 async function pollSharedBuffer(): Promise<void> {
 	if (!consumer || !useSharedBuffer) return;
 
-	if (!sharedReadBuffer || sharedReadBufferSize < consumer.getSlotSize()) {
-		sharedReadBuffer = new Uint8Array(consumer.getSlotSize());
-		sharedReadBufferSize = sharedReadBuffer.byteLength;
-	}
+	if (renderMode === "webgpu" && webgpuRenderer) {
+		const borrowed = consumer.borrow(50);
+		if (borrowed !== null) {
+			const result = await processFrameBytes(borrowed.data, borrowed.release);
+			if (result.type === "decoded") {
+				self.postMessage(result, { transfer: [result.bitmap] });
+			} else if (result.type === "frame-queued") {
+				self.postMessage(result);
+			} else if (result.type === "error") {
+				borrowed.release();
+				self.postMessage(result);
+			}
+		}
+	} else {
+		if (!sharedReadBuffer || sharedReadBufferSize < consumer.getSlotSize()) {
+			sharedReadBuffer = new Uint8Array(consumer.getSlotSize());
+			sharedReadBufferSize = sharedReadBuffer.byteLength;
+		}
 
-	const size = consumer.readInto(sharedReadBuffer, 50);
-	if (size != null && size > 0) {
-		const result = await processFrameBytes(sharedReadBuffer.subarray(0, size));
-		if (result.type === "decoded") {
-			self.postMessage(result, { transfer: [result.bitmap] });
-		} else if (result.type === "frame-queued") {
-			self.postMessage(result);
-		} else if (result.type === "error") {
-			self.postMessage(result);
+		const size = consumer.readInto(sharedReadBuffer, 50);
+		if (size != null && size > 0) {
+			const result = await processFrameBytes(
+				sharedReadBuffer.subarray(0, size),
+			);
+			if (result.type === "decoded") {
+				self.postMessage(result, { transfer: [result.bitmap] });
+			} else if (result.type === "frame-queued") {
+				self.postMessage(result);
+			} else if (result.type === "error") {
+				self.postMessage(result);
+			}
 		}
 	}
 
@@ -557,6 +599,12 @@ self.onmessage = async (e: MessageEvent<IncomingMessage>) => {
 		return;
 	}
 
+	if (e.data.type === "reset-frame-state") {
+		lastRenderedFrameNumber = -1;
+		playbackStartTime = null;
+		return;
+	}
+
 	if (e.data.type === "init-shared-buffer") {
 		consumer = createConsumer(e.data.buffer);
 		useSharedBuffer = true;
diff --git a/apps/desktop/src/utils/shared-frame-buffer.ts b/apps/desktop/src/utils/shared-frame-buffer.ts
index 8b5d94d0ae..275e76c421 100644
--- a/apps/desktop/src/utils/shared-frame-buffer.ts
+++ b/apps/desktop/src/utils/shared-frame-buffer.ts
@@ -233,9 +233,16 @@ export function createProducer(init: SharedFrameBufferInit): Producer {
 	};
 }
 
+export interface BorrowedFrame {
+	data: Uint8Array;
+	frameSize: number;
+	release(): void;
+}
+
 export interface Consumer {
 	read(timeoutMs?: number): ArrayBuffer | null;
 	readInto(target: Uint8Array, timeoutMs?: number): number | null;
+	borrow(timeoutMs?: number): BorrowedFrame | null;
 	isShutdown(): boolean;
 	getSlotSize(): number;
 }
@@ -463,6 +470,102 @@ export function createConsumer(buffer: SharedArrayBuffer): Consumer {
 			return null;
 		},
 
+		borrow(timeoutMs: number = 100): BorrowedFrame | null {
+			const MAX_CAS_RETRIES = 3;
+
+			for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
+				const shutdownFlag = Atomics.load(controlView, CONTROL_SHUTDOWN);
+				if (shutdownFlag) {
+					return null;
+				}
+
+				const readIdx = Atomics.load(controlView, CONTROL_READ_INDEX);
+				const slotMetaIdx =
+					(metadataOffset + readIdx * METADATA_ENTRY_SIZE) / 4;
+
+				let state = Atomics.load(metadataView, slotMetaIdx + META_SLOT_STATE);
+
+				if (state !== SLOT_STATE.READY) {
+					const waitResult = Atomics.wait(
+						metadataView,
+						slotMetaIdx + META_SLOT_STATE,
+						state,
+						timeoutMs,
+					);
+					if (waitResult === "timed-out") {
+						return null;
+					}
+
+					const shutdownCheck = Atomics.load(controlView, CONTROL_SHUTDOWN);
+					if (shutdownCheck) {
+						return null;
+					}
+
+					state = Atomics.load(metadataView, slotMetaIdx + META_SLOT_STATE);
+					if (state !== SLOT_STATE.READY) {
+						continue;
+					}
+				}
+
+				const exchangedState = Atomics.compareExchange(
+					metadataView,
+					slotMetaIdx + META_SLOT_STATE,
+					SLOT_STATE.READY,
+					SLOT_STATE.READING,
+				);
+				if (exchangedState !== SLOT_STATE.READY) {
+					continue;
+				}
+
+				const frameSize = Atomics.load(
+					metadataView,
+					slotMetaIdx + META_FRAME_SIZE,
+				);
+				const slotDataOffset = dataOffset + readIdx * slotSize;
+
+				if (
+					!Number.isInteger(frameSize) ||
+					frameSize < 0 ||
+					frameSize > slotSize ||
+					slotDataOffset < 0 ||
+					slotDataOffset + frameSize > buffer.byteLength
+				) {
+					Atomics.store(
+						metadataView,
+						slotMetaIdx + META_SLOT_STATE,
+						SLOT_STATE.EMPTY,
+					);
+					const nextIdx = (readIdx + 1) % slotCount;
+					advanceReadIndexCAS(controlView, readIdx, nextIdx);
+					return null;
+				}
+
+				const data = new Uint8Array(buffer, slotDataOffset, frameSize);
+
+				let released = false;
+				const capturedReadIdx = readIdx;
+				const capturedSlotMetaIdx = slotMetaIdx;
+
+				const release = () => {
+					if (released) return;
+					released = true;
+
+					Atomics.store(
+						metadataView,
+						capturedSlotMetaIdx + META_SLOT_STATE,
+						SLOT_STATE.EMPTY,
+					);
+
+					const nextIdx = (capturedReadIdx + 1) % slotCount;
+					advanceReadIndexCAS(controlView, capturedReadIdx, nextIdx);
+				};
+
+				return { data, frameSize, release };
+			}
+
+			return null;
+		},
+
 		isShutdown(): boolean {
 			return Atomics.load(controlView, CONTROL_SHUTDOWN) === 1;
 		},
diff --git a/apps/desktop/src/utils/socket.ts b/apps/desktop/src/utils/socket.ts
index 85646420e7..fc256c058c 100644
--- a/apps/desktop/src/utils/socket.ts
+++ b/apps/desktop/src/utils/socket.ts
@@ -26,6 +26,7 @@ export type CanvasControls = {
 	resizeCanvas: (width: number, height: number) => void;
 	hasRenderedFrame: () => boolean;
 	initDirectCanvas: (canvas: HTMLCanvasElement) => void;
+	resetFrameState: () => void;
 };
 
 interface ReadyMessage {
@@ -142,6 +143,9 @@ export function createImageDataWS(
 			directCanvas = canvas;
 			directCtx = canvas.getContext("2d", { alpha: false });
 		},
+		resetFrameState: () => {
+			worker.postMessage({ type: "reset-frame-state" });
+		},
 	};
 
 	worker.onmessage = (e: MessageEvent<WorkerMessage>) => {

From 6a2bc341284973351c87cb4f6a272adf21ba396d Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Tue, 30 Dec 2025 16:50:33 +0000
Subject: [PATCH 05/12] Add stride correction worker for image data processing

---
 apps/desktop/src/utils/socket.ts              | 81 ++++++++++++-------
 .../src/utils/stride-correction-worker.ts     | 60 ++++++++++++++
 2 files changed, 114 insertions(+), 27 deletions(-)
 create mode 100644 apps/desktop/src/utils/stride-correction-worker.ts

diff --git a/apps/desktop/src/utils/socket.ts b/apps/desktop/src/utils/socket.ts
index fc256c058c..f01a445443 100644
--- a/apps/desktop/src/utils/socket.ts
+++ b/apps/desktop/src/utils/socket.ts
@@ -8,6 +8,8 @@ import {
 	type Producer,
 	type SharedFrameBufferConfig,
 } from "./shared-frame-buffer";
+import type { StrideCorrectionResponse } from "./stride-correction-worker";
+import StrideCorrectionWorker from "./stride-correction-worker?worker";
 
 const SAB_SUPPORTED = isSharedArrayBufferSupported();
 const FRAME_BUFFER_CONFIG: SharedFrameBufferConfig = {
@@ -111,6 +113,7 @@ export function createImageDataWS(
 
 	let directCanvas: HTMLCanvasElement | null = null;
 	let directCtx: CanvasRenderingContext2D | null = null;
+	let strideWorker: Worker | null = null;
 
 	function cleanup() {
 		if (isCleanedUp) return;
@@ -124,6 +127,12 @@ export function createImageDataWS(
 		worker.onmessage = null;
 		worker.terminate();
 
+		if (strideWorker) {
+			strideWorker.onmessage = null;
+			strideWorker.terminate();
+			strideWorker = null;
+		}
+
 		pendingFrame = null;
 		nextFrame = null;
 		isProcessing = false;
@@ -142,6 +151,25 @@ export function createImageDataWS(
 		initDirectCanvas: (canvas: HTMLCanvasElement) => {
 			directCanvas = canvas;
 			directCtx = canvas.getContext("2d", { alpha: false });
+			strideWorker = new StrideCorrectionWorker();
+			strideWorker.onmessage = (e: MessageEvent<StrideCorrectionResponse>) => {
+				if (e.data.type !== "corrected" || !directCanvas || !directCtx) return;
+
+				const { buffer, width, height } = e.data;
+				if (directCanvas.width !== width || directCanvas.height !== height) {
+					directCanvas.width = width;
+					directCanvas.height = height;
+				}
+
+				const frameData = new Uint8ClampedArray(buffer);
+				const imageData = new ImageData(frameData, width, height);
+				directCtx.putImageData(imageData, 0, 0);
+
+				if (!hasRenderedFrame()) {
+					setHasRenderedFrame(true);
+				}
+				onmessage({ width, height });
+			};
 		},
 		resetFrameState: () => {
 			worker.postMessage({ type: "reset-frame-state" });
@@ -229,7 +257,7 @@ export function createImageDataWS(
 	ws.onmessage = (event) => {
 		const buffer = event.data as ArrayBuffer;
 
-		if (directCanvas && directCtx) {
+		if (directCanvas && directCtx && strideWorker) {
 			const data = new Uint8Array(buffer);
 			if (data.length >= 24) {
 				const metadataOffset = data.length - 24;
@@ -240,42 +268,41 @@ export function createImageDataWS(
 
 				if (width > 0 && height > 0) {
 					const expectedRowBytes = width * 4;
-					let frameData: Uint8ClampedArray;
 
 					if (strideBytes === expectedRowBytes) {
-						frameData = new Uint8ClampedArray(
+						const frameData = new Uint8ClampedArray(
 							buffer,
 							0,
 							expectedRowBytes * height,
 						);
-					} else {
-						frameData = new Uint8ClampedArray(expectedRowBytes * height);
-						for (let row = 0; row < height; row++) {
-							const srcStart = row * strideBytes;
-							const destStart = row * expectedRowBytes;
-							frameData.set(
-								new Uint8ClampedArray(buffer, srcStart, expectedRowBytes),
-								destStart,
-							);
-						}
-					}
 
-					if (directCanvas.width !== width || directCanvas.height !== height) {
-						directCanvas.width = width;
-						directCanvas.height = height;
-					}
+						if (
+							directCanvas.width !== width ||
+							directCanvas.height !== height
+						) {
+							directCanvas.width = width;
+							directCanvas.height = height;
+						}
 
-					const imageData = new ImageData(
-						new Uint8ClampedArray(frameData.buffer as ArrayBuffer),
-						width,
-						height,
-					);
-					directCtx.putImageData(imageData, 0, 0);
+						const imageData = new ImageData(frameData, width, height);
+						directCtx.putImageData(imageData, 0, 0);
 
-					if (!hasRenderedFrame()) {
-						setHasRenderedFrame(true);
+						if (!hasRenderedFrame()) {
+							setHasRenderedFrame(true);
+						}
+						onmessage({ width, height });
+					} else {
+						strideWorker.postMessage(
+							{
+								type: "correct-stride",
+								buffer,
+								strideBytes,
+								width,
+								height,
+							},
+							[buffer],
+						);
 					}
-					onmessage({ width, height });
 				}
 			}
 			return;
diff --git a/apps/desktop/src/utils/stride-correction-worker.ts b/apps/desktop/src/utils/stride-correction-worker.ts
new file mode 100644
index 0000000000..b98a355bd3
--- /dev/null
+++ b/apps/desktop/src/utils/stride-correction-worker.ts
@@ -0,0 +1,60 @@
+interface StrideCorrectionRequest {
+	type: "correct-stride";
+	buffer: ArrayBuffer;
+	strideBytes: number;
+	width: number;
+	height: number;
+}
+
+interface StrideCorrectionResponse {
+	type: "corrected";
+	buffer: ArrayBuffer;
+	width: number;
+	height: number;
+}
+
+interface ErrorResponse {
+	type: "error";
+	message: string;
+}
+
+let correctionBuffer: Uint8ClampedArray | null = null;
+let correctionBufferSize = 0;
+
+self.onmessage = (e: MessageEvent<StrideCorrectionRequest>) => {
+	if (e.data.type !== "correct-stride") return;
+
+	const { buffer, strideBytes, width, height } = e.data;
+	const expectedRowBytes = width * 4;
+	const expectedLength = expectedRowBytes * height;
+
+	if (!correctionBuffer || correctionBufferSize < expectedLength) {
+		correctionBuffer = new Uint8ClampedArray(expectedLength);
+		correctionBufferSize = expectedLength;
+	}
+
+	const srcData = new Uint8ClampedArray(buffer);
+	for (let row = 0; row < height; row++) {
+		const srcStart = row * strideBytes;
+		const destStart = row * expectedRowBytes;
+		correctionBuffer.set(
+			srcData.subarray(srcStart, srcStart + expectedRowBytes),
+			destStart,
+		);
+	}
+
+	const result = correctionBuffer.slice(0, expectedLength);
+	const response: StrideCorrectionResponse = {
+		type: "corrected",
+		buffer: result.buffer,
+		width,
+		height,
+	};
+	self.postMessage(response, { transfer: [result.buffer] });
+};
+
+export type {
+	StrideCorrectionRequest,
+	StrideCorrectionResponse,
+	ErrorResponse,
+};

From 8b025aa44c3b1946ae822c94e99baabaa228b206 Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Tue, 30 Dec 2025 16:55:57 +0000
Subject: [PATCH 06/12] Optimize ImageData object reuse in socket utility

---
 apps/desktop/src/utils/socket.ts | 46 +++++++++++++++++++++++++++-----
 1 file changed, 39 insertions(+), 7 deletions(-)

diff --git a/apps/desktop/src/utils/socket.ts b/apps/desktop/src/utils/socket.ts
index f01a445443..921681fcc7 100644
--- a/apps/desktop/src/utils/socket.ts
+++ b/apps/desktop/src/utils/socket.ts
@@ -115,6 +115,14 @@ export function createImageDataWS(
 	let directCtx: CanvasRenderingContext2D | null = null;
 	let strideWorker: Worker | null = null;
 
+	let cachedDirectImageData: ImageData | null = null;
+	let cachedDirectWidth = 0;
+	let cachedDirectHeight = 0;
+
+	let cachedStrideImageData: ImageData | null = null;
+	let cachedStrideWidth = 0;
+	let cachedStrideHeight = 0;
+
 	function cleanup() {
 		if (isCleanedUp) return;
 		isCleanedUp = true;
@@ -137,6 +145,13 @@ export function createImageDataWS(
 		nextFrame = null;
 		isProcessing = false;
 
+		cachedDirectImageData = null;
+		cachedDirectWidth = 0;
+		cachedDirectHeight = 0;
+		cachedStrideImageData = null;
+		cachedStrideWidth = 0;
+		cachedStrideHeight = 0;
+
 		setIsConnected(false);
 	}
 
@@ -162,8 +177,17 @@ export function createImageDataWS(
 				}
 
 				const frameData = new Uint8ClampedArray(buffer);
-				const imageData = new ImageData(frameData, width, height);
-				directCtx.putImageData(imageData, 0, 0);
+				if (
+					!cachedStrideImageData ||
+					cachedStrideWidth !== width ||
+					cachedStrideHeight !== height
+				) {
+					cachedStrideImageData = new ImageData(width, height);
+					cachedStrideWidth = width;
+					cachedStrideHeight = height;
+				}
+				cachedStrideImageData.data.set(frameData);
+				directCtx.putImageData(cachedStrideImageData, 0, 0);
 
 				if (!hasRenderedFrame()) {
 					setHasRenderedFrame(true);
@@ -258,9 +282,8 @@ export function createImageDataWS(
 		const buffer = event.data as ArrayBuffer;
 
 		if (directCanvas && directCtx && strideWorker) {
-			const data = new Uint8Array(buffer);
-			if (data.length >= 24) {
-				const metadataOffset = data.length - 24;
+			if (buffer.byteLength >= 24) {
+				const metadataOffset = buffer.byteLength - 24;
 				const meta = new DataView(buffer, metadataOffset, 24);
 				const strideBytes = meta.getUint32(0, true);
 				const height = meta.getUint32(4, true);
@@ -284,8 +307,17 @@ export function createImageDataWS(
 							directCanvas.height = height;
 						}
 
-						const imageData = new ImageData(frameData, width, height);
-						directCtx.putImageData(imageData, 0, 0);
+						if (
+							!cachedDirectImageData ||
+							cachedDirectWidth !== width ||
+							cachedDirectHeight !== height
+						) {
+							cachedDirectImageData = new ImageData(width, height);
+							cachedDirectWidth = width;
+							cachedDirectHeight = height;
+						}
+						cachedDirectImageData.data.set(frameData);
+						directCtx.putImageData(cachedDirectImageData, 0, 0);
 
 						if (!hasRenderedFrame()) {
 							setHasRenderedFrame(true);

From 8403b5059081709fd8e3cf568ad8321dfa10b837 Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Tue, 30 Dec 2025 16:58:31 +0000
Subject: [PATCH 07/12] Start render loop when queuing new frames

---
 apps/desktop/src/utils/frame-worker.ts | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/apps/desktop/src/utils/frame-worker.ts b/apps/desktop/src/utils/frame-worker.ts
index 705c902807..d3218fc015 100644
--- a/apps/desktop/src/utils/frame-worker.ts
+++ b/apps/desktop/src/utils/frame-worker.ts
@@ -237,7 +237,11 @@ function renderLoop() {
 		}
 	}
 
-	_rafId = requestAnimationFrame(renderLoop);
+	if (pendingRenderFrame !== null) {
+		_rafId = requestAnimationFrame(renderLoop);
+	} else {
+		rafRunning = false;
+	}
 }
 
 function startRenderLoop() {
@@ -486,6 +490,7 @@ async function processFrameBytes(
 			timing,
 			releaseCallback,
 		};
+		startRenderLoop();
 		return { type: "frame-queued", width, height };
 	}
 
@@ -531,7 +536,7 @@ async function processFrameBytes(
 			height,
 			timing,
 		};
-
+		startRenderLoop();
 		return { type: "frame-queued", width, height };
 	}
 

From ab62608770291625d4547447ebf9a49702248843 Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Tue, 30 Dec 2025 17:00:21 +0000
Subject: [PATCH 08/12] Increase pollSharedBuffer timeout delay to 1ms

---
 apps/desktop/src/utils/frame-worker.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apps/desktop/src/utils/frame-worker.ts b/apps/desktop/src/utils/frame-worker.ts
index d3218fc015..b40e81a8eb 100644
--- a/apps/desktop/src/utils/frame-worker.ts
+++ b/apps/desktop/src/utils/frame-worker.ts
@@ -594,7 +594,7 @@ async function pollSharedBuffer(): Promise<void> {
 	}
 
 	if (!consumer.isShutdown()) {
-		setTimeout(pollSharedBuffer, 0);
+		setTimeout(pollSharedBuffer, 1);
 	}
 }
 

From ba81a7656dbbcdfba99c4995c2e5543762977ba4 Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Tue, 30 Dec 2025 17:03:30 +0000
Subject: [PATCH 09/12] Limit retries in advanceReadIndexCAS loop

---
 apps/desktop/src/utils/shared-frame-buffer.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/apps/desktop/src/utils/shared-frame-buffer.ts b/apps/desktop/src/utils/shared-frame-buffer.ts
index 275e76c421..24a4637242 100644
--- a/apps/desktop/src/utils/shared-frame-buffer.ts
+++ b/apps/desktop/src/utils/shared-frame-buffer.ts
@@ -252,8 +252,10 @@ function advanceReadIndexCAS(
 	readIdx: number,
 	nextIdx: number,
 ): void {
+	const MAX_ADVANCE_RETRIES = 16;
 	let expectedIdx = readIdx;
-	while (true) {
+
+	for (let attempt = 0; attempt < MAX_ADVANCE_RETRIES; attempt++) {
 		const exchanged = Atomics.compareExchange(
 			controlView,
 			CONTROL_READ_INDEX,

From 3a28f0a19382739dfb554963ed83db06c6f74dfb Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Tue, 30 Dec 2025 17:31:41 +0000
Subject: [PATCH 10/12] Refactor frame handling to use a frame queue

---
 apps/desktop/src-tauri/src/editor_window.rs |  28 +-
 apps/desktop/src-tauri/src/frame_ws.rs      | 100 ++++-
 apps/desktop/src/utils/frame-worker.ts      | 418 +++++++++++++-------
 apps/desktop/src/utils/socket.ts            |   2 +-
 4 files changed, 398 insertions(+), 150 deletions(-)

diff --git a/apps/desktop/src-tauri/src/editor_window.rs b/apps/desktop/src-tauri/src/editor_window.rs
index 5dc0618eb5..97e2b439a1 100644
--- a/apps/desktop/src-tauri/src/editor_window.rs
+++ b/apps/desktop/src-tauri/src/editor_window.rs
@@ -1,14 +1,16 @@
 use std::{collections::HashMap, ops::Deref, path::PathBuf, sync::Arc, time::Instant};
 use tauri::{AppHandle, Manager, Runtime, Window, ipc::CommandArg};
-use tokio::sync::{RwLock, watch};
+use tokio::sync::{Mutex, RwLock, mpsc};
 use tokio_util::sync::CancellationToken;
 use tracing::debug;
 
 use crate::{
     create_editor_instance_impl,
-    frame_ws::{WSFrame, create_watch_frame_ws},
+    frame_ws::{WSFrame, create_mpsc_frame_ws},
 };
 
+const FRAME_CHANNEL_BUFFER: usize = 8;
+
 pub struct EditorInstance {
     inner: Arc<cap_editor::EditorInstance>,
     pub ws_port: u16,
@@ -22,9 +24,10 @@ type PendingReceiver = tokio::sync::watch::Receiver<Option<PendingResult>>;
 pub struct PendingEditorInstances(Arc<RwLock<HashMap<String, PendingReceiver>>>);
 
 async fn do_prewarm(app: AppHandle, path: PathBuf) -> PendingResult {
-    let (frame_tx, frame_rx) = watch::channel(None);
+    let (frame_tx, frame_rx) = mpsc::channel(FRAME_CHANNEL_BUFFER);
+    let frame_rx = Arc::new(Mutex::new(frame_rx));
 
-    let (ws_port, ws_shutdown_token) = create_watch_frame_ws(frame_rx).await;
+    let (ws_port, ws_shutdown_token) = create_mpsc_frame_ws(frame_rx).await;
     let inner = create_editor_instance_impl(
         &app,
         path,
@@ -35,7 +38,7 @@ async fn do_prewarm(app: AppHandle, path: PathBuf) -> PendingResult {
             let frame_number = frame.frame_number;
             let target_time_ns = frame.target_time_ns;
             let data = frame.data;
-            if let Err(e) = frame_tx.send(Some(WSFrame {
+            if let Err(e) = frame_tx.try_send(WSFrame {
                 data,
                 width,
                 height,
@@ -43,8 +46,8 @@ async fn do_prewarm(app: AppHandle, path: PathBuf) -> PendingResult {
                 frame_number,
                 target_time_ns,
                 created_at: Instant::now(),
-            })) {
-                debug!("Frame receiver dropped during prewarm: {e}");
+            }) {
+                debug!("Frame channel full or closed during prewarm: {e}");
             }
         }),
     )
@@ -188,9 +191,10 @@ impl EditorInstances {
                     }
                 }
 
-                let (frame_tx, frame_rx) = watch::channel(None);
+                let (frame_tx, frame_rx) = mpsc::channel(FRAME_CHANNEL_BUFFER);
+                let frame_rx = Arc::new(Mutex::new(frame_rx));
 
-                let (ws_port, ws_shutdown_token) = create_watch_frame_ws(frame_rx).await;
+                let (ws_port, ws_shutdown_token) = create_mpsc_frame_ws(frame_rx).await;
                 let instance = create_editor_instance_impl(
                     window.app_handle(),
                     path,
@@ -201,7 +205,7 @@ impl EditorInstances {
                         let frame_number = frame.frame_number;
                         let target_time_ns = frame.target_time_ns;
                         let data = frame.data;
-                        if let Err(e) = frame_tx.send(Some(WSFrame {
+                        if let Err(e) = frame_tx.try_send(WSFrame {
                             data,
                             width,
                             height,
@@ -209,8 +213,8 @@ impl EditorInstances {
                             frame_number,
                             target_time_ns,
                             created_at: Instant::now(),
-                        })) {
-                            debug!("Frame receiver dropped in get_or_create: {e}");
+                        }) {
+                            debug!("Frame channel full or closed in get_or_create: {e}");
                         }
                     }),
                 )
diff --git a/apps/desktop/src-tauri/src/frame_ws.rs b/apps/desktop/src-tauri/src/frame_ws.rs
index 31c9e4addd..df0c542bd4 100644
--- a/apps/desktop/src-tauri/src/frame_ws.rs
+++ b/apps/desktop/src-tauri/src/frame_ws.rs
@@ -1,5 +1,6 @@
+use std::sync::Arc;
 use std::time::Instant;
-use tokio::sync::{broadcast, watch};
+use tokio::sync::{Mutex, broadcast, mpsc, watch};
 use tokio_util::sync::CancellationToken;
 
 fn pack_frame_data(
@@ -147,6 +148,103 @@ pub async fn create_watch_frame_ws(
     (port, cancel_token_child)
 }
 
+pub async fn create_mpsc_frame_ws(
+    frame_rx: Arc<Mutex<mpsc::Receiver<WSFrame>>>,
+) -> (u16, CancellationToken) {
+    use axum::{
+        extract::{
+            State,
+            ws::{Message, WebSocket, WebSocketUpgrade},
+        },
+        response::IntoResponse,
+        routing::get,
+    };
+
+    type RouterState = Arc<Mutex<mpsc::Receiver<WSFrame>>>;
+
+    #[axum::debug_handler]
+    async fn ws_handler(
+        ws: WebSocketUpgrade,
+        State(state): State<RouterState>,
+    ) -> impl IntoResponse {
+        ws.on_upgrade(move |socket| handle_socket(socket, state))
+    }
+
+    async fn handle_socket(mut socket: WebSocket, frame_rx: RouterState) {
+        tracing::info!("Socket connection established (mpsc)");
+        let now = std::time::Instant::now();
+
+        loop {
+            let frame_opt = {
+                let mut rx = frame_rx.lock().await;
+                tokio::select! {
+                    biased;
+                    msg = socket.recv() => {
+                        match msg {
+                            Some(Ok(Message::Close(_))) | None => {
+                                tracing::info!("WebSocket closed");
+                                break;
+                            }
+                            Some(Ok(_)) => {
+                                continue;
+                            }
+                            Some(Err(e)) => {
+                                tracing::error!("WebSocket error: {:?}", e);
+                                break;
+                            }
+                        }
+                    },
+                    frame = rx.recv() => frame,
+                }
+            };
+
+            let Some(frame) = frame_opt else {
+                tracing::info!("Frame channel closed");
+                break;
+            };
+
+            let packed = pack_frame_data(
+                frame.data,
+                frame.stride,
+                frame.height,
+                frame.width,
+                frame.frame_number,
+                frame.target_time_ns,
+            );
+
+            if let Err(e) = socket.send(Message::Binary(packed)).await {
+                tracing::error!("Failed to send frame to socket: {:?}", e);
+                break;
+            }
+        }
+
+        let elapsed = now.elapsed();
+        tracing::info!("Websocket closing after {elapsed:.2?}");
+    }
+
+    let router = axum::Router::new()
+        .route("/", get(ws_handler))
+        .with_state(frame_rx);
+
+    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+    let port = listener.local_addr().unwrap().port();
+    tracing::info!("WebSocket server (mpsc) listening on port {}", port);
+
+    let cancel_token = CancellationToken::new();
+    let cancel_token_child = cancel_token.child_token();
+    tokio::spawn(async move {
+        let server = axum::serve(listener, router.into_make_service());
+        tokio::select! {
+            _ = server => {},
+            _ = cancel_token.cancelled() => {
+                tracing::info!("WebSocket server shutting down");
+            }
+        }
+    });
+
+    (port, cancel_token_child)
+}
+
 pub async fn create_frame_ws(frame_tx: broadcast::Sender<WSFrame>) -> (u16, CancellationToken) {
     use axum::{
         extract::{
diff --git a/apps/desktop/src/utils/frame-worker.ts b/apps/desktop/src/utils/frame-worker.ts
index b40e81a8eb..b24c12241d 100644
--- a/apps/desktop/src/utils/frame-worker.ts
+++ b/apps/desktop/src/utils/frame-worker.ts
@@ -147,7 +147,8 @@ let useSharedBuffer = false;
 let sharedReadBuffer: Uint8Array | null = null;
 let sharedReadBufferSize = 0;
 
-let pendingRenderFrame: PendingFrame | null = null;
+const FRAME_QUEUE_SIZE = 5;
+let frameQueue: PendingFrame[] = [];
 let _rafId: number | null = null;
 let rafRunning = false;
 
@@ -155,6 +156,153 @@ let playbackStartTime: number | null = null;
 let playbackStartFrameNumber = 0;
 let lastRenderedFrameNumber = -1;
 
+const FRAME_DURATION_NS = 1_000_000_000n / 60n;
+const EARLY_THRESHOLD_MS = 2;
+const LATE_THRESHOLD_MS = 100;
+
+function tryPollSharedBuffer(): boolean {
+	if (!consumer || !useSharedBuffer) return false;
+
+	if (renderMode === "webgpu" && webgpuRenderer) {
+		const borrowed = consumer.borrow(0);
+		if (borrowed !== null) {
+			queueFrameFromBytes(borrowed.data, borrowed.release);
+			return true;
+		}
+	} else {
+		if (!sharedReadBuffer || sharedReadBufferSize < consumer.getSlotSize()) {
+			sharedReadBuffer = new Uint8Array(consumer.getSlotSize());
+			sharedReadBufferSize = sharedReadBuffer.byteLength;
+		}
+
+		const size = consumer.readInto(sharedReadBuffer, 0);
+		if (size != null && size > 0) {
+			queueFrameFromBytes(sharedReadBuffer.subarray(0, size));
+			return true;
+		}
+	}
+	return false;
+}
+
+function queueFrameFromBytes(
+	bytes: Uint8Array,
+	releaseCallback?: () => void,
+): void {
+	if (bytes.byteLength < 24) {
+		releaseCallback?.();
+		return;
+	}
+
+	const metadataOffset = bytes.byteOffset + bytes.byteLength - 24;
+	const meta = new DataView(bytes.buffer, metadataOffset, 24);
+	const strideBytes = meta.getUint32(0, true);
+	const height = meta.getUint32(4, true);
+	const width = meta.getUint32(8, true);
+	const frameNumber = meta.getUint32(12, true);
+	const targetTimeNs = meta.getBigUint64(16, true);
+	const timing: FrameTiming = { frameNumber, targetTimeNs };
+
+	if (!width || !height) {
+		releaseCallback?.();
+		return;
+	}
+
+	const expectedRowBytes = width * 4;
+	const availableLength = strideBytes * height;
+
+	if (
+		strideBytes === 0 ||
+		strideBytes < expectedRowBytes ||
+		bytes.byteLength - 24 < availableLength
+	) {
+		releaseCallback?.();
+		return;
+	}
+
+	const frameData = new Uint8ClampedArray(
+		bytes.buffer,
+		bytes.byteOffset,
+		bytes.byteLength - 24,
+	);
+
+	if (renderMode === "webgpu" && webgpuRenderer) {
+		while (frameQueue.length >= FRAME_QUEUE_SIZE) {
+			const dropped = frameQueue.shift();
+			if (dropped?.mode === "webgpu" && dropped.releaseCallback) {
+				dropped.releaseCallback();
+			}
+			frameDropCount++;
+		}
+
+		frameQueue.push({
+			mode: "webgpu",
+			data: frameData.subarray(0, availableLength),
+			width,
+			height,
+			strideBytes,
+			timing,
+			releaseCallback,
+		});
+	} else {
+		const expectedLength = expectedRowBytes * height;
+		let processedFrameData: Uint8ClampedArray;
+
+		if (strideBytes === expectedRowBytes) {
+			processedFrameData = frameData.subarray(0, expectedLength);
+		} else {
+			if (!strideBuffer || strideBufferSize < expectedLength) {
+				strideBuffer = new Uint8ClampedArray(expectedLength);
+				strideBufferSize = expectedLength;
+			}
+			for (let row = 0; row < height; row += 1) {
+				const srcStart = row * strideBytes;
+				const destStart = row * expectedRowBytes;
+				strideBuffer.set(
+					frameData.subarray(srcStart, srcStart + expectedRowBytes),
+					destStart,
+				);
+			}
+			processedFrameData = strideBuffer.subarray(0, expectedLength);
+		}
+
+		if (!lastRawFrameData || lastRawFrameData.length < expectedLength) {
+			lastRawFrameData = new Uint8ClampedArray(expectedLength);
+		}
+		lastRawFrameData.set(processedFrameData);
+		lastRawFrameWidth = width;
+		lastRawFrameHeight = height;
+
+		if (!cachedImageData || cachedWidth !== width || cachedHeight !== height) {
+			cachedImageData = new ImageData(width, height);
+			cachedWidth = width;
+			cachedHeight = height;
+		}
+		cachedImageData.data.set(processedFrameData);
+		lastImageData = cachedImageData;
+
+		releaseCallback?.();
+
+		while (frameQueue.length >= FRAME_QUEUE_SIZE) {
+			frameQueue.shift();
+			frameDropCount++;
+		}
+
+		frameQueue.push({
+			mode: "canvas2d",
+			imageData: cachedImageData,
+			width,
+			height,
+			timing,
+		});
+	}
+
+	self.postMessage({
+		type: "frame-queued",
+		width,
+		height,
+	} satisfies FrameQueuedMessage);
+}
+
 function renderLoop() {
 	_rafId = null;
 
@@ -168,76 +316,128 @@ function renderLoop() {
 		return;
 	}
 
-	const frame = pendingRenderFrame;
-	if (frame) {
-		const now = performance.now();
+	if (useSharedBuffer && consumer && !consumer.isShutdown()) {
+		let polled = 0;
+		while (polled < 4 && tryPollSharedBuffer()) {
+			polled++;
+		}
+	}
+
+	const now = performance.now();
+	let frameToRender: PendingFrame | null = null;
+	let frameIndex = -1;
+	let fallbackFrame: PendingFrame | null = null;
+	let fallbackIndex = -1;
+	let fallbackDiffMs = Number.MAX_VALUE;
+
+	for (let i = 0; i < frameQueue.length; i++) {
+		const frame = frameQueue[i];
 		const frameNum = frame.timing.frameNumber;
 
 		const isSeek =
 			playbackStartTime !== null &&
 			lastRenderedFrameNumber >= 0 &&
 			(frameNum < lastRenderedFrameNumber ||
-				frameNum > lastRenderedFrameNumber + 10);
+				frameNum > lastRenderedFrameNumber + 30);
 
 		if (playbackStartTime === null || isSeek) {
 			playbackStartTime = now;
 			playbackStartFrameNumber = frameNum;
 		}
 
-		let shouldRender = true;
-
 		if (frame.timing.targetTimeNs > 0n) {
 			const elapsedMs = now - playbackStartTime;
 			const startFrameTimeNs =
-				(BigInt(playbackStartFrameNumber) * 1_000_000_000n) / 60n;
+				BigInt(playbackStartFrameNumber) * FRAME_DURATION_NS;
 			const adjustedTargetNs = frame.timing.targetTimeNs - startFrameTimeNs;
 			const targetMs = Number(adjustedTargetNs / 1_000_000n);
 			const diffMs = targetMs - elapsedMs;
 
-			if (diffMs > 8) {
-				shouldRender = false;
-			} else if (diffMs < -33) {
-				if (frameNum <= lastRenderedFrameNumber && !isSeek) {
-					shouldRender = false;
-				}
+			const absDiff = Math.abs(diffMs);
+			if (absDiff < fallbackDiffMs) {
+				fallbackDiffMs = absDiff;
+				fallbackFrame = frame;
+				fallbackIndex = i;
 			}
-		}
 
-		if (shouldRender) {
-			pendingRenderFrame = null;
-			lastRenderedFrameNumber = frameNum;
-
-			if (frame.mode === "webgpu" && webgpuRenderer) {
-				renderFrameWebGPU(
-					webgpuRenderer,
-					frame.data,
-					frame.width,
-					frame.height,
-					frame.strideBytes,
-				);
-				if (frame.releaseCallback) {
+			if (diffMs > EARLY_THRESHOLD_MS) {
+				continue;
+			}
+
+			if (diffMs < -LATE_THRESHOLD_MS && !isSeek) {
+				if (frame.mode === "webgpu" && frame.releaseCallback) {
 					frame.releaseCallback();
 				}
-			} else if (frame.mode === "canvas2d" && offscreenCanvas && offscreenCtx) {
-				if (
-					offscreenCanvas.width !== frame.width ||
-					offscreenCanvas.height !== frame.height
-				) {
-					offscreenCanvas.width = frame.width;
-					offscreenCanvas.height = frame.height;
+				frameQueue.splice(i, 1);
+				i--;
+				frameDropCount++;
+				const logNow = performance.now();
+				if (logNow - lastFrameDropLogTime > 1000) {
+					if (frameDropCount > 0) {
+						console.warn(
+							`[frame-worker] Dropped ${frameDropCount} frames in the last second`,
+						);
+					}
+					frameDropCount = 0;
+					lastFrameDropLogTime = logNow;
 				}
-				offscreenCtx.putImageData(frame.imageData, 0, 0);
+				continue;
 			}
+		}
 
-			self.postMessage({
-				type: "frame-rendered",
-				width: frame.width,
-				height: frame.height,
-			} satisfies FrameRenderedMessage);
+		if (
+			frameToRender === null ||
+			frame.timing.frameNumber < frameToRender.timing.frameNumber
+		) {
+			frameToRender = frame;
+			frameIndex = i;
 		}
 	}
 
-	if (pendingRenderFrame !== null) {
+	if (frameToRender === null && fallbackFrame !== null) {
+		frameToRender = fallbackFrame;
+		frameIndex = fallbackIndex;
+	}
+
+	if (frameToRender !== null && frameIndex >= 0) {
+		frameQueue.splice(frameIndex, 1);
+		const frame = frameToRender;
+		lastRenderedFrameNumber = frame.timing.frameNumber;
+
+		if (frame.mode === "webgpu" && webgpuRenderer) {
+			renderFrameWebGPU(
+				webgpuRenderer,
+				frame.data,
+				frame.width,
+				frame.height,
+				frame.strideBytes,
+			);
+			if (frame.releaseCallback) {
+				frame.releaseCallback();
+			}
+		} else if (frame.mode === "canvas2d" && offscreenCanvas && offscreenCtx) {
+			if (
+				offscreenCanvas.width !== frame.width ||
+				offscreenCanvas.height !== frame.height
+			) {
+				offscreenCanvas.width = frame.width;
+				offscreenCanvas.height = frame.height;
+			}
+			offscreenCtx.putImageData(frame.imageData, 0, 0);
+		}
+
+		self.postMessage({
+			type: "frame-rendered",
+			width: frame.width,
+			height: frame.height,
+		} satisfies FrameRenderedMessage);
+	}
+
+	const shouldContinue =
+		frameQueue.length > 0 ||
+		(useSharedBuffer && consumer && !consumer.isShutdown());
+
+	if (shouldContinue) {
 		_rafId = requestAnimationFrame(renderLoop);
 	} else {
 		rafRunning = false;
@@ -268,12 +468,12 @@ function stopRenderLoop() {
 
 function cleanup() {
 	stopRenderLoop();
-	if (
-		pendingRenderFrame?.mode === "webgpu" &&
-		pendingRenderFrame.releaseCallback
-	) {
-		pendingRenderFrame.releaseCallback();
+	for (const frame of frameQueue) {
+		if (frame.mode === "webgpu" && frame.releaseCallback) {
+			frame.releaseCallback();
+		}
 	}
+	frameQueue = [];
 	if (webgpuRenderer) {
 		disposeWebGPU(webgpuRenderer);
 		webgpuRenderer = null;
@@ -284,7 +484,6 @@ function cleanup() {
 	useSharedBuffer = false;
 	sharedReadBuffer = null;
 	sharedReadBufferSize = 0;
-	pendingRenderFrame = null;
 	lastImageData = null;
 	cachedImageData = null;
 	cachedWidth = 0;
@@ -311,7 +510,7 @@ function initWorker() {
 	}
 
 	if (useSharedBuffer && consumer) {
-		pollSharedBuffer();
+		startRenderLoop();
 	}
 }
 
@@ -413,11 +612,12 @@ async function initCanvas(canvas: OffscreenCanvas): Promise<void> {
 
 type DecodeResult = FrameQueuedMessage | DecodedFrame | ErrorMessage;
 
-async function processFrameBytes(
+function processFrameBytesSync(
 	bytes: Uint8Array,
 	releaseCallback?: () => void,
-): Promise<DecodeResult> {
+): DecodeResult {
 	if (bytes.byteLength < 24) {
+		releaseCallback?.();
 		return {
 			type: "error",
 			message: "Received frame too small to contain metadata",
@@ -439,6 +639,7 @@ async function processFrameBytes(
 	);
 
 	if (!width || !height) {
+		releaseCallback?.();
 		return {
 			type: "error",
 			message: `Received invalid frame dimensions: ${width}x${height}`,
@@ -454,6 +655,7 @@ async function processFrameBytes(
 		strideBytes < expectedRowBytes ||
 		frameData.length < availableLength
 	) {
+		releaseCallback?.();
 		return {
 			type: "error",
 			message: `Received invalid frame stride: ${strideBytes}, expected: ${expectedRowBytes}`,
@@ -461,27 +663,15 @@ async function processFrameBytes(
 	}
 
 	if (renderMode === "webgpu" && webgpuRenderer) {
-		if (pendingRenderFrame !== null) {
-			if (
-				pendingRenderFrame.mode === "webgpu" &&
-				pendingRenderFrame.releaseCallback
-			) {
-				pendingRenderFrame.releaseCallback();
+		while (frameQueue.length >= FRAME_QUEUE_SIZE) {
+			const dropped = frameQueue.shift();
+			if (dropped?.mode === "webgpu" && dropped.releaseCallback) {
+				dropped.releaseCallback();
 			}
 			frameDropCount++;
-			const now = performance.now();
-			if (now - lastFrameDropLogTime > 1000) {
-				if (frameDropCount > 0) {
-					console.warn(
-						`[frame-worker] Dropped ${frameDropCount} frames in the last second`,
-					);
-				}
-				frameDropCount = 0;
-				lastFrameDropLogTime = now;
-			}
 		}
 
-		pendingRenderFrame = {
+		frameQueue.push({
 			mode: "webgpu",
 			data: frameData.subarray(0, availableLength),
 			width,
@@ -489,7 +679,7 @@ async function processFrameBytes(
 			strideBytes,
 			timing,
 			releaseCallback,
-		};
+		});
 		startRenderLoop();
 		return { type: "frame-queued", width, height };
 	}
@@ -528,74 +718,26 @@ async function processFrameBytes(
 	cachedImageData.data.set(processedFrameData);
 	lastImageData = cachedImageData;
 
-	if (offscreenCanvas && offscreenCtx) {
-		pendingRenderFrame = {
-			mode: "canvas2d",
-			imageData: cachedImageData,
-			width,
-			height,
-			timing,
-		};
-		startRenderLoop();
-		return { type: "frame-queued", width, height };
-	}
+	releaseCallback?.();
 
-	try {
-		const bitmap = await createImageBitmap(cachedImageData);
-		return {
-			type: "decoded",
-			bitmap,
-			width,
-			height,
-		};
-	} catch (e) {
-		return {
-			type: "error",
-			message: `Failed to create ImageBitmap: ${e}`,
-		};
+	while (frameQueue.length >= FRAME_QUEUE_SIZE) {
+		frameQueue.shift();
+		frameDropCount++;
 	}
-}
 
-async function pollSharedBuffer(): Promise<void> {
-	if (!consumer || !useSharedBuffer) return;
-
-	if (renderMode === "webgpu" && webgpuRenderer) {
-		const borrowed = consumer.borrow(50);
-		if (borrowed !== null) {
-			const result = await processFrameBytes(borrowed.data, borrowed.release);
-			if (result.type === "decoded") {
-				self.postMessage(result, { transfer: [result.bitmap] });
-			} else if (result.type === "frame-queued") {
-				self.postMessage(result);
-			} else if (result.type === "error") {
-				borrowed.release();
-				self.postMessage(result);
-			}
-		}
-	} else {
-		if (!sharedReadBuffer || sharedReadBufferSize < consumer.getSlotSize()) {
-			sharedReadBuffer = new Uint8Array(consumer.getSlotSize());
-			sharedReadBufferSize = sharedReadBuffer.byteLength;
-		}
+	frameQueue.push({
+		mode: "canvas2d",
+		imageData: cachedImageData,
+		width,
+		height,
+		timing,
+	});
 
-		const size = consumer.readInto(sharedReadBuffer, 50);
-		if (size != null && size > 0) {
-			const result = await processFrameBytes(
-				sharedReadBuffer.subarray(0, size),
-			);
-			if (result.type === "decoded") {
-				self.postMessage(result, { transfer: [result.bitmap] });
-			} else if (result.type === "frame-queued") {
-				self.postMessage(result);
-			} else if (result.type === "error") {
-				self.postMessage(result);
-			}
-		}
+	if (offscreenCanvas && offscreenCtx) {
+		startRenderLoop();
 	}
 
-	if (!consumer.isShutdown()) {
-		setTimeout(pollSharedBuffer, 1);
-	}
+	return { type: "frame-queued", width, height };
 }
 
 self.onmessage = async (e: MessageEvent<IncomingMessage>) => {
@@ -607,6 +749,12 @@ self.onmessage = async (e: MessageEvent<IncomingMessage>) => {
 	if (e.data.type === "reset-frame-state") {
 		lastRenderedFrameNumber = -1;
 		playbackStartTime = null;
+		for (const frame of frameQueue) {
+			if (frame.mode === "webgpu" && frame.releaseCallback) {
+				frame.releaseCallback();
+			}
+		}
+		frameQueue = [];
 		return;
 	}
 
@@ -617,7 +765,7 @@ self.onmessage = async (e: MessageEvent<IncomingMessage>) => {
 		sharedReadBufferSize = 0;
 
 		if (workerReady) {
-			pollSharedBuffer();
+			startRenderLoop();
 		}
 		return;
 	}
@@ -656,10 +804,8 @@ self.onmessage = async (e: MessageEvent<IncomingMessage>) => {
 	}
 
 	if (e.data.type === "frame") {
-		const result = await processFrameBytes(new Uint8Array(e.data.buffer));
-		if (result.type === "decoded") {
-			self.postMessage(result, { transfer: [result.bitmap] });
-		} else if (result.type === "frame-queued") {
+		const result = processFrameBytesSync(new Uint8Array(e.data.buffer));
+		if (result.type === "frame-queued") {
 			self.postMessage(result);
 		} else if (result.type === "error") {
 			self.postMessage(result);
diff --git a/apps/desktop/src/utils/socket.ts b/apps/desktop/src/utils/socket.ts
index 921681fcc7..b87f660f52 100644
--- a/apps/desktop/src/utils/socket.ts
+++ b/apps/desktop/src/utils/socket.ts
@@ -13,7 +13,7 @@ import StrideCorrectionWorker from "./stride-correction-worker?worker";
 
 const SAB_SUPPORTED = isSharedArrayBufferSupported();
 const FRAME_BUFFER_CONFIG: SharedFrameBufferConfig = {
-	slotCount: 4,
+	slotCount: 6,
 	slotSize: 16 * 1024 * 1024,
 };
 

From f1f0509252e0b1b784d3262ef87e29bfa639512d Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Tue, 30 Dec 2025 17:42:51 +0000
Subject: [PATCH 11/12] Refactor export UI and improve frame rendering logic

---
 apps/desktop/src/routes/editor/ExportPage.tsx | 199 +++++++++---------
 apps/desktop/src/utils/frame-worker.ts        | 166 +++++++++++----
 apps/desktop/src/utils/socket.ts              |   2 +
 3 files changed, 224 insertions(+), 143 deletions(-)

diff --git a/apps/desktop/src/routes/editor/ExportPage.tsx b/apps/desktop/src/routes/editor/ExportPage.tsx
index b905656a61..56c0f724dd 100644
--- a/apps/desktop/src/routes/editor/ExportPage.tsx
+++ b/apps/desktop/src/routes/editor/ExportPage.tsx
@@ -1,5 +1,4 @@
 import { Button } from "@cap/ui-solid";
-import { RadioGroup as KRadioGroup } from "@kobalte/core/radio-group";
 import { debounce } from "@solid-primitives/scheduled";
 import { makePersisted } from "@solid-primitives/storage";
 import { createMutation } from "@tanstack/solid-query";
@@ -13,7 +12,6 @@ import {
 	createEffect,
 	createSignal,
 	For,
-	type JSX,
 	Match,
 	mergeProps,
 	on,
@@ -41,7 +39,7 @@ import {
 } from "~/utils/tauri";
 import { type RenderState, useEditorContext } from "./context";
 import { RESOLUTION_OPTIONS } from "./Header";
-import { Dialog, Field, Slider } from "./ui";
+import { Dialog, Field } from "./ui";
 
 class SilentError extends Error {}
 
@@ -56,13 +54,6 @@ export const COMPRESSION_OPTIONS: Array<{
 	{ label: "Potato", value: "Potato", bpp: 0.04 },
 ];
 
-const BPP_TO_COMPRESSION: Record<string, ExportCompression> = {
-	"0.3": "Minimal",
-	"0.15": "Social",
-	"0.08": "Web",
-	"0.04": "Potato",
-};
-
 const COMPRESSION_TO_BPP: Record<ExportCompression, number> = {
 	Minimal: 0.3,
 	Social: 0.15,
@@ -585,13 +576,6 @@ export function ExportPage() {
 		},
 	}));
 
-	const qualityLabel = () => {
-		const option = COMPRESSION_OPTIONS.find(
-			(opt) => opt.value === settings.compression,
-		);
-		return option?.label ?? "Minimal";
-	};
-
 	const formatDuration = (seconds: number) => {
 		const hours = Math.floor(seconds / 3600);
 		const minutes = Math.floor((seconds % 3600) / 60);
@@ -756,46 +740,47 @@ export function ExportPage() {
 				<div class="w-[400px] border-l border-gray-3 flex flex-col bg-gray-1 dark:bg-gray-2">
 					<div class="flex-1 overflow-y-auto p-4 space-y-5">
 						<Field name="Destination" icon={<IconCapUpload class="size-4" />}>
-							<KRadioGroup
-								class="flex flex-col gap-2"
-								value={settings.exportTo}
-								onChange={(value) => {
-									setSettings(
-										produce((newSettings) => {
-											newSettings.exportTo = value as ExportToOption;
-											if (value === "link" && settings.format === "Gif") {
-												newSettings.format = "Mp4";
-											}
-										}),
-									);
-								}}
-							>
+							<div class="flex gap-1.5">
 								<For each={EXPORT_TO_OPTIONS}>
 									{(option) => {
 										const Icon = option.icon;
+										const isSelected = () => settings.exportTo === option.value;
 										return (
-											<KRadioGroup.Item
-												value={option.value}
-												class="rounded-lg border border-gray-3 transition-colors ui-checked:border-blue-8 ui-checked:bg-blue-3/40"
+											<button
+												type="button"
+												class={cx(
+													"flex-1 flex flex-col items-center gap-1.5 px-3 py-2.5 rounded-lg border transition-colors",
+													isSelected()
+														? "bg-gray-3 border-gray-5 text-gray-12"
+														: "bg-transparent border-transparent text-gray-11 hover:bg-gray-3 hover:border-gray-4",
+												)}
+												onClick={() => {
+													setSettings(
+														produce((newSettings) => {
+															newSettings.exportTo =
+																option.value as ExportToOption;
+															if (
+																option.value === "link" &&
+																settings.format === "Gif"
+															) {
+																newSettings.format = "Mp4";
+															}
+														}),
+													);
+												}}
 											>
-												<KRadioGroup.ItemInput class="sr-only" />
-												<KRadioGroup.ItemLabel class="flex cursor-pointer items-center gap-3 p-3">
-													<KRadioGroup.ItemControl class="size-4 rounded-full border border-gray-7 ui-checked:border-blue-9 ui-checked:bg-blue-9 shrink-0" />
-													<Icon class="size-4 text-gray-11" />
-													<div class="flex flex-col text-left flex-1">
-														<span class="text-sm font-medium text-gray-12">
-															{option.label}
-														</span>
-														<span class="text-xs text-gray-11">
-															{option.description}
-														</span>
-													</div>
-												</KRadioGroup.ItemLabel>
-											</KRadioGroup.Item>
+												<Icon
+													class={cx(
+														"size-5",
+														isSelected() ? "text-gray-12" : "text-gray-10",
+													)}
+												/>
+												<span class="text-xs font-medium">{option.label}</span>
+											</button>
 										);
 									}}
 								</For>
-							</KRadioGroup>
+							</div>
 
 							<Suspense>
 								<Show
@@ -805,7 +790,7 @@ export function ExportPage() {
 								>
 									<button
 										type="button"
-										class="w-full flex items-center justify-between px-3 py-2 mt-2 rounded-lg bg-gray-3 hover:bg-gray-4 transition-colors text-sm"
+										class="w-full flex items-center justify-between px-3 py-2 mt-3 rounded-lg bg-gray-3 hover:bg-gray-4 transition-colors text-sm"
 										onClick={async () => {
 											const menu = await Menu.new({
 												items: await Promise.all(
@@ -840,7 +825,7 @@ export function ExportPage() {
 						</Field>
 
 						<Field name="Format" icon={<IconLucideVideo class="size-4" />}>
-							<div class="flex gap-2">
+							<div class="flex gap-1.5">
 								<For each={FORMAT_OPTIONS}>
 									{(option) => {
 										const isDisabled = () =>
@@ -855,10 +840,15 @@ export function ExportPage() {
 													: undefined;
 
 										const button = (
-											<Button
-												variant="gray"
-												class="flex-1"
-												data-selected={settings.format === option.value}
+											<button
+												type="button"
+												class={cx(
+													"flex-1 py-2 text-sm font-medium rounded-lg border transition-colors",
+													settings.format === option.value
+														? "bg-gray-3 border-gray-5 text-gray-12"
+														: "bg-transparent border-transparent text-gray-11 hover:bg-gray-3 hover:border-gray-4",
+													isDisabled() && "opacity-50 cursor-not-allowed",
+												)}
 												disabled={isDisabled()}
 												onClick={() => {
 													updateSettings(
@@ -893,7 +883,7 @@ export function ExportPage() {
 												}}
 											>
 												{option.label}
-											</Button>
+											</button>
 										);
 
 										return disabledReason() ? (
@@ -910,7 +900,7 @@ export function ExportPage() {
 							name="Resolution"
 							icon={<IconLucideMonitor class="size-4" />}
 						>
-							<div class="flex gap-2">
+							<div class="flex gap-1.5">
 								<For
 									each={
 										settings.format === "Gif"
@@ -923,40 +913,39 @@ export function ExportPage() {
 									}
 								>
 									{(option) => (
-										<Button
-											variant="gray"
-											class="flex-1"
-											data-selected={settings.resolution.value === option.value}
+										<button
+											type="button"
+											class={cx(
+												"flex-1 py-2 text-sm font-medium rounded-lg border transition-colors",
+												settings.resolution.value === option.value
+													? "bg-gray-3 border-gray-5 text-gray-12"
+													: "bg-transparent border-transparent text-gray-11 hover:bg-gray-3 hover:border-gray-4",
+											)}
 											onClick={() => updateSettings("resolution", option)}
 										>
 											{option.label}
-										</Button>
+										</button>
 									)}
 								</For>
 							</div>
 						</Field>
 
-						<Field
-							name="Frame Rate"
-							icon={<IconLucideGauge class="size-4" />}
-							value={
-								<span class="text-xs text-gray-11 tabular-nums">
-									{settings.fps} FPS
-								</span>
-							}
-						>
-							<div class="flex gap-2">
+						<Field name="Frame Rate" icon={<IconLucideGauge class="size-4" />}>
+							<div class="flex gap-1.5">
 								<For
 									each={
 										settings.format === "Gif" ? GIF_FPS_OPTIONS : FPS_OPTIONS
 									}
 								>
 									{(option) => (
-										<Button
-											variant="gray"
-											size="sm"
-											class="flex-1"
-											data-selected={settings.fps === option.value}
+										<button
+											type="button"
+											class={cx(
+												"flex-1 py-2 text-sm font-medium rounded-lg border transition-colors",
+												settings.fps === option.value
+													? "bg-gray-3 border-gray-5 text-gray-12"
+													: "bg-transparent border-transparent text-gray-11 hover:bg-gray-3 hover:border-gray-4",
+											)}
 											onClick={() => {
 												trackEvent("export_fps_changed", {
 													fps: option.value,
@@ -965,7 +954,7 @@ export function ExportPage() {
 											}}
 										>
 											{option.value}
-										</Button>
+										</button>
 									)}
 								</For>
 							</div>
@@ -975,33 +964,35 @@ export function ExportPage() {
 							<Field
 								name="Quality"
 								icon={<IconLucideSparkles class="size-4" />}
-								value={
-									<span class="text-xs text-gray-11">{qualityLabel()}</span>
-								}
 							>
-								<Slider
-									value={[
-										COMPRESSION_OPTIONS.length -
-											1 -
-											COMPRESSION_OPTIONS.findIndex(
-												(opt) => opt.value === settings.compression,
-											),
-									]}
-									minValue={0}
-									maxValue={COMPRESSION_OPTIONS.length - 1}
-									step={1}
-									onChange={([v]) => {
-										if (v === undefined) return;
-										const option =
-											COMPRESSION_OPTIONS[COMPRESSION_OPTIONS.length - 1 - v];
-										if (option) {
-											setPreviewLoading(true);
-											setCompressionBpp(option.bpp);
-											setSettings("compression", option.value);
-										}
-									}}
-									history={{ pause: () => () => {} }}
-								/>
+								<div class="grid grid-cols-4 gap-1.5">
+									<For each={[...COMPRESSION_OPTIONS].reverse()}>
+										{(option) => (
+											<button
+												type="button"
+												class={cx(
+													"px-2 py-2 text-xs font-medium rounded-lg border transition-colors",
+													settings.compression === option.value
+														? "bg-gray-3 border-gray-5 text-gray-12"
+														: "bg-transparent border-transparent text-gray-11 hover:bg-gray-3 hover:border-gray-4",
+												)}
+												onClick={() => {
+													setPreviewLoading(true);
+													setCompressionBpp(option.bpp);
+													setSettings("compression", option.value);
+												}}
+											>
+												{option.label === "Social Media"
+													? "Social"
+													: option.label}
+											</button>
+										)}
+									</For>
+								</div>
+								<div class="flex justify-between text-[10px] text-gray-10 mt-1.5 px-0.5">
+									<span>Smaller file</span>
+									<span>Larger file</span>
+								</div>
 							</Field>
 						</Show>
 					</div>
diff --git a/apps/desktop/src/utils/frame-worker.ts b/apps/desktop/src/utils/frame-worker.ts
index b24c12241d..65b4f83e03 100644
--- a/apps/desktop/src/utils/frame-worker.ts
+++ b/apps/desktop/src/utils/frame-worker.ts
@@ -153,23 +153,16 @@ let _rafId: number | null = null;
 let rafRunning = false;
 
 let playbackStartTime: number | null = null;
-let playbackStartFrameNumber = 0;
+let playbackStartTargetTimeNs: bigint | null = null;
 let lastRenderedFrameNumber = -1;
 
-const FRAME_DURATION_NS = 1_000_000_000n / 60n;
-const EARLY_THRESHOLD_MS = 2;
-const LATE_THRESHOLD_MS = 100;
+const EARLY_THRESHOLD_MS = 16;
+const LATE_THRESHOLD_MS = 40;
 
 function tryPollSharedBuffer(): boolean {
 	if (!consumer || !useSharedBuffer) return false;
 
-	if (renderMode === "webgpu" && webgpuRenderer) {
-		const borrowed = consumer.borrow(0);
-		if (borrowed !== null) {
-			queueFrameFromBytes(borrowed.data, borrowed.release);
-			return true;
-		}
-	} else {
+	if (renderMode !== "webgpu") {
 		if (!sharedReadBuffer || sharedReadBufferSize < consumer.getSlotSize()) {
 			sharedReadBuffer = new Uint8Array(consumer.getSlotSize());
 			sharedReadBufferSize = sharedReadBuffer.byteLength;
@@ -184,14 +177,15 @@ function tryPollSharedBuffer(): boolean {
 	return false;
 }
 
-function queueFrameFromBytes(
-	bytes: Uint8Array,
-	releaseCallback?: () => void,
-): void {
-	if (bytes.byteLength < 24) {
-		releaseCallback?.();
-		return;
-	}
+function parseFrameMetadata(bytes: Uint8Array): {
+	width: number;
+	height: number;
+	strideBytes: number;
+	frameNumber: number;
+	targetTimeNs: bigint;
+	availableLength: number;
+} | null {
+	if (bytes.byteLength < 24) return null;
 
 	const metadataOffset = bytes.byteOffset + bytes.byteLength - 24;
 	const meta = new DataView(bytes.buffer, metadataOffset, 24);
@@ -200,12 +194,8 @@ function queueFrameFromBytes(
 	const width = meta.getUint32(8, true);
 	const frameNumber = meta.getUint32(12, true);
 	const targetTimeNs = meta.getBigUint64(16, true);
-	const timing: FrameTiming = { frameNumber, targetTimeNs };
 
-	if (!width || !height) {
-		releaseCallback?.();
-		return;
-	}
+	if (!width || !height) return null;
 
 	const expectedRowBytes = width * 4;
 	const availableLength = strideBytes * height;
@@ -215,10 +205,105 @@ function queueFrameFromBytes(
 		strideBytes < expectedRowBytes ||
 		bytes.byteLength - 24 < availableLength
 	) {
+		return null;
+	}
+
+	return {
+		width,
+		height,
+		strideBytes,
+		frameNumber,
+		targetTimeNs,
+		availableLength,
+	};
+}
+
+function renderBorrowedWebGPU(bytes: Uint8Array, release: () => void): boolean {
+	if (renderMode !== "webgpu" || !webgpuRenderer) {
+		release();
+		return false;
+	}
+
+	const meta = parseFrameMetadata(bytes);
+	if (!meta) {
+		release();
+		return false;
+	}
+
+	const {
+		width,
+		height,
+		strideBytes,
+		frameNumber,
+		targetTimeNs,
+		availableLength,
+	} = meta;
+
+	const frameData = new Uint8ClampedArray(
+		bytes.buffer,
+		bytes.byteOffset,
+		bytes.byteLength - 24,
+	).subarray(0, availableLength);
+
+	lastRenderedFrameNumber = frameNumber;
+	if (playbackStartTime === null || playbackStartTargetTimeNs === null) {
+		playbackStartTime = performance.now();
+		playbackStartTargetTimeNs = targetTimeNs;
+	}
+
+	renderFrameWebGPU(webgpuRenderer, frameData, width, height, strideBytes);
+	release();
+
+	self.postMessage({
+		type: "frame-rendered",
+		width,
+		height,
+	} satisfies FrameRenderedMessage);
+
+	return true;
+}
+
+function drainAndRenderLatestSharedWebGPU(maxDrain: number): boolean {
+	if (!consumer || !useSharedBuffer || consumer.isShutdown()) return false;
+	if (renderMode !== "webgpu" || !webgpuRenderer) return false;
+
+	let latest: { bytes: Uint8Array; release: () => void } | null = null;
+
+	for (let i = 0; i < maxDrain; i += 1) {
+		const borrowed = consumer.borrow(0);
+		if (!borrowed) break;
+
+		if (latest) {
+			latest.release();
+		}
+		latest = { bytes: borrowed.data, release: borrowed.release };
+	}
+
+	if (!latest) return false;
+
+	return renderBorrowedWebGPU(latest.bytes, latest.release);
+}
+
+function queueFrameFromBytes(
+	bytes: Uint8Array,
+	releaseCallback?: () => void,
+): void {
+	const meta = parseFrameMetadata(bytes);
+	if (!meta) {
 		releaseCallback?.();
 		return;
 	}
 
+	const {
+		width,
+		height,
+		strideBytes,
+		frameNumber,
+		targetTimeNs,
+		availableLength,
+	} = meta;
+	const timing: FrameTiming = { frameNumber, targetTimeNs };
+
 	const frameData = new Uint8ClampedArray(
 		bytes.buffer,
 		bytes.byteOffset,
@@ -226,13 +311,12 @@ function queueFrameFromBytes(
 	);
 
 	if (renderMode === "webgpu" && webgpuRenderer) {
-		while (frameQueue.length >= FRAME_QUEUE_SIZE) {
-			const dropped = frameQueue.shift();
-			if (dropped?.mode === "webgpu" && dropped.releaseCallback) {
-				dropped.releaseCallback();
+		for (const queued of frameQueue) {
+			if (queued.mode === "webgpu" && queued.releaseCallback) {
+				queued.releaseCallback();
 			}
-			frameDropCount++;
 		}
+		frameQueue = frameQueue.filter((f) => f.mode !== "webgpu");
 
 		frameQueue.push({
 			mode: "webgpu",
@@ -317,6 +401,14 @@ function renderLoop() {
 	}
 
 	if (useSharedBuffer && consumer && !consumer.isShutdown()) {
+		if (renderMode === "webgpu" && webgpuRenderer) {
+			const rendered = drainAndRenderLatestSharedWebGPU(8);
+			if (rendered) {
+				_rafId = requestAnimationFrame(renderLoop);
+				return;
+			}
+		}
+
 		let polled = 0;
 		while (polled < 4 && tryPollSharedBuffer()) {
 			polled++;
@@ -342,14 +434,14 @@ function renderLoop() {
 
 		if (playbackStartTime === null || isSeek) {
 			playbackStartTime = now;
-			playbackStartFrameNumber = frameNum;
+			playbackStartTargetTimeNs = frame.timing.targetTimeNs;
 		}
 
 		if (frame.timing.targetTimeNs > 0n) {
 			const elapsedMs = now - playbackStartTime;
-			const startFrameTimeNs =
-				BigInt(playbackStartFrameNumber) * FRAME_DURATION_NS;
-			const adjustedTargetNs = frame.timing.targetTimeNs - startFrameTimeNs;
+			const startTarget =
+				playbackStartTargetTimeNs ?? frame.timing.targetTimeNs;
+			const adjustedTargetNs = frame.timing.targetTimeNs - startTarget;
 			const targetMs = Number(adjustedTargetNs / 1_000_000n);
 			const diffMs = targetMs - elapsedMs;
 
@@ -360,10 +452,6 @@ function renderLoop() {
 				fallbackIndex = i;
 			}
 
-			if (diffMs > EARLY_THRESHOLD_MS) {
-				continue;
-			}
-
 			if (diffMs < -LATE_THRESHOLD_MS && !isSeek) {
 				if (frame.mode === "webgpu" && frame.releaseCallback) {
 					frame.releaseCallback();
@@ -387,7 +475,7 @@ function renderLoop() {
 
 		if (
 			frameToRender === null ||
-			frame.timing.frameNumber < frameToRender.timing.frameNumber
+			frame.timing.frameNumber > frameToRender.timing.frameNumber
 		) {
 			frameToRender = frame;
 			frameIndex = i;
@@ -496,7 +584,7 @@ function cleanup() {
 	frameDropCount = 0;
 	lastFrameDropLogTime = 0;
 	playbackStartTime = null;
-	playbackStartFrameNumber = 0;
+	playbackStartTargetTimeNs = null;
 	lastRenderedFrameNumber = -1;
 }
 
diff --git a/apps/desktop/src/utils/socket.ts b/apps/desktop/src/utils/socket.ts
index b87f660f52..5bf9c6ad47 100644
--- a/apps/desktop/src/utils/socket.ts
+++ b/apps/desktop/src/utils/socket.ts
@@ -222,6 +222,8 @@ export function createImageDataWS(
 		}
 
 		if (e.data.type === "frame-rendered") {
+			const { width, height } = e.data;
+			onmessage({ width, height });
 			if (!hasRenderedFrame()) {
 				setHasRenderedFrame(true);
 			}

From 2435f662e32e3dcbd7e029240d125a4e3338ce05 Mon Sep 17 00:00:00 2001
From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com>
Date: Tue, 30 Dec 2025 17:47:33 +0000
Subject: [PATCH 12/12] fmt

---
 apps/desktop/src/utils/frame-worker.ts | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/apps/desktop/src/utils/frame-worker.ts b/apps/desktop/src/utils/frame-worker.ts
index 65b4f83e03..ae93be6930 100644
--- a/apps/desktop/src/utils/frame-worker.ts
+++ b/apps/desktop/src/utils/frame-worker.ts
@@ -245,12 +245,22 @@ function renderBorrowedWebGPU(bytes: Uint8Array, release: () => void): boolean {
 		bytes.byteLength - 24,
 	).subarray(0, availableLength);
 
-	lastRenderedFrameNumber = frameNumber;
-	if (playbackStartTime === null || playbackStartTargetTimeNs === null) {
+	const isSeek =
+		lastRenderedFrameNumber >= 0 &&
+		(frameNumber < lastRenderedFrameNumber ||
+			frameNumber > lastRenderedFrameNumber + 30);
+
+	if (
+		playbackStartTime === null ||
+		playbackStartTargetTimeNs === null ||
+		isSeek
+	) {
 		playbackStartTime = performance.now();
 		playbackStartTargetTimeNs = targetTimeNs;
 	}
 
+	lastRenderedFrameNumber = frameNumber;
+
 	renderFrameWebGPU(webgpuRenderer, frameData, width, height, strideBytes);
 	release();
 
@@ -303,6 +313,7 @@ function queueFrameFromBytes(
 		availableLength,
 	} = meta;
 	const timing: FrameTiming = { frameNumber, targetTimeNs };
+	const expectedRowBytes = width * 4;
 
 	const frameData = new Uint8ClampedArray(
 		bytes.buffer,
@@ -837,6 +848,7 @@ self.onmessage = async (e: MessageEvent<IncomingMessage>) => {
 	if (e.data.type === "reset-frame-state") {
 		lastRenderedFrameNumber = -1;
 		playbackStartTime = null;
+		playbackStartTargetTimeNs = null;
 		for (const frame of frameQueue) {
 			if (frame.mode === "webgpu" && frame.releaseCallback) {
 				frame.releaseCallback();
