jassub 2.0.10 → 2.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
1
+ // TypeScript bindings for emscripten-generated code. Automatically generated at compile time.
2
+ declare namespace RuntimeExports {
3
+ function getTempRet0(val: any): any;
4
+ function setTempRet0(val: any): any;
5
+ }
6
+ interface WasmModule {
7
+ __ZdlPvm(_0: number, _1: number): void;
8
+ _malloc(_0: number): number;
9
+ _calloc(_0: number, _1: number): number;
10
+ _emscripten_builtin_free(_0: number): void;
11
+ ___libc_free(_0: number): void;
12
+ _emscripten_builtin_malloc(_0: number): number;
13
+ ___libc_malloc(_0: number): number;
14
+ __ZdaPv(_0: number): void;
15
+ __ZdaPvm(_0: number, _1: number): void;
16
+ __ZdlPv(_0: number): void;
17
+ __Znaj(_0: number): number;
18
+ __ZnajSt11align_val_t(_0: number, _1: number): number;
19
+ __Znwj(_0: number): number;
20
+ __ZnwjSt11align_val_t(_0: number, _1: number): number;
21
+ ___libc_calloc(_0: number, _1: number): number;
22
+ ___libc_realloc(_0: number, _1: number): number;
23
+ _emscripten_builtin_calloc(_0: number, _1: number): number;
24
+ _emscripten_builtin_realloc(_0: number, _1: number): number;
25
+ _malloc_size(_0: number): number;
26
+ _malloc_usable_size(_0: number): number;
27
+ _reallocf(_0: number, _1: number): number;
28
+ }
29
+
30
+ type EmbindString = ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|string;
31
+ export interface ClassHandle {
32
+ isAliasOf(other: ClassHandle): boolean;
33
+ delete(): void;
34
+ deleteLater(): this;
35
+ isDeleted(): boolean;
36
+ // @ts-ignore - If targeting lower than ESNext, this symbol might not exist.
37
+ [Symbol.dispose](): void;
38
+ clone(): this;
39
+ }
40
+ export interface ASS_Image extends ClassHandle {
41
+ readonly next: ASS_Image;
42
+ w: number;
43
+ h: number;
44
+ dst_x: number;
45
+ dst_y: number;
46
+ stride: number;
47
+ color: number;
48
+ readonly bitmap: number;
49
+ }
50
+
51
+ export interface ASS_Style extends ClassHandle {
52
+ Bold: number;
53
+ Italic: number;
54
+ Underline: number;
55
+ StrikeOut: number;
56
+ BorderStyle: number;
57
+ Alignment: number;
58
+ MarginL: number;
59
+ MarginR: number;
60
+ MarginV: number;
61
+ Encoding: number;
62
+ treat_fontname_as_pattern: number;
63
+ Justify: number;
64
+ PrimaryColour: number;
65
+ SecondaryColour: number;
66
+ OutlineColour: number;
67
+ BackColour: number;
68
+ FontSize: number;
69
+ ScaleX: number;
70
+ ScaleY: number;
71
+ Spacing: number;
72
+ Angle: number;
73
+ Outline: number;
74
+ Shadow: number;
75
+ Blur: number;
76
+ get Name(): string;
77
+ set Name(value: EmbindString);
78
+ get FontName(): string;
79
+ set FontName(value: EmbindString);
80
+ }
81
+
82
+ export interface ASS_Event extends ClassHandle {
83
+ ReadOrder: number;
84
+ Layer: number;
85
+ Style: number;
86
+ MarginL: number;
87
+ MarginR: number;
88
+ MarginV: number;
89
+ Start: number;
90
+ Duration: number;
91
+ get Name(): string;
92
+ set Name(value: EmbindString);
93
+ get Effect(): string;
94
+ set Effect(value: EmbindString);
95
+ get Text(): string;
96
+ set Text(value: EmbindString);
97
+ }
98
+
99
+ export interface JASSUB extends ClassHandle {
100
+ trackColorSpace: number;
101
+ changed: number;
102
+ count: number;
103
+ removeTrack(): void;
104
+ quitLibrary(): void;
105
+ reloadFonts(): void;
106
+ removeAllEvents(): void;
107
+ styleOverride(_0: ASS_Style): void;
108
+ disableStyleOverride(): void;
109
+ setLogLevel(_0: number): void;
110
+ resizeCanvas(_0: number, _1: number, _2: number, _3: number): void;
111
+ setMargin(_0: number, _1: number, _2: number, _3: number): void;
112
+ getEventCount(): number;
113
+ allocEvent(): number;
114
+ allocStyle(): number;
115
+ removeEvent(_0: number): void;
116
+ getStyleCount(): number;
117
+ removeStyle(_0: number): void;
118
+ setMemoryLimits(_0: number, _1: number): void;
119
+ getEvent(_0: number): ASS_Event | null;
120
+ getStyle(_0: number): ASS_Style | null;
121
+ setThreads(_0: number): number;
122
+ rawRender(_0: number, _1: number): ASS_Image | null;
123
+ createTrackMem(_0: EmbindString): void;
124
+ addFont(_0: EmbindString, _1: number, _2: number): void;
125
+ setDefaultFont(_0: EmbindString): void;
126
+ }
127
+
128
+ interface EmbindModule {
129
+ ASS_Image: {};
130
+ ASS_Style: {};
131
+ ASS_Event: {};
132
+ JASSUB: {
133
+ new(_0: number, _1: number, _2: EmbindString): JASSUB;
134
+ };
135
+ }
136
+
137
+ export type MainModule = WasmModule & typeof RuntimeExports & EmbindModule;
138
+ export default function MainModuleFactory (options?: unknown): Promise<MainModule>;
@@ -0,0 +1,35 @@
1
+ import type { ASSEvent, ASSStyle } from '../jassub'
2
+
3
+ export const read_ = (url: string, ab = false) => {
4
+ const xhr = new XMLHttpRequest()
5
+ xhr.open('GET', url, false)
6
+ xhr.responseType = ab ? 'arraybuffer' : 'text'
7
+ xhr.send(null)
8
+ return xhr.response
9
+ }
10
+
11
+ export const readAsync = (url: string, load: (response: ArrayBuffer) => void, err: (error: unknown) => void) => {
12
+ const xhr = new XMLHttpRequest()
13
+ xhr.open('GET', url, true)
14
+ xhr.responseType = 'arraybuffer'
15
+ xhr.onload = () => {
16
+ if ((xhr.status === 200 || xhr.status === 0) && xhr.response) {
17
+ return load(xhr.response)
18
+ }
19
+ }
20
+ xhr.onerror = err
21
+ xhr.send(null)
22
+ }
23
+
24
+ const a = 'BT601'
25
+ const b = 'BT709'
26
+ const c = 'SMPTE240M'
27
+ const d = 'FCC'
28
+
29
+ export const libassYCbCrMap = [null, a, null, a, a, b, b, c, c, d, d] as const
30
+
31
+ export function _applyKeys<T extends (ASSEvent | ASSStyle)> (input: T, output: T) {
32
+ for (const v of Object.keys(input) as Array<keyof T>) {
33
+ output[v] = input[v]
34
+ }
35
+ }
@@ -0,0 +1,497 @@
1
+ import type { ASSImage } from '../jassub'
2
+
3
+ const IDENTITY_MATRIX = new Float32Array([
4
+ 1, 0, 0, 0,
5
+ 0, 1, 0, 0,
6
+ 0, 0, 1, 0
7
+ ])
8
+
9
+ // Color matrix conversion map - mat3x3 pre-padded for WGSL (each column padded to vec4f)
10
+ // Each matrix converts FROM the key color space TO the nested key color space
11
+ export const colorMatrixConversionMap = {
12
+ BT601: {
13
+ BT709: new Float32Array([
14
+ 1.0863, 0.0965, -0.0141, 0,
15
+ -0.0723, 0.8451, -0.0277, 0,
16
+ -0.014, 0.0584, 1.0418, 0
17
+ ]),
18
+ BT601: IDENTITY_MATRIX
19
+ },
20
+ BT709: {
21
+ BT601: new Float32Array([
22
+ 0.9137, -0.1049, 0.0096, 0,
23
+ 0.0784, 1.1722, 0.0322, 0,
24
+ 0.0079, -0.0671, 0.9582, 0
25
+ ]),
26
+ BT709: IDENTITY_MATRIX
27
+ },
28
+ FCC: {
29
+ BT709: new Float32Array([
30
+ 1.0873, 0.0974, -0.0127, 0,
31
+ -0.0736, 0.8494, -0.0251, 0,
32
+ -0.0137, 0.0531, 1.0378, 0
33
+ ]),
34
+ BT601: new Float32Array([
35
+ 1.001, 0.0009, 0.0013, 0,
36
+ -0.0008, 1.005, 0.0027, 0,
37
+ -0.0002, -0.006, 0.996, 0
38
+ ])
39
+ },
40
+ SMPTE240M: {
41
+ BT709: new Float32Array([
42
+ 0.9993, -0.0004, -0.0034, 0,
43
+ 0.0006, 0.9812, -0.0114, 0,
44
+ 0.0001, 0.0192, 1.0148, 0
45
+ ]),
46
+ BT601: new Float32Array([
47
+ 0.913, -0.1051, 0.0063, 0,
48
+ 0.0774, 1.1508, 0.0207, 0,
49
+ 0.0096, -0.0456, 0.973, 0
50
+ ])
51
+ }
52
+ } as const
53
+
54
+ export type ColorSpace = keyof typeof colorMatrixConversionMap
55
+
56
+ // WGSL Vertex Shader
57
+ const VERTEX_SHADER = /* wgsl */`
58
+ struct VertexOutput {
59
+ @builtin(position) position: vec4f,
60
+ @location(0) @interpolate(flat) destXY: vec2f, // destination top-left (flat, no interpolation)
61
+ @location(1) @interpolate(flat) color: vec4f,
62
+ @location(2) @interpolate(flat) texSize: vec2f,
63
+ }
64
+
65
+ struct Uniforms {
66
+ resolution: vec2f,
67
+ }
68
+
69
+ struct ImageData {
70
+ destRect: vec4f, // x, y, w, h
71
+ srcInfo: vec4f, // texW, texH, stride, 0
72
+ color: vec4f, // RGBA
73
+ }
74
+
75
+ @group(0) @binding(0) var<uniform> uniforms: Uniforms;
76
+ @group(0) @binding(1) var<storage, read> imageData: ImageData;
77
+
78
+ // Quad vertices (two triangles)
79
+ const QUAD_POSITIONS = array<vec2f, 6>(
80
+ vec2f(0.0, 0.0),
81
+ vec2f(1.0, 0.0),
82
+ vec2f(0.0, 1.0),
83
+ vec2f(1.0, 0.0),
84
+ vec2f(1.0, 1.0),
85
+ vec2f(0.0, 1.0)
86
+ );
87
+
88
+ @vertex
89
+ fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
90
+ var output: VertexOutput;
91
+
92
+ let quadPos = QUAD_POSITIONS[vertexIndex];
93
+ let wh = imageData.destRect.zw;
94
+
95
+ // Calculate pixel position
96
+ let pixelPos = imageData.destRect.xy + quadPos * wh;
97
+
98
+ // Convert to clip space (-1 to 1)
99
+ var clipPos = (pixelPos / uniforms.resolution) * 2.0 - 1.0;
100
+ clipPos.y = -clipPos.y; // Flip Y for canvas coordinates
101
+
102
+ output.position = vec4f(clipPos, 0.0, 1.0);
103
+ output.destXY = imageData.destRect.xy;
104
+ output.color = imageData.color;
105
+ output.texSize = imageData.srcInfo.xy;
106
+
107
+ return output;
108
+ }
109
+ `
110
+
111
+ // WGSL Fragment Shader - use textureLoad with integer coords for pixel-perfect sampling
112
+ const FRAGMENT_SHADER = /* wgsl */`
113
+ @group(0) @binding(3) var tex: texture_2d<f32>;
114
+ @group(0) @binding(4) var<uniform> colorMatrix: mat3x3f;
115
+
116
+ struct FragmentInput {
117
+ @builtin(position) fragCoord: vec4f,
118
+ @location(0) @interpolate(flat) destXY: vec2f,
119
+ @location(1) @interpolate(flat) color: vec4f,
120
+ @location(2) @interpolate(flat) texSize: vec2f,
121
+ }
122
+
123
+ @fragment
124
+ fn fragmentMain(input: FragmentInput) -> @location(0) vec4f {
125
+ // Calculate integer texel coordinates from fragment position
126
+ // fragCoord.xy is the pixel center (e.g., 0.5, 1.5, 2.5...)
127
+ let texCoord = vec2i(floor(input.fragCoord.xy - input.destXY));
128
+
129
+ // Bounds check (should not be needed but prevents any out-of-bounds access)
130
+ let texSizeI = vec2i(input.texSize);
131
+ if (texCoord.x < 0 || texCoord.y < 0 || texCoord.x >= texSizeI.x || texCoord.y >= texSizeI.y) {
132
+ return vec4f(0.0);
133
+ }
134
+
135
+ // Load texel directly using integer coordinates - no interpolation, no precision issues
136
+ let mask = textureLoad(tex, texCoord, 0).r;
137
+
138
+ // Apply color matrix conversion (identity if no conversion needed)
139
+ let correctedColor = colorMatrix * input.color.rgb;
140
+
141
+ // libass color alpha: 0 = opaque, 255 = transparent (inverted)
142
+ let colorAlpha = 1.0 - input.color.a;
143
+
144
+ // Final alpha = colorAlpha * mask (like libass: alpha * mask)
145
+ let a = colorAlpha * mask;
146
+
147
+ // Premultiplied alpha output
148
+ return vec4f(correctedColor * a, a);
149
+ }
150
+ `
151
+
152
+ interface TextureInfo {
153
+ texture: GPUTexture
154
+ view: GPUTextureView
155
+ width: number
156
+ height: number
157
+ }
158
+
159
+ export class WebGPURenderer {
160
+ device: GPUDevice | null = null
161
+ context: GPUCanvasContext | null = null
162
+ pipeline: GPURenderPipeline | null = null
163
+ bindGroupLayout: GPUBindGroupLayout | null = null
164
+
165
+ // Uniform buffer
166
+ uniformBuffer: GPUBuffer | null = null
167
+
168
+ // Color matrix buffer (mat3x3f = 48 bytes with padding)
169
+ colorMatrixBuffer: GPUBuffer | null = null
170
+
171
+ // Image data buffers (created on-demand, one per image)
172
+ imageDataBuffers: GPUBuffer[] = []
173
+
174
+ // Textures created on-demand (no fixed limit)
175
+ textures: TextureInfo[] = []
176
+ pendingDestroyTextures: GPUTexture[] = []
177
+
178
+ // eslint-disable-next-line no-undef
179
+ format: GPUTextureFormat = 'bgra8unorm'
180
+ readonly initPromise: Promise<void> | null = null
181
+
182
+ constructor () {
183
+ // Start async initialization immediately
184
+ this.initPromise = this._initDevice()
185
+ }
186
+
187
+ async _initDevice (): Promise<void> {
188
+ // Check WebGPU support
189
+ if (!navigator.gpu) {
190
+ throw new Error('WebGPU not supported')
191
+ }
192
+
193
+ const adapter = await navigator.gpu.requestAdapter({
194
+ powerPreference: 'high-performance'
195
+ })
196
+
197
+ if (!adapter) {
198
+ throw new Error('No WebGPU adapter found')
199
+ }
200
+
201
+ this.device = await adapter.requestDevice()
202
+ this.format = navigator.gpu.getPreferredCanvasFormat()
203
+
204
+ // Create shader modules
205
+ const vertexModule = this.device.createShaderModule({
206
+ code: VERTEX_SHADER
207
+ })
208
+
209
+ const fragmentModule = this.device.createShaderModule({
210
+ code: FRAGMENT_SHADER
211
+ })
212
+
213
+ // Create uniform buffer
214
+ this.uniformBuffer = this.device.createBuffer({
215
+ size: 16, // vec2f resolution + padding
216
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
217
+ })
218
+
219
+ // Create color matrix buffer (mat3x3f requires 48 bytes: 3 vec3f padded to vec4f each)
220
+ this.colorMatrixBuffer = this.device.createBuffer({
221
+ size: 48, // 3 x vec4f (each column is vec3f padded to 16 bytes)
222
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
223
+ })
224
+ // Initialize with identity matrix
225
+ this.setColorMatrix()
226
+
227
+ // Create bind group layout (no sampler needed - using textureLoad for pixel-perfect sampling)
228
+ this.bindGroupLayout = this.device.createBindGroupLayout({
229
+ entries: [
230
+ {
231
+ binding: 0,
232
+ visibility: GPUShaderStage.VERTEX,
233
+ buffer: { type: 'uniform' }
234
+ },
235
+ {
236
+ binding: 1,
237
+ visibility: GPUShaderStage.VERTEX,
238
+ buffer: { type: 'read-only-storage' }
239
+ },
240
+ {
241
+ binding: 3,
242
+ visibility: GPUShaderStage.FRAGMENT,
243
+ texture: { sampleType: 'unfilterable-float' } // textureLoad requires unfilterable
244
+ },
245
+ {
246
+ binding: 4,
247
+ visibility: GPUShaderStage.FRAGMENT,
248
+ buffer: { type: 'uniform' }
249
+ }
250
+ ]
251
+ })
252
+
253
+ // Create pipeline layout
254
+ const pipelineLayout = this.device.createPipelineLayout({
255
+ bindGroupLayouts: [this.bindGroupLayout]
256
+ })
257
+
258
+ // Create render pipeline
259
+ this.pipeline = this.device.createRenderPipeline({
260
+ layout: pipelineLayout,
261
+ vertex: {
262
+ module: vertexModule,
263
+ entryPoint: 'vertexMain'
264
+ },
265
+ fragment: {
266
+ module: fragmentModule,
267
+ entryPoint: 'fragmentMain',
268
+ targets: [
269
+ {
270
+ format: this.format,
271
+ blend: {
272
+ color: {
273
+ srcFactor: 'one',
274
+ dstFactor: 'one-minus-src-alpha',
275
+ operation: 'add'
276
+ },
277
+ alpha: {
278
+ srcFactor: 'one',
279
+ dstFactor: 'one-minus-src-alpha',
280
+ operation: 'add'
281
+ }
282
+ }
283
+ }
284
+ ]
285
+ },
286
+ primitive: {
287
+ topology: 'triangle-list'
288
+ }
289
+ })
290
+ }
291
+
292
+ async setCanvas (canvas: OffscreenCanvas, width: number, height: number) {
293
+ await this.initPromise
294
+ if (!this.device) {
295
+ throw new Error('WebGPU device not initialized. Did you await the constructor promise?')
296
+ }
297
+
298
+ canvas.width = width
299
+ canvas.height = height
300
+
301
+ if (!this.context) {
302
+ // Get canvas context
303
+ this.context = canvas.getContext('webgpu')
304
+ if (!this.context) {
305
+ throw new Error('Could not get WebGPU context')
306
+ }
307
+
308
+ this.context.configure({
309
+ device: this.device,
310
+ format: this.format,
311
+ alphaMode: 'premultiplied'
312
+ })
313
+ }
314
+
315
+ // Update uniform buffer with resolution
316
+ this.device.queue.writeBuffer(
317
+ this.uniformBuffer!,
318
+ 0,
319
+ new Float32Array([width, height])
320
+ )
321
+ }
322
+
323
+ /**
324
+ * Set the color matrix for color space conversion.
325
+ * Pass null or undefined to use identity (no conversion).
326
+ * Matrix should be a pre-padded Float32Array with 12 values (3 columns × 4 floats each).
327
+ */
328
+ setColorMatrix (matrix?: Float32Array<ArrayBuffer>): void {
329
+ if (!this.device || !this.colorMatrixBuffer) return
330
+ this.device.queue.writeBuffer(this.colorMatrixBuffer, 0, matrix ?? IDENTITY_MATRIX)
331
+ }
332
+
333
+ private createTextureInfo (width: number, height: number): TextureInfo {
334
+ const texture = this.device!.createTexture({
335
+ size: [width, height],
336
+ format: 'r8unorm',
337
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
338
+ })
339
+
340
+ return {
341
+ texture,
342
+ view: texture.createView(),
343
+ width,
344
+ height
345
+ }
346
+ }
347
+
348
+ render (images: ASSImage[], heap: Uint8Array): void {
349
+ if (!this.device || !this.context || !this.pipeline) return
350
+
351
+ const commandEncoder = this.device.createCommandEncoder()
352
+
353
+ const textureView = this.context.getCurrentTexture().createView()
354
+
355
+ // Begin render pass
356
+ const renderPass = commandEncoder.beginRenderPass({
357
+ colorAttachments: [
358
+ {
359
+ view: textureView,
360
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
361
+ loadOp: 'clear',
362
+ storeOp: 'store'
363
+ }
364
+ ]
365
+ })
366
+
367
+ renderPass.setPipeline(this.pipeline)
368
+
369
+ // Grow arrays if needed
370
+ while (this.textures.length < images.length) {
371
+ this.textures.push(this.createTextureInfo(64, 64))
372
+ }
373
+ while (this.imageDataBuffers.length < images.length) {
374
+ this.imageDataBuffers.push(this.device.createBuffer({
375
+ size: 48, // 3 x vec4f
376
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
377
+ }))
378
+ }
379
+
380
+ // Render each image
381
+ for (let i = 0, texIndex = 0; i < images.length; i++, texIndex++) {
382
+ const img = images[i]!
383
+
384
+ // Skip images with invalid dimensions (WebGPU doesn't allow 0-sized textures)
385
+ if (img.w <= 0 || img.h <= 0) continue
386
+
387
+ let texInfo = this.textures[texIndex]!
388
+
389
+ // Recreate texture if size changed (use actual w, not stride)
390
+ if (texInfo.width !== img.w || texInfo.height !== img.h) {
391
+ // Defer destruction until after submit to avoid destroying textures still in use
392
+ this.pendingDestroyTextures.push(texInfo.texture)
393
+ texInfo = this.createTextureInfo(img.w, img.h)
394
+ this.textures[texIndex] = texInfo
395
+ }
396
+
397
+ // Upload bitmap data using bytesPerRow to handle stride
398
+ // Only need stride * (h-1) + w bytes per ASS spec
399
+ // this... didnt work, is the used alternative bad?
400
+ // const dataSize = img.stride * (img.h - 1) + img.w
401
+ // const bitmapData = heap.subarray(img.bitmap, img.bitmap + dataSize)
402
+
403
+ // this.device.queue.writeTexture(
404
+ // { texture: texInfo.texture },
405
+ // bitmapData as unknown as ArrayBuffer,
406
+ // { bytesPerRow: img.stride }, // Source rows are stride bytes apart
407
+ // { width: img.w, height: img.h } // But we only copy w pixels per row
408
+ // )
409
+
410
+ this.device.queue.writeTexture(
411
+ { texture: texInfo.texture },
412
+ heap.buffer,
413
+ { bytesPerRow: img.stride, offset: img.bitmap }, // Source rows are stride bytes apart
414
+ { width: img.w, height: img.h } // But we only copy w pixels per row
415
+ )
416
+
417
+ // Update image data buffer
418
+ const imageData = new Float32Array([
419
+ // destRect
420
+ img.dst_x, img.dst_y, img.w, img.h,
421
+ // srcInfo
422
+ img.w, img.h, img.stride, 0,
423
+ // color (RGBA from 0xRRGGBBAA)
424
+ ((img.color >>> 24) & 0xFF) / 255,
425
+ ((img.color >>> 16) & 0xFF) / 255,
426
+ ((img.color >>> 8) & 0xFF) / 255,
427
+ (img.color & 0xFF) / 255
428
+ ])
429
+
430
+ const imageBuffer = this.imageDataBuffers[texIndex]!
431
+ this.device.queue.writeBuffer(imageBuffer, 0, imageData)
432
+
433
+ // Create bind group for this image (no sampler - using textureLoad)
434
+ const bindGroup = this.device.createBindGroup({
435
+ layout: this.bindGroupLayout!,
436
+ entries: [
437
+ { binding: 0, resource: { buffer: this.uniformBuffer! } },
438
+ { binding: 1, resource: { buffer: imageBuffer } },
439
+ { binding: 3, resource: texInfo.view },
440
+ { binding: 4, resource: { buffer: this.colorMatrixBuffer! } }
441
+ ]
442
+ })
443
+
444
+ renderPass.setBindGroup(0, bindGroup)
445
+ renderPass.draw(6) // 6 vertices for quad
446
+ }
447
+
448
+ renderPass.end()
449
+
450
+ this.device.queue.submit([commandEncoder.finish()])
451
+
452
+ // Now safe to destroy old textures
453
+ for (const tex of this.pendingDestroyTextures) {
454
+ tex.destroy()
455
+ }
456
+ this.pendingDestroyTextures = []
457
+ }
458
+
459
+ clear (): void {
460
+ if (!this.device || !this.context) return
461
+
462
+ const commandEncoder = this.device.createCommandEncoder()
463
+ const textureView = this.context.getCurrentTexture().createView()
464
+
465
+ const renderPass = commandEncoder.beginRenderPass({
466
+ colorAttachments: [
467
+ {
468
+ view: textureView,
469
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
470
+ loadOp: 'clear',
471
+ storeOp: 'store'
472
+ }
473
+ ]
474
+ })
475
+
476
+ renderPass.end()
477
+ this.device.queue.submit([commandEncoder.finish()])
478
+ }
479
+
480
+ destroy (): void {
481
+ for (const tex of this.textures) {
482
+ tex.texture.destroy()
483
+ }
484
+ this.textures = []
485
+
486
+ this.uniformBuffer?.destroy()
487
+ this.colorMatrixBuffer?.destroy()
488
+ for (const buf of this.imageDataBuffers) {
489
+ buf.destroy()
490
+ }
491
+ this.imageDataBuffers = []
492
+
493
+ this.device?.destroy()
494
+ this.device = null
495
+ this.context = null
496
+ }
497
+ }