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/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 seconds) */
11
+ /** Start Time of the Event (in milliseconds) */
12
12
  Start: number
13
- /** Duration of the Event (in seconds) */
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
- private _nextPow2(n: number): number {
198
- n--; n |= n >> 1; n |= n >> 2; n |= n >> 4; n |= n >> 8; n |= n >> 16; return n + 1
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._nextPow2(Math.max(width, 64))
204
- const h = this._nextPow2(Math.max(height, 64))
205
- const l = Math.min(this._nextPow2(Math.max(layers, 16)), MAX_TEXTURE_ARRAY_LAYERS)
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.texImage3D(gl.TEXTURE_2D_ARRAY, 0, gl.RGBA8, w, h, l, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)
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 = this._nextPow2(Math.max(this._texWidth, maxW))
223
- const newH = this._nextPow2(Math.max(this._texHeight, maxH))
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 next power of 2 for GPU-friendly sizes
255
- private nextPowerOf2(n: number): number {
256
- n--
257
- n |= n >> 1
258
- n |= n >> 2
259
- n |= n >> 4
260
- n |= n >> 8
261
- n |= n >> 16
262
- return n + 1
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
- // Use power-of-2 dimensions for better GPU performance
271
- const w = this.nextPowerOf2(Math.max(width, 64))
272
- const h = this.nextPowerOf2(Math.max(height, 64))
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: this.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
- // Grow with some headroom to avoid frequent resizes, but cap at max layers
323
- const newWidth = this.nextPowerOf2(Math.max(this.textureArrayWidth, maxWidth))
324
- const newHeight = this.nextPowerOf2(Math.max(this.textureArrayHeight, maxHeight))
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, isBGRA)
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
- const size = width * height * 4
643
- const src = toUint8View(rgbaBuffer)
644
-
645
- if (swapRB) {
646
- // Use reusable conversion buffer
647
- const uploadData = this.ensureConversionBuffer(size)
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[] = new Array(MAX_POOLED_IMAGES)
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
- if (index < MAX_POOLED_IMAGES) {
204
- return imagePool[index]
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 { w: 0, h: 0, x: 0, y: 0, image: 0 }
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 getFirstEventStartTime = (): number | null => {
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 count = api.getEventCount(handle)
261
- if (count <= 0) return null
288
+ const outPtr = _Module._malloc(2 * Int32Array.BYTES_PER_ELEMENT)
289
+ if (!outPtr) return null
262
290
 
263
- let firstStart = Number.POSITIVE_INFINITY
291
+ try {
292
+ if (!api.getEventTimeRange(handle, outPtr)) return null
264
293
 
265
- for (let i = 0; i < count; i++) {
266
- const start = api.eventGetInt(handle, i, EVENT_INT_FIELDS.Start) / ASS_TIME_SCALE
267
- if (Number.isFinite(start) && start < firstStart) {
268
- firstStart = start
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 getTrackEventTimeRange = (): { start: number; end: number } | null => {
277
- if (!akariSubHandle) return null
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
- let start = Number.POSITIVE_INFINITY
285
- let end = 0
307
+ let emptyWindowFrom = -1
308
+ let emptyWindowUntil = -1
286
309
 
287
- for (let i = 0; i < count; i++) {
288
- const eventStart = api.eventGetInt(handle, i, EVENT_INT_FIELDS.Start) / ASS_TIME_SCALE
289
- const eventDuration = Math.max(0, api.eventGetInt(handle, i, EVENT_INT_FIELDS.Duration) / ASS_TIME_SCALE)
310
+ const invalidateEmptyWindow = (): void => {
311
+ emptyWindowFrom = -1
312
+ emptyWindowUntil = -1
313
+ }
290
314
 
291
- if (!Number.isFinite(eventStart)) continue
315
+ const computeEmptyWindow = (time: number): void => {
316
+ if (!akariSubHandle || !_Module) return
292
317
 
293
- const eventEnd = eventStart + eventDuration
294
- if (eventStart < start) start = eventStart
295
- if (eventEnd > end) end = eventEnd
296
- }
318
+ const outPtr = _Module._malloc(Int32Array.BYTES_PER_ELEMENT)
319
+ if (!outPtr) return
297
320
 
298
- if (!Number.isFinite(start)) return null
299
- if (end < start) end = start
321
+ try {
322
+ if (!requireApi().getEmptyWindow(requireHandle(), time, outPtr)) return
300
323
 
301
- return {
302
- start: Math.max(0, start),
303
- end: Math.max(0, end)
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
- const headerView = new Int32Array(self.wasmMemory.buffer, rrcBufPtr, RRC_HEADER_INTS)
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
- const imageCount = headerView[1]
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 imgDataOffset = rrcBufPtr + RRC_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT
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
- const rawData = new Uint8ClampedArray(self.wasmMemory.buffer, pointer, byteLength)
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 (!offCanvasCtx) {
1187
+ if (copyTarget) {
1132
1188
  const imagePtr = item.image as number
1133
1189
  const byteLength = item.w * item.h * 4
1134
- const copiedData = new Uint8ClampedArray(byteLength)
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',