akarisub 0.2.2 → 1.0.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 +198 -167
- package/THIRD_PARTY_NOTICES.md +55 -163
- package/dist/COPYRIGHT +56 -164
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -147
- package/dist/index.js.map +1 -0
- package/dist/ts/{ts/akarisub.d.ts → akarisub.d.ts} +80 -99
- package/dist/ts/akarisub.d.ts.map +1 -0
- package/dist/ts/akarisub.js +2086 -0
- package/dist/ts/akarisub.js.map +1 -0
- package/dist/ts/timing.d.ts +63 -0
- package/dist/ts/timing.d.ts.map +1 -0
- package/dist/ts/timing.js +219 -0
- package/dist/ts/timing.js.map +1 -0
- package/dist/ts/{ts/types.d.ts → types.d.ts} +103 -78
- package/dist/ts/types.d.ts.map +1 -0
- package/dist/ts/types.js +2 -0
- package/dist/ts/{ts/types.js.map → types.js.map} +1 -1
- package/dist/ts/{ts/utils.d.ts → utils.d.ts} +0 -31
- package/dist/ts/utils.d.ts.map +1 -0
- package/dist/ts/{ts/utils.js → utils.js} +0 -56
- package/dist/ts/utils.js.map +1 -0
- package/dist/ts/wasm.d.ts +12 -0
- package/dist/ts/wasm.d.ts.map +1 -0
- package/dist/ts/wasm.js +46 -0
- package/dist/ts/wasm.js.map +1 -0
- package/dist/ts/{ts/webgl2-renderer.d.ts → webgl2-renderer.d.ts} +0 -12
- package/dist/ts/webgl2-renderer.d.ts.map +1 -0
- package/dist/ts/{ts/webgl2-renderer.js → webgl2-renderer.js} +0 -27
- package/dist/ts/webgl2-renderer.js.map +1 -0
- package/dist/ts/{ts/webgpu-renderer.d.ts → webgpu-renderer.d.ts} +17 -16
- package/dist/ts/webgpu-renderer.d.ts.map +1 -0
- package/dist/ts/{ts/webgpu-renderer.js → webgpu-renderer.js} +148 -71
- package/dist/ts/webgpu-renderer.js.map +1 -0
- package/dist/ts/worker.d.ts +2 -0
- package/dist/ts/{ts/worker.d.ts.map → worker.d.ts.map} +1 -1
- package/dist/ts/{ts/worker.js → worker.js} +1064 -264
- package/dist/ts/worker.js.map +1 -0
- package/dist/wrapper.d.ts +8 -0
- package/dist/wrapper.d.ts.map +1 -0
- package/dist/{ts/index.js → wrapper.js} +4 -14
- package/dist/wrapper.js.map +1 -0
- package/package.json +24 -22
- package/pkg/akarisub.js +53 -0
- package/{dist/akarisub-worker.wasm → pkg/akarisub.wasm} +0 -0
- package/src/wrapper.ts +46 -0
- package/dist/akarisub-worker.js +0 -39
- package/dist/akarisub.umd.js +0 -159
- package/dist/ts/index.d.ts +0 -14
- package/dist/ts/index.d.ts.map +0 -1
- package/dist/ts/index.js.map +0 -1
- package/dist/ts/ts/akarisub.d.ts.map +0 -1
- package/dist/ts/ts/akarisub.js +0 -1073
- package/dist/ts/ts/akarisub.js.map +0 -1
- package/dist/ts/ts/types.d.ts.map +0 -1
- package/dist/ts/ts/types.js +0 -5
- package/dist/ts/ts/utils.d.ts.map +0 -1
- package/dist/ts/ts/utils.js.map +0 -1
- package/dist/ts/ts/webgl2-renderer.d.ts.map +0 -1
- package/dist/ts/ts/webgl2-renderer.js.map +0 -1
- package/dist/ts/ts/webgpu-renderer.d.ts.map +0 -1
- package/dist/ts/ts/webgpu-renderer.js.map +0 -1
- package/dist/ts/ts/worker.d.ts +0 -6
- package/dist/ts/ts/worker.js.map +0 -1
- package/dist/ts/wrapper.d.ts +0 -8
- package/dist/ts/wrapper.d.ts.map +0 -1
- package/dist/ts/wrapper.js +0 -9
- package/dist/ts/wrapper.js.map +0 -1
- package/src/ts/akarisub.ts +0 -1272
- package/src/ts/types.ts +0 -397
- package/src/ts/utils.ts +0 -512
- package/src/ts/webgl2-renderer.ts +0 -426
- package/src/ts/webgpu-renderer.ts +0 -691
- package/src/ts/worker.ts +0 -2052
- /package/{dist → assets}/default.woff2 +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AkariSub Worker - TypeScript implementation.
|
|
3
|
-
* Runs in a Web Worker to offload subtitle rendering from the main thread.
|
|
4
|
-
*/
|
|
5
1
|
/// <reference lib="webworker" />
|
|
6
|
-
// @ts-ignore - WASM module is aliased during build
|
|
7
|
-
import WASM from 'wasm';
|
|
8
2
|
import { parseAss, dropBlur, fixPlayRes, libassYCbCrMap } from './utils';
|
|
3
|
+
import { glueUrlFromWasmUrl } from './wasm';
|
|
4
|
+
import { compensatedMediaTime, isStalePresentation, updateTimingCompensation } from './timing';
|
|
9
5
|
let lastCurrentTime = 0;
|
|
6
|
+
let lastRenderedRequestTime = Number.NaN;
|
|
7
|
+
let lastRenderedRequestWidth = 0;
|
|
8
|
+
let lastRenderedRequestHeight = 0;
|
|
10
9
|
let rate = 1;
|
|
11
10
|
let rafId = null;
|
|
12
11
|
let nextIsRaf = false;
|
|
@@ -14,21 +13,29 @@ const nowMs = () => (typeof performance !== 'undefined' ? performance.now() : Da
|
|
|
14
13
|
let lastCurrentTimeReceivedAt = nowMs();
|
|
15
14
|
let targetFps = 24;
|
|
16
15
|
let onDemandRenderMode = false;
|
|
16
|
+
let frameTimelineMode = false;
|
|
17
|
+
let adaptiveTiming = true;
|
|
18
|
+
let configuredRenderAheadSeconds = 0;
|
|
19
|
+
let fallbackTimingCompensationSeconds = 0;
|
|
20
|
+
let rawAssImageGpuEnabled = false;
|
|
17
21
|
let useLocalFonts = false;
|
|
22
|
+
let useFontconfigProvider = true;
|
|
18
23
|
let blendMode = 'wasm';
|
|
19
24
|
let availableFonts = {};
|
|
20
25
|
const fontMap_ = {};
|
|
21
26
|
let attachedFontId = 0; // For attached/preloaded fonts (higher priority)
|
|
22
27
|
let fallbackFontId = 0; // For fallback fonts (lower priority)
|
|
23
|
-
const
|
|
28
|
+
const MAX_FONT_BYTES = 32 * 1024 * 1024;
|
|
29
|
+
const FONT_FETCH_TIMEOUT_MS = 30_000;
|
|
30
|
+
const pendingFontFamilies = new Set();
|
|
24
31
|
let debug = false;
|
|
25
32
|
let clampPos = false;
|
|
26
33
|
let renderInFlight = false;
|
|
34
|
+
let latestPresentationId = 0;
|
|
27
35
|
const MAX_QUEUED_RENDERS = 3;
|
|
28
36
|
const queuedRenders = [];
|
|
29
37
|
self.width = 0;
|
|
30
38
|
self.height = 0;
|
|
31
|
-
// Performance metrics
|
|
32
39
|
const metrics = {
|
|
33
40
|
framesRendered: 0,
|
|
34
41
|
framesDropped: 0,
|
|
@@ -41,7 +48,9 @@ const metrics = {
|
|
|
41
48
|
totalEvents: 0,
|
|
42
49
|
currentEventIndex: 0,
|
|
43
50
|
cacheHits: 0,
|
|
44
|
-
cacheMisses: 0
|
|
51
|
+
cacheMisses: 0,
|
|
52
|
+
lastImageCount: 0,
|
|
53
|
+
lastImagePixels: 0
|
|
45
54
|
};
|
|
46
55
|
const resetMetrics = () => {
|
|
47
56
|
metrics.framesRendered = 0;
|
|
@@ -50,6 +59,8 @@ const resetMetrics = () => {
|
|
|
50
59
|
metrics.maxRenderTime = 0;
|
|
51
60
|
metrics.minRenderTime = Infinity;
|
|
52
61
|
metrics.lastRenderTime = 0;
|
|
62
|
+
metrics.lastImageCount = 0;
|
|
63
|
+
metrics.lastImagePixels = 0;
|
|
53
64
|
metrics.cacheHits = 0;
|
|
54
65
|
metrics.cacheMisses = 0;
|
|
55
66
|
};
|
|
@@ -58,19 +69,25 @@ let asyncRenderOptions = true;
|
|
|
58
69
|
let offCanvas = null;
|
|
59
70
|
let offCanvasCtx = null;
|
|
60
71
|
let offscreenRender = false;
|
|
72
|
+
let rawAssWebGL2Renderer = null;
|
|
61
73
|
let bufferCanvas = null;
|
|
62
74
|
let bufferCtx = null;
|
|
75
|
+
let preparedCanvas = null;
|
|
76
|
+
let preparedCtx = null;
|
|
77
|
+
let preparedBufferCanvas = null;
|
|
78
|
+
let preparedBufferCtx = null;
|
|
63
79
|
let akariSubHandle = 0;
|
|
64
80
|
let subtitleColorSpace = null;
|
|
65
81
|
let dropAllBlur = false;
|
|
66
82
|
let fullTrackWarmupEnabled = false;
|
|
67
83
|
let hasBitmapBug = false;
|
|
68
84
|
let _Module = null;
|
|
69
|
-
let
|
|
85
|
+
let forceNextRender = false;
|
|
86
|
+
let trackGeneration = 0;
|
|
87
|
+
let fullTrackWarmupGeneration = 0;
|
|
70
88
|
const TEXT_ENCODER = new TextEncoder();
|
|
71
89
|
const TEXT_DECODER = new TextDecoder();
|
|
72
90
|
let akariSubApi = null;
|
|
73
|
-
// Pre-allocated object pool for render results
|
|
74
91
|
const MAX_POOLED_IMAGES = 128;
|
|
75
92
|
const RENDER_COLLECT_MAX_IMAGES = Math.max(MAX_POOLED_IMAGES, 4096);
|
|
76
93
|
const PREWARM_MAX_IMAGES = RENDER_COLLECT_MAX_IMAGES;
|
|
@@ -84,10 +101,12 @@ const FULL_WARMUP_YIELD_EVERY = 24;
|
|
|
84
101
|
const ASS_TIME_SCALE = 1000;
|
|
85
102
|
const imagePool = [];
|
|
86
103
|
let poolInitialized = false;
|
|
87
|
-
// Batch render-collect buffer: 3 header ints (changed, count, time) +
|
|
104
|
+
// Batch render-collect buffer: 3 header ints (changed, count, time) + metadata per image.
|
|
105
|
+
// RGBA paths use 5 ints per image (x, y, w, h, image_ptr); raw ASS_Image
|
|
106
|
+
// GPU path uses 8 ints (dst_x, dst_y, w, h, bitmap_ptr, color, stride, type).
|
|
88
107
|
const RRC_HEADER_INTS = 3;
|
|
89
108
|
const RRC_IMG_STRIDE = 5;
|
|
90
|
-
|
|
109
|
+
const RAW_RRC_IMG_STRIDE = 8;
|
|
91
110
|
let rrcBufPtr = 0;
|
|
92
111
|
let rrcBufCapacity = 0;
|
|
93
112
|
// Cached views over the render-collect buffer; refreshed when the wasm memory
|
|
@@ -98,6 +117,7 @@ let rrcViewsBuffer = null;
|
|
|
98
117
|
let rrcViewsPtr = 0;
|
|
99
118
|
let rrcViewsCapacity = 0;
|
|
100
119
|
const frameImages = [];
|
|
120
|
+
const frameRawAssImages = [];
|
|
101
121
|
const frameArrayBuffers = [];
|
|
102
122
|
const frameBitmapPromises = [];
|
|
103
123
|
let warmupTimer = null;
|
|
@@ -106,8 +126,365 @@ let warmupEndTime = 0;
|
|
|
106
126
|
let warmupEnabled = false;
|
|
107
127
|
let firstTrackEventStartTime = null;
|
|
108
128
|
let fullTrackWarmupPromise = null;
|
|
129
|
+
let fullTrackWarmupStarted = false;
|
|
130
|
+
let blockingFullTrackWarmup = false;
|
|
131
|
+
let fullTrackWarmupStepSeconds = FULL_WARMUP_STEP_SECONDS;
|
|
109
132
|
let protectedTrackContent = false;
|
|
110
133
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
134
|
+
const RAW_ASS_MAX_TEXTURE_ARRAY_LAYERS = 128;
|
|
135
|
+
const RAW_ASS_INSTANCE_FLOATS = 12;
|
|
136
|
+
const RAW_ASS_VERTEX_SHADER = `#version 300 es
|
|
137
|
+
precision highp float;
|
|
138
|
+
|
|
139
|
+
in vec4 a_destRect;
|
|
140
|
+
in vec4 a_texInfo;
|
|
141
|
+
in vec4 a_color;
|
|
142
|
+
|
|
143
|
+
uniform vec2 u_resolution;
|
|
144
|
+
|
|
145
|
+
out vec2 v_destXY;
|
|
146
|
+
flat out int v_texIndex;
|
|
147
|
+
flat out vec2 v_texSize;
|
|
148
|
+
flat out vec4 v_color;
|
|
149
|
+
|
|
150
|
+
vec2 quadPos(int id) {
|
|
151
|
+
if (id == 0) return vec2(0.0, 0.0);
|
|
152
|
+
if (id == 1) return vec2(1.0, 0.0);
|
|
153
|
+
if (id == 2) return vec2(0.0, 1.0);
|
|
154
|
+
if (id == 3) return vec2(1.0, 0.0);
|
|
155
|
+
if (id == 4) return vec2(1.0, 1.0);
|
|
156
|
+
return vec2(0.0, 1.0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
void main() {
|
|
160
|
+
vec2 qp = quadPos(gl_VertexID);
|
|
161
|
+
vec2 pixelPos = a_destRect.xy + qp * a_destRect.zw;
|
|
162
|
+
vec2 clip = (pixelPos / u_resolution) * 2.0 - 1.0;
|
|
163
|
+
clip.y = -clip.y;
|
|
164
|
+
gl_Position = vec4(clip, 0.0, 1.0);
|
|
165
|
+
v_destXY = a_destRect.xy;
|
|
166
|
+
v_texIndex = int(a_texInfo.z);
|
|
167
|
+
v_texSize = a_destRect.zw;
|
|
168
|
+
v_color = a_color;
|
|
169
|
+
}
|
|
170
|
+
`;
|
|
171
|
+
const RAW_ASS_FRAGMENT_SHADER = `#version 300 es
|
|
172
|
+
precision highp float;
|
|
173
|
+
precision highp sampler2DArray;
|
|
174
|
+
|
|
175
|
+
uniform sampler2DArray u_maskArray;
|
|
176
|
+
uniform vec2 u_resolution;
|
|
177
|
+
|
|
178
|
+
in vec2 v_destXY;
|
|
179
|
+
flat in int v_texIndex;
|
|
180
|
+
flat in vec2 v_texSize;
|
|
181
|
+
flat in vec4 v_color;
|
|
182
|
+
|
|
183
|
+
out vec4 fragColor;
|
|
184
|
+
|
|
185
|
+
void main() {
|
|
186
|
+
vec2 fragPos = vec2(gl_FragCoord.x, u_resolution.y - gl_FragCoord.y);
|
|
187
|
+
ivec2 texCoord = ivec2(floor(fragPos - v_destXY));
|
|
188
|
+
ivec2 texSizeI = ivec2(v_texSize);
|
|
189
|
+
if (texCoord.x < 0 || texCoord.y < 0 || texCoord.x >= texSizeI.x || texCoord.y >= texSizeI.y) {
|
|
190
|
+
discard;
|
|
191
|
+
}
|
|
192
|
+
float coverage = texelFetch(u_maskArray, ivec3(texCoord, v_texIndex), 0).r;
|
|
193
|
+
float alpha = coverage * v_color.a;
|
|
194
|
+
fragColor = vec4(v_color.rgb * alpha, alpha);
|
|
195
|
+
}
|
|
196
|
+
`;
|
|
197
|
+
function compileRawAssShader(gl, type, source) {
|
|
198
|
+
const shader = gl.createShader(type);
|
|
199
|
+
if (!shader)
|
|
200
|
+
throw new Error('Failed to create WebGL2 shader');
|
|
201
|
+
gl.shaderSource(shader, source);
|
|
202
|
+
gl.compileShader(shader);
|
|
203
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
204
|
+
const info = gl.getShaderInfoLog(shader);
|
|
205
|
+
gl.deleteShader(shader);
|
|
206
|
+
throw new Error(`Raw ASS WebGL2 shader compilation failed: ${info}`);
|
|
207
|
+
}
|
|
208
|
+
return shader;
|
|
209
|
+
}
|
|
210
|
+
class RawASSImageWebGL2Renderer {
|
|
211
|
+
_canvas = null;
|
|
212
|
+
_gl = null;
|
|
213
|
+
_program = null;
|
|
214
|
+
_vao = null;
|
|
215
|
+
_instanceBuffer = null;
|
|
216
|
+
_maskArray = null;
|
|
217
|
+
_resolutionLoc = null;
|
|
218
|
+
_texWidth = 0;
|
|
219
|
+
_texHeight = 0;
|
|
220
|
+
_texLayers = 0;
|
|
221
|
+
_lastWidth = 0;
|
|
222
|
+
_lastHeight = 0;
|
|
223
|
+
_instanceData = new Float32Array(RAW_ASS_MAX_TEXTURE_ARRAY_LAYERS * RAW_ASS_INSTANCE_FLOATS);
|
|
224
|
+
_maskScratch = new Uint8Array(0);
|
|
225
|
+
init(canvas, width, height) {
|
|
226
|
+
this._canvas = canvas;
|
|
227
|
+
const gl = canvas.getContext('webgl2', {
|
|
228
|
+
alpha: true,
|
|
229
|
+
premultipliedAlpha: true,
|
|
230
|
+
antialias: false,
|
|
231
|
+
depth: false,
|
|
232
|
+
preserveDrawingBuffer: false,
|
|
233
|
+
stencil: false,
|
|
234
|
+
powerPreference: 'high-performance'
|
|
235
|
+
});
|
|
236
|
+
if (!gl)
|
|
237
|
+
return false;
|
|
238
|
+
this._gl = gl;
|
|
239
|
+
const vert = compileRawAssShader(gl, gl.VERTEX_SHADER, RAW_ASS_VERTEX_SHADER);
|
|
240
|
+
const frag = compileRawAssShader(gl, gl.FRAGMENT_SHADER, RAW_ASS_FRAGMENT_SHADER);
|
|
241
|
+
const program = gl.createProgram();
|
|
242
|
+
if (!program)
|
|
243
|
+
throw new Error('Failed to create WebGL2 program');
|
|
244
|
+
gl.attachShader(program, vert);
|
|
245
|
+
gl.attachShader(program, frag);
|
|
246
|
+
gl.linkProgram(program);
|
|
247
|
+
gl.deleteShader(vert);
|
|
248
|
+
gl.deleteShader(frag);
|
|
249
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
250
|
+
throw new Error(`Raw ASS WebGL2 program link failed: ${gl.getProgramInfoLog(program)}`);
|
|
251
|
+
}
|
|
252
|
+
this._program = program;
|
|
253
|
+
this._resolutionLoc = gl.getUniformLocation(program, 'u_resolution');
|
|
254
|
+
this._vao = gl.createVertexArray();
|
|
255
|
+
this._instanceBuffer = gl.createBuffer();
|
|
256
|
+
if (!this._vao || !this._instanceBuffer)
|
|
257
|
+
throw new Error('Failed to create WebGL2 buffers');
|
|
258
|
+
gl.bindVertexArray(this._vao);
|
|
259
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this._instanceBuffer);
|
|
260
|
+
gl.bufferData(gl.ARRAY_BUFFER, this._instanceData.byteLength, gl.DYNAMIC_DRAW);
|
|
261
|
+
const stride = RAW_ASS_INSTANCE_FLOATS * Float32Array.BYTES_PER_ELEMENT;
|
|
262
|
+
const aDestRect = gl.getAttribLocation(program, 'a_destRect');
|
|
263
|
+
gl.enableVertexAttribArray(aDestRect);
|
|
264
|
+
gl.vertexAttribPointer(aDestRect, 4, gl.FLOAT, false, stride, 0);
|
|
265
|
+
gl.vertexAttribDivisor(aDestRect, 1);
|
|
266
|
+
const aTexInfo = gl.getAttribLocation(program, 'a_texInfo');
|
|
267
|
+
gl.enableVertexAttribArray(aTexInfo);
|
|
268
|
+
gl.vertexAttribPointer(aTexInfo, 4, gl.FLOAT, false, stride, 4 * Float32Array.BYTES_PER_ELEMENT);
|
|
269
|
+
gl.vertexAttribDivisor(aTexInfo, 1);
|
|
270
|
+
const aColor = gl.getAttribLocation(program, 'a_color');
|
|
271
|
+
gl.enableVertexAttribArray(aColor);
|
|
272
|
+
gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, stride, 8 * Float32Array.BYTES_PER_ELEMENT);
|
|
273
|
+
gl.vertexAttribDivisor(aColor, 1);
|
|
274
|
+
gl.bindVertexArray(null);
|
|
275
|
+
gl.enable(gl.BLEND);
|
|
276
|
+
gl.blendEquation(gl.FUNC_ADD);
|
|
277
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
278
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
279
|
+
this._allocateTextureArray(256, 256, RAW_ASS_MAX_TEXTURE_ARRAY_LAYERS);
|
|
280
|
+
this.updateSize(width, height);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
_roundDim(n) {
|
|
284
|
+
return (Math.max(n, 64) + 63) & ~63;
|
|
285
|
+
}
|
|
286
|
+
_roundLayers(n) {
|
|
287
|
+
return Math.min((Math.max(n, 8) + 7) & ~7, RAW_ASS_MAX_TEXTURE_ARRAY_LAYERS);
|
|
288
|
+
}
|
|
289
|
+
_allocateTextureArray(width, height, layers) {
|
|
290
|
+
const gl = this._gl;
|
|
291
|
+
const w = this._roundDim(width);
|
|
292
|
+
const h = this._roundDim(height);
|
|
293
|
+
const l = this._roundLayers(layers);
|
|
294
|
+
if (this._maskArray)
|
|
295
|
+
gl.deleteTexture(this._maskArray);
|
|
296
|
+
this._maskArray = gl.createTexture();
|
|
297
|
+
if (!this._maskArray)
|
|
298
|
+
throw new Error('Failed to create WebGL2 mask texture array');
|
|
299
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this._maskArray);
|
|
300
|
+
gl.texImage3D(gl.TEXTURE_2D_ARRAY, 0, gl.R8, w, h, l, 0, gl.RED, gl.UNSIGNED_BYTE, null);
|
|
301
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
|
302
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
|
303
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
304
|
+
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
305
|
+
this._texWidth = w;
|
|
306
|
+
this._texHeight = h;
|
|
307
|
+
this._texLayers = l;
|
|
308
|
+
}
|
|
309
|
+
_ensureTextureArray(maxW, maxH, count) {
|
|
310
|
+
const c = Math.min(count, RAW_ASS_MAX_TEXTURE_ARRAY_LAYERS);
|
|
311
|
+
if (maxW <= this._texWidth && maxH <= this._texHeight && c <= this._texLayers)
|
|
312
|
+
return;
|
|
313
|
+
this._allocateTextureArray(Math.max(this._texWidth, maxW), Math.max(this._texHeight, maxH), Math.max(this._texLayers, c));
|
|
314
|
+
}
|
|
315
|
+
updateSize(width, height) {
|
|
316
|
+
if (!this._gl || !this._canvas || width <= 0 || height <= 0)
|
|
317
|
+
return;
|
|
318
|
+
if (this._lastWidth === width && this._lastHeight === height)
|
|
319
|
+
return;
|
|
320
|
+
this._canvas.width = width;
|
|
321
|
+
this._canvas.height = height;
|
|
322
|
+
this._gl.viewport(0, 0, width, height);
|
|
323
|
+
this._lastWidth = width;
|
|
324
|
+
this._lastHeight = height;
|
|
325
|
+
}
|
|
326
|
+
_uploadMaskRegion(w, h, stride, ptr, layer, heap) {
|
|
327
|
+
const gl = this._gl;
|
|
328
|
+
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, stride > 0 ? stride : w);
|
|
329
|
+
gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, layer, w, h, 1, gl.RED, gl.UNSIGNED_BYTE, heap, ptr);
|
|
330
|
+
}
|
|
331
|
+
_uploadMask(img, layer, heap) {
|
|
332
|
+
this._uploadMaskRegion(img.w, img.h, img.stride, img.bitmap, layer, heap);
|
|
333
|
+
}
|
|
334
|
+
renderRawAssImages(images, heap, width, height) {
|
|
335
|
+
if (!this._gl || !this._program || !this._maskArray || !this._instanceBuffer || !this._vao)
|
|
336
|
+
return;
|
|
337
|
+
this.updateSize(width, height);
|
|
338
|
+
let maxW = 0;
|
|
339
|
+
let maxH = 0;
|
|
340
|
+
for (const img of images) {
|
|
341
|
+
if (img.w > maxW)
|
|
342
|
+
maxW = img.w;
|
|
343
|
+
if (img.h > maxH)
|
|
344
|
+
maxH = img.h;
|
|
345
|
+
}
|
|
346
|
+
this._ensureTextureArray(maxW, maxH, Math.min(images.length, RAW_ASS_MAX_TEXTURE_ARRAY_LAYERS));
|
|
347
|
+
const gl = this._gl;
|
|
348
|
+
gl.clearColor(0, 0, 0, 0);
|
|
349
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
350
|
+
if (!images.length)
|
|
351
|
+
return;
|
|
352
|
+
gl.useProgram(this._program);
|
|
353
|
+
gl.uniform2f(this._resolutionLoc, width, height);
|
|
354
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
355
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this._maskArray);
|
|
356
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
357
|
+
let imageIndex = 0;
|
|
358
|
+
while (imageIndex < images.length) {
|
|
359
|
+
let count = 0;
|
|
360
|
+
while (imageIndex < images.length && count < RAW_ASS_MAX_TEXTURE_ARRAY_LAYERS) {
|
|
361
|
+
const img = images[imageIndex++];
|
|
362
|
+
if (img.w <= 0 || img.h <= 0 || img.bitmap <= 0)
|
|
363
|
+
continue;
|
|
364
|
+
const color = img.color >>> 0;
|
|
365
|
+
const opacity = (255 - (color & 0xff)) / 255;
|
|
366
|
+
if (opacity <= 0)
|
|
367
|
+
continue;
|
|
368
|
+
this._uploadMask(img, count, heap);
|
|
369
|
+
const off = count * RAW_ASS_INSTANCE_FLOATS;
|
|
370
|
+
this._instanceData[off] = img.dst_x;
|
|
371
|
+
this._instanceData[off + 1] = img.dst_y;
|
|
372
|
+
this._instanceData[off + 2] = img.w;
|
|
373
|
+
this._instanceData[off + 3] = img.h;
|
|
374
|
+
this._instanceData[off + 4] = img.w;
|
|
375
|
+
this._instanceData[off + 5] = img.h;
|
|
376
|
+
this._instanceData[off + 6] = count;
|
|
377
|
+
this._instanceData[off + 7] = 0;
|
|
378
|
+
this._instanceData[off + 8] = ((color >>> 24) & 0xff) / 255;
|
|
379
|
+
this._instanceData[off + 9] = ((color >>> 16) & 0xff) / 255;
|
|
380
|
+
this._instanceData[off + 10] = ((color >>> 8) & 0xff) / 255;
|
|
381
|
+
this._instanceData[off + 11] = opacity;
|
|
382
|
+
count++;
|
|
383
|
+
}
|
|
384
|
+
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 0);
|
|
385
|
+
if (count === 0)
|
|
386
|
+
continue;
|
|
387
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this._instanceBuffer);
|
|
388
|
+
gl.bufferData(gl.ARRAY_BUFFER, this._instanceData.subarray(0, count * RAW_ASS_INSTANCE_FLOATS), gl.DYNAMIC_DRAW);
|
|
389
|
+
gl.bindVertexArray(this._vao);
|
|
390
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count);
|
|
391
|
+
gl.bindVertexArray(null);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
renderRawAssMetadata(meta, imageCount, heap, width, height) {
|
|
395
|
+
if (!this._gl || !this._program || !this._maskArray || !this._instanceBuffer || !this._vao)
|
|
396
|
+
return 0;
|
|
397
|
+
this.updateSize(width, height);
|
|
398
|
+
let maxW = 0;
|
|
399
|
+
let maxH = 0;
|
|
400
|
+
let validImages = 0;
|
|
401
|
+
for (let i = 0; i < imageCount; i++) {
|
|
402
|
+
const off = i * RAW_RRC_IMG_STRIDE;
|
|
403
|
+
const w = meta[off + 2];
|
|
404
|
+
const h = meta[off + 3];
|
|
405
|
+
const ptr = meta[off + 4];
|
|
406
|
+
if (w <= 0 || h <= 0 || ptr <= 0)
|
|
407
|
+
continue;
|
|
408
|
+
const color = meta[off + 5] >>> 0;
|
|
409
|
+
if (255 - (color & 0xff) <= 0)
|
|
410
|
+
continue;
|
|
411
|
+
if (w > maxW)
|
|
412
|
+
maxW = w;
|
|
413
|
+
if (h > maxH)
|
|
414
|
+
maxH = h;
|
|
415
|
+
validImages++;
|
|
416
|
+
}
|
|
417
|
+
this._ensureTextureArray(maxW, maxH, Math.min(validImages, RAW_ASS_MAX_TEXTURE_ARRAY_LAYERS));
|
|
418
|
+
const gl = this._gl;
|
|
419
|
+
gl.clearColor(0, 0, 0, 0);
|
|
420
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
421
|
+
if (validImages === 0)
|
|
422
|
+
return 0;
|
|
423
|
+
gl.useProgram(this._program);
|
|
424
|
+
gl.uniform2f(this._resolutionLoc, width, height);
|
|
425
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
426
|
+
gl.bindTexture(gl.TEXTURE_2D_ARRAY, this._maskArray);
|
|
427
|
+
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
|
428
|
+
let imageIndex = 0;
|
|
429
|
+
let pixels = 0;
|
|
430
|
+
while (imageIndex < imageCount) {
|
|
431
|
+
let count = 0;
|
|
432
|
+
while (imageIndex < imageCount && count < RAW_ASS_MAX_TEXTURE_ARRAY_LAYERS) {
|
|
433
|
+
const metaOffset = imageIndex++ * RAW_RRC_IMG_STRIDE;
|
|
434
|
+
const w = meta[metaOffset + 2];
|
|
435
|
+
const h = meta[metaOffset + 3];
|
|
436
|
+
const ptr = meta[metaOffset + 4];
|
|
437
|
+
if (w <= 0 || h <= 0 || ptr <= 0)
|
|
438
|
+
continue;
|
|
439
|
+
const color = meta[metaOffset + 5] >>> 0;
|
|
440
|
+
const opacity = (255 - (color & 0xff)) / 255;
|
|
441
|
+
if (opacity <= 0)
|
|
442
|
+
continue;
|
|
443
|
+
const stride = meta[metaOffset + 6];
|
|
444
|
+
this._uploadMaskRegion(w, h, stride, ptr, count, heap);
|
|
445
|
+
const off = count * RAW_ASS_INSTANCE_FLOATS;
|
|
446
|
+
this._instanceData[off] = meta[metaOffset];
|
|
447
|
+
this._instanceData[off + 1] = meta[metaOffset + 1];
|
|
448
|
+
this._instanceData[off + 2] = w;
|
|
449
|
+
this._instanceData[off + 3] = h;
|
|
450
|
+
this._instanceData[off + 4] = w;
|
|
451
|
+
this._instanceData[off + 5] = h;
|
|
452
|
+
this._instanceData[off + 6] = count;
|
|
453
|
+
this._instanceData[off + 7] = 0;
|
|
454
|
+
this._instanceData[off + 8] = ((color >>> 24) & 0xff) / 255;
|
|
455
|
+
this._instanceData[off + 9] = ((color >>> 16) & 0xff) / 255;
|
|
456
|
+
this._instanceData[off + 10] = ((color >>> 8) & 0xff) / 255;
|
|
457
|
+
this._instanceData[off + 11] = opacity;
|
|
458
|
+
pixels += w * h;
|
|
459
|
+
count++;
|
|
460
|
+
}
|
|
461
|
+
gl.pixelStorei(gl.UNPACK_ROW_LENGTH, 0);
|
|
462
|
+
if (count === 0)
|
|
463
|
+
continue;
|
|
464
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this._instanceBuffer);
|
|
465
|
+
gl.bufferData(gl.ARRAY_BUFFER, this._instanceData.subarray(0, count * RAW_ASS_INSTANCE_FLOATS), gl.DYNAMIC_DRAW);
|
|
466
|
+
gl.bindVertexArray(this._vao);
|
|
467
|
+
gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, count);
|
|
468
|
+
gl.bindVertexArray(null);
|
|
469
|
+
}
|
|
470
|
+
return pixels;
|
|
471
|
+
}
|
|
472
|
+
destroy() {
|
|
473
|
+
const gl = this._gl;
|
|
474
|
+
if (gl) {
|
|
475
|
+
gl.deleteProgram(this._program);
|
|
476
|
+
gl.deleteVertexArray(this._vao);
|
|
477
|
+
gl.deleteBuffer(this._instanceBuffer);
|
|
478
|
+
gl.deleteTexture(this._maskArray);
|
|
479
|
+
}
|
|
480
|
+
this._canvas = null;
|
|
481
|
+
this._gl = null;
|
|
482
|
+
this._program = null;
|
|
483
|
+
this._vao = null;
|
|
484
|
+
this._instanceBuffer = null;
|
|
485
|
+
this._maskArray = null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
111
488
|
const initPool = () => {
|
|
112
489
|
if (poolInitialized)
|
|
113
490
|
return;
|
|
@@ -119,7 +496,6 @@ const initPool = () => {
|
|
|
119
496
|
const getPooledItem = (index) => {
|
|
120
497
|
let item = imagePool[index];
|
|
121
498
|
if (!item) {
|
|
122
|
-
// Grow the pool on demand so frames with many images reuse items too
|
|
123
499
|
item = { w: 0, h: 0, x: 0, y: 0, image: 0 };
|
|
124
500
|
imagePool[index] = item;
|
|
125
501
|
}
|
|
@@ -130,10 +506,10 @@ const getPooledItem = (index) => {
|
|
|
130
506
|
* Layout: [changed, count, time, (x, y, w, h, image_ptr) * N]
|
|
131
507
|
* = 3 + 5*N ints
|
|
132
508
|
*/
|
|
133
|
-
const ensureRenderCollectBuffer = (maxImages) => {
|
|
509
|
+
const ensureRenderCollectBuffer = (maxImages, imageStride = RRC_IMG_STRIDE) => {
|
|
134
510
|
if (!_Module || maxImages <= 0)
|
|
135
511
|
return;
|
|
136
|
-
const totalInts = RRC_HEADER_INTS +
|
|
512
|
+
const totalInts = RRC_HEADER_INTS + imageStride * maxImages;
|
|
137
513
|
if (rrcBufCapacity >= totalInts && rrcBufPtr)
|
|
138
514
|
return;
|
|
139
515
|
const nextCapacity = Math.max(totalInts, (rrcBufCapacity || 64) * 2);
|
|
@@ -161,8 +537,8 @@ const refreshRrcViews = () => {
|
|
|
161
537
|
rrcViewsPtr = rrcBufPtr;
|
|
162
538
|
rrcViewsCapacity = rrcBufCapacity;
|
|
163
539
|
};
|
|
164
|
-
const prewarmRenderer = (time) => {
|
|
165
|
-
if (!akariSubHandle)
|
|
540
|
+
const prewarmRenderer = (time, generation = trackGeneration) => {
|
|
541
|
+
if (!akariSubHandle || generation !== trackGeneration)
|
|
166
542
|
return;
|
|
167
543
|
const api = requireApi();
|
|
168
544
|
const handle = requireHandle();
|
|
@@ -173,6 +549,10 @@ const prewarmRenderer = (time) => {
|
|
|
173
549
|
else {
|
|
174
550
|
api.renderImageCollect(handle, time, 0, rrcBufPtr, rrcBufCapacity);
|
|
175
551
|
}
|
|
552
|
+
// Prewarming uses the visible libass renderer and can advance libass'
|
|
553
|
+
// internal changed-frame baseline without painting the result. Force the next
|
|
554
|
+
// visible render so a changed=0 response cannot leave stale/blank output.
|
|
555
|
+
markNextRenderForced();
|
|
176
556
|
};
|
|
177
557
|
const syncTotalEventsMetric = () => {
|
|
178
558
|
metrics.totalEvents = akariSubHandle ? requireApi().getEventCount(akariSubHandle) : 0;
|
|
@@ -205,6 +585,25 @@ let emptyWindowUntil = -1;
|
|
|
205
585
|
const invalidateEmptyWindow = () => {
|
|
206
586
|
emptyWindowFrom = -1;
|
|
207
587
|
emptyWindowUntil = -1;
|
|
588
|
+
lastRenderedRequestTime = Number.NaN;
|
|
589
|
+
};
|
|
590
|
+
const markNextRenderForced = () => {
|
|
591
|
+
forceNextRender = true;
|
|
592
|
+
invalidateEmptyWindow();
|
|
593
|
+
};
|
|
594
|
+
const consumeNextRenderForce = () => {
|
|
595
|
+
if (!forceNextRender)
|
|
596
|
+
return 0;
|
|
597
|
+
forceNextRender = false;
|
|
598
|
+
return 1;
|
|
599
|
+
};
|
|
600
|
+
const advanceTrackGeneration = () => {
|
|
601
|
+
trackGeneration++;
|
|
602
|
+
fullTrackWarmupGeneration = trackGeneration;
|
|
603
|
+
fullTrackWarmupPromise = null;
|
|
604
|
+
fullTrackWarmupStarted = false;
|
|
605
|
+
markNextRenderForced();
|
|
606
|
+
return trackGeneration;
|
|
208
607
|
};
|
|
209
608
|
const computeEmptyWindow = (time) => {
|
|
210
609
|
if (!akariSubHandle || !_Module)
|
|
@@ -223,28 +622,32 @@ const computeEmptyWindow = (time) => {
|
|
|
223
622
|
_Module._free(outPtr);
|
|
224
623
|
}
|
|
225
624
|
};
|
|
226
|
-
const prewarmEntireTrack = async () => {
|
|
227
|
-
if (!akariSubHandle)
|
|
625
|
+
const prewarmEntireTrack = async (generation) => {
|
|
626
|
+
if (!akariSubHandle || generation !== trackGeneration)
|
|
228
627
|
return;
|
|
229
628
|
const range = getTrackEventTimeRange();
|
|
230
|
-
if (!range)
|
|
629
|
+
if (!range || generation !== trackGeneration)
|
|
231
630
|
return;
|
|
232
631
|
const cappedEnd = Math.min(range.end, range.start + FULL_WARMUP_CAP_SECONDS);
|
|
233
632
|
let ticks = 0;
|
|
234
|
-
for (let time = range.start; time <= cappedEnd; time +=
|
|
235
|
-
if (!akariSubHandle)
|
|
633
|
+
for (let time = range.start; time <= cappedEnd; time += fullTrackWarmupStepSeconds) {
|
|
634
|
+
if (!akariSubHandle || generation !== trackGeneration)
|
|
236
635
|
return;
|
|
237
636
|
if (onDemandRenderMode && (renderInFlight || queuedRenders.length > 0 || metrics.pendingRenders > 0)) {
|
|
238
637
|
await sleep(0);
|
|
638
|
+
if (generation !== trackGeneration)
|
|
639
|
+
return;
|
|
239
640
|
continue;
|
|
240
641
|
}
|
|
241
|
-
prewarmRenderer(time);
|
|
642
|
+
prewarmRenderer(time, generation);
|
|
242
643
|
ticks++;
|
|
243
644
|
if (onDemandRenderMode || ticks % FULL_WARMUP_YIELD_EVERY === 0) {
|
|
244
645
|
await sleep(0);
|
|
646
|
+
if (generation !== trackGeneration)
|
|
647
|
+
return;
|
|
245
648
|
}
|
|
246
649
|
}
|
|
247
|
-
prewarmRenderer(cappedEnd);
|
|
650
|
+
prewarmRenderer(cappedEnd, generation);
|
|
248
651
|
};
|
|
249
652
|
const getWarmupAnchorTime = (fallbackTime) => {
|
|
250
653
|
if (firstTrackEventStartTime == null)
|
|
@@ -261,20 +664,30 @@ const stopWarmup = () => {
|
|
|
261
664
|
}
|
|
262
665
|
};
|
|
263
666
|
const scheduleFullTrackWarmup = () => {
|
|
264
|
-
if (
|
|
667
|
+
if (frameTimelineMode ||
|
|
668
|
+
!fullTrackWarmupEnabled ||
|
|
669
|
+
fullTrackWarmupStarted ||
|
|
670
|
+
fullTrackWarmupPromise ||
|
|
671
|
+
!akariSubHandle) {
|
|
265
672
|
return;
|
|
673
|
+
}
|
|
674
|
+
fullTrackWarmupStarted = true;
|
|
675
|
+
const generation = trackGeneration;
|
|
676
|
+
fullTrackWarmupGeneration = generation;
|
|
266
677
|
fullTrackWarmupPromise = (async () => {
|
|
267
678
|
await sleep(0);
|
|
679
|
+
if (generation !== trackGeneration)
|
|
680
|
+
return;
|
|
268
681
|
try {
|
|
269
|
-
await prewarmEntireTrack();
|
|
682
|
+
await prewarmEntireTrack(generation);
|
|
270
683
|
}
|
|
271
684
|
catch (e) {
|
|
272
685
|
if (debug)
|
|
273
686
|
console.warn('[AkariSub] Full track warmup failed, continuing:', e);
|
|
274
687
|
}
|
|
275
688
|
try {
|
|
276
|
-
if (akariSubHandle) {
|
|
277
|
-
prewarmRenderer(getCurrentTime());
|
|
689
|
+
if (akariSubHandle && generation === trackGeneration) {
|
|
690
|
+
prewarmRenderer(getCurrentTime(), generation);
|
|
278
691
|
}
|
|
279
692
|
}
|
|
280
693
|
catch (e) {
|
|
@@ -282,7 +695,9 @@ const scheduleFullTrackWarmup = () => {
|
|
|
282
695
|
console.warn('[AkariSub] Post-warmup re-prime failed, continuing:', e);
|
|
283
696
|
}
|
|
284
697
|
})().finally(() => {
|
|
285
|
-
|
|
698
|
+
if (fullTrackWarmupGeneration === generation) {
|
|
699
|
+
fullTrackWarmupPromise = null;
|
|
700
|
+
}
|
|
286
701
|
});
|
|
287
702
|
};
|
|
288
703
|
const scheduleWarmupTick = () => {
|
|
@@ -494,49 +909,119 @@ const requireHandle = () => {
|
|
|
494
909
|
throw new Error('AkariSub instance is not initialized');
|
|
495
910
|
return akariSubHandle;
|
|
496
911
|
};
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
912
|
+
// Fonts added via addFont are explicitly requested, so they should be attached (high priority).
|
|
913
|
+
// A correlated acknowledgement prevents callers from treating rejected bytes as loaded.
|
|
914
|
+
const readBoundedFontUrl = async (url) => {
|
|
915
|
+
const controller = new AbortController();
|
|
916
|
+
const timeout = setTimeout(() => controller.abort(), FONT_FETCH_TIMEOUT_MS);
|
|
917
|
+
try {
|
|
918
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
919
|
+
if (!response.ok)
|
|
920
|
+
throw new Error(`Font request failed with HTTP ${response.status}`);
|
|
921
|
+
const declaredLength = Number(response.headers.get('content-length'));
|
|
922
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_FONT_BYTES) {
|
|
923
|
+
await response.body?.cancel();
|
|
924
|
+
throw new Error('Font files are limited to 32 MiB');
|
|
925
|
+
}
|
|
926
|
+
if (!response.body) {
|
|
927
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
928
|
+
if (bytes.byteLength > MAX_FONT_BYTES)
|
|
929
|
+
throw new Error('Font files are limited to 32 MiB');
|
|
930
|
+
return bytes;
|
|
931
|
+
}
|
|
932
|
+
const reader = response.body.getReader();
|
|
933
|
+
const chunks = [];
|
|
934
|
+
let total = 0;
|
|
935
|
+
while (true) {
|
|
936
|
+
const { done, value } = await reader.read();
|
|
937
|
+
if (done)
|
|
938
|
+
break;
|
|
939
|
+
total += value.byteLength;
|
|
940
|
+
if (total > MAX_FONT_BYTES) {
|
|
941
|
+
await reader.cancel();
|
|
942
|
+
throw new Error('Font files are limited to 32 MiB');
|
|
943
|
+
}
|
|
944
|
+
chunks.push(value);
|
|
945
|
+
}
|
|
946
|
+
const result = new Uint8Array(total);
|
|
947
|
+
let offset = 0;
|
|
948
|
+
for (const chunk of chunks) {
|
|
949
|
+
result.set(chunk, offset);
|
|
950
|
+
offset += chunk.byteLength;
|
|
951
|
+
}
|
|
952
|
+
return result;
|
|
953
|
+
}
|
|
954
|
+
finally {
|
|
955
|
+
clearTimeout(timeout);
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
self.addFont = async ({ font, requestId }) => {
|
|
959
|
+
const respond = (bytes) => {
|
|
960
|
+
if (bytes.byteLength > MAX_FONT_BYTES) {
|
|
961
|
+
postMessage({ target: 'addFont', requestId, success: false, error: 'Font files are limited to 32 MiB' });
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const success = writeFontToFS(bytes, false);
|
|
965
|
+
postMessage({
|
|
966
|
+
target: 'addFont',
|
|
967
|
+
requestId,
|
|
968
|
+
success,
|
|
969
|
+
error: success ? undefined : 'The renderer rejected the font bytes'
|
|
970
|
+
});
|
|
971
|
+
};
|
|
972
|
+
try {
|
|
973
|
+
respond(typeof font === 'string' ? await readBoundedFontUrl(font) : font);
|
|
974
|
+
}
|
|
975
|
+
catch (cause) {
|
|
976
|
+
const error = cause instanceof Error ? cause.message : 'The font could not be read';
|
|
977
|
+
postMessage({ target: 'addFont', requestId, success: false, error });
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
self.localFontResult = ({ font, success }) => {
|
|
981
|
+
pendingFontFamilies.delete(font);
|
|
982
|
+
if (success)
|
|
983
|
+
fontMap_[font] = true;
|
|
984
|
+
};
|
|
502
985
|
const findAvailableFonts = (font) => {
|
|
503
986
|
font = font.trim().toLowerCase();
|
|
504
987
|
if (font.startsWith('@'))
|
|
505
988
|
font = font.substring(1);
|
|
506
989
|
if (fontMap_[font])
|
|
507
990
|
return;
|
|
508
|
-
fontMap_[font] = true;
|
|
509
991
|
if (!availableFonts[font]) {
|
|
510
|
-
if (useLocalFonts)
|
|
992
|
+
if (useLocalFonts && !pendingFontFamilies.has(font)) {
|
|
993
|
+
pendingFontFamilies.add(font);
|
|
511
994
|
postMessage({ target: 'getLocalFont', font });
|
|
995
|
+
}
|
|
512
996
|
}
|
|
513
997
|
else {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
else {
|
|
524
|
-
writeFontToFS(font, isFallback);
|
|
998
|
+
if (pendingFontFamilies.has(font))
|
|
999
|
+
return;
|
|
1000
|
+
pendingFontFamilies.add(font);
|
|
1001
|
+
asyncWrite(availableFonts[font], true, (success) => {
|
|
1002
|
+
pendingFontFamilies.delete(font);
|
|
1003
|
+
if (success)
|
|
1004
|
+
fontMap_[font] = true;
|
|
1005
|
+
});
|
|
525
1006
|
}
|
|
526
1007
|
};
|
|
527
|
-
|
|
528
|
-
const syncWrite = (font, isFallback = true) => {
|
|
1008
|
+
const asyncWrite = (font, isFallback = true, settled) => {
|
|
529
1009
|
if (typeof font === 'string') {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
1010
|
+
void readBoundedFontUrl(font)
|
|
1011
|
+
.then((fontData) => {
|
|
1012
|
+
const success = writeFontToFS(fontData, isFallback);
|
|
1013
|
+
settled?.(success);
|
|
1014
|
+
})
|
|
1015
|
+
.catch((error) => {
|
|
1016
|
+
console.error(error);
|
|
1017
|
+
settled?.(false);
|
|
1018
|
+
});
|
|
534
1019
|
}
|
|
535
1020
|
else {
|
|
536
|
-
|
|
1021
|
+
const success = writeFontToFS(font, isFallback);
|
|
1022
|
+
settled?.(success);
|
|
537
1023
|
}
|
|
538
1024
|
};
|
|
539
|
-
// Debounced font reload
|
|
540
1025
|
let pendingFontReload = null;
|
|
541
1026
|
const scheduleReloadFonts = () => {
|
|
542
1027
|
if (pendingFontReload)
|
|
@@ -546,6 +1031,7 @@ const scheduleReloadFonts = () => {
|
|
|
546
1031
|
if (akariSubHandle) {
|
|
547
1032
|
const api = requireApi();
|
|
548
1033
|
api.reloadFonts(akariSubHandle);
|
|
1034
|
+
markNextRenderForced();
|
|
549
1035
|
}
|
|
550
1036
|
}, 16);
|
|
551
1037
|
};
|
|
@@ -557,30 +1043,63 @@ const addFontAsEmbedded = (uint8, name) => {
|
|
|
557
1043
|
if (!_Module || !akariSubHandle) {
|
|
558
1044
|
if (debug)
|
|
559
1045
|
console.warn('[AkariSub] Cannot add embedded font, module or AkariSub not ready:', name);
|
|
560
|
-
return;
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
if (uint8.byteLength > MAX_FONT_BYTES) {
|
|
1049
|
+
if (debug)
|
|
1050
|
+
console.warn('[AkariSub] Rejected oversized embedded font:', name);
|
|
1051
|
+
return false;
|
|
561
1052
|
}
|
|
562
1053
|
try {
|
|
563
1054
|
const api = requireApi();
|
|
564
|
-
// Allocate memory in WASM heap and copy font data
|
|
565
1055
|
const ptr = _Module._malloc(uint8.length);
|
|
566
1056
|
if (!ptr) {
|
|
567
1057
|
console.warn('[AkariSub] Failed to allocate memory for embedded font:', name);
|
|
568
|
-
return;
|
|
1058
|
+
return false;
|
|
569
1059
|
}
|
|
570
|
-
// Copy font data to WASM heap
|
|
571
1060
|
self.HEAPU8.set(uint8, ptr);
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
1061
|
+
let handedToNative = false;
|
|
1062
|
+
let accepted = false;
|
|
1063
|
+
try {
|
|
1064
|
+
accepted = withCString(name, (namePtr) => {
|
|
1065
|
+
handedToNative = true;
|
|
1066
|
+
return api.addFont(akariSubHandle, namePtr, ptr, uint8.length) === 1;
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
finally {
|
|
1070
|
+
if (!handedToNative)
|
|
1071
|
+
_Module._free(ptr);
|
|
1072
|
+
}
|
|
1073
|
+
if (!accepted) {
|
|
1074
|
+
if (debug)
|
|
1075
|
+
console.warn('[AkariSub] Rejected embedded font:', name);
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
575
1078
|
if (debug)
|
|
576
1079
|
console.log('[AkariSub] Added embedded font:', name, 'size:', uint8.length);
|
|
1080
|
+
return true;
|
|
577
1081
|
}
|
|
578
1082
|
catch (e) {
|
|
579
1083
|
console.warn('[AkariSub] Failed to add embedded font:', name, e);
|
|
1084
|
+
return false;
|
|
580
1085
|
}
|
|
581
1086
|
};
|
|
582
1087
|
/**
|
|
583
|
-
*
|
|
1088
|
+
* Admit a font through FreeType/libass before retaining a MEMFS copy.
|
|
1089
|
+
*/
|
|
1090
|
+
const admitAndPersistFont = (uint8, fontDir, fontFileName) => {
|
|
1091
|
+
if (!_Module || !akariSubHandle || !addFontAsEmbedded(uint8, fontFileName))
|
|
1092
|
+
return false;
|
|
1093
|
+
try {
|
|
1094
|
+
_Module.FS_createDataFile(fontDir, fontFileName, uint8, true, true, true);
|
|
1095
|
+
}
|
|
1096
|
+
catch (e) {
|
|
1097
|
+
console.warn('Failed to write font to filesystem:', fontDir + '/' + fontFileName, e);
|
|
1098
|
+
}
|
|
1099
|
+
return true;
|
|
1100
|
+
};
|
|
1101
|
+
/**
|
|
1102
|
+
* Write an admitted font to the virtual filesystem so fontconfig can index it.
|
|
584
1103
|
* Fonts are written to separate directories based on priority:
|
|
585
1104
|
* - /fonts/attached: For attached/preloaded fonts (highest priority)
|
|
586
1105
|
* - /fonts/fallback: For fallback fonts
|
|
@@ -588,67 +1107,35 @@ const addFontAsEmbedded = (uint8, name) => {
|
|
|
588
1107
|
const writeFontToFS = (uint8, isFallback = true) => {
|
|
589
1108
|
const fontDir = isFallback ? '/fonts/fallback' : '/fonts/attached';
|
|
590
1109
|
const fontFileName = isFallback ? 'fallback-' + fallbackFontId++ : 'attached-' + attachedFontId++;
|
|
591
|
-
if (
|
|
592
|
-
|
|
593
|
-
_Module.FS_createDataFile(fontDir, fontFileName, uint8, true, true, true);
|
|
594
|
-
}
|
|
595
|
-
catch (e) {
|
|
596
|
-
console.warn('Failed to write font to filesystem:', fontDir + '/' + fontFileName, e);
|
|
597
|
-
}
|
|
598
|
-
if (!isFallback) {
|
|
599
|
-
addFontAsEmbedded(uint8, fontFileName);
|
|
600
|
-
}
|
|
601
|
-
else if (akariSubHandle) {
|
|
602
|
-
addFontAsEmbedded(uint8, fontFileName);
|
|
603
|
-
}
|
|
604
|
-
else {
|
|
605
|
-
pendingFallbackFonts.push({ data: uint8, name: fontFileName });
|
|
606
|
-
}
|
|
607
|
-
}
|
|
1110
|
+
if (!admitAndPersistFont(uint8, fontDir, fontFileName))
|
|
1111
|
+
return false;
|
|
608
1112
|
scheduleReloadFonts();
|
|
1113
|
+
return true;
|
|
609
1114
|
};
|
|
610
1115
|
/**
|
|
611
|
-
* Immediate font write without debounced reload (for synchronous loading).
|
|
1116
|
+
* Immediate admitted-font write without debounced reload (for synchronous loading).
|
|
612
1117
|
*/
|
|
613
1118
|
const writeFontToFSImmediate = (uint8, isFallback = true) => {
|
|
614
1119
|
const fontDir = isFallback ? '/fonts/fallback' : '/fonts/attached';
|
|
615
1120
|
const fontFileName = isFallback ? 'fallback-' + fallbackFontId++ : 'attached-' + attachedFontId++;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
console.log('[AkariSub] Wrote font to FS:', fontDir + '/' + fontFileName, 'size:', uint8.length);
|
|
621
|
-
}
|
|
622
|
-
catch (e) {
|
|
623
|
-
console.warn('Failed to write font to filesystem:', fontDir + '/' + fontFileName, e);
|
|
624
|
-
}
|
|
625
|
-
if (!isFallback) {
|
|
626
|
-
addFontAsEmbedded(uint8, fontFileName);
|
|
627
|
-
}
|
|
628
|
-
else if (akariSubHandle) {
|
|
629
|
-
addFontAsEmbedded(uint8, fontFileName);
|
|
630
|
-
}
|
|
631
|
-
else {
|
|
632
|
-
pendingFallbackFonts.push({ data: uint8, name: fontFileName });
|
|
633
|
-
}
|
|
634
|
-
}
|
|
1121
|
+
const success = admitAndPersistFont(uint8, fontDir, fontFileName);
|
|
1122
|
+
if (success && debug)
|
|
1123
|
+
console.log('[AkariSub] Wrote admitted font to FS:', fontDir + '/' + fontFileName, 'size:', uint8.length);
|
|
1124
|
+
return success;
|
|
635
1125
|
};
|
|
636
1126
|
const processAvailableFonts = (content) => {
|
|
637
1127
|
if (!availableFonts)
|
|
638
1128
|
return;
|
|
639
1129
|
const isLargeFile = content.length > 500000;
|
|
640
1130
|
if (isLargeFile) {
|
|
641
|
-
// Extract only the styles section for large files
|
|
642
1131
|
const stylesMatch = content.match(/\[V4\+?\s*Styles?\][^\[]*(?=\[|$)/i);
|
|
643
1132
|
if (stylesMatch) {
|
|
644
1133
|
const stylesSection = stylesMatch[0];
|
|
645
|
-
// Parse only the styles section
|
|
646
1134
|
const styleFontMatches = stylesSection.matchAll(/^Style:[^,]*,([^,]+)/gm);
|
|
647
1135
|
for (const match of styleFontMatches) {
|
|
648
1136
|
findAvailableFonts(match[1].trim());
|
|
649
1137
|
}
|
|
650
1138
|
}
|
|
651
|
-
// For Events section in large files, limit to first 1000 \fn tags
|
|
652
1139
|
const eventsMatch = content.match(/\[Events\][\s\S]*/i);
|
|
653
1140
|
if (eventsMatch) {
|
|
654
1141
|
const eventsContent = eventsMatch[0];
|
|
@@ -662,7 +1149,6 @@ const processAvailableFonts = (content) => {
|
|
|
662
1149
|
}
|
|
663
1150
|
}
|
|
664
1151
|
else {
|
|
665
|
-
// Original behavior for small files
|
|
666
1152
|
const sections = parseAss(content, true);
|
|
667
1153
|
for (let i = 0; i < sections.length; i++) {
|
|
668
1154
|
for (let j = 0; j < sections[i].body.length; j++) {
|
|
@@ -672,7 +1158,6 @@ const processAvailableFonts = (content) => {
|
|
|
672
1158
|
}
|
|
673
1159
|
}
|
|
674
1160
|
}
|
|
675
|
-
// Use matchAll for Events section
|
|
676
1161
|
const eventsMatch = content.match(/\[Events\][\s\S]*/i);
|
|
677
1162
|
if (eventsMatch) {
|
|
678
1163
|
const eventsContent = eventsMatch[0];
|
|
@@ -683,9 +1168,6 @@ const processAvailableFonts = (content) => {
|
|
|
683
1168
|
}
|
|
684
1169
|
}
|
|
685
1170
|
};
|
|
686
|
-
// =============================================================================
|
|
687
|
-
// Network Utilities
|
|
688
|
-
// =============================================================================
|
|
689
1171
|
const read_ = (url, ab) => {
|
|
690
1172
|
const xhr = new XMLHttpRequest();
|
|
691
1173
|
xhr.open('GET', url, false);
|
|
@@ -705,22 +1187,19 @@ const readAsync = (url, load, err) => {
|
|
|
705
1187
|
xhr.onerror = err;
|
|
706
1188
|
xhr.send(null);
|
|
707
1189
|
};
|
|
708
|
-
// =============================================================================
|
|
709
|
-
// Track Management
|
|
710
|
-
// =============================================================================
|
|
711
1190
|
const finishTrackLoad = () => {
|
|
712
1191
|
const api = requireApi();
|
|
713
1192
|
const handle = requireHandle();
|
|
714
1193
|
syncTotalEventsMetric();
|
|
715
1194
|
firstTrackEventStartTime = getFirstEventStartTime();
|
|
716
1195
|
subtitleColorSpace = libassYCbCrMap[api.getTrackColorSpace(handle)];
|
|
717
|
-
|
|
1196
|
+
markNextRenderForced();
|
|
718
1197
|
postMessage({ target: 'verifyColorSpace', subtitleColorSpace });
|
|
719
1198
|
postMessage({ target: 'trackReady' });
|
|
720
1199
|
};
|
|
721
1200
|
self.setTrack = ({ content }) => {
|
|
722
1201
|
stopWarmup();
|
|
723
|
-
|
|
1202
|
+
advanceTrackGeneration();
|
|
724
1203
|
protectedTrackContent = false;
|
|
725
1204
|
if (isBinaryContent(content)) {
|
|
726
1205
|
createTrackFromBytes(toUint8Array(content));
|
|
@@ -737,7 +1216,7 @@ self.setTrack = ({ content }) => {
|
|
|
737
1216
|
};
|
|
738
1217
|
self.setEncryptedTrack = async ({ content }) => {
|
|
739
1218
|
stopWarmup();
|
|
740
|
-
|
|
1219
|
+
advanceTrackGeneration();
|
|
741
1220
|
protectedTrackContent = true;
|
|
742
1221
|
const decrypted = await decryptSubtitleContent(content);
|
|
743
1222
|
try {
|
|
@@ -753,21 +1232,35 @@ self.getColorSpace = () => {
|
|
|
753
1232
|
};
|
|
754
1233
|
self.freeTrack = () => {
|
|
755
1234
|
stopWarmup();
|
|
756
|
-
|
|
1235
|
+
advanceTrackGeneration();
|
|
757
1236
|
firstTrackEventStartTime = null;
|
|
758
1237
|
protectedTrackContent = false;
|
|
759
1238
|
const api = requireApi();
|
|
760
1239
|
const handle = requireHandle();
|
|
761
1240
|
api.removeTrack(handle);
|
|
762
1241
|
syncTotalEventsMetric();
|
|
1242
|
+
markNextRenderForced();
|
|
763
1243
|
};
|
|
764
1244
|
self.setTrackByUrl = ({ url }) => {
|
|
765
1245
|
self.setTrack({ content: read_(url) });
|
|
766
1246
|
};
|
|
767
|
-
// =============================================================================
|
|
768
|
-
// Time Management
|
|
769
|
-
// =============================================================================
|
|
770
1247
|
let _isPaused = true;
|
|
1248
|
+
const projectedCurrentTime = () => {
|
|
1249
|
+
if (_isPaused)
|
|
1250
|
+
return lastCurrentTime;
|
|
1251
|
+
return lastCurrentTime + ((nowMs() - lastCurrentTimeReceivedAt) / 1000) * rate;
|
|
1252
|
+
};
|
|
1253
|
+
const freezeClockAtProjectedTime = () => {
|
|
1254
|
+
lastCurrentTime = projectedCurrentTime();
|
|
1255
|
+
lastCurrentTimeReceivedAt = nowMs();
|
|
1256
|
+
};
|
|
1257
|
+
const setPlaybackRate = (newRate) => {
|
|
1258
|
+
if (!Number.isFinite(newRate))
|
|
1259
|
+
return;
|
|
1260
|
+
if (!_isPaused)
|
|
1261
|
+
freezeClockAtProjectedTime();
|
|
1262
|
+
rate = newRate;
|
|
1263
|
+
};
|
|
771
1264
|
const getCurrentTime = () => {
|
|
772
1265
|
const diff = (nowMs() - lastCurrentTimeReceivedAt) / 1000;
|
|
773
1266
|
if (_isPaused) {
|
|
@@ -776,18 +1269,27 @@ const getCurrentTime = () => {
|
|
|
776
1269
|
else {
|
|
777
1270
|
if (diff > 5) {
|
|
778
1271
|
console.error("Didn't receive currentTime > 5 seconds. Assuming video was paused.");
|
|
1272
|
+
freezeClockAtProjectedTime();
|
|
779
1273
|
setIsPaused(true);
|
|
1274
|
+
return lastCurrentTime;
|
|
780
1275
|
}
|
|
781
1276
|
return lastCurrentTime + diff * rate;
|
|
782
1277
|
}
|
|
783
1278
|
};
|
|
1279
|
+
const getRenderClockTime = () => compensatedMediaTime(getCurrentTime(), rate, configuredRenderAheadSeconds, adaptiveTiming ? fallbackTimingCompensationSeconds : 0, _isPaused);
|
|
784
1280
|
const setCurrentTime = (currentTime) => {
|
|
1281
|
+
if (!Number.isFinite(currentTime))
|
|
1282
|
+
return;
|
|
785
1283
|
lastCurrentTime = currentTime;
|
|
786
1284
|
lastCurrentTimeReceivedAt = nowMs();
|
|
787
1285
|
if (onDemandRenderMode) {
|
|
788
1286
|
return;
|
|
789
1287
|
}
|
|
790
1288
|
if (!rafId) {
|
|
1289
|
+
if (_isPaused) {
|
|
1290
|
+
renderLoop();
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
791
1293
|
if (nextIsRaf) {
|
|
792
1294
|
rafId = requestAnimationFrame(renderLoop);
|
|
793
1295
|
}
|
|
@@ -801,27 +1303,32 @@ const setCurrentTime = (currentTime) => {
|
|
|
801
1303
|
}
|
|
802
1304
|
};
|
|
803
1305
|
const setIsPaused = (isPaused) => {
|
|
1306
|
+
if (isPaused === _isPaused)
|
|
1307
|
+
return;
|
|
1308
|
+
if (isPaused) {
|
|
1309
|
+
freezeClockAtProjectedTime();
|
|
1310
|
+
}
|
|
804
1311
|
if (onDemandRenderMode) {
|
|
805
1312
|
_isPaused = isPaused;
|
|
1313
|
+
if (!isPaused)
|
|
1314
|
+
lastCurrentTimeReceivedAt = nowMs();
|
|
806
1315
|
if (rafId) {
|
|
807
1316
|
cancelAnimationFrame(rafId);
|
|
808
1317
|
rafId = null;
|
|
809
1318
|
}
|
|
810
1319
|
return;
|
|
811
1320
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
if (
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
rafId = null;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
else {
|
|
821
|
-
lastCurrentTimeReceivedAt = nowMs();
|
|
822
|
-
rafId = requestAnimationFrame(renderLoop);
|
|
1321
|
+
_isPaused = isPaused;
|
|
1322
|
+
if (isPaused) {
|
|
1323
|
+
if (rafId) {
|
|
1324
|
+
cancelAnimationFrame(rafId);
|
|
1325
|
+
rafId = null;
|
|
823
1326
|
}
|
|
824
1327
|
}
|
|
1328
|
+
else {
|
|
1329
|
+
lastCurrentTimeReceivedAt = nowMs();
|
|
1330
|
+
rafId = requestAnimationFrame(renderLoop);
|
|
1331
|
+
}
|
|
825
1332
|
};
|
|
826
1333
|
const flushQueuedRender = () => {
|
|
827
1334
|
if (renderInFlight || queuedRenders.length === 0)
|
|
@@ -830,41 +1337,72 @@ const flushQueuedRender = () => {
|
|
|
830
1337
|
const dropped = queuedRenders.length - 1;
|
|
831
1338
|
metrics.framesDropped += dropped;
|
|
832
1339
|
const latest = queuedRenders[queuedRenders.length - 1];
|
|
1340
|
+
latest.force = queuedRenders.some((item) => item.force) ? 1 : latest.force;
|
|
833
1341
|
queuedRenders.length = 0;
|
|
834
1342
|
queuedRenders.push(latest);
|
|
835
1343
|
}
|
|
836
1344
|
const next = queuedRenders.shift();
|
|
837
1345
|
if (!next)
|
|
838
1346
|
return;
|
|
839
|
-
render(next.time, next.force);
|
|
1347
|
+
render(next.time, next.force, next.requestId, next.renderEpoch, next.prepareId, next.presentationId);
|
|
840
1348
|
};
|
|
841
|
-
const completeRenderCycle = () => {
|
|
1349
|
+
const completeRenderCycle = (painted = false) => {
|
|
1350
|
+
if (painted && !onDemandRenderMode && !_isPaused && metrics.renderStartTime > 0 && adaptiveTiming) {
|
|
1351
|
+
fallbackTimingCompensationSeconds = updateTimingCompensation(fallbackTimingCompensationSeconds, performance.now(), metrics.renderStartTime);
|
|
1352
|
+
}
|
|
1353
|
+
metrics.renderStartTime = 0;
|
|
842
1354
|
renderInFlight = false;
|
|
1355
|
+
const hadQueuedRender = queuedRenders.length > 0;
|
|
843
1356
|
flushQueuedRender();
|
|
1357
|
+
if (!hadQueuedRender && !renderInFlight)
|
|
1358
|
+
scheduleFullTrackWarmup();
|
|
844
1359
|
};
|
|
845
|
-
const render = (time, force) => {
|
|
1360
|
+
const render = (time, force, requestId, renderEpoch, prepareId, presentationId) => {
|
|
846
1361
|
if (renderInFlight) {
|
|
847
|
-
const queuedItem = { time, force: force ? 1 : 0 };
|
|
1362
|
+
const queuedItem = { time, force: force ? 1 : 0, requestId, renderEpoch, prepareId, presentationId };
|
|
848
1363
|
if (queuedItem.force) {
|
|
849
1364
|
queuedRenders.length = 0;
|
|
850
1365
|
}
|
|
851
1366
|
else {
|
|
852
1367
|
const lastQueued = queuedRenders[queuedRenders.length - 1];
|
|
853
1368
|
if (lastQueued && Math.abs(lastQueued.time - queuedItem.time) > 0.25) {
|
|
1369
|
+
queuedItem.force = queuedRenders.some((item) => item.force) ? 1 : queuedItem.force;
|
|
854
1370
|
queuedRenders.length = 0;
|
|
855
1371
|
}
|
|
856
1372
|
}
|
|
857
1373
|
if (queuedRenders.length >= MAX_QUEUED_RENDERS) {
|
|
858
|
-
queuedRenders.shift();
|
|
1374
|
+
const dropped = queuedRenders.shift();
|
|
1375
|
+
if (dropped?.force)
|
|
1376
|
+
queuedItem.force = 1;
|
|
859
1377
|
metrics.framesDropped++;
|
|
860
1378
|
}
|
|
861
1379
|
queuedRenders.push(queuedItem);
|
|
862
1380
|
return;
|
|
863
1381
|
}
|
|
1382
|
+
// Repeated manual renders at the exact same media time and canvas size are
|
|
1383
|
+
// deterministic. Skip the WASM call and reuse the existing canvas contents;
|
|
1384
|
+
// this matches players/benchmark harnesses that repaint paused frames.
|
|
1385
|
+
if (!force &&
|
|
1386
|
+
time === lastRenderedRequestTime &&
|
|
1387
|
+
self.width === lastRenderedRequestWidth &&
|
|
1388
|
+
self.height === lastRenderedRequestHeight) {
|
|
1389
|
+
metrics.cacheHits++;
|
|
1390
|
+
if (prepareId == null)
|
|
1391
|
+
postMessage({ target: 'unbusy', requestId, renderEpoch });
|
|
1392
|
+
else
|
|
1393
|
+
postPreparedSnapshot(prepareId, renderEpoch, time);
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
lastRenderedRequestTime = time;
|
|
1397
|
+
lastRenderedRequestWidth = self.width;
|
|
1398
|
+
lastRenderedRequestHeight = self.height;
|
|
864
1399
|
// Inside a known-empty window the output cannot change: skip the WASM call
|
|
865
1400
|
if (!force && emptyWindowFrom >= 0 && time >= emptyWindowFrom && time < emptyWindowUntil) {
|
|
866
1401
|
metrics.cacheHits++;
|
|
867
|
-
|
|
1402
|
+
if (prepareId == null)
|
|
1403
|
+
postMessage({ target: 'unbusy', requestId, renderEpoch });
|
|
1404
|
+
else
|
|
1405
|
+
postPreparedSnapshot(prepareId, renderEpoch, time);
|
|
868
1406
|
return;
|
|
869
1407
|
}
|
|
870
1408
|
renderInFlight = true;
|
|
@@ -876,11 +1414,14 @@ const render = (time, force) => {
|
|
|
876
1414
|
const api = requireApi();
|
|
877
1415
|
const handle = requireHandle();
|
|
878
1416
|
const forceInt = force ? 1 : 0;
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1417
|
+
const useRawAssImagePath = prepareId == null && !!rawAssWebGL2Renderer && offscreenRender === true;
|
|
1418
|
+
const imageStride = useRawAssImagePath ? RAW_RRC_IMG_STRIDE : RRC_IMG_STRIDE;
|
|
1419
|
+
ensureRenderCollectBuffer(RENDER_COLLECT_MAX_IMAGES, imageStride);
|
|
1420
|
+
const written = useRawAssImagePath
|
|
1421
|
+
? api.renderRawCollect(handle, time, forceInt, rrcBufPtr, rrcBufCapacity)
|
|
1422
|
+
: blendMode === 'wasm'
|
|
1423
|
+
? api.renderBlendCollect(handle, time, forceInt, rrcBufPtr, rrcBufCapacity)
|
|
1424
|
+
: api.renderImageCollect(handle, time, forceInt, rrcBufPtr, rrcBufCapacity);
|
|
884
1425
|
// Refresh after the WASM call: rendering may have grown the memory
|
|
885
1426
|
refreshRrcViews();
|
|
886
1427
|
const headerView = rrcHeaderView;
|
|
@@ -889,7 +1430,6 @@ const render = (time, force) => {
|
|
|
889
1430
|
if (headerView[1] === 0 && (emptyWindowFrom < 0 || time < emptyWindowFrom || time >= emptyWindowUntil)) {
|
|
890
1431
|
computeEmptyWindow(time);
|
|
891
1432
|
}
|
|
892
|
-
// Update metrics
|
|
893
1433
|
const renderEndTime = performance.now();
|
|
894
1434
|
const renderDuration = renderEndTime - renderStartTime;
|
|
895
1435
|
metrics.lastRenderTime = renderDuration;
|
|
@@ -903,6 +1443,8 @@ const render = (time, force) => {
|
|
|
903
1443
|
metrics.cacheMisses++;
|
|
904
1444
|
}
|
|
905
1445
|
else {
|
|
1446
|
+
metrics.lastImageCount = 0;
|
|
1447
|
+
metrics.lastImagePixels = 0;
|
|
906
1448
|
metrics.cacheHits++;
|
|
907
1449
|
}
|
|
908
1450
|
if (debug) {
|
|
@@ -917,10 +1459,21 @@ const render = (time, force) => {
|
|
|
917
1459
|
const buffers = frameArrayBuffers;
|
|
918
1460
|
images.length = 0;
|
|
919
1461
|
buffers.length = 0;
|
|
920
|
-
if (written === 0)
|
|
921
|
-
return paintImages({ images, buffers, times });
|
|
922
1462
|
const meta = rrcMetaView;
|
|
923
|
-
|
|
1463
|
+
metrics.lastImageCount = written;
|
|
1464
|
+
metrics.lastImagePixels = 0;
|
|
1465
|
+
const stride = useRawAssImagePath ? RAW_RRC_IMG_STRIDE : RRC_IMG_STRIDE;
|
|
1466
|
+
for (let i = 0; i < written; ++i) {
|
|
1467
|
+
const metaOffset = i * stride;
|
|
1468
|
+
metrics.lastImagePixels += Math.max(0, meta[metaOffset + 2]) * Math.max(0, meta[metaOffset + 3]);
|
|
1469
|
+
}
|
|
1470
|
+
if (useRawAssImagePath) {
|
|
1471
|
+
return paintRawAssImages({ meta, count: written, times, requestId, renderEpoch, presentationId });
|
|
1472
|
+
}
|
|
1473
|
+
if (written === 0) {
|
|
1474
|
+
return paintImages({ images, buffers, times, requestId, renderEpoch, prepareId, time, presentationId });
|
|
1475
|
+
}
|
|
1476
|
+
const useAsyncBitmapPath = asyncRender;
|
|
924
1477
|
if (useAsyncBitmapPath) {
|
|
925
1478
|
const promises = frameBitmapPromises;
|
|
926
1479
|
promises.length = written;
|
|
@@ -946,25 +1499,32 @@ const render = (time, force) => {
|
|
|
946
1499
|
: createImageBitmap(imageData);
|
|
947
1500
|
images[i] = item;
|
|
948
1501
|
}
|
|
949
|
-
Promise.all(promises)
|
|
1502
|
+
Promise.all(promises)
|
|
1503
|
+
.then((bitmaps) => {
|
|
950
1504
|
for (let i = 0; i < written; i++) {
|
|
951
1505
|
images[i].image = bitmaps[i];
|
|
952
1506
|
}
|
|
953
1507
|
if (debug)
|
|
954
1508
|
times.JSBitmapGenerationTime = Date.now() - (times.JSRenderTime || 0);
|
|
955
|
-
paintImages({ images, buffers: bitmaps, times });
|
|
956
|
-
})
|
|
1509
|
+
paintImages({ images, buffers: bitmaps, times, requestId, renderEpoch, prepareId, time, presentationId });
|
|
1510
|
+
})
|
|
1511
|
+
.catch(() => {
|
|
957
1512
|
if (asyncRenderOptions) {
|
|
958
1513
|
asyncRenderOptions = false;
|
|
959
1514
|
console.warn('[AkariSub] createImageBitmap options not supported, disabling');
|
|
960
1515
|
metrics.pendingRenders--;
|
|
961
1516
|
completeRenderCycle();
|
|
962
|
-
render(time, force);
|
|
1517
|
+
render(time, force, requestId, renderEpoch, prepareId, presentationId);
|
|
963
1518
|
}
|
|
964
1519
|
else {
|
|
965
1520
|
metrics.pendingRenders--;
|
|
966
|
-
|
|
967
|
-
|
|
1521
|
+
if (prepareId == null) {
|
|
1522
|
+
postMessage({ target: 'unbusy', requestId, renderEpoch });
|
|
1523
|
+
completeRenderCycle();
|
|
1524
|
+
}
|
|
1525
|
+
else {
|
|
1526
|
+
void postPreparedSnapshot(prepareId, renderEpoch, time).finally(() => completeRenderCycle());
|
|
1527
|
+
}
|
|
968
1528
|
}
|
|
969
1529
|
});
|
|
970
1530
|
}
|
|
@@ -1000,31 +1560,156 @@ const render = (time, force) => {
|
|
|
1000
1560
|
}
|
|
1001
1561
|
images[i] = item;
|
|
1002
1562
|
}
|
|
1003
|
-
paintImages({ images, buffers, times });
|
|
1563
|
+
paintImages({ images, buffers, times, requestId, renderEpoch, prepareId, time, presentationId });
|
|
1004
1564
|
}
|
|
1005
1565
|
}
|
|
1006
1566
|
else {
|
|
1007
1567
|
metrics.pendingRenders--;
|
|
1008
|
-
|
|
1009
|
-
|
|
1568
|
+
if (prepareId == null) {
|
|
1569
|
+
postMessage({ target: 'unbusy', requestId, renderEpoch });
|
|
1570
|
+
completeRenderCycle();
|
|
1571
|
+
}
|
|
1572
|
+
else {
|
|
1573
|
+
void postPreparedSnapshot(prepareId, renderEpoch, time).finally(() => completeRenderCycle());
|
|
1574
|
+
}
|
|
1010
1575
|
}
|
|
1011
1576
|
};
|
|
1012
|
-
self.demand = ({ time }) => {
|
|
1577
|
+
self.demand = ({ time, force: demandForce, requestId, renderEpoch, presentationId }) => {
|
|
1013
1578
|
lastCurrentTime = time;
|
|
1014
1579
|
lastCurrentTimeReceivedAt = nowMs();
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1580
|
+
if (presentationId != null)
|
|
1581
|
+
latestPresentationId = Math.max(latestPresentationId, presentationId);
|
|
1582
|
+
const force = demandForce ? 1 : consumeNextRenderForce();
|
|
1583
|
+
render(time, force, requestId, renderEpoch, undefined, presentationId);
|
|
1584
|
+
};
|
|
1585
|
+
self.prepare = ({ time, prepareId, renderEpoch, force }) => {
|
|
1586
|
+
render(time, force ? 1 : 0, undefined, renderEpoch, prepareId);
|
|
1018
1587
|
};
|
|
1019
1588
|
const renderLoop = (force) => {
|
|
1020
1589
|
rafId = null;
|
|
1021
|
-
render(
|
|
1590
|
+
render(getRenderClockTime(), force || consumeNextRenderForce());
|
|
1022
1591
|
if (!_isPaused) {
|
|
1023
1592
|
rafId = requestAnimationFrame(renderLoop);
|
|
1024
1593
|
}
|
|
1025
1594
|
};
|
|
1026
|
-
const
|
|
1595
|
+
const paintRawAssImages = ({ times, meta, count, requestId, renderEpoch, presentationId }) => {
|
|
1027
1596
|
metrics.pendingRenders--;
|
|
1597
|
+
if (isStalePresentation(presentationId, latestPresentationId)) {
|
|
1598
|
+
postMessage({ target: 'unbusy', requestId, renderEpoch, presentationId, painted: false });
|
|
1599
|
+
completeRenderCycle();
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
const width = self.width;
|
|
1603
|
+
const height = self.height;
|
|
1604
|
+
const renderStart = performance.now();
|
|
1605
|
+
let painted = true;
|
|
1606
|
+
try {
|
|
1607
|
+
const pixels = rawAssWebGL2Renderer.renderRawAssMetadata(meta, count, self.HEAPU8, width, height);
|
|
1608
|
+
if (debug) {
|
|
1609
|
+
times.JSRenderTime = performance.now() - renderStart;
|
|
1610
|
+
times.bitmaps = count;
|
|
1611
|
+
times.rawPixels = pixels;
|
|
1612
|
+
let total = 0;
|
|
1613
|
+
for (const key in times) {
|
|
1614
|
+
if (key !== 'bitmaps' && key !== 'rawPixels')
|
|
1615
|
+
total += times[key] || 0;
|
|
1616
|
+
}
|
|
1617
|
+
console.log(`[WEBGL2-RAW-ASS] Bitmaps: ${count} Pixels: ${pixels} Total: ${total | 0}ms`, times);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
catch (error) {
|
|
1621
|
+
painted = false;
|
|
1622
|
+
console.error('[AkariSub] Raw ASS_Image WebGL2 render failed:', error);
|
|
1623
|
+
}
|
|
1624
|
+
postMessage({ target: 'unbusy', requestId, renderEpoch, presentationId, painted });
|
|
1625
|
+
completeRenderCycle(painted);
|
|
1626
|
+
};
|
|
1627
|
+
const ensurePreparedCanvas = () => {
|
|
1628
|
+
const width = self.width;
|
|
1629
|
+
const height = self.height;
|
|
1630
|
+
if (!preparedCanvas)
|
|
1631
|
+
preparedCanvas = new OffscreenCanvas(width, height);
|
|
1632
|
+
if (preparedCanvas.width !== width || preparedCanvas.height !== height) {
|
|
1633
|
+
preparedCanvas.width = width;
|
|
1634
|
+
preparedCanvas.height = height;
|
|
1635
|
+
preparedCtx = null;
|
|
1636
|
+
}
|
|
1637
|
+
preparedCtx ??= preparedCanvas.getContext('2d');
|
|
1638
|
+
if (!preparedCtx)
|
|
1639
|
+
throw new Error('Prepared-frame canvas is unavailable');
|
|
1640
|
+
return preparedCtx;
|
|
1641
|
+
};
|
|
1642
|
+
const postPreparedSnapshot = async (prepareId, renderEpoch, time) => {
|
|
1643
|
+
try {
|
|
1644
|
+
const context = ensurePreparedCanvas();
|
|
1645
|
+
const bitmap = context.canvas.transferToImageBitmap();
|
|
1646
|
+
postMessage({ target: 'preparedFrame', prepareId, renderEpoch, time, width: self.width, height: self.height, bitmap }, [bitmap]);
|
|
1647
|
+
}
|
|
1648
|
+
catch (error) {
|
|
1649
|
+
if (debug)
|
|
1650
|
+
console.warn('[AkariSub] Failed to copy prepared exact frame:', error);
|
|
1651
|
+
postMessage({ target: 'preparedFrame', prepareId, renderEpoch, time });
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1654
|
+
const paintPreparedImages = (images, prepareId, renderEpoch, time) => {
|
|
1655
|
+
const width = self.width;
|
|
1656
|
+
const height = self.height;
|
|
1657
|
+
try {
|
|
1658
|
+
preparedCtx = ensurePreparedCanvas();
|
|
1659
|
+
preparedCtx.clearRect(0, 0, width, height);
|
|
1660
|
+
for (const image of images) {
|
|
1661
|
+
if (!image.image)
|
|
1662
|
+
continue;
|
|
1663
|
+
if (image.image instanceof ImageBitmap) {
|
|
1664
|
+
preparedCtx.drawImage(image.image, image.x, image.y);
|
|
1665
|
+
image.image.close();
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1668
|
+
if (!preparedBufferCanvas)
|
|
1669
|
+
preparedBufferCanvas = new OffscreenCanvas(image.w, image.h);
|
|
1670
|
+
if (preparedBufferCanvas.width !== image.w || preparedBufferCanvas.height !== image.h) {
|
|
1671
|
+
preparedBufferCanvas.width = image.w;
|
|
1672
|
+
preparedBufferCanvas.height = image.h;
|
|
1673
|
+
preparedBufferCtx = null;
|
|
1674
|
+
}
|
|
1675
|
+
preparedBufferCtx ??= preparedBufferCanvas.getContext('2d');
|
|
1676
|
+
if (!preparedBufferCtx)
|
|
1677
|
+
throw new Error('Prepared-frame buffer canvas is unavailable');
|
|
1678
|
+
const byteLength = image.w * image.h * 4;
|
|
1679
|
+
const pixels = image.image instanceof Uint8ClampedArray
|
|
1680
|
+
? image.image
|
|
1681
|
+
: self.HEAPU8C.subarray(image.image, image.image + byteLength);
|
|
1682
|
+
preparedBufferCtx.putImageData(new ImageData(pixels, image.w, image.h), 0, 0);
|
|
1683
|
+
preparedCtx.drawImage(preparedBufferCanvas, image.x, image.y);
|
|
1684
|
+
}
|
|
1685
|
+
void postPreparedSnapshot(prepareId, renderEpoch, time).finally(() => completeRenderCycle());
|
|
1686
|
+
}
|
|
1687
|
+
catch (error) {
|
|
1688
|
+
for (const image of images) {
|
|
1689
|
+
if (image.image instanceof ImageBitmap)
|
|
1690
|
+
image.image.close();
|
|
1691
|
+
}
|
|
1692
|
+
if (debug)
|
|
1693
|
+
console.warn('[AkariSub] Failed to prepare exact frame:', error);
|
|
1694
|
+
postMessage({ target: 'preparedFrame', prepareId, renderEpoch, time, unchanged: true });
|
|
1695
|
+
completeRenderCycle();
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
const paintImages = ({ times, images, buffers, requestId, renderEpoch, prepareId, time, presentationId }) => {
|
|
1699
|
+
metrics.pendingRenders--;
|
|
1700
|
+
if (prepareId != null) {
|
|
1701
|
+
paintPreparedImages(images, prepareId, renderEpoch, time ?? 0);
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
if (isStalePresentation(presentationId, latestPresentationId)) {
|
|
1705
|
+
for (const image of images) {
|
|
1706
|
+
if (image.image instanceof ImageBitmap)
|
|
1707
|
+
image.image.close();
|
|
1708
|
+
}
|
|
1709
|
+
postMessage({ target: 'unbusy', requestId, renderEpoch, presentationId, painted: false });
|
|
1710
|
+
completeRenderCycle();
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1028
1713
|
const width = self.width;
|
|
1029
1714
|
const height = self.height;
|
|
1030
1715
|
const imageCount = images.length;
|
|
@@ -1035,17 +1720,23 @@ const paintImages = ({ times, images, buffers }) => {
|
|
|
1035
1720
|
times,
|
|
1036
1721
|
width,
|
|
1037
1722
|
height,
|
|
1038
|
-
colorSpace: subtitleColorSpace
|
|
1723
|
+
colorSpace: subtitleColorSpace,
|
|
1724
|
+
requestId,
|
|
1725
|
+
renderEpoch,
|
|
1726
|
+
presentationId
|
|
1039
1727
|
};
|
|
1040
1728
|
if (offscreenRender) {
|
|
1041
|
-
// Only resize canvas when dimensions actually change
|
|
1042
1729
|
if (offCanvas.height !== height || offCanvas.width !== width) {
|
|
1043
1730
|
offCanvas.width = width;
|
|
1044
1731
|
offCanvas.height = height;
|
|
1045
1732
|
}
|
|
1046
1733
|
offCanvasCtx.clearRect(0, 0, width, height);
|
|
1047
|
-
|
|
1048
|
-
|
|
1734
|
+
const firstImage = imageCount > 0 ? images[0].image : null;
|
|
1735
|
+
const hasImageBitmapInputs = typeof ImageBitmap !== 'undefined' && firstImage instanceof ImageBitmap;
|
|
1736
|
+
if (hasImageBitmapInputs) {
|
|
1737
|
+
// Batch draw all ImageBitmaps. This is much faster than rebuilding ImageData
|
|
1738
|
+
// from WASM-backed RGBA pointers and works for both plain offscreenRender=true
|
|
1739
|
+
// and hybrid offscreen rendering.
|
|
1049
1740
|
for (let i = 0; i < imageCount; i++) {
|
|
1050
1741
|
const img = images[i];
|
|
1051
1742
|
if (img.image) {
|
|
@@ -1055,13 +1746,11 @@ const paintImages = ({ times, images, buffers }) => {
|
|
|
1055
1746
|
}
|
|
1056
1747
|
}
|
|
1057
1748
|
else {
|
|
1058
|
-
// Non-async path with buffer canvas
|
|
1059
1749
|
for (let i = 0; i < imageCount; i++) {
|
|
1060
1750
|
const img = images[i];
|
|
1061
1751
|
if (img.image) {
|
|
1062
1752
|
const imgW = img.w;
|
|
1063
1753
|
const imgH = img.h;
|
|
1064
|
-
// Only resize buffer canvas when needed
|
|
1065
1754
|
if (bufferCanvas.width !== imgW || bufferCanvas.height !== imgH) {
|
|
1066
1755
|
bufferCanvas.width = imgW;
|
|
1067
1756
|
bufferCanvas.height = imgH;
|
|
@@ -1077,7 +1766,7 @@ const paintImages = ({ times, images, buffers }) => {
|
|
|
1077
1766
|
if (offscreenRender === 'hybrid') {
|
|
1078
1767
|
if (!imageCount) {
|
|
1079
1768
|
postMessage(resultObject);
|
|
1080
|
-
completeRenderCycle();
|
|
1769
|
+
completeRenderCycle(true);
|
|
1081
1770
|
return;
|
|
1082
1771
|
}
|
|
1083
1772
|
if (debug)
|
|
@@ -1090,10 +1779,10 @@ const paintImages = ({ times, images, buffers }) => {
|
|
|
1090
1779
|
asyncRender: true
|
|
1091
1780
|
};
|
|
1092
1781
|
postMessage(result, [bitmap]);
|
|
1093
|
-
completeRenderCycle();
|
|
1782
|
+
completeRenderCycle(true);
|
|
1094
1783
|
}
|
|
1095
1784
|
catch {
|
|
1096
|
-
postMessage({ target: 'unbusy' });
|
|
1785
|
+
postMessage({ target: 'unbusy', requestId, renderEpoch });
|
|
1097
1786
|
completeRenderCycle();
|
|
1098
1787
|
}
|
|
1099
1788
|
}
|
|
@@ -1105,41 +1794,54 @@ const paintImages = ({ times, images, buffers }) => {
|
|
|
1105
1794
|
total += times[key] || 0;
|
|
1106
1795
|
console.log('Bitmaps: ' + imageCount + ' Total: ' + (total | 0) + 'ms', times);
|
|
1107
1796
|
}
|
|
1108
|
-
postMessage({ target: 'unbusy' });
|
|
1109
|
-
completeRenderCycle();
|
|
1797
|
+
postMessage({ target: 'unbusy', requestId, renderEpoch, painted: true });
|
|
1798
|
+
completeRenderCycle(true);
|
|
1110
1799
|
}
|
|
1111
1800
|
}
|
|
1112
1801
|
else {
|
|
1113
1802
|
postMessage(resultObject, buffers);
|
|
1114
|
-
completeRenderCycle();
|
|
1803
|
+
completeRenderCycle(true);
|
|
1115
1804
|
}
|
|
1116
1805
|
};
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
nextRAF
|
|
1124
|
-
|
|
1125
|
-
else {
|
|
1126
|
-
while (now + 2 >= nextRAF) {
|
|
1127
|
-
nextRAF += 1000 / targetFps;
|
|
1806
|
+
const requestAnimationFrame = self.requestAnimationFrame
|
|
1807
|
+
? self.requestAnimationFrame.bind(self)
|
|
1808
|
+
: (() => {
|
|
1809
|
+
let nextRAF = 0;
|
|
1810
|
+
return (func) => {
|
|
1811
|
+
const now = nowMs();
|
|
1812
|
+
if (nextRAF === 0) {
|
|
1813
|
+
nextRAF = now + 1000 / targetFps;
|
|
1128
1814
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
}
|
|
1815
|
+
else {
|
|
1816
|
+
while (now + 2 >= nextRAF) {
|
|
1817
|
+
nextRAF += 1000 / targetFps;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
const delay = Math.max(nextRAF - now, 0);
|
|
1821
|
+
return setTimeout(func, delay);
|
|
1822
|
+
};
|
|
1823
|
+
})();
|
|
1134
1824
|
const cancelAnimationFrame = self.cancelAnimationFrame ? self.cancelAnimationFrame.bind(self) : clearTimeout;
|
|
1135
|
-
// =============================================================================
|
|
1136
|
-
// WASM Initialization
|
|
1137
|
-
// =============================================================================
|
|
1138
1825
|
self.init = async (data) => {
|
|
1139
1826
|
hasBitmapBug = data.hasBitmapBug;
|
|
1140
1827
|
fullTrackWarmupEnabled = !!data.fullTrackWarmup;
|
|
1828
|
+
frameTimelineMode = !!data.frameTimelineMode;
|
|
1829
|
+
adaptiveTiming = data.adaptiveTiming ?? true;
|
|
1830
|
+
configuredRenderAheadSeconds = Number.isFinite(data.renderAhead) ? data.renderAhead : 0;
|
|
1831
|
+
_isPaused = data.initialIsPaused ?? true;
|
|
1832
|
+
if (typeof data.initialPlaybackRate === 'number' && Number.isFinite(data.initialPlaybackRate)) {
|
|
1833
|
+
rate = data.initialPlaybackRate;
|
|
1834
|
+
}
|
|
1141
1835
|
if (typeof data.initialTime === 'number' && Number.isFinite(data.initialTime)) {
|
|
1142
1836
|
lastCurrentTime = data.initialTime;
|
|
1837
|
+
if (!_isPaused &&
|
|
1838
|
+
typeof data.initialTimeSnapshotAtMs === 'number' &&
|
|
1839
|
+
Number.isFinite(data.initialTimeSnapshotAtMs)) {
|
|
1840
|
+
lastCurrentTimeReceivedAt = nowMs() - Math.max(0, Date.now() - data.initialTimeSnapshotAtMs);
|
|
1841
|
+
}
|
|
1842
|
+
else {
|
|
1843
|
+
lastCurrentTimeReceivedAt = nowMs();
|
|
1844
|
+
}
|
|
1143
1845
|
}
|
|
1144
1846
|
const _fetch = self.fetch;
|
|
1145
1847
|
const setWasmUrl = (wasmUrl) => {
|
|
@@ -1150,11 +1852,20 @@ self.init = async (data) => {
|
|
|
1150
1852
|
const restoreFetch = () => {
|
|
1151
1853
|
self.fetch = _fetch;
|
|
1152
1854
|
};
|
|
1153
|
-
const loadWasm = (wasmUrl) => {
|
|
1855
|
+
const loadWasm = async (wasmUrl, glueUrl) => {
|
|
1856
|
+
const jsGlueUrl = glueUrl || glueUrlFromWasmUrl(wasmUrl);
|
|
1857
|
+
const glue = (await import(/* @vite-ignore */ jsGlueUrl));
|
|
1858
|
+
const WASM = glue.default;
|
|
1154
1859
|
setWasmUrl(wasmUrl);
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1860
|
+
try {
|
|
1861
|
+
return await WASM({
|
|
1862
|
+
locateFile: (path) => (path.endsWith('.wasm') ? wasmUrl : path),
|
|
1863
|
+
wasm: !WebAssembly.instantiateStreaming ? read_(wasmUrl, true) : undefined
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
finally {
|
|
1867
|
+
restoreFetch();
|
|
1868
|
+
}
|
|
1158
1869
|
};
|
|
1159
1870
|
const onWasmLoaded = async (Module) => {
|
|
1160
1871
|
_Module = Module; // Store module reference for FS access
|
|
@@ -1162,6 +1873,7 @@ self.init = async (data) => {
|
|
|
1162
1873
|
create: Module._akarisub_create,
|
|
1163
1874
|
destroy: Module._akarisub_destroy,
|
|
1164
1875
|
setDropAnimations: Module._akarisub_set_drop_animations,
|
|
1876
|
+
setAdaptiveBlendLayouts: Module._akarisub_set_adaptive_blend_layouts,
|
|
1165
1877
|
createTrackMem: Module._akarisub_create_track_mem,
|
|
1166
1878
|
removeTrack: Module._akarisub_remove_track,
|
|
1167
1879
|
resizeCanvas: Module._akarisub_resize_canvas,
|
|
@@ -1169,6 +1881,7 @@ self.init = async (data) => {
|
|
|
1169
1881
|
reloadFonts: Module._akarisub_reload_fonts,
|
|
1170
1882
|
setDefaultFont: Module._akarisub_set_default_font,
|
|
1171
1883
|
setFallbackFonts: Module._akarisub_set_fallback_fonts,
|
|
1884
|
+
setUseFontconfigProvider: Module._akarisub_set_use_fontconfig_provider,
|
|
1172
1885
|
setMemoryLimits: Module._akarisub_set_memory_limits,
|
|
1173
1886
|
getEventCount: Module._akarisub_get_event_count,
|
|
1174
1887
|
allocEvent: Module._akarisub_alloc_event,
|
|
@@ -1190,9 +1903,9 @@ self.init = async (data) => {
|
|
|
1190
1903
|
getEventTimeRange: Module._akarisub_get_event_time_range,
|
|
1191
1904
|
getEmptyWindow: Module._akarisub_get_empty_window,
|
|
1192
1905
|
renderBlendCollect: Module._akarisub_render_blend_collect,
|
|
1193
|
-
renderImageCollect: Module._akarisub_render_image_collect
|
|
1906
|
+
renderImageCollect: Module._akarisub_render_image_collect,
|
|
1907
|
+
renderRawCollect: Module._akarisub_render_raw_collect
|
|
1194
1908
|
};
|
|
1195
|
-
// Normalize fallback fonts and deduplicate
|
|
1196
1909
|
const fallbackFonts = [];
|
|
1197
1910
|
const fallbackFontKeys = new Set();
|
|
1198
1911
|
if (data.fallbackFonts && data.fallbackFonts.length > 0) {
|
|
@@ -1262,6 +1975,7 @@ self.init = async (data) => {
|
|
|
1262
1975
|
self.width = data.width;
|
|
1263
1976
|
self.height = data.height;
|
|
1264
1977
|
onDemandRenderMode = !!data.onDemandRender;
|
|
1978
|
+
rawAssImageGpuEnabled = !!data.rawAssImageGpu;
|
|
1265
1979
|
blendMode = data.blendMode;
|
|
1266
1980
|
asyncRender = data.asyncRender;
|
|
1267
1981
|
if (asyncRender && typeof createImageBitmap === 'undefined') {
|
|
@@ -1274,8 +1988,7 @@ self.init = async (data) => {
|
|
|
1274
1988
|
const testCtx = testCanvas.getContext('2d');
|
|
1275
1989
|
if (testCtx) {
|
|
1276
1990
|
const testData = testCtx.getImageData(0, 0, 1, 1);
|
|
1277
|
-
await createImageBitmap(testData, { premultiplyAlpha: 'none', colorSpaceConversion: 'none' })
|
|
1278
|
-
.catch(() => {
|
|
1991
|
+
await createImageBitmap(testData, { premultiplyAlpha: 'none', colorSpaceConversion: 'none' }).catch(() => {
|
|
1279
1992
|
asyncRenderOptions = false;
|
|
1280
1993
|
console.warn('[AkariSub] createImageBitmap options not supported (Safari?), rendering without options');
|
|
1281
1994
|
});
|
|
@@ -1289,6 +2002,7 @@ self.init = async (data) => {
|
|
|
1289
2002
|
debug = data.debug;
|
|
1290
2003
|
targetFps = data.targetFps || targetFps;
|
|
1291
2004
|
useLocalFonts = data.useLocalFonts;
|
|
2005
|
+
useFontconfigProvider = data.useFontconfigProvider;
|
|
1292
2006
|
dropAllBlur = data.dropAllBlur;
|
|
1293
2007
|
clampPos = data.clampPos;
|
|
1294
2008
|
// Load fallback fonts asynchronously to avoid blocking worker thread
|
|
@@ -1301,29 +2015,26 @@ self.init = async (data) => {
|
|
|
1301
2015
|
if (availableFonts && availableFonts[fontKey]) {
|
|
1302
2016
|
const fontUrl = availableFonts[fontKey];
|
|
1303
2017
|
if (typeof fontUrl === 'string') {
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
2018
|
+
const promise = readBoundedFontUrl(fontUrl)
|
|
2019
|
+
.then((fontData) => {
|
|
2020
|
+
const accepted = writeFontToFSImmediate(fontData, true);
|
|
2021
|
+
if (accepted) {
|
|
1308
2022
|
fontMap_[fontKey] = true;
|
|
1309
2023
|
if (debug)
|
|
1310
2024
|
console.log('[AkariSub] Loaded fallback font async:', fontKey);
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
});
|
|
2025
|
+
}
|
|
2026
|
+
})
|
|
2027
|
+
.catch((error) => {
|
|
2028
|
+
console.error('Failed to load fallback font:', fontKey, error);
|
|
1316
2029
|
});
|
|
1317
2030
|
fontPromises.push(promise);
|
|
1318
2031
|
}
|
|
1319
2032
|
else {
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
fontMap_[fontKey] = true;
|
|
2033
|
+
if (writeFontToFSImmediate(fontUrl, true))
|
|
2034
|
+
fontMap_[fontKey] = true;
|
|
1323
2035
|
}
|
|
1324
2036
|
}
|
|
1325
2037
|
}
|
|
1326
|
-
// Wait for all fonts to load (with 30s timeout to prevent blocking forever)
|
|
1327
2038
|
if (fontPromises.length > 0) {
|
|
1328
2039
|
let timeoutId = null;
|
|
1329
2040
|
let timedOut = false;
|
|
@@ -1346,18 +2057,13 @@ self.init = async (data) => {
|
|
|
1346
2057
|
}
|
|
1347
2058
|
}
|
|
1348
2059
|
};
|
|
1349
|
-
await loadFallbackFontsAsync();
|
|
1350
2060
|
const primaryFallback = fallbackFonts.length > 0 ? fallbackFonts[0] : null;
|
|
1351
2061
|
akariSubHandle = withCString(primaryFallback || '', (fontPtr) => {
|
|
1352
2062
|
return requireApi().create(self.width, self.height, fontPtr, debug ? 1 : 0);
|
|
1353
2063
|
});
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
}
|
|
1358
|
-
pendingFallbackFonts.length = 0;
|
|
1359
|
-
requireApi().reloadFonts(akariSubHandle);
|
|
1360
|
-
}
|
|
2064
|
+
// Admission requires a live libass handle. Load fallback bytes only after
|
|
2065
|
+
// creation so rejected files are never retained in MEMFS or fontMap_.
|
|
2066
|
+
await loadFallbackFontsAsync();
|
|
1361
2067
|
if (fallbackFonts.length > 0) {
|
|
1362
2068
|
withCString(fallbackFonts.join(','), (fontsPtr) => {
|
|
1363
2069
|
requireApi().setFallbackFonts(requireHandle(), fontsPtr);
|
|
@@ -1375,11 +2081,7 @@ self.init = async (data) => {
|
|
|
1375
2081
|
if (!subContent)
|
|
1376
2082
|
subContent = read_(data.subUrl);
|
|
1377
2083
|
}
|
|
1378
|
-
|
|
1379
|
-
// while font loading and track parsing continues in the background
|
|
1380
|
-
const isLargeSubtitle = typeof subContent === 'string'
|
|
1381
|
-
? subContent.length > 500000
|
|
1382
|
-
: toUint8Array(subContent).byteLength > 500000;
|
|
2084
|
+
const isLargeSubtitle = typeof subContent === 'string' ? subContent.length > 500000 : toUint8Array(subContent).byteLength > 500000;
|
|
1383
2085
|
if (isLargeSubtitle) {
|
|
1384
2086
|
postMessage({ target: 'partial_ready' });
|
|
1385
2087
|
if (debug)
|
|
@@ -1400,23 +2102,23 @@ self.init = async (data) => {
|
|
|
1400
2102
|
const attachedFontPromises = [];
|
|
1401
2103
|
for (const font of data.fonts || []) {
|
|
1402
2104
|
if (typeof font === 'string') {
|
|
1403
|
-
const promise =
|
|
1404
|
-
|
|
1405
|
-
|
|
2105
|
+
const promise = readBoundedFontUrl(font)
|
|
2106
|
+
.then((fontData) => {
|
|
2107
|
+
const accepted = writeFontToFSImmediate(fontData, false);
|
|
2108
|
+
if (accepted) {
|
|
1406
2109
|
hasAttachedFonts = true;
|
|
1407
2110
|
if (debug)
|
|
1408
2111
|
console.log('[AkariSub] Loaded attached font async:', font);
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
});
|
|
2112
|
+
}
|
|
2113
|
+
})
|
|
2114
|
+
.catch((error) => {
|
|
2115
|
+
console.error('Failed to load attached font:', font, error);
|
|
1414
2116
|
});
|
|
1415
2117
|
attachedFontPromises.push(promise);
|
|
1416
2118
|
}
|
|
1417
2119
|
else {
|
|
1418
|
-
writeFontToFSImmediate(font, false)
|
|
1419
|
-
|
|
2120
|
+
if (writeFontToFSImmediate(font, false))
|
|
2121
|
+
hasAttachedFonts = true;
|
|
1420
2122
|
}
|
|
1421
2123
|
}
|
|
1422
2124
|
if (attachedFontPromises.length > 0) {
|
|
@@ -1447,6 +2149,8 @@ self.init = async (data) => {
|
|
|
1447
2149
|
if (debug)
|
|
1448
2150
|
console.log('[AkariSub] Font reload complete');
|
|
1449
2151
|
}
|
|
2152
|
+
// Enable fontconfig only after startup fonts are in MEMFS (see AkariSub ctor).
|
|
2153
|
+
requireApi().setUseFontconfigProvider(requireHandle(), useFontconfigProvider ? 1 : 0);
|
|
1450
2154
|
if (typeof subContent === 'string') {
|
|
1451
2155
|
createTrackFromString(subContent);
|
|
1452
2156
|
}
|
|
@@ -1462,45 +2166,118 @@ self.init = async (data) => {
|
|
|
1462
2166
|
firstTrackEventStartTime = getFirstEventStartTime();
|
|
1463
2167
|
subtitleColorSpace = libassYCbCrMap[requireApi().getTrackColorSpace(requireHandle())];
|
|
1464
2168
|
requireApi().setDropAnimations(requireHandle(), data.dropAllAnimations || 0);
|
|
2169
|
+
requireApi().setAdaptiveBlendLayouts(requireHandle(), data.adaptiveBlendLayouts ? 1 : 0);
|
|
2170
|
+
blockingFullTrackWarmup = data.blockingFullTrackWarmup;
|
|
2171
|
+
fullTrackWarmupStepSeconds = data.fullTrackWarmupStep > 0 ? data.fullTrackWarmupStep : FULL_WARMUP_STEP_SECONDS;
|
|
1465
2172
|
if (data.libassMemoryLimit > 0 || data.libassGlyphLimit > 0) {
|
|
1466
2173
|
requireApi().setMemoryLimits(requireHandle(), data.libassGlyphLimit || 0, data.libassMemoryLimit || 0);
|
|
1467
2174
|
}
|
|
1468
2175
|
initPool();
|
|
1469
2176
|
ensureRenderCollectBuffer(PREWARM_MAX_IMAGES);
|
|
1470
2177
|
try {
|
|
1471
|
-
prewarmRenderer(
|
|
2178
|
+
prewarmRenderer(getCurrentTime());
|
|
1472
2179
|
}
|
|
1473
2180
|
catch (e) {
|
|
1474
2181
|
if (debug)
|
|
1475
2182
|
console.warn('[AkariSub] Prewarm render failed, continuing:', e);
|
|
1476
2183
|
}
|
|
1477
|
-
|
|
2184
|
+
if (blockingFullTrackWarmup && fullTrackWarmupEnabled && !fullTrackWarmupStarted) {
|
|
2185
|
+
fullTrackWarmupStarted = true;
|
|
2186
|
+
const generation = trackGeneration;
|
|
2187
|
+
fullTrackWarmupGeneration = generation;
|
|
2188
|
+
try {
|
|
2189
|
+
await prewarmEntireTrack(generation);
|
|
2190
|
+
}
|
|
2191
|
+
catch (e) {
|
|
2192
|
+
if (debug)
|
|
2193
|
+
console.warn('[AkariSub] Full track warmup failed, continuing:', e);
|
|
2194
|
+
}
|
|
2195
|
+
try {
|
|
2196
|
+
prewarmRenderer(getCurrentTime(), generation);
|
|
2197
|
+
}
|
|
2198
|
+
catch (e) {
|
|
2199
|
+
if (debug)
|
|
2200
|
+
console.warn('[AkariSub] Post-warmup re-prime failed, continuing:', e);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
markNextRenderForced();
|
|
1478
2204
|
postMessage({ target: 'ready' });
|
|
1479
2205
|
postMessage({ target: 'verifyColorSpace', subtitleColorSpace });
|
|
1480
|
-
scheduleFullTrackWarmup();
|
|
1481
2206
|
};
|
|
1482
|
-
loadWasm(data.wasmUrl
|
|
2207
|
+
loadWasm(data.wasmUrl, data.glueUrl)
|
|
2208
|
+
.then(onWasmLoaded)
|
|
2209
|
+
.catch((e) => {
|
|
1483
2210
|
console.error('[AkariSub] WASM loading failed:', e);
|
|
1484
2211
|
postMessage({ target: 'error', error: 'WASM loading failed: ' + (e && e.message ? e.message : String(e)) });
|
|
1485
2212
|
});
|
|
1486
2213
|
};
|
|
1487
|
-
|
|
1488
|
-
// Canvas Management
|
|
1489
|
-
// =============================================================================
|
|
1490
|
-
self.offscreenCanvas = ({ transferable }) => {
|
|
2214
|
+
self.offscreenCanvas = ({ transferable, rawAssImageGpu }) => {
|
|
1491
2215
|
offCanvas = transferable[0];
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
2216
|
+
rawAssWebGL2Renderer = null;
|
|
2217
|
+
const useRawAssImageGpu = rawAssImageGpu ?? rawAssImageGpuEnabled;
|
|
2218
|
+
if (useRawAssImageGpu) {
|
|
2219
|
+
try {
|
|
2220
|
+
const renderer = new RawASSImageWebGL2Renderer();
|
|
2221
|
+
if (renderer.init(offCanvas, self.width, self.height)) {
|
|
2222
|
+
rawAssWebGL2Renderer = renderer;
|
|
2223
|
+
offCanvasCtx = null;
|
|
2224
|
+
bufferCanvas = null;
|
|
2225
|
+
bufferCtx = null;
|
|
2226
|
+
offscreenRender = true;
|
|
2227
|
+
if (debug)
|
|
2228
|
+
console.log('[AkariSub] Using worker WebGL2 raw ASS_Image renderer');
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
catch (error) {
|
|
2233
|
+
rawAssWebGL2Renderer = null;
|
|
2234
|
+
if (debug)
|
|
2235
|
+
console.warn('[AkariSub] Worker WebGL2 raw ASS_Image renderer unavailable:', error);
|
|
2236
|
+
}
|
|
1496
2237
|
}
|
|
2238
|
+
offCanvasCtx = offCanvas.getContext('2d');
|
|
2239
|
+
// Plain offscreen rendering draws raw WASM image pointers through a reusable
|
|
2240
|
+
// buffer canvas. Create it regardless of asyncRender because the render path
|
|
2241
|
+
// deliberately disables per-image ImageBitmap creation for offscreenRender=true.
|
|
2242
|
+
bufferCanvas = new OffscreenCanvas(self.width, self.height);
|
|
2243
|
+
bufferCtx = bufferCanvas.getContext('2d');
|
|
1497
2244
|
offscreenRender = true;
|
|
1498
2245
|
};
|
|
1499
2246
|
self.detachOffscreen = () => {
|
|
2247
|
+
rawAssWebGL2Renderer?.destroy();
|
|
2248
|
+
rawAssWebGL2Renderer = null;
|
|
1500
2249
|
offCanvas = new OffscreenCanvas(self.width, self.height);
|
|
1501
|
-
offCanvasCtx = offCanvas.getContext('2d'
|
|
2250
|
+
offCanvasCtx = offCanvas.getContext('2d');
|
|
1502
2251
|
offscreenRender = 'hybrid';
|
|
1503
2252
|
};
|
|
2253
|
+
self.presentFrame = ({ bitmap, presentationId, renderEpoch, frameIndex }) => {
|
|
2254
|
+
let painted = false;
|
|
2255
|
+
try {
|
|
2256
|
+
if (isStalePresentation(presentationId, latestPresentationId))
|
|
2257
|
+
return;
|
|
2258
|
+
latestPresentationId = Math.max(latestPresentationId, presentationId);
|
|
2259
|
+
if (!offCanvasCtx || !offCanvas)
|
|
2260
|
+
return;
|
|
2261
|
+
offCanvasCtx.clearRect(0, 0, offCanvas.width, offCanvas.height);
|
|
2262
|
+
offCanvasCtx.drawImage(bitmap, 0, 0);
|
|
2263
|
+
painted = true;
|
|
2264
|
+
}
|
|
2265
|
+
finally {
|
|
2266
|
+
bitmap.close();
|
|
2267
|
+
if (painted)
|
|
2268
|
+
postMessage({ target: 'presentedFrame', presentationId, renderEpoch, frameIndex });
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
self.presentation = ({ presentationId }) => {
|
|
2272
|
+
if (Number.isSafeInteger(presentationId)) {
|
|
2273
|
+
latestPresentationId = Math.max(latestPresentationId, presentationId);
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
2276
|
+
self.frameTimelineMode = ({ enabled }) => {
|
|
2277
|
+
frameTimelineMode = enabled;
|
|
2278
|
+
if (enabled)
|
|
2279
|
+
stopWarmup();
|
|
2280
|
+
};
|
|
1504
2281
|
self.canvas = ({ width, height, videoWidth, videoHeight, force }) => {
|
|
1505
2282
|
if (width == null)
|
|
1506
2283
|
throw new Error('Invalid canvas size specified');
|
|
@@ -1509,20 +2286,28 @@ self.canvas = ({ width, height, videoWidth, videoHeight, force }) => {
|
|
|
1509
2286
|
if (akariSubHandle)
|
|
1510
2287
|
requireApi().resizeCanvas(akariSubHandle, width, height, videoWidth, videoHeight);
|
|
1511
2288
|
if (force)
|
|
1512
|
-
render(
|
|
2289
|
+
render(getRenderClockTime(), true);
|
|
1513
2290
|
};
|
|
1514
|
-
self.video = ({ currentTime, isPaused, rate: newRate }) => {
|
|
2291
|
+
self.video = ({ currentTime, isPaused, rate: newRate, renderAhead }) => {
|
|
2292
|
+
if (renderAhead != null && Number.isFinite(renderAhead))
|
|
2293
|
+
configuredRenderAheadSeconds = renderAhead;
|
|
1515
2294
|
if (currentTime != null)
|
|
1516
2295
|
setCurrentTime(currentTime);
|
|
1517
2296
|
if (isPaused != null)
|
|
1518
2297
|
setIsPaused(isPaused);
|
|
1519
2298
|
if (newRate != null)
|
|
1520
|
-
|
|
2299
|
+
setPlaybackRate(newRate);
|
|
1521
2300
|
};
|
|
1522
2301
|
self.destroy = () => {
|
|
1523
2302
|
stopWarmup();
|
|
1524
|
-
|
|
2303
|
+
advanceTrackGeneration();
|
|
1525
2304
|
firstTrackEventStartTime = null;
|
|
2305
|
+
rawAssWebGL2Renderer?.destroy();
|
|
2306
|
+
rawAssWebGL2Renderer = null;
|
|
2307
|
+
preparedCanvas = null;
|
|
2308
|
+
preparedCtx = null;
|
|
2309
|
+
preparedBufferCanvas = null;
|
|
2310
|
+
preparedBufferCtx = null;
|
|
1526
2311
|
if (_Module) {
|
|
1527
2312
|
if (rrcBufPtr) {
|
|
1528
2313
|
_Module._free(rrcBufPtr);
|
|
@@ -1544,9 +2329,6 @@ self.destroy = () => {
|
|
|
1544
2329
|
self.setAsyncRender = ({ value }) => {
|
|
1545
2330
|
asyncRender = value && typeof createImageBitmap !== 'undefined';
|
|
1546
2331
|
};
|
|
1547
|
-
// =============================================================================
|
|
1548
|
-
// Event Management
|
|
1549
|
-
// =============================================================================
|
|
1550
2332
|
const applyEventFields = (index, event) => {
|
|
1551
2333
|
const api = requireApi();
|
|
1552
2334
|
const handle = requireHandle();
|
|
@@ -1637,6 +2419,7 @@ self.createEvent = ({ event }) => {
|
|
|
1637
2419
|
if (index >= 0)
|
|
1638
2420
|
applyEventFields(index, event);
|
|
1639
2421
|
syncTotalEventsMetric();
|
|
2422
|
+
markNextRenderForced();
|
|
1640
2423
|
};
|
|
1641
2424
|
self.getEvents = () => {
|
|
1642
2425
|
const events = [];
|
|
@@ -1655,18 +2438,18 @@ self.getEvents = () => {
|
|
|
1655
2438
|
};
|
|
1656
2439
|
self.setEvent = ({ event, index }) => {
|
|
1657
2440
|
applyEventFields(index, event);
|
|
2441
|
+
markNextRenderForced();
|
|
1658
2442
|
};
|
|
1659
2443
|
self.removeEvent = ({ index }) => {
|
|
1660
2444
|
requireApi().removeEvent(requireHandle(), index);
|
|
1661
2445
|
syncTotalEventsMetric();
|
|
2446
|
+
markNextRenderForced();
|
|
1662
2447
|
};
|
|
1663
|
-
// =============================================================================
|
|
1664
|
-
// Style Management
|
|
1665
|
-
// =============================================================================
|
|
1666
2448
|
self.createStyle = ({ style }) => {
|
|
1667
2449
|
const index = requireApi().allocStyle(requireHandle());
|
|
1668
2450
|
if (index >= 0)
|
|
1669
2451
|
applyStyleFields(index, style);
|
|
2452
|
+
markNextRenderForced();
|
|
1670
2453
|
return index;
|
|
1671
2454
|
};
|
|
1672
2455
|
self.getStyles = () => {
|
|
@@ -1680,27 +2463,29 @@ self.getStyles = () => {
|
|
|
1680
2463
|
};
|
|
1681
2464
|
self.setStyle = ({ style, index }) => {
|
|
1682
2465
|
applyStyleFields(index, style);
|
|
2466
|
+
markNextRenderForced();
|
|
1683
2467
|
};
|
|
1684
2468
|
self.removeStyle = ({ index }) => {
|
|
1685
2469
|
requireApi().removeStyle(requireHandle(), index);
|
|
2470
|
+
markNextRenderForced();
|
|
1686
2471
|
};
|
|
1687
2472
|
self.styleOverride = (data) => {
|
|
1688
2473
|
const index = self.createStyle(data);
|
|
1689
2474
|
if (typeof index === 'number' && index >= 0) {
|
|
1690
2475
|
requireApi().styleOverrideIndex(requireHandle(), index);
|
|
1691
2476
|
}
|
|
2477
|
+
markNextRenderForced();
|
|
1692
2478
|
};
|
|
1693
2479
|
self.disableStyleOverride = () => {
|
|
1694
2480
|
requireApi().disableStyleOverride(requireHandle());
|
|
2481
|
+
markNextRenderForced();
|
|
1695
2482
|
};
|
|
1696
2483
|
self.defaultFont = ({ font }) => {
|
|
1697
2484
|
withCString(font, (fontPtr) => {
|
|
1698
2485
|
requireApi().setDefaultFont(requireHandle(), fontPtr);
|
|
1699
2486
|
});
|
|
2487
|
+
markNextRenderForced();
|
|
1700
2488
|
};
|
|
1701
|
-
// =============================================================================
|
|
1702
|
-
// Performance Metrics
|
|
1703
|
-
// =============================================================================
|
|
1704
2489
|
self.getStats = () => {
|
|
1705
2490
|
const avgRenderTime = metrics.framesRendered > 0 ? metrics.totalRenderTime / metrics.framesRendered : 0;
|
|
1706
2491
|
postMessage({
|
|
@@ -1712,8 +2497,22 @@ self.getStats = () => {
|
|
|
1712
2497
|
maxRenderTime: Math.round(metrics.maxRenderTime * 100) / 100,
|
|
1713
2498
|
minRenderTime: metrics.minRenderTime === Infinity ? 0 : Math.round(metrics.minRenderTime * 100) / 100,
|
|
1714
2499
|
lastRenderTime: Math.round(metrics.lastRenderTime * 100) / 100,
|
|
2500
|
+
timingCompensationMs: Math.round(fallbackTimingCompensationSeconds * 100_000) / 100,
|
|
2501
|
+
lastImageCount: metrics.lastImageCount,
|
|
2502
|
+
lastImagePixels: metrics.lastImagePixels,
|
|
1715
2503
|
pendingRenders: Math.max(0, metrics.pendingRenders),
|
|
1716
2504
|
totalEvents: metrics.totalEvents,
|
|
2505
|
+
usingWorker: true,
|
|
2506
|
+
offscreenRender: !!offscreenRender,
|
|
2507
|
+
rawAssImageGpu: !!rawAssWebGL2Renderer,
|
|
2508
|
+
workerRenderer: rawAssWebGL2Renderer
|
|
2509
|
+
? 'webgl2-raw-ass'
|
|
2510
|
+
: offscreenRender === 'hybrid'
|
|
2511
|
+
? 'hybrid'
|
|
2512
|
+
: offscreenRender
|
|
2513
|
+
? 'canvas2d'
|
|
2514
|
+
: 'main-thread',
|
|
2515
|
+
onDemandRender: onDemandRenderMode,
|
|
1717
2516
|
cacheHits: metrics.cacheHits,
|
|
1718
2517
|
cacheMisses: metrics.cacheMisses
|
|
1719
2518
|
}
|
|
@@ -1731,11 +2530,12 @@ self.getStyleCount = () => {
|
|
|
1731
2530
|
const count = akariSubHandle ? requireApi().getStyleCount(akariSubHandle) : 0;
|
|
1732
2531
|
postMessage({ target: 'getStyleCount', count });
|
|
1733
2532
|
};
|
|
1734
|
-
// =============================================================================
|
|
1735
|
-
// Message Handler
|
|
1736
|
-
// =============================================================================
|
|
1737
2533
|
const RENDER_SAFE_TARGETS = new Set([
|
|
1738
2534
|
'demand',
|
|
2535
|
+
'prepare',
|
|
2536
|
+
'presentation',
|
|
2537
|
+
'presentFrame',
|
|
2538
|
+
'frameTimelineMode',
|
|
1739
2539
|
'video',
|
|
1740
2540
|
'getEvents',
|
|
1741
2541
|
'getStyles',
|