Merge pull request #330 from maxbailey/main

fix: resolve green MP4 exports on CachyOS/Arch Linux (Wayland)
This commit is contained in:
Sid
2026-04-07 22:28:00 -07:00
committed by GitHub
+49 -1
View File
@@ -112,6 +112,8 @@ export class FrameRenderer {
private shadowCtx: CanvasRenderingContext2D | null = null;
private compositeCanvas: HTMLCanvasElement | null = null;
private compositeCtx: CanvasRenderingContext2D | null = null;
private rasterCanvas: HTMLCanvasElement | null = null;
private rasterCtx: CanvasRenderingContext2D | null = null;
private config: FrameRenderConfig;
private animationState: AnimationState;
private layoutCache: LayoutCache | null = null;
@@ -191,6 +193,14 @@ export class FrameRenderer {
throw new Error("Failed to get 2D context for composite canvas");
}
this.rasterCanvas = document.createElement("canvas");
this.rasterCanvas.width = this.config.width;
this.rasterCanvas.height = this.config.height;
this.rasterCtx = this.rasterCanvas.getContext("2d");
if (!this.rasterCtx) {
throw new Error("Failed to get 2D context for raster canvas");
}
// Setup shadow canvas if needed
if (this.config.showShadow) {
this.shadowCanvas = document.createElement("canvas");
@@ -675,10 +685,46 @@ export class FrameRenderer {
);
}
// On Linux/Wayland the implicit GPU→2D texture-sharing path
// used by drawImage(webglCanvas) can fail silently (EGL/Ozone),
// producing green/empty frames. Explicit gl.readPixels always
// copies from GPU to CPU memory, bypassing that path.
private readbackVideoCanvas(): HTMLCanvasElement {
const glCanvas = this.app!.canvas as HTMLCanvasElement;
const gl =
(glCanvas.getContext("webgl2") as WebGL2RenderingContext | null) ??
(glCanvas.getContext("webgl") as WebGLRenderingContext | null);
if (!gl || !this.rasterCanvas || !this.rasterCtx) {
return glCanvas;
}
const w = glCanvas.width;
const h = glCanvas.height;
const buf = new Uint8Array(w * h * 4);
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, buf);
// readPixels returns rows bottom-to-top; flip vertically
const rowSize = w * 4;
const temp = new Uint8Array(rowSize);
for (let top = 0, bot = h - 1; top < bot; top++, bot--) {
const tOff = top * rowSize;
const bOff = bot * rowSize;
temp.set(buf.subarray(tOff, tOff + rowSize));
buf.copyWithin(tOff, bOff, bOff + rowSize);
buf.set(temp, bOff);
}
const imageData = new ImageData(new Uint8ClampedArray(buf.buffer), w, h);
this.rasterCtx.putImageData(imageData, 0, 0);
return this.rasterCanvas;
}
private compositeWithShadows(webcamFrame?: VideoFrame | null): void {
if (!this.compositeCanvas || !this.compositeCtx || !this.app) return;
const videoCanvas = this.app.canvas as HTMLCanvasElement;
const videoCanvas = this.readbackVideoCanvas();
const ctx = this.compositeCtx;
const w = this.compositeCanvas.width;
const h = this.compositeCanvas.height;
@@ -795,5 +841,7 @@ export class FrameRenderer {
this.shadowCtx = null;
this.compositeCanvas = null;
this.compositeCtx = null;
this.rasterCanvas = null;
this.rasterCtx = null;
}
}