akarisub 0.2.2 → 1.0.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.
Files changed (76) hide show
  1. package/README.md +198 -167
  2. package/THIRD_PARTY_NOTICES.md +55 -163
  3. package/dist/COPYRIGHT +56 -164
  4. package/dist/index.d.ts +12 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +11 -147
  7. package/dist/index.js.map +1 -0
  8. package/dist/ts/{ts/akarisub.d.ts → akarisub.d.ts} +80 -99
  9. package/dist/ts/akarisub.d.ts.map +1 -0
  10. package/dist/ts/akarisub.js +2086 -0
  11. package/dist/ts/akarisub.js.map +1 -0
  12. package/dist/ts/timing.d.ts +63 -0
  13. package/dist/ts/timing.d.ts.map +1 -0
  14. package/dist/ts/timing.js +219 -0
  15. package/dist/ts/timing.js.map +1 -0
  16. package/dist/ts/{ts/types.d.ts → types.d.ts} +103 -78
  17. package/dist/ts/types.d.ts.map +1 -0
  18. package/dist/ts/types.js +2 -0
  19. package/dist/ts/{ts/types.js.map → types.js.map} +1 -1
  20. package/dist/ts/{ts/utils.d.ts → utils.d.ts} +0 -31
  21. package/dist/ts/utils.d.ts.map +1 -0
  22. package/dist/ts/{ts/utils.js → utils.js} +0 -56
  23. package/dist/ts/utils.js.map +1 -0
  24. package/dist/ts/wasm.d.ts +12 -0
  25. package/dist/ts/wasm.d.ts.map +1 -0
  26. package/dist/ts/wasm.js +46 -0
  27. package/dist/ts/wasm.js.map +1 -0
  28. package/dist/ts/{ts/webgl2-renderer.d.ts → webgl2-renderer.d.ts} +0 -12
  29. package/dist/ts/webgl2-renderer.d.ts.map +1 -0
  30. package/dist/ts/{ts/webgl2-renderer.js → webgl2-renderer.js} +0 -27
  31. package/dist/ts/webgl2-renderer.js.map +1 -0
  32. package/dist/ts/{ts/webgpu-renderer.d.ts → webgpu-renderer.d.ts} +17 -16
  33. package/dist/ts/webgpu-renderer.d.ts.map +1 -0
  34. package/dist/ts/{ts/webgpu-renderer.js → webgpu-renderer.js} +148 -71
  35. package/dist/ts/webgpu-renderer.js.map +1 -0
  36. package/dist/ts/worker.d.ts +2 -0
  37. package/dist/ts/{ts/worker.d.ts.map → worker.d.ts.map} +1 -1
  38. package/dist/ts/{ts/worker.js → worker.js} +1064 -264
  39. package/dist/ts/worker.js.map +1 -0
  40. package/dist/wrapper.d.ts +8 -0
  41. package/dist/wrapper.d.ts.map +1 -0
  42. package/dist/{ts/index.js → wrapper.js} +4 -14
  43. package/dist/wrapper.js.map +1 -0
  44. package/package.json +24 -22
  45. package/pkg/akarisub.js +53 -0
  46. package/{dist/akarisub-worker.wasm → pkg/akarisub.wasm} +0 -0
  47. package/src/wrapper.ts +46 -0
  48. package/dist/akarisub-worker.js +0 -39
  49. package/dist/akarisub.umd.js +0 -159
  50. package/dist/ts/index.d.ts +0 -14
  51. package/dist/ts/index.d.ts.map +0 -1
  52. package/dist/ts/index.js.map +0 -1
  53. package/dist/ts/ts/akarisub.d.ts.map +0 -1
  54. package/dist/ts/ts/akarisub.js +0 -1073
  55. package/dist/ts/ts/akarisub.js.map +0 -1
  56. package/dist/ts/ts/types.d.ts.map +0 -1
  57. package/dist/ts/ts/types.js +0 -5
  58. package/dist/ts/ts/utils.d.ts.map +0 -1
  59. package/dist/ts/ts/utils.js.map +0 -1
  60. package/dist/ts/ts/webgl2-renderer.d.ts.map +0 -1
  61. package/dist/ts/ts/webgl2-renderer.js.map +0 -1
  62. package/dist/ts/ts/webgpu-renderer.d.ts.map +0 -1
  63. package/dist/ts/ts/webgpu-renderer.js.map +0 -1
  64. package/dist/ts/ts/worker.d.ts +0 -6
  65. package/dist/ts/ts/worker.js.map +0 -1
  66. package/dist/ts/wrapper.d.ts +0 -8
  67. package/dist/ts/wrapper.d.ts.map +0 -1
  68. package/dist/ts/wrapper.js +0 -9
  69. package/dist/ts/wrapper.js.map +0 -1
  70. package/src/ts/akarisub.ts +0 -1272
  71. package/src/ts/types.ts +0 -397
  72. package/src/ts/utils.ts +0 -512
  73. package/src/ts/webgl2-renderer.ts +0 -426
  74. package/src/ts/webgpu-renderer.ts +0 -691
  75. package/src/ts/worker.ts +0 -2052
  76. /package/{dist → assets}/default.woff2 +0 -0
@@ -1,691 +0,0 @@
1
- /// <reference types="@webgpu/types" />
2
-
3
- import type { RenderImage } from './types'
4
-
5
- // Maximum images per batch
6
- const MAX_IMAGES_PER_BATCH = 256
7
-
8
- // WebGPU max texture array layers
9
- const MAX_TEXTURE_ARRAY_LAYERS = 256
10
-
11
- // WGSL Vertex Shader
12
- const VERTEX_SHADER = /* wgsl */ `
13
- struct VertexOutput {
14
- @builtin(position) position: vec4f,
15
- @location(0) @interpolate(flat) instanceIndex: u32,
16
- @location(1) @interpolate(flat) destXY: vec2f,
17
- @location(2) @interpolate(flat) texSize: vec2f,
18
- }
19
-
20
- struct Uniforms {
21
- resolution: vec2f,
22
- }
23
-
24
- struct ImageData {
25
- destRect: vec4f, // x, y, w, h
26
- texInfo: vec4f, // texW, texH, texIndex, 0
27
- }
28
-
29
- @group(0) @binding(0) var<uniform> uniforms: Uniforms;
30
- @group(0) @binding(1) var<storage, read> imageData: array<ImageData>;
31
-
32
- // Quad vertices (two triangles)
33
- const QUAD_POSITIONS = array<vec2f, 6>(
34
- vec2f(0.0, 0.0),
35
- vec2f(1.0, 0.0),
36
- vec2f(0.0, 1.0),
37
- vec2f(1.0, 0.0),
38
- vec2f(1.0, 1.0),
39
- vec2f(0.0, 1.0)
40
- );
41
-
42
- @vertex
43
- fn vertexMain(
44
- @builtin(vertex_index) vertexIndex: u32,
45
- @builtin(instance_index) instanceIndex: u32
46
- ) -> VertexOutput {
47
- var output: VertexOutput;
48
-
49
- let data = imageData[instanceIndex];
50
- let quadPos = QUAD_POSITIONS[vertexIndex];
51
- let wh = data.destRect.zw;
52
-
53
- // Calculate pixel position
54
- let pixelPos = data.destRect.xy + quadPos * wh;
55
-
56
- // Convert to clip space (-1 to 1)
57
- var clipPos = (pixelPos / uniforms.resolution) * 2.0 - 1.0;
58
- clipPos.y = -clipPos.y;
59
-
60
- output.position = vec4f(clipPos, 0.0, 1.0);
61
- output.instanceIndex = instanceIndex;
62
- output.destXY = data.destRect.xy;
63
- output.texSize = data.texInfo.xy;
64
-
65
- return output;
66
- }
67
- `
68
-
69
- // WGSL Fragment Shader
70
- const FRAGMENT_SHADER = /* wgsl */ `
71
- @group(0) @binding(2) var texArray: texture_2d_array<f32>;
72
-
73
- struct ImageData {
74
- destRect: vec4f,
75
- texInfo: vec4f,
76
- }
77
-
78
- @group(0) @binding(1) var<storage, read> imageData: array<ImageData>;
79
-
80
- struct FragmentInput {
81
- @builtin(position) fragCoord: vec4f,
82
- @location(0) @interpolate(flat) instanceIndex: u32,
83
- @location(1) @interpolate(flat) destXY: vec2f,
84
- @location(2) @interpolate(flat) texSize: vec2f,
85
- }
86
-
87
- @fragment
88
- fn fragmentMain(input: FragmentInput) -> @location(0) vec4f {
89
- let data = imageData[input.instanceIndex];
90
- let texIndex = u32(data.texInfo.z);
91
-
92
- // Calculate texel coordinates
93
- let texCoordF = floor(input.fragCoord.xy - input.destXY);
94
- let texCoord = vec2i(texCoordF);
95
-
96
- // Bounds check
97
- let texSizeI = vec2i(input.texSize);
98
- if (texCoord.x < 0 || texCoord.y < 0 || texCoord.x >= texSizeI.x || texCoord.y >= texSizeI.y) {
99
- discard;
100
- }
101
-
102
- // Load from texture array
103
- let color = textureLoad(texArray, texCoord, texIndex, 0);
104
-
105
- // Premultiplied alpha output
106
- return vec4f(color.rgb * color.a, color.a);
107
- }
108
- `
109
-
110
- /**
111
- * Check if WebGPU is supported in the current browser.
112
- */
113
- export function isWebGPUSupported(): boolean {
114
- return typeof navigator !== 'undefined' && 'gpu' in navigator
115
- }
116
-
117
- function toUint8View(data: ArrayBuffer | Uint8Array | Uint8ClampedArray): Uint8Array {
118
- if (data instanceof ArrayBuffer) {
119
- return new Uint8Array(data)
120
- }
121
-
122
- return new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
123
- }
124
-
125
- /**
126
- * High-performance WebGPU subtitle renderer for AkariSub.
127
- */
128
- export class WebGPURenderer {
129
- private device: GPUDevice | null = null
130
- private context: GPUCanvasContext | null = null
131
- private pipeline: GPURenderPipeline | null = null
132
- private bindGroupLayout: GPUBindGroupLayout | null = null
133
-
134
- private uniformBuffer: GPUBuffer | null = null
135
- private imageDataBuffer: GPUBuffer | null = null
136
-
137
- // Texture array for batched rendering
138
- private textureArray: GPUTexture | null = null
139
- private textureArrayView: GPUTextureView | null = null
140
- private textureArraySize = 0
141
- private textureArrayWidth = 0
142
- private textureArrayHeight = 0
143
-
144
- private pendingDestroyTextures: GPUTexture[] = []
145
-
146
- // Pre-allocated typed arrays (reused every frame - ZERO allocations in hot path)
147
- private readonly imageDataArray: Float32Array
148
- private readonly resolutionArray = new Float32Array(2)
149
-
150
- // Bind group (recreated only when texture array changes)
151
- private bindGroup: GPUBindGroup | null = null
152
- private bindGroupDirty = true
153
-
154
- // Track canvas size to avoid redundant updates
155
- private lastCanvasWidth = 0
156
- private lastCanvasHeight = 0
157
-
158
- format: GPUTextureFormat = 'bgra8unorm'
159
-
160
- private _canvas: HTMLCanvasElement | null = null
161
- private _initPromise: Promise<void> | null = null
162
- private _initialized = false
163
-
164
- constructor() {
165
- // Pre-allocate buffer for max images (8 floats per image: destRect + texInfo)
166
- this.imageDataArray = new Float32Array(MAX_IMAGES_PER_BATCH * 8)
167
- }
168
-
169
- async init(): Promise<void> {
170
- if (this._initPromise) return this._initPromise
171
- this._initPromise = this._initDevice()
172
- return this._initPromise
173
- }
174
-
175
- private async _initDevice(): Promise<void> {
176
- if (!navigator.gpu) {
177
- throw new Error('WebGPU not supported')
178
- }
179
-
180
- const adapter = await navigator.gpu.requestAdapter({
181
- powerPreference: 'high-performance'
182
- })
183
-
184
- if (!adapter) {
185
- throw new Error('No WebGPU adapter found')
186
- }
187
-
188
- this.device = await adapter.requestDevice()
189
- this.format = navigator.gpu.getPreferredCanvasFormat()
190
-
191
- const vertexModule = this.device.createShaderModule({ code: VERTEX_SHADER })
192
- const fragmentModule = this.device.createShaderModule({ code: FRAGMENT_SHADER })
193
-
194
- this.uniformBuffer = this.device.createBuffer({
195
- size: 16,
196
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
197
- })
198
-
199
- // Large storage buffer for all image data
200
- this.imageDataBuffer = this.device.createBuffer({
201
- size: MAX_IMAGES_PER_BATCH * 8 * 4,
202
- usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
203
- })
204
-
205
- // Create initial texture array with reasonable defaults
206
- this.createTextureArray(256, 256, 32)
207
-
208
- this.bindGroupLayout = this.device.createBindGroupLayout({
209
- entries: [
210
- { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
211
- {
212
- binding: 1,
213
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
214
- buffer: { type: 'read-only-storage' }
215
- },
216
- {
217
- binding: 2,
218
- visibility: GPUShaderStage.FRAGMENT,
219
- texture: { sampleType: 'unfilterable-float', viewDimension: '2d-array' }
220
- }
221
- ]
222
- })
223
-
224
- const pipelineLayout = this.device.createPipelineLayout({
225
- bindGroupLayouts: [this.bindGroupLayout]
226
- })
227
-
228
- this.pipeline = this.device.createRenderPipeline({
229
- layout: pipelineLayout,
230
- vertex: { module: vertexModule, entryPoint: 'vertexMain' },
231
- fragment: {
232
- module: fragmentModule,
233
- entryPoint: 'fragmentMain',
234
- targets: [
235
- {
236
- format: this.format,
237
- blend: {
238
- color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
239
- alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' }
240
- }
241
- }
242
- ]
243
- },
244
- primitive: { topology: 'triangle-list' }
245
- })
246
-
247
- this._initialized = true
248
- }
249
-
250
- // Round up to a multiple of 64: gives headroom against size jitter without
251
- // power-of-2 over-allocation (a 1920x1080 blend region would otherwise
252
- // round to 2048x2048 and waste ~2x memory).
253
- private roundDim(n: number): number {
254
- return (Math.max(n, 64) + 63) & ~63
255
- }
256
-
257
- // Round layers up to a multiple of 8, clamped to the WebGPU max
258
- private roundLayers(n: number): number {
259
- return Math.min((Math.max(n, 8) + 7) & ~7, MAX_TEXTURE_ARRAY_LAYERS)
260
- }
261
-
262
- private createTextureArray(width: number, height: number, layers: number): void {
263
- if (this.textureArray) {
264
- this.pendingDestroyTextures.push(this.textureArray)
265
- }
266
-
267
- const w = this.roundDim(width)
268
- const h = this.roundDim(height)
269
- const l = this.roundLayers(layers)
270
-
271
- this.textureArray = this.device!.createTexture({
272
- size: [w, h, l],
273
- format: 'rgba8unorm',
274
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
275
- })
276
- this.textureArrayView = this.textureArray.createView({ dimension: '2d-array' })
277
- this.textureArrayWidth = w
278
- this.textureArrayHeight = h
279
- this.textureArraySize = l
280
- this.bindGroupDirty = true
281
-
282
- // Clear all texture layers to transparent to prevent garbage border artifacts
283
- const commandEncoder = this.device!.createCommandEncoder()
284
- for (let layer = 0; layer < l; layer++) {
285
- const layerView = this.textureArray.createView({
286
- dimension: '2d',
287
- baseArrayLayer: layer,
288
- arrayLayerCount: 1
289
- })
290
- const renderPass = commandEncoder.beginRenderPass({
291
- colorAttachments: [
292
- {
293
- view: layerView,
294
- clearValue: { r: 0, g: 0, b: 0, a: 0 },
295
- loadOp: 'clear',
296
- storeOp: 'store'
297
- }
298
- ]
299
- })
300
- renderPass.end()
301
- }
302
- this.device!.queue.submit([commandEncoder.finish()])
303
- }
304
-
305
- private ensureTextureArray(maxWidth: number, maxHeight: number, count: number): boolean {
306
- // Clamp count to max layers
307
- const clampedCount = Math.min(count, MAX_TEXTURE_ARRAY_LAYERS)
308
-
309
- if (
310
- maxWidth <= this.textureArrayWidth &&
311
- maxHeight <= this.textureArrayHeight &&
312
- clampedCount <= this.textureArraySize
313
- ) {
314
- return false
315
- }
316
-
317
- const newWidth = Math.max(this.textureArrayWidth, maxWidth)
318
- const newHeight = Math.max(this.textureArrayHeight, maxHeight)
319
- const newLayers = Math.max(clampedCount, Math.min(this.textureArraySize, 16))
320
-
321
- this.createTextureArray(newWidth, newHeight, newLayers)
322
- return true
323
- }
324
-
325
- private updateBindGroup(): void {
326
- if (!this.bindGroupDirty || !this.device || !this.bindGroupLayout) return
327
-
328
- this.bindGroup = this.device.createBindGroup({
329
- layout: this.bindGroupLayout,
330
- entries: [
331
- { binding: 0, resource: { buffer: this.uniformBuffer! } },
332
- { binding: 1, resource: { buffer: this.imageDataBuffer! } },
333
- { binding: 2, resource: this.textureArrayView! }
334
- ]
335
- })
336
- this.bindGroupDirty = false
337
- }
338
-
339
- async setCanvas(canvas: HTMLCanvasElement, width: number, height: number): Promise<void> {
340
- await this.init()
341
-
342
- if (!this.device) throw new Error('WebGPU device not initialized')
343
- if (width <= 0 || height <= 0) return
344
-
345
- this._canvas = canvas
346
- canvas.width = width
347
- canvas.height = height
348
-
349
- if (!this.context) {
350
- this.context = canvas.getContext('webgpu')
351
- if (!this.context) throw new Error('Could not get WebGPU context')
352
-
353
- this.context.configure({
354
- device: this.device,
355
- format: this.format,
356
- alphaMode: 'premultiplied'
357
- })
358
- }
359
-
360
- this.resolutionArray[0] = width
361
- this.resolutionArray[1] = height
362
- this.device.queue.writeBuffer(this.uniformBuffer!, 0, this.resolutionArray)
363
-
364
- this.lastCanvasWidth = width
365
- this.lastCanvasHeight = height
366
- }
367
-
368
- updateSize(width: number, height: number): void {
369
- if (!this.device || !this._canvas || width <= 0 || height <= 0) return
370
- if (width === this.lastCanvasWidth && height === this.lastCanvasHeight) return
371
-
372
- this._canvas.width = width
373
- this._canvas.height = height
374
- this.resolutionArray[0] = width
375
- this.resolutionArray[1] = height
376
- this.device.queue.writeBuffer(this.uniformBuffer!, 0, this.resolutionArray)
377
-
378
- this.lastCanvasWidth = width
379
- this.lastCanvasHeight = height
380
- }
381
-
382
- /**
383
- * Render ImageBitmaps (async render mode)
384
- * Handles batching when image count exceeds MAX_TEXTURE_ARRAY_LAYERS
385
- */
386
- renderBitmaps(
387
- images: { image: ImageBitmap; x: number; y: number }[],
388
- _canvasWidth: number,
389
- _canvasHeight: number
390
- ): void {
391
- if (!this.device || !this.context || !this.pipeline) return
392
-
393
- const len = images.length
394
- if (len === 0) {
395
- this.clear()
396
- return
397
- }
398
-
399
- const currentTexture = this.context.getCurrentTexture()
400
- if (currentTexture.width === 0 || currentTexture.height === 0) return
401
-
402
- // Single pass: find max dimensions and count valid images
403
- let maxW = 0,
404
- maxH = 0,
405
- validCount = 0
406
- for (let i = 0; i < len; i++) {
407
- const { image } = images[i]
408
- const w = image.width,
409
- h = image.height
410
- if (w > 0 && h > 0) {
411
- if (w > maxW) maxW = w
412
- if (h > maxH) maxH = h
413
- validCount++
414
- }
415
- }
416
-
417
- if (validCount === 0) {
418
- this.clear()
419
- return
420
- }
421
-
422
- // Ensure texture array is large enough (capped at MAX_TEXTURE_ARRAY_LAYERS)
423
- const batchSize = Math.min(validCount, MAX_TEXTURE_ARRAY_LAYERS)
424
- this.ensureTextureArray(maxW, maxH, batchSize)
425
- this.updateBindGroup()
426
-
427
- const device = this.device
428
- const queue = device.queue
429
- const textureArray = this.textureArray!
430
- const imageDataArray = this.imageDataArray
431
- const textureView = currentTexture.createView()
432
-
433
- // Process images in batches if needed
434
- let imageIndex = 0
435
- let isFirstBatch = true
436
-
437
- while (imageIndex < len) {
438
- let texIndex = 0
439
-
440
- // Upload batch of textures
441
- while (imageIndex < len && texIndex < MAX_TEXTURE_ARRAY_LAYERS) {
442
- const img = images[imageIndex++]
443
- const bitmap = img.image
444
- const w = bitmap.width,
445
- h = bitmap.height
446
- if (w <= 0 || h <= 0) continue
447
-
448
- // Copy to texture array layer
449
- queue.copyExternalImageToTexture(
450
- { source: bitmap, flipY: false },
451
- { texture: textureArray, origin: [0, 0, texIndex], premultipliedAlpha: false },
452
- { width: w, height: h }
453
- )
454
-
455
- // Fill pre-allocated array
456
- const offset = texIndex << 3
457
- imageDataArray[offset] = img.x
458
- imageDataArray[offset + 1] = img.y
459
- imageDataArray[offset + 2] = w
460
- imageDataArray[offset + 3] = h
461
- imageDataArray[offset + 4] = w
462
- imageDataArray[offset + 5] = h
463
- imageDataArray[offset + 6] = texIndex
464
- imageDataArray[offset + 7] = 0
465
-
466
- texIndex++
467
- }
468
-
469
- if (texIndex === 0) continue
470
-
471
- // Upload buffer and draw batch
472
- queue.writeBuffer(this.imageDataBuffer!, 0, imageDataArray.buffer, 0, texIndex << 5)
473
-
474
- const commandEncoder = device.createCommandEncoder()
475
- const renderPass = commandEncoder.beginRenderPass({
476
- colorAttachments: [
477
- {
478
- view: textureView,
479
- clearValue: { r: 0, g: 0, b: 0, a: 0 },
480
- loadOp: isFirstBatch ? 'clear' : 'load',
481
- storeOp: 'store'
482
- }
483
- ]
484
- })
485
-
486
- renderPass.setPipeline(this.pipeline)
487
- renderPass.setBindGroup(0, this.bindGroup!)
488
- renderPass.draw(6, texIndex)
489
- renderPass.end()
490
-
491
- queue.submit([commandEncoder.finish()])
492
- isFirstBatch = false
493
- }
494
-
495
- this.cleanupPendingTextures()
496
- }
497
-
498
- /**
499
- * Render from raw ArrayBuffer data (non-async render mode)
500
- * Handles batching when image count exceeds MAX_TEXTURE_ARRAY_LAYERS
501
- */
502
- render(
503
- images: RenderImage[],
504
- _canvasWidth: number,
505
- _canvasHeight: number
506
- ): void {
507
- if (!this.device || !this.context || !this.pipeline) return
508
-
509
- const len = images.length
510
- if (len === 0) {
511
- this.clear()
512
- return
513
- }
514
-
515
- const currentTexture = this.context.getCurrentTexture()
516
- if (currentTexture.width === 0 || currentTexture.height === 0) return
517
-
518
- // Single pass: find max dimensions and count valid images
519
- let maxW = 0,
520
- maxH = 0,
521
- validCount = 0
522
- for (let i = 0; i < len; i++) {
523
- const { w, h } = images[i]
524
- if (w > 0 && h > 0) {
525
- if (w > maxW) maxW = w
526
- if (h > maxH) maxH = h
527
- validCount++
528
- }
529
- }
530
-
531
- if (validCount === 0) {
532
- this.clear()
533
- return
534
- }
535
-
536
- // Ensure texture array is large enough (capped at MAX_TEXTURE_ARRAY_LAYERS)
537
- const batchSize = Math.min(validCount, MAX_TEXTURE_ARRAY_LAYERS)
538
- this.ensureTextureArray(maxW, maxH, batchSize)
539
- this.updateBindGroup()
540
-
541
- const device = this.device
542
- const queue = device.queue
543
- const textureArray = this.textureArray!
544
- const imageDataArray = this.imageDataArray
545
- const textureView = currentTexture.createView()
546
-
547
- // Process images in batches if needed
548
- let imageIndex = 0
549
- let isFirstBatch = true
550
-
551
- while (imageIndex < len) {
552
- let texIndex = 0
553
-
554
- // Upload batch of textures
555
- while (imageIndex < len && texIndex < MAX_TEXTURE_ARRAY_LAYERS) {
556
- const img = images[imageIndex++]
557
- const w = img.w,
558
- h = img.h
559
- if (w <= 0 || h <= 0) continue
560
-
561
- // Upload texture data
562
- const imgData = img.image
563
- if (imgData instanceof ImageBitmap) {
564
- queue.copyExternalImageToTexture(
565
- { source: imgData, flipY: false },
566
- { texture: textureArray, origin: [0, 0, texIndex], premultipliedAlpha: false },
567
- { width: w, height: h }
568
- )
569
- } else if (imgData instanceof ArrayBuffer || imgData instanceof Uint8Array || imgData instanceof Uint8ClampedArray) {
570
- this.uploadTextureData(texIndex, imgData, w, h)
571
- }
572
-
573
- // Fill pre-allocated array
574
- const offset = texIndex << 3
575
- imageDataArray[offset] = img.x
576
- imageDataArray[offset + 1] = img.y
577
- imageDataArray[offset + 2] = w
578
- imageDataArray[offset + 3] = h
579
- imageDataArray[offset + 4] = w
580
- imageDataArray[offset + 5] = h
581
- imageDataArray[offset + 6] = texIndex
582
- imageDataArray[offset + 7] = 0
583
-
584
- texIndex++
585
- }
586
-
587
- if (texIndex === 0) continue
588
-
589
- // Upload buffer and draw batch
590
- queue.writeBuffer(this.imageDataBuffer!, 0, imageDataArray.buffer, 0, texIndex << 5)
591
-
592
- const commandEncoder = device.createCommandEncoder()
593
- const renderPass = commandEncoder.beginRenderPass({
594
- colorAttachments: [
595
- {
596
- view: textureView,
597
- clearValue: { r: 0, g: 0, b: 0, a: 0 },
598
- loadOp: isFirstBatch ? 'clear' : 'load',
599
- storeOp: 'store'
600
- }
601
- ]
602
- })
603
-
604
- renderPass.setPipeline(this.pipeline)
605
- renderPass.setBindGroup(0, this.bindGroup!)
606
- renderPass.draw(6, texIndex)
607
- renderPass.end()
608
-
609
- queue.submit([commandEncoder.finish()])
610
- isFirstBatch = false
611
- }
612
-
613
- this.cleanupPendingTextures()
614
- }
615
-
616
- private uploadTextureData(
617
- layerIndex: number,
618
- rgbaBuffer: ArrayBuffer | Uint8Array | Uint8ClampedArray,
619
- width: number,
620
- height: number
621
- ): void {
622
- this.device!.queue.writeTexture(
623
- { texture: this.textureArray!, origin: [0, 0, layerIndex] },
624
- toUint8View(rgbaBuffer),
625
- { bytesPerRow: width * 4 },
626
- { width, height }
627
- )
628
- }
629
-
630
- private cleanupPendingTextures(): void {
631
- const pending = this.pendingDestroyTextures
632
- const len = pending.length
633
- if (len === 0) return
634
-
635
- for (let i = 0; i < len; i++) {
636
- pending[i].destroy()
637
- }
638
- pending.length = 0
639
- }
640
-
641
- clear(): void {
642
- if (!this.device || !this.context) return
643
-
644
- try {
645
- const currentTexture = this.context.getCurrentTexture()
646
- if (currentTexture.width === 0 || currentTexture.height === 0) return
647
-
648
- const commandEncoder = this.device.createCommandEncoder()
649
- const renderPass = commandEncoder.beginRenderPass({
650
- colorAttachments: [
651
- {
652
- view: currentTexture.createView(),
653
- clearValue: { r: 0, g: 0, b: 0, a: 0 },
654
- loadOp: 'clear',
655
- storeOp: 'store'
656
- }
657
- ]
658
- })
659
- renderPass.end()
660
- this.device.queue.submit([commandEncoder.finish()])
661
- } catch {
662
- // Ignore errors
663
- }
664
- }
665
-
666
- get initialized(): boolean {
667
- return this._initialized
668
- }
669
-
670
- destroy(): void {
671
- this.cleanupPendingTextures()
672
-
673
- this.textureArray?.destroy()
674
- this.textureArray = null
675
- this.textureArrayView = null
676
-
677
- this.uniformBuffer?.destroy()
678
- this.uniformBuffer = null
679
- this.imageDataBuffer?.destroy()
680
- this.imageDataBuffer = null
681
-
682
- this.bindGroup = null
683
-
684
- this.device?.destroy()
685
- this.device = null
686
- this.context = null
687
- this._canvas = null
688
- this._initialized = false
689
- this._initPromise = null
690
- }
691
- }