buninu-browser 0.6.0 → 0.9.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/AGENTS.md +5 -0
- package/README.md +94 -4
- package/buninu-browser.js +1 -1
- package/frontends/wayland/README.md +35 -2
- package/frontends/wayland/client.js +8 -6
- package/frontends/wayland/clipboard.js +1 -1
- package/frontends/wayland/index.js +1 -1
- package/frontends/wayland/input.js +1 -1
- package/frontends/wayland/resource-io.js +6 -0
- package/frontends/wayland/transport-posix.js +120 -0
- package/frontends/wayland/transport-win32.js +9 -0
- package/frontends/wayland/transport.js +3 -120
- package/frontends/wayland/win32-resources.js +80 -0
- package/frontends/wayland/win32-wire.js +62 -0
- package/index.js +10 -6
- package/lib/audio/biquad.js +25 -0
- package/lib/audio/piano.js +6 -3
- package/lib/audio/playback-buffer.js +5 -5
- package/lib/audio/pulse-client.js +24 -5
- package/lib/audio/web-audio.js +570 -0
- package/lib/browser-dialog.js +116 -0
- package/lib/cdp-server.js +128 -72
- package/lib/file-picker.js +48 -0
- package/lib/headless-shell.js +34 -12
- package/lib/layout/text-layout.js +112 -19
- package/lib/media/mp4-frame-times.js +93 -0
- package/lib/paint/direct-canvas.ts +39 -0
- package/lib/paint/display-list.js +37 -12
- package/lib/paint/raster-readback.js +16 -7
- package/lib/renderer/animation-frames.js +10 -5
- package/lib/renderer/bindings.js +69 -16
- package/lib/renderer/canvas-2d.js +265 -15
- package/lib/renderer/canvas-round-rect.js +36 -0
- package/lib/renderer/css-animations.js +173 -0
- package/lib/renderer/dialog-client.js +30 -0
- package/lib/renderer/dialog-worker.js +35 -0
- package/lib/renderer/eval-environment.js +28 -0
- package/lib/renderer/file-input.js +49 -0
- package/lib/renderer/host.js +33 -4
- package/lib/renderer/html-drag.js +77 -0
- package/lib/renderer/image-loading.js +45 -2
- package/lib/renderer/main.js +4 -0
- package/lib/renderer/module-transform.js +5 -3
- package/lib/renderer/page-builtins.js +59 -0
- package/lib/renderer/page-realm.js +177 -40
- package/lib/renderer/page-renderer.js +397 -49
- package/lib/renderer/piano-input.js +3 -1
- package/lib/renderer/raster-surface.js +18 -0
- package/lib/renderer/script-rewrite.js +165 -9
- package/lib/renderer/video-presentation.js +5 -4
- package/lib/renderer/webgl/clip.js +24 -0
- package/lib/renderer/webgl/context.js +158 -0
- package/lib/renderer/webgl/cpu-fragment.js +211 -0
- package/lib/renderer/webgl/draw-cache.js +22 -0
- package/lib/renderer/webgl/pipeline.js +70 -0
- package/lib/renderer/webgl/shaders.js +58 -0
- package/lib/renderer/webgl/textures.js +43 -0
- package/lib/renderer/window-named-properties.js +46 -0
- package/lib/screenshot-cli.js +4 -7
- package/lib/style/animation-values.js +29 -0
- package/lib/style/computed-style.js +109 -13
- package/lib/style/transform.js +44 -0
- package/lib/window-size.js +10 -0
- package/package.json +1 -1
- package/patches/happy-dom/0007-scope-content-models-to-html-namespace.patch +29 -0
- package/patches/happy-dom/README.md +16 -0
- package/test/animation-frames.test.js +15 -0
- package/test/canvas-2d.test.js +252 -4
- package/test/canvas-frame-pacing.test.js +7 -0
- package/test/cdp-viewers.test.js +79 -0
- package/test/classic-games-patches/0001-fix-game-initialization.patch +177 -0
- package/test/classic-games-patches/README.md +156 -0
- package/test/classic-games-scenarios.js +133 -0
- package/test/clipboard.test.js +16 -0
- package/test/computed-style.test.js +18 -0
- package/test/css-animation-lifecycle.test.js +28 -0
- package/test/css-animations.test.js +83 -0
- package/test/direct-canvas.test.js +39 -0
- package/test/dynamic-inline-handlers.test.js +25 -0
- package/test/eval-declarations.test.js +22 -0
- package/test/file-picker.test.js +26 -0
- package/test/fixtures/canvas-damage.js +23 -0
- package/test/fixtures/canvas-frame-pacing.js +35 -0
- package/test/fixtures/direct-canvas.js +96 -0
- package/test/fixtures/javascript-dialogs.html +8 -0
- package/test/fixtures/media-audio-lifetime.js +65 -0
- package/test/fixtures/renderer-memory.js +7 -5
- package/test/fixtures/video-black-replay.js +8 -6
- package/test/fixtures/video-slow-end.js +39 -0
- package/test/fixtures/web-audio-renderer.js +45 -0
- package/test/flex.test.js +26 -0
- package/test/grid.test.js +13 -0
- package/test/headless-shell.test.js +9 -0
- package/test/headless-window-size.test.js +28 -0
- package/test/html-rcdata.test.js +35 -0
- package/test/image-decode.test.js +18 -0
- package/test/index.test.js +1 -0
- package/test/javascript-dialog.test.js +83 -0
- package/test/manual-breakout-paddle.js +42 -0
- package/test/manual-breakout-static.js +35 -0
- package/test/manual-classic-games.js +41 -0
- package/test/manual-clumsy-bird-three-pipes.js +81 -0
- package/test/manual-clumsy-bird.js +52 -0
- package/test/manual-hextris.js +30 -0
- package/test/manual-numbered-music.js +24 -0
- package/test/manual-t-rex-runner.js +49 -0
- package/test/manual-threejs-scene.js +64 -0
- package/test/manual-webgl-benchmark.js +27 -0
- package/test/manual-webgl-cpu.html +26 -0
- package/test/media-audio-lifetime.test.js +8 -0
- package/test/mp4-frame-times.test.js +33 -0
- package/test/page-scripts.test.js +96 -1
- package/test/piano-input.test.js +15 -2
- package/test/piano.test.js +11 -2
- package/test/playback-buffer.test.js +11 -0
- package/test/pointer-drag.test.js +33 -0
- package/test/positioning.test.js +82 -0
- package/test/pulse-client.test.js +51 -2
- package/test/raster-readback.test.js +14 -0
- package/test/script-rewrite.test.js +61 -3
- package/test/video-slow-end.test.js +6 -0
- package/test/viewport-keyboard-overflow.test.js +25 -0
- package/test/wayland-win32-resources.test.js +47 -0
- package/test/wayland-win32-wire.test.js +59 -0
- package/test/web-audio-renderer.test.js +8 -0
- package/test/web-audio.test.js +264 -0
- package/test/webgl-clip.test.js +9 -0
- package/test/webgl-cpu-fragment.test.js +43 -0
- package/test/window-named-properties.test.js +41 -0
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfig.ts +2 -2
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfigContentModelEnum.ts +1 -0
- package/vendor/happy-dom/packages/happy-dom/src/html-parser/HTMLParser.ts +23 -3
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +1 -1
- package/vendor/land-renderer/LICENSE +21 -0
- package/vendor/land-renderer/README.md +14 -0
- package/vendor/land-renderer/direct-compositor.ts +443 -0
- package/vendor/land-renderer/frame.js +88 -0
- package/vendor/land-renderer/png-fast.js +85 -0
- package/vendor/land-renderer/raw-frames.js +66 -0
- package/vendor/land-renderer/render-types.ts +7 -0
- package/vendor/land-renderer/renderer.ts +134 -0
- package/vendor/land-renderer/snapshot.js +37 -0
- package/vendor/land-renderer/viewport.js +18 -0
- package/vendor/ses/dist/ses.mjs +1 -1
|
@@ -17,6 +17,8 @@ export function handlePianoInput(element, input, play) {
|
|
|
17
17
|
if (note === null) return false;
|
|
18
18
|
// Suppressing text insertion must not depend on whether a note is retriggered
|
|
19
19
|
// or an audio backend is available. Shift still selects a physical piano key.
|
|
20
|
-
|
|
20
|
+
// A modifier key that is itself a piano key (AltGr) reports its own modifier.
|
|
21
|
+
const modifierKey = /^(Shift|Alt|Control|Meta)(Left|Right)$/.test(String(input.code ?? "")) || input.key === "AltGraph";
|
|
22
|
+
if (!input.repeat && (modifierKey || !(Number(input.modifiers) & 7))) play?.(note);
|
|
21
23
|
return true;
|
|
22
24
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Skia's fast software blending uses premultiplied pixels internally.
|
|
2
|
+
* Readback/export still explicitly requests ordinary unpremultiplied RGBA.
|
|
3
|
+
* Keep the allocation tied to its own CanvasKit instance, including finalizers.
|
|
4
|
+
*/
|
|
5
|
+
export function makeRasterSurface(kit,width,height) {
|
|
6
|
+
const memory=kit.Malloc(Uint8Array,width*height*4);
|
|
7
|
+
let surface;
|
|
8
|
+
try {
|
|
9
|
+
surface=kit.MakeRasterDirectSurface({width,height,
|
|
10
|
+
colorType:kit.ColorType.RGBA_8888,alphaType:kit.AlphaType.Premul,
|
|
11
|
+
colorSpace:kit.ColorSpace.SRGB},memory,width*4);
|
|
12
|
+
if(!surface){kit.Free(memory);return null;}
|
|
13
|
+
surface.getCanvas().clear(kit.TRANSPARENT);
|
|
14
|
+
let disposed=false;
|
|
15
|
+
surface.dispose=()=>{if(disposed)return;disposed=true;surface.delete();kit.Free(memory);};
|
|
16
|
+
return surface;
|
|
17
|
+
} catch(error) {surface?.delete();kit.Free(memory);throw error;}
|
|
18
|
+
}
|
|
@@ -226,13 +226,15 @@ export function rewriteClassicScript(source, parsed = null) {
|
|
|
226
226
|
export function rewriteClassicScriptSource(source) {
|
|
227
227
|
const direct = rewriteDirectEval(source);
|
|
228
228
|
const reads = rewriteConstructorReads(direct.code, "script", direct.program);
|
|
229
|
-
|
|
229
|
+
const members = rewriteMemberReads(reads.code, "script", reads.program);
|
|
230
|
+
return rewriteClassicScript(members.code, members.program);
|
|
230
231
|
}
|
|
231
232
|
|
|
232
233
|
/** A module's source with direct evals and constructor reads rewritten (see rewriteClassicScriptSource). */
|
|
233
234
|
export function rewriteModuleSource(source) {
|
|
234
235
|
const direct = rewriteDirectEval(source, "module");
|
|
235
|
-
|
|
236
|
+
const reads = rewriteConstructorReads(direct.code, "module", direct.program);
|
|
237
|
+
return rewriteMemberReads(reads.code, "module", reads.program).code;
|
|
236
238
|
}
|
|
237
239
|
|
|
238
240
|
/** Whether a function body or program starts with a "use strict" directive. */
|
|
@@ -297,6 +299,53 @@ export function boundNames(pattern) {
|
|
|
297
299
|
}
|
|
298
300
|
}
|
|
299
301
|
|
|
302
|
+
/** Page-local primitive extensions, without changing frozen shared intrinsics. */
|
|
303
|
+
export const MEMBER_HELPER = "$buninu$member";
|
|
304
|
+
export function rewriteMemberReads(source, sourceType = "script", parsed = null) {
|
|
305
|
+
let program;
|
|
306
|
+
try { program = parseSource(source, sourceType, parsed); }
|
|
307
|
+
catch { return {code:source, rewritten:false, program:null}; }
|
|
308
|
+
const edits=[];
|
|
309
|
+
const encode = text => encodeURIComponent(text).replaceAll('*','%2A');
|
|
310
|
+
const visit=(node,parent,key,writeTarget=false)=>{
|
|
311
|
+
if(node.type==='ObjectPattern'){for(const property of node.properties){if(property.type==='RestElement')visit(property.argument,property,'argument',true);else{if(property.computed)visit(property.key,property,'key');visit(property.value,property,'value',true);}}return;}
|
|
312
|
+
if(node.type==='ArrayPattern'){for(const child of node.elements)if(child)visit(child,node,'elements',true);return;}
|
|
313
|
+
if(node.type==='RestElement'){visit(node.argument,node,'argument',true);return;}
|
|
314
|
+
if(node.type==='AssignmentPattern'){visit(node.left,node,'left',true);visit(node.right,node,'right');return;}
|
|
315
|
+
if(node.type==='ChainExpression')return;
|
|
316
|
+
if(node.type==='MemberExpression'&&!node.optional&&node.object.type!=='Super'
|
|
317
|
+
&&node.property.type!=='PrivateIdentifier'&&!writeTarget&&!isWrite(parent,key)
|
|
318
|
+
&&!(parent?.type==='TaggedTemplateExpression'&&key==='tag')){
|
|
319
|
+
const call=parent?.type==='CallExpression'&&key==='callee';
|
|
320
|
+
const construct=parent?.type==='NewExpression'&&key==='callee';
|
|
321
|
+
// Preserve the source accessor in a comment, including brackets and
|
|
322
|
+
// parentheses, for exact Function#toString restoration. Receiver and
|
|
323
|
+
// computed key are each evaluated once, before any call arguments.
|
|
324
|
+
const accessorStart=source.lastIndexOf(node.computed?'[':'.',node.property.start);
|
|
325
|
+
const gap=source.slice(node.object.end,accessorStart);
|
|
326
|
+
const accessor=source.slice(node.object.end,node.computed?node.property.start:node.property.end);
|
|
327
|
+
const tail=source.slice(node.property.end,node.end);
|
|
328
|
+
edits.push({start:node.object.start,end:node.object.start,text:`(${MEMBER_HELPER}((`});
|
|
329
|
+
edits.push({start:node.object.end,end:node.property.start,text:`), ${node.computed?'(':JSON.stringify(node.property.name)+'/*$buninu$key*/'}/*$buninu$access:${encode(accessor)}*/`});
|
|
330
|
+
if(!node.computed)edits.push({start:node.property.start,end:node.property.end,text:''});
|
|
331
|
+
edits.push({start:node.property.end,end:node.end,text:`${node.computed?')':''}${call?', true':''}/*$buninu$member:${construct?'n':call?'c':'p'}:${encode(tail)}:${encode(gap)}*/))${gap}`});
|
|
332
|
+
}
|
|
333
|
+
forEachChild(node,(child,key)=>visit(child,node,key));
|
|
334
|
+
};
|
|
335
|
+
visit(program,null,null);
|
|
336
|
+
if(!edits.length)return {code:source,rewritten:false,program};
|
|
337
|
+
// Member reads also gain a leading parenthesis. Preserve ASI before this
|
|
338
|
+
// rewrite is parsed again; doing it only during the later global rewrite
|
|
339
|
+
// is too late, since two original statements can already have merged.
|
|
340
|
+
const boundaries=automaticSemicolonBoundaries(source,program);
|
|
341
|
+
if(boundaries.length) {
|
|
342
|
+
const terminated=applyEdits(source,boundaries.map(node=>({start:node.end,end:node.end,text:"/*$buninu$asi*/;"})).sort((a,b)=>b.start-a.start));
|
|
343
|
+
return rewriteMemberReads(terminated,sourceType);
|
|
344
|
+
}
|
|
345
|
+
edits.sort((a,b)=>b.start-a.start||b.end-a.end);
|
|
346
|
+
return {code:applyEdits(source,edits),rewritten:true,program:null};
|
|
347
|
+
}
|
|
348
|
+
|
|
300
349
|
/** The global the constructor-read rewrite calls (see rewriteConstructorReads). */
|
|
301
350
|
export const CONSTRUCTOR_HELPER = "$buninu$constructor";
|
|
302
351
|
|
|
@@ -456,6 +505,8 @@ function isWrite(parent, key) {
|
|
|
456
505
|
|
|
457
506
|
/** The global a direct eval calls instead (see rewriteDirectEval). */
|
|
458
507
|
export const DIRECT_EVAL_HELPER = "$buninu$eval";
|
|
508
|
+
export const DIRECT_EVAL_ENV = "$buninu$evalEnv";
|
|
509
|
+
export const MAKE_EVAL_ENV = "$buninu$makeEvalEnv";
|
|
459
510
|
/** The lexical name `this` of the calling code takes inside directly evaluated code. */
|
|
460
511
|
export const DIRECT_EVAL_THIS = "$buninu$this";
|
|
461
512
|
/** Present (true) in a direct eval's scope when the calling code is strict. */
|
|
@@ -538,6 +589,7 @@ export function rewriteDirectEval(source, sourceType = "script", outerNames = []
|
|
|
538
589
|
const edits = [];
|
|
539
590
|
// Each scope: { names: Set<string>, ordinaryFunction: boolean, strict: boolean }.
|
|
540
591
|
const scopes = [{ names: new Set(outerNames), ordinaryFunction: false, strict: outerStrict || sourceType === "module" }];
|
|
592
|
+
const environments = new Map();
|
|
541
593
|
|
|
542
594
|
const visit = (node, parent, key) => {
|
|
543
595
|
if (!node || typeof node.type !== "string") return;
|
|
@@ -570,7 +622,7 @@ export function rewriteDirectEval(source, sourceType = "script", outerNames = []
|
|
|
570
622
|
const enclosing = scopes[scopes.length - 1];
|
|
571
623
|
const strict = enclosing.strict || node.type === "ClassDeclaration" || node.type === "ClassExpression"
|
|
572
624
|
|| (node.type === "Program" && hasUseStrict(node)) || (isFunction && hasUseStrict(node.body));
|
|
573
|
-
scopes.push({ names, ordinaryFunction: isFunction ? node.type !== "ArrowFunctionExpression"
|
|
625
|
+
scopes.push({ names, functionNode: isFunction ? node : null, ordinaryFunction: isFunction ? node.type !== "ArrowFunctionExpression"
|
|
574
626
|
: enclosing.ordinaryFunction, strict });
|
|
575
627
|
}
|
|
576
628
|
if (node.type === "CallExpression" && !node.optional && node.callee.type === "Identifier"
|
|
@@ -586,6 +638,14 @@ export function rewriteDirectEval(source, sourceType = "script", outerNames = []
|
|
|
586
638
|
accessors.push("arguments: arguments");
|
|
587
639
|
}
|
|
588
640
|
if (scopes[scopes.length - 1].strict) accessors.push(`${DIRECT_EVAL_STRICT}: true`);
|
|
641
|
+
else {
|
|
642
|
+
const caller = scopes.findLast(scope => scope.functionNode)?.functionNode;
|
|
643
|
+
if (caller?.body.type === "BlockStatement") {
|
|
644
|
+
let environment = environments.get(caller);
|
|
645
|
+
if (!environment) environments.set(caller, environment = { node: caller, name: `$buninu$ev${environments.size}` });
|
|
646
|
+
accessors.push(`${DIRECT_EVAL_ENV}: ${environment.name}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
589
649
|
// An arrow function keeps the caller's `this`.
|
|
590
650
|
accessors.push(`${DIRECT_EVAL_THIS}: () => this`);
|
|
591
651
|
const open = source.indexOf("(", node.callee.end);
|
|
@@ -596,6 +656,26 @@ export function rewriteDirectEval(source, sourceType = "script", outerNames = []
|
|
|
596
656
|
if (isScope) scopes.pop();
|
|
597
657
|
};
|
|
598
658
|
visit(program, null, null);
|
|
659
|
+
if (environments.size) {
|
|
660
|
+
const envs = [...environments.values()];
|
|
661
|
+
const environmentAt = node => envs.filter(env => node.start > env.node.body.start && node.end < env.node.body.end)
|
|
662
|
+
.sort((a,b) => b.node.start - a.node.start)[0];
|
|
663
|
+
for (const env of envs) {
|
|
664
|
+
const parent = environmentAt(env.node);
|
|
665
|
+
edits.push({start:env.node.body.start+1,end:env.node.body.start+1,
|
|
666
|
+
text:`/*$buninu$env*/const ${env.name}=${MAKE_EVAL_ENV}(${parent?.name ?? "null"});/*$buninu$endenv*/`});
|
|
667
|
+
}
|
|
668
|
+
const property = (name,node) => `${environmentAt(node).name}.${name}`;
|
|
669
|
+
const dynamic = referenceEdits(program, {
|
|
670
|
+
isOuter:(name,node) => name !== 'eval' && !name.startsWith('$buninu$') && !outerNames.includes(name) && Boolean(environmentAt(node)),
|
|
671
|
+
read:property, write:property,
|
|
672
|
+
callRead:(name,node)=>`(0, ${property(name,node)}/*$buninu$ecall*/)`,
|
|
673
|
+
typeofRead:(name,node)=>`(${JSON.stringify(name)} in ${environmentAt(node).name} ? ${property(name,node)} : undefined)`,
|
|
674
|
+
shorthand:(name,node)=>`${name}: ${property(name,node)}/*$buninu$esh*/`,
|
|
675
|
+
withBodies:false, programScope:true,
|
|
676
|
+
});
|
|
677
|
+
edits.push(...dynamic.map(({node,text})=>({start:node.start,end:node.end,text})));
|
|
678
|
+
}
|
|
599
679
|
if (!edits.length) return { code: source, rewritten: false, program: reusable ? program : null };
|
|
600
680
|
// From the end; where an insertion and a replacement start at the same
|
|
601
681
|
// place (`eval(eval(x))`), the replacement goes first so the insertion
|
|
@@ -661,6 +741,43 @@ function collectDeclarations(statements, names, functionBody) {
|
|
|
661
741
|
* @param {boolean} strict whether the calling code is strict
|
|
662
742
|
* @returns {{ code: string, parsed: boolean, strict: boolean }}
|
|
663
743
|
*/
|
|
744
|
+
// Turn sloppy eval var/function declarations into writes to caller bindings.
|
|
745
|
+
// Lexical declarations remain local to this evaluation; strict eval is untouched.
|
|
746
|
+
export function evalDeclarations(source, strict = false) {
|
|
747
|
+
let program;
|
|
748
|
+
try { program = parseSource(source, "script"); } catch { return {source,names:[]}; }
|
|
749
|
+
if (strict || hasUseStrict(program)) return {source,names:[]};
|
|
750
|
+
const names = new Set(), edits = [], hoisted = [];
|
|
751
|
+
const visit = (node, parent) => {
|
|
752
|
+
if (FUNCTION_TYPES.has(node.type)) {
|
|
753
|
+
if (node.type === 'FunctionDeclaration' && parent === program) {
|
|
754
|
+
names.add(node.id.name);
|
|
755
|
+
hoisted.push(`${node.id.name}=(${source.slice(node.start,node.end)});`);
|
|
756
|
+
edits.push({start:node.start,end:node.end,text:''});
|
|
757
|
+
}
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (node.type === 'ClassBody' || node.type === 'StaticBlock') return;
|
|
761
|
+
if (node.type === 'VariableDeclaration' && node.kind === 'var') {
|
|
762
|
+
for (const item of node.declarations) for (const name of boundNames(item.id)) names.add(name);
|
|
763
|
+
const slice = node => source.slice(node.start,node.end);
|
|
764
|
+
let text;
|
|
765
|
+
if ((parent.type === 'ForInStatement' || parent.type === 'ForOfStatement') && parent.left === node)
|
|
766
|
+
text = slice(node.declarations[0].id);
|
|
767
|
+
else {
|
|
768
|
+
text = node.declarations.filter(item=>item.init).map(item => `(${slice(item.id)}=${slice(item.init)})`).join(',');
|
|
769
|
+
if (!(parent.type === 'ForStatement' && parent.init === node)) text += ';';
|
|
770
|
+
}
|
|
771
|
+
edits.push({start:node.start,end:node.end,text});
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
forEachChild(node,child=>visit(child,node));
|
|
775
|
+
};
|
|
776
|
+
visit(program,null);
|
|
777
|
+
edits.sort((a,b)=>b.start-a.start);
|
|
778
|
+
return {source:hoisted.join('')+applyEdits(source,edits),names:[...names]};
|
|
779
|
+
}
|
|
780
|
+
|
|
664
781
|
export function prepareDirectEvalSource(source, outerNames, strict = false) {
|
|
665
782
|
const code = rewriteDirectEval(source, "script", outerNames, strict).code;
|
|
666
783
|
let program;
|
|
@@ -751,7 +868,7 @@ function outerReferenceEdits(program, outerNames) {
|
|
|
751
868
|
* escapes (restoreSourceText reproduces the page's text from the name).
|
|
752
869
|
*/
|
|
753
870
|
function globalReferenceEdits(source, program, { exclude = null, programScope = false } = {}) {
|
|
754
|
-
|
|
871
|
+
const edits = referenceEdits(program, {
|
|
755
872
|
isOuter: (name, node) => !name.startsWith("$buninu$") && !exclude?.has(name) && source.slice(node.start, node.end) === name,
|
|
756
873
|
read: globalRead,
|
|
757
874
|
write: null,
|
|
@@ -760,6 +877,22 @@ function globalReferenceEdits(source, program, { exclude = null, programScope =
|
|
|
760
877
|
withBodies: false,
|
|
761
878
|
programScope,
|
|
762
879
|
});
|
|
880
|
+
if (edits.length) for(const node of automaticSemicolonBoundaries(source,program))
|
|
881
|
+
edits.push({node:{start:node.end,end:node.end},text:"/*$buninu$asi*/;"});
|
|
882
|
+
return edits;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** Statement boundaries inserted by the original parser, excluding for-loop headers. */
|
|
886
|
+
function automaticSemicolonBoundaries(source,program) {
|
|
887
|
+
const boundaries=[];
|
|
888
|
+
const visit=(node,parent)=>{
|
|
889
|
+
const terminated=["ExpressionStatement","VariableDeclaration","ReturnStatement","ThrowStatement","BreakStatement","ContinueStatement","DebuggerStatement"].includes(node.type);
|
|
890
|
+
const loopHeader=parent&&((parent.type==="ForStatement"&&parent.init===node)
|
|
891
|
+
||((parent.type==="ForInStatement"||parent.type==="ForOfStatement")&&parent.left===node));
|
|
892
|
+
if(terminated&&!loopHeader&&source[node.end-1]!==";")boundaries.push(node);
|
|
893
|
+
forEachChild(node,child=>visit(child,node));
|
|
894
|
+
};
|
|
895
|
+
visit(program,null);return boundaries;
|
|
763
896
|
}
|
|
764
897
|
|
|
765
898
|
/**
|
|
@@ -773,15 +906,15 @@ function referenceEdits(program, { isOuter, read, callRead = read, write, typeof
|
|
|
773
906
|
const free = (identifier) => isOuter(identifier.name, identifier) && !scopes.some((names) => names.has(identifier.name));
|
|
774
907
|
const reference = (identifier) => {
|
|
775
908
|
if (!free(identifier)) return false;
|
|
776
|
-
edits.push({ node: identifier, text: read(identifier.name) });
|
|
909
|
+
edits.push({ node: identifier, text: read(identifier.name, identifier) });
|
|
777
910
|
return true;
|
|
778
911
|
};
|
|
779
912
|
const assigned = (identifier) => {
|
|
780
|
-
if (write && free(identifier)) edits.push({ node: identifier, text: write(identifier.name) });
|
|
913
|
+
if (write && free(identifier)) edits.push({ node: identifier, text: write(identifier.name, identifier) });
|
|
781
914
|
};
|
|
782
915
|
const shorthand = (property) => {
|
|
783
916
|
if (!free(property.value)) return;
|
|
784
|
-
edits.push({ node: property, text: shorthandText(property.value.name) });
|
|
917
|
+
edits.push({ node: property, text: shorthandText(property.value.name, property.value) });
|
|
785
918
|
};
|
|
786
919
|
const withScope = (names, body) => {
|
|
787
920
|
scopes.push(names);
|
|
@@ -855,7 +988,7 @@ function referenceEdits(program, { isOuter, read, callRead = read, write, typeof
|
|
|
855
988
|
case "CallExpression": case "TaggedTemplateExpression": {
|
|
856
989
|
const callee = node.type === "CallExpression" ? node.callee : node.tag;
|
|
857
990
|
if (callee.type === "Identifier" && free(callee)) {
|
|
858
|
-
edits.push({ node: callee, text: callRead(callee.name) });
|
|
991
|
+
edits.push({ node: callee, text: callRead(callee.name, callee) });
|
|
859
992
|
} else expression(callee);
|
|
860
993
|
if (node.type === "CallExpression") for (const argument of node.arguments) expression(argument);
|
|
861
994
|
else expression(node.quasi);
|
|
@@ -889,7 +1022,7 @@ function referenceEdits(program, { isOuter, read, callRead = read, write, typeof
|
|
|
889
1022
|
return;
|
|
890
1023
|
case "UnaryExpression":
|
|
891
1024
|
if (node.argument.type === "Identifier" && node.operator === "typeof") {
|
|
892
|
-
if (free(node.argument)) edits.push({ node: node.argument, text: typeofRead(node.argument.name) });
|
|
1025
|
+
if (free(node.argument)) edits.push({ node: node.argument, text: typeofRead(node.argument.name, node.argument) });
|
|
893
1026
|
return;
|
|
894
1027
|
}
|
|
895
1028
|
if (node.argument.type === "Identifier" && node.operator === "delete") {
|
|
@@ -977,13 +1110,36 @@ const CONSTRUCTOR_CLOSE = /\/\*\$buninu\$ctor:([pcn]):(\d+):([^*]*?)(:s)?\*\/\)/
|
|
|
977
1110
|
* @param {string} text
|
|
978
1111
|
*/
|
|
979
1112
|
export function restoreSourceText(text) {
|
|
1113
|
+
text = text.replaceAll("/*$buninu$asi*/;", "");
|
|
980
1114
|
if (!text.includes("$buninu$") && !text.includes("__import__") && !text.includes("\\x21--")
|
|
981
1115
|
&& !text.includes("\\x2d>")) return text;
|
|
982
1116
|
let code = text;
|
|
1117
|
+
code = code.replace(/\/\*\$buninu\$env\*\/[\s\S]*?\/\*\$buninu\$endenv\*\//g, "")
|
|
1118
|
+
.replace(/\(0, \$buninu\$ev\d+\.([^\s()]+)\/\*\$buninu\$ecall\*\/\)/g, "$1")
|
|
1119
|
+
.replace(/"([^"\s]+)" in \$buninu\$ev\d+ \? \$buninu\$ev\d+\.\1 : undefined/g, "$1")
|
|
1120
|
+
.replace(/([^\s{,:]+): \$buninu\$ev\d+\.\1\/\*\$buninu\$esh\*\//g, "$1")
|
|
1121
|
+
.replace(/\$buninu\$ev\d+\./g, "");
|
|
983
1122
|
if (code.includes(GLOBAL_BINDING)) {
|
|
984
1123
|
code = code.replace(GLOBAL_SHORTHAND, "$1").replace(GLOBAL_READ, "$1").replaceAll(`${GLOBAL_BINDING}.`, "");
|
|
985
1124
|
}
|
|
986
1125
|
code = code.replaceAll(THIS_REWRITE, "this").replaceAll(`${DIRECT_EVAL_THIS}()`, "this");
|
|
1126
|
+
for(let match;(match=/\/\*\$buninu\$member:([ncp]):([^:*]*):([^*]*)\*\/\)/.exec(code));){
|
|
1127
|
+
const open=code.lastIndexOf(`${MEMBER_HELPER}((`,match.index);
|
|
1128
|
+
if(open<0)break;
|
|
1129
|
+
const access=/\/\*\$buninu\$access:([^*]*)\*\//g;
|
|
1130
|
+
access.lastIndex=open;let part,last;
|
|
1131
|
+
while((part=access.exec(code))&&part.index<match.index)last=part;
|
|
1132
|
+
if(!last)break;
|
|
1133
|
+
const middle=code.slice(open+MEMBER_HELPER.length+2,last.index);
|
|
1134
|
+
const separator=middle.lastIndexOf('), ');
|
|
1135
|
+
if(separator<0)break;
|
|
1136
|
+
let object=middle.slice(0,separator),property=code.slice(last.index+last[0].length,match.index);
|
|
1137
|
+
const computed=!middle.endsWith('/*$buninu$key*/');
|
|
1138
|
+
if(computed){if(match[1]==='c')property=property.slice(0,-6);property=property.slice(0,-1);}
|
|
1139
|
+
else{property='';}
|
|
1140
|
+
const start=open-1,end=match.index+match[0].length+1+decodeURIComponent(match[3]).length;
|
|
1141
|
+
code=code.slice(0,start)+object+decodeURIComponent(last[1])+property+decodeURIComponent(match[2])+code.slice(end);
|
|
1142
|
+
}
|
|
987
1143
|
if (code.includes(`${DIRECT_EVAL_SCOPE}.`)) {
|
|
988
1144
|
code = code.replace(/\(0, \$buninu\$s\.([^\s()]+)\/\*\$buninu\$call\*\/\)/g, "$1")
|
|
989
1145
|
.replace(SHORTHAND_PROPERTY, "$1").replaceAll(`${DIRECT_EVAL_SCOPE}.`, "");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { timeVideoDecoder } from "../media/mp4-frame-times.js";
|
|
1
2
|
import { seekVideoKeyframe } from "../media/video-keyframe.js";
|
|
2
3
|
import { captureViewport } from "../paint/frame.js";
|
|
3
4
|
|
|
@@ -22,7 +23,7 @@ export class VideoPresentation {
|
|
|
22
23
|
this.continuationFloor = null;
|
|
23
24
|
config.started = performance.now();
|
|
24
25
|
try {
|
|
25
|
-
const decoder = await this.engine.decode(config.bytes, config.options);
|
|
26
|
+
const decoder = timeVideoDecoder(await this.engine.decode(config.bytes, config.options),config.bytes);
|
|
26
27
|
// WASM opens synchronously: audio timing messages may still be queued
|
|
27
28
|
// behind it. Yield a task before choosing the resumed frame position.
|
|
28
29
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
@@ -104,7 +105,7 @@ export class VideoPresentation {
|
|
|
104
105
|
this.decoder = null;
|
|
105
106
|
let decoder;
|
|
106
107
|
try {
|
|
107
|
-
decoder = await this.engine.decode(request.bytes, current.options);
|
|
108
|
+
decoder = timeVideoDecoder(await this.engine.decode(request.bytes, current.options),request.bytes);
|
|
108
109
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
109
110
|
if (epoch !== this.epoch || this.config !== current || this.extension !== extension) {
|
|
110
111
|
decoder.close();
|
|
@@ -199,8 +200,8 @@ export class VideoPresentation {
|
|
|
199
200
|
// Only frames that can enter the queue need full-size RGBA conversion.
|
|
200
201
|
const pixels = this.decoder.nextFrame(keep ? this.width : 1, keep ? this.height : 1);
|
|
201
202
|
if (!pixels) { this.eof = true; break; }
|
|
202
|
-
if (keep) this.queue.push({ pixels, time: this.nextTime, width: this.width, height: this.height });
|
|
203
|
-
this.nextTime
|
|
203
|
+
if (keep) this.queue.push({ pixels, time: this.decoder.frameTime ?? this.nextTime, width: this.width, height: this.height });
|
|
204
|
+
this.nextTime = this.decoder.nextFrameTime ?? (this.nextTime + duration);
|
|
204
205
|
if (performance.now() >= deadline) break;
|
|
205
206
|
}
|
|
206
207
|
} catch {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Homogeneous clipping preserves attributes before perspective division.
|
|
2
|
+
export function clipTriangles(vertices, indices, stride) {
|
|
3
|
+
// Most meshes are wholly inside the frustum. Keep their indexed vertices;
|
|
4
|
+
// expanding every triangle here duplicates vertices and allocates six sets
|
|
5
|
+
// of clipping polygons despite not changing the geometry.
|
|
6
|
+
let inside=true;
|
|
7
|
+
for(let i=0;i<vertices.length&&inside;i+=stride){
|
|
8
|
+
const w=vertices[i+3];
|
|
9
|
+
for(let c=0;c<stride;c++)if(!Number.isFinite(vertices[i+c])){inside=false;break;}
|
|
10
|
+
if(w<0||Math.abs(vertices[i])>w||Math.abs(vertices[i+1])>w||Math.abs(vertices[i+2])>w)inside=false;
|
|
11
|
+
}
|
|
12
|
+
if(inside)return {vertices,indices};
|
|
13
|
+
const output=[],outIndices=[];
|
|
14
|
+
const distance=(v,plane)=>v[3]+(plane&1?-1:1)*v[plane>>1];
|
|
15
|
+
for(let t=0;t+2<indices.length;t+=3){let polygon=indices.slice(t,t+3).map(i=>Array.from(vertices.subarray(i*stride,(i+1)*stride)));
|
|
16
|
+
if(polygon.some(v=>!v.every(Number.isFinite)))continue;
|
|
17
|
+
for(let plane=0;plane<6&&polygon.length;plane++){
|
|
18
|
+
const next=[];let a=polygon.at(-1),da=distance(a,plane);
|
|
19
|
+
for(const b of polygon){const db=distance(b,plane);if((da>=0)!==(db>=0)){const w=da/(da-db);next.push(a.map((v,i)=>v+(b[i]-v)*w));}if(db>=0)next.push(b);a=b;da=db;}polygon=next;
|
|
20
|
+
}
|
|
21
|
+
for(let i=1;i+1<polygon.length;i++)for(const v of [polygon[0],polygon[i],polygon[i+1]]){outIndices.push(output.length/stride);output.push(...v);}
|
|
22
|
+
}
|
|
23
|
+
return {vertices:new Float32Array(output),indices:outIndices};
|
|
24
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {runCPUFragment} from './cpu-fragment.js';
|
|
2
|
+
import {DrawCache} from './draw-cache.js';
|
|
3
|
+
import {textureState} from './textures.js';
|
|
4
|
+
import {clipTriangles} from './clip.js';
|
|
5
|
+
import {components,glType} from './shaders.js';
|
|
6
|
+
import {compileStage,runVertices,shader,floatImage,imageShader} from './pipeline.js';
|
|
7
|
+
const GL={DEPTH_BUFFER_BIT:256,STENCIL_BUFFER_BIT:1024,COLOR_BUFFER_BIT:16384,POINTS:0,LINES:1,LINE_LOOP:2,LINE_STRIP:3,TRIANGLES:4,TRIANGLE_STRIP:5,TRIANGLE_FAN:6,ZERO:0,ONE:1,SRC_ALPHA:770,ONE_MINUS_SRC_ALPHA:771,FUNC_ADD:32774,NEVER:512,LESS:513,EQUAL:514,LEQUAL:515,GREATER:516,NOTEQUAL:517,GEQUAL:518,ALWAYS:519,FRONT:1028,BACK:1029,FRONT_AND_BACK:1032,CW:2304,CCW:2305,CULL_FACE:2884,BLEND:3042,DEPTH_TEST:2929,SCISSOR_TEST:3089,STENCIL_TEST:2960,POLYGON_OFFSET_FILL:32823,BYTE:5120,UNSIGNED_BYTE:5121,SHORT:5122,UNSIGNED_SHORT:5123,INT:5124,UNSIGNED_INT:5125,FLOAT:5126,ARRAY_BUFFER:34962,ELEMENT_ARRAY_BUFFER:34963,STATIC_DRAW:35044,DYNAMIC_DRAW:35048,STREAM_DRAW:35040,VERTEX_SHADER:35633,FRAGMENT_SHADER:35632,COMPILE_STATUS:35713,LINK_STATUS:35714,VALIDATE_STATUS:35715,ACTIVE_UNIFORMS:35718,ACTIVE_ATTRIBUTES:35721,HIGH_FLOAT:36338,MEDIUM_FLOAT:36337,LOW_FLOAT:36336,HIGH_INT:36341,MEDIUM_INT:36340,LOW_INT:36339,FLOAT_VEC2:35664,FLOAT_VEC3:35665,FLOAT_VEC4:35666,FLOAT_MAT3:35675,FLOAT_MAT4:35676,SAMPLER_2D:35678,TEXTURE_2D:3553,TEXTURE_CUBE_MAP:34067,TEXTURE_CUBE_MAP_POSITIVE_X:34069,TEXTURE0:33984,RGBA:6408,RGB:6407,ALPHA:6406,NEAREST:9728,LINEAR:9729,NEAREST_MIPMAP_NEAREST:9984,LINEAR_MIPMAP_NEAREST:9985,NEAREST_MIPMAP_LINEAR:9986,LINEAR_MIPMAP_LINEAR:9987,CLAMP_TO_EDGE:33071,REPEAT:10497,MIRRORED_REPEAT:33648,TEXTURE_MAG_FILTER:10240,TEXTURE_MIN_FILTER:10241,TEXTURE_WRAP_S:10242,TEXTURE_WRAP_T:10243,UNPACK_FLIP_Y_WEBGL:37440,UNPACK_PREMULTIPLY_ALPHA_WEBGL:37441,UNPACK_ALIGNMENT:3317,PACK_ALIGNMENT:3333,FRAMEBUFFER:36160,RENDERBUFFER:36161,FRAMEBUFFER_COMPLETE:36053,MAX_TEXTURE_SIZE:3379,MAX_CUBE_MAP_TEXTURE_SIZE:34076,MAX_TEXTURE_IMAGE_UNITS:34930,MAX_VERTEX_TEXTURE_IMAGE_UNITS:35660,MAX_COMBINED_TEXTURE_IMAGE_UNITS:35661,MAX_VERTEX_ATTRIBS:34921,MAX_VERTEX_UNIFORM_VECTORS:36347,MAX_FRAGMENT_UNIFORM_VECTORS:36349,MAX_VARYING_VECTORS:36348,MAX_VIEWPORT_DIMS:3386,VERSION:7938,SHADING_LANGUAGE_VERSION:35724,VENDOR:7936,RENDERER:7937,VIEWPORT:2978,SCISSOR_BOX:3088,NO_ERROR:0,INVALID_ENUM:1280,INVALID_VALUE:1281,INVALID_OPERATION:1282,OUT_OF_MEMORY:1285};
|
|
8
|
+
export function createSoftwareGL(kit,state,changed,options={}){
|
|
9
|
+
options ??= {};
|
|
10
|
+
let resolveImage=options.resolveImage;
|
|
11
|
+
let publicCanvas=options.canvas ? new WeakRef(options.canvas) : null;
|
|
12
|
+
let error=0,program=null,array=null,index=null,attrs=new Map(),enabled=new Set(),depth=new Float32Array(state.width*state.height).fill(1),vp=[0,0,state.width,state.height],clear=[0,0,0,0],clearZ=1,writeDepth=true,depthFunc=GL.LESS,front=GL.CCW,cull=GL.BACK;
|
|
13
|
+
// Like a graphics command buffer, defer raster work until the bitmap is read.
|
|
14
|
+
// A full color+depth clear makes earlier unobserved draws redundant. Vertex
|
|
15
|
+
// outputs and uniforms are captured at draw time, so later mutations are safe.
|
|
16
|
+
let pending=[],pendingBytes=0,colorPixels=new Uint8Array(state.width*state.height*4),pixelsDirty=false;
|
|
17
|
+
function flushPending(){const jobs=pending;pending=[];pendingBytes=0;for(const job of jobs)job();texture?.releaseRetired();if(pixelsDirty){state.raster.getCanvas().writePixels(colorPixels,state.width,state.height,0,0,kit.AlphaType.Unpremul,kit.ColorType.RGBA_8888,kit.ColorSpace.SRGB);pixelsDirty=false;}}
|
|
18
|
+
const buffers=new WeakMap(),shaders=new WeakMap(),programs=new WeakMap(),locations=new WeakMap(),resources=new Set();
|
|
19
|
+
const object=(tag,map,data)=>{const obj=Object.create(null);Object.defineProperty(obj,Symbol.toStringTag,{value:tag});map.set(obj,data);return obj;};
|
|
20
|
+
const fail=code=>{error ||= code;};
|
|
21
|
+
const getP=()=>programs.get(program);
|
|
22
|
+
const fragments=new DrawCache();
|
|
23
|
+
const texture=textureState(kit,()=>resolveImage,flushPending,fail);
|
|
24
|
+
function allocateDepth(){if(depth.length!==state.width*state.height)depth=new Float32Array(state.width*state.height).fill(1);}
|
|
25
|
+
const methods={
|
|
26
|
+
getContextAttributes:()=>({alpha:true,depth:true,stencil:false,antialias:false,premultipliedAlpha:true,preserveDrawingBuffer:true}),isContextLost:()=>!state.surface,
|
|
27
|
+
getError:()=>{const e=error;error=0;return e;},getSupportedExtensions:()=>['OES_element_index_uint'],getExtension:name=>name==='OES_element_index_uint'?Object.create(null):null,
|
|
28
|
+
getShaderPrecisionFormat:()=>({rangeMin:127,rangeMax:127,precision:23}),
|
|
29
|
+
getParameter:p=>({[GL.VERSION]:'WebGL 1.0 (Buninu CPU)',[GL.SHADING_LANGUAGE_VERSION]:'WebGL GLSL ES 1.0',[GL.VENDOR]:'Buninu',[GL.RENDERER]:'CanvasKit CPU',[GL.MAX_TEXTURE_SIZE]:4096,[GL.MAX_CUBE_MAP_TEXTURE_SIZE]:4096,[GL.MAX_TEXTURE_IMAGE_UNITS]:8,[GL.MAX_VERTEX_TEXTURE_IMAGE_UNITS]:0,[GL.MAX_COMBINED_TEXTURE_IMAGE_UNITS]:8,[GL.MAX_VERTEX_ATTRIBS]:8,[GL.MAX_VERTEX_UNIFORM_VECTORS]:256,[GL.MAX_FRAGMENT_UNIFORM_VECTORS]:256,[GL.MAX_VARYING_VECTORS]:8,[GL.MAX_VIEWPORT_DIMS]:new Int32Array([4096,4096]),[GL.VIEWPORT]:new Int32Array(vp),[GL.SCISSOR_BOX]:new Int32Array(vp),[0x846D]:new Float32Array([1,1]),[0x846E]:new Float32Array([1,1])}[p]??0),
|
|
30
|
+
createBuffer:()=>object('WebGLBuffer',buffers,{data:new Uint8Array()}),bindBuffer:(target,b)=>{if(b&&!buffers.has(b)){fail(GL.INVALID_OPERATION);return;}if(target===GL.ARRAY_BUFFER)array=b;else if(target===GL.ELEMENT_ARRAY_BUFFER)index=b;else fail(GL.INVALID_ENUM);},
|
|
31
|
+
bufferData:(target,data)=>{const b=buffers.get(target===GL.ARRAY_BUFFER?array:index);if(!b){fail(GL.INVALID_OPERATION);return;}const size=typeof data==='number'?data:data?.byteLength;if(!(size>=0&&size<=64*1024*1024)){fail(GL.INVALID_VALUE);return;}b.data=typeof data==='number'?new Uint8Array(data):new Uint8Array(data.buffer??data,data.byteOffset??0,size).slice();},
|
|
32
|
+
bufferSubData:(target,offset,data)=>{const b=buffers.get(target===GL.ARRAY_BUFFER?array:index);if(!b||offset<0||offset+data.byteLength>b.data.length){fail(GL.INVALID_VALUE);return;}b.data.set(new Uint8Array(data.buffer,data.byteOffset,data.byteLength),offset);},deleteBuffer:b=>buffers.delete(b),
|
|
33
|
+
createShader:type=>object('WebGLShader',shaders,{type,source:'',compiled:null,log:''}),shaderSource:(s,source)=>{const sh=shaders.get(s);if(sh)sh.source=String(source);else fail(GL.INVALID_OPERATION);},
|
|
34
|
+
compileShader:s=>{const sh=shaders.get(s);if(!sh)return fail(GL.INVALID_OPERATION);try{if(sh.compiled){flushPending();resources.delete(sh.compiled.effect);sh.compiled.effect.delete();}sh.compiled=compileStage(kit,sh.source,sh.type===GL.VERTEX_SHADER?'vertex':'fragment');resources.add(sh.compiled.effect);sh.log='';}catch(e){sh.compiled=null;sh.log=e.message;}},
|
|
35
|
+
getShaderParameter:(s,p)=>p===GL.COMPILE_STATUS?!!shaders.get(s)?.compiled:null,getShaderInfoLog:s=>shaders.get(s)?.log??'',getShaderSource:s=>shaders.get(s)?.source??'',deleteShader:()=>{},
|
|
36
|
+
createProgram:()=>object('WebGLProgram',programs,{shaders:[],uniforms:[],attributes:[],values:new Map(),linked:false,log:''}),attachShader:(p,s)=>programs.get(p)?.shaders.push(s),
|
|
37
|
+
linkProgram:p=>{const pr=programs.get(p);if(!pr)return;pr.vertex=pr.shaders.map(s=>shaders.get(s)).find(s=>s.type===GL.VERTEX_SHADER)?.compiled;pr.fragment=pr.shaders.map(s=>shaders.get(s)).find(s=>s.type===GL.FRAGMENT_SHADER)?.compiled;pr.linked=!!(pr.vertex&&pr.fragment);if(!pr.linked){pr.log='A shader failed to compile';return;}pr.uniforms=[...new Map([...pr.vertex.uniforms,...pr.fragment.uniforms].map(u=>[u.name,u])).values()];pr.attributes=pr.vertex.attributes;},
|
|
38
|
+
getProgramParameter:(p,k)=>{const pr=programs.get(p);return k===GL.LINK_STATUS||k===GL.VALIDATE_STATUS?!!pr?.linked:k===GL.ACTIVE_UNIFORMS?pr?.uniforms.length:k===GL.ACTIVE_ATTRIBUTES?pr?.attributes.length:0;},getProgramInfoLog:p=>programs.get(p)?.log??'',validateProgram:()=>{},
|
|
39
|
+
getActiveUniform:(p,i)=>{const u=programs.get(p)?.uniforms[i];return u?{name:u.name+(u.size>1?'[0]':''),size:u.size,type:glType(u.type)}:null;},
|
|
40
|
+
getUniformLocation:(p,name)=>{const pr=programs.get(p),n=String(name).replace(/\[0\]$/,'');return pr?.uniforms.some(u=>u.name===n)?object('WebGLUniformLocation',locations,{pr,name:n}):null;},
|
|
41
|
+
getActiveAttrib:(p,i)=>{const a=programs.get(p)?.attributes[i];return a?{name:a.name,size:1,type:glType(a.type)}:null;},getAttribLocation:(p,name)=>programs.get(p)?.attributes.findIndex(a=>a.name===name)??-1,
|
|
42
|
+
useProgram:p=>{if(p&&!programs.get(p)?.linked)return fail(GL.INVALID_OPERATION);program=p;},deleteProgram:p=>programs.delete(p),
|
|
43
|
+
enableVertexAttribArray:i=>{const a=attrs.get(i)||{};a.enabled=true;attrs.set(i,a);},disableVertexAttribArray:i=>{const a=attrs.get(i);if(a)a.enabled=false;},
|
|
44
|
+
vertexAttribPointer:(i,size,type,normalized,stride,offset)=>{attrs.set(i,{...attrs.get(i),buffer:array,size,type,normalized,stride,offset});},
|
|
45
|
+
enable:cap=>enabled.add(cap),disable:cap=>enabled.delete(cap),isEnabled:cap=>enabled.has(cap),depthFunc:v=>{depthFunc=v;},depthMask:v=>{writeDepth=!!v;},frontFace:v=>{front=v;},cullFace:v=>{cull=v;},
|
|
46
|
+
viewport:(x,y,w,h)=>{vp=[x,y,w,h];},clearColor:(...c)=>{clear=c;},clearDepth:v=>{clearZ=v;},
|
|
47
|
+
clear:mask=>{if((mask&(GL.COLOR_BUFFER_BIT|GL.DEPTH_BUFFER_BIT))===(GL.COLOR_BUFFER_BIT|GL.DEPTH_BUFFER_BIT)&&writeDepth&&!enabled.has(GL.SCISSOR_TEST)){pending=[];pendingBytes=0;}else flushPending();allocateDepth();if(mask&GL.COLOR_BUFFER_BIT){const rgba=Uint8Array.from(clear,v=>Math.round(Math.max(0,Math.min(1,v))*255)),word=new Uint32Array(rgba.buffer)[0];new Uint32Array(colorPixels.buffer).fill(word);pixelsDirty=true;changed(state);}if(mask&GL.DEPTH_BUFFER_BIT&&writeDepth)depth.fill(clearZ);},
|
|
48
|
+
drawArrays:(mode,first,count)=>draw(mode,Array.from({length:count},(_,i)=>first+i)),
|
|
49
|
+
drawElements:(mode,count,type,offset)=>{const b=buffers.get(index);if(!b)return fail(GL.INVALID_OPERATION);const bytes=type===GL.UNSIGNED_INT?4:type===GL.UNSIGNED_SHORT?2:1;if(offset<0||count<0||offset+count*bytes>b.data.length)return fail(GL.INVALID_OPERATION);const v=new DataView(b.data.buffer,b.data.byteOffset,b.data.byteLength);draw(mode,Array.from({length:count},(_,i)=>bytes===4?v.getUint32(offset+i*4,true):bytes===2?v.getUint16(offset+i*2,true):v.getUint8(offset+i)));},
|
|
50
|
+
flush:()=>{flushPending();state.raster.flush();},finish:()=>{flushPending();state.raster.flush();},
|
|
51
|
+
};
|
|
52
|
+
// Fixed-function state that does not affect the initial opaque triangle subset.
|
|
53
|
+
for(const name of ['clearStencil','colorMask','blendEquation','blendEquationSeparate','blendFunc','blendFuncSeparate','blendColor','stencilMask','stencilFunc','stencilOp','pixelStorei','scissor','polygonOffset','lineWidth','hint','activeTexture','bindTexture','texParameteri','texParameterf','texImage2D','texSubImage2D','generateMipmap','deleteTexture','bindFramebuffer','bindRenderbuffer','deleteFramebuffer','deleteRenderbuffer'])methods[name]=()=>{};
|
|
54
|
+
Object.assign(methods,texture.methods);methods.checkFramebufferStatus=()=>GL.FRAMEBUFFER_COMPLETE;
|
|
55
|
+
for(const n of [1,2,3,4])for(const suffix of ['f','i','fv','iv'])methods[`uniform${n}${suffix}`]=(loc,...v)=>{if(loc===null)return;const l=locations.get(loc);if(!l||getP()!==l.pr)return fail(GL.INVALID_OPERATION);l.pr.values.set(l.name,suffix.endsWith('v')?Array.from(v[0]):n===1?v[0]:v);};
|
|
56
|
+
for(const n of [2,3,4])methods[`uniformMatrix${n}fv`]=(loc,transpose,v)=>{if(loc===null)return;const l=locations.get(loc);if(!l||transpose||getP()!==l.pr)return fail(GL.INVALID_OPERATION);l.pr.values.set(l.name,Array.from(v));};
|
|
57
|
+
function draw(mode,indices){
|
|
58
|
+
if(mode!==GL.TRIANGLES||indices.length>1000000)return fail(GL.INVALID_ENUM);const pr=getP();if(!pr?.linked)return fail(GL.INVALID_OPERATION);allocateDepth();
|
|
59
|
+
const count=indices.reduce((m,i)=>Math.max(m,i),-1)+1;if(!count||count>1000000)return fail(GL.INVALID_VALUE);const inputs=new Map();
|
|
60
|
+
for(let i=0;i<pr.attributes.length;i++){const spec=pr.attributes[i],a=attrs.get(i),b=buffers.get(a?.buffer);if(!a?.enabled||!b)continue;const bytes=a.type===GL.FLOAT?4:2,stride=a.stride||a.size*bytes;if(a.offset+(count-1)*stride+a.size*bytes>b.data.length)return fail(GL.INVALID_OPERATION);const view=new DataView(b.data.buffer,b.data.byteOffset,b.data.byteLength),data=new Float32Array(count*spec.size);for(let v=0;v<count;v++)for(let c=0;c<spec.size;c++)data[v*spec.size+c]=c<a.size?(a.type===GL.FLOAT?view.getFloat32(a.offset+v*stride+c*4,true):view.getInt16(a.offset+v*stride+c*2,true)):0;inputs.set(spec.name,data);}
|
|
61
|
+
const saved={...pr,values:new Map(pr.values)},samplers=texture.capture(pr.fragment,pr.values),settings={vp:vp.slice(),enabled:new Set(enabled),depthFunc,writeDepth,front,cull};
|
|
62
|
+
const reusable=!settings.enabled.has(GL.DEPTH_TEST)||(settings.writeDepth&&(settings.depthFunc===GL.LESS||settings.depthFunc===GL.LEQUAL));
|
|
63
|
+
const key=reusable?fragments.key(saved,samplers,settings,[state.width,state.height],inputs,indices):null;
|
|
64
|
+
pending.push(()=>{try{
|
|
65
|
+
const cached=key&&fragments.get(key);
|
|
66
|
+
if(cached){present(cached.pixels,cached.left,cached.top,cached.width,cached.height,null,cached.depth,settings.depthFunc);changed(state);return;}
|
|
67
|
+
const verts=runVertices(kit,saved.vertex,saved.values,inputs,count);const clipped=clipTriangles(verts,indices,saved.vertex.outputCount);
|
|
68
|
+
raster(saved,clipped.vertices,clipped.indices,settings,samplers,key);
|
|
69
|
+
}catch(e){fail(GL.INVALID_OPERATION);console.error('CPU WebGL draw:',e.message);}});
|
|
70
|
+
pendingBytes+=indices.length*4+[...inputs.values()].reduce((n,a)=>n+a.byteLength,0);
|
|
71
|
+
changed(state);if(pendingBytes>32*1024*1024)flushPending();
|
|
72
|
+
}
|
|
73
|
+
function applyDepth(geometryDepth,left,top,width,height,func){
|
|
74
|
+
if(!geometryDepth)return null;
|
|
75
|
+
const visible=new Uint8Array(width*height*4);let count=0,occluded=0;
|
|
76
|
+
for(let y=0;y<height;y++)for(let x=0;x<width;x++){
|
|
77
|
+
const i=y*width+x,z=geometryDepth[i];if(z===Infinity)continue;
|
|
78
|
+
const pos=(y+top)*state.width+x+left;
|
|
79
|
+
if(func===GL.LEQUAL?z<=depth[pos]:z<depth[pos]){visible[i*4+3]=255;depth[pos]=z;count++;}else occluded++;
|
|
80
|
+
}
|
|
81
|
+
return count?(occluded?visible:null):false;
|
|
82
|
+
}
|
|
83
|
+
// The canvas surface is unpremultiplied RGBA. Compose decoded fragments in
|
|
84
|
+
// JS and upload once per flush instead of invoking Skia for every mesh/mask.
|
|
85
|
+
function present(pixels,left,top,width,height,mask,geometryDepth=null,func=GL.LESS){
|
|
86
|
+
const source=new Uint32Array(pixels.buffer,pixels.byteOffset,pixels.byteLength/4),target=new Uint32Array(colorPixels.buffer);
|
|
87
|
+
for(let y=0;y<height;y++){let pos=(y+top)*state.width+left,i=y*width;for(let x=0;x<width;x++,pos++,i++){
|
|
88
|
+
const s=i*4;if(mask&&!mask[s+3])continue;
|
|
89
|
+
if(geometryDepth){const z=geometryDepth[i];if(z===Infinity||(func===GL.LEQUAL?z>depth[pos]:z>=depth[pos]))continue;depth[pos]=z;}
|
|
90
|
+
const alpha=pixels[s+3];if(!alpha)continue;
|
|
91
|
+
const d=pos*4;
|
|
92
|
+
if(alpha===255){target[pos]=source[i];continue;}
|
|
93
|
+
const remainder=(1-alpha/255)*colorPixels[d+3]/255,opacity=alpha/255+remainder;
|
|
94
|
+
for(let c=0;c<3;c++)colorPixels[d+c]=Math.round(Math.max(0,Math.min(255,(pixels[s+c]+colorPixels[d+c]*remainder)/opacity)));
|
|
95
|
+
colorPixels[d+3]=Math.round(opacity*255);
|
|
96
|
+
}}
|
|
97
|
+
pixelsDirty=true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function raster(pr,verts,indices,{vp,enabled,depthFunc,writeDepth,front,cull},samplers,key){
|
|
101
|
+
if(!indices.length)return;
|
|
102
|
+
const width=state.width,height=state.height,n=pr.vertex.outputCount;
|
|
103
|
+
const triangles=[];let left=width,top=height,right=-1,bottom=-1;
|
|
104
|
+
const offsets=new Map();let off=4;for(const v of pr.vertex.varyings){offsets.set(v.name,off);off+=v.size;}
|
|
105
|
+
let touched=false;
|
|
106
|
+
for(let t=0;t+2<indices.length;t+=3){const ids=indices.slice(t,t+3),p=ids.map(i=>{const o=i*n,w=verts[o+3];return [vp[0]+(verts[o]/w+1)*vp[2]/2,height-vp[1]-(verts[o+1]/w+1)*vp[3]/2,verts[o+2]/w,1/w];});
|
|
107
|
+
if(p.some(a=>!a.every(Number.isFinite)||a[3]<=0))continue;
|
|
108
|
+
const area=(p[1][0]-p[0][0])*(p[2][1]-p[0][1])-(p[1][1]-p[0][1])*(p[2][0]-p[0][0]);if(!area)continue;
|
|
109
|
+
const facing=front===GL.CCW?area<0:area>0;if(enabled.has(GL.CULL_FACE)&&((cull===GL.BACK&&!facing)||(cull===GL.FRONT&&facing)||cull===GL.FRONT_AND_BACK))continue;
|
|
110
|
+
const x0=Math.max(0,Math.floor(Math.min(...p.map(a=>a[0])))),x1=Math.min(width-1,Math.ceil(Math.max(...p.map(a=>a[0]))));const y0=Math.max(0,Math.floor(Math.min(...p.map(a=>a[1])))),y1=Math.min(height-1,Math.ceil(Math.max(...p.map(a=>a[1]))));
|
|
111
|
+
if(x1<x0||y1<y0)continue;
|
|
112
|
+
triangles.push({ids,p,area,facing,x0,x1,y0,y1});left=Math.min(left,x0);right=Math.max(right,x1);top=Math.min(top,y0);bottom=Math.max(bottom,y1);
|
|
113
|
+
}
|
|
114
|
+
if(!triangles.length)return;
|
|
115
|
+
// Compact attribute images to the mesh bounds. Align to derivative quads
|
|
116
|
+
// and keep a zero border for helper samples outside triangle coverage.
|
|
117
|
+
left=Math.max(0,(left&~1)-2);top=Math.max(0,(top&~1)-2);right=Math.min(width-1,(right|1)+2);bottom=Math.min(height-1,(bottom|1)+2);
|
|
118
|
+
const rw=right-left+1,rh=bottom-top+1;
|
|
119
|
+
// Cache geometry shading independently of other draw calls' depth masks.
|
|
120
|
+
// Moving a foreground object must not invalidate a static wall's lighting.
|
|
121
|
+
const separateDepth=enabled.has(GL.DEPTH_TEST)&&writeDepth&&(depthFunc===GL.LESS||depthFunc===GL.LEQUAL);
|
|
122
|
+
const geometryDepth=separateDepth?new Float32Array(rw*rh).fill(Infinity):null;
|
|
123
|
+
const coverage=new Float32Array(rw*rh*4),varying=pr.fragment.varyings.map(v=>new Float32Array(rw*rh*4));
|
|
124
|
+
for(const {ids,p,area,facing,x0,x1,y0,y1} of triangles){
|
|
125
|
+
for(let y=y0;y<=y1;y++)for(let x=x0;x<=x1;x++){const px=x+.5,py=y+.5,b0=((p[1][0]-px)*(p[2][1]-py)-(p[1][1]-py)*(p[2][0]-px))/area,b1=((p[2][0]-px)*(p[0][1]-py)-(p[2][1]-py)*(p[0][0]-px))/area,b2=1-b0-b1;if(b0<0||b1<0||b2<0)continue;const z=Math.fround((b0*p[0][2]+b1*p[1][2]+b2*p[2][2])*.5+.5);if(z<0||z>1)continue;const pos=y*width+x;
|
|
126
|
+
const local=(y-top)*rw+x-left;
|
|
127
|
+
const oldDepth=separateDepth?geometryDepth[local]:depth[pos];
|
|
128
|
+
if(enabled.has(GL.DEPTH_TEST)&&!(depthFunc===GL.ALWAYS||(depthFunc===GL.LEQUAL?z<=oldDepth:z<oldDepth)))continue;
|
|
129
|
+
if(separateDepth)geometryDepth[local]=z;
|
|
130
|
+
else if(enabled.has(GL.DEPTH_TEST)&&writeDepth)depth[pos]=z;
|
|
131
|
+
touched=true;
|
|
132
|
+
const pixel=((y-top)*rw+x-left)*4;
|
|
133
|
+
coverage[pixel]=facing?1:-1;coverage[pixel+1]=height-py;coverage[pixel+2]=z;coverage[pixel+3]=1;
|
|
134
|
+
// Avoid per-pixel arrays and reduce callbacks: large foreground meshes can
|
|
135
|
+
// cover millions of samples in a single draw.
|
|
136
|
+
const w=b0*p[0][3]+b1*p[1][3]+b2*p[2][3],w0=b0*p[0][3]/w,w1=b1*p[1][3]/w,w2=b2*p[2][3]/w;
|
|
137
|
+
for(let k=0;k<pr.fragment.varyings.length;k++){const v=pr.fragment.varyings[k],o=offsets.get(v.name);if(o===undefined)continue;for(let c=0;c<v.size;c++)varying[k][pixel+c]=w0*verts[ids[0]*n+o+c]+w1*verts[ids[1]*n+o+c]+w2*verts[ids[2]*n+o+c];}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if(!touched)return;
|
|
141
|
+
const mask=applyDepth(geometryDepth,left,top,rw,rh,depthFunc);
|
|
142
|
+
if(mask===false)return;
|
|
143
|
+
const show=pixels=>{present(pixels,left,top,rw,rh,mask);fragments.put(key,pixels,left,top,rw,rh,geometryDepth);};
|
|
144
|
+
if(pr.fragment.cpu){
|
|
145
|
+
for(const t of samplers)pr.values.set('_sw_size_'+t.key,[t.width??1,t.height??1]);
|
|
146
|
+
const pixels=runCPUFragment(pr.fragment,pr.values,samplers,coverage,varying,rw,rh,left,top,height);
|
|
147
|
+
if(pixels){show(pixels);changed(state);return;}
|
|
148
|
+
}
|
|
149
|
+
const images=[],children=[];let effect,paint;
|
|
150
|
+
try{for(const data of [coverage,...varying]){const img=floatImage(kit,rw,rh,data);images.push(img);children.push(imageShader(kit,img,[1,0,left,0,1,top,0,0,1]));}children.push(...texture.shaders(samplers,pr.values));effect=shader(kit,pr.fragment,pr.values,children);paint=new kit.Paint();paint.setShader(effect);paint.setBlendMode(kit.BlendMode.SrcOver);
|
|
151
|
+
const surface=kit.MakeSurface(rw,rh);
|
|
152
|
+
if(surface){let image;try{const canvas=surface.getCanvas();canvas.clear(kit.TRANSPARENT);canvas.translate(-left,-top);canvas.drawRect(kit.LTRBRect(left,top,right+1,bottom+1),paint);surface.flush();image=surface.makeImageSnapshot();const pixels=image.readPixels(0,0,{width:rw,height:rh,colorType:kit.ColorType.RGBA_8888,alphaType:kit.AlphaType.Premul,colorSpace:kit.ColorSpace.SRGB});if(!pixels)throw Error('Cannot read fragment surface');show(Uint8Array.from(pixels));}finally{image?.delete();surface.dispose();}}
|
|
153
|
+
else throw Error('Cannot allocate fragment surface');
|
|
154
|
+
changed(state);}finally{paint?.delete();effect?.delete();for(const c of children)c.delete();for(const i of images)i.delete();}
|
|
155
|
+
}
|
|
156
|
+
const proxy=Object.create(null);for(const [name,value] of Object.entries({...GL,...methods}))Object.defineProperty(proxy,name,{value,enumerable:true});Object.defineProperties(proxy,{canvas:{get:()=>publicCanvas?.deref()??null},drawingBufferWidth:{get:()=>state.width},drawingBufferHeight:{get:()=>state.height},[Symbol.toStringTag]:{value:'WebGLRenderingContext'}});
|
|
157
|
+
return {proxy,flush:flushPending,dispose(){pending=[];pendingBytes=0;colorPixels=new Uint8Array();pixelsDirty=false;texture.dispose();fragments.clear();for(const r of resources)r.delete();resources.clear();},reset(){pending=[];pendingBytes=0;colorPixels=new Uint8Array(state.width*state.height*4);pixelsDirty=false;fragments.clear();depth=new Float32Array(state.width*state.height).fill(1);vp=[0,0,state.width,state.height];},configure:o=>{resolveImage=o.resolveImage;publicCanvas=o.canvas?new WeakRef(o.canvas):null;}};
|
|
158
|
+
}
|