jassub 2.2.3 → 2.2.5

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.
@@ -6,9 +6,19 @@ declare const self: DedicatedWorkerGlobalScope &
6
6
  WASMMEMORY: WebAssembly.Memory
7
7
  }
8
8
 
9
+ const IS_FIREFOX = navigator.userAgent.toLowerCase().includes('firefox')
10
+
11
+ const THREAD_COUNT = !IS_FIREFOX && self.crossOriginIsolated ? Math.min(Math.max(1, navigator.hardwareConcurrency - 2), 8) : 1
12
+
9
13
  // @ts-expect-error new experimental API
10
14
  const SUPPORTS_GROWTH = !!WebAssembly.Memory.prototype.toResizableBuffer
11
15
 
16
+ // HACK: 3 memory hacks to support here:
17
+ // 1. Chrome WASM Growable memory which can use a reference to the buffer to fix visual artifacts, which happen both with multithreading or without [fastest]
18
+ // 2. Chrome WASM non-growable, but mult-threaded only memory which needs to re-create the HEAPU8 on growth because of race conditions [medium]
19
+ // 3. Firefox non-growable memory which needs a copy of the data into a non-resizable buffer and can't use a reference [fastest single threaded, but only on Firefox, on Chrome this is slowest]
20
+ const SHOULD_REFERENCE_MEMORY = !IS_FIREFOX && (SUPPORTS_GROWTH || THREAD_COUNT > 1)
21
+
12
22
  const IDENTITY_MATRIX = new Float32Array([
13
23
  1, 0, 0,
14
24
  0, 1, 0,
@@ -62,9 +72,9 @@ export const colorMatrixConversionMap = {
62
72
 
63
73
  export type ColorSpace = keyof typeof colorMatrixConversionMap
64
74
 
65
- // GLSL ES 3.0 Vertex Shader
75
+ // GLSL ES 3.0 Vertex Shader with Instancing
66
76
  const VERTEX_SHADER = /* glsl */`#version 300 es
67
- precision highp float;
77
+ precision mediump float;
68
78
 
69
79
  const vec2 QUAD_POSITIONS[6] = vec2[6](
70
80
  vec2(0.0, 0.0),
@@ -76,9 +86,11 @@ const vec2 QUAD_POSITIONS[6] = vec2[6](
76
86
  );
77
87
 
78
88
  uniform vec2 u_resolution;
79
- uniform vec4 u_destRect; // x, y, w, h
80
- uniform vec4 u_color; // r, g, b, a
81
- uniform float u_texLayer;
89
+
90
+ // Instance attributes
91
+ in vec4 a_destRect; // x, y, w, h
92
+ in vec4 a_color; // r, g, b, a
93
+ in float a_texLayer;
82
94
 
83
95
  flat out vec2 v_destXY;
84
96
  flat out vec4 v_color;
@@ -87,22 +99,22 @@ flat out float v_texLayer;
87
99
 
88
100
  void main() {
89
101
  vec2 quadPos = QUAD_POSITIONS[gl_VertexID];
90
- vec2 pixelPos = u_destRect.xy + quadPos * u_destRect.zw;
102
+ vec2 pixelPos = a_destRect.xy + quadPos * a_destRect.zw;
91
103
  vec2 clipPos = (pixelPos / u_resolution) * 2.0 - 1.0;
92
104
  clipPos.y = -clipPos.y;
93
105
 
94
106
  gl_Position = vec4(clipPos, 0.0, 1.0);
95
- v_destXY = u_destRect.xy;
96
- v_color = u_color;
97
- v_texSize = u_destRect.zw;
98
- v_texLayer = u_texLayer;
107
+ v_destXY = a_destRect.xy;
108
+ v_color = a_color;
109
+ v_texSize = a_destRect.zw;
110
+ v_texLayer = a_texLayer;
99
111
  }
100
112
  `
101
113
 
102
114
  // GLSL ES 3.0 Fragment Shader - use texelFetch for pixel-perfect sampling
103
115
  const FRAGMENT_SHADER = /* glsl */`#version 300 es
104
- precision highp float;
105
- precision highp sampler2DArray;
116
+ precision mediump float;
117
+ precision mediump sampler2DArray;
106
118
 
107
119
  uniform sampler2DArray u_texArray;
108
120
  uniform mat3 u_colorMatrix;
@@ -128,8 +140,7 @@ void main() {
128
140
  // Bounds check (prevents out-of-bounds access)
129
141
  ivec2 texSizeI = ivec2(v_texSize);
130
142
  if (texCoord.x < 0 || texCoord.y < 0 || texCoord.x >= texSizeI.x || texCoord.y >= texSizeI.y) {
131
- fragColor = vec4(0.0);
132
- return;
143
+ discard;
133
144
  }
134
145
 
135
146
  // texelFetch: integer coords, no interpolation, no precision issues
@@ -152,6 +163,7 @@ void main() {
152
163
  // Texture array configuration
153
164
  const TEX_ARRAY_SIZE = 64 // Fixed layer count
154
165
  const TEX_INITIAL_SIZE = 256 // Initial width/height
166
+ const MAX_INSTANCES = 256 // Maximum instances per draw call
155
167
 
156
168
  export class WebGL2Renderer {
157
169
  gl: WebGL2RenderingContext | null = null
@@ -160,19 +172,31 @@ export class WebGL2Renderer {
160
172
 
161
173
  // Uniform locations
162
174
  u_resolution: WebGLUniformLocation | null = null
163
- u_destRect: WebGLUniformLocation | null = null
164
- u_color: WebGLUniformLocation | null = null
165
175
  u_texArray: WebGLUniformLocation | null = null
166
176
  u_colorMatrix: WebGLUniformLocation | null = null
167
- u_texLayer: WebGLUniformLocation | null = null
168
177
 
169
- // Single texture array instead of individual textures
178
+ // Instance attribute buffers
179
+ instanceDestRectBuffer: WebGLBuffer | null = null
180
+ instanceColorBuffer: WebGLBuffer | null = null
181
+ instanceTexLayerBuffer: WebGLBuffer | null = null
182
+
183
+ // Instance data arrays
184
+ instanceDestRectData: Float32Array
185
+ instanceColorData: Float32Array
186
+ instanceTexLayerData: Float32Array
187
+
170
188
  texArray: WebGLTexture | null = null
171
189
  texArrayWidth = 0
172
190
  texArrayHeight = 0
173
191
 
174
192
  colorMatrix: Float32Array = IDENTITY_MATRIX
175
193
 
194
+ constructor () {
195
+ this.instanceDestRectData = new Float32Array(MAX_INSTANCES * 4)
196
+ this.instanceColorData = new Float32Array(MAX_INSTANCES * 4)
197
+ this.instanceTexLayerData = new Float32Array(MAX_INSTANCES)
198
+ }
199
+
176
200
  setCanvas (canvas: OffscreenCanvas, width: number, height: number) {
177
201
  // WebGL2 doesn't allow 0-sized canvases
178
202
  if (width <= 0 || height <= 0) return
@@ -181,16 +205,15 @@ export class WebGL2Renderer {
181
205
  canvas.height = height
182
206
 
183
207
  if (!this.gl) {
184
- // Get canvas context
185
- // Note: preserveDrawingBuffer is false (default) - the browser handles
186
- // buffer swaps for OffscreenCanvas, avoiding flicker
187
208
  this.gl = canvas.getContext('webgl2', {
188
209
  alpha: true,
189
210
  premultipliedAlpha: true,
190
211
  antialias: false,
191
212
  depth: false,
213
+ preserveDrawingBuffer: false,
192
214
  stencil: false,
193
- desynchronized: true
215
+ desynchronized: true,
216
+ powerPreference: 'high-performance'
194
217
  })
195
218
 
196
219
  if (!this.gl) {
@@ -221,16 +244,37 @@ export class WebGL2Renderer {
221
244
 
222
245
  // Get uniform locations
223
246
  this.u_resolution = this.gl.getUniformLocation(this.program, 'u_resolution')
224
- this.u_destRect = this.gl.getUniformLocation(this.program, 'u_destRect')
225
- this.u_color = this.gl.getUniformLocation(this.program, 'u_color')
226
247
  this.u_texArray = this.gl.getUniformLocation(this.program, 'u_texArray')
227
248
  this.u_colorMatrix = this.gl.getUniformLocation(this.program, 'u_colorMatrix')
228
- this.u_texLayer = this.gl.getUniformLocation(this.program, 'u_texLayer')
249
+
250
+ // Create instance attribute buffers
251
+ this.instanceDestRectBuffer = this.gl.createBuffer()
252
+ this.instanceColorBuffer = this.gl.createBuffer()
253
+ this.instanceTexLayerBuffer = this.gl.createBuffer()
229
254
 
230
255
  // Create a VAO (required for WebGL2)
231
256
  this.vao = this.gl.createVertexArray()
232
257
  this.gl.bindVertexArray(this.vao)
233
258
 
259
+ // Setup instance attributes
260
+ const destRectLoc = this.gl.getAttribLocation(this.program, 'a_destRect')
261
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.instanceDestRectBuffer)
262
+ this.gl.enableVertexAttribArray(destRectLoc)
263
+ this.gl.vertexAttribPointer(destRectLoc, 4, this.gl.FLOAT, false, 0, 0)
264
+ this.gl.vertexAttribDivisor(destRectLoc, 1)
265
+
266
+ const colorLoc = this.gl.getAttribLocation(this.program, 'a_color')
267
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.instanceColorBuffer)
268
+ this.gl.enableVertexAttribArray(colorLoc)
269
+ this.gl.vertexAttribPointer(colorLoc, 4, this.gl.FLOAT, false, 0, 0)
270
+ this.gl.vertexAttribDivisor(colorLoc, 1)
271
+
272
+ const texLayerLoc = this.gl.getAttribLocation(this.program, 'a_texLayer')
273
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.instanceTexLayerBuffer)
274
+ this.gl.enableVertexAttribArray(texLayerLoc)
275
+ this.gl.vertexAttribPointer(texLayerLoc, 1, this.gl.FLOAT, false, 0, 0)
276
+ this.gl.vertexAttribDivisor(texLayerLoc, 1)
277
+
234
278
  // Set up blending for premultiplied alpha
235
279
  this.gl.enable(this.gl.BLEND)
236
280
  this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA)
@@ -273,10 +317,8 @@ export class WebGL2Renderer {
273
317
  return shader
274
318
  }
275
319
 
276
- /**
277
- * Set the color matrix for color space conversion.
278
- * Pass null or undefined to use identity (no conversion).
279
- */
320
+ // Set the color matrix for color space conversion.
321
+ // Pass null or undefined to use identity (no conversion).
280
322
  setColorMatrix (subtitleColorSpace?: 'BT601' | 'BT709' | 'SMPTE240M' | 'FCC', videoColorSpace?: 'BT601' | 'BT709') {
281
323
  this.colorMatrix = (subtitleColorSpace && videoColorSpace && colorMatrixConversionMap[subtitleColorSpace]?.[videoColorSpace]) ?? IDENTITY_MATRIX
282
324
  if (this.gl && this.u_colorMatrix && this.program) {
@@ -304,7 +346,7 @@ export class WebGL2Renderer {
304
346
  0,
305
347
  this.gl!.RED,
306
348
  this.gl!.UNSIGNED_BYTE,
307
- null
349
+ null // Firefox cries about uninitialized data, but is slower with zero initialized data...
308
350
  )
309
351
 
310
352
  // Set texture parameters (no filtering needed for texelFetch, but set anyway)
@@ -320,72 +362,110 @@ export class WebGL2Renderer {
320
362
  render (images: ASSImage[], heap: Uint8Array): void {
321
363
  if (!this.gl || !this.program || !this.vao || !this.texArray) return
322
364
 
323
- // Hack: work around shared memory issues, webGL doesnt support shared memory, so there are race conditions when growing memory
324
- if ((self.HEAPU8RAW.buffer !== self.WASMMEMORY.buffer) || SUPPORTS_GROWTH) {
365
+ // HACK 1 and 2 [see above for explanation]
366
+ if ((self.HEAPU8RAW.buffer !== self.WASMMEMORY.buffer) || SHOULD_REFERENCE_MEMORY) {
325
367
  heap = self.HEAPU8RAW = new Uint8Array(self.WASMMEMORY.buffer)
326
368
  }
327
369
 
328
370
  // Clear canvas
329
371
  this.gl.clear(this.gl.COLOR_BUFFER_BIT)
330
372
 
331
- // Find max dimensions needed
373
+ // Find max dimensions needed and filter valid images
332
374
  let maxW = this.texArrayWidth
333
375
  let maxH = this.texArrayHeight
376
+ const validImages: ASSImage[] = []
377
+
334
378
  for (const img of images) {
379
+ if (img.w <= 0 || img.h <= 0) continue
380
+ validImages.push(img)
335
381
  if (img.w > maxW) maxW = img.w
336
382
  if (img.h > maxH) maxH = img.h
337
383
  }
338
384
 
385
+ if (validImages.length === 0) return
386
+
339
387
  // Resize texture array if needed
340
388
  if (maxW > this.texArrayWidth || maxH > this.texArrayHeight) {
341
389
  this.createTexArray(maxW, maxH)
342
- this.gl.bindTexture(this.gl.TEXTURE_2D_ARRAY, this.texArray)
343
390
  }
344
391
 
345
- // Render each image
346
- for (let i = 0, texLayer = 0; i < images.length; i++) {
347
- const img = images[i]!
392
+ // Process images in chunks that fit within texture array size
393
+ const batchSize = Math.min(TEX_ARRAY_SIZE, MAX_INSTANCES)
394
+
395
+ for (let batchStart = 0; batchStart < validImages.length; batchStart += batchSize) {
396
+ const batchEnd = Math.min(batchStart + batchSize, validImages.length)
397
+ let instanceCount = 0
398
+
399
+ // Upload textures for this batch
400
+ for (let i = batchStart; i < batchEnd; i++) {
401
+ const img = validImages[i]!
402
+ const layer = instanceCount
403
+
404
+ // Upload bitmap data to texture array layer
405
+ this.gl.pixelStorei(this.gl.UNPACK_ROW_LENGTH, img.stride)
406
+
407
+ if (IS_FIREFOX) {
408
+ // HACK 3 [see above for explanation]
409
+ const sourceView = new Uint8Array(heap.buffer, img.bitmap, img.stride * img.h)
410
+ const bitmapData = new Uint8Array(sourceView)
411
+
412
+ this.gl.texSubImage3D(
413
+ this.gl.TEXTURE_2D_ARRAY,
414
+ 0,
415
+ 0, 0, layer, // x, y, z offset
416
+ img.w,
417
+ img.h,
418
+ 1, // depth (1 layer)
419
+ this.gl.RED,
420
+ this.gl.UNSIGNED_BYTE,
421
+ bitmapData
422
+ )
423
+ } else {
424
+ this.gl.texSubImage3D(
425
+ this.gl.TEXTURE_2D_ARRAY,
426
+ 0,
427
+ 0, 0, layer, // x, y, z offset
428
+ img.w,
429
+ img.h,
430
+ 1, // depth (1 layer)
431
+ this.gl.RED,
432
+ this.gl.UNSIGNED_BYTE,
433
+ heap,
434
+ img.bitmap
435
+ )
436
+ }
437
+ // Fill instance data
438
+ const idx = instanceCount * 4
439
+ this.instanceDestRectData[idx] = img.dst_x
440
+ this.instanceDestRectData[idx + 1] = img.dst_y
441
+ this.instanceDestRectData[idx + 2] = img.w
442
+ this.instanceDestRectData[idx + 3] = img.h
443
+
444
+ this.instanceColorData[idx] = ((img.color >>> 24) & 0xFF) / 255
445
+ this.instanceColorData[idx + 1] = ((img.color >>> 16) & 0xFF) / 255
446
+ this.instanceColorData[idx + 2] = ((img.color >>> 8) & 0xFF) / 255
447
+ this.instanceColorData[idx + 3] = (img.color & 0xFF) / 255
448
+
449
+ this.instanceTexLayerData[instanceCount] = layer
450
+
451
+ instanceCount++
452
+ }
348
453
 
349
- // Skip images with invalid dimensions
350
- if (img.w <= 0 || img.h <= 0) continue
454
+ this.gl.pixelStorei(this.gl.UNPACK_ROW_LENGTH, 0)
351
455
 
352
- // Use modulo to cycle through layers if we have more images than layers
353
- const layer = texLayer % TEX_ARRAY_SIZE
354
- texLayer++
355
-
356
- // Upload bitmap data to texture array layer
357
- this.gl.pixelStorei(this.gl.UNPACK_ROW_LENGTH, img.stride)
358
-
359
- this.gl.texSubImage3D(
360
- this.gl.TEXTURE_2D_ARRAY,
361
- 0,
362
- 0, 0, layer, // x, y, z offset
363
- img.w,
364
- img.h,
365
- 1, // depth (1 layer)
366
- this.gl.RED,
367
- this.gl.UNSIGNED_BYTE,
368
- heap,
369
- img.bitmap
370
- )
456
+ if (instanceCount === 0) continue
457
+ // Upload instance data to buffers
458
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.instanceDestRectBuffer)
459
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, this.instanceDestRectData.subarray(0, instanceCount * 4), this.gl.DYNAMIC_DRAW)
371
460
 
372
- this.gl.pixelStorei(this.gl.UNPACK_ROW_LENGTH, 0)
461
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.instanceColorBuffer)
462
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, this.instanceColorData.subarray(0, instanceCount * 4), this.gl.DYNAMIC_DRAW)
463
+
464
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.instanceTexLayerBuffer)
465
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, this.instanceTexLayerData.subarray(0, instanceCount), this.gl.DYNAMIC_DRAW)
373
466
 
374
- // Set uniforms
375
- this.gl.uniform4f(this.u_destRect, img.dst_x, img.dst_y, img.w, img.h)
376
- this.gl.uniform1f(this.u_texLayer, layer)
377
-
378
- // color (RGBA from 0xRRGGBBAA)
379
- this.gl.uniform4f(
380
- this.u_color,
381
- ((img.color >>> 24) & 0xFF) / 255,
382
- ((img.color >>> 16) & 0xFF) / 255,
383
- ((img.color >>> 8) & 0xFF) / 255,
384
- (img.color & 0xFF) / 255
385
- )
386
-
387
- // 6 vertices for quad
388
- this.gl.drawArrays(this.gl.TRIANGLES, 0, 6)
467
+ // Single instanced draw call
468
+ this.gl.drawArraysInstanced(this.gl.TRIANGLES, 0, 6, instanceCount)
389
469
  }
390
470
  }
391
471
 
@@ -396,6 +476,21 @@ export class WebGL2Renderer {
396
476
  this.texArray = null
397
477
  }
398
478
 
479
+ if (this.instanceDestRectBuffer) {
480
+ this.gl.deleteBuffer(this.instanceDestRectBuffer)
481
+ this.instanceDestRectBuffer = null
482
+ }
483
+
484
+ if (this.instanceColorBuffer) {
485
+ this.gl.deleteBuffer(this.instanceColorBuffer)
486
+ this.instanceColorBuffer = null
487
+ }
488
+
489
+ if (this.instanceTexLayerBuffer) {
490
+ this.gl.deleteBuffer(this.instanceTexLayerBuffer)
491
+ this.instanceTexLayerBuffer = null
492
+ }
493
+
399
494
  if (this.vao) {
400
495
  this.gl.deleteVertexArray(this.vao)
401
496
  this.vao = null
@@ -11,6 +11,8 @@ import { WebGL2Renderer } from './webgl-renderer'
11
11
  import type { ASSEvent, ASSImage, ASSStyle } from '../jassub'
12
12
  import type { JASSUB, MainModule } from '../wasm/types.js'
13
13
 
14
+ const IS_FIREFOX = navigator.userAgent.toLowerCase().includes('firefox')
15
+
14
16
  declare const self: DedicatedWorkerGlobalScope &
15
17
  typeof globalThis & {
16
18
  HEAPU8RAW: Uint8Array<ArrayBuffer>
@@ -80,7 +82,9 @@ export class ASSRenderer {
80
82
 
81
83
  const fallbackFont = data.fallbackFont.toLowerCase()
82
84
  this._wasm = new Module.JASSUB(data.width, data.height, fallbackFont)
83
- this._wasm.setThreads(self.crossOriginIsolated ? Math.min(Math.max(1, navigator.hardwareConcurrency - 2), 8) : 1)
85
+ // Firefox seems to have issues with multithreading in workers
86
+ // a worker inside a worker does not recive messages properly
87
+ this._wasm.setThreads(!IS_FIREFOX && self.crossOriginIsolated ? Math.min(Math.max(1, navigator.hardwareConcurrency - 2), 8) : 1)
84
88
 
85
89
  if (fallbackFont) this._findAvailableFonts(fallbackFont)
86
90