akarisub 0.1.0 → 0.2.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.
- package/README.md +14 -0
- package/dist/akarisub-worker.js +2 -2
- package/dist/akarisub-worker.wasm +0 -0
- package/dist/akarisub.umd.js +5 -5
- package/dist/index.js +5 -5
- package/package.json +1 -1
- package/src/ts/akarisub.ts +148 -71
- package/src/ts/types.ts +4 -13
- package/src/ts/webgl2-renderer.ts +7 -2
- package/src/ts/webgpu-renderer.ts +12 -4
- package/src/ts/worker.ts +31 -78
package/src/ts/akarisub.ts
CHANGED
|
@@ -28,6 +28,27 @@ import { WebGL2Renderer, isWebGL2Supported } from './webgl2-renderer'
|
|
|
28
28
|
|
|
29
29
|
type AnyGPURenderer = WebGPURenderer | WebGL2Renderer
|
|
30
30
|
|
|
31
|
+
const DEFAULT_RENDER_AHEAD = 0.008
|
|
32
|
+
const DEFAULT_PIPELINE_LATENCY_MS = DEFAULT_RENDER_AHEAD * 1000
|
|
33
|
+
const DEFAULT_WEBKIT_PIPELINE_LATENCY_MS = 16
|
|
34
|
+
|
|
35
|
+
const isLikelyWebKit = (): boolean => {
|
|
36
|
+
if (typeof navigator === 'undefined') return false
|
|
37
|
+
|
|
38
|
+
const userAgent = navigator.userAgent || ''
|
|
39
|
+
const vendor = navigator.vendor || ''
|
|
40
|
+
const isIOSWebKit = /\b(iPhone|iPad|iPod)\b/i.test(userAgent)
|
|
41
|
+
|
|
42
|
+
if (!/AppleWebKit/i.test(userAgent)) return false
|
|
43
|
+
if (isIOSWebKit) return true
|
|
44
|
+
|
|
45
|
+
if (/\b(Chrome|Chromium|Edg|OPR|SamsungBrowser|Firefox)\b/i.test(userAgent)) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return vendor.includes('Apple')
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
/**
|
|
32
53
|
* AkariSub - JavaScript ASS/SSA Subtitle Renderer
|
|
33
54
|
*
|
|
@@ -74,6 +95,9 @@ export default class AkariSub extends EventTarget {
|
|
|
74
95
|
private _ro?: ResizeObserver
|
|
75
96
|
private _worker: Worker
|
|
76
97
|
private _pendingDemandTimes: Array<{ mediaTime: number; width: number; height: number }> = []
|
|
98
|
+
private readonly _isLikelyWebKit: boolean
|
|
99
|
+
private _activeDemandStartedAt: number = 0
|
|
100
|
+
private _smoothedDemandLatencyMs: number
|
|
77
101
|
|
|
78
102
|
// Bound methods for event listeners
|
|
79
103
|
private _boundResize: () => void
|
|
@@ -90,6 +114,7 @@ export default class AkariSub extends EventTarget {
|
|
|
90
114
|
// Cached render data to reduce allocations
|
|
91
115
|
private _lastRenderWidth: number = 0
|
|
92
116
|
private _lastRenderHeight: number = 0
|
|
117
|
+
private _gpuBitmapImages: Array<{ image: ImageBitmap; x: number; y: number }> = []
|
|
93
118
|
|
|
94
119
|
// Public properties
|
|
95
120
|
public timeOffset: number
|
|
@@ -114,6 +139,11 @@ export default class AkariSub extends EventTarget {
|
|
|
114
139
|
this._init = resolve
|
|
115
140
|
})
|
|
116
141
|
|
|
142
|
+
this._isLikelyWebKit = isLikelyWebKit()
|
|
143
|
+
this._smoothedDemandLatencyMs = this._isLikelyWebKit
|
|
144
|
+
? DEFAULT_WEBKIT_PIPELINE_LATENCY_MS
|
|
145
|
+
: DEFAULT_PIPELINE_LATENCY_MS
|
|
146
|
+
|
|
117
147
|
// Run feature tests
|
|
118
148
|
const test = AkariSub._test()
|
|
119
149
|
|
|
@@ -121,7 +151,8 @@ export default class AkariSub extends EventTarget {
|
|
|
121
151
|
|
|
122
152
|
this._onCanvasFallback = options.onCanvasFallback
|
|
123
153
|
|
|
124
|
-
const canUseGPURenderer = !options.canvas && (isWebGPUSupported() || isWebGL2Supported())
|
|
154
|
+
const canUseGPURenderer = !this._isLikelyWebKit && !options.canvas && (isWebGPUSupported() || isWebGL2Supported())
|
|
155
|
+
const shouldUseAsyncRender = typeof createImageBitmap !== 'undefined' && (options.asyncRender ?? !this._isLikelyWebKit)
|
|
125
156
|
|
|
126
157
|
// Don't support offscreen rendering on custom canvases
|
|
127
158
|
this._offscreenRender =
|
|
@@ -153,7 +184,7 @@ export default class AkariSub extends EventTarget {
|
|
|
153
184
|
if (canUseGPURenderer) {
|
|
154
185
|
this._initGPURenderer()
|
|
155
186
|
} else if (!this._offscreenRender) {
|
|
156
|
-
this._ctx = this._canvas.getContext('2d')
|
|
187
|
+
this._ctx = this._canvas.getContext('2d', { alpha: true, desynchronized: true })
|
|
157
188
|
}
|
|
158
189
|
|
|
159
190
|
this._canvasctrl = this._offscreenRender
|
|
@@ -167,7 +198,7 @@ export default class AkariSub extends EventTarget {
|
|
|
167
198
|
this.prescaleFactor = options.prescaleFactor || 1.0
|
|
168
199
|
this.prescaleHeightLimit = options.prescaleHeightLimit || 1080
|
|
169
200
|
this.maxRenderHeight = options.maxRenderHeight || 0
|
|
170
|
-
this.renderAhead = options.renderAhead ??
|
|
201
|
+
this.renderAhead = options.renderAhead ?? DEFAULT_RENDER_AHEAD
|
|
171
202
|
|
|
172
203
|
// Bind methods
|
|
173
204
|
this._boundResize = this.resize.bind(this)
|
|
@@ -195,7 +226,8 @@ export default class AkariSub extends EventTarget {
|
|
|
195
226
|
this._worker.postMessage({
|
|
196
227
|
target: 'init',
|
|
197
228
|
wasmUrl: options.wasmUrl ?? 'akarisub-worker.wasm',
|
|
198
|
-
asyncRender:
|
|
229
|
+
asyncRender: shouldUseAsyncRender,
|
|
230
|
+
fullTrackWarmup: options.fullTrackWarmup ?? false,
|
|
199
231
|
onDemandRender: this._onDemandRender,
|
|
200
232
|
initialTime: (this._video?.currentTime ?? 0) + this.timeOffset,
|
|
201
233
|
width: this._canvasctrl.width || 0,
|
|
@@ -327,7 +359,7 @@ export default class AkariSub extends EventTarget {
|
|
|
327
359
|
|
|
328
360
|
this._rendererType = 'canvas2d'
|
|
329
361
|
if (!this._offscreenRender && !this._ctx) {
|
|
330
|
-
this._ctx = this._canvas.getContext('2d')
|
|
362
|
+
this._ctx = this._canvas.getContext('2d', { alpha: true, desynchronized: true })
|
|
331
363
|
}
|
|
332
364
|
this.sendMessage('setAsyncRender', { value: false })
|
|
333
365
|
this._onCanvasFallback?.()
|
|
@@ -743,6 +775,8 @@ export default class AkariSub extends EventTarget {
|
|
|
743
775
|
}
|
|
744
776
|
|
|
745
777
|
private _unbusy(): void {
|
|
778
|
+
this._observeDemandCompletion()
|
|
779
|
+
|
|
746
780
|
if (this._pendingDemandTimes.length > 0) {
|
|
747
781
|
if (this._pendingDemandTimes.length > 1) {
|
|
748
782
|
const latestDemand = this._pendingDemandTimes[this._pendingDemandTimes.length - 1]
|
|
@@ -760,6 +794,31 @@ export default class AkariSub extends EventTarget {
|
|
|
760
794
|
this.busy = false
|
|
761
795
|
}
|
|
762
796
|
|
|
797
|
+
private _markDemandDispatched(): void {
|
|
798
|
+
if (!this._onDemandRender) return
|
|
799
|
+
this._activeDemandStartedAt = performance.now()
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
private _observeDemandCompletion(): void {
|
|
803
|
+
if (!this._onDemandRender || this._activeDemandStartedAt === 0) return
|
|
804
|
+
|
|
805
|
+
const elapsed = performance.now() - this._activeDemandStartedAt
|
|
806
|
+
this._activeDemandStartedAt = 0
|
|
807
|
+
|
|
808
|
+
if (!Number.isFinite(elapsed) || elapsed <= 0) return
|
|
809
|
+
|
|
810
|
+
this._smoothedDemandLatencyMs = this._smoothedDemandLatencyMs <= 0
|
|
811
|
+
? elapsed
|
|
812
|
+
: this._smoothedDemandLatencyMs * 0.75 + elapsed * 0.25
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
private _getDemandPipelineLeadSeconds(now: number, metadata: VideoFrameCallbackMetadata): number {
|
|
816
|
+
const expectedDisplayTime = metadata.expectedDisplayTime ?? metadata.presentationTime ?? now
|
|
817
|
+
const displayLeadSeconds = Math.max(0, expectedDisplayTime - now) / 1000
|
|
818
|
+
|
|
819
|
+
return Math.max(0, this._smoothedDemandLatencyMs / 1000 - displayLeadSeconds)
|
|
820
|
+
}
|
|
821
|
+
|
|
763
822
|
private _enqueueDemand(metadata: { mediaTime: number; width: number; height: number }): void {
|
|
764
823
|
const queue = this._pendingDemandTimes
|
|
765
824
|
|
|
@@ -782,7 +841,8 @@ export default class AkariSub extends EventTarget {
|
|
|
782
841
|
|
|
783
842
|
// Get the video's playback rate to correctly scale time offsets
|
|
784
843
|
const playbackRate = this._video?.playbackRate ?? 1
|
|
785
|
-
const
|
|
844
|
+
const pipelineLeadSeconds = this._getDemandPipelineLeadSeconds(now, metadata)
|
|
845
|
+
const renderTime = metadata.mediaTime + (pipelineLeadSeconds + this.renderAhead) * playbackRate
|
|
786
846
|
|
|
787
847
|
const demandData = {
|
|
788
848
|
mediaTime: renderTime,
|
|
@@ -813,6 +873,7 @@ export default class AkariSub extends EventTarget {
|
|
|
813
873
|
this.resize()
|
|
814
874
|
}
|
|
815
875
|
|
|
876
|
+
this._markDemandDispatched()
|
|
816
877
|
this.sendMessage('demand', { time: metadata.mediaTime + this.timeOffset })
|
|
817
878
|
}
|
|
818
879
|
|
|
@@ -822,9 +883,10 @@ export default class AkariSub extends EventTarget {
|
|
|
822
883
|
this._canvas.remove()
|
|
823
884
|
this._createCanvas()
|
|
824
885
|
this._canvasctrl = this._canvas
|
|
825
|
-
this._ctx = this._canvasctrl.getContext('2d')
|
|
886
|
+
this._ctx = this._canvasctrl.getContext('2d', { alpha: true, desynchronized: true })
|
|
826
887
|
this.sendMessage('detachOffscreen')
|
|
827
888
|
this.busy = false
|
|
889
|
+
this._activeDemandStartedAt = 0
|
|
828
890
|
this._pendingDemandTimes.length = 0
|
|
829
891
|
this.resize(0, 0, 0, 0, true)
|
|
830
892
|
}
|
|
@@ -878,80 +940,87 @@ export default class AkariSub extends EventTarget {
|
|
|
878
940
|
height: number
|
|
879
941
|
colorSpace: SubtitleColorSpace
|
|
880
942
|
}): void {
|
|
881
|
-
|
|
943
|
+
try {
|
|
944
|
+
const dataWidth = data.width
|
|
945
|
+
const dataHeight = data.height
|
|
882
946
|
|
|
883
|
-
|
|
884
|
-
|
|
947
|
+
if (this.debug) {
|
|
948
|
+
data.times.IPCTime = Date.now() - (data.times.JSRenderTime || 0)
|
|
949
|
+
}
|
|
885
950
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
951
|
+
// Check if canvas size changed
|
|
952
|
+
const sizeChanged = this._canvasctrl.width !== dataWidth || this._canvasctrl.height !== dataHeight
|
|
953
|
+
if (sizeChanged) {
|
|
954
|
+
this._canvasctrl.width = dataWidth
|
|
955
|
+
this._canvasctrl.height = dataHeight
|
|
956
|
+
this._lastRenderWidth = dataWidth
|
|
957
|
+
this._lastRenderHeight = dataHeight
|
|
958
|
+
|
|
959
|
+
// Update GPU renderer size if canvas size changed
|
|
960
|
+
if (this._gpuRenderer) {
|
|
961
|
+
this._gpuRenderer.updateSize(dataWidth, dataHeight)
|
|
962
|
+
}
|
|
889
963
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
if (sizeChanged) {
|
|
893
|
-
this._canvasctrl.width = dataWidth
|
|
894
|
-
this._canvasctrl.height = dataHeight
|
|
895
|
-
this._lastRenderWidth = dataWidth
|
|
896
|
-
this._lastRenderHeight = dataHeight
|
|
964
|
+
this._verifyColorSpace({ subtitleColorSpace: data.colorSpace })
|
|
965
|
+
}
|
|
897
966
|
|
|
898
|
-
//
|
|
967
|
+
// Use GPU renderer (WebGPU or WebGL2) if available
|
|
899
968
|
if (this._gpuRenderer) {
|
|
900
|
-
this.
|
|
969
|
+
this._renderGPU(data)
|
|
970
|
+
return
|
|
901
971
|
}
|
|
902
972
|
|
|
903
|
-
this.
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// Use GPU renderer (WebGPU or WebGL2) if available
|
|
907
|
-
if (this._gpuRenderer) {
|
|
908
|
-
this._renderGPU(data)
|
|
909
|
-
return
|
|
910
|
-
}
|
|
973
|
+
if (!this._ctx) return
|
|
911
974
|
|
|
912
|
-
|
|
975
|
+
const ctx = this._ctx
|
|
976
|
+
const images = data.images
|
|
977
|
+
const imageCount = images.length
|
|
913
978
|
|
|
914
|
-
|
|
915
|
-
const images = data.images
|
|
916
|
-
const imageCount = images.length
|
|
979
|
+
ctx.clearRect(0, 0, dataWidth, dataHeight)
|
|
917
980
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
981
|
+
if (data.asyncRender) {
|
|
982
|
+
for (let i = 0; i < imageCount; i++) {
|
|
983
|
+
const image = images[i]
|
|
984
|
+
if (image.image) {
|
|
985
|
+
ctx.drawImage(image.image as ImageBitmap, image.x, image.y)
|
|
986
|
+
;(image.image as ImageBitmap).close()
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
} else {
|
|
990
|
+
const hasAlphaBug = AkariSub._hasAlphaBug ?? false
|
|
991
|
+
|
|
992
|
+
for (let i = 0; i < imageCount; i++) {
|
|
993
|
+
const image = images[i]
|
|
994
|
+
if (image.image) {
|
|
995
|
+
const imgW = image.w
|
|
996
|
+
const imgH = image.h
|
|
997
|
+
|
|
998
|
+
const rawImage = image.image
|
|
999
|
+
const rawData = rawImage instanceof Uint8ClampedArray
|
|
1000
|
+
? rawImage
|
|
1001
|
+
: rawImage instanceof Uint8Array
|
|
1002
|
+
? new Uint8ClampedArray(rawImage.buffer, rawImage.byteOffset, rawImage.byteLength)
|
|
1003
|
+
: new Uint8ClampedArray(rawImage as ArrayBuffer)
|
|
1004
|
+
const fixedData = fixAlpha(rawData, hasAlphaBug)
|
|
1005
|
+
ctx.putImageData(new ImageData(fixedData as Uint8ClampedArray<ArrayBuffer>, imgW, imgH), image.x, image.y)
|
|
1006
|
+
}
|
|
926
1007
|
}
|
|
927
1008
|
}
|
|
928
|
-
} else {
|
|
929
|
-
const hasAlphaBug = AkariSub._hasAlphaBug ?? false
|
|
930
1009
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1010
|
+
if (this.debug) {
|
|
1011
|
+
data.times.JSRenderTime = Date.now() - (data.times.JSRenderTime || 0) - (data.times.IPCTime || 0)
|
|
1012
|
+
let total = 0
|
|
1013
|
+
const count = data.times.bitmaps || imageCount
|
|
1014
|
+
delete data.times.bitmaps
|
|
936
1015
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
ctx.putImageData(new ImageData(fixedData as Uint8ClampedArray<ArrayBuffer>, imgW, imgH), image.x, image.y)
|
|
1016
|
+
for (const key in data.times) {
|
|
1017
|
+
total += (data.times as any)[key] || 0
|
|
940
1018
|
}
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
if (this.debug) {
|
|
945
|
-
data.times.JSRenderTime = Date.now() - (data.times.JSRenderTime || 0) - (data.times.IPCTime || 0)
|
|
946
|
-
let total = 0
|
|
947
|
-
const count = data.times.bitmaps || imageCount
|
|
948
|
-
delete data.times.bitmaps
|
|
949
1019
|
|
|
950
|
-
|
|
951
|
-
total += (data.times as any)[key] || 0
|
|
1020
|
+
console.log('Bitmaps: ' + count + ' Total: ' + (total | 0) + 'ms', data.times)
|
|
952
1021
|
}
|
|
953
|
-
|
|
954
|
-
|
|
1022
|
+
} finally {
|
|
1023
|
+
this._unbusy()
|
|
955
1024
|
}
|
|
956
1025
|
}
|
|
957
1026
|
|
|
@@ -966,13 +1035,21 @@ export default class AkariSub extends EventTarget {
|
|
|
966
1035
|
|
|
967
1036
|
if (data.asyncRender) {
|
|
968
1037
|
// For async render mode with ImageBitmaps
|
|
969
|
-
const bitmapImages =
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1038
|
+
const bitmapImages = this._gpuBitmapImages
|
|
1039
|
+
let bitmapCount = 0
|
|
1040
|
+
|
|
1041
|
+
for (let i = 0; i < data.images.length; i++) {
|
|
1042
|
+
const img = data.images[i]
|
|
1043
|
+
if (!(img.image instanceof ImageBitmap)) continue
|
|
1044
|
+
|
|
1045
|
+
const target = bitmapImages[bitmapCount] || (bitmapImages[bitmapCount] = { image: img.image, x: 0, y: 0 })
|
|
1046
|
+
target.image = img.image
|
|
1047
|
+
target.x = img.x
|
|
1048
|
+
target.y = img.y
|
|
1049
|
+
bitmapCount++
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
bitmapImages.length = bitmapCount
|
|
976
1053
|
|
|
977
1054
|
renderer.renderBitmaps(bitmapImages, this._canvasctrl.width, this._canvasctrl.height)
|
|
978
1055
|
|
package/src/ts/types.ts
CHANGED
|
@@ -186,6 +186,8 @@ export interface AkariSubOptions {
|
|
|
186
186
|
onCanvasFallback?: () => void
|
|
187
187
|
/** Additional time in seconds to render subtitles ahead for pipeline latency compensation (default: 0.008) */
|
|
188
188
|
renderAhead?: number
|
|
189
|
+
/** Pre-render early track windows after load to warm libass caches (default: false) */
|
|
190
|
+
fullTrackWarmup?: boolean
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
// =============================================================================
|
|
@@ -211,7 +213,7 @@ export interface RenderImage {
|
|
|
211
213
|
y: number
|
|
212
214
|
w: number
|
|
213
215
|
h: number
|
|
214
|
-
image: ImageBitmap | ArrayBuffer | number
|
|
216
|
+
image: ImageBitmap | ArrayBuffer | Uint8Array | Uint8ClampedArray | number
|
|
215
217
|
}
|
|
216
218
|
|
|
217
219
|
/** Render timing debug info */
|
|
@@ -255,6 +257,7 @@ export interface WorkerInitMessage {
|
|
|
255
257
|
target: 'init'
|
|
256
258
|
wasmUrl: string
|
|
257
259
|
asyncRender: boolean
|
|
260
|
+
fullTrackWarmup: boolean
|
|
258
261
|
onDemandRender: boolean
|
|
259
262
|
initialTime: number
|
|
260
263
|
width: number
|
|
@@ -363,11 +366,6 @@ export interface AkariSubModule extends EmscriptenModule {
|
|
|
363
366
|
_akarisub_remove_style: (handle: number, index: number) => void
|
|
364
367
|
_akarisub_style_override_index: (handle: number, index: number) => void
|
|
365
368
|
_akarisub_disable_style_override: (handle: number) => void
|
|
366
|
-
_akarisub_render_blend: (handle: number, time: number, force: number) => number
|
|
367
|
-
_akarisub_render_image: (handle: number, time: number, force: number) => number
|
|
368
|
-
_akarisub_get_changed: (handle: number) => number
|
|
369
|
-
_akarisub_get_count: (handle: number) => number
|
|
370
|
-
_akarisub_get_time: (handle: number) => number
|
|
371
369
|
_akarisub_get_track_color_space: (handle: number) => number
|
|
372
370
|
_akarisub_event_get_int: (handle: number, index: number, field: number) => number
|
|
373
371
|
_akarisub_event_set_int: (handle: number, index: number, field: number, value: number) => void
|
|
@@ -377,13 +375,6 @@ export interface AkariSubModule extends EmscriptenModule {
|
|
|
377
375
|
_akarisub_style_set_num: (handle: number, index: number, field: number, value: number) => void
|
|
378
376
|
_akarisub_style_get_str: (handle: number, index: number, field: number) => number
|
|
379
377
|
_akarisub_style_set_str: (handle: number, index: number, field: number, valuePtr: number) => void
|
|
380
|
-
_akarisub_render_result_x: (resultPtr: number) => number
|
|
381
|
-
_akarisub_render_result_y: (resultPtr: number) => number
|
|
382
|
-
_akarisub_render_result_w: (resultPtr: number) => number
|
|
383
|
-
_akarisub_render_result_h: (resultPtr: number) => number
|
|
384
|
-
_akarisub_render_result_image: (resultPtr: number) => number
|
|
385
|
-
_akarisub_render_result_next: (resultPtr: number) => number
|
|
386
|
-
_akarisub_render_result_collect: (resultPtr: number, outPtr: number, maxItems: number) => number
|
|
387
378
|
_akarisub_render_blend_collect: (handle: number, time: number, force: number, outPtr: number, maxItems: number) => number
|
|
388
379
|
_akarisub_render_image_collect: (handle: number, time: number, force: number, outPtr: number, maxItems: number) => number
|
|
389
380
|
FS_createPath: (parent: string, path: string, canRead: boolean, canWrite: boolean) => void
|
|
@@ -75,6 +75,10 @@ export function isWebGL2Supported(): boolean {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function isArrayBufferView(value: unknown): value is Uint8Array | Uint8ClampedArray {
|
|
79
|
+
return value instanceof Uint8Array || value instanceof Uint8ClampedArray
|
|
80
|
+
}
|
|
81
|
+
|
|
78
82
|
function compileShader(gl: WebGL2RenderingContext, type: number, source: string): WebGLShader {
|
|
79
83
|
const shader = gl.createShader(type)!
|
|
80
84
|
gl.shaderSource(shader, source)
|
|
@@ -362,8 +366,9 @@ export class WebGL2Renderer {
|
|
|
362
366
|
const imgData = img.image
|
|
363
367
|
if (imgData instanceof ImageBitmap) {
|
|
364
368
|
gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, count, w, h, 1, gl.RGBA, gl.UNSIGNED_BYTE, imgData)
|
|
365
|
-
} else if (imgData instanceof ArrayBuffer) {
|
|
366
|
-
|
|
369
|
+
} else if (imgData instanceof ArrayBuffer || isArrayBufferView(imgData)) {
|
|
370
|
+
const uploadData = imgData instanceof ArrayBuffer ? new Uint8Array(imgData) : imgData
|
|
371
|
+
gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, count, w, h, 1, gl.RGBA, gl.UNSIGNED_BYTE, uploadData)
|
|
367
372
|
}
|
|
368
373
|
const off = count << 3
|
|
369
374
|
instanceData[off] = img.x
|
|
@@ -114,6 +114,14 @@ export function isWebGPUSupported(): boolean {
|
|
|
114
114
|
return typeof navigator !== 'undefined' && 'gpu' in navigator
|
|
115
115
|
}
|
|
116
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
|
+
|
|
117
125
|
/**
|
|
118
126
|
* High-performance WebGPU subtitle renderer for AkariSub.
|
|
119
127
|
*/
|
|
@@ -577,7 +585,7 @@ export class WebGPURenderer {
|
|
|
577
585
|
{ texture: textureArray, origin: [0, 0, texIndex], premultipliedAlpha: false },
|
|
578
586
|
{ width: w, height: h }
|
|
579
587
|
)
|
|
580
|
-
} else if (imgData instanceof ArrayBuffer) {
|
|
588
|
+
} else if (imgData instanceof ArrayBuffer || imgData instanceof Uint8Array || imgData instanceof Uint8ClampedArray) {
|
|
581
589
|
this.uploadTextureData(texIndex, imgData, w, h, isBGRA)
|
|
582
590
|
}
|
|
583
591
|
|
|
@@ -626,17 +634,17 @@ export class WebGPURenderer {
|
|
|
626
634
|
|
|
627
635
|
private uploadTextureData(
|
|
628
636
|
layerIndex: number,
|
|
629
|
-
rgbaBuffer: ArrayBuffer,
|
|
637
|
+
rgbaBuffer: ArrayBuffer | Uint8Array | Uint8ClampedArray,
|
|
630
638
|
width: number,
|
|
631
639
|
height: number,
|
|
632
640
|
swapRB: boolean
|
|
633
641
|
): void {
|
|
634
642
|
const size = width * height * 4
|
|
643
|
+
const src = toUint8View(rgbaBuffer)
|
|
635
644
|
|
|
636
645
|
if (swapRB) {
|
|
637
646
|
// Use reusable conversion buffer
|
|
638
647
|
const uploadData = this.ensureConversionBuffer(size)
|
|
639
|
-
const src = new Uint8Array(rgbaBuffer)
|
|
640
648
|
|
|
641
649
|
// Unrolled loop for better performance
|
|
642
650
|
for (let j = 0; j < size; j += 4) {
|
|
@@ -655,7 +663,7 @@ export class WebGPURenderer {
|
|
|
655
663
|
} else {
|
|
656
664
|
this.device!.queue.writeTexture(
|
|
657
665
|
{ texture: this.textureArray!, origin: [0, 0, layerIndex] },
|
|
658
|
-
|
|
666
|
+
src,
|
|
659
667
|
{ bytesPerRow: width * 4 },
|
|
660
668
|
{ width, height }
|
|
661
669
|
)
|