jassub 2.0.10 → 2.0.13
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 +8 -4
- package/dist/jassub.js +1 -2
- package/dist/jassub.js.map +1 -1
- package/dist/wasm/jassub-worker-modern.wasm +0 -0
- package/dist/wasm/jassub-worker.js +2 -3
- package/dist/wasm/jassub-worker.js.map +1 -1
- package/dist/wasm/jassub-worker.wasm +0 -0
- package/dist/worker/webgpu-renderer.d.ts +0 -1
- package/dist/worker/webgpu-renderer.js +23 -32
- package/dist/worker/webgpu-renderer.js.map +1 -1
- package/dist/worker/worker.d.ts +0 -1
- package/dist/worker/worker.js +1 -1
- package/dist/worker/worker.js.map +1 -1
- package/package.json +3 -2
- package/src/jassub.ts +340 -0
- package/src/wasm/types.d.ts +138 -0
- package/src/worker/util.ts +35 -0
- package/src/worker/webgpu-renderer.ts +497 -0
- package/src/worker/worker.ts +278 -0
- package/dist/wasm/jassub-worker-modern.d.ts +0 -2
- package/dist/wasm/jassub-worker-modern.js.map +0 -1
- package/dist/wasm/old.js +0 -1034
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// TypeScript bindings for emscripten-generated code. Automatically generated at compile time.
|
|
2
|
+
declare namespace RuntimeExports {
|
|
3
|
+
function getTempRet0(val: any): any;
|
|
4
|
+
function setTempRet0(val: any): any;
|
|
5
|
+
}
|
|
6
|
+
interface WasmModule {
|
|
7
|
+
__ZdlPvm(_0: number, _1: number): void;
|
|
8
|
+
_malloc(_0: number): number;
|
|
9
|
+
_calloc(_0: number, _1: number): number;
|
|
10
|
+
_emscripten_builtin_free(_0: number): void;
|
|
11
|
+
___libc_free(_0: number): void;
|
|
12
|
+
_emscripten_builtin_malloc(_0: number): number;
|
|
13
|
+
___libc_malloc(_0: number): number;
|
|
14
|
+
__ZdaPv(_0: number): void;
|
|
15
|
+
__ZdaPvm(_0: number, _1: number): void;
|
|
16
|
+
__ZdlPv(_0: number): void;
|
|
17
|
+
__Znaj(_0: number): number;
|
|
18
|
+
__ZnajSt11align_val_t(_0: number, _1: number): number;
|
|
19
|
+
__Znwj(_0: number): number;
|
|
20
|
+
__ZnwjSt11align_val_t(_0: number, _1: number): number;
|
|
21
|
+
___libc_calloc(_0: number, _1: number): number;
|
|
22
|
+
___libc_realloc(_0: number, _1: number): number;
|
|
23
|
+
_emscripten_builtin_calloc(_0: number, _1: number): number;
|
|
24
|
+
_emscripten_builtin_realloc(_0: number, _1: number): number;
|
|
25
|
+
_malloc_size(_0: number): number;
|
|
26
|
+
_malloc_usable_size(_0: number): number;
|
|
27
|
+
_reallocf(_0: number, _1: number): number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type EmbindString = ArrayBuffer|Uint8Array|Uint8ClampedArray|Int8Array|string;
|
|
31
|
+
export interface ClassHandle {
|
|
32
|
+
isAliasOf(other: ClassHandle): boolean;
|
|
33
|
+
delete(): void;
|
|
34
|
+
deleteLater(): this;
|
|
35
|
+
isDeleted(): boolean;
|
|
36
|
+
// @ts-ignore - If targeting lower than ESNext, this symbol might not exist.
|
|
37
|
+
[Symbol.dispose](): void;
|
|
38
|
+
clone(): this;
|
|
39
|
+
}
|
|
40
|
+
export interface ASS_Image extends ClassHandle {
|
|
41
|
+
readonly next: ASS_Image;
|
|
42
|
+
w: number;
|
|
43
|
+
h: number;
|
|
44
|
+
dst_x: number;
|
|
45
|
+
dst_y: number;
|
|
46
|
+
stride: number;
|
|
47
|
+
color: number;
|
|
48
|
+
readonly bitmap: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ASS_Style extends ClassHandle {
|
|
52
|
+
Bold: number;
|
|
53
|
+
Italic: number;
|
|
54
|
+
Underline: number;
|
|
55
|
+
StrikeOut: number;
|
|
56
|
+
BorderStyle: number;
|
|
57
|
+
Alignment: number;
|
|
58
|
+
MarginL: number;
|
|
59
|
+
MarginR: number;
|
|
60
|
+
MarginV: number;
|
|
61
|
+
Encoding: number;
|
|
62
|
+
treat_fontname_as_pattern: number;
|
|
63
|
+
Justify: number;
|
|
64
|
+
PrimaryColour: number;
|
|
65
|
+
SecondaryColour: number;
|
|
66
|
+
OutlineColour: number;
|
|
67
|
+
BackColour: number;
|
|
68
|
+
FontSize: number;
|
|
69
|
+
ScaleX: number;
|
|
70
|
+
ScaleY: number;
|
|
71
|
+
Spacing: number;
|
|
72
|
+
Angle: number;
|
|
73
|
+
Outline: number;
|
|
74
|
+
Shadow: number;
|
|
75
|
+
Blur: number;
|
|
76
|
+
get Name(): string;
|
|
77
|
+
set Name(value: EmbindString);
|
|
78
|
+
get FontName(): string;
|
|
79
|
+
set FontName(value: EmbindString);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ASS_Event extends ClassHandle {
|
|
83
|
+
ReadOrder: number;
|
|
84
|
+
Layer: number;
|
|
85
|
+
Style: number;
|
|
86
|
+
MarginL: number;
|
|
87
|
+
MarginR: number;
|
|
88
|
+
MarginV: number;
|
|
89
|
+
Start: number;
|
|
90
|
+
Duration: number;
|
|
91
|
+
get Name(): string;
|
|
92
|
+
set Name(value: EmbindString);
|
|
93
|
+
get Effect(): string;
|
|
94
|
+
set Effect(value: EmbindString);
|
|
95
|
+
get Text(): string;
|
|
96
|
+
set Text(value: EmbindString);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface JASSUB extends ClassHandle {
|
|
100
|
+
trackColorSpace: number;
|
|
101
|
+
changed: number;
|
|
102
|
+
count: number;
|
|
103
|
+
removeTrack(): void;
|
|
104
|
+
quitLibrary(): void;
|
|
105
|
+
reloadFonts(): void;
|
|
106
|
+
removeAllEvents(): void;
|
|
107
|
+
styleOverride(_0: ASS_Style): void;
|
|
108
|
+
disableStyleOverride(): void;
|
|
109
|
+
setLogLevel(_0: number): void;
|
|
110
|
+
resizeCanvas(_0: number, _1: number, _2: number, _3: number): void;
|
|
111
|
+
setMargin(_0: number, _1: number, _2: number, _3: number): void;
|
|
112
|
+
getEventCount(): number;
|
|
113
|
+
allocEvent(): number;
|
|
114
|
+
allocStyle(): number;
|
|
115
|
+
removeEvent(_0: number): void;
|
|
116
|
+
getStyleCount(): number;
|
|
117
|
+
removeStyle(_0: number): void;
|
|
118
|
+
setMemoryLimits(_0: number, _1: number): void;
|
|
119
|
+
getEvent(_0: number): ASS_Event | null;
|
|
120
|
+
getStyle(_0: number): ASS_Style | null;
|
|
121
|
+
setThreads(_0: number): number;
|
|
122
|
+
rawRender(_0: number, _1: number): ASS_Image | null;
|
|
123
|
+
createTrackMem(_0: EmbindString): void;
|
|
124
|
+
addFont(_0: EmbindString, _1: number, _2: number): void;
|
|
125
|
+
setDefaultFont(_0: EmbindString): void;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface EmbindModule {
|
|
129
|
+
ASS_Image: {};
|
|
130
|
+
ASS_Style: {};
|
|
131
|
+
ASS_Event: {};
|
|
132
|
+
JASSUB: {
|
|
133
|
+
new(_0: number, _1: number, _2: EmbindString): JASSUB;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type MainModule = WasmModule & typeof RuntimeExports & EmbindModule;
|
|
138
|
+
export default function MainModuleFactory (options?: unknown): Promise<MainModule>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ASSEvent, ASSStyle } from '../jassub'
|
|
2
|
+
|
|
3
|
+
export const read_ = (url: string, ab = false) => {
|
|
4
|
+
const xhr = new XMLHttpRequest()
|
|
5
|
+
xhr.open('GET', url, false)
|
|
6
|
+
xhr.responseType = ab ? 'arraybuffer' : 'text'
|
|
7
|
+
xhr.send(null)
|
|
8
|
+
return xhr.response
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const readAsync = (url: string, load: (response: ArrayBuffer) => void, err: (error: unknown) => void) => {
|
|
12
|
+
const xhr = new XMLHttpRequest()
|
|
13
|
+
xhr.open('GET', url, true)
|
|
14
|
+
xhr.responseType = 'arraybuffer'
|
|
15
|
+
xhr.onload = () => {
|
|
16
|
+
if ((xhr.status === 200 || xhr.status === 0) && xhr.response) {
|
|
17
|
+
return load(xhr.response)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
xhr.onerror = err
|
|
21
|
+
xhr.send(null)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const a = 'BT601'
|
|
25
|
+
const b = 'BT709'
|
|
26
|
+
const c = 'SMPTE240M'
|
|
27
|
+
const d = 'FCC'
|
|
28
|
+
|
|
29
|
+
export const libassYCbCrMap = [null, a, null, a, a, b, b, c, c, d, d] as const
|
|
30
|
+
|
|
31
|
+
export function _applyKeys<T extends (ASSEvent | ASSStyle)> (input: T, output: T) {
|
|
32
|
+
for (const v of Object.keys(input) as Array<keyof T>) {
|
|
33
|
+
output[v] = input[v]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import type { ASSImage } from '../jassub'
|
|
2
|
+
|
|
3
|
+
const IDENTITY_MATRIX = new Float32Array([
|
|
4
|
+
1, 0, 0, 0,
|
|
5
|
+
0, 1, 0, 0,
|
|
6
|
+
0, 0, 1, 0
|
|
7
|
+
])
|
|
8
|
+
|
|
9
|
+
// Color matrix conversion map - mat3x3 pre-padded for WGSL (each column padded to vec4f)
|
|
10
|
+
// Each matrix converts FROM the key color space TO the nested key color space
|
|
11
|
+
export const colorMatrixConversionMap = {
|
|
12
|
+
BT601: {
|
|
13
|
+
BT709: new Float32Array([
|
|
14
|
+
1.0863, 0.0965, -0.0141, 0,
|
|
15
|
+
-0.0723, 0.8451, -0.0277, 0,
|
|
16
|
+
-0.014, 0.0584, 1.0418, 0
|
|
17
|
+
]),
|
|
18
|
+
BT601: IDENTITY_MATRIX
|
|
19
|
+
},
|
|
20
|
+
BT709: {
|
|
21
|
+
BT601: new Float32Array([
|
|
22
|
+
0.9137, -0.1049, 0.0096, 0,
|
|
23
|
+
0.0784, 1.1722, 0.0322, 0,
|
|
24
|
+
0.0079, -0.0671, 0.9582, 0
|
|
25
|
+
]),
|
|
26
|
+
BT709: IDENTITY_MATRIX
|
|
27
|
+
},
|
|
28
|
+
FCC: {
|
|
29
|
+
BT709: new Float32Array([
|
|
30
|
+
1.0873, 0.0974, -0.0127, 0,
|
|
31
|
+
-0.0736, 0.8494, -0.0251, 0,
|
|
32
|
+
-0.0137, 0.0531, 1.0378, 0
|
|
33
|
+
]),
|
|
34
|
+
BT601: new Float32Array([
|
|
35
|
+
1.001, 0.0009, 0.0013, 0,
|
|
36
|
+
-0.0008, 1.005, 0.0027, 0,
|
|
37
|
+
-0.0002, -0.006, 0.996, 0
|
|
38
|
+
])
|
|
39
|
+
},
|
|
40
|
+
SMPTE240M: {
|
|
41
|
+
BT709: new Float32Array([
|
|
42
|
+
0.9993, -0.0004, -0.0034, 0,
|
|
43
|
+
0.0006, 0.9812, -0.0114, 0,
|
|
44
|
+
0.0001, 0.0192, 1.0148, 0
|
|
45
|
+
]),
|
|
46
|
+
BT601: new Float32Array([
|
|
47
|
+
0.913, -0.1051, 0.0063, 0,
|
|
48
|
+
0.0774, 1.1508, 0.0207, 0,
|
|
49
|
+
0.0096, -0.0456, 0.973, 0
|
|
50
|
+
])
|
|
51
|
+
}
|
|
52
|
+
} as const
|
|
53
|
+
|
|
54
|
+
export type ColorSpace = keyof typeof colorMatrixConversionMap
|
|
55
|
+
|
|
56
|
+
// WGSL Vertex Shader
|
|
57
|
+
const VERTEX_SHADER = /* wgsl */`
|
|
58
|
+
struct VertexOutput {
|
|
59
|
+
@builtin(position) position: vec4f,
|
|
60
|
+
@location(0) @interpolate(flat) destXY: vec2f, // destination top-left (flat, no interpolation)
|
|
61
|
+
@location(1) @interpolate(flat) color: vec4f,
|
|
62
|
+
@location(2) @interpolate(flat) texSize: vec2f,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
struct Uniforms {
|
|
66
|
+
resolution: vec2f,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
struct ImageData {
|
|
70
|
+
destRect: vec4f, // x, y, w, h
|
|
71
|
+
srcInfo: vec4f, // texW, texH, stride, 0
|
|
72
|
+
color: vec4f, // RGBA
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
|
|
76
|
+
@group(0) @binding(1) var<storage, read> imageData: ImageData;
|
|
77
|
+
|
|
78
|
+
// Quad vertices (two triangles)
|
|
79
|
+
const QUAD_POSITIONS = array<vec2f, 6>(
|
|
80
|
+
vec2f(0.0, 0.0),
|
|
81
|
+
vec2f(1.0, 0.0),
|
|
82
|
+
vec2f(0.0, 1.0),
|
|
83
|
+
vec2f(1.0, 0.0),
|
|
84
|
+
vec2f(1.0, 1.0),
|
|
85
|
+
vec2f(0.0, 1.0)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
@vertex
|
|
89
|
+
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
90
|
+
var output: VertexOutput;
|
|
91
|
+
|
|
92
|
+
let quadPos = QUAD_POSITIONS[vertexIndex];
|
|
93
|
+
let wh = imageData.destRect.zw;
|
|
94
|
+
|
|
95
|
+
// Calculate pixel position
|
|
96
|
+
let pixelPos = imageData.destRect.xy + quadPos * wh;
|
|
97
|
+
|
|
98
|
+
// Convert to clip space (-1 to 1)
|
|
99
|
+
var clipPos = (pixelPos / uniforms.resolution) * 2.0 - 1.0;
|
|
100
|
+
clipPos.y = -clipPos.y; // Flip Y for canvas coordinates
|
|
101
|
+
|
|
102
|
+
output.position = vec4f(clipPos, 0.0, 1.0);
|
|
103
|
+
output.destXY = imageData.destRect.xy;
|
|
104
|
+
output.color = imageData.color;
|
|
105
|
+
output.texSize = imageData.srcInfo.xy;
|
|
106
|
+
|
|
107
|
+
return output;
|
|
108
|
+
}
|
|
109
|
+
`
|
|
110
|
+
|
|
111
|
+
// WGSL Fragment Shader - use textureLoad with integer coords for pixel-perfect sampling
|
|
112
|
+
const FRAGMENT_SHADER = /* wgsl */`
|
|
113
|
+
@group(0) @binding(3) var tex: texture_2d<f32>;
|
|
114
|
+
@group(0) @binding(4) var<uniform> colorMatrix: mat3x3f;
|
|
115
|
+
|
|
116
|
+
struct FragmentInput {
|
|
117
|
+
@builtin(position) fragCoord: vec4f,
|
|
118
|
+
@location(0) @interpolate(flat) destXY: vec2f,
|
|
119
|
+
@location(1) @interpolate(flat) color: vec4f,
|
|
120
|
+
@location(2) @interpolate(flat) texSize: vec2f,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@fragment
|
|
124
|
+
fn fragmentMain(input: FragmentInput) -> @location(0) vec4f {
|
|
125
|
+
// Calculate integer texel coordinates from fragment position
|
|
126
|
+
// fragCoord.xy is the pixel center (e.g., 0.5, 1.5, 2.5...)
|
|
127
|
+
let texCoord = vec2i(floor(input.fragCoord.xy - input.destXY));
|
|
128
|
+
|
|
129
|
+
// Bounds check (should not be needed but prevents any out-of-bounds access)
|
|
130
|
+
let texSizeI = vec2i(input.texSize);
|
|
131
|
+
if (texCoord.x < 0 || texCoord.y < 0 || texCoord.x >= texSizeI.x || texCoord.y >= texSizeI.y) {
|
|
132
|
+
return vec4f(0.0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Load texel directly using integer coordinates - no interpolation, no precision issues
|
|
136
|
+
let mask = textureLoad(tex, texCoord, 0).r;
|
|
137
|
+
|
|
138
|
+
// Apply color matrix conversion (identity if no conversion needed)
|
|
139
|
+
let correctedColor = colorMatrix * input.color.rgb;
|
|
140
|
+
|
|
141
|
+
// libass color alpha: 0 = opaque, 255 = transparent (inverted)
|
|
142
|
+
let colorAlpha = 1.0 - input.color.a;
|
|
143
|
+
|
|
144
|
+
// Final alpha = colorAlpha * mask (like libass: alpha * mask)
|
|
145
|
+
let a = colorAlpha * mask;
|
|
146
|
+
|
|
147
|
+
// Premultiplied alpha output
|
|
148
|
+
return vec4f(correctedColor * a, a);
|
|
149
|
+
}
|
|
150
|
+
`
|
|
151
|
+
|
|
152
|
+
interface TextureInfo {
|
|
153
|
+
texture: GPUTexture
|
|
154
|
+
view: GPUTextureView
|
|
155
|
+
width: number
|
|
156
|
+
height: number
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export class WebGPURenderer {
|
|
160
|
+
device: GPUDevice | null = null
|
|
161
|
+
context: GPUCanvasContext | null = null
|
|
162
|
+
pipeline: GPURenderPipeline | null = null
|
|
163
|
+
bindGroupLayout: GPUBindGroupLayout | null = null
|
|
164
|
+
|
|
165
|
+
// Uniform buffer
|
|
166
|
+
uniformBuffer: GPUBuffer | null = null
|
|
167
|
+
|
|
168
|
+
// Color matrix buffer (mat3x3f = 48 bytes with padding)
|
|
169
|
+
colorMatrixBuffer: GPUBuffer | null = null
|
|
170
|
+
|
|
171
|
+
// Image data buffers (created on-demand, one per image)
|
|
172
|
+
imageDataBuffers: GPUBuffer[] = []
|
|
173
|
+
|
|
174
|
+
// Textures created on-demand (no fixed limit)
|
|
175
|
+
textures: TextureInfo[] = []
|
|
176
|
+
pendingDestroyTextures: GPUTexture[] = []
|
|
177
|
+
|
|
178
|
+
// eslint-disable-next-line no-undef
|
|
179
|
+
format: GPUTextureFormat = 'bgra8unorm'
|
|
180
|
+
readonly initPromise: Promise<void> | null = null
|
|
181
|
+
|
|
182
|
+
constructor () {
|
|
183
|
+
// Start async initialization immediately
|
|
184
|
+
this.initPromise = this._initDevice()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async _initDevice (): Promise<void> {
|
|
188
|
+
// Check WebGPU support
|
|
189
|
+
if (!navigator.gpu) {
|
|
190
|
+
throw new Error('WebGPU not supported')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const adapter = await navigator.gpu.requestAdapter({
|
|
194
|
+
powerPreference: 'high-performance'
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
if (!adapter) {
|
|
198
|
+
throw new Error('No WebGPU adapter found')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.device = await adapter.requestDevice()
|
|
202
|
+
this.format = navigator.gpu.getPreferredCanvasFormat()
|
|
203
|
+
|
|
204
|
+
// Create shader modules
|
|
205
|
+
const vertexModule = this.device.createShaderModule({
|
|
206
|
+
code: VERTEX_SHADER
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const fragmentModule = this.device.createShaderModule({
|
|
210
|
+
code: FRAGMENT_SHADER
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// Create uniform buffer
|
|
214
|
+
this.uniformBuffer = this.device.createBuffer({
|
|
215
|
+
size: 16, // vec2f resolution + padding
|
|
216
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// Create color matrix buffer (mat3x3f requires 48 bytes: 3 vec3f padded to vec4f each)
|
|
220
|
+
this.colorMatrixBuffer = this.device.createBuffer({
|
|
221
|
+
size: 48, // 3 x vec4f (each column is vec3f padded to 16 bytes)
|
|
222
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
223
|
+
})
|
|
224
|
+
// Initialize with identity matrix
|
|
225
|
+
this.setColorMatrix()
|
|
226
|
+
|
|
227
|
+
// Create bind group layout (no sampler needed - using textureLoad for pixel-perfect sampling)
|
|
228
|
+
this.bindGroupLayout = this.device.createBindGroupLayout({
|
|
229
|
+
entries: [
|
|
230
|
+
{
|
|
231
|
+
binding: 0,
|
|
232
|
+
visibility: GPUShaderStage.VERTEX,
|
|
233
|
+
buffer: { type: 'uniform' }
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
binding: 1,
|
|
237
|
+
visibility: GPUShaderStage.VERTEX,
|
|
238
|
+
buffer: { type: 'read-only-storage' }
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
binding: 3,
|
|
242
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
243
|
+
texture: { sampleType: 'unfilterable-float' } // textureLoad requires unfilterable
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
binding: 4,
|
|
247
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
248
|
+
buffer: { type: 'uniform' }
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// Create pipeline layout
|
|
254
|
+
const pipelineLayout = this.device.createPipelineLayout({
|
|
255
|
+
bindGroupLayouts: [this.bindGroupLayout]
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
// Create render pipeline
|
|
259
|
+
this.pipeline = this.device.createRenderPipeline({
|
|
260
|
+
layout: pipelineLayout,
|
|
261
|
+
vertex: {
|
|
262
|
+
module: vertexModule,
|
|
263
|
+
entryPoint: 'vertexMain'
|
|
264
|
+
},
|
|
265
|
+
fragment: {
|
|
266
|
+
module: fragmentModule,
|
|
267
|
+
entryPoint: 'fragmentMain',
|
|
268
|
+
targets: [
|
|
269
|
+
{
|
|
270
|
+
format: this.format,
|
|
271
|
+
blend: {
|
|
272
|
+
color: {
|
|
273
|
+
srcFactor: 'one',
|
|
274
|
+
dstFactor: 'one-minus-src-alpha',
|
|
275
|
+
operation: 'add'
|
|
276
|
+
},
|
|
277
|
+
alpha: {
|
|
278
|
+
srcFactor: 'one',
|
|
279
|
+
dstFactor: 'one-minus-src-alpha',
|
|
280
|
+
operation: 'add'
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
]
|
|
285
|
+
},
|
|
286
|
+
primitive: {
|
|
287
|
+
topology: 'triangle-list'
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async setCanvas (canvas: OffscreenCanvas, width: number, height: number) {
|
|
293
|
+
await this.initPromise
|
|
294
|
+
if (!this.device) {
|
|
295
|
+
throw new Error('WebGPU device not initialized. Did you await the constructor promise?')
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
canvas.width = width
|
|
299
|
+
canvas.height = height
|
|
300
|
+
|
|
301
|
+
if (!this.context) {
|
|
302
|
+
// Get canvas context
|
|
303
|
+
this.context = canvas.getContext('webgpu')
|
|
304
|
+
if (!this.context) {
|
|
305
|
+
throw new Error('Could not get WebGPU context')
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.context.configure({
|
|
309
|
+
device: this.device,
|
|
310
|
+
format: this.format,
|
|
311
|
+
alphaMode: 'premultiplied'
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Update uniform buffer with resolution
|
|
316
|
+
this.device.queue.writeBuffer(
|
|
317
|
+
this.uniformBuffer!,
|
|
318
|
+
0,
|
|
319
|
+
new Float32Array([width, height])
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Set the color matrix for color space conversion.
|
|
325
|
+
* Pass null or undefined to use identity (no conversion).
|
|
326
|
+
* Matrix should be a pre-padded Float32Array with 12 values (3 columns × 4 floats each).
|
|
327
|
+
*/
|
|
328
|
+
setColorMatrix (matrix?: Float32Array<ArrayBuffer>): void {
|
|
329
|
+
if (!this.device || !this.colorMatrixBuffer) return
|
|
330
|
+
this.device.queue.writeBuffer(this.colorMatrixBuffer, 0, matrix ?? IDENTITY_MATRIX)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private createTextureInfo (width: number, height: number): TextureInfo {
|
|
334
|
+
const texture = this.device!.createTexture({
|
|
335
|
+
size: [width, height],
|
|
336
|
+
format: 'r8unorm',
|
|
337
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
texture,
|
|
342
|
+
view: texture.createView(),
|
|
343
|
+
width,
|
|
344
|
+
height
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
render (images: ASSImage[], heap: Uint8Array): void {
|
|
349
|
+
if (!this.device || !this.context || !this.pipeline) return
|
|
350
|
+
|
|
351
|
+
const commandEncoder = this.device.createCommandEncoder()
|
|
352
|
+
|
|
353
|
+
const textureView = this.context.getCurrentTexture().createView()
|
|
354
|
+
|
|
355
|
+
// Begin render pass
|
|
356
|
+
const renderPass = commandEncoder.beginRenderPass({
|
|
357
|
+
colorAttachments: [
|
|
358
|
+
{
|
|
359
|
+
view: textureView,
|
|
360
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
361
|
+
loadOp: 'clear',
|
|
362
|
+
storeOp: 'store'
|
|
363
|
+
}
|
|
364
|
+
]
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
renderPass.setPipeline(this.pipeline)
|
|
368
|
+
|
|
369
|
+
// Grow arrays if needed
|
|
370
|
+
while (this.textures.length < images.length) {
|
|
371
|
+
this.textures.push(this.createTextureInfo(64, 64))
|
|
372
|
+
}
|
|
373
|
+
while (this.imageDataBuffers.length < images.length) {
|
|
374
|
+
this.imageDataBuffers.push(this.device.createBuffer({
|
|
375
|
+
size: 48, // 3 x vec4f
|
|
376
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
|
|
377
|
+
}))
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Render each image
|
|
381
|
+
for (let i = 0, texIndex = 0; i < images.length; i++, texIndex++) {
|
|
382
|
+
const img = images[i]!
|
|
383
|
+
|
|
384
|
+
// Skip images with invalid dimensions (WebGPU doesn't allow 0-sized textures)
|
|
385
|
+
if (img.w <= 0 || img.h <= 0) continue
|
|
386
|
+
|
|
387
|
+
let texInfo = this.textures[texIndex]!
|
|
388
|
+
|
|
389
|
+
// Recreate texture if size changed (use actual w, not stride)
|
|
390
|
+
if (texInfo.width !== img.w || texInfo.height !== img.h) {
|
|
391
|
+
// Defer destruction until after submit to avoid destroying textures still in use
|
|
392
|
+
this.pendingDestroyTextures.push(texInfo.texture)
|
|
393
|
+
texInfo = this.createTextureInfo(img.w, img.h)
|
|
394
|
+
this.textures[texIndex] = texInfo
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Upload bitmap data using bytesPerRow to handle stride
|
|
398
|
+
// Only need stride * (h-1) + w bytes per ASS spec
|
|
399
|
+
// this... didnt work, is the used alternative bad?
|
|
400
|
+
// const dataSize = img.stride * (img.h - 1) + img.w
|
|
401
|
+
// const bitmapData = heap.subarray(img.bitmap, img.bitmap + dataSize)
|
|
402
|
+
|
|
403
|
+
// this.device.queue.writeTexture(
|
|
404
|
+
// { texture: texInfo.texture },
|
|
405
|
+
// bitmapData as unknown as ArrayBuffer,
|
|
406
|
+
// { bytesPerRow: img.stride }, // Source rows are stride bytes apart
|
|
407
|
+
// { width: img.w, height: img.h } // But we only copy w pixels per row
|
|
408
|
+
// )
|
|
409
|
+
|
|
410
|
+
this.device.queue.writeTexture(
|
|
411
|
+
{ texture: texInfo.texture },
|
|
412
|
+
heap.buffer,
|
|
413
|
+
{ bytesPerRow: img.stride, offset: img.bitmap }, // Source rows are stride bytes apart
|
|
414
|
+
{ width: img.w, height: img.h } // But we only copy w pixels per row
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
// Update image data buffer
|
|
418
|
+
const imageData = new Float32Array([
|
|
419
|
+
// destRect
|
|
420
|
+
img.dst_x, img.dst_y, img.w, img.h,
|
|
421
|
+
// srcInfo
|
|
422
|
+
img.w, img.h, img.stride, 0,
|
|
423
|
+
// color (RGBA from 0xRRGGBBAA)
|
|
424
|
+
((img.color >>> 24) & 0xFF) / 255,
|
|
425
|
+
((img.color >>> 16) & 0xFF) / 255,
|
|
426
|
+
((img.color >>> 8) & 0xFF) / 255,
|
|
427
|
+
(img.color & 0xFF) / 255
|
|
428
|
+
])
|
|
429
|
+
|
|
430
|
+
const imageBuffer = this.imageDataBuffers[texIndex]!
|
|
431
|
+
this.device.queue.writeBuffer(imageBuffer, 0, imageData)
|
|
432
|
+
|
|
433
|
+
// Create bind group for this image (no sampler - using textureLoad)
|
|
434
|
+
const bindGroup = this.device.createBindGroup({
|
|
435
|
+
layout: this.bindGroupLayout!,
|
|
436
|
+
entries: [
|
|
437
|
+
{ binding: 0, resource: { buffer: this.uniformBuffer! } },
|
|
438
|
+
{ binding: 1, resource: { buffer: imageBuffer } },
|
|
439
|
+
{ binding: 3, resource: texInfo.view },
|
|
440
|
+
{ binding: 4, resource: { buffer: this.colorMatrixBuffer! } }
|
|
441
|
+
]
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
renderPass.setBindGroup(0, bindGroup)
|
|
445
|
+
renderPass.draw(6) // 6 vertices for quad
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
renderPass.end()
|
|
449
|
+
|
|
450
|
+
this.device.queue.submit([commandEncoder.finish()])
|
|
451
|
+
|
|
452
|
+
// Now safe to destroy old textures
|
|
453
|
+
for (const tex of this.pendingDestroyTextures) {
|
|
454
|
+
tex.destroy()
|
|
455
|
+
}
|
|
456
|
+
this.pendingDestroyTextures = []
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
clear (): void {
|
|
460
|
+
if (!this.device || !this.context) return
|
|
461
|
+
|
|
462
|
+
const commandEncoder = this.device.createCommandEncoder()
|
|
463
|
+
const textureView = this.context.getCurrentTexture().createView()
|
|
464
|
+
|
|
465
|
+
const renderPass = commandEncoder.beginRenderPass({
|
|
466
|
+
colorAttachments: [
|
|
467
|
+
{
|
|
468
|
+
view: textureView,
|
|
469
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
470
|
+
loadOp: 'clear',
|
|
471
|
+
storeOp: 'store'
|
|
472
|
+
}
|
|
473
|
+
]
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
renderPass.end()
|
|
477
|
+
this.device.queue.submit([commandEncoder.finish()])
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
destroy (): void {
|
|
481
|
+
for (const tex of this.textures) {
|
|
482
|
+
tex.texture.destroy()
|
|
483
|
+
}
|
|
484
|
+
this.textures = []
|
|
485
|
+
|
|
486
|
+
this.uniformBuffer?.destroy()
|
|
487
|
+
this.colorMatrixBuffer?.destroy()
|
|
488
|
+
for (const buf of this.imageDataBuffers) {
|
|
489
|
+
buf.destroy()
|
|
490
|
+
}
|
|
491
|
+
this.imageDataBuffers = []
|
|
492
|
+
|
|
493
|
+
this.device?.destroy()
|
|
494
|
+
this.device = null
|
|
495
|
+
this.context = null
|
|
496
|
+
}
|
|
497
|
+
}
|