jassub 2.0.19 → 2.1.0

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.
@@ -177,118 +177,100 @@ export class WebGPURenderer {
177
177
 
178
178
  // eslint-disable-next-line no-undef
179
179
  format: GPUTextureFormat = 'bgra8unorm'
180
- _ready
181
-
182
- constructor () {
183
- // Start async initialization immediately
184
- this._ready = (async () => {
185
- // Check WebGPU support
186
- if (!navigator.gpu) {
187
- throw new Error('WebGPU not supported')
188
- }
189
180
 
190
- const adapter = await navigator.gpu.requestAdapter({
191
- powerPreference: 'high-performance'
192
- })
181
+ constructor (device: GPUDevice) {
182
+ this.device = device
183
+ this.format = navigator.gpu.getPreferredCanvasFormat()
193
184
 
194
- if (!adapter) {
195
- throw new Error('No WebGPU adapter found')
196
- }
185
+ // Create shader modules
186
+ const vertexModule = this.device.createShaderModule({
187
+ code: VERTEX_SHADER
188
+ })
197
189
 
198
- this.device = await adapter.requestDevice()
199
- this.format = navigator.gpu.getPreferredCanvasFormat()
190
+ const fragmentModule = this.device.createShaderModule({
191
+ code: FRAGMENT_SHADER
192
+ })
200
193
 
201
- // Create shader modules
202
- const vertexModule = this.device.createShaderModule({
203
- code: VERTEX_SHADER
204
- })
194
+ // Create uniform buffer
195
+ this.uniformBuffer = this.device.createBuffer({
196
+ size: 16, // vec2f resolution + padding
197
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
198
+ })
205
199
 
206
- const fragmentModule = this.device.createShaderModule({
207
- code: FRAGMENT_SHADER
208
- })
200
+ // Create color matrix buffer (mat3x3f requires 48 bytes: 3 vec3f padded to vec4f each)
201
+ this.colorMatrixBuffer = this.device.createBuffer({
202
+ size: 48, // 3 x vec4f (each column is vec3f padded to 16 bytes)
203
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
204
+ })
205
+ // Initialize with identity matrix
206
+ this.device.queue.writeBuffer(this.colorMatrixBuffer, 0, IDENTITY_MATRIX)
209
207
 
210
- // Create uniform buffer
211
- this.uniformBuffer = this.device.createBuffer({
212
- size: 16, // vec2f resolution + padding
213
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
214
- })
208
+ // Create bind group layout (no sampler needed - using textureLoad for pixel-perfect sampling)
209
+ this.bindGroupLayout = this.device.createBindGroupLayout({
210
+ entries: [
211
+ {
212
+ binding: 0,
213
+ visibility: GPUShaderStage.VERTEX,
214
+ buffer: { type: 'uniform' }
215
+ },
216
+ {
217
+ binding: 1,
218
+ visibility: GPUShaderStage.VERTEX,
219
+ buffer: { type: 'read-only-storage' }
220
+ },
221
+ {
222
+ binding: 3,
223
+ visibility: GPUShaderStage.FRAGMENT,
224
+ texture: { sampleType: 'unfilterable-float' } // textureLoad requires unfilterable
225
+ },
226
+ {
227
+ binding: 4,
228
+ visibility: GPUShaderStage.FRAGMENT,
229
+ buffer: { type: 'uniform' }
230
+ }
231
+ ]
232
+ })
215
233
 
216
- // Create color matrix buffer (mat3x3f requires 48 bytes: 3 vec3f padded to vec4f each)
217
- this.colorMatrixBuffer = this.device.createBuffer({
218
- size: 48, // 3 x vec4f (each column is vec3f padded to 16 bytes)
219
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
220
- })
221
- // Initialize with identity matrix
222
- this.device.queue.writeBuffer(this.colorMatrixBuffer, 0, IDENTITY_MATRIX)
234
+ // Create pipeline layout
235
+ const pipelineLayout = this.device.createPipelineLayout({
236
+ bindGroupLayouts: [this.bindGroupLayout]
237
+ })
223
238
 
224
- // Create bind group layout (no sampler needed - using textureLoad for pixel-perfect sampling)
225
- this.bindGroupLayout = this.device.createBindGroupLayout({
226
- entries: [
227
- {
228
- binding: 0,
229
- visibility: GPUShaderStage.VERTEX,
230
- buffer: { type: 'uniform' }
231
- },
232
- {
233
- binding: 1,
234
- visibility: GPUShaderStage.VERTEX,
235
- buffer: { type: 'read-only-storage' }
236
- },
239
+ // Create render pipeline
240
+ this.pipeline = this.device.createRenderPipeline({
241
+ layout: pipelineLayout,
242
+ vertex: {
243
+ module: vertexModule,
244
+ entryPoint: 'vertexMain'
245
+ },
246
+ fragment: {
247
+ module: fragmentModule,
248
+ entryPoint: 'fragmentMain',
249
+ targets: [
237
250
  {
238
- binding: 3,
239
- visibility: GPUShaderStage.FRAGMENT,
240
- texture: { sampleType: 'unfilterable-float' } // textureLoad requires unfilterable
241
- },
242
- {
243
- binding: 4,
244
- visibility: GPUShaderStage.FRAGMENT,
245
- buffer: { type: 'uniform' }
246
- }
247
- ]
248
- })
249
-
250
- // Create pipeline layout
251
- const pipelineLayout = this.device.createPipelineLayout({
252
- bindGroupLayouts: [this.bindGroupLayout]
253
- })
254
-
255
- // Create render pipeline
256
- this.pipeline = this.device.createRenderPipeline({
257
- layout: pipelineLayout,
258
- vertex: {
259
- module: vertexModule,
260
- entryPoint: 'vertexMain'
261
- },
262
- fragment: {
263
- module: fragmentModule,
264
- entryPoint: 'fragmentMain',
265
- targets: [
266
- {
267
- format: this.format,
268
- blend: {
269
- color: {
270
- srcFactor: 'one',
271
- dstFactor: 'one-minus-src-alpha',
272
- operation: 'add'
273
- },
274
- alpha: {
275
- srcFactor: 'one',
276
- dstFactor: 'one-minus-src-alpha',
277
- operation: 'add'
278
- }
251
+ format: this.format,
252
+ blend: {
253
+ color: {
254
+ srcFactor: 'one',
255
+ dstFactor: 'one-minus-src-alpha',
256
+ operation: 'add'
257
+ },
258
+ alpha: {
259
+ srcFactor: 'one',
260
+ dstFactor: 'one-minus-src-alpha',
261
+ operation: 'add'
279
262
  }
280
263
  }
281
- ]
282
- },
283
- primitive: {
284
- topology: 'triangle-list'
285
- }
286
- })
287
- })()
264
+ }
265
+ ]
266
+ },
267
+ primitive: {
268
+ topology: 'triangle-list'
269
+ }
270
+ })
288
271
  }
289
272
 
290
- async setCanvas (canvas: OffscreenCanvas, width: number, height: number) {
291
- await this._ready
273
+ setCanvas (canvas: OffscreenCanvas, width: number, height: number) {
292
274
  if (!this.device) return
293
275
 
294
276
  // WebGPU doesn't allow 0-sized textures/swapchains
@@ -324,13 +306,13 @@ export class WebGPURenderer {
324
306
  * Pass null or undefined to use identity (no conversion).
325
307
  * Matrix should be a pre-padded Float32Array with 12 values (3 columns × 4 floats each).
326
308
  */
327
- async setColorMatrix (matrix?: Float32Array<ArrayBuffer>) {
328
- await this._ready
309
+ setColorMatrix (subtitleColorSpace?: 'BT601' | 'BT709' | 'SMPTE240M' | 'FCC', videoColorSpace?: 'BT601' | 'BT709') {
329
310
  if (!this.device) return
330
- this.device.queue.writeBuffer(this.colorMatrixBuffer!, 0, matrix ?? IDENTITY_MATRIX)
311
+ const colorMatrix = (subtitleColorSpace && videoColorSpace && colorMatrixConversionMap[subtitleColorSpace]?.[videoColorSpace]) ?? IDENTITY_MATRIX
312
+ this.device.queue.writeBuffer(this.colorMatrixBuffer!, 0, colorMatrix)
331
313
  }
332
314
 
333
- private createTextureInfo (width: number, height: number): TextureInfo {
315
+ createTextureInfo (width: number, height: number): TextureInfo {
334
316
  const texture = this.device!.createTexture({
335
317
  size: [width, height],
336
318
  format: 'r8unorm',
@@ -5,7 +5,8 @@ import { expose } from 'abslink/w3c'
5
5
  import WASM from '../wasm/jassub-worker.js'
6
6
 
7
7
  import { libassYCbCrMap, read_, readAsync, _applyKeys } from './util'
8
- import { colorMatrixConversionMap, WebGPURenderer } from './webgpu-renderer'
8
+ import { WebGL2Renderer } from './webgl-renderer'
9
+ import { WebGPURenderer } from './webgpu-renderer'
9
10
 
10
11
  import type { ASSEvent, ASSImage, ASSStyle } from '../jassub'
11
12
  import type { JASSUB, MainModule } from '../wasm/types.js'
@@ -13,6 +14,7 @@ import type { JASSUB, MainModule } from '../wasm/types.js'
13
14
  declare const self: DedicatedWorkerGlobalScope &
14
15
  typeof globalThis & {
15
16
  HEAPU8RAW: Uint8Array<ArrayBuffer>
17
+ WASMMEMORY: WebAssembly.Memory
16
18
  }
17
19
 
18
20
  interface opts {
@@ -36,7 +38,7 @@ export class ASSRenderer {
36
38
  _subtitleColorSpace?: 'BT601' | 'BT709' | 'SMPTE240M' | 'FCC' | null
37
39
  _videoColorSpace?: 'BT709' | 'BT601'
38
40
  _malloc!: (size: number) => number
39
- _gpurender = new WebGPURenderer()
41
+ _gpurender?: WebGL2Renderer | WebGPURenderer
40
42
 
41
43
  debug = false
42
44
  useLocalFonts = false
@@ -58,16 +60,21 @@ export class ASSRenderer {
58
60
  globalThis.fetch = _ => _fetch(data.wasmUrl)
59
61
 
60
62
  // TODO: abslink doesnt support transferables yet
61
- const handleMessage = ({ data }: MessageEvent) => {
63
+ const handleMessage = async ({ data }: MessageEvent) => {
62
64
  if (data.name === 'offscreenCanvas') {
65
+ await this._ready
63
66
  this._offCanvas = data.ctrl
64
- this._gpurender.setCanvas(this._offCanvas!, this._offCanvas!.width, this._offCanvas!.height)
67
+ this._gpurender!.setCanvas(this._offCanvas!, this._offCanvas!.width, this._offCanvas!.height)
65
68
  removeEventListener('message', handleMessage)
66
69
  }
67
70
  }
68
71
  addEventListener('message', handleMessage)
69
72
 
70
- this._ready = (WASM({ __url: data.wasmUrl }) as Promise<MainModule>).then(Module => {
73
+ const devicePromise = navigator.gpu?.requestAdapter({
74
+ powerPreference: 'high-performance'
75
+ }).then(adapter => adapter?.requestDevice())
76
+
77
+ this._ready = (WASM({ __url: data.wasmUrl }) as Promise<MainModule>).then(async Module => {
71
78
  // eslint-disable-next-line @typescript-eslint/unbound-method
72
79
  this._malloc = Module._malloc
73
80
 
@@ -89,6 +96,9 @@ export class ASSRenderer {
89
96
  if (data.libassMemoryLimit > 0 || data.libassGlyphLimit > 0) {
90
97
  this._wasm.setMemoryLimits(data.libassGlyphLimit || 0, data.libassMemoryLimit || 0)
91
98
  }
99
+ const device = await devicePromise
100
+ this._gpurender = device ? new WebGPURenderer(device) : new WebGL2Renderer()
101
+ if (this._offCanvas) this._gpurender.setCanvas(this._offCanvas, this._offCanvas.width, this._offCanvas.height)
92
102
  this._checkColorSpace()
93
103
  })
94
104
  }
@@ -176,7 +186,7 @@ export class ASSRenderer {
176
186
 
177
187
  _checkColorSpace () {
178
188
  if (!this._subtitleColorSpace || !this._videoColorSpace) return
179
- this._gpurender.setColorMatrix(colorMatrixConversionMap[this._subtitleColorSpace][this._videoColorSpace])
189
+ this._gpurender!.setColorMatrix(this._subtitleColorSpace, this._videoColorSpace)
180
190
  }
181
191
 
182
192
  _findAvailableFonts (font: string) {
@@ -228,14 +238,15 @@ export class ASSRenderer {
228
238
  }
229
239
 
230
240
  _canvas (width: number, height: number, videoWidth: number, videoHeight: number) {
231
- if (this._offCanvas) this._gpurender.setCanvas(this._offCanvas, width, height)
241
+ if (this._offCanvas && this._gpurender) this._gpurender.setCanvas(this._offCanvas, width, height)
232
242
 
233
243
  this._wasm.resizeCanvas(width, height, videoWidth, videoHeight)
234
244
  }
235
245
 
236
- [finalizer] () {
246
+ async [finalizer] () {
247
+ await this._ready
237
248
  this._wasm.quitLibrary()
238
- this._gpurender.destroy()
249
+ this._gpurender!.destroy()
239
250
  // @ts-expect-error force GC
240
251
  this._wasm = null
241
252
  // @ts-expect-error force GC
@@ -244,7 +255,7 @@ export class ASSRenderer {
244
255
  }
245
256
 
246
257
  _draw (time: number, force = false) {
247
- if (!this._offCanvas) return
258
+ if (!this._offCanvas || !this._gpurender) return
248
259
 
249
260
  const result: ASSImage = this._wasm.rawRender(time, Number(force))!
250
261
  if (this._wasm.changed === 0 && !force) return