jassub 2.3.3 → 2.4.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 +3 -3
- package/dist/worker/renderers/2d-renderer.d.ts +16 -0
- package/dist/worker/renderers/2d-renderer.js +71 -0
- package/dist/worker/renderers/2d-renderer.js.map +1 -0
- package/dist/worker/renderers/webgl1-renderer.d.ts +38 -0
- package/dist/worker/renderers/webgl1-renderer.js +363 -0
- package/dist/worker/renderers/webgl1-renderer.js.map +1 -0
- package/dist/worker/renderers/webgl2-renderer.d.ts +32 -0
- package/dist/worker/renderers/webgl2-renderer.js +361 -0
- package/dist/worker/renderers/webgl2-renderer.js.map +1 -0
- package/dist/worker/renderers/webgpu-renderer.d.ts +50 -0
- package/dist/worker/renderers/webgpu-renderer.js +404 -0
- package/dist/worker/renderers/webgpu-renderer.js.map +1 -0
- package/dist/worker/util.d.ts +23 -0
- package/dist/worker/util.js +57 -0
- package/dist/worker/util.js.map +1 -1
- package/dist/worker/worker.d.ts +4 -2
- package/dist/worker/worker.js +19 -5
- package/dist/worker/worker.js.map +1 -1
- package/package.json +1 -1
- package/src/worker/renderers/2d-renderer.ts +81 -0
- package/src/worker/renderers/webgl1-renderer.ts +445 -0
- package/src/worker/{webgl-renderer.ts → renderers/webgl2-renderer.ts} +2 -65
- package/src/worker/{webgpu-renderer.ts → renderers/webgpu-renderer.ts} +1 -1
- package/src/worker/util.ts +64 -0
- package/src/worker/worker.ts +17 -5
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// fallback for browsers that don't support GPU acceleration
|
|
2
|
+
import type { ASSImage } from '../util.ts'
|
|
3
|
+
|
|
4
|
+
export class Canvas2DRenderer {
|
|
5
|
+
canvas: OffscreenCanvas | null = null
|
|
6
|
+
ctx: OffscreenCanvasRenderingContext2D | null = null
|
|
7
|
+
bufferCanvas = new OffscreenCanvas(1, 1)
|
|
8
|
+
bufferCtx = this.bufferCanvas.getContext('2d', {
|
|
9
|
+
alpha: true,
|
|
10
|
+
desynchronized: true,
|
|
11
|
+
willReadFrequently: false
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
_scheduledResize?: { width: number, height: number }
|
|
15
|
+
|
|
16
|
+
resizeCanvas (width: number, height: number) {
|
|
17
|
+
if (width <= 0 || height <= 0) return
|
|
18
|
+
|
|
19
|
+
this._scheduledResize = { width, height }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setCanvas (canvas: OffscreenCanvas) {
|
|
23
|
+
this.canvas = canvas
|
|
24
|
+
this.ctx = canvas.getContext('2d', {
|
|
25
|
+
alpha: true,
|
|
26
|
+
desynchronized: true,
|
|
27
|
+
willReadFrequently: false
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
if (!this.ctx) throw new Error('Could not get 2D context')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setColorMatrix (subtitleColorSpace?: 'BT601' | 'BT709' | 'SMPTE240M' | 'FCC', videoColorSpace?: 'BT601' | 'BT709') {}
|
|
34
|
+
|
|
35
|
+
// this is horribly inefficient, but it's a fallback for systems without a GPU, this is the least of their problems
|
|
36
|
+
render (images: ASSImage[], heap: Uint8Array): void {
|
|
37
|
+
if (!this.ctx || !this.canvas) return
|
|
38
|
+
|
|
39
|
+
if (this._scheduledResize) {
|
|
40
|
+
const { width, height } = this._scheduledResize
|
|
41
|
+
this._scheduledResize = undefined
|
|
42
|
+
this.canvas.width = width
|
|
43
|
+
this.canvas.height = height
|
|
44
|
+
} else {
|
|
45
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const img of images) {
|
|
49
|
+
if (img.w <= 0 || img.h <= 0) continue
|
|
50
|
+
const imageData = new ImageData(img.w, img.h)
|
|
51
|
+
const pixels = new Uint32Array(imageData.data.buffer)
|
|
52
|
+
|
|
53
|
+
const color = ((img.color << 8) & 0xff0000) | ((img.color >> 8) & 0xff00) | ((img.color >> 24) & 0xff)
|
|
54
|
+
const alpha = (255 - (img.color & 255)) / 255
|
|
55
|
+
|
|
56
|
+
const stride = img.stride
|
|
57
|
+
const h = img.h
|
|
58
|
+
const w = img.w
|
|
59
|
+
|
|
60
|
+
for (let y = h + 1, pos = img.bitmap, res = 0; --y; pos += stride) {
|
|
61
|
+
for (let z = 0; z < w; ++z, ++res) {
|
|
62
|
+
const k = heap[pos + z]!
|
|
63
|
+
if (k !== 0) pixels[res] = ((alpha * k) << 24) | color
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Draw the ImageData to canvas at the destination position
|
|
68
|
+
this.bufferCanvas.width = w
|
|
69
|
+
this.bufferCanvas.height = h
|
|
70
|
+
this.bufferCtx!.putImageData(imageData, 0, 0)
|
|
71
|
+
this.ctx.drawImage(this.bufferCanvas, img.dst_x, img.dst_y)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
destroy () {
|
|
76
|
+
this.ctx = null
|
|
77
|
+
this.canvas = null
|
|
78
|
+
this.bufferCtx = null!
|
|
79
|
+
this.bufferCanvas = null!
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { colorMatrixConversionMap, IDENTITY_MATRIX, type ASSImage } from '../util.ts'
|
|
2
|
+
|
|
3
|
+
// GLSL ES 1.0 Vertex Shader with Instancing (using extension)
|
|
4
|
+
const VERTEX_SHADER = /* glsl */`
|
|
5
|
+
precision mediump float;
|
|
6
|
+
|
|
7
|
+
// Quad position attribute (0,0), (1,0), (0,1), (1,0), (1,1), (0,1)
|
|
8
|
+
attribute vec2 a_quadPos;
|
|
9
|
+
|
|
10
|
+
uniform vec2 u_resolution;
|
|
11
|
+
|
|
12
|
+
// Instance attributes
|
|
13
|
+
attribute vec4 a_destRect; // x, y, w, h
|
|
14
|
+
attribute vec4 a_color; // r, g, b, a
|
|
15
|
+
attribute float a_texLayer;
|
|
16
|
+
|
|
17
|
+
varying vec2 v_destXY;
|
|
18
|
+
varying vec4 v_color;
|
|
19
|
+
varying vec2 v_texSize;
|
|
20
|
+
varying float v_texLayer;
|
|
21
|
+
varying vec2 v_texCoord;
|
|
22
|
+
|
|
23
|
+
void main() {
|
|
24
|
+
vec2 pixelPos = a_destRect.xy + a_quadPos * a_destRect.zw;
|
|
25
|
+
vec2 clipPos = (pixelPos / u_resolution) * 2.0 - 1.0;
|
|
26
|
+
clipPos.y = -clipPos.y;
|
|
27
|
+
|
|
28
|
+
gl_Position = vec4(clipPos, 0.0, 1.0);
|
|
29
|
+
v_destXY = a_destRect.xy;
|
|
30
|
+
v_color = a_color;
|
|
31
|
+
v_texSize = a_destRect.zw;
|
|
32
|
+
v_texLayer = a_texLayer;
|
|
33
|
+
v_texCoord = a_quadPos;
|
|
34
|
+
}
|
|
35
|
+
`
|
|
36
|
+
|
|
37
|
+
// GLSL ES 1.0 Fragment Shader
|
|
38
|
+
// WebGL1 doesn't support texture arrays or texelFetch, so we use individual textures
|
|
39
|
+
const FRAGMENT_SHADER = /* glsl */`
|
|
40
|
+
precision mediump float;
|
|
41
|
+
|
|
42
|
+
uniform sampler2D u_tex;
|
|
43
|
+
uniform mat3 u_colorMatrix;
|
|
44
|
+
uniform vec2 u_resolution;
|
|
45
|
+
uniform vec2 u_texDimensions; // Actual texture dimensions
|
|
46
|
+
|
|
47
|
+
varying vec2 v_destXY;
|
|
48
|
+
varying vec4 v_color;
|
|
49
|
+
varying vec2 v_texSize;
|
|
50
|
+
varying float v_texLayer;
|
|
51
|
+
varying vec2 v_texCoord;
|
|
52
|
+
|
|
53
|
+
void main() {
|
|
54
|
+
// v_texCoord is in 0-1 range for the quad
|
|
55
|
+
// We need to map it to the actual image size within the texture
|
|
56
|
+
// The image occupies only (v_texSize.x / u_texDimensions.x, v_texSize.y / u_texDimensions.y) of the texture
|
|
57
|
+
vec2 normalizedImageSize = v_texSize / u_texDimensions;
|
|
58
|
+
vec2 texCoord = v_texCoord * normalizedImageSize;
|
|
59
|
+
|
|
60
|
+
// Sample texture (r channel contains mask)
|
|
61
|
+
float mask = texture2D(u_tex, texCoord).r;
|
|
62
|
+
|
|
63
|
+
// Apply color matrix conversion (identity if no conversion needed)
|
|
64
|
+
vec3 correctedColor = u_colorMatrix * v_color.rgb;
|
|
65
|
+
|
|
66
|
+
// libass color alpha: 0 = opaque, 255 = transparent (inverted)
|
|
67
|
+
float colorAlpha = 1.0 - v_color.a;
|
|
68
|
+
|
|
69
|
+
// Final alpha = colorAlpha * mask
|
|
70
|
+
float a = colorAlpha * mask;
|
|
71
|
+
|
|
72
|
+
// Premultiplied alpha output
|
|
73
|
+
gl_FragColor = vec4(correctedColor * a, a);
|
|
74
|
+
}
|
|
75
|
+
`
|
|
76
|
+
|
|
77
|
+
// Configuration
|
|
78
|
+
const MAX_INSTANCES = 256 // Maximum instances per draw call
|
|
79
|
+
|
|
80
|
+
export class WebGL1Renderer {
|
|
81
|
+
canvas: OffscreenCanvas | null = null
|
|
82
|
+
gl: WebGLRenderingContext | null = null
|
|
83
|
+
program: WebGLProgram | null = null
|
|
84
|
+
|
|
85
|
+
// Extensions
|
|
86
|
+
instancedArraysExt: ANGLE_instanced_arrays | null = null
|
|
87
|
+
|
|
88
|
+
// Uniform locations
|
|
89
|
+
u_resolution: WebGLUniformLocation | null = null
|
|
90
|
+
u_tex: WebGLUniformLocation | null = null
|
|
91
|
+
u_colorMatrix: WebGLUniformLocation | null = null
|
|
92
|
+
u_texDimensions: WebGLUniformLocation | null = null
|
|
93
|
+
|
|
94
|
+
// Attribute locations
|
|
95
|
+
a_quadPos = -1
|
|
96
|
+
a_destRect = -1
|
|
97
|
+
a_color = -1
|
|
98
|
+
a_texLayer = -1
|
|
99
|
+
|
|
100
|
+
// Quad vertex buffer (shared for all instances)
|
|
101
|
+
quadPosBuffer: WebGLBuffer | null = null
|
|
102
|
+
|
|
103
|
+
// Instance attribute buffers
|
|
104
|
+
instanceDestRectBuffer: WebGLBuffer | null = null
|
|
105
|
+
instanceColorBuffer: WebGLBuffer | null = null
|
|
106
|
+
instanceTexLayerBuffer: WebGLBuffer | null = null
|
|
107
|
+
|
|
108
|
+
// Instance data arrays
|
|
109
|
+
instanceDestRectData: Float32Array
|
|
110
|
+
instanceColorData: Float32Array
|
|
111
|
+
instanceTexLayerData: Float32Array
|
|
112
|
+
|
|
113
|
+
// Texture cache (since WebGL1 doesn't support texture arrays)
|
|
114
|
+
textureCache = new Map<number, WebGLTexture>()
|
|
115
|
+
textureWidth = 0
|
|
116
|
+
textureHeight = 0
|
|
117
|
+
|
|
118
|
+
colorMatrix: Float32Array = IDENTITY_MATRIX
|
|
119
|
+
|
|
120
|
+
constructor () {
|
|
121
|
+
this.instanceDestRectData = new Float32Array(MAX_INSTANCES * 4)
|
|
122
|
+
this.instanceColorData = new Float32Array(MAX_INSTANCES * 4)
|
|
123
|
+
this.instanceTexLayerData = new Float32Array(MAX_INSTANCES)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_scheduledResize?: { width: number, height: number }
|
|
127
|
+
|
|
128
|
+
resizeCanvas (width: number, height: number) {
|
|
129
|
+
// WebGL doesn't allow 0-sized canvases
|
|
130
|
+
if (width <= 0 || height <= 0) return
|
|
131
|
+
|
|
132
|
+
this._scheduledResize = { width, height }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
setCanvas (canvas: OffscreenCanvas) {
|
|
136
|
+
this.canvas = canvas
|
|
137
|
+
this.gl = canvas.getContext('webgl', {
|
|
138
|
+
alpha: true,
|
|
139
|
+
premultipliedAlpha: true,
|
|
140
|
+
antialias: false,
|
|
141
|
+
depth: false,
|
|
142
|
+
preserveDrawingBuffer: false,
|
|
143
|
+
stencil: false,
|
|
144
|
+
desynchronized: true,
|
|
145
|
+
powerPreference: 'high-performance'
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
if (!this.gl) {
|
|
149
|
+
throw new Error('Could not get WebGL context')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Get instanced arrays extension (required for instancing in WebGL1)
|
|
153
|
+
this.instancedArraysExt = this.gl.getExtension('ANGLE_instanced_arrays')
|
|
154
|
+
if (!this.instancedArraysExt) {
|
|
155
|
+
throw new Error('ANGLE_instanced_arrays extension not supported')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Create shaders
|
|
159
|
+
const vertexShader = this.createShader(this.gl.VERTEX_SHADER, VERTEX_SHADER)
|
|
160
|
+
const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, FRAGMENT_SHADER)
|
|
161
|
+
|
|
162
|
+
if (!vertexShader || !fragmentShader) {
|
|
163
|
+
throw new Error('Failed to create shaders')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Create program
|
|
167
|
+
this.program = this.gl.createProgram()!
|
|
168
|
+
this.gl.attachShader(this.program, vertexShader)
|
|
169
|
+
this.gl.attachShader(this.program, fragmentShader)
|
|
170
|
+
this.gl.linkProgram(this.program)
|
|
171
|
+
|
|
172
|
+
if (!this.gl.getProgramParameter(this.program, this.gl.LINK_STATUS)) {
|
|
173
|
+
const info = this.gl.getProgramInfoLog(this.program)
|
|
174
|
+
throw new Error('Failed to link program: ' + info)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Get uniform locations
|
|
178
|
+
this.u_resolution = this.gl.getUniformLocation(this.program, 'u_resolution')
|
|
179
|
+
this.u_tex = this.gl.getUniformLocation(this.program, 'u_tex')
|
|
180
|
+
this.u_colorMatrix = this.gl.getUniformLocation(this.program, 'u_colorMatrix')
|
|
181
|
+
this.u_texDimensions = this.gl.getUniformLocation(this.program, 'u_texDimensions')
|
|
182
|
+
|
|
183
|
+
// Get attribute locations
|
|
184
|
+
this.a_quadPos = this.gl.getAttribLocation(this.program, 'a_quadPos')
|
|
185
|
+
this.a_destRect = this.gl.getAttribLocation(this.program, 'a_destRect')
|
|
186
|
+
this.a_color = this.gl.getAttribLocation(this.program, 'a_color')
|
|
187
|
+
this.a_texLayer = this.gl.getAttribLocation(this.program, 'a_texLayer')
|
|
188
|
+
|
|
189
|
+
// Create quad position buffer (6 vertices for 2 triangles)
|
|
190
|
+
this.quadPosBuffer = this.gl.createBuffer()
|
|
191
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadPosBuffer)
|
|
192
|
+
const quadPositions = new Float32Array([
|
|
193
|
+
0.0, 0.0,
|
|
194
|
+
1.0, 0.0,
|
|
195
|
+
0.0, 1.0,
|
|
196
|
+
1.0, 0.0,
|
|
197
|
+
1.0, 1.0,
|
|
198
|
+
0.0, 1.0
|
|
199
|
+
])
|
|
200
|
+
this.gl.bufferData(this.gl.ARRAY_BUFFER, quadPositions, this.gl.STATIC_DRAW)
|
|
201
|
+
|
|
202
|
+
// Create instance attribute buffers
|
|
203
|
+
this.instanceDestRectBuffer = this.gl.createBuffer()
|
|
204
|
+
this.instanceColorBuffer = this.gl.createBuffer()
|
|
205
|
+
this.instanceTexLayerBuffer = this.gl.createBuffer()
|
|
206
|
+
|
|
207
|
+
// Set up vertex attributes
|
|
208
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.quadPosBuffer)
|
|
209
|
+
this.gl.enableVertexAttribArray(this.a_quadPos)
|
|
210
|
+
this.gl.vertexAttribPointer(this.a_quadPos, 2, this.gl.FLOAT, false, 0, 0)
|
|
211
|
+
|
|
212
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.instanceDestRectBuffer)
|
|
213
|
+
this.gl.enableVertexAttribArray(this.a_destRect)
|
|
214
|
+
this.gl.vertexAttribPointer(this.a_destRect, 4, this.gl.FLOAT, false, 0, 0)
|
|
215
|
+
this.instancedArraysExt.vertexAttribDivisorANGLE(this.a_destRect, 1)
|
|
216
|
+
|
|
217
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.instanceColorBuffer)
|
|
218
|
+
this.gl.enableVertexAttribArray(this.a_color)
|
|
219
|
+
this.gl.vertexAttribPointer(this.a_color, 4, this.gl.FLOAT, false, 0, 0)
|
|
220
|
+
this.instancedArraysExt.vertexAttribDivisorANGLE(this.a_color, 1)
|
|
221
|
+
|
|
222
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.instanceTexLayerBuffer)
|
|
223
|
+
this.gl.enableVertexAttribArray(this.a_texLayer)
|
|
224
|
+
this.gl.vertexAttribPointer(this.a_texLayer, 1, this.gl.FLOAT, false, 0, 0)
|
|
225
|
+
this.instancedArraysExt.vertexAttribDivisorANGLE(this.a_texLayer, 1)
|
|
226
|
+
|
|
227
|
+
// Set up blending for premultiplied alpha
|
|
228
|
+
this.gl.enable(this.gl.BLEND)
|
|
229
|
+
this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA)
|
|
230
|
+
|
|
231
|
+
// Use the program
|
|
232
|
+
this.gl.useProgram(this.program)
|
|
233
|
+
|
|
234
|
+
// Set texture unit
|
|
235
|
+
this.gl.uniform1i(this.u_tex, 0)
|
|
236
|
+
|
|
237
|
+
// Set initial color matrix
|
|
238
|
+
this.gl.uniformMatrix3fv(this.u_colorMatrix, false, this.colorMatrix)
|
|
239
|
+
|
|
240
|
+
// Set one-time GL state
|
|
241
|
+
this.gl.pixelStorei(this.gl.UNPACK_ALIGNMENT, 1)
|
|
242
|
+
this.gl.clearColor(0, 0, 0, 0)
|
|
243
|
+
this.gl.activeTexture(this.gl.TEXTURE0)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
createShader (type: number, source: string): WebGLShader | null {
|
|
247
|
+
const shader = this.gl!.createShader(type)!
|
|
248
|
+
this.gl!.shaderSource(shader, source)
|
|
249
|
+
this.gl!.compileShader(shader)
|
|
250
|
+
|
|
251
|
+
if (!this.gl!.getShaderParameter(shader, this.gl!.COMPILE_STATUS)) {
|
|
252
|
+
const info = this.gl!.getShaderInfoLog(shader)
|
|
253
|
+
console.log(info)
|
|
254
|
+
this.gl!.deleteShader(shader)
|
|
255
|
+
return null
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return shader
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Set the color matrix for color space conversion.
|
|
262
|
+
// Pass null or undefined to use identity (no conversion).
|
|
263
|
+
setColorMatrix (subtitleColorSpace?: 'BT601' | 'BT709' | 'SMPTE240M' | 'FCC', videoColorSpace?: 'BT601' | 'BT709') {
|
|
264
|
+
this.colorMatrix = (subtitleColorSpace && videoColorSpace && colorMatrixConversionMap[subtitleColorSpace]?.[videoColorSpace]) ?? IDENTITY_MATRIX
|
|
265
|
+
if (this.gl && this.u_colorMatrix && this.program) {
|
|
266
|
+
this.gl.useProgram(this.program)
|
|
267
|
+
this.gl.uniformMatrix3fv(this.u_colorMatrix, false, this.colorMatrix)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
createTexture (width: number, height: number): WebGLTexture {
|
|
272
|
+
const texture = this.gl!.createTexture()
|
|
273
|
+
this.gl!.bindTexture(this.gl!.TEXTURE_2D, texture)
|
|
274
|
+
|
|
275
|
+
// Allocate storage for texture (WebGL1 uses LUMINANCE instead of R8)
|
|
276
|
+
this.gl!.texImage2D(
|
|
277
|
+
this.gl!.TEXTURE_2D,
|
|
278
|
+
0,
|
|
279
|
+
this.gl!.LUMINANCE,
|
|
280
|
+
width,
|
|
281
|
+
height,
|
|
282
|
+
0,
|
|
283
|
+
this.gl!.LUMINANCE,
|
|
284
|
+
this.gl!.UNSIGNED_BYTE,
|
|
285
|
+
null
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
// Set texture parameters
|
|
289
|
+
this.gl!.texParameteri(this.gl!.TEXTURE_2D, this.gl!.TEXTURE_MIN_FILTER, this.gl!.NEAREST)
|
|
290
|
+
this.gl!.texParameteri(this.gl!.TEXTURE_2D, this.gl!.TEXTURE_MAG_FILTER, this.gl!.NEAREST)
|
|
291
|
+
this.gl!.texParameteri(this.gl!.TEXTURE_2D, this.gl!.TEXTURE_WRAP_S, this.gl!.CLAMP_TO_EDGE)
|
|
292
|
+
this.gl!.texParameteri(this.gl!.TEXTURE_2D, this.gl!.TEXTURE_WRAP_T, this.gl!.CLAMP_TO_EDGE)
|
|
293
|
+
|
|
294
|
+
return texture
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
render (images: ASSImage[], heap: Uint8Array): void {
|
|
298
|
+
if (!this.gl || !this.program || !this.instancedArraysExt) return
|
|
299
|
+
|
|
300
|
+
// we scheduled a resize because changing the canvas size clears it, and we don't want it to flicker
|
|
301
|
+
// so we do it here, right before rendering
|
|
302
|
+
if (this._scheduledResize) {
|
|
303
|
+
const { width, height } = this._scheduledResize
|
|
304
|
+
this._scheduledResize = undefined
|
|
305
|
+
this.canvas!.width = width
|
|
306
|
+
this.canvas!.height = height
|
|
307
|
+
|
|
308
|
+
// Update viewport and resolution uniform
|
|
309
|
+
this.gl.viewport(0, 0, width, height)
|
|
310
|
+
this.gl.uniform2f(this.u_resolution, width, height)
|
|
311
|
+
} else {
|
|
312
|
+
// Clear canvas
|
|
313
|
+
this.gl.clear(this.gl.COLOR_BUFFER_BIT)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Find max dimensions needed and filter valid images
|
|
317
|
+
let maxW = this.textureWidth
|
|
318
|
+
let maxH = this.textureHeight
|
|
319
|
+
const validImages: ASSImage[] = []
|
|
320
|
+
|
|
321
|
+
for (const img of images) {
|
|
322
|
+
if (img.w <= 0 || img.h <= 0) continue
|
|
323
|
+
validImages.push(img)
|
|
324
|
+
if (img.w > maxW) maxW = img.w
|
|
325
|
+
if (img.h > maxH) maxH = img.h
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (validImages.length === 0) return
|
|
329
|
+
|
|
330
|
+
// Update texture dimensions if needed
|
|
331
|
+
if (maxW > this.textureWidth || maxH > this.textureHeight) {
|
|
332
|
+
this.textureWidth = maxW
|
|
333
|
+
this.textureHeight = maxH
|
|
334
|
+
// Clear texture cache as we need to recreate textures
|
|
335
|
+
for (const texture of this.textureCache.values()) {
|
|
336
|
+
this.gl.deleteTexture(texture)
|
|
337
|
+
}
|
|
338
|
+
this.textureCache.clear()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Process images individually (WebGL1 limitation: no texture arrays)
|
|
342
|
+
// We'll render them one by one instead of in batches
|
|
343
|
+
for (let i = 0; i < validImages.length; i++) {
|
|
344
|
+
const img = validImages[i]!
|
|
345
|
+
|
|
346
|
+
// Get or create texture for this image
|
|
347
|
+
let texture = this.textureCache.get(i)
|
|
348
|
+
if (!texture) {
|
|
349
|
+
texture = this.createTexture(this.textureWidth, this.textureHeight)
|
|
350
|
+
this.textureCache.set(i, texture)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, texture)
|
|
354
|
+
|
|
355
|
+
// Upload bitmap data to texture
|
|
356
|
+
// WebGL1 doesn't support UNPACK_ROW_LENGTH, so we need to handle strided data manually
|
|
357
|
+
// Strided data - need to copy row by row to remove padding
|
|
358
|
+
const sourceView = new Uint8Array(heap.buffer, img.bitmap, img.stride * img.h)
|
|
359
|
+
const tightData = new Uint8Array(img.w * img.h)
|
|
360
|
+
|
|
361
|
+
for (let y = 0; y < img.h; y++) {
|
|
362
|
+
const srcOffset = y * img.stride
|
|
363
|
+
const dstOffset = y * img.w
|
|
364
|
+
tightData.set(sourceView.subarray(srcOffset, srcOffset + img.w), dstOffset)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
this.gl.texSubImage2D(
|
|
368
|
+
this.gl.TEXTURE_2D,
|
|
369
|
+
0,
|
|
370
|
+
0, 0, // x, y offset
|
|
371
|
+
img.w,
|
|
372
|
+
img.h,
|
|
373
|
+
this.gl.LUMINANCE,
|
|
374
|
+
this.gl.UNSIGNED_BYTE,
|
|
375
|
+
tightData
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
// Fill instance data (single instance)
|
|
379
|
+
this.instanceDestRectData[0] = img.dst_x
|
|
380
|
+
this.instanceDestRectData[1] = img.dst_y
|
|
381
|
+
this.instanceDestRectData[2] = img.w
|
|
382
|
+
this.instanceDestRectData[3] = img.h
|
|
383
|
+
|
|
384
|
+
this.instanceColorData[0] = ((img.color >>> 24) & 0xFF) / 255
|
|
385
|
+
this.instanceColorData[1] = ((img.color >>> 16) & 0xFF) / 255
|
|
386
|
+
this.instanceColorData[2] = ((img.color >>> 8) & 0xFF) / 255
|
|
387
|
+
this.instanceColorData[3] = (img.color & 0xFF) / 255
|
|
388
|
+
|
|
389
|
+
this.instanceTexLayerData[0] = 0 // Not used in WebGL1 version
|
|
390
|
+
|
|
391
|
+
// Upload instance data to buffers
|
|
392
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.instanceDestRectBuffer)
|
|
393
|
+
this.gl.bufferData(this.gl.ARRAY_BUFFER, this.instanceDestRectData.subarray(0, 4), this.gl.DYNAMIC_DRAW)
|
|
394
|
+
|
|
395
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.instanceColorBuffer)
|
|
396
|
+
this.gl.bufferData(this.gl.ARRAY_BUFFER, this.instanceColorData.subarray(0, 4), this.gl.DYNAMIC_DRAW)
|
|
397
|
+
|
|
398
|
+
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.instanceTexLayerBuffer)
|
|
399
|
+
this.gl.bufferData(this.gl.ARRAY_BUFFER, this.instanceTexLayerData.subarray(0, 1), this.gl.DYNAMIC_DRAW)
|
|
400
|
+
|
|
401
|
+
// Set texture dimensions uniform
|
|
402
|
+
this.gl.uniform2f(this.u_texDimensions, this.textureWidth, this.textureHeight)
|
|
403
|
+
|
|
404
|
+
// Single instanced draw call
|
|
405
|
+
this.instancedArraysExt.drawArraysInstancedANGLE(this.gl.TRIANGLES, 0, 6, 1)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
destroy () {
|
|
410
|
+
if (this.gl) {
|
|
411
|
+
// Delete all cached textures
|
|
412
|
+
for (const texture of this.textureCache.values()) {
|
|
413
|
+
this.gl.deleteTexture(texture)
|
|
414
|
+
}
|
|
415
|
+
this.textureCache.clear()
|
|
416
|
+
|
|
417
|
+
if (this.quadPosBuffer) {
|
|
418
|
+
this.gl.deleteBuffer(this.quadPosBuffer)
|
|
419
|
+
this.quadPosBuffer = null
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (this.instanceDestRectBuffer) {
|
|
423
|
+
this.gl.deleteBuffer(this.instanceDestRectBuffer)
|
|
424
|
+
this.instanceDestRectBuffer = null
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (this.instanceColorBuffer) {
|
|
428
|
+
this.gl.deleteBuffer(this.instanceColorBuffer)
|
|
429
|
+
this.instanceColorBuffer = null
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (this.instanceTexLayerBuffer) {
|
|
433
|
+
this.gl.deleteBuffer(this.instanceTexLayerBuffer)
|
|
434
|
+
this.instanceTexLayerBuffer = null
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (this.program) {
|
|
438
|
+
this.gl.deleteProgram(this.program)
|
|
439
|
+
this.program = null
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
this.gl = null
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// fallback for browsers that don't support WebGL2
|
|
2
|
+
import { colorMatrixConversionMap, IDENTITY_MATRIX, IS_FIREFOX, SHOULD_REFERENCE_MEMORY, type ASSImage } from '../util.ts'
|
|
2
3
|
|
|
3
4
|
declare const self: DedicatedWorkerGlobalScope &
|
|
4
5
|
typeof globalThis & {
|
|
@@ -6,70 +7,6 @@ declare const self: DedicatedWorkerGlobalScope &
|
|
|
6
7
|
WASMMEMORY: WebAssembly.Memory
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
const THREAD_COUNT = !IS_FIREFOX && self.crossOriginIsolated ? Math.min(Math.max(1, navigator.hardwareConcurrency - 2), 8) : 1
|
|
10
|
-
|
|
11
|
-
// @ts-expect-error new experimental API
|
|
12
|
-
const SUPPORTS_GROWTH = !!WebAssembly.Memory.prototype.toResizableBuffer
|
|
13
|
-
|
|
14
|
-
// HACK: 3 memory hacks to support here:
|
|
15
|
-
// 1. Chrome WASM Growable memory which can use a reference to the buffer to fix visual artifacts, which happen both with multithreading or without [fastest]
|
|
16
|
-
// 2. Chrome WASM non-growable, but mult-threaded only memory which needs to re-create the HEAPU8 on growth because of race conditions [medium]
|
|
17
|
-
// 3. Firefox non-growable memory which needs a copy of the data into a non-resizable buffer and can't use a reference [fastest single threaded, but only on Firefox, on Chrome this is slowest]
|
|
18
|
-
const SHOULD_REFERENCE_MEMORY = !IS_FIREFOX && (SUPPORTS_GROWTH || THREAD_COUNT > 1)
|
|
19
|
-
|
|
20
|
-
const IDENTITY_MATRIX = new Float32Array([
|
|
21
|
-
1, 0, 0,
|
|
22
|
-
0, 1, 0,
|
|
23
|
-
0, 0, 1
|
|
24
|
-
])
|
|
25
|
-
|
|
26
|
-
// Color matrix conversion map - mat3x3 for WebGL2
|
|
27
|
-
// Each matrix converts FROM the key color space TO the nested key color space
|
|
28
|
-
export const colorMatrixConversionMap = {
|
|
29
|
-
BT601: {
|
|
30
|
-
BT709: new Float32Array([
|
|
31
|
-
1.0863, 0.0965, -0.01411,
|
|
32
|
-
-0.0723, 0.8451, -0.0277,
|
|
33
|
-
-0.0141, 0.0584, 1.0418
|
|
34
|
-
]),
|
|
35
|
-
BT601: IDENTITY_MATRIX
|
|
36
|
-
},
|
|
37
|
-
BT709: {
|
|
38
|
-
BT601: new Float32Array([
|
|
39
|
-
0.9137, 0.0784, 0.0079,
|
|
40
|
-
-0.1049, 1.1722, -0.0671,
|
|
41
|
-
0.0096, 0.0322, 0.9582
|
|
42
|
-
]),
|
|
43
|
-
BT709: IDENTITY_MATRIX
|
|
44
|
-
},
|
|
45
|
-
FCC: {
|
|
46
|
-
BT709: new Float32Array([
|
|
47
|
-
1.0873, -0.0736, -0.0137,
|
|
48
|
-
0.0974, 0.8494, 0.0531,
|
|
49
|
-
-0.0127, -0.0251, 1.0378
|
|
50
|
-
]),
|
|
51
|
-
BT601: new Float32Array([
|
|
52
|
-
1.001, -0.0008, -0.0002,
|
|
53
|
-
0.0009, 1.005, -0.006,
|
|
54
|
-
0.0013, 0.0027, 0.996
|
|
55
|
-
])
|
|
56
|
-
},
|
|
57
|
-
SMPTE240M: {
|
|
58
|
-
BT709: new Float32Array([
|
|
59
|
-
0.9993, 0.0006, 0.0001,
|
|
60
|
-
-0.0004, 0.9812, 0.0192,
|
|
61
|
-
-0.0034, -0.0114, 1.0148
|
|
62
|
-
]),
|
|
63
|
-
BT601: new Float32Array([
|
|
64
|
-
0.913, 0.0774, 0.0096,
|
|
65
|
-
-0.1051, 1.1508, -0.0456,
|
|
66
|
-
0.0063, 0.0207, 0.973
|
|
67
|
-
])
|
|
68
|
-
}
|
|
69
|
-
} as const
|
|
70
|
-
|
|
71
|
-
export type ColorSpace = keyof typeof colorMatrixConversionMap
|
|
72
|
-
|
|
73
10
|
// GLSL ES 3.0 Vertex Shader with Instancing
|
|
74
11
|
const VERTEX_SHADER = /* glsl */`#version 300 es
|
|
75
12
|
precision mediump float;
|
package/src/worker/util.ts
CHANGED
|
@@ -41,3 +41,67 @@ export async function fetchtext (url: string) {
|
|
|
41
41
|
const res = await _fetch(url)
|
|
42
42
|
return await res.text()
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
export const THREAD_COUNT = !IS_FIREFOX && self.crossOriginIsolated ? Math.min(Math.max(1, navigator.hardwareConcurrency - 2), 8) : 1
|
|
46
|
+
|
|
47
|
+
// @ts-expect-error new experimental API
|
|
48
|
+
export const SUPPORTS_GROWTH = !!WebAssembly.Memory.prototype.toResizableBuffer
|
|
49
|
+
|
|
50
|
+
// HACK: 3 memory hacks to support here:
|
|
51
|
+
// 1. Chrome WASM Growable memory which can use a reference to the buffer to fix visual artifacts, which happen both with multithreading or without [fastest]
|
|
52
|
+
// 2. Chrome WASM non-growable, but mult-threaded only memory which needs to re-create the HEAPU8 on growth because of race conditions [medium]
|
|
53
|
+
// 3. Firefox non-growable memory which needs a copy of the data into a non-resizable buffer and can't use a reference [fastest single threaded, but only on Firefox, on Chrome this is slowest]
|
|
54
|
+
export const SHOULD_REFERENCE_MEMORY = !IS_FIREFOX && (SUPPORTS_GROWTH || THREAD_COUNT > 1)
|
|
55
|
+
|
|
56
|
+
export const IDENTITY_MATRIX = new Float32Array([
|
|
57
|
+
1, 0, 0,
|
|
58
|
+
0, 1, 0,
|
|
59
|
+
0, 0, 1
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
// Color matrix conversion map - mat3x3 for WebGL2
|
|
63
|
+
// Each matrix converts FROM the key color space TO the nested key color space
|
|
64
|
+
export const colorMatrixConversionMap = {
|
|
65
|
+
BT601: {
|
|
66
|
+
BT709: new Float32Array([
|
|
67
|
+
1.0863, 0.0965, -0.01411,
|
|
68
|
+
-0.0723, 0.8451, -0.0277,
|
|
69
|
+
-0.0141, 0.0584, 1.0418
|
|
70
|
+
]),
|
|
71
|
+
BT601: IDENTITY_MATRIX
|
|
72
|
+
},
|
|
73
|
+
BT709: {
|
|
74
|
+
BT601: new Float32Array([
|
|
75
|
+
0.9137, 0.0784, 0.0079,
|
|
76
|
+
-0.1049, 1.1722, -0.0671,
|
|
77
|
+
0.0096, 0.0322, 0.9582
|
|
78
|
+
]),
|
|
79
|
+
BT709: IDENTITY_MATRIX
|
|
80
|
+
},
|
|
81
|
+
FCC: {
|
|
82
|
+
BT709: new Float32Array([
|
|
83
|
+
1.0873, -0.0736, -0.0137,
|
|
84
|
+
0.0974, 0.8494, 0.0531,
|
|
85
|
+
-0.0127, -0.0251, 1.0378
|
|
86
|
+
]),
|
|
87
|
+
BT601: new Float32Array([
|
|
88
|
+
1.001, -0.0008, -0.0002,
|
|
89
|
+
0.0009, 1.005, -0.006,
|
|
90
|
+
0.0013, 0.0027, 0.996
|
|
91
|
+
])
|
|
92
|
+
},
|
|
93
|
+
SMPTE240M: {
|
|
94
|
+
BT709: new Float32Array([
|
|
95
|
+
0.9993, 0.0006, 0.0001,
|
|
96
|
+
-0.0004, 0.9812, 0.0192,
|
|
97
|
+
-0.0034, -0.0114, 1.0148
|
|
98
|
+
]),
|
|
99
|
+
BT601: new Float32Array([
|
|
100
|
+
0.913, 0.0774, 0.0096,
|
|
101
|
+
-0.1051, 1.1508, -0.0456,
|
|
102
|
+
0.0063, 0.0207, 0.973
|
|
103
|
+
])
|
|
104
|
+
}
|
|
105
|
+
} as const
|
|
106
|
+
|
|
107
|
+
export type ColorSpace = keyof typeof colorMatrixConversionMap
|