akarisub 0.2.1 → 0.2.2
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.
- package/LICENSE +3 -0
- package/README.md +53 -51
- package/THIRD_PARTY_NOTICES.md +1590 -0
- package/dist/COPYRIGHT +1588 -949
- package/dist/akarisub-worker.js +2 -2
- package/dist/akarisub-worker.wasm +0 -0
- package/dist/akarisub.umd.js +3 -3
- package/dist/index.js +3 -3
- package/dist/ts/ts/akarisub.d.ts +0 -4
- package/dist/ts/ts/akarisub.d.ts.map +1 -1
- package/dist/ts/ts/akarisub.js +1 -7
- package/dist/ts/ts/akarisub.js.map +1 -1
- package/dist/ts/ts/types.d.ts +4 -4
- package/dist/ts/ts/types.d.ts.map +1 -1
- package/dist/ts/ts/webgl2-renderer.d.ts +2 -1
- package/dist/ts/ts/webgl2-renderer.d.ts.map +1 -1
- package/dist/ts/ts/webgl2-renderer.js +19 -16
- package/dist/ts/ts/webgl2-renderer.js.map +1 -1
- package/dist/ts/ts/webgpu-renderer.d.ts +2 -4
- package/dist/ts/ts/webgpu-renderer.d.ts.map +1 -1
- package/dist/ts/ts/webgpu-renderer.js +19 -52
- package/dist/ts/ts/webgpu-renderer.js.map +1 -1
- package/dist/ts/ts/worker.js +120 -53
- package/dist/ts/ts/worker.js.map +1 -1
- package/package.json +3 -1
- package/src/ts/akarisub.ts +2 -8
- package/src/ts/types.ts +4 -3
- package/src/ts/webgl2-renderer.ts +19 -13
- package/src/ts/webgpu-renderer.ts +26 -71
- package/src/ts/worker.ts +126 -47
package/src/ts/types.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
/** ASS Event (dialogue/subtitle entry) */
|
|
10
10
|
export interface ASSEvent {
|
|
11
|
-
/** Start Time of the Event (in
|
|
11
|
+
/** Start Time of the Event (in milliseconds) */
|
|
12
12
|
Start: number
|
|
13
|
-
/** Duration of the Event (in
|
|
13
|
+
/** Duration of the Event (in milliseconds) */
|
|
14
14
|
Duration: number
|
|
15
15
|
/** Style name */
|
|
16
16
|
Style: string
|
|
@@ -321,7 +321,6 @@ export type WorkerInboundMessage =
|
|
|
321
321
|
| { target: 'resetStats' }
|
|
322
322
|
| { target: 'getEventCount' }
|
|
323
323
|
| { target: 'getStyleCount' }
|
|
324
|
-
| { target: 'runBenchmark' }
|
|
325
324
|
| { target: 'getColorSpace' }
|
|
326
325
|
|
|
327
326
|
// =============================================================================
|
|
@@ -389,6 +388,8 @@ export interface AkariSubModule extends EmscriptenModule {
|
|
|
389
388
|
_akarisub_style_set_num: (handle: number, index: number, field: number, value: number) => void
|
|
390
389
|
_akarisub_style_get_str: (handle: number, index: number, field: number) => number
|
|
391
390
|
_akarisub_style_set_str: (handle: number, index: number, field: number, valuePtr: number) => void
|
|
391
|
+
_akarisub_get_event_time_range: (handle: number, outPtr: number) => number
|
|
392
|
+
_akarisub_get_empty_window: (handle: number, tm: number, outPtr: number) => number
|
|
392
393
|
_akarisub_render_blend_collect: (handle: number, time: number, force: number, outPtr: number, maxItems: number) => number
|
|
393
394
|
_akarisub_render_image_collect: (handle: number, time: number, force: number, outPtr: number, maxItems: number) => number
|
|
394
395
|
FS_createPath: (parent: string, path: string, canRead: boolean, canWrite: boolean) => void
|
|
@@ -179,7 +179,6 @@ export class WebGL2Renderer {
|
|
|
179
179
|
gl.bindVertexArray(null)
|
|
180
180
|
|
|
181
181
|
// Texture array
|
|
182
|
-
this._texArray = gl.createTexture()!
|
|
183
182
|
this._allocateTextureArray(256, 256, 32)
|
|
184
183
|
|
|
185
184
|
// Premultiplied-alpha blending
|
|
@@ -194,18 +193,28 @@ export class WebGL2Renderer {
|
|
|
194
193
|
// Texture management
|
|
195
194
|
// ==========================================================================
|
|
196
195
|
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
// Round up to a multiple of 64: gives headroom against size jitter without
|
|
197
|
+
// power-of-2 over-allocation (a 1920x1080 blend region would otherwise
|
|
198
|
+
// round to 2048x2048 and waste ~2x memory).
|
|
199
|
+
private _roundDim(n: number): number {
|
|
200
|
+
return (Math.max(n, 64) + 63) & ~63
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Round layers up to a multiple of 8, clamped to the layer cap
|
|
204
|
+
private _roundLayers(n: number): number {
|
|
205
|
+
return Math.min((Math.max(n, 8) + 7) & ~7, MAX_TEXTURE_ARRAY_LAYERS)
|
|
199
206
|
}
|
|
200
207
|
|
|
201
208
|
private _allocateTextureArray(width: number, height: number, layers: number): void {
|
|
202
209
|
const gl = this._gl!
|
|
203
|
-
const w = this.
|
|
204
|
-
const h = this.
|
|
205
|
-
const l =
|
|
210
|
+
const w = this._roundDim(width)
|
|
211
|
+
const h = this._roundDim(height)
|
|
212
|
+
const l = this._roundLayers(layers)
|
|
206
213
|
|
|
214
|
+
if (this._texArray) gl.deleteTexture(this._texArray)
|
|
215
|
+
this._texArray = gl.createTexture()!
|
|
207
216
|
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this._texArray)
|
|
208
|
-
gl.
|
|
217
|
+
gl.texStorage3D(gl.TEXTURE_2D_ARRAY, 1, gl.RGBA8, w, h, l)
|
|
209
218
|
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
|
|
210
219
|
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
|
211
220
|
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
|
|
@@ -219,12 +228,9 @@ export class WebGL2Renderer {
|
|
|
219
228
|
private _ensureTextureArray(maxW: number, maxH: number, count: number): void {
|
|
220
229
|
const c = Math.min(count, MAX_TEXTURE_ARRAY_LAYERS)
|
|
221
230
|
if (maxW <= this._texWidth && maxH <= this._texHeight && c <= this._texLayers) return
|
|
222
|
-
const newW =
|
|
223
|
-
const newH =
|
|
224
|
-
const newL = Math.min(
|
|
225
|
-
this._nextPow2(Math.max(this._texLayers, c, c + 16)),
|
|
226
|
-
MAX_TEXTURE_ARRAY_LAYERS
|
|
227
|
-
)
|
|
231
|
+
const newW = Math.max(this._texWidth, maxW)
|
|
232
|
+
const newH = Math.max(this._texHeight, maxH)
|
|
233
|
+
const newL = Math.max(c, Math.min(this._texLayers, 16))
|
|
228
234
|
this._allocateTextureArray(newW, newH, newL)
|
|
229
235
|
}
|
|
230
236
|
|
|
@@ -147,10 +147,6 @@ export class WebGPURenderer {
|
|
|
147
147
|
private readonly imageDataArray: Float32Array
|
|
148
148
|
private readonly resolutionArray = new Float32Array(2)
|
|
149
149
|
|
|
150
|
-
// Reusable conversion buffer for RGBA->BGRA (grows as needed, never shrinks)
|
|
151
|
-
private conversionBuffer: Uint8Array | null = null
|
|
152
|
-
private conversionBufferSize = 0
|
|
153
|
-
|
|
154
150
|
// Bind group (recreated only when texture array changes)
|
|
155
151
|
private bindGroup: GPUBindGroup | null = null
|
|
156
152
|
private bindGroupDirty = true
|
|
@@ -251,15 +247,16 @@ export class WebGPURenderer {
|
|
|
251
247
|
this._initialized = true
|
|
252
248
|
}
|
|
253
249
|
|
|
254
|
-
// Round up to
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
n
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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)
|
|
263
260
|
}
|
|
264
261
|
|
|
265
262
|
private createTextureArray(width: number, height: number, layers: number): void {
|
|
@@ -267,15 +264,13 @@ export class WebGPURenderer {
|
|
|
267
264
|
this.pendingDestroyTextures.push(this.textureArray)
|
|
268
265
|
}
|
|
269
266
|
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
// Clamp layers to WebGPU max (256)
|
|
274
|
-
const l = Math.min(this.nextPowerOf2(Math.max(layers, 16)), MAX_TEXTURE_ARRAY_LAYERS)
|
|
267
|
+
const w = this.roundDim(width)
|
|
268
|
+
const h = this.roundDim(height)
|
|
269
|
+
const l = this.roundLayers(layers)
|
|
275
270
|
|
|
276
271
|
this.textureArray = this.device!.createTexture({
|
|
277
272
|
size: [w, h, l],
|
|
278
|
-
format:
|
|
273
|
+
format: 'rgba8unorm',
|
|
279
274
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
|
|
280
275
|
})
|
|
281
276
|
this.textureArrayView = this.textureArray.createView({ dimension: '2d-array' })
|
|
@@ -318,14 +313,10 @@ export class WebGPURenderer {
|
|
|
318
313
|
) {
|
|
319
314
|
return false
|
|
320
315
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
const newLayers = Math.min(
|
|
326
|
-
this.nextPowerOf2(Math.max(this.textureArraySize, clampedCount, clampedCount + 16)),
|
|
327
|
-
MAX_TEXTURE_ARRAY_LAYERS
|
|
328
|
-
)
|
|
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))
|
|
329
320
|
|
|
330
321
|
this.createTextureArray(newWidth, newHeight, newLayers)
|
|
331
322
|
return true
|
|
@@ -345,15 +336,6 @@ export class WebGPURenderer {
|
|
|
345
336
|
this.bindGroupDirty = false
|
|
346
337
|
}
|
|
347
338
|
|
|
348
|
-
private ensureConversionBuffer(size: number): Uint8Array {
|
|
349
|
-
if (this.conversionBufferSize < size) {
|
|
350
|
-
// Grow with 50% headroom to reduce reallocations
|
|
351
|
-
this.conversionBufferSize = Math.max(size, (this.conversionBufferSize * 1.5) | 0, 65536)
|
|
352
|
-
this.conversionBuffer = new Uint8Array(this.conversionBufferSize)
|
|
353
|
-
}
|
|
354
|
-
return this.conversionBuffer!
|
|
355
|
-
}
|
|
356
|
-
|
|
357
339
|
async setCanvas(canvas: HTMLCanvasElement, width: number, height: number): Promise<void> {
|
|
358
340
|
await this.init()
|
|
359
341
|
|
|
@@ -560,7 +542,6 @@ export class WebGPURenderer {
|
|
|
560
542
|
const queue = device.queue
|
|
561
543
|
const textureArray = this.textureArray!
|
|
562
544
|
const imageDataArray = this.imageDataArray
|
|
563
|
-
const isBGRA = this.format === 'bgra8unorm'
|
|
564
545
|
const textureView = currentTexture.createView()
|
|
565
546
|
|
|
566
547
|
// Process images in batches if needed
|
|
@@ -586,7 +567,7 @@ export class WebGPURenderer {
|
|
|
586
567
|
{ width: w, height: h }
|
|
587
568
|
)
|
|
588
569
|
} else if (imgData instanceof ArrayBuffer || imgData instanceof Uint8Array || imgData instanceof Uint8ClampedArray) {
|
|
589
|
-
this.uploadTextureData(texIndex, imgData, w, h
|
|
570
|
+
this.uploadTextureData(texIndex, imgData, w, h)
|
|
590
571
|
}
|
|
591
572
|
|
|
592
573
|
// Fill pre-allocated array
|
|
@@ -636,38 +617,14 @@ export class WebGPURenderer {
|
|
|
636
617
|
layerIndex: number,
|
|
637
618
|
rgbaBuffer: ArrayBuffer | Uint8Array | Uint8ClampedArray,
|
|
638
619
|
width: number,
|
|
639
|
-
height: number
|
|
640
|
-
swapRB: boolean
|
|
620
|
+
height: number
|
|
641
621
|
): void {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
// Unrolled loop for better performance
|
|
650
|
-
for (let j = 0; j < size; j += 4) {
|
|
651
|
-
uploadData[j] = src[j + 2] // B <- R
|
|
652
|
-
uploadData[j + 1] = src[j + 1] // G
|
|
653
|
-
uploadData[j + 2] = src[j] // R <- B
|
|
654
|
-
uploadData[j + 3] = src[j + 3] // A
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
this.device!.queue.writeTexture(
|
|
658
|
-
{ texture: this.textureArray!, origin: [0, 0, layerIndex] },
|
|
659
|
-
uploadData.buffer,
|
|
660
|
-
{ bytesPerRow: width * 4 },
|
|
661
|
-
{ width, height }
|
|
662
|
-
)
|
|
663
|
-
} else {
|
|
664
|
-
this.device!.queue.writeTexture(
|
|
665
|
-
{ texture: this.textureArray!, origin: [0, 0, layerIndex] },
|
|
666
|
-
src,
|
|
667
|
-
{ bytesPerRow: width * 4 },
|
|
668
|
-
{ width, height }
|
|
669
|
-
)
|
|
670
|
-
}
|
|
622
|
+
this.device!.queue.writeTexture(
|
|
623
|
+
{ texture: this.textureArray!, origin: [0, 0, layerIndex] },
|
|
624
|
+
toUint8View(rgbaBuffer),
|
|
625
|
+
{ bytesPerRow: width * 4 },
|
|
626
|
+
{ width, height }
|
|
627
|
+
)
|
|
671
628
|
}
|
|
672
629
|
|
|
673
630
|
private cleanupPendingTextures(): void {
|
|
@@ -723,8 +680,6 @@ export class WebGPURenderer {
|
|
|
723
680
|
this.imageDataBuffer = null
|
|
724
681
|
|
|
725
682
|
this.bindGroup = null
|
|
726
|
-
this.conversionBuffer = null
|
|
727
|
-
this.conversionBufferSize = 0
|
|
728
683
|
|
|
729
684
|
this.device?.destroy()
|
|
730
685
|
this.device = null
|
package/src/ts/worker.ts
CHANGED
|
@@ -144,6 +144,8 @@ interface AkariSubApi {
|
|
|
144
144
|
styleSetNum: (handle: number, index: number, field: number, value: number) => void
|
|
145
145
|
styleGetStr: (handle: number, index: number, field: number) => number
|
|
146
146
|
styleSetStr: (handle: number, index: number, field: number, valuePtr: number) => void
|
|
147
|
+
getEventTimeRange: (handle: number, outPtr: number) => number
|
|
148
|
+
getEmptyWindow: (handle: number, tm: number, outPtr: number) => number
|
|
147
149
|
renderBlendCollect: (handle: number, time: number, force: number, outPtr: number, maxItems: number) => number
|
|
148
150
|
renderImageCollect: (handle: number, time: number, force: number, outPtr: number, maxItems: number) => number
|
|
149
151
|
}
|
|
@@ -162,7 +164,7 @@ const FULL_WARMUP_CAP_SECONDS = 30
|
|
|
162
164
|
const FULL_WARMUP_STEP_SECONDS = 1
|
|
163
165
|
const FULL_WARMUP_YIELD_EVERY = 24
|
|
164
166
|
const ASS_TIME_SCALE = 1000
|
|
165
|
-
const imagePool: RenderResultItem[] =
|
|
167
|
+
const imagePool: RenderResultItem[] = []
|
|
166
168
|
let poolInitialized = false
|
|
167
169
|
// Batch render-collect buffer: 3 header ints (changed, count, time) + 5 ints per image (x, y, w, h, image_ptr)
|
|
168
170
|
const RRC_HEADER_INTS = 3
|
|
@@ -170,6 +172,13 @@ const RRC_IMG_STRIDE = 5
|
|
|
170
172
|
// Pre-allocated buffer for batch render-collect calls
|
|
171
173
|
let rrcBufPtr = 0
|
|
172
174
|
let rrcBufCapacity = 0
|
|
175
|
+
// Cached views over the render-collect buffer; refreshed when the wasm memory
|
|
176
|
+
// grows (buffer identity changes) or the buffer is reallocated/resized.
|
|
177
|
+
let rrcHeaderView: Int32Array | null = null
|
|
178
|
+
let rrcMetaView: Int32Array | null = null
|
|
179
|
+
let rrcViewsBuffer: ArrayBufferLike | null = null
|
|
180
|
+
let rrcViewsPtr = 0
|
|
181
|
+
let rrcViewsCapacity = 0
|
|
173
182
|
const frameImages: RenderResultItem[] = []
|
|
174
183
|
const frameArrayBuffers: ArrayBuffer[] = []
|
|
175
184
|
const frameBitmapPromises: Promise<ImageBitmap>[] = []
|
|
@@ -200,10 +209,13 @@ const initPool = (): void => {
|
|
|
200
209
|
}
|
|
201
210
|
|
|
202
211
|
const getPooledItem = (index: number): RenderResultItem => {
|
|
203
|
-
|
|
204
|
-
|
|
212
|
+
let item = imagePool[index]
|
|
213
|
+
if (!item) {
|
|
214
|
+
// Grow the pool on demand so frames with many images reuse items too
|
|
215
|
+
item = { w: 0, h: 0, x: 0, y: 0, image: 0 }
|
|
216
|
+
imagePool[index] = item
|
|
205
217
|
}
|
|
206
|
-
return
|
|
218
|
+
return item
|
|
207
219
|
}
|
|
208
220
|
|
|
209
221
|
/**
|
|
@@ -234,6 +246,22 @@ const ensureRenderCollectBuffer = (maxImages: number): void => {
|
|
|
234
246
|
rrcBufCapacity = nextCapacity
|
|
235
247
|
}
|
|
236
248
|
|
|
249
|
+
const refreshRrcViews = (): void => {
|
|
250
|
+
const buffer = self.wasmMemory.buffer
|
|
251
|
+
if (rrcHeaderView && rrcViewsBuffer === buffer && rrcViewsPtr === rrcBufPtr && rrcViewsCapacity === rrcBufCapacity) {
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
rrcHeaderView = new Int32Array(buffer, rrcBufPtr, RRC_HEADER_INTS)
|
|
255
|
+
rrcMetaView = new Int32Array(
|
|
256
|
+
buffer,
|
|
257
|
+
rrcBufPtr + RRC_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT,
|
|
258
|
+
rrcBufCapacity - RRC_HEADER_INTS
|
|
259
|
+
)
|
|
260
|
+
rrcViewsBuffer = buffer
|
|
261
|
+
rrcViewsPtr = rrcBufPtr
|
|
262
|
+
rrcViewsCapacity = rrcBufCapacity
|
|
263
|
+
}
|
|
264
|
+
|
|
237
265
|
const prewarmRenderer = (time: number): void => {
|
|
238
266
|
if (!akariSubHandle) return
|
|
239
267
|
|
|
@@ -252,55 +280,52 @@ const syncTotalEventsMetric = (): void => {
|
|
|
252
280
|
metrics.totalEvents = akariSubHandle ? requireApi().getEventCount(akariSubHandle) : 0
|
|
253
281
|
}
|
|
254
282
|
|
|
255
|
-
const
|
|
256
|
-
if (!akariSubHandle) return null
|
|
283
|
+
const getTrackEventTimeRange = (): { start: number; end: number } | null => {
|
|
284
|
+
if (!akariSubHandle || !_Module) return null
|
|
257
285
|
|
|
258
286
|
const api = requireApi()
|
|
259
287
|
const handle = requireHandle()
|
|
260
|
-
const
|
|
261
|
-
if (
|
|
288
|
+
const outPtr = _Module._malloc(2 * Int32Array.BYTES_PER_ELEMENT)
|
|
289
|
+
if (!outPtr) return null
|
|
262
290
|
|
|
263
|
-
|
|
291
|
+
try {
|
|
292
|
+
if (!api.getEventTimeRange(handle, outPtr)) return null
|
|
264
293
|
|
|
265
|
-
|
|
266
|
-
const start =
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
294
|
+
const view = new Int32Array(self.wasmMemory.buffer, outPtr, 2)
|
|
295
|
+
const start = Math.max(0, view[0] / ASS_TIME_SCALE)
|
|
296
|
+
const end = Math.max(start, view[1] / ASS_TIME_SCALE)
|
|
297
|
+
return { start, end }
|
|
298
|
+
} finally {
|
|
299
|
+
_Module._free(outPtr)
|
|
270
300
|
}
|
|
271
|
-
|
|
272
|
-
if (!Number.isFinite(firstStart)) return null
|
|
273
|
-
return Math.max(0, firstStart)
|
|
274
301
|
}
|
|
275
302
|
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const api = requireApi()
|
|
280
|
-
const handle = requireHandle()
|
|
281
|
-
const count = api.getEventCount(handle)
|
|
282
|
-
if (count <= 0) return null
|
|
303
|
+
const getFirstEventStartTime = (): number | null => {
|
|
304
|
+
return getTrackEventTimeRange()?.start ?? null
|
|
305
|
+
}
|
|
283
306
|
|
|
284
|
-
|
|
285
|
-
|
|
307
|
+
let emptyWindowFrom = -1
|
|
308
|
+
let emptyWindowUntil = -1
|
|
286
309
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
310
|
+
const invalidateEmptyWindow = (): void => {
|
|
311
|
+
emptyWindowFrom = -1
|
|
312
|
+
emptyWindowUntil = -1
|
|
313
|
+
}
|
|
290
314
|
|
|
291
|
-
|
|
315
|
+
const computeEmptyWindow = (time: number): void => {
|
|
316
|
+
if (!akariSubHandle || !_Module) return
|
|
292
317
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (eventEnd > end) end = eventEnd
|
|
296
|
-
}
|
|
318
|
+
const outPtr = _Module._malloc(Int32Array.BYTES_PER_ELEMENT)
|
|
319
|
+
if (!outPtr) return
|
|
297
320
|
|
|
298
|
-
|
|
299
|
-
|
|
321
|
+
try {
|
|
322
|
+
if (!requireApi().getEmptyWindow(requireHandle(), time, outPtr)) return
|
|
300
323
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
324
|
+
const nextStart = new Int32Array(self.wasmMemory.buffer, outPtr, 1)[0]
|
|
325
|
+
emptyWindowFrom = time
|
|
326
|
+
emptyWindowUntil = nextStart < 0 ? Number.POSITIVE_INFINITY : nextStart / ASS_TIME_SCALE
|
|
327
|
+
} finally {
|
|
328
|
+
_Module._free(outPtr)
|
|
304
329
|
}
|
|
305
330
|
}
|
|
306
331
|
|
|
@@ -1012,6 +1037,13 @@ const render = (time: number, force?: boolean | number): void => {
|
|
|
1012
1037
|
return
|
|
1013
1038
|
}
|
|
1014
1039
|
|
|
1040
|
+
// Inside a known-empty window the output cannot change: skip the WASM call
|
|
1041
|
+
if (!force && emptyWindowFrom >= 0 && time >= emptyWindowFrom && time < emptyWindowUntil) {
|
|
1042
|
+
metrics.cacheHits++
|
|
1043
|
+
postMessage({ target: 'unbusy' })
|
|
1044
|
+
return
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1015
1047
|
renderInFlight = true
|
|
1016
1048
|
initPool() // Ensure pool is ready
|
|
1017
1049
|
|
|
@@ -1032,9 +1064,15 @@ const render = (time: number, force?: boolean | number): void => {
|
|
|
1032
1064
|
? api.renderBlendCollect(handle, time, forceInt, rrcBufPtr, rrcBufCapacity)
|
|
1033
1065
|
: api.renderImageCollect(handle, time, forceInt, rrcBufPtr, rrcBufCapacity)
|
|
1034
1066
|
|
|
1035
|
-
|
|
1067
|
+
// Refresh after the WASM call: rendering may have grown the memory
|
|
1068
|
+
refreshRrcViews()
|
|
1069
|
+
const headerView = rrcHeaderView!
|
|
1036
1070
|
const changed = headerView[0]
|
|
1037
|
-
|
|
1071
|
+
|
|
1072
|
+
// Frame produced no images: ask how long renders can be skipped.
|
|
1073
|
+
if (headerView[1] === 0 && (emptyWindowFrom < 0 || time < emptyWindowFrom || time >= emptyWindowUntil)) {
|
|
1074
|
+
computeEmptyWindow(time)
|
|
1075
|
+
}
|
|
1038
1076
|
|
|
1039
1077
|
// Update metrics
|
|
1040
1078
|
const renderEndTime = performance.now()
|
|
@@ -1069,8 +1107,7 @@ const render = (time: number, force?: boolean | number): void => {
|
|
|
1069
1107
|
|
|
1070
1108
|
if (written === 0) return paintImages({ images, buffers, times })
|
|
1071
1109
|
|
|
1072
|
-
const
|
|
1073
|
-
const meta = new Int32Array(self.wasmMemory.buffer, imgDataOffset, written * RRC_IMG_STRIDE)
|
|
1110
|
+
const meta = rrcMetaView!
|
|
1074
1111
|
|
|
1075
1112
|
const useAsyncBitmapPath = asyncRender && offscreenRender !== true
|
|
1076
1113
|
|
|
@@ -1089,7 +1126,12 @@ const render = (time: number, force?: boolean | number): void => {
|
|
|
1089
1126
|
|
|
1090
1127
|
const pointer = meta[metaOffset + 4]
|
|
1091
1128
|
const byteLength = item.w * item.h * 4
|
|
1092
|
-
|
|
1129
|
+
let rawData = new Uint8ClampedArray(self.wasmMemory.buffer, pointer, byteLength)
|
|
1130
|
+
if (hasBitmapBug) {
|
|
1131
|
+
// Browsers with the partial-bitmap bug mis-render ImageData backed
|
|
1132
|
+
// by a view into a larger buffer; give them a standalone copy.
|
|
1133
|
+
rawData = rawData.slice()
|
|
1134
|
+
}
|
|
1093
1135
|
|
|
1094
1136
|
const imageData = new ImageData(rawData as Uint8ClampedArray<ArrayBuffer>, item.w, item.h)
|
|
1095
1137
|
|
|
@@ -1119,6 +1161,20 @@ const render = (time: number, force?: boolean | number): void => {
|
|
|
1119
1161
|
}
|
|
1120
1162
|
})
|
|
1121
1163
|
} else {
|
|
1164
|
+
// When posting to the main thread, copy all image pixels into one
|
|
1165
|
+
// transferable buffer instead of allocating/transferring one per image.
|
|
1166
|
+
let copyTarget: Uint8ClampedArray<ArrayBuffer> | null = null
|
|
1167
|
+
let copyOffset = 0
|
|
1168
|
+
if (!offCanvasCtx) {
|
|
1169
|
+
let totalBytes = 0
|
|
1170
|
+
for (let i = 0; i < written; ++i) {
|
|
1171
|
+
const metaOffset = i * RRC_IMG_STRIDE
|
|
1172
|
+
totalBytes += meta[metaOffset + 2] * meta[metaOffset + 3] * 4
|
|
1173
|
+
}
|
|
1174
|
+
copyTarget = new Uint8ClampedArray(totalBytes)
|
|
1175
|
+
buffers.push(copyTarget.buffer)
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1122
1178
|
for (let i = 0; i < written; ++i) {
|
|
1123
1179
|
const metaOffset = i * RRC_IMG_STRIDE
|
|
1124
1180
|
const item = getPooledItem(i)
|
|
@@ -1128,13 +1184,13 @@ const render = (time: number, force?: boolean | number): void => {
|
|
|
1128
1184
|
item.h = meta[metaOffset + 3]
|
|
1129
1185
|
item.image = meta[metaOffset + 4]
|
|
1130
1186
|
|
|
1131
|
-
if (
|
|
1187
|
+
if (copyTarget) {
|
|
1132
1188
|
const imagePtr = item.image as number
|
|
1133
1189
|
const byteLength = item.w * item.h * 4
|
|
1134
|
-
const copiedData =
|
|
1190
|
+
const copiedData = copyTarget.subarray(copyOffset, copyOffset + byteLength)
|
|
1135
1191
|
copiedData.set(self.HEAPU8C.subarray(imagePtr, imagePtr + byteLength))
|
|
1136
|
-
buffers.push(copiedData.buffer)
|
|
1137
1192
|
item.image = copiedData
|
|
1193
|
+
copyOffset += byteLength
|
|
1138
1194
|
}
|
|
1139
1195
|
images[i] = item
|
|
1140
1196
|
}
|
|
@@ -1353,6 +1409,8 @@ self.init = async (data: any): Promise<void> => {
|
|
|
1353
1409
|
styleSetNum: Module._akarisub_style_set_num,
|
|
1354
1410
|
styleGetStr: Module._akarisub_style_get_str,
|
|
1355
1411
|
styleSetStr: Module._akarisub_style_set_str,
|
|
1412
|
+
getEventTimeRange: Module._akarisub_get_event_time_range,
|
|
1413
|
+
getEmptyWindow: Module._akarisub_get_empty_window,
|
|
1356
1414
|
renderBlendCollect: Module._akarisub_render_blend_collect,
|
|
1357
1415
|
renderImageCollect: Module._akarisub_render_image_collect
|
|
1358
1416
|
}
|
|
@@ -1732,6 +1790,11 @@ self.destroy = (): void => {
|
|
|
1732
1790
|
_Module._free(rrcBufPtr)
|
|
1733
1791
|
rrcBufPtr = 0
|
|
1734
1792
|
rrcBufCapacity = 0
|
|
1793
|
+
rrcHeaderView = null
|
|
1794
|
+
rrcMetaView = null
|
|
1795
|
+
rrcViewsBuffer = null
|
|
1796
|
+
rrcViewsPtr = 0
|
|
1797
|
+
rrcViewsCapacity = 0
|
|
1735
1798
|
}
|
|
1736
1799
|
}
|
|
1737
1800
|
if (akariSubHandle) {
|
|
@@ -1959,11 +2022,27 @@ self.getStyleCount = (): void => {
|
|
|
1959
2022
|
// Message Handler
|
|
1960
2023
|
// =============================================================================
|
|
1961
2024
|
|
|
2025
|
+
const RENDER_SAFE_TARGETS = new Set([
|
|
2026
|
+
'demand',
|
|
2027
|
+
'video',
|
|
2028
|
+
'getEvents',
|
|
2029
|
+
'getStyles',
|
|
2030
|
+
'getStats',
|
|
2031
|
+
'resetStats',
|
|
2032
|
+
'getEventCount',
|
|
2033
|
+
'getStyleCount',
|
|
2034
|
+
'getColorSpace'
|
|
2035
|
+
])
|
|
2036
|
+
|
|
1962
2037
|
onmessage = ({ data }: MessageEvent): void => {
|
|
1963
2038
|
if (!self[data.target]) {
|
|
1964
2039
|
throw new Error('Unknown event target ' + data.target)
|
|
1965
2040
|
}
|
|
1966
2041
|
|
|
2042
|
+
if (!RENDER_SAFE_TARGETS.has(data.target)) {
|
|
2043
|
+
invalidateEmptyWindow()
|
|
2044
|
+
}
|
|
2045
|
+
|
|
1967
2046
|
Promise.resolve(self[data.target](data)).catch((error) => {
|
|
1968
2047
|
postMessage({
|
|
1969
2048
|
target: 'error',
|