buninu-browser 0.7.0 → 0.9.1

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.
Files changed (142) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +90 -1
  3. package/buninu-browser.js +1 -1
  4. package/frontends/wayland/README.md +35 -2
  5. package/frontends/wayland/client.js +8 -6
  6. package/frontends/wayland/clipboard.js +1 -1
  7. package/frontends/wayland/index.js +1 -1
  8. package/frontends/wayland/input.js +1 -1
  9. package/frontends/wayland/resource-io.js +6 -0
  10. package/frontends/wayland/transport-posix.js +120 -0
  11. package/frontends/wayland/transport-win32.js +9 -0
  12. package/frontends/wayland/transport.js +3 -120
  13. package/frontends/wayland/win32-resources.js +80 -0
  14. package/frontends/wayland/win32-wire.js +62 -0
  15. package/lib/audio/biquad.js +25 -0
  16. package/lib/audio/piano.js +6 -3
  17. package/lib/audio/playback-buffer.js +5 -5
  18. package/lib/audio/pulse-client.js +24 -5
  19. package/lib/audio/web-audio.js +570 -0
  20. package/lib/browser-dialog.js +126 -0
  21. package/lib/cdp-server.js +43 -11
  22. package/lib/file-picker.js +48 -0
  23. package/lib/headless-shell.js +34 -12
  24. package/lib/layout/text-layout.js +112 -19
  25. package/lib/media/mp4-frame-times.js +93 -0
  26. package/lib/paint/direct-canvas.ts +39 -0
  27. package/lib/paint/display-list.js +37 -12
  28. package/lib/paint/raster-readback.js +16 -7
  29. package/lib/renderer/animation-frames.js +10 -5
  30. package/lib/renderer/bindings.js +69 -16
  31. package/lib/renderer/canvas-2d.js +265 -15
  32. package/lib/renderer/canvas-round-rect.js +36 -0
  33. package/lib/renderer/css-animations.js +173 -0
  34. package/lib/renderer/dialog-client.js +30 -0
  35. package/lib/renderer/dialog-worker.js +35 -0
  36. package/lib/renderer/eval-environment.js +28 -0
  37. package/lib/renderer/file-input.js +49 -0
  38. package/lib/renderer/host.js +33 -4
  39. package/lib/renderer/html-drag.js +77 -0
  40. package/lib/renderer/image-loading.js +45 -2
  41. package/lib/renderer/main.js +4 -0
  42. package/lib/renderer/module-transform.js +5 -3
  43. package/lib/renderer/page-builtins.js +59 -0
  44. package/lib/renderer/page-realm.js +177 -40
  45. package/lib/renderer/page-renderer.js +397 -49
  46. package/lib/renderer/piano-input.js +3 -1
  47. package/lib/renderer/raster-surface.js +18 -0
  48. package/lib/renderer/script-rewrite.js +165 -9
  49. package/lib/renderer/video-presentation.js +5 -4
  50. package/lib/renderer/webgl/clip.js +24 -0
  51. package/lib/renderer/webgl/context.js +158 -0
  52. package/lib/renderer/webgl/cpu-fragment.js +211 -0
  53. package/lib/renderer/webgl/draw-cache.js +22 -0
  54. package/lib/renderer/webgl/pipeline.js +70 -0
  55. package/lib/renderer/webgl/shaders.js +58 -0
  56. package/lib/renderer/webgl/textures.js +43 -0
  57. package/lib/renderer/window-named-properties.js +46 -0
  58. package/lib/screenshot-cli.js +4 -7
  59. package/lib/style/animation-values.js +29 -0
  60. package/lib/style/computed-style.js +109 -13
  61. package/lib/style/transform.js +44 -0
  62. package/lib/window-size.js +10 -0
  63. package/package.json +1 -1
  64. package/patches/happy-dom/0007-scope-content-models-to-html-namespace.patch +29 -0
  65. package/patches/happy-dom/README.md +16 -0
  66. package/test/animation-frames.test.js +15 -0
  67. package/test/canvas-2d.test.js +252 -4
  68. package/test/canvas-frame-pacing.test.js +7 -0
  69. package/test/cdp-viewers.test.js +36 -0
  70. package/test/classic-games-patches/0001-fix-game-initialization.patch +177 -0
  71. package/test/classic-games-patches/README.md +156 -0
  72. package/test/classic-games-scenarios.js +133 -0
  73. package/test/clipboard.test.js +16 -0
  74. package/test/computed-style.test.js +18 -0
  75. package/test/css-animation-lifecycle.test.js +28 -0
  76. package/test/css-animations.test.js +83 -0
  77. package/test/direct-canvas.test.js +39 -0
  78. package/test/dynamic-inline-handlers.test.js +25 -0
  79. package/test/eval-declarations.test.js +22 -0
  80. package/test/file-picker.test.js +34 -0
  81. package/test/fixtures/canvas-damage.js +23 -0
  82. package/test/fixtures/canvas-frame-pacing.js +35 -0
  83. package/test/fixtures/direct-canvas.js +96 -0
  84. package/test/fixtures/javascript-dialogs.html +8 -0
  85. package/test/fixtures/media-audio-lifetime.js +65 -0
  86. package/test/fixtures/renderer-memory.js +7 -5
  87. package/test/fixtures/video-black-replay.js +8 -6
  88. package/test/fixtures/video-slow-end.js +39 -0
  89. package/test/fixtures/web-audio-renderer.js +45 -0
  90. package/test/flex.test.js +26 -0
  91. package/test/grid.test.js +13 -0
  92. package/test/headless-shell.test.js +9 -0
  93. package/test/headless-window-size.test.js +28 -0
  94. package/test/html-rcdata.test.js +35 -0
  95. package/test/image-decode.test.js +18 -0
  96. package/test/javascript-dialog.test.js +83 -0
  97. package/test/manual-breakout-paddle.js +42 -0
  98. package/test/manual-breakout-static.js +35 -0
  99. package/test/manual-classic-games.js +41 -0
  100. package/test/manual-clumsy-bird-three-pipes.js +81 -0
  101. package/test/manual-clumsy-bird.js +52 -0
  102. package/test/manual-hextris.js +30 -0
  103. package/test/manual-numbered-music.js +24 -0
  104. package/test/manual-t-rex-runner.js +49 -0
  105. package/test/manual-threejs-scene.js +64 -0
  106. package/test/manual-webgl-benchmark.js +27 -0
  107. package/test/manual-webgl-cpu.html +26 -0
  108. package/test/media-audio-lifetime.test.js +8 -0
  109. package/test/mp4-frame-times.test.js +33 -0
  110. package/test/page-scripts.test.js +96 -1
  111. package/test/piano-input.test.js +15 -2
  112. package/test/piano.test.js +11 -2
  113. package/test/playback-buffer.test.js +11 -0
  114. package/test/pointer-drag.test.js +33 -0
  115. package/test/positioning.test.js +82 -0
  116. package/test/pulse-client.test.js +51 -2
  117. package/test/raster-readback.test.js +14 -0
  118. package/test/script-rewrite.test.js +61 -3
  119. package/test/video-slow-end.test.js +6 -0
  120. package/test/viewport-keyboard-overflow.test.js +25 -0
  121. package/test/wayland-win32-resources.test.js +47 -0
  122. package/test/wayland-win32-wire.test.js +59 -0
  123. package/test/web-audio-renderer.test.js +8 -0
  124. package/test/web-audio.test.js +264 -0
  125. package/test/webgl-clip.test.js +9 -0
  126. package/test/webgl-cpu-fragment.test.js +43 -0
  127. package/test/window-named-properties.test.js +41 -0
  128. package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfig.ts +2 -2
  129. package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfigContentModelEnum.ts +1 -0
  130. package/vendor/happy-dom/packages/happy-dom/src/html-parser/HTMLParser.ts +23 -3
  131. package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +1 -1
  132. package/vendor/land-renderer/LICENSE +21 -0
  133. package/vendor/land-renderer/README.md +14 -0
  134. package/vendor/land-renderer/direct-compositor.ts +443 -0
  135. package/vendor/land-renderer/frame.js +88 -0
  136. package/vendor/land-renderer/png-fast.js +85 -0
  137. package/vendor/land-renderer/raw-frames.js +66 -0
  138. package/vendor/land-renderer/render-types.ts +7 -0
  139. package/vendor/land-renderer/renderer.ts +134 -0
  140. package/vendor/land-renderer/snapshot.js +37 -0
  141. package/vendor/land-renderer/viewport.js +18 -0
  142. package/vendor/ses/dist/ses.mjs +1 -1
@@ -0,0 +1,211 @@
1
+ // Optional numeric CPU lowering for the straight-line GLSL/SkSL subset. The
2
+ // shader is first validated by CanvasKit; unsupported syntax keeps that path.
3
+ // Only tokens parsed below generate code. Source identifiers never become JS
4
+ // identifiers, property accesses or host calls. No page text is evaluated.
5
+ // Keep the host compiler before SES replaces Function with its compartment
6
+ // evaluator. Only our validated numeric IR reaches it; GLSL identifiers and
7
+ // source snippets are never interpolated into the generated JavaScript.
8
+ const NumericFunction = Function;
9
+ const scalar=new Set(['float','int','bool','void','half']);
10
+ const qualifiers=new Set(['const','in','out','inout','uniform','highp','mediump','lowp']);
11
+ class Parser {
12
+ constructor(source){this.t=source.match(/[A-Za-z_]\w*|(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?|==|!=|<=|>=|&&|\|\||\+=|-=|\*=|\/=|\+\+|--|[{}()[\].,;?:+*/%<>=!-]/g)??[];this.i=0;this.types=new Set([...scalar,'shader',...Array.from({length:3},(_,i)=>'float'+(i+2)),...Array.from({length:3},(_,i)=>'float'+(i+2)+'x'+(i+2))]);this.structs=new Map();this.functions=new Map();this.globals=[];if(this.t.length>60000)throw Error('shader token budget');}
13
+ peek(){return this.t[this.i];}pop(){if(this.i>=this.t.length)throw Error('Unexpected shader end');return this.t[this.i++];}
14
+ take(t){if(this.peek()===t){this.i++;return true;}return false;}need(t){if(!this.take(t))throw Error('Expected '+t+' got '+this.peek());}
15
+ type(){const q=[];while(qualifiers.has(this.peek()))q.push(this.pop());const type=this.pop();if(!this.types.has(type))throw Error('Unknown type '+type);return {type,q};}
16
+ dimensions(type){if(this.take('[')){const n=Number(this.pop());this.need(']');if(!Number.isInteger(n)||n<1||n>64)throw Error('array bound');return {array:type,size:n};}return type;}
17
+ parse(){while(this.i<this.t.length){if(this.take(';'))continue;if(this.take('struct')){const name=this.pop();this.need('{');const fields=[];while(!this.take('}')){const t=this.type(),n=this.pop(),type=this.dimensions(t.type);this.need(';');fields.push({name:n,type});}this.need(';');this.structs.set(name,fields);this.types.add(name);continue;}
18
+ const t=this.type(),name=this.pop();if(this.take('(')){const args=[];if(!this.take(')')){do{const p=this.type(),n=this.pop();args.push({name:n,type:this.dimensions(p.type),q:p.q});}while(this.take(','));this.need(')');}const body=this.statement();const f={name,type:t.type,args,body};const list=this.functions.get(name)??[];list.push(f);this.functions.set(name,list);
19
+ }else{this.globals.push(...this.declarations(t,name));this.need(';');}}
20
+ return this;
21
+ }
22
+ declarations(t,name){const out=[];do{const type=this.dimensions(t.type),value=this.take('=')?this.expr(2):null;out.push({kind:'decl',type,q:t.q,name,value});if(!this.take(','))break;name=this.pop();}while(true);return out;}
23
+ statement(){this.nesting=(this.nesting??0)+1;if(this.nesting>256)throw Error('shader nesting budget');try{return this.statementBody();}finally{this.nesting--;}}
24
+ statementBody(){if(this.take('{')){const body=[];while(!this.take('}'))body.push(this.statement());return {kind:'block',body};}
25
+ if(this.take('return')){const value=this.peek()===';'?null:this.expr();this.need(';');return {kind:'return',value};}
26
+ if(this.take('if')){this.need('(');const cond=this.expr();this.need(')');const yes=this.statement(),no=this.take('else')?this.statement():null;return {kind:'if',cond,yes,no};}
27
+ if(this.take('discard')){this.need(';');return {kind:'discard'};}
28
+ if(this.types.has(this.peek())||qualifiers.has(this.peek())){const t=this.type(),name=this.pop(),body=this.declarations(t,name);this.need(';');return {kind:'decls',body};}
29
+ if(this.take(';'))return {kind:'block',body:[]};
30
+ const value=this.expr();this.need(';');return {kind:'expr',value};
31
+ }
32
+ expr(min=0){this.nesting=(this.nesting??0)+1;if(this.nesting>256)throw Error('shader nesting budget');try{return this.expressionBody(min);}finally{this.nesting--;}}
33
+ expressionBody(min){let t=this.pop(),left;if(t==='('){left=this.expr();this.need(')');}else if(['-','+','!'].includes(t))left={kind:'unary',op:t,value:this.expr(14)};else if(/^\d|^\./.test(t))left={kind:'number',value:Number(t)};else left={kind:'id',name:t};
34
+ const prec={'=':1,'+=':1,'-=':1,'*=':1,'/=':1,'?':2,'||':3,'&&':4,'==':5,'!=':5,'<':6,'>':6,'<=':6,'>=':6,'+':7,'-':7,'*':8,'/':8,'%':8};
35
+ while(this.i<this.t.length){const op=this.peek();if(op==='('){this.pop();const args=[];if(!this.take(')')){do{args.push(this.expr(2));}while(this.take(','));this.need(')');}left={kind:'call',callee:left,args};continue;}
36
+ if(op==='.'||op==='['){this.pop();const key=op==='.'?this.pop():this.expr();if(op==='[')this.need(']');left={kind:op==='.'?'field':'index',object:left,key};continue;}
37
+ const p=prec[op];if(p===undefined||p<min)break;this.pop();if(op==='?'){const yes=this.expr();this.need(':');left={kind:'ternary',cond:left,yes,no:this.expr(p)};}else left={kind:'binary',op,left,right:this.expr(p+(p===1?0:1))};
38
+ }return left;
39
+ }
40
+ }
41
+ class Compiler {
42
+ constructor(parsed,varyings,uniforms){Object.assign(this,{parsed,varyings,uniforms});this.lines=[];this.n=0;this.scopes=[new Map()];this.depth=0;this.derivatives=[];this.samplerNames=[];this.controlDepth=0;this.mainStatement=null;this.derivativeStatement=null;}
43
+ line(s){this.lines.push(s);if(this.lines.length>50000)throw Error('shader instruction budget');}
44
+ size(type){
45
+ this.sizes??=new Map();if(this.sizes.has(type))return this.sizes.get(type);
46
+ this.typeDepth=(this.typeDepth??0)+1;if(this.typeDepth>32)throw Error('shader type depth');
47
+ try{let n;if(typeof type==='object')n=type.size*this.size(type.array);
48
+ else if(this.parsed.structs.has(type))n=this.parsed.structs.get(type).reduce((n,f)=>n+this.size(f.type),0);
49
+ else {const ns=type.match(/\d/g);n=type==='void'?0:ns?ns.reduce((n,v)=>n*Number(v),1):1;}
50
+ if(n>1024)throw Error('shader value size');this.sizes.set(type,n);return n;
51
+ }finally{this.typeDepth--;}
52
+ }
53
+ value(type,parts){return {type,parts};}
54
+ variable(type,initial){const names=Array.from({length:this.size(type)},()=>`r${++this.n}`);for(let i=0;i<names.length;i++)this.line(`let ${names[i]}=${initial?.[i]??0};`);return this.value(type,names);}
55
+ lookup(name){for(let i=this.scopes.length-1;i>=0;i--)if(this.scopes[i].has(name))return this.scopes[i].get(name);if(name==='true'||name==='false')return this.value('bool',[name]);throw Error('Unknown shader identifier '+name);}
56
+ assign(to,from,op='='){if(to.parts.length!==from.parts.length&&from.parts.length!==1)throw Error('assignment shape');const tmp=this.variable(to.type,to.parts.map((_,i)=>from.parts[i%from.parts.length]));for(let i=0;i<to.parts.length;i++)this.line(`${to.parts[i]}${op}${tmp.parts[i]};`);return to;}
57
+ expr(e){if(e.kind==='number')return this.value('float',[String(e.value)]);if(e.kind==='id')return this.lookup(e.name);
58
+ if(e.kind==='field'){const a=this.expr(e.object);if(this.parsed.structs.has(a.type)){let offset=0;for(const f of this.parsed.structs.get(a.type)){const n=this.size(f.type);if(f.name===e.key)return this.value(f.type,a.parts.slice(offset,offset+n));offset+=n;}throw Error('struct field');}
59
+ const chars=e.key.split(''),index=c=>'xyzw'.includes(c)?'xyzw'.indexOf(c):'rgba'.includes(c)?'rgba'.indexOf(c):'stpq'.indexOf(c);if(chars.some(c=>index(c)<0||index(c)>=a.parts.length))throw Error('swizzle');return this.value(chars.length===1?'float':'float'+chars.length,chars.map(c=>a.parts[index(c)]));}
60
+ if(e.kind==='index'){const a=this.expr(e.object),index=this.expr(e.key).parts[0],n=typeof a.type==='object'?this.size(a.type.array):a.type.includes('x')?Number(a.type.at(-1)):1,type=typeof a.type==='object'?a.type.array:n>1?'float'+n:'float';
61
+ if(/^\d+$/.test(index))return this.value(type,a.parts.slice(Number(index)*n,(Number(index)+1)*n));
62
+ return this.value(type,Array.from({length:n},(_,j)=>a.parts.filter((_,i)=>i%n===j).map((v,i,all)=>i===all.length-1?v:`(${index}===${i}?${v}:`).join('')+')'.repeat(a.parts.length/n-1)));}
63
+ if(e.kind==='unary'){const a=this.expr(e.value);return this.variable(e.op==='!'?'bool':a.type,a.parts.map(x=>`(${e.op}${x})`));}
64
+ if(e.kind==='ternary'){
65
+ const c=this.expr(e.cond),outer=this.lines;this.lines=[];this.controlDepth++;
66
+ const a=this.expr(e.yes),yes=this.lines;this.lines=[];const b=this.expr(e.no),no=this.lines;this.lines=outer;this.controlDepth--;
67
+ if(a.parts.length!==b.parts.length)throw Error('conditional shape');const out=this.variable(a.type);
68
+ this.line(`if(${c.parts[0]}){`);for(const line of yes)this.line(line);this.assign(out,a);this.line('}else{');for(const line of no)this.line(line);this.assign(out,b);this.line('}');return out;
69
+ }
70
+ if(e.kind==='binary'){const a=this.expr(e.left);if(['=','+=','-=','*=','/='].includes(e.op))return this.assign(a,this.expr(e.right),e.op);if(e.op==='&&'||e.op==='||'){
71
+ const out=this.variable('bool',[a.parts[0]]);this.line(`if(${e.op==='||'?'!':''}${out.parts[0]}){`);this.controlDepth++;this.assign(out,this.expr(e.right));this.controlDepth--;this.line('}');return out;
72
+ }const b=this.expr(e.right),n=Math.max(a.parts.length,b.parts.length);if(e.op==='*'&&(String(a.type).includes('x')||String(b.type).includes('x')))return this.matrix(a,b);
73
+ return this.variable(n===1?(['==','!=','<=','>=','<','>','&&','||'].includes(e.op)?'bool':a.type):'float'+n,Array.from({length:n},(_,i)=>`(${a.parts[i%a.parts.length]}${e.op}${b.parts[i%b.parts.length]})`));}
74
+ if(e.kind==='call'){if(e.callee.kind==='field'&&e.callee.key==='eval'){const s=this.expr(e.callee.object),uv=this.expr(e.args[0]);return this.sampleTexture(s,uv);}
75
+ if(e.callee.kind!=='id')throw Error('shader callee');const name=e.callee.name;
76
+ if(name==='dFdx'||name==='dFdy'){
77
+ if(this.controlDepth||this.depth!==1||(this.derivativeStatement!==null&&this.derivativeStatement!==this.mainStatement))throw Error('nonlocal derivative');
78
+ this.derivativeStatement=this.mainStatement;
79
+ const before=this.derivatives.length,a=this.expr(e.args[0]);if(this.derivatives.length!==before)throw Error('nested derivative');const id=this.derivatives.length;this.derivatives.push({name,size:a.parts.length});for(let i=0;i<a.parts.length;i++)this.line(`captured[${id*4+i}]=${a.parts[i]};`);return this.value(a.type,a.parts.map((_,i)=>`delta[${id*4+i}]`));}
80
+ const args=e.args.map(a=>this.expr(a));
81
+ if(this.parsed.types.has(name))return this.construct(name,args);
82
+ const builtin=this.builtin(name,args);if(builtin)return builtin;
83
+ const candidates=this.parsed.functions.get(name)??[],fn=candidates.find(f=>f.args.length===args.length&&f.args.every((a,i)=>this.size(a.type)===args[i].parts.length));if(!fn)throw Error('Unsupported function '+name);return this.call(fn,args);
84
+ }throw Error('Unsupported expression '+e.kind);
85
+ }
86
+ construct(type,args){let parts=args.flatMap(a=>a.parts),n=this.size(type);if(n===1)return this.variable(type,[type==='bool'?`!!(${parts[0]})`:type==='int'?`Math.trunc(${parts[0]})`:parts[0]]);
87
+ if(parts.length===1){if(type.includes('x')){const dim=Number(type.at(-1));parts=Array.from({length:n},(_,i)=>i%(dim+1)===0?parts[0]:'0');}else parts=Array(n).fill(parts[0]);}
88
+ if(parts.length<n)throw Error('constructor shape');return this.variable(type,parts.slice(0,n));
89
+ }
90
+ matrix(a,b){const am=String(a.type).includes('x'),bm=String(b.type).includes('x');if(am&&bm)throw Error('matrix matrix subset');const m=am?a:b,v=am?b:a,n=Math.sqrt(m.parts.length);if(v.parts.length!==n)throw Error('matrix shape');return this.variable('float'+n,Array.from({length:n},(_,row)=>Array.from({length:n},(_,col)=>`(${m.parts[am?col*n+row:row*n+col]}*${v.parts[col]})`).join('+')));}
91
+ builtin(name,args){const a=args[0],b=args[1],c=args[2],n=Math.max(...args.map(a=>a.parts.length)),type=n===1?'float':'float'+n;
92
+ const at=(a,i)=>a.parts[i%a.parts.length],each=fn=>this.variable(type,Array.from({length:n},(_,i)=>fn(i)));
93
+ if(['abs','sqrt','exp','log','floor','ceil','sin','cos','tan','asin','acos','sign'].includes(name))return each(i=>`Math.${name}(${at(a,i)})`);
94
+ if(name==='atan')return each(i=>b?`Math.atan2(${at(a,i)},${at(b,i)})`:`Math.atan(${at(a,i)})`);
95
+ if(name==='exp2')return each(i=>`Math.pow(2,${at(a,i)})`);
96
+ if(name==='log2')return each(i=>`Math.log2(${at(a,i)})`);
97
+ if(name==='inversesqrt')return each(i=>`(1/Math.sqrt(${at(a,i)}))`);
98
+ if(name==='pow')return each(i=>`Math.pow(${at(a,i)},${at(b,i)})`);
99
+ if(name==='min'||name==='max')return each(i=>`Math.${name}(${at(a,i)},${at(b,i)})`);
100
+ if(name==='clamp')return each(i=>`Math.max(${at(b,i)},Math.min(${at(c,i)},${at(a,i)}))`);
101
+ if(name==='mix')return each(i=>`(${at(a,i)}*(1-${at(c,i)})+${at(b,i)}*${at(c,i)})`);
102
+ if(name==='mod')return each(i=>`(${at(a,i)}-${at(b,i)}*Math.floor(${at(a,i)}/${at(b,i)}))`);
103
+ if(name==='fract')return each(i=>`(${at(a,i)}-Math.floor(${at(a,i)}))`);
104
+ if(name==='step')return each(i=>`(${at(b,i)}<${at(a,i)}?0:1)`);
105
+ if(name==='smoothstep'){const t=this.variable(type,Array.from({length:n},(_,i)=>`Math.max(0,Math.min(1,(${at(c,i)}-${at(a,i)})/(${at(b,i)}-${at(a,i)})))`));return this.variable(type,t.parts.map(v=>`(${v}*${v}*(3-2*${v}))`));}
106
+ if(name==='dot')return this.variable('float',[a.parts.map((v,i)=>`(${v}*${b.parts[i]})`).join('+')]);
107
+ if(name==='length'||name==='normalize'){const len=this.variable('float',[`Math.sqrt(${a.parts.map(v=>`(${v}*${v})`).join('+')})`]).parts[0];return name==='length'?this.value('float',[len]):this.variable(a.type,a.parts.map(v=>`(${v}/${len})`));}
108
+ if(name==='cross')return this.variable('float3',[0,1,2].map(i=>`(${a.parts[(i+1)%3]}*${b.parts[(i+2)%3]}-${a.parts[(i+2)%3]}*${b.parts[(i+1)%3]})`));
109
+ if(name==='reflect'){const dot=a.parts.map((v,i)=>`(${v}*${b.parts[i]})`).join('+');return this.variable(a.type,a.parts.map((v,i)=>`(${v}-2*(${dot})*${b.parts[i]})`));}
110
+ const op={lessThan:'<',lessThanEqual:'<=',greaterThan:'>',greaterThanEqual:'>=',equal:'===',notEqual:'!=='}[name];if(op)return each(i=>`(${at(a,i)}${op}${at(b,i)})`);
111
+ return null;
112
+ }
113
+ sampleTexture(s,uv){const id=Number(s.parts[0]);if(!Number.isInteger(id))throw Error('dynamic sampler');const out=this.variable('float4');this.line(`sampleTexture(textures[${id}],${uv.parts[0]},${uv.parts[1]},texel);`);for(let i=0;i<4;i++)this.line(`${out.parts[i]}=texel[${i}]${i<3?"*texel[3]":""};`);return out;}
114
+ stmt(s,ret,label){if(s.kind==='block'){this.scopes.push(new Map());for(const node of s.body){if(this.depth===1)this.mainStatement=node;this.stmt(node,ret,label);}this.scopes.pop();}
115
+ else if(s.kind==='decls'){for(const d of s.body){const init=d.value?this.expr(d.value):null;const v=this.variable(d.type,init?.parts);this.scopes.at(-1).set(d.name,v);}}
116
+ else if(s.kind==='expr')this.expr(s.value);
117
+ else if(s.kind==='if'){const c=this.expr(s.cond);this.line(`if(${c.parts[0]}){`);this.controlDepth++;this.stmt(s.yes,ret,label);if(s.no){this.line('}else{');this.stmt(s.no,ret,label);}this.controlDepth--;this.line('}');}
118
+ else if(s.kind==='return'){if(s.value)this.assign(ret,this.expr(s.value));this.line(`break ${label};`);}
119
+ else if(s.kind==='discard')throw Error('discard requires native shader');
120
+ else throw Error('Unsupported statement');
121
+ }
122
+ call(fn,args){if(++this.depth>32)throw Error('shader call depth');const ret=this.variable(fn.type),label='L'+(++this.n);this.line(`${label}:{`);this.scopes.push(new Map());
123
+ for(let i=0;i<args.length;i++)this.scopes.at(-1).set(fn.args[i].name,fn.args[i].q.some(q=>q==='out'||q==='inout')?args[i]:this.variable(fn.args[i].type,args[i].parts));
124
+ this.stmt(fn.body,ret,label);this.scopes.pop();this.line('}');this.depth--;return ret;
125
+ }
126
+ compile(init,outputNames=['gl_FragColor']){
127
+ const uniformSlots=[];const global=this.scopes[0];
128
+ for(const d of this.parsed.globals){if(d.type==='shader'){global.set(d.name,this.value('shader',[String(this.samplerNames.length)]));this.samplerNames.push(d.name);continue;}
129
+ const v=this.variable(d.type);global.set(d.name,v);
130
+ if(d.q.includes('uniform')){const offset=uniformSlots.length;for(let i=0;i<v.parts.length;i++){uniformSlots.push([d.name,i]);this.line(`${v.parts[i]}=uniforms[${offset+i}];`);}}
131
+ else if(d.value)this.assign(v,this.expr(d.value));
132
+ }
133
+ const initParser=new Parser('void _init(){'+init+'}').parse();if(init)this.stmt(initParser.functions.get('_init')[0].body,this.value('void',[]),'unused');
134
+ const mutable=this.parsed.globals.filter(d=>d.type!=='shader'&&!d.q.includes('uniform')&&!d.q.includes('const')&&!this.varyings.some(v=>v.name===d.name)).flatMap(d=>this.lookup(d.name).parts);
135
+ this.line(`const initialGlobals=[${mutable.join(',')}];`);
136
+ const declarations=this.lines.join('\n');this.lines=[];
137
+ mutable.forEach((v,i)=>this.line(`let ${v}=initialGlobals[${i}];`));
138
+ for(let k=0;k<this.varyings.length;k++){const v=this.lookup(this.varyings[k].name);for(let c=0;c<v.parts.length;c++)this.line(`let ${v.parts[c]}=varyings[${k}][index+${c}];`);}
139
+ const coord=global.get('gl_FragCoord');if(coord)coord.parts.forEach((v,i)=>this.line(`${v}=${['fx','fy','z','1'][i]};`));const face=global.get('gl_FrontFacing');if(face)this.line(`${face.parts[0]}=front;`);
140
+ const start=this.lines.length;this.call(this.parsed.functions.get('_sw_run')[0],[]);
141
+ const body=this.lines.join('\n'),outputs=outputNames.flatMap(name=>{const value=global.get(name);if(!value)throw Error('shader output');return value.parts;});
142
+ const output=outputs.map((v,i)=>`result[${i}]=${v};`).join('');
143
+ // The CPU JIT can skip the remainder of main for derivative helper lanes.
144
+ // Find the last derivative capture in the emitted straight-line code.
145
+ let shaderBody=body;
146
+ if(this.derivatives.length){const end=Math.max(...[...body.matchAll(/captured\[\d+\]=[^;]+;/g)].map(m=>m.index+m[0].length));if(end>0)shaderBody=body.slice(0,end)+'if(collect)return;'+body.slice(end);}
147
+ const code=`"use strict";${declarations}\nreturn function shade(index,fx,fy,z,front,collect){${shaderBody}\n${output}};`;
148
+ const make=new NumericFunction('uniforms','varyings','delta','captured','result','textures','sampleTexture','texel',code);
149
+ // Specialization leaves uniforms as numeric literals inside the function,
150
+ // allowing the JS JIT to fold lighting/material constants across pixels.
151
+ // Cache factories, not bound images/arrays, so draw resources are not retained.
152
+ const localBody=shaderBody.replace(/let r\d+=initialGlobals\[\d+\];/g,'').replace(/let (r\d+=varyings\[)/g,'$1');
153
+ const localDecl=declarations.replace(/const initialGlobals=\[[^;]*;/,'');
154
+ const literalCode=`"use strict";return function shade(index,fx,fy,z,front,collect){${localDecl}\n${localBody}\n${output}};`;
155
+ return {make,uniformSlots,samplers:this.samplerNames,derivatives:this.derivatives,code,literalCode,factories:new Map()};
156
+ }
157
+ }
158
+ export function compileCPUFragment(translated){try{
159
+ const output=translated.output||'gl_FragColor';
160
+ const parsed=new Parser((output==='gl_FragColor'?'float4 gl_FragColor;':'')+'float4 gl_FragCoord;bool gl_FrontFacing;'+translated.code).parse();
161
+ return new Compiler(parsed,translated.varyings,translated.uniforms).compile(translated.init,[output]);
162
+ }catch{return null;}}
163
+
164
+ export function compileCPUVertex(translated){try{
165
+ const parsed=new Parser('float4 gl_Position;'+translated.code).parse();
166
+ const cpu=new Compiler(parsed,translated.attributes,translated.uniforms).compile(translated.init,['gl_Position',...translated.varyings.map(v=>v.name)]);
167
+ return cpu.samplers.length||cpu.derivatives.length?null:cpu;
168
+ }catch{return null;}}
169
+ function bindShader(compiled,cpu,values,inputs,delta,captured,result,textures,texel,specialize=true){
170
+ const uniforms=new Float64Array(cpu.uniformSlots.length);
171
+ for(let i=0;i<uniforms.length;i++){const [name,index]=cpu.uniformSlots[i],spec=compiled.uniforms.find(u=>u.key===name),value=values.get(spec?.name??name);uniforms[i]=Math.fround(typeof value==='number'?(index===0?value:0):value?.[index]??0);}
172
+ // Model matrices often change on every draw. Reuse one compiled vertex
173
+ // function instead of compiling another JS function for each moved object.
174
+ if(!specialize)return cpu.make(uniforms,inputs,delta,captured,result,textures,sampleTexture,texel);
175
+ const number=n=>Object.is(n,-0)?'-0':String(n),key=Array.from(uniforms,number).join(',');let factory=cpu.factories.get(key);
176
+ if(!factory){const literal=cpu.literalCode.replace(/uniforms\[(\d+)\]/g,(_,i)=>number(uniforms[Number(i)]));factory=new NumericFunction('uniforms','varyings','delta','captured','result','textures','sampleTexture','texel',literal);cpu.factories.set(key,factory);if(cpu.factories.size>16)cpu.factories.delete(cpu.factories.keys().next().value);}
177
+ return factory(uniforms,inputs,delta,captured,result,textures,sampleTexture,texel);
178
+ }
179
+ export function runCPUVertices(compiled,values,attributes,count){
180
+ const inputs=compiled.attributes.map(a=>{const padded=new Float32Array(count*4),source=attributes.get(a.name);for(let i=0;i<count;i++){padded[i*4+3]=1;if(source)for(let c=0;c<a.size;c++)padded[i*4+c]=source[i*a.size+c];}return padded;});
181
+ const result=new Float64Array(compiled.outputCount),out=new Float32Array(count*compiled.outputCount);
182
+ const run=bindShader(compiled,compiled.cpuVertex,values,inputs,null,null,result,[],null,false);
183
+ for(let i=0;i<count;i++){run(i*4,0,0,0,true,false);out.set(result,i*compiled.outputCount);}
184
+ return out;
185
+ }
186
+
187
+ function sampleTexture(t,x,y,out){
188
+ if(!t?.pixels){out[0]=out[1]=out[2]=0;out[3]=1;return;}
189
+ const wrap=(n,size,mode)=>mode===10497?((n%size)+size)%size:mode===33648?(()=>{const v=((n%(2*size))+2*size)%(2*size);return v<size?v:2*size-v-1;})():Math.max(0,Math.min(size-1,n));
190
+ const offset=(x,y)=>(wrap(y,t.height,t.wrapT)*t.width+wrap(x,t.width,t.wrapS))*4;
191
+ if(t.mag===9728){const p=offset(Math.floor(x),Math.floor(y));for(let c=0;c<4;c++)out[c]=t.pixels[p+c]/255;return;}
192
+ x-=.5;y-=.5;const ix=Math.floor(x),iy=Math.floor(y),dx=x-ix,dy=y-iy;
193
+ const a=offset(ix,iy),b=offset(ix+1,iy),c=offset(ix,iy+1),d=offset(ix+1,iy+1);
194
+ for(let k=0;k<4;k++)out[k]=((t.pixels[a+k]*(1-dx)+t.pixels[b+k]*dx)*(1-dy)+(t.pixels[c+k]*(1-dx)+t.pixels[d+k]*dx)*dy)/255;
195
+ }
196
+ export function runCPUFragment(compiled,values,samplers,coverage,varyings,width,height,left,top,canvasHeight){
197
+ const cpu=compiled.cpu;if(!cpu)return null;
198
+ const textures=cpu.samplers.map(name=>samplers.find(s=>s.key===name));
199
+ const delta=new Float64Array(cpu.derivatives.length*4),captured=new Float64Array(delta.length),result=new Float64Array(4),texel=new Float64Array(4),out=new Uint8Array(width*height*4);
200
+ const shade=bindShader(compiled,cpu,values,varyings,delta,captured,result,textures,texel),samples=new Float64Array(delta.length*4);
201
+ function sample(x,y,collect){x=Math.max(0,Math.min(width-1,x));y=Math.max(0,Math.min(height-1,y));const i=(y*width+x)*4;shade(i,left+x+.5,canvasHeight-top-y-.5,coverage[i+2],coverage[i]>0,collect);}
202
+ for(let qy=0;qy<height;qy+=2)for(let qx=0;qx<width;qx+=2){
203
+ let covered=false;for(let lane=0;lane<4;lane++){const x=qx+(lane&1),y=qy+(lane>>1);if(x<width&&y<height&&coverage[(y*width+x)*4+3])covered=true;}if(!covered)continue;
204
+ if(cpu.derivatives.length){delta.fill(0);for(let lane=0;lane<4;lane++){sample(qx+(lane&1),qy+(lane>>1),true);samples.set(captured,lane*delta.length);}}
205
+ for(let lane=0;lane<4;lane++){const x=qx+(lane&1),y=qy+(lane>>1);if(x>=width||y>=height)continue;const i=(y*width+x)*4;if(!coverage[i+3])continue;
206
+ for(let d=0;d<cpu.derivatives.length;d++){const horizontal=cpu.derivatives[d].name==='dFdx',a=horizontal?(lane>>1)*2:lane&1,b=a+(horizontal?1:2);for(let c=0;c<cpu.derivatives[d].size;c++){const k=d*4+c;delta[k]=(samples[b*delta.length+k]-samples[a*delta.length+k])*(horizontal?1:-1);}}
207
+ sample(x,y,false);const alpha=Math.max(0,Math.min(1,result[3]));for(let c=0;c<3;c++)out[i+c]=Math.round(Math.max(0,Math.min(1,result[c]))*alpha*255);out[i+3]=Math.round(alpha*255);
208
+ }
209
+ }
210
+ return out;
211
+ }
@@ -0,0 +1,22 @@
1
+ // Exact draw reuse: geometry and shading are independent of other objects'
2
+ // depth. Keep a geometric depth map with the image; reapply live depth on hits.
3
+ export class DrawCache {
4
+ constructor(){this.entries=new Map();this.bytes=0;this.ids=new WeakMap();this.next=0;}
5
+ id(object){let id=this.ids.get(object);if(!id){id=++this.next;this.ids.set(object,id);}return id;}
6
+ key(program,samplers,settings,size,inputs,indices){
7
+ const hash=new Bun.CryptoHasher('sha256');
8
+ hash.update(JSON.stringify([this.id(program.vertex),this.id(program.fragment),size,
9
+ settings.vp,[...settings.enabled].sort(),settings.depthFunc,settings.writeDepth,settings.front,settings.cull,
10
+ [...program.values],samplers.map(s=>[s.version,s.width,s.height,s.mag,s.min,s.wrapS,s.wrapT])]));
11
+ for(const [name,data]of inputs){hash.update(name);hash.update(new Uint8Array(data.buffer,data.byteOffset,data.byteLength));}
12
+ hash.update(new Uint32Array(indices));return hash.digest('hex');
13
+ }
14
+ get(key){const entry=this.entries.get(key);if(entry){this.entries.delete(key);this.entries.set(key,entry);}return entry;}
15
+ put(key,pixels,left,top,width,height,depth){
16
+ const bytes=pixels.byteLength+(depth?.byteLength??0);if(!key||bytes>32*1024*1024)return false;
17
+ const previous=this.entries.get(key);if(previous){this.bytes-=previous.bytes;this.entries.delete(key);}
18
+ while(this.bytes+bytes>32*1024*1024||this.entries.size>=64){const first=this.entries.keys().next().value,old=this.entries.get(first);this.bytes-=old.bytes;this.entries.delete(first);}
19
+ this.entries.set(key,{pixels,left,top,width,height,depth,bytes});this.bytes+=bytes;return true;
20
+ }
21
+ clear(){this.entries.clear();this.bytes=0;}
22
+ }
@@ -0,0 +1,70 @@
1
+ import {compileCPUFragment,compileCPUVertex,runCPUVertices} from './cpu-fragment.js';
2
+ import {translate,components} from './shaders.js';
3
+ export function compileStage(kit,source,stage) {
4
+ const t=translate(source,stage),decl=[],init=[],outputs=[];
5
+ const cpu=stage==='fragment'?compileCPUFragment(t):null,cpuVertex=stage==='vertex'?compileCPUVertex(t):null;
6
+ if(stage==='vertex'){
7
+ decl.push('float4 gl_Position;');
8
+ for(const a of t.attributes){decl.push(`uniform shader _sw_a_${a.name};`);init.push(`${a.name}=${a.type==='float'?'float':'float'+a.size}(_sw_a_${a.name}.eval(float2(_sw_coord.x,0.5))${a.size<4?'.'+'xyzw'.slice(0,a.size):''});`);}
9
+ outputs.push(...['x','y','z','w'].map(c=>'gl_Position.'+c));
10
+ for(const v of t.varyings)outputs.push(...Array.from({length:v.size},(_,i)=>v.size===1?v.name:v.name+'.'+'xyzw'[i]));
11
+ const groups=[];for(let i=0;i<outputs.length;i+=3)groups.push(`if(int(_sw_coord.y)==${i/3})return half4(${[...outputs.slice(i,i+3),...Array(Math.max(0,3-(outputs.length-i))).fill('0')].join(',')},1);`);
12
+ t.code=decl.join('\n')+'\n'+t.code+`\nhalf4 main(float2 _sw_coord){${t.init}${init.join('')}_sw_run();${groups.join('')}return half4(0);}`;
13
+ t.rows=Math.ceil(outputs.length/3);t.outputCount=outputs.length;
14
+ }else{
15
+ const output=t.output||'gl_FragColor';decl.push(`float4 gl_FragCoord; bool gl_FrontFacing;${output==='gl_FragColor'?'float4 gl_FragColor;':''}uniform shader _sw_coverage;`);
16
+ for(const v of t.varyings){decl.push(`uniform shader _sw_v_${v.name};`);init.push(`${v.name}=${v.type==='float'?'float':'float'+v.size}(_sw_v_${v.name}.eval(p)${v.size<4?'.'+'xyzw'.slice(0,v.size):''});`);}
17
+ const derivatives=[];
18
+ // Probe only the prefix that computes derivatives when every derivative is
19
+ // in main's top-level statements. Re-running lighting/tone mapping four
20
+ // times cannot affect those inputs. Other control flow keeps the full path.
21
+ const main=t.code.indexOf('void _sw_run('),body=main<0?-1:t.code.indexOf('{',main);
22
+ const positions=[...t.code.matchAll(/\b(?:dFdx|dFdy)\s*\(/g)].map(m=>m.index);
23
+ let cutoff=-1;
24
+ if(body>=0&&positions.length){let depth=0,valid=true,last=positions.at(-1);
25
+ for(let i=body;i<=last;i++){if(t.code[i]==='{')depth++;if(t.code[i]==='}')depth--;if(positions.includes(i)&&depth!==1)valid=false;}
26
+ if(positions[0]<body)valid=false;
27
+ if(valid)cutoff=t.code.indexOf(';',last);
28
+ }
29
+ let probe=cutoff>=0?t.code.slice(main,cutoff+1).replace('void _sw_run','void _sw_probe')+'}':'';
30
+ t.code=t.code.replace(/\b(dFdx|dFdy)\s*\(/g,(_,op)=>{const name='_sw_derivative'+derivatives.length;derivatives.push({name,op});return name+'(';});
31
+ if(probe){let i=0;probe=probe.replace(/\b(?:dFdx|dFdy)\s*\(/g,()=>derivatives[i++].name+'(');t.code+='\n'+probe;}
32
+ const helpers=derivatives.map(({name})=>`float4 ${name}_value; float4 ${name}_delta;`+Array.from({length:4},(_,i)=>{
33
+ const n=i+1,type=n===1?'float':'float'+n,tail=n<4?','+Array(4-n).fill('0').join(','):'';
34
+ return `${type} ${name}(${type} v){${name}_value=float4(v${tail});return ${name}_delta.${'xyzw'.slice(0,n)};}`;
35
+ }).join('')).join('\n');
36
+ const load=`gl_FragCoord=float4(p.x,hit.y,hit.z,1);gl_FrontFacing=hit.x>0;${t.init}${init.join('')}_sw_run();`;
37
+ const samples=['float2 q=floor(p/2)*2+0.5;',...derivatives.map(d=>`${d.name}_delta=float4(0);`)];
38
+ if(derivatives.length){
39
+ for(let i=0;i<4;i++){
40
+ samples.push(`${probe?'_sw_probe_sample':'_sw_sample'}(q+float2(${i%2},${i>>1}));`);
41
+ for(const d of derivatives)samples.push(`float4 ${d.name}_${i}=${d.name}_value;`);
42
+ }
43
+ for(const d of derivatives)samples.push(`${d.name}_delta=${d.op==='dFdx'?`(mod(floor(p.y),2)==0?${d.name}_1-${d.name}_0:${d.name}_3-${d.name}_2)`:`-(mod(floor(p.x),2)==0?${d.name}_2-${d.name}_0:${d.name}_3-${d.name}_1)`};`);
44
+ }
45
+ const probeLoad=probe?`void _sw_probe_sample(float2 p){float4 hit=_sw_coverage.eval(p);${load.replace('_sw_run();','_sw_probe();')}}`:'';
46
+ t.code=decl.join('\n')+'\n'+helpers+'\n'+t.code+'\n'+probeLoad+`\nvoid _sw_sample(float2 p){float4 hit=_sw_coverage.eval(p);${load}}\nhalf4 main(float2 p){if(_sw_coverage.eval(p).a==0)return half4(0);${samples.join('')}_sw_sample(p);return half4(${output}.rgb*${output}.a,${output}.a);}`;
47
+
48
+ }
49
+ t.code=t.code.replace(/\bgl_(\w+)/g,'_sw_gl_$1');
50
+ let log='';const effect=kit.RuntimeEffect.Make(t.code,s=>log+=s);if(!effect)throw Object.assign(Error(log),{shader:t.code});return {...t,effect,cpu,cpuVertex};
51
+ }
52
+ export function shader(kit,compiled,values,children){
53
+ const e=compiled.effect,data=new Float32Array(e.getUniformFloatCount());
54
+ for(let i=0;i<e.getUniformCount();i++){const name=e.getUniformName(i),info=e.getUniform(i),u=compiled.uniforms.find(u=>u.key===name);let val=values.get(u?.name??name);if(val===undefined)continue;val=typeof val==='number'?[val]:val;data.set(Array.from(val).slice(0,info.columns*info.rows),info.slot);}
55
+ return e.makeShaderWithChildren(data,children);
56
+ }
57
+ export function floatImage(kit,width,height,data){return kit.MakeImage({width,height,colorType:kit.ColorType.RGBA_F32,alphaType:kit.AlphaType.Premul,colorSpace:kit.ColorSpace.SRGB},new Uint8Array(data.buffer,data.byteOffset,data.byteLength),width*16);}
58
+ export function imageShader(kit,img,matrix){return img.makeShaderOptions(kit.TileMode.Clamp,kit.TileMode.Clamp,kit.FilterMode.Nearest,kit.MipmapMode.None,matrix);}
59
+ export function runVertices(kit,compiled,values,attrs,count){
60
+ if(compiled.cpuVertex)return runCPUVertices(compiled,values,attrs,count);
61
+ const images=[],children=[];let surface,mem,paint,sh;
62
+ try{
63
+ for(const a of compiled.attributes){const data=new Float32Array(count*4),input=attrs.get(a.name);for(let i=0;i<count;i++){data[i*4+3]=1;if(input)for(let c=0;c<a.size;c++)data[i*4+c]=input[i*a.size+c]??0;}const img=floatImage(kit,count,1,data);images.push(img);children.push(imageShader(kit,img));}
64
+ mem=kit.Malloc(Float32Array,count*compiled.rows*4);surface=kit.MakeRasterDirectSurface({width:count,height:compiled.rows,colorType:kit.ColorType.RGBA_F32,alphaType:kit.AlphaType.Premul,colorSpace:kit.ColorSpace.SRGB},mem,count*16);
65
+ if(!surface)throw Error('Cannot allocate vertex surface');
66
+ sh=shader(kit,compiled,values,children);paint=new kit.Paint();paint.setShader(sh);paint.setBlendMode(kit.BlendMode.Src);surface.getCanvas().drawPaint(paint);surface.flush();
67
+ const raw=mem.toTypedArray(),out=new Float32Array(count*compiled.outputCount);
68
+ for(let i=0;i<count;i++)for(let c=0;c<compiled.outputCount;c++)out[i*compiled.outputCount+c]=raw[(Math.floor(c/3)*count+i)*4+c%3];return out;
69
+ }finally{paint?.delete();sh?.delete();surface?.delete();if(mem)kit.Free(mem);for(const c of children)c.delete();for(const i of images)i.delete();}
70
+ }
@@ -0,0 +1,58 @@
1
+ // CPU WebGL shaders use the browser's existing SkSL compiler. No host eval,
2
+ // native GL, or library-specific shader replacements are used here.
3
+ export function preprocess(source) {
4
+ const macros=new Map(), stack=[]; let active=true;
5
+ const expand=(text,depth=0)=>{
6
+ if(depth>30)throw Error('Shader macro recursion limit');
7
+ for(const [name,m] of macros)if(m.args){
8
+ const re=new RegExp('\\b'+name+'\\s*\\(','g');let hit;
9
+ while((hit=re.exec(text))){let end=re.lastIndex,level=1,start=end,args=[];for(;end<text.length&&level;end++){const c=text[end];if(c==='(')level++;else if(c===')'){if(--level===0)args.push(text.slice(start,end));}else if(c===','&&level===1){args.push(text.slice(start,end));start=end+1;}}if(level)break;
10
+ const body=m.value.replace(/\b\w+\b/g,k=>m.args.includes(k)?args[m.args.indexOf(k)]:k);
11
+ const value=expand(body,depth+1);text=text.slice(0,hit.index)+value+text.slice(end);re.lastIndex=hit.index+value.length;
12
+ }
13
+ }
14
+ return text.replace(/\b[A-Za-z_]\w*\b/g,name=>{
15
+ const m=macros.get(name);return m && !m.args ? expand(m.value,depth+1) : name;
16
+ });
17
+ };
18
+ const condition=text=>{
19
+ text=text.replace(/defined\s*(?:\(\s*(\w+)\s*\)|(\w+))/g,(_,a,b)=>macros.has(a||b)?'1':'0');
20
+ text=expand(text).replace(/\b[A-Za-z_]\w*\b/g,'0');
21
+ // A small expression parser, rather than executing shader text as JS.
22
+ const tokens=text.match(/\d+(?:\.\d+)?|&&|\|\||==|!=|<=|>=|[()!~+*/%<>&|^-]/g)||[];let at=0;
23
+ const prec={'||':1,'&&':2,'|':3,'^':4,'&':5,'==':6,'!=':6,'<':7,'>':7,'<=':7,'>=':7,'+':8,'-':8,'*':9,'/':9,'%':9};
24
+ function expr(min=0){let t=tokens[at++],a;if(t==='('){a=expr();if(tokens[at++]!==')')throw Error('Invalid shader condition');}else if(['!','~','-','+'].includes(t)){const v=expr(10);a=t==='!'?!v:t==='~'?~v:t==='-'?-v:v;}else a=Number(t)||0;
25
+ while(at<tokens.length && (prec[tokens[at]]??-1)>=min){const op=tokens[at++],b=expr(prec[op]+1);a=op==='||'?a||b:op==='&&'?a&&b:op==='=='?a===b:op==='!='?a!==b:op==='<'?a<b:op==='>'?a>b:op==='<='?a<=b:op==='>='?a>=b:op==='+'?a+b:op==='-'?a-b:op==='*'?a*b:op==='/'?a/b:op==='%'?a%b:op==='&'?a&b:op==='|'?a|b:a^b;}return a;}
26
+ return !!expr();
27
+ };
28
+ const lines=String(source).replace(/\\\r?\n/g,'').replace(/\/\*[\s\S]*?\*\//g,' ').replace(/\/\/[^\n]*/g,'').split('\n'),out=[];
29
+ for(const line of lines){const d=line.match(/^\s*#\s*(\w+)\s*(.*)$/);if(!d){if(active)out.push(expand(line));continue;}const [,op,rest]=d;
30
+ if(['if','ifdef','ifndef'].includes(op)){const yes=op==='if'?condition(rest):op==='ifdef'?macros.has(rest.trim()):!macros.has(rest.trim());stack.push({parent:active,taken:yes});active=active&&yes;}
31
+ else if(op==='else'||op==='elif'){const top=stack.at(-1);if(!top)throw Error('Unmatched shader conditional');const yes=!top.taken&&(op==='else'||condition(rest));top.taken ||= yes;active=top.parent&&yes;}
32
+ else if(op==='endif'){const top=stack.pop();if(!top)throw Error('Unmatched shader endif');active=top.parent;}
33
+ else if(active&&op==='define'){const m=rest.match(/^(\w+)(\([^)]*\))?\s*(.*)$/);if(m)macros.set(m[1],{args:m[2]?.slice(1,-1).split(',').map(s=>s.trim()),value:m[3]||'1'});}
34
+ else if(active&&op==='undef')macros.delete(rest.trim());
35
+ else if(active&&op==='error')throw Error(rest);
36
+ }
37
+ if(stack.length)throw Error('Unclosed shader conditional');return out.join('\n');
38
+ }
39
+ const typeMap={vec2:'float2',vec3:'float3',vec4:'float4',ivec2:'int2',ivec3:'int3',ivec4:'int4',bvec2:'bool2',bvec3:'bool3',bvec4:'bool4',mat2:'float2x2',mat3:'float3x3',mat4:'float4x4'};
40
+ export const components=type=>({float:1,int:1,bool:1,vec2:2,vec3:3,vec4:4,mat2:4,mat3:9,mat4:16}[type]||1);
41
+ export const glType=type=>({float:0x1406,int:0x1404,bool:0x8B56,vec2:0x8B50,vec3:0x8B51,vec4:0x8B52,mat2:0x8B5A,mat3:0x8B5B,mat4:0x8B5C,sampler2D:0x8B5E}[type]||0);
42
+ export function translate(source,stage) {
43
+ let code=preprocess(source).replace(/\bprecision\s+\w+\s+\w+\s*;/g,'').replace(/\b(?:highp|mediump|lowp)\s+/g,'');
44
+ const structs=new Map();for(const m of code.matchAll(/struct\s+(\w+)\s*\{([^{}]*)\}\s*;/g))structs.set(m[1],fields(m[2]));
45
+ const uniforms=[],attributes=[],varyings=[],assign=[];let output=null;
46
+ function flatten(type,name,path,size=1){if(structs.has(type)){for(let i=0;i<size;i++)for(const f of structs.get(type))flatten(f.type,name+'_'+i+'_'+f.name,path+(size>1?'['+i+']':'')+'.'+f.name,f.size);return;}uniforms.push({name:path,key:name,type,size});assign.push(`${path}=${name};`);}
47
+ code=code.replace(/\buniform\s+(\w+)\s+(\w+)\s*(?:\[\s*(\d+)\s*\])?\s*;/g,(_,type,name,n)=>{const size=Number(n)||1;if(structs.has(type)){flatten(type,'_sw_u_'+name,name,size);return `${type} ${name}${n?'['+n+']':''};`;}uniforms.push({name,key:name,type,size});return '';});
48
+ code=code.replace(/layout\s*\([^)]*\)\s*/g,'');
49
+ code=code.replace(/\b(attribute|varying|in|out)\s+(\w+)\s+(\w+)\s*;/g,(_,kind,type,name)=>{if(kind==='out'&&stage==='fragment'){output=name;return `${type} ${name};`;}const list=kind==='attribute'||(kind==='in'&&stage==='vertex')?attributes:varyings;list.push({name,type,size:components(type)});return `${type} ${name};`;});
50
+ code=code.replace(/\bvoid\s+main\s*\(\s*(?:void)?\s*\)/,'void _sw_run()');
51
+ code=code.replace(/\b(?:[ib]?vec[234]|mat[234])\b/g,t=>typeMap[t]);
52
+ for(const u of uniforms)if(u.type==='bool'){u.key='_sw_bool_'+u.key;code='bool '+u.name+';\n'+code;assign.push(`${u.name}=bool(${u.key});`);}
53
+ const samplers=uniforms.filter(u=>u.type==='sampler2D');
54
+ for(const u of samplers)code=code.replace(new RegExp('\\btexture2D\\s*\\(\\s*'+u.key+'\\s*,','g'),'_sw_texture_'+u.key+'(');
55
+ const declarations=uniforms.map(u=>u.type==='sampler2D'?`uniform shader ${u.key}; uniform float2 _sw_size_${u.key}; float4 _sw_texture_${u.key}(float2 uv){float4 c=float4(${u.key}.eval(uv*_sw_size_${u.key}));return float4(c.a>0?c.rgb/c.a:float3(0),c.a);} float4 _sw_texture_${u.key}(float2 uv,float bias){return _sw_texture_${u.key}(uv);}`:`uniform ${u.type==='bool'?'int':typeMap[u.type]||u.type} ${u.key}${u.size>1?'['+u.size+']':''};`).join('\n');
56
+ return {output,code:declarations+'\n'+code,uniforms,attributes,varyings,init:assign.join('\n')};
57
+ }
58
+ function fields(body){return [...body.matchAll(/(\w+)\s+(\w+)\s*(?:\[\s*(\d+)\s*\])?\s*;/g)].map(m=>({type:m[1],name:m[2],size:Number(m[3])||1}));}
@@ -0,0 +1,43 @@
1
+ // CPU texture storage. Images are copied at upload, not sampled from mutable DOM.
2
+ export function textureState(kit,resolveImage,flush,fail){
3
+ const textures=new WeakMap(),owned=new Set(),units=new Map(),retired=[];let unit=0,flip=false,alignment=4,version=0;
4
+ const current=()=>textures.get(units.get(unit));
5
+ function upload(...args){
6
+ const [target,level,internal]=args;if(target!==3553||level!==0)return;
7
+ const tex=current();if(!tex){fail(1282);return;}
8
+ let width,height,bytes,format,type;
9
+ if(args.length===6){const src=resolveImage()?.(args[5]);format=args[3];type=args[4];
10
+ if(!src?.image){fail(1281);return;}if(src.originClean===false)throw new DOMException('Cross-origin texture','SecurityError');
11
+ width=src.width;height=src.height;bytes=src.image.readPixels(0,0,{width,height,colorType:kit.ColorType.RGBA_8888,alphaType:kit.AlphaType.Unpremul,colorSpace:kit.ColorSpace.SRGB});
12
+ }else{width=args[3];height=args[4];format=args[6];type=args[7];const input=args[8];
13
+ if(!(width>0&&height>0&&width<=4096&&height<=4096)){fail(1281);return;}
14
+ if(type!==5121){fail(1280);return;}const count=format===6408?4:format===6407?3:format===6406?1:0;if(!count){fail(1280);return;}
15
+ bytes=new Uint8Array(width*height*4);const stride=Math.ceil(width*count/alignment)*alignment;
16
+ if(input&&input.byteLength<(height-1)*stride+width*count){fail(1282);return;}
17
+ for(let y=0;y<height;y++)for(let x=0;x<width;x++){const i=(y*width+x)*4,j=y*stride+x*count;
18
+ if(input){if(count===1)bytes[i+3]=input[j];else{bytes[i]=input[j];bytes[i+1]=input[j+1];bytes[i+2]=input[j+2];bytes[i+3]=count===4?input[j+3]:255;}}}
19
+ }
20
+ if(!bytes){fail(1282);return;}
21
+ // CanvasKit readPixels may expose its WASM heap; textures own a stable copy.
22
+ bytes=Uint8Array.from(bytes);
23
+ if(flip){const row=width*4,copy=new Uint8Array(bytes.length);for(let y=0;y<height;y++)copy.set(bytes.subarray(y*row,(y+1)*row),(height-y-1)*row);bytes=copy;}
24
+ if(tex.image)retired.push(tex.image);if(retired.length>32)flush();tex.width=width;tex.height=height;tex.pixels=bytes;tex.version=++version;
25
+ tex.image=kit.MakeImage({width,height,colorType:kit.ColorType.RGBA_8888,alphaType:kit.AlphaType.Unpremul,colorSpace:kit.ColorSpace.SRGB},bytes,width*4);
26
+ }
27
+ return {
28
+ methods:{createTexture(){const obj=Object.create(null),t={width:1,height:1,image:null,wrapS:10497,wrapT:10497,mag:9729,min:9986};textures.set(obj,t);owned.add(t);return obj;},
29
+ activeTexture(value){if(value<33984||value>=33992){fail(1280);return;}unit=value-33984;},
30
+ bindTexture(target,obj){if(target!==3553)return;if(obj&&!textures.has(obj)){fail(1282);return;}units.set(unit,obj);},
31
+ texParameteri(target,key,value){const t=current();if(!t)return;const field={10240:'mag',10241:'min',10242:'wrapS',10243:'wrapT'}[key];if(field)t[field]=value;},
32
+ pixelStorei(key,value){if(key===37440)flip=!!value;else if(key===3317&&[1,2,4,8].includes(value))alignment=value;},
33
+ texImage2D:upload,generateMipmap(){},deleteTexture(obj){const t=textures.get(obj);if(t){flush();t.image?.delete();owned.delete(t);textures.delete(obj);}},
34
+ },
35
+ capture(compiled,values){return compiled.uniforms.filter(u=>u.type==='sampler2D').map(u=>({key:u.key,...textures.get(units.get(Number(values.get(u.name)??0)))}));},
36
+ shaders(captured,values){const out=[];for(const t of captured){values.set('_sw_size_'+t.key,[t.width??1,t.height??1]);
37
+ const tile=n=>n===10497?kit.TileMode.Repeat:n===33648?kit.TileMode.Mirror:kit.TileMode.Clamp;
38
+ out.push(t.image?t.image.makeShaderOptions(tile(t.wrapS),tile(t.wrapT),t.mag===9728?kit.FilterMode.Nearest:kit.FilterMode.Linear,kit.MipmapMode.None):kit.Shader.MakeColor(kit.Color(0,0,0,1),kit.ColorSpace.SRGB));}
39
+ return out;},
40
+ releaseRetired(){for(const img of retired)img.delete();retired.length=0;},
41
+ dispose(){for(const img of retired)img.delete();retired.length=0;for(const t of owned)t.image?.delete();owned.clear();units.clear();},
42
+ };
43
+ }
@@ -0,0 +1,46 @@
1
+ // HTML named access on Window. Keep names on the per-document prototype,
2
+ // below explicit window properties, and resolve the live document at access
3
+ // time so renaming/removing an element never leaves a stale global behind.
4
+ export function windowNamedProperties(document, realm, prototype) {
5
+ const namedTags = new Set(['EMBED', 'FORM', 'IMG', 'OBJECT']);
6
+ const collections = new Map();
7
+ function matches(name) {
8
+ if (!name || typeof name !== 'string') return [];
9
+ // Most missing global reads are not DOM names. Avoid walking the tree for them.
10
+ if (!document.getElementById(name) && !document.getElementsByName(name).length) return [];
11
+ return [...document.querySelectorAll('[id], [name]')].filter(element =>
12
+ element.namespaceURI === 'http://www.w3.org/1999/xhtml' &&
13
+ (element.id === name || (namedTags.has(element.tagName) || element.tagName === 'IFRAME') && element.getAttribute('name') === name));
14
+ }
15
+ function value(name, items) {
16
+ const frame = items.find(e => e.tagName === 'IFRAME' && e.getAttribute('name') === name);
17
+ if (frame) return realm.wrap(frame).contentWindow;
18
+ if (items.length === 1) return realm.wrap(items[0]);
19
+ let collection = collections.get(name);
20
+ if (!collection) {
21
+ collection = realm.live(new Proxy({}, {
22
+ get(_target, key) {
23
+ const current = matches(name);
24
+ if (key === 'length') return current.length;
25
+ if (typeof key === 'string' && /^(0|[1-9]\d*)$/.test(key)) return current[Number(key)];
26
+ },
27
+ }));
28
+ collections.set(name, collection);
29
+ }
30
+ return collection;
31
+ }
32
+ return new Proxy(Object.create(prototype), {
33
+ get(target, name, receiver) {
34
+ if (Reflect.has(target, name)) return Reflect.get(target, name, receiver);
35
+ const items = matches(name);
36
+ return items.length ? value(name, items) : undefined;
37
+ },
38
+ has: (target, name) => Reflect.has(target, name) || matches(name).length > 0,
39
+ getOwnPropertyDescriptor(target, name) {
40
+ const own = Reflect.getOwnPropertyDescriptor(target, name);
41
+ if (own || Reflect.has(target, name)) return own;
42
+ const items = matches(name);
43
+ return items.length ? {value:value(name, items), writable:true, enumerable:true, configurable:true} : undefined;
44
+ },
45
+ });
46
+ }
@@ -1,3 +1,4 @@
1
+ import { parseWindowSize } from "./window-size.js";
1
2
  import { resolve } from "node:path";
2
3
  import { pathToFileURL } from "node:url";
3
4
  import { createBrowser } from "./headless-shell.js";
@@ -21,30 +22,26 @@ export async function runScreenshot(args) {
21
22
  if (!extension) throw new Error("--screenshot filename must end in .png, .jpg, .jpeg, or .webp");
22
23
  const format = extension === "jpg" ? "jpeg" : extension;
23
24
 
24
- const size = args.find((arg) => arg.startsWith("--window-size="))?.slice("--window-size=".length);
25
+ const initialViewport = parseWindowSize(args);
25
26
  const waitArg = args.find((arg) => arg.startsWith("--screenshot-wait="))?.slice("--screenshot-wait=".length);
26
27
  const waitMs = waitArg === undefined ? 0 : Number(waitArg);
27
28
  if (waitArg !== undefined && (!/^\d+$/.test(waitArg) || !Number.isSafeInteger(waitMs))) {
28
29
  throw new Error("--screenshot-wait must be a nonnegative number of milliseconds");
29
30
  }
30
- const match = size?.match(/^(\d+)[,x](\d+)$/i);
31
- if (size && (!match || !Number.isSafeInteger(Number(match[1])) || !Number.isSafeInteger(Number(match[2]))
32
- || Number(match[1]) < 1 || Number(match[2]) < 1)) {
33
- throw new Error("--window-size must be WIDTH,HEIGHT with positive integers");
34
- }
31
+
35
32
 
36
33
  const urlArg = args.find((arg) => !arg.startsWith("-"));
37
34
  if (!urlArg) throw new Error("--screenshot requires a URL");
38
35
  const url = screenshotTargetUrl(urlArg);
39
36
 
40
37
  const browser = await createBrowser({
38
+ initialViewport,
41
39
  allowHostJs: args.includes("--dangerously-allow-host-js"),
42
40
  mobile: args.includes("--mobile"),
43
41
  webkit: webkitFromArgs(args),
44
42
  spareRenderer: false,
45
43
  });
46
44
  try {
47
- if (match) await browser.context.resize(Number(match[1]), Number(match[2]));
48
45
  await browser.context.navigate(url);
49
46
  if (!browser.context.lifecycleState().includes("load")) {
50
47
  await new Promise((done) => {
@@ -0,0 +1,29 @@
1
+ // WebKit's historic animation properties are aliases, not expando properties.
2
+ export function animationProperty(name) {
3
+ name = String(name);
4
+ if(name.startsWith("--"))return name;
5
+ if (/^webkitAnimation(?:[A-Z]|$)/.test(name)) name = name.slice(6);
6
+ name = name.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
7
+ return name.replace(/^-webkit-(animation(?:-|$))/, '$1').replace(/^-animation/, 'animation');
8
+ }
9
+
10
+ export function animationLists(value) {
11
+ return String(value).split(/,(?![^()]*\))/).map(value => value.trim());
12
+ }
13
+
14
+ export function animationShorthand(value) {
15
+ return animationLists(value).map(source => {
16
+ const result = {name:'none', duration:'0s', delay:'0s', iterationCount:'1', direction:'normal', fillMode:'none', playState:'running', timingFunction:'ease'};
17
+ let times = 0;
18
+ for (const token of source.match(/(?:[^\s(]+\([^)]*\)|[^\s]+)/g) ?? []) {
19
+ if (/^-?(?:\d*\.)?\d+m?s$/.test(token)) result[times++ ? 'delay' : 'duration'] = token;
20
+ else if (/^(infinite|(?:\d*\.)?\d+)$/.test(token)) result.iterationCount = token;
21
+ else if (/^(normal|reverse|alternate|alternate-reverse)$/.test(token)) result.direction = token;
22
+ else if (/^(forwards|backwards|both|none)$/.test(token)) result.fillMode = token;
23
+ else if (/^(running|paused)$/.test(token)) result.playState = token;
24
+ else if (/^(linear|ease|ease-in|ease-out|ease-in-out|step-start|step-end|cubic-bezier\(|steps\()/.test(token)) result.timingFunction = token;
25
+ else result.name = token.replace(/^(['"])(.*)\1$/, '$2');
26
+ }
27
+ return result;
28
+ });
29
+ }