summaryrefslogtreecommitdiff
path: root/apps/web/src/services/renderer/compositor/wasm-compositor.ts
blob: 16b0385335c0289012bac36592ed67878040473b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import {
	getCompositorCanvas,
	initCompositor,
	releaseTexture,
	renderFrame,
	resizeCompositor,
	uploadTexture,
} from "opencut-wasm";
import type { FrameDescriptor } from "./types";

function ensureOffscreenCanvas({
	source,
	width,
	height,
	label,
}: {
	source: CanvasImageSource;
	width: number;
	height: number;
	label: string;
}): OffscreenCanvas {
	if (source instanceof OffscreenCanvas) {
		return source;
	}

	if (typeof OffscreenCanvas === "undefined") {
		throw new Error(`OffscreenCanvas is required for ${label}`);
	}

	const canvas = new OffscreenCanvas(width, height);
	const context = canvas.getContext("2d");
	if (!context) {
		throw new Error(`Failed to get 2d context for ${label}`);
	}
	context.clearRect(0, 0, width, height);
	context.drawImage(source, 0, 0, width, height);
	return canvas;
}

export type TextureUploadDescriptor = {
	id: string;
	source: CanvasImageSource;
	width: number;
	height: number;
};

class WasmCompositor {
	private canvas: HTMLCanvasElement | null = null;
	private initializedSize: { width: number; height: number } | null = null;
	private retainedTextureIds = new Set<string>();
	private uploadedTextures = new Map<
		string,
		{ source: CanvasImageSource; width: number; height: number }
	>();

	ensureInitialized({ width, height }: { width: number; height: number }) {
		if (!this.canvas) {
			initCompositor(width, height);
			this.canvas = getCompositorCanvas();
			this.initializedSize = { width, height };
			return;
		}

		if (
			!this.initializedSize ||
			this.initializedSize.width !== width ||
			this.initializedSize.height !== height
		) {
			resizeCompositor(width, height);
			this.initializedSize = { width, height };
		}
	}

	getCanvas(): HTMLCanvasElement {
		if (!this.canvas) {
			throw new Error("Compositor is not initialized");
		}
		return this.canvas;
	}

	syncTextures(textures: TextureUploadDescriptor[]) {
		const nextIds = new Set(textures.map((texture) => texture.id));
		for (const previousId of this.retainedTextureIds) {
			if (!nextIds.has(previousId)) {
				releaseTexture(previousId);
				this.uploadedTextures.delete(previousId);
			}
		}

		for (const texture of textures) {
			const previousTexture = this.uploadedTextures.get(texture.id);
			if (
				previousTexture?.source === texture.source &&
				previousTexture.width === texture.width &&
				previousTexture.height === texture.height
			) {
				continue;
			}

			const sourceCanvas = ensureOffscreenCanvas({
				source: texture.source,
				width: texture.width,
				height: texture.height,
				label: `texture upload ${texture.id}`,
			});
			uploadTexture({
				id: texture.id,
				source: sourceCanvas,
				width: texture.width,
				height: texture.height,
			});
			this.uploadedTextures.set(texture.id, {
				source: texture.source,
				width: texture.width,
				height: texture.height,
			});
		}

		this.retainedTextureIds = nextIds;
	}

	render(frame: FrameDescriptor) {
		renderFrame(frame);
	}
}

export const wasmCompositor = new WasmCompositor();