akarisub 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,415 @@
1
+ import type { RenderImage } from './types'
2
+
3
+ const MAX_IMAGES_PER_BATCH = 256
4
+ const MAX_TEXTURE_ARRAY_LAYERS = 256
5
+
6
+ // GLSL Vertex Shader (GLSL ES 3.00)
7
+ const VERTEX_SHADER = /* glsl */ `#version 300 es
8
+ precision highp float;
9
+
10
+ in vec4 a_destRect;
11
+ in vec4 a_texInfo;
12
+
13
+ uniform vec2 u_resolution;
14
+
15
+ out vec2 v_uv;
16
+ flat out int v_texIndex;
17
+ flat out vec2 v_texSize;
18
+
19
+ vec2 quadPos(int id) {
20
+ if (id == 0) return vec2(0.0, 0.0);
21
+ if (id == 1) return vec2(1.0, 0.0);
22
+ if (id == 2) return vec2(0.0, 1.0);
23
+ if (id == 3) return vec2(1.0, 0.0);
24
+ if (id == 4) return vec2(1.0, 1.0);
25
+ return vec2(0.0, 1.0);
26
+ }
27
+
28
+ void main() {
29
+ vec2 qp = quadPos(gl_VertexID);
30
+ vec2 pixelPos = a_destRect.xy + qp * a_destRect.zw;
31
+
32
+ // Convert CSS pixel coords (y=0 top) to GL clip space (y=1 top)
33
+ vec2 clip = (pixelPos / u_resolution) * 2.0 - 1.0;
34
+ clip.y = -clip.y;
35
+
36
+ gl_Position = vec4(clip, 0.0, 1.0);
37
+ v_uv = qp;
38
+ v_texIndex = int(a_texInfo.z);
39
+ v_texSize = a_texInfo.xy;
40
+ }
41
+ `
42
+
43
+ // GLSL Fragment Shader (GLSL ES 3.00)
44
+ const FRAGMENT_SHADER = /* glsl */ `#version 300 es
45
+ precision highp float;
46
+ precision highp sampler2DArray;
47
+
48
+ uniform sampler2DArray u_texArray;
49
+ uniform ivec2 u_texArraySize;
50
+
51
+ in vec2 v_uv;
52
+ flat in int v_texIndex;
53
+ flat in vec2 v_texSize;
54
+
55
+ out vec4 fragColor;
56
+
57
+ void main() {
58
+ vec2 normalizedCoord = v_uv * v_texSize / vec2(u_texArraySize);
59
+ vec4 color = texture(u_texArray, vec3(normalizedCoord, float(v_texIndex)));
60
+ // Premultiplied alpha output (matches WebGPU renderer behaviour)
61
+ fragColor = vec4(color.rgb * color.a, color.a);
62
+ }
63
+ `
64
+
65
+ /**
66
+ * Check if WebGL2 is supported in the current browser.
67
+ */
68
+ export function isWebGL2Supported(): boolean {
69
+ if (typeof document === 'undefined') return false
70
+ try {
71
+ const canvas = document.createElement('canvas')
72
+ return canvas.getContext('webgl2') !== null
73
+ } catch {
74
+ return false
75
+ }
76
+ }
77
+
78
+ function compileShader(gl: WebGL2RenderingContext, type: number, source: string): WebGLShader {
79
+ const shader = gl.createShader(type)!
80
+ gl.shaderSource(shader, source)
81
+ gl.compileShader(shader)
82
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
83
+ const info = gl.getShaderInfoLog(shader)
84
+ gl.deleteShader(shader)
85
+ throw new Error(`WebGL2 shader compilation failed: ${info}`)
86
+ }
87
+ return shader
88
+ }
89
+
90
+ /**
91
+ * High-performance WebGL2 subtitle renderer for AkariSub.
92
+ */
93
+ export class WebGL2Renderer {
94
+ private _gl: WebGL2RenderingContext | null = null
95
+ private _canvas: HTMLCanvasElement | null = null
96
+ private _program: WebGLProgram | null = null
97
+ private _vao: WebGLVertexArrayObject | null = null
98
+ private _instanceBuffer: WebGLBuffer | null = null
99
+ private _texArray: WebGLTexture | null = null
100
+
101
+ private _texWidth = 0
102
+ private _texHeight = 0
103
+ private _texLayers = 0
104
+
105
+ private _resolutionLoc: WebGLUniformLocation | null = null
106
+ private _texArraySizeLoc: WebGLUniformLocation | null = null
107
+ private readonly _instanceData: Float32Array
108
+
109
+ private _lastCanvasWidth = 0
110
+ private _lastCanvasHeight = 0
111
+ private _initialized = false
112
+ private _initPromise: Promise<void> | null = null
113
+
114
+ constructor() {
115
+ this._instanceData = new Float32Array(MAX_IMAGES_PER_BATCH * 8)
116
+ }
117
+
118
+ async init(): Promise<void> {
119
+ if (this._initPromise) return this._initPromise
120
+ this._initPromise = this._checkSupport()
121
+ return this._initPromise
122
+ }
123
+
124
+ private async _checkSupport(): Promise<void> {
125
+ if (typeof document === 'undefined') throw new Error('WebGL2 requires a DOM environment')
126
+ const canvas = document.createElement('canvas')
127
+ if (!canvas.getContext('webgl2')) throw new Error('WebGL2 not supported')
128
+ }
129
+
130
+ private _initGL(): void {
131
+ if (!this._canvas) throw new Error('Canvas not set before _initGL')
132
+ if (this._gl) return // already initialised
133
+
134
+ const gl = this._canvas.getContext('webgl2', { alpha: true, premultipliedAlpha: true, antialias: false })
135
+ if (!gl) throw new Error('Failed to create WebGL2 context')
136
+ this._gl = gl
137
+
138
+ // Compile and link program
139
+ const vert = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER)
140
+ const frag = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER)
141
+ const program = gl.createProgram()!
142
+ gl.attachShader(program, vert)
143
+ gl.attachShader(program, frag)
144
+ gl.linkProgram(program)
145
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
146
+ throw new Error(`WebGL2 program link failed: ${gl.getProgramInfoLog(program)}`)
147
+ }
148
+ gl.deleteShader(vert)
149
+ gl.deleteShader(frag)
150
+ this._program = program
151
+
152
+ this._resolutionLoc = gl.getUniformLocation(program, 'u_resolution')
153
+ this._texArraySizeLoc = gl.getUniformLocation(program, 'u_texArraySize')
154
+
155
+ // VAO + instance VBO
156
+ this._vao = gl.createVertexArray()!
157
+ gl.bindVertexArray(this._vao)
158
+
159
+ this._instanceBuffer = gl.createBuffer()!
160
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._instanceBuffer)
161
+ // Size: MAX_IMAGES * 8 floats * 4 bytes
162
+ gl.bufferData(gl.ARRAY_BUFFER, MAX_IMAGES_PER_BATCH * 32, gl.DYNAMIC_DRAW)
163
+
164
+ // stride = 32 bytes (8 × float)
165
+ const aDestRect = gl.getAttribLocation(program, 'a_destRect')
166
+ gl.enableVertexAttribArray(aDestRect)
167
+ gl.vertexAttribPointer(aDestRect, 4, gl.FLOAT, false, 32, 0)
168
+ gl.vertexAttribDivisor(aDestRect, 1)
169
+
170
+ const aTexInfo = gl.getAttribLocation(program, 'a_texInfo')
171
+ gl.enableVertexAttribArray(aTexInfo)
172
+ gl.vertexAttribPointer(aTexInfo, 4, gl.FLOAT, false, 32, 16)
173
+ gl.vertexAttribDivisor(aTexInfo, 1)
174
+
175
+ gl.bindVertexArray(null)
176
+
177
+ // Texture array
178
+ this._texArray = gl.createTexture()!
179
+ this._allocateTextureArray(256, 256, 32)
180
+
181
+ // Premultiplied-alpha blending
182
+ gl.enable(gl.BLEND)
183
+ gl.blendEquation(gl.FUNC_ADD)
184
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
185
+
186
+ this._initialized = true
187
+ }
188
+
189
+ // ==========================================================================
190
+ // Texture management
191
+ // ==========================================================================
192
+
193
+ private _nextPow2(n: number): number {
194
+ n--; n |= n >> 1; n |= n >> 2; n |= n >> 4; n |= n >> 8; n |= n >> 16; return n + 1
195
+ }
196
+
197
+ private _allocateTextureArray(width: number, height: number, layers: number): void {
198
+ const gl = this._gl!
199
+ const w = this._nextPow2(Math.max(width, 64))
200
+ const h = this._nextPow2(Math.max(height, 64))
201
+ const l = Math.min(this._nextPow2(Math.max(layers, 16)), MAX_TEXTURE_ARRAY_LAYERS)
202
+
203
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this._texArray)
204
+ gl.texImage3D(gl.TEXTURE_2D_ARRAY, 0, gl.RGBA8, w, h, l, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)
205
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
206
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
207
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
208
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
209
+
210
+ this._texWidth = w
211
+ this._texHeight = h
212
+ this._texLayers = l
213
+ }
214
+
215
+ private _ensureTextureArray(maxW: number, maxH: number, count: number): void {
216
+ const c = Math.min(count, MAX_TEXTURE_ARRAY_LAYERS)
217
+ if (maxW <= this._texWidth && maxH <= this._texHeight && c <= this._texLayers) return
218
+ const newW = this._nextPow2(Math.max(this._texWidth, maxW))
219
+ const newH = this._nextPow2(Math.max(this._texHeight, maxH))
220
+ const newL = Math.min(
221
+ this._nextPow2(Math.max(this._texLayers, c, c + 16)),
222
+ MAX_TEXTURE_ARRAY_LAYERS
223
+ )
224
+ this._allocateTextureArray(newW, newH, newL)
225
+ }
226
+
227
+ // ==========================================================================
228
+ // Public interface
229
+ // ==========================================================================
230
+
231
+ async setCanvas(canvas: HTMLCanvasElement, width: number, height: number): Promise<void> {
232
+ await this.init()
233
+ if (width <= 0 || height <= 0) return
234
+ this._canvas = canvas
235
+ canvas.width = width
236
+ canvas.height = height
237
+ this._initGL()
238
+ this._gl!.viewport(0, 0, width, height)
239
+ this._lastCanvasWidth = width
240
+ this._lastCanvasHeight = height
241
+ }
242
+
243
+ updateSize(width: number, height: number): void {
244
+ if (!this._gl || !this._canvas || width <= 0 || height <= 0) return
245
+ if (width === this._lastCanvasWidth && height === this._lastCanvasHeight) return
246
+ this._canvas.width = width
247
+ this._canvas.height = height
248
+ this._gl.viewport(0, 0, width, height)
249
+ this._lastCanvasWidth = width
250
+ this._lastCanvasHeight = height
251
+ }
252
+
253
+ /**
254
+ * Render from ImageBitmaps (async render mode)
255
+ */
256
+ renderBitmaps(
257
+ images: { image: ImageBitmap; x: number; y: number }[],
258
+ _canvasWidth: number,
259
+ _canvasHeight: number
260
+ ): void {
261
+ if (!this._gl || !this._initialized) return
262
+
263
+ const len = images.length
264
+ if (len === 0) {
265
+ this.clear()
266
+ return
267
+ }
268
+
269
+ let maxW = 0, maxH = 0
270
+ for (let i = 0; i < len; i++) {
271
+ const { image } = images[i]
272
+ if (image.width > maxW) maxW = image.width
273
+ if (image.height > maxH) maxH = image.height
274
+ }
275
+
276
+ this._ensureTextureArray(maxW, maxH, Math.min(len, MAX_TEXTURE_ARRAY_LAYERS))
277
+
278
+ const gl = this._gl
279
+ gl.clearColor(0, 0, 0, 0)
280
+ gl.clear(gl.COLOR_BUFFER_BIT)
281
+ gl.useProgram(this._program)
282
+ gl.uniform2f(this._resolutionLoc, this._lastCanvasWidth, this._lastCanvasHeight)
283
+ gl.uniform2i(this._texArraySizeLoc, this._texWidth, this._texHeight)
284
+ gl.activeTexture(gl.TEXTURE0)
285
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this._texArray)
286
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false)
287
+
288
+ const instanceData = this._instanceData
289
+ let imageIndex = 0
290
+
291
+ while (imageIndex < len) {
292
+ let count = 0
293
+ while (imageIndex < len && count < MAX_TEXTURE_ARRAY_LAYERS) {
294
+ const img = images[imageIndex++]
295
+ const w = img.image.width, h = img.image.height
296
+ if (w <= 0 || h <= 0) continue
297
+ gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, count, w, h, 1, gl.RGBA, gl.UNSIGNED_BYTE, img.image)
298
+ const off = count << 3
299
+ instanceData[off] = img.x
300
+ instanceData[off + 1] = img.y
301
+ instanceData[off + 2] = w
302
+ instanceData[off + 3] = h
303
+ instanceData[off + 4] = w
304
+ instanceData[off + 5] = h
305
+ instanceData[off + 6] = count
306
+ instanceData[off + 7] = 0
307
+ count++
308
+ }
309
+ if (count === 0) continue
310
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._instanceBuffer)
311
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, instanceData, 0, count << 3)
312
+ gl.bindVertexArray(this._vao)
313
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count)
314
+ gl.bindVertexArray(null)
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Render from raw ArrayBuffer data (non-async render mode)
320
+ */
321
+ render(
322
+ images: RenderImage[],
323
+ _canvasWidth: number,
324
+ _canvasHeight: number
325
+ ): void {
326
+ if (!this._gl || !this._initialized) return
327
+
328
+ const len = images.length
329
+ if (len === 0) {
330
+ this.clear()
331
+ return
332
+ }
333
+
334
+ let maxW = 0, maxH = 0
335
+ for (let i = 0; i < len; i++) {
336
+ const { w, h } = images[i]
337
+ if (w > maxW) maxW = w
338
+ if (h > maxH) maxH = h
339
+ }
340
+
341
+ this._ensureTextureArray(maxW, maxH, Math.min(len, MAX_TEXTURE_ARRAY_LAYERS))
342
+
343
+ const gl = this._gl
344
+ gl.clearColor(0, 0, 0, 0)
345
+ gl.clear(gl.COLOR_BUFFER_BIT)
346
+ gl.useProgram(this._program)
347
+ gl.uniform2f(this._resolutionLoc, this._lastCanvasWidth, this._lastCanvasHeight)
348
+ gl.uniform2i(this._texArraySizeLoc, this._texWidth, this._texHeight)
349
+ gl.activeTexture(gl.TEXTURE0)
350
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, this._texArray)
351
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false)
352
+
353
+ const instanceData = this._instanceData
354
+ let imageIndex = 0
355
+
356
+ while (imageIndex < len) {
357
+ let count = 0
358
+ while (imageIndex < len && count < MAX_TEXTURE_ARRAY_LAYERS) {
359
+ const img = images[imageIndex++]
360
+ const w = img.w, h = img.h
361
+ if (w <= 0 || h <= 0) continue
362
+ const imgData = img.image
363
+ if (imgData instanceof ImageBitmap) {
364
+ 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
+ gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, count, w, h, 1, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(imgData))
367
+ }
368
+ const off = count << 3
369
+ instanceData[off] = img.x
370
+ instanceData[off + 1] = img.y
371
+ instanceData[off + 2] = w
372
+ instanceData[off + 3] = h
373
+ instanceData[off + 4] = w
374
+ instanceData[off + 5] = h
375
+ instanceData[off + 6] = count
376
+ instanceData[off + 7] = 0
377
+ count++
378
+ }
379
+ if (count === 0) continue
380
+ gl.bindBuffer(gl.ARRAY_BUFFER, this._instanceBuffer)
381
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, instanceData, 0, count << 3)
382
+ gl.bindVertexArray(this._vao)
383
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count)
384
+ gl.bindVertexArray(null)
385
+ }
386
+ }
387
+
388
+ clear(): void {
389
+ if (!this._gl) return
390
+ this._gl.clearColor(0, 0, 0, 0)
391
+ this._gl.clear(this._gl.COLOR_BUFFER_BIT)
392
+ }
393
+
394
+ get initialized(): boolean {
395
+ return this._initialized
396
+ }
397
+
398
+ destroy(): void {
399
+ const gl = this._gl
400
+ if (gl) {
401
+ gl.deleteProgram(this._program)
402
+ gl.deleteVertexArray(this._vao)
403
+ gl.deleteBuffer(this._instanceBuffer)
404
+ gl.deleteTexture(this._texArray)
405
+ }
406
+ this._gl = null
407
+ this._program = null
408
+ this._vao = null
409
+ this._instanceBuffer = null
410
+ this._texArray = null
411
+ this._canvas = null
412
+ this._initialized = false
413
+ this._initPromise = null
414
+ }
415
+ }