@displayxr/inline3d 1.47.5 → 1.47.6

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/CHANGELOG.md CHANGED
@@ -7,6 +7,68 @@ which tier they touch, because that is what tells you whether an upgrade can mov
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 1.47.6 — 2026-11-03
11
+
12
+ ### Fixed — a page that destroys PlayCanvas materials no longer keeps their programs (`./splat`, `./model`)
13
+
14
+ Preview tier. When a page destroys a material on a PlayCanvas tile the SDK created, either
15
+ directly or through a glTF container's `asset.unload()`, the SDK now destroys the programs that
16
+ material linked, one frame later, when no mesh instance in the app, no material asset in the
17
+ registry and no still-alive material holds them. A page that follows the engine's own teardown
18
+ (`entity.destroy()` plus destroying or unloading what it drew with) is bounded without calling
19
+ `handle.releaseEntity`. Measured on macOS, 24 swaps of distinct products under one tile: 2 live
20
+ programs (25 created, 23 deleted), against 25 (none deleted) before. A kiosk app with one
21
+ persistent stage had reached 170 live programs over 28 screen changes.
22
+
23
+ Content shown again after its material was destroyed recompiles on its first draw
24
+ (`handle.warmEntity` hides that). A material the page keeps alive keeps its programs.
25
+ `?dxrdiag=keepprograms` turns the release off for an A/B, and the overlay shows
26
+ `release patched (−N)`. It installs only on PlayCanvas 2.22.x's `Material` shape and otherwise
27
+ reports `release skipped: …`. `docs/content-lifecycle-rules.md` rule 1 and §6 are updated.
28
+
29
+ ### Added — `handle.releaseEntity`: swap content in a PlayCanvas tile without leaking its programs (`./splat`, `./model`)
30
+
31
+ Preview tier. PlayCanvas never frees a shader program by itself: its `ProgramLibrary` caches every
32
+ variant for the device's life, and `Material.destroy()` (which `entity.destroy()` and a container's
33
+ `asset.unload()` reach) only empties `material.variants`. A page that swaps glTFs under
34
+ `handle.engine.root` (a carousel, a catalogue) therefore kept every program it ever showed, and a
35
+ desktop driver keeps each linked program's compiled form (about 5 MB per program, measured on one
36
+ Linux driver): a production page's GPU process grew about 8 MB/min for as long as it was open.
37
+
38
+ - `handle.releaseEntity(entity, { asset })` on `addSplat(…, { engine: 'playcanvas' })` and
39
+ PlayCanvas `addModel` handles: collects the programs the entity's materials linked, destroys the
40
+ entity, unloads its glTF container `asset` unless something else in the tile still draws from it,
41
+ then destroys each collected program nothing left uses (`Shader.destroy()`, the engine's own
42
+ release). Returns `{ programs, container }`. A no-op on Spark and three.
43
+ Measured over 24 distinct models: live programs 5 → 47 without it, bounded at 5–16 (the model on
44
+ screen) with it.
45
+
46
+ ### Fixed — the engine kept every compiled shader until the device died (`./splat`, `./model`)
47
+
48
+ Preview tier. PlayCanvas 2.22.3 keeps every compiled vertex and fragment shader in a
49
+ module-private cache keyed by source, and `WebglShader.destroy()` deletes only the program. So
50
+ even with the programs destroyed, two compiled shaders per program stayed (10 → 91 over the same 24
51
+ swaps). The SDK now counts, per cached shader, the programs that use it and deletes it with the last
52
+ one, on every PlayCanvas device it creates (`js/inline3d-pc-shader-cache.js`: a run-time patch of the
53
+ engine's `WebglShader` prototype, since the engine is the page's own dependency; it installs only on
54
+ the 2.22.x shape and otherwise steps aside). With `releaseEntity`, shaders stay at exactly twice the
55
+ live programs. `?dxrdiag=keepshaders` turns it off for an A/B.
56
+
57
+ ### Added — live GL object counts and a leak warning in `?dxrdiag` (`./splat`, `./model`)
58
+
59
+ The diagnostics overlay shows live programs and shaders on the tile's context (created and deleted
60
+ since diag attached) and the shader-cache fix's state; the dump carries them under `glObjects`. At
61
+ each content swap the SDK sees (`setSource`, a glTF container loaded into the tile's registry,
62
+ `releaseEntity`) it samples the live program count, and warns once when it has not dropped across
63
+ 10 swaps of one kind and rose at more than 60% of them. `addModel`'s PlayCanvas tile now honours
64
+ `?dxrdiag` too. Nothing runs when diag is off.
65
+
66
+ ### Docs
67
+
68
+ - New [`docs/content-lifecycle-rules.md`](docs/content-lifecycle-rules.md): what a swap must release,
69
+ PlayCanvas and three.js engine facts, the hook-and-plateau pass test, the SDK's own swap paths
70
+ measured, and what the SDK does vs what the page owns.
71
+
10
72
  ## 1.47.5 — 2026-11-01
11
73
 
12
74
  ### Fixed — the camera permission prime could open a phone lens and crash its camera (`./camera`, `./call`)
package/README.md CHANGED
@@ -166,7 +166,8 @@ No projection math lands in your page or in the SDK — the off-axis frustum sta
166
166
  Full API + authoring guidance: [`docs/authoring-inline-3d.md`](docs/authoring-inline-3d.md).
167
167
  Before you ship a page that navigates or remounts, read
168
168
  [`docs/woven-canvas-rules.md`](docs/woven-canvas-rules.md): how to avoid a raw side-by-side
169
- flash, and releasing a poster on `handle.firstWoven`.
169
+ flash, and releasing a poster on `handle.firstWoven`. A page that swaps content in a window (a
170
+ carousel, a catalogue): [`docs/content-lifecycle-rules.md`](docs/content-lifecycle-rules.md).
170
171
  Three.js glue (an off-axis `EyeCamera`) in [`js/inline3d-three.js`](js/inline3d-three.js).
171
172
 
172
173
  ## What's here
@@ -198,6 +199,8 @@ js/
198
199
  inline3d-model-entry.js experimental: addModel() — picks the engine (PlayCanvas default)
199
200
  inline3d-model-playcanvas.js the PlayCanvas model backend (Sample-Viewer lighting, engine decoders)
200
201
  inline3d-pc-look.js PlayCanvas look helpers for both adapters (environments, transmission)
202
+ inline3d-pc-release.js handle.releaseEntity: content under engine.root out, its programs + container released
203
+ inline3d-pc-shader-cache.js engine fix: a compiled shader is deleted with the last program using it
201
204
  inline3d-model.js the three.js model backend (engine:'three'; ./model/three)
202
205
  inline3d-playcanvas-engine.js named playcanvas imports for both PlayCanvas adapters (tree-shaking)
203
206
  inline3d-splat-effects.js splat shader effects: the runner, the effect registry (PlayCanvas)
@@ -215,6 +218,9 @@ docs/
215
218
  the WebXR→inline-3d mapping table and the whole render loop
216
219
  woven-canvas-rules.md never show a raw side-by-side frame: the join, the rules,
217
220
  handle.firstWoven / rewoven(), reading the browser's `withheld` log line
221
+ content-lifecycle-rules.md
222
+ release what a content swap replaces (programs included):
223
+ handle.releaseEntity, engine facts, the hook-and-plateau pass test
218
224
  camera.md The 3D camera primitive (/camera): openCamera, the mirrored self view,
219
225
  capturePhoto / record with the stereo record inside the file.
220
226
  call.md 3D video calls: the <dxr-call> / mountCall() one-line embed, options,
package/dist/call.js CHANGED
@@ -1,4 +1,4 @@
1
- /* @displayxr/inline3d 1.47.5 — dist/call.js — Apache-2.0 — https://github.com/DisplayXR/displayxr-web */
1
+ /* @displayxr/inline3d 1.47.6 — dist/call.js — Apache-2.0 — https://github.com/DisplayXR/displayxr-web */
2
2
  var Ds=Object.defineProperty;var le=(r,e)=>()=>(r&&(e=r(r=0)),e);var Ps=(r,e)=>{for(var t in e)Ds(r,t,{get:e[t],enumerable:!0})};function yr(r,e,t=1){let i=Ke,n=!!r&&r.displayWidthMeters>0&&r.displayHeightMeters>0&&r.displayPixelWidth>0&&r.displayPixelHeight>0,s=n?[r.displayWidthMeters,r.displayHeightMeters]:[...i.displaySizeM],o=n?[r.displayPixelWidth,r.displayPixelHeight]:[...i.displayPixels],a=(s[0]/o[0]+s[1]/o[1])/2,l=t>0?t:1,c=n?a*l:a*(o[1]/Math.max(1,globalThis.innerHeight||o[1])),d=r?.nominalViewerPosition?.z,f=Number.isFinite(d)&&d>.1;return{canvasSizeM:[(e?.width||0)*c,(e?.height||0)*c],metersPerCssPx:c,displaySizeM:s,nominalViewerM:f?d:i.nominalViewerM,eyeSeparationM:i.eyeSeparationM,source:{size:n?"display":"default",viewer:f?"display":"default",eyeSeparation:"default"}}}var Ke,Bt=le(()=>{"use strict";Ke=Object.freeze({displaySizeM:Object.freeze([.3456,.1944]),displayPixels:Object.freeze([1920,1080]),nominalViewerM:.65,eyeSeparationM:.063})});function ut(r,e,t,i,n,s,o,a,l,c,d){if(s<0||s+a>t||o<0)return-1;let f=0,h=0,u=0,m=a*l;for(let w=0;w<l;w++){let g=(n+w)*e+i,x=(o+w)*e+t+s;for(let _=0;_<a;_++){let M=r[x+_];f+=M,h+=M*M,u+=r[g+_]*M}}let p=f/m,b=h/m-p*p;return b<1?-1:(u/m-c*p)/(d*Math.sqrt(b))}function Oo(r,e,t,i,n,s){let o=0,a=0;for(let d=0;d<s;d++){let f=(i+d)*e+t;for(let h=0;h<n;h++){let u=r[f+h];o+=u,a+=u*u}}let l=n*s,c=o/l;return{mean:c,std:Math.sqrt(Math.max(0,a/l-c*c))}}function ge(r,e,t,i,n,s,o,{dMin:a,dMax:l,dyMax:c=2,uniq:d}={}){let f=e/2,{mean:h,std:u}=Oo(r,e,i,n,s,o);if(u<6)return null;let m=-2,p=0,b=0,w=new Map,g=(T,v,S,y,R)=>{for(let I=y;I<=R;I++){let O=n+I;if(!(O<0||O+o>t))for(let N=T;N<=v;N+=S){let $=ut(r,e,f,i,n,i-N,O,s,o,h,u);$>(w.get(N)??-2)&&w.set(N,$),$>m&&(m=$,p=N,b=I)}}},x=l-a>8;if(g(a,l,x?2:1,-c,c),x&&m>-1){let T=p,v=b;g(Math.max(a,T-2),Math.min(l,T+2),1,Math.max(-c,v-1),Math.min(c,v+1))}if(m<-1)return null;let _=-2,M=T=>{let v=-2;for(let S=-c;S<=c;S++){let y=n+S;if(!(y<0||y+o>t))for(let R=T-1;R<=T+1;R++)v=Math.max(v,ut(r,e,f,i,n,i-R,y,s,o,h,u))}return v};for(let[T,v]of w){if(Math.abs(T-p)<3||v<=_)continue;let y=v>=(w.get(T-2)??-2)&&v>=(w.get(T+2)??-2)&&v>0?M(T):v;y>_&&(_=y)}if(l-a>12&&_>-2&&m-_<(d??.08))return null;let E=ut(r,e,f,i,n,i-(p-1),n+b,s,o,h,u),C=ut(r,e,f,i,n,i-(p+1),n+b,s,o,h,u),P=0,D=E-2*m+C;return E>-1&&C>-1&&D<0&&(P=Math.max(-.5,Math.min(.5,.5*(E-C)/D))),{d:p+P,dy:b,c:m}}function Do(r,e,t,i={}){let n=e/2,s=i.block||Math.max(8,Math.round(n/20)),o=i.step||s,a=i.dMax??Math.round(n*.3),l=i.dMin??-Math.round(n*.05),c=Math.round(n*(i.roiX0??.15)),d=Math.round(n*(i.roiX1??.85))-s,f=Math.round(t*(i.roiY0??.05)),h=Math.round(t*(i.roiY1??.85))-s,u=[];for(let m=f;m<=h;m+=o)for(let p=c;p<=d;p+=o){let b=ge(r,e,t,p,m,s,s,{dMin:l,dMax:a,dyMax:i.dyMax??2,uniq:i.uniq});b&&b.c>=(i.minNcc??.8)&&u.push({x:p,y:m,...b})}return u}function Po(r,{tol:e=2,relTol:t=.1,minFrac:i=.12,minBlocks:n=3}={}){if(!r.length)return null;let s=Math.max(n,Math.ceil(r.length*i)),o=r.map(a=>a.d).sort((a,l)=>l-a);for(let a of o){let l=Math.max(e,t*Math.abs(a)),c=r.filter(d=>Math.abs(d.d-a)<=l);if(c.length>=s){let d=c.map(f=>f.d).sort((f,h)=>f-h);return{d:d[d.length>>1],members:c}}}return null}function Ao(r,e){let t=Math.min(...r.map(l=>l.y)),n=r.filter(l=>l.y<=t+2*e).map(l=>l.x),s=Math.min(...n),o=Math.max(...n)+e,a=o-s;return{x:(s+o)/2,y:t+.4*1.3*a}}function Mi(r,e,t,i={}){let n=e/2,s=i.block||Math.max(8,Math.round(n/20)),o=i.dMax??Math.round(n*.3),a=i.dMin??-Math.round(n*.05),l=i.focus||null,c="focus",d=0,f=null;if(!l){let x=Do(r,e,t,{...i,block:s,dMax:o,dMin:a});d=x.length;let _=Po(x,i);if(!_)return null;l=Ao(_.members,s),f=_.d,c="mode"}let h=Math.round(s*(i.refineW??3)),u=Math.round(s*(i.refineH??1.5)),m=Math.round(Math.max(0,Math.min(n-h,l.x-h/2))),p=Math.round(Math.max(0,Math.min(t-u,l.y-u/2))),b=f===null?a:Math.max(a,Math.floor(f)-4),w=f===null?o:Math.min(o,Math.ceil(f)+4),g=ge(r,e,t,m,p,h,u,{dMin:b,dMax:w,dyMax:i.dyMax??2});return g&&g.c>=(i.minNcc??.8)?{d:g.d,x:l.x,y:l.y,c:g.c,method:c,blocks:d}:f!==null?{d:f,x:l.x,y:l.y,c:0,method:c,blocks:d}:null}function dt(r,e){let t=new Uint8Array(e);for(let i=0,n=0;i<e;i++,n+=4)t[i]=r[n]*77+r[n+1]*150+r[n+2]*29>>8;return t}function ft({n:r=3}={}){let e=[],t={value:null,push(i){if(i===null||!Number.isFinite(i))return t.value;e.push(i),e.length>r&&e.shift();let n=[...e].sort((s,o)=>s-o);return t.value=n[n.length>>1],t.value},reset(){e.length=0,t.value=null}};return t}function rn(r,e,t,i,n,s){let o=new Float32Array(n*s);for(let a=0;a<s;a++)for(let l=0;l<n;l++)o[a*n+l]=r[(i+a)*e+t+l];return o}function Lo(r,e,t,i,n,s,o,a,l,c){let d=e/2,f=n*s,h=0,u=0;for(let g=0;g<f;g++)h+=i[g],u+=i[g]*i[g];h/=f;let m=Math.sqrt(Math.max(1e-6,u/f-h*h)),p=-2,b=o,w=a;for(let g=Math.max(0,a-c);g<=Math.min(t-s,a+c);g++)for(let x=Math.max(0,o-l);x<=Math.min(d-n,o+l);x++){let _=0,M=0,E=0;for(let T=0;T<s;T++){let v=(g+T)*e+x;for(let S=0;S<n;S++){let y=r[v+S];_+=y,M+=y*y,E+=y*i[T*n+S]}}let C=_/f,P=M/f-C*C;if(P<1)continue;let D=(E/f-C*h)/(Math.sqrt(P)*m);D>p&&(p=D,b=x,w=g)}return{x:b,y:w,c:p}}function $e({fullEveryMs:r=2e3,dWin:e=4,rx:t=12,ry:i=8,minTrackNcc:n=.75,jump:s=8}={}){let o=null,a=null;return{measure(c,d,f,h,u={}){let m=d/2,p=u.block||Math.max(8,Math.round(m/20)),b=Math.round(p*3),w=Math.round(p*1.5),g=null;if(o&&o.w===b&&o.h===w){let C=Lo(c,d,f,o.tpl,b,w,o.x,o.y,t,i);if(C.c>=n){let P=ge(c,d,f,C.x,C.y,b,w,{dMin:Math.floor(o.d)-e,dMax:Math.ceil(o.d)+e,dyMax:2});P&&P.c>=.8&&(g={m:P,f:C})}}let x=({m:C,f:P})=>(o={...o,x:P.x,y:P.y,d:C.d,tpl:rn(c,d,P.x,P.y,b,w)},{d:C.d,x:P.x+b/2,y:P.y+w/2,c:C.c,method:"track",blocks:0});if(g&&h-o.at<r)return x(g);let _=Mi(c,d,f,u);if(g&&(!_||Math.abs(_.d-g.m.d)>s)){let C=_&&a&&Math.abs(a.d-_.d)<=3;if(a=_&&!C?{d:_.d}:null,!C)return o.at=h,x(g)}else a=null;if(!_)return o=null,null;let M=Math.round(Math.max(0,Math.min(m-b,_.x-b/2))),E=Math.round(Math.max(0,Math.min(f-w,_.y-w/2)));return o={x:M,y:E,d:_.d,tpl:rn(c,d,M,E,b,w),w:b,h:w,at:h},_},reset(){o=null,a=null}}}function nn(r,e,t,i,n,s){let o=Math.max(1,s|0),a=Math.floor(i/o);a-=a&1;let l=Math.floor(n/o),c=new Uint8Array(a*l);if(o<=2){let h=o*o;for(let u=0;u<l;u++)for(let m=0;m<a;m++){let p=0;for(let b=0;b<o;b++){let w=e+(u*o+b)*t+m*o;for(let g=0;g<o;g++)p+=r[w+g]}c[u*a+m]=p/h|0}return{img:c,w:a,h:l}}let d=o>>2,f=3*o>>2;for(let h=0;h<l;h++){let u=e+(h*o+d)*t,m=e+(h*o+f)*t;for(let p=0;p<a;p++){let b=p*o+d,w=p*o+f;c[h*a+p]=r[u+b]+r[u+w]+r[m+b]+r[m+w]>>2}}return{img:c,w:a,h:l}}var Ei=le(()=>{"use strict"});function vt(r,e="@displayxr/inline3d"){if(r==null||r===!0)return{..._e};r===!1&&(r="off");let t=()=>new Error(`${e}: cursor.soften ${JSON.stringify(r)} \u2014 expected ${mt.map(s=>`'${s}'`).join(" | ")} or { ${gt.join(", ")} }.`);if(typeof r=="string"){if(!mt.includes(r))throw t();return{..._e,...sn(r)}}if(typeof r!="object"||Array.isArray(r))throw t();let i=Object.keys(r).filter(s=>!gt.includes(s));if(i.length)throw new Error(`${e}: cursor.soften \u2014 unknown option(s) ${i.join(", ")}; expected ${gt.join(", ")}.`);if(r.mode!==void 0&&!mt.includes(r.mode))throw t();let n={..._e,...sn(r.mode??"both")};for(let s of Object.keys(_t)){if(r[s]===void 0)continue;let[o,a,l]=_t[s];if(!Io(s,r[s]))throw new Error(`${e}: cursor.soften.${s} must be a number in ${l?"(":"["}${o}, ${a}].`);n[s]=r[s]}return n}function an(r,e="@displayxr/inline3d"){if(r==null||r===!1)return null;if(r==="depth"||r===!0)return{};if(typeof r!="object")throw new Error(`${e}: cursor ${JSON.stringify(r)} \u2014 expected 'depth' or { ${pt.join(", ")} }.`);let t=Object.keys(r).filter(i=>!pt.includes(i));if(t.length)throw new Error(`${e}: cursor \u2014 unknown option(s) ${t.join(", ")}; expected ${pt.join(", ")}.`);if(r.anchor!==void 0&&!Ri.includes(r.anchor))throw new Error(`${e}: cursor.anchor "${r.anchor}" \u2014 expected ${Ri.join(", ")}.`);if(r.pointerScope!==void 0&&r.pointerScope!=="canvas"&&r.pointerScope!=="window")throw new Error(`${e}: cursor.pointerScope "${r.pointerScope}" \u2014 expected 'canvas' or 'window'.`);if(r.debug!==void 0&&typeof r.debug!="boolean"&&r.debug!=="both")throw new Error(`${e}: cursor.debug ${JSON.stringify(r.debug)} \u2014 expected true, false or 'both'.`);for(let i of["height","margin"])if(r[i]!==void 0&&!(typeof r[i]=="number"&&r[i]>0&&Number.isFinite(r[i])))throw new Error(`${e}: cursor.${i} must be a positive number.`);if(r.soften!==void 0&&vt(r.soften,e),r.shape!==void 0&&!ve.includes(r.shape))throw new Error(`${e}: cursor.shape ${JSON.stringify(r.shape)} \u2014 expected ${ve.map(i=>`'${i}'`).join(" | ")}.`);if(r.shapeFor!==void 0&&r.shapeFor!==null&&typeof r.shapeFor!="function")throw new Error(`${e}: cursor.shapeFor must be a function (hit) => shape.`);return{...r}}function yt(r,e="@displayxr/inline3d"){let t=an(r,e);if(!t)return null;let i=Object.keys(t).filter(n=>!on.includes(n));if(i.length)throw new Error(`${e}: cursor \u2014 ${i.join(", ")} not supported on a stereo pair; expected ${on.join(", ")}.`);return t}var Ri,pt,ye,ve,mt,_e,_t,gt,Io,sn,on,we=le(()=>{"use strict";Ri=Object.freeze(["hybrid","screen","world"]),pt=Object.freeze(["height","margin","anchor","pointerScope","debug","soften","shape","shapeFor"]),ye=Object.freeze(["arrow","pointer","grab","grabbing","move","text","vertical-text","crosshair","not-allowed","help","wait","progress","copy","alias","context-menu","cell","ns-resize","ew-resize","nesw-resize","nwse-resize","col-resize","row-resize","zoom-in","zoom-out"]),ve=Object.freeze(["auto","none",...ye]),mt=Object.freeze(["both","off","alpha","blur"]),_e=Object.freeze({blur:!0,alpha:!0,blurK:.1,blurMaxPx:8,blurMaxFrac:.06,alphaMin:.5,fullD:.03}),_t=Object.freeze({blurK:[0,1e3,!1],blurMaxPx:[0,8,!1],blurMaxFrac:[0,.5,!1],alphaMin:[0,1,!1],fullD:[0,1e3,!0]}),gt=Object.freeze(["mode",...Object.keys(_t)]),Io=(r,e)=>{let[t,i,n]=_t[r];return typeof e=="number"&&Number.isFinite(e)&&e>=t&&e<=i&&!(n&&e<=t)},sn=r=>({blur:r==="both"||r==="blur",alpha:r==="both"||r==="alpha"});on=Object.freeze(["height","pointerScope","soften","shape","shapeFor"])});function Ai(r){return typeof r=="string"&&kn[r.trim().toLowerCase()]||"arrow"}function Uo(r){let e=[],t=0,i="",n="";for(let s=0;s<r.length;s++){let o=r[s];if(i){if(o==="\\"&&s+1<r.length){n+=o+r[++s];continue}o===i&&(i="")}else if(o==='"'||o==="'")i=o;else if(o==="(")t++;else if(o===")")t=Math.max(0,t-1);else if(o===","&&t===0){e.push(n.trim()),n="";continue}n+=o}return n.trim()&&e.push(n.trim()),e}function Li(r){if(typeof r!="string")return{images:[],keyword:"auto"};let e=St.get(r);if(e)return e;let t=[],i="auto";for(let n of Uo(r)){let s=/^url\(\s*(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|([^)\s]*))\s*\)\s*(.*)$/is.exec(n);if(s){let o=(s[1]??s[2]??s[3]??"").replace(/\\(.)/g,"$1"),a=(s[4]||"").trim().split(/\s+/).filter(Boolean).map(Number),l=a.length===2&&a.every(Number.isFinite)?[a[0],a[1]]:null;o&&t.push({url:o,hotspot:l});continue}/^(-webkit-)?image-set\(/i.test(n)||(i=n.trim().toLowerCase())}return e=Object.freeze({images:Object.freeze(t),keyword:i}),St.size>64&&St.clear(),St.set(r,e),e}function Tn(r){return Ai(Li(r).keyword)}function On(r,e=0){if(!r||r.byteLength<22)return null;let t=new DataView(r);if(t.getUint16(0,!0)!==0||t.getUint16(2,!0)!==2)return null;let i=t.getUint16(4,!0),n=-1;for(let o=0;o<i&&6+16*o+16<=r.byteLength;o++){let a=t.getUint8(6+16*o)||256;if((n<0||a===e)&&(n=o),a===e)break}if(n<0)return null;let s=6+16*n;return[t.getUint16(s+4,!0),t.getUint16(s+6,!0)]}function Dn(r,e=Ii){let t=Li(r);for(let i of t.images){let n=e.get(i.url);if(n.state==="failed")continue;if(n.state!=="ready")break;let s=Math.min(1,Cn/Math.max(n.width,n.height)),o=i.hotspot||n.curHotspot||[0,0],a=n.width*s,l=n.height*s,c=[Math.min(a,Math.max(0,o[0]*s)),Math.min(l,Math.max(0,o[1]*s))];return{kind:"image",key:i.url,entry:n,width:a,height:l,hotspot:c}}return Se(Ai(t.keyword))}function Se(r){if(r==="none")return{kind:"none"};let e=ye.includes(r)?r:"arrow";return{kind:"shape",name:e,key:`shape:${e}`}}function k(r,e,t,i,[n=0,s=0,o=0,a=0]=[]){let l=[],c=(d,f)=>{let h=l[l.length-1];(!h||Math.abs(h[0]-d)>1e-9||Math.abs(h[1]-f)>1e-9)&&l.push([d,f])};return c(r+n,e),c(t-s,e),c(t,e+s),c(t,i-o),c(t-o,i),c(r+a,i),c(r,i-a),c(r,e+n),l.length>1&&Math.abs(l[0][0]-l[l.length-1][0])<1e-9&&Math.abs(l[0][1]-l[l.length-1][1])<1e-9&&l.pop(),l}function Et(r,e,t,i,n,s=n/3.5){let o=t-r,a=i-e,l=Math.hypot(o,a),c=o/l,d=a/l,f=-d*(n/2),h=c*(n/2),u=t-c*s,m=i-d*s,p=1-2*s/n;return Me([[r+f,e+h],[u+f,m+h],[t+f*p,i+h*p],[t-f*p,i-h*p],[u-f,m-h],[r-f,e-h]])}function xn(r,e,t,i,n){let s=t-r,o=i-e,a=Math.hypot(s,o),l=-o/a*(n/2),c=s/a*(n/2);return Me([[r+l,e+c],[t+l,i+c],[t-l,i-c],[r-l,e-c]])}function Pn(r,e,t,i=20){return Me(Array.from({length:i},(n,s)=>[r+t*Math.cos(2*Math.PI*s/i),e+t*Math.sin(2*Math.PI*s/i)]))}function Bo(r,e,t,i,n=24){let s=[],o=(a,l)=>[r+a*Math.cos(l),e+a*Math.sin(l)];for(let a=0;a<n;a++){let l=[0,1,2,3].map(c=>2*Math.PI*(a+c)/n);s.push(Me([...l.map(c=>o(i,c)),...l.reverse().map(c=>o(t,c))]))}return s}function qo(){return{pointer:{hot:[6.8,0],parts:[k(5,0,8.6,14,[1.2,1.2,0,0]),k(7.6,5.4,10.9,14,[1.1,1.1,0,0]),k(9.9,6.4,13.2,14,[1.1,1.1,0,0]),k(12.2,7.6,15.2,14,[.9,.9,0,0]),k(4.2,10.8,15.2,19,[0,0,3,2.2]),Et(7.4,15,1,9,3.6)]},grab:{hot:[8.6,12.5],parts:[k(3,2.6,6.6,12.4,[1.2,1.2,0,0]),k(5.6,.6,9.2,12.4,[1.2,1.2,0,0]),k(8.2,1.4,11.8,12.4,[1.2,1.2,0,0]),k(10.8,3.6,14.2,12.4,[1.1,1.1,0,0]),k(3,9.4,14.2,18,[0,0,2.6,2.6]),Et(6.2,14.4,.4,7,3.6)]},grabbing:{hot:[8.6,13],parts:[k(3,5.6,6.6,12.4,[1.2,1.2,0,0]),k(5.6,5,9.2,12.4,[1.2,1.2,0,0]),k(8.2,5.2,11.8,12.4,[1.2,1.2,0,0]),k(10.8,6,14.2,12.4,[1.1,1.1,0,0]),k(3,9.4,14.2,18,[0,0,2.6,2.6]),Et(6,14.2,1.2,9,3.6)]},move:{hot:[9.5,9.5],parts:[k(3,7.8,16,11.2),k(7.8,3,11.2,16),V([9.5,0],[14,4.6],[5,4.6]),V([9.5,19],[5,14.4],[14,14.4]),V([0,9.5],[4.6,5],[4.6,14]),V([19,9.5],[14.4,14],[14.4,5])]},text:{hot:[9.5,9.5],parts:[k(8,.5,11,18.5),k(4.5,0,14.5,3),k(4.5,16,14.5,19)]},"vertical-text":{hot:[9.5,9.5],parts:Ve([k(8,.5,11,18.5),k(4.5,0,14.5,3),k(4.5,16,14.5,19)],90,9.5,9.5)},crosshair:{hot:[9.5,9.5],parts:[k(0,8,19,11),k(8,0,11,19)]},cell:{hot:[9.5,9.5],parts:[k(1.5,6.3,17.5,12.7),k(6.3,1.5,12.7,17.5)]},"not-allowed":{hot:[9.5,9.5],parts:[...Bo(9.5,9.5,5.4,8.8),xn(4.6,4.6,14.4,14.4,3.4)]},help:{hot:[0,0],parts:[...se(Be(),.8),Pn(13.6,12.4,5.4)],detail:[k(11.6,9,15.4,10.2),k(14.4,9,15.6,12.6),k(12.9,11.6,15.6,12.8),k(12.9,11.6,14.1,13.8),k(12.9,14.8,14.1,16)]},wait:{hot:[9.5,9.5],parts:Xo()},progress:{hot:[0,0],parts:[...se(Be(),.8),k(10.6,9,18.8,11.6),k(10.6,16.4,18.8,19),V([11.4,10.2],[18,10.2],[14.7,14.4]),V([14.7,13.6],[18,17.8],[11.4,17.8])]},copy:{hot:[0,0],parts:[...se(Be(),.8),k(9.6,9.8,18.8,19,[1.2,1.2,1.2,1.2])],detail:[k(13.6,11.8,14.8,17),k(11.6,13.8,16.8,15)]},alias:{hot:[0,0],parts:[...se(Be(),.8),k(9.6,9.8,18.8,19,[1.2,1.2,1.2,1.2])],detail:[xn(11.8,16.8,15.6,13,1.3),V([16.8,11.8],[16.8,15.4],[13.2,11.8])]},"context-menu":{hot:[0,0],parts:[...se(Be(),.8),k(9.2,8.6,18.8,19)],detail:[k(11,10.8,17,11.8),k(11,13.3,17,14.3),k(11,15.8,17,16.8)]},"ns-resize":{hot:[9.5,9.5],parts:Mt()},"ew-resize":{hot:[9.5,9.5],parts:Ve(Mt(),90,9.5,9.5)},"nesw-resize":{hot:[9.5,9.5],parts:se(Ve(Mt(),45,9.5,9.5),1.2,-1.9,-1.9)},"nwse-resize":{hot:[9.5,9.5],parts:se(Ve(Mt(),-45,9.5,9.5),1.2,-1.9,-1.9)},"col-resize":{hot:[9.5,9.5],parts:Sn()},"row-resize":{hot:[9.5,9.5],parts:Ve(Sn(),90,9.5,9.5)},"zoom-in":{hot:[7.5,7.5],parts:Mn(),detail:[k(4.4,6.9,10.6,8.1),k(6.9,4.4,8.1,10.6)]},"zoom-out":{hot:[7.5,7.5],parts:Mn(),detail:[k(4.4,6.9,10.6,8.1)]}}}function Fi(r){let e=ye.includes(r)?r:"arrow",t=En.get(e);if(t)return t;let i=c=>Object.freeze(c.map(d=>Object.freeze(d.map(f=>Object.freeze(f))))),n,s,o;if(e==="arrow")n=[ee],s=[Nn],o=[];else{let c=(Go||=qo())[e],d=f=>f.map(h=>h.map(([u,m])=>[(u-c.hot[0])/19,(m-c.hot[1])/19]));n=i(d(c.parts)),s=i(n.map(f=>Tt(1/19,f))),o=i(d(c.detail||[]))}let a=n.flat().map(c=>c[0]),l=n.flat().map(c=>c[1]);return t=Object.freeze({name:e,parts:Object.freeze(n),fill:Object.freeze(s),detail:Object.freeze(o),box:Object.freeze([Math.min(...a),Math.min(...l),Math.max(...a),Math.max(...l)])}),En.set(e,t),t}function Ee(r,e=1/19){let t=Fi(r);return Math.abs(e-1/19)<1e-12?{outline:t.parts,fill:t.fill,detail:t.detail}:{outline:t.parts,fill:t.parts.map(i=>Tt(e,i)),detail:t.detail}}function An(r,e,t=0){let i=Fi(r).box,n=(e>0&&Number.isFinite(e)?e:0)+(t>0&&Number.isFinite(t)?t:0);return[i[0]-n,i[1]-n,i[2]+n,i[3]+n]}function Ln(r,e,t,i,n,s=Hi,o=$i,a=Ee(r)){let l=0,c=0,d=0,f=0,h=0,u=i>0?qe:qe.slice(0,1),m=a.detail&&a.detail.length?a.detail:null;for(let[p,b,w]of u){let g=e+p*i,x=t+b*i,_=Pi(Di(a.outline,g,x),n),M=_>0?Pi(Di(a.fill,g,x),n):0;m&&M>0&&(M*=1-Pi(Di(m,g,x),n)),l+=w*(o[0]*_+(s[0]-o[0])*M),c+=w*(o[1]*_+(s[1]-o[1])*M),d+=w*(o[2]*_+(s[2]-o[2])*M),f+=w*_,h+=w}return f/=h,f>1e-5?[l/h/f,c/h/f,d/h/f,f]:[0,0,0,0]}function Ni(r,e,t,i,n,s,o=Hi,a=$i,l=Ee(r)){let c=i>0&&Number.isFinite(i)?i:1,d=1/c,[f,h,u,m]=An(r,n,d),p=Math.floor(e+f*c),b=Math.floor(t+h*c),w=Math.max(1,Math.ceil(e+u*c)-p),g=Math.max(1,Math.ceil(t+m*c)-b),x=new Uint8ClampedArray(w*g*4),_=s>=0&&s<=1?s:1;for(let M=0;M<g;M++){let E=(b+M+.5-t)/c;for(let C=0;C<w;C++){let P=Ln(r,(p+C+.5-e)/c,E,n,d,o,a,l);if(!(P[3]>0))continue;let D=(M*w+C)*4;x[D]=Math.round(P[0]*255),x[D+1]=Math.round(P[1]*255),x[D+2]=Math.round(P[2]*255),x[D+3]=Math.round(P[3]*_*255)}}return{x0:p,y0:b,width:w,height:g,data:x}}function ji(r,e,t,i,n,s=Ee(e),o="#fff",a="#000"){for(let[l,c]of[[s.outline,a],[s.fill,o],[s.detail||[],a]]){r.fillStyle=c;for(let d of l)r.beginPath(),d.forEach(([f,h],u)=>(u?r.lineTo:r.moveTo).call(r,t+f*n,i+h*n)),r.closePath(),r.fill()}}function zi(r,e,t,i,n){let s=i>0&&Number.isFinite(i)?Math.ceil(i)+1:0,o=e+2*s,a=t+2*s,l=new Uint8ClampedArray(o*a*4),c=n>=0&&n<=1?n:1,d=s?qe:qe.slice(0,1);for(let f=0;f<a;f++)for(let h=0;h<o;h++){let u=0,m=0,p=0,b=0,w=0;for(let[x,_,M]of d){w+=M;let E=Math.round(h-s+x*i),C=Math.round(f-s+_*i);if(E<0||C<0||E>=e||C>=t)continue;let P=(C*e+E)*4,D=r[P+3]/255*M;u+=r[P]*D,m+=r[P+1]*D,p+=r[P+2]*D,b+=D}if(!(b>1e-5))continue;let g=(f*o+h)*4;l[g]=u/b,l[g+1]=m/b,l[g+2]=p/b,l[g+3]=b/w*c*255}return{pad:s,width:o,height:a,data:l}}function kt(r=globalThis.document){if(!r||!r.createElement)return!1;let e="inline3d-cursor-style";if(r.getElementById&&r.getElementById(e))return!0;let t=r.createElement("style");return t.id=e,t.textContent=Rn,(r.head||r.documentElement)?.appendChild(t),!0}function Fn(r,e){let t=globalThis.getComputedStyle;if(!r||typeof t!="function")return null;let i=[];for(let n of e)n&&n.hasAttribute&&n.hasAttribute(B)&&(i.push([n,n.getAttribute(B)]),n.removeAttribute(B));try{let n=t(r).cursor;return typeof n=="string"?n:null}catch{return null}finally{for(let[n,s]of i)n.setAttribute(B,s??"")}}var B,Rn,Cn,kn,$o,St,Vo,Rt,Ii,Me,V,Ve,se,Be,Mt,Sn,Xo,Mn,Go,En,Di,Pi,In,Ct,Xe,Wi=le(()=>{"use strict";Ui();we();B="data-inline3d-cursor-hidden",Rn=`[${B}],[${B}] *{cursor:none!important}`,Cn=128,kn=Object.freeze({auto:"arrow",default:"arrow",none:"none",pointer:"pointer",grab:"grab","-webkit-grab":"grab",grabbing:"grabbing","-webkit-grabbing":"grabbing",move:"move","all-scroll":"move",text:"text","vertical-text":"vertical-text",crosshair:"crosshair","not-allowed":"not-allowed","no-drop":"not-allowed",help:"help",wait:"wait",progress:"progress",copy:"copy",alias:"alias","context-menu":"context-menu",cell:"cell","n-resize":"ns-resize","s-resize":"ns-resize","ns-resize":"ns-resize","e-resize":"ew-resize","w-resize":"ew-resize","ew-resize":"ew-resize","ne-resize":"nesw-resize","sw-resize":"nesw-resize","nesw-resize":"nesw-resize","nw-resize":"nwse-resize","se-resize":"nwse-resize","nwse-resize":"nwse-resize","col-resize":"col-resize","row-resize":"row-resize","zoom-in":"zoom-in","-webkit-zoom-in":"zoom-in","zoom-out":"zoom-out","-webkit-zoom-out":"zoom-out"}),$o=Object.freeze(Object.keys(kn));St=new Map;Vo=r=>/\.cur(?:[?#]|$)/i.test(r)||/^data:image\/(?:x-win-bitmap|x-icon|vnd\.microsoft\.icon)/i.test(r),Rt=class{constructor({ImageCtor:e=globalThis.Image,fetchFn:t=globalThis.fetch}={}){this._Image=e,this._fetch=t,this._m=new Map}get(e){let t=this._m.get(e);if(t)return t;if(t={url:e,state:"loading",image:null,width:0,height:0,curHotspot:null},this._m.set(e,t),typeof this._Image!="function")return t.state="failed",t;let i=new this._Image;i.crossOrigin="anonymous";let n=1,s=()=>{--n>0||t.state==="failed"||(t.state="ready")};return i.onload=()=>{let o=i.naturalWidth||i.width,a=i.naturalHeight||i.height;if(!(o>0&&a>0))return void(t.state="failed");t.image=i,t.width=o,t.height=a,s()},i.onerror=()=>{t.state="failed"},Vo(e)&&typeof this._fetch=="function"&&(n++,Promise.resolve().then(()=>this._fetch(e,e.startsWith("data:")?void 0:{mode:"cors"})).then(o=>o&&o.ok!==!1?o.arrayBuffer():null).then(o=>{t.curHotspot=o?On(o,t.width):null}).catch(()=>{}).finally(s)),i.src=e,t}},Ii=new Rt;Me=r=>{let e=0;for(let t=0;t<r.length;t++){let[i,n]=r[t],[s,o]=r[(t+1)%r.length];e+=i*o-s*n}return e>0?r:r.reverse()};V=(r,e,t)=>Me([r,e,t]),Ve=(r,e,t,i)=>{let n=e*Math.PI/180,s=Math.cos(n),o=Math.sin(n);return r.map(a=>Me(a.map(([l,c])=>[t+(l-t)*s-(c-i)*o,i+(l-t)*o+(c-i)*s])))},se=(r,e,t=0,i=0)=>r.map(n=>n.map(([s,o])=>[s*e+t,o*e+i])),Be=()=>[ee.map(([r,e])=>[r*19,e*19])],Mt=()=>[k(8,3,11,16),V([9.5,0],[14.2,5],[4.8,5]),V([9.5,19],[4.8,14],[14.2,14])],Sn=()=>[k(8,1,11,18),k(3.4,8,7.4,11),V([0,9.5],[4.4,5],[4.4,14]),k(11.6,8,15.6,11),V([19,9.5],[14.6,14],[14.6,5])],Xo=()=>[k(3,0,16,3),k(3,16,16,19),V([4.2,.5],[14.8,.5],[9.5,10]),V([9.5,9],[14.8,18.5],[4.2,18.5])],Mn=()=>[Pn(7.5,7.5,7),Et(11.6,11.6,18.4,18.4,3.8,1)];Go=null,En=new Map;Di=(r,e,t)=>{let i=1/0;for(let n of r)i=Math.min(i,jn(n,e,t));return i},Pi=(r,e)=>e>0?Math.min(1,Math.max(0,.5-r/e)):r<0?1:0;In=100;Ct=class{constructor(e,{throttleMs:t=In}={}){this.pointer=e,this.throttleMs=t,this.css=null,this.reads=0,this._el=null,this._stale=!0,this._urgent=!0,this._lastRead=-1/0;let i=n=>s=>{let o=typeof s.composedPath=="function"?s.composedPath():null,a=o&&o.length?o[0]:s.target;a&&a.nodeType===1&&(this._el=a),this._stale=!0,n&&(this._urgent=!0)};this._onMove=i(!1),this._onPress=i(!0),this._target=e.scope==="window"?globalThis:e.canvas,this._target&&this._target.addEventListener&&(this._target.addEventListener("pointermove",this._onMove),this._target.addEventListener("pointerdown",this._onPress),this._target.addEventListener("pointerup",this._onPress),this._target.addEventListener("pointercancel",this._onPress))}value(e=globalThis.performance?globalThis.performance.now():Date.now()){return this._stale&&(this._urgent||this.css===null||e-this._lastRead>=this.throttleMs)&&this.read(e),this.css??"auto"}shape(e){return Tn(this.value(e))}read(e=globalThis.performance?globalThis.performance.now():Date.now()){let t=globalThis.document,i=this.pointer.canvas,n=this._el;n&&n.isConnected===!1&&(n=null);let s=Fn(n||i,[i,t&&t.documentElement]);return this.reads++,this._lastRead=e,this._stale=!1,this._urgent=!1,s!==null&&(this.css=s),this.css??"auto"}dispose(){this._target&&this._target.removeEventListener&&(this._target.removeEventListener("pointermove",this._onMove),this._target.removeEventListener("pointerdown",this._onPress),this._target.removeEventListener("pointerup",this._onPress),this._target.removeEventListener("pointercancel",this._onPress))}},Xe=class{constructor(e,{shape:t="auto",shapeFor:i=null,images:n=Ii}={}){if(!ve.includes(t))throw new Error(`@displayxr/inline3d: cursor.shape "${t}" \u2014 expected ${ve.join(", ")}.`);this.option=t,this.shapeFor=typeof i=="function"?i:null,this.images=n,this.reader=t==="auto"?new Ct(e):null,this.sprite=Se(t==="auto"?"arrow":t)}get current(){return this.sprite.kind==="shape"?this.sprite.name:this.sprite.kind}pick(e,t){let i=this.reader?this.reader.value(t):null,n=i===null?null:Dn(i,this.images),s=n?n.kind==="shape"?n.name:n.kind:null,o;if(this.shapeFor)try{o=this.shapeFor({...e,css:s,cssValue:i})}catch(l){this._warned||(this._warned=!0,console.warn("[inline3d] cursor.shapeFor threw; using the page cursor.",l))}let a=l=>l==="none"||ye.includes(l);return a(o)?this.sprite=Se(o):a(this.option)?this.sprite=Se(this.option):this.sprite=n||Se("arrow"),this.sprite}dispose(){this.reader?.dispose()}}});function $n(r){return r>0&&Number.isFinite(r)?Math.min(.5,Xi/r):Yo}function Un(r,e){return r<=Ko?!(e>0&&Number.isFinite(e))||-r*e>=Jo:!1}function qi(r,e,t){if(!e||!Number.isFinite(t)||!(t<0))return 0;let i=Math.min(0,Number.isFinite(r.maxDisparity)?r.maxDisparity:0);return Math.min(i,Math.max(r.minDisparity,t-r.margin))}function Vn(r,e,t,i){if(!r.primed||i<r.last||i-r.last>e.stale)return r.primed=!0,r.disparity=t,r.last=i,t;if(i===r.last)return r.disparity;let n=i-r.last,s=t<r.disparity?e.riseTau:e.sinkTau,o=s>0?1-Math.exp(-n/s):1;return r.disparity+=o*(t-r.disparity),r.last=i,r.disparity}function zn(r){let e=r&&r.pointerType;return!e||e==="mouse"?!0:e==="pen"?!(r.buttons>0):!1}function Zo(r){return!!r&&"onpointerrawupdate"in r&&globalThis.isSecureContext!==!1}function Qo(r){let e=globalThis.document;if(!e||!e.documentElement)return;r&&kt(e),Vi=Math.max(0,Vi+(r?1:-1));let t=e.documentElement;Vi>0?t.setAttribute?.(B,""):t.removeAttribute?.(B)}function Wn(r){return!!r&&typeof r.setCursorDepth=="function"&&typeof r.getCursorDepthSupport=="function"}function ra(r,e,t,i){let n=.75*t,s=[[r,e]];for(let o=0;o<8;o++){let a=o*Math.PI/4;s.push([r+n*Math.cos(a)/i,e+n*Math.sin(a)])}return s}function Bn(r,e,t,i){return[[r,e],...ra(r+.3*t/i,e+.5*t,t,i)]}function Tt(r,e=ee){let t=0;for(let s=0;s<e.length;s++){let[o,a]=e[s],[l,c]=e[(s+1)%e.length];t+=o*c-l*a}let i=t>0?1:-1,n=e.map((s,o)=>{let a=e[(o+1)%e.length],l=a[0]-s[0],c=a[1]-s[1],d=Math.hypot(l,c)||1;return[-c/d*i,l/d*i]});return e.map((s,o)=>{let a=n[(o+e.length-1)%e.length],l=n[o],c=r/(1+a[0]*l[0]+a[1]*l[1]);return[s[0]+(a[0]+l[0])*c,s[1]+(a[1]+l[1])*c]})}function Xn(r,e,t,i=_e){let n={blurPx:0,alpha:1},s=i;if(!s)return n;if(s.blur&&Number.isFinite(e)&&s.blurK>0&&s.blurMaxPx>0){let o=s.blurMaxPx;if(s.blurMaxFrac>0&&Number.isFinite(t)&&t>0){let l=s.blurMaxFrac*t;o=l<o?l:o}let a=s.blurK*Math.abs(e);n.blurPx=a<o?a:o}if(s.alpha&&Number.isFinite(r)&&s.fullD>0){let o=Math.abs(r)/s.fullD;o=o<1?o:1;let a=o*o*(3-2*o),l=s.alphaMin<0?0:s.alphaMin>1?1:s.alphaMin;n.alpha=1-(1-l)*a}return n}function jn(r,e,t){let i=(e-r[0][0])**2+(t-r[0][1])**2,n=1;for(let s=0,o=r.length-1;s<r.length;o=s,s++){let a=r[s],l=r[o],c=l[0]-a[0],d=l[1]-a[1],f=e-a[0],h=t-a[1],u=Math.min(1,Math.max(0,(f*c+h*d)/(c*c+d*d))),m=f-c*u,p=h-d*u;i=Math.min(i,m*m+p*p);let b=t>=a[1],w=t<l[1],g=c*h>d*f;(b&&w&&g||!b&&!w&&!g)&&(n=-n)}return n*Math.sqrt(i)}var Yo,Xi,Ko,Jo,Pt,Ot,Vi,ea,ta,ia,Bi,Dt,Hn,ee,Tl,Nn,Hi,$i,qe,Ol,Dl,Pl,Ui=le(()=>{"use strict";we();we();Wi();Wi();Yo=.03,Xi=19;Ko=-.005,Jo=.5;Pt=Object.freeze({margin:.015,minDisparity:-.6,maxDisparity:0,riseTau:.03,sinkTau:.25,stale:.5});Ot=class{constructor(e,{scope:t="canvas",keepCss:i=!1}={}){if(t!=="canvas"&&t!=="window")throw new Error(`@displayxr/inline3d: cursor pointerScope "${t}" \u2014 expected 'canvas' or 'window'.`);this.canvas=e,this.scope=t,this.keepCss=!!i,this._uv=null,this.sampleTime=0,this.samples=0,this._hidden=!1,this._cx=0,this._cy=0,this._pending=!1,this._onMove=n=>this._sample(n),this._onLeave=()=>{this.uv=null},this._onDown=n=>{zn(n)||(this.uv=null)},this._target=t==="window"?globalThis:e,this.source=Zo(this._target)?"pointerrawupdate":"pointermove",this._target.addEventListener("pointermove",this._onMove),this.source==="pointerrawupdate"&&this._target.addEventListener("pointerrawupdate",this._onMove),this._target.addEventListener("pointerdown",this._onDown),t==="window"?(this._onOut=n=>{n.relatedTarget||(this.uv=null)},globalThis.addEventListener("pointerout",this._onOut),globalThis.addEventListener("blur",this._onLeave)):e.addEventListener("pointerleave",this._onLeave)}get uv(){if(this._pending){this._pending=!1;let e=this.canvas.getBoundingClientRect();if(!(e.width>0&&e.height>0))this._uv=null;else{let t=(this._cx-e.left)/e.width,i=(this._cy-e.top)/e.height;this._uv=t>=0&&t<=1&&i>=0&&i<=1?[t,i]:null}}return this._uv}set uv(e){this._pending=!1,this._uv=e}_sample(e){if(!zn(e)){this.uv=null;return}let t=typeof e.getCoalescedEvents=="function"?e.getCoalescedEvents():null,i=t&&t.length?t[t.length-1]:e,n=i.timeStamp>0?i.timeStamp:e.timeStamp>0?e.timeStamp:globalThis.performance?globalThis.performance.now():Date.now();n<this.sampleTime||(this.samples++,this.sampleTime=n,this._cx=i.clientX,this._cy=i.clientY,this._pending=!0)}set(e,t){this.uv=e==null?null:[e,t]}aspect(){let e=this.canvas.getBoundingClientRect();return e.height>0?e.width/e.height:1}cssHeight(){let e=this.canvas.getBoundingClientRect();return e.height>0?e.height:0}panelPx(){return this.cssHeight()*(globalThis.devicePixelRatio>0?globalThis.devicePixelRatio:1)}panelSize(){let e=this.canvas.getBoundingClientRect(),t=globalThis.devicePixelRatio>0?globalThis.devicePixelRatio:1;return e.width>0&&e.height>0?[e.width*t,e.height*t]:[0,0]}hideCss(e){this.keepCss&&(e=!1),e!==this._hidden&&(this.scope==="window"?Qo(e):this.canvas.setAttribute&&(e?(kt(),this.canvas.setAttribute(B,"")):this.canvas.removeAttribute(B)),this._hidden=e)}dispose(){this._target.removeEventListener("pointermove",this._onMove),this.source==="pointerrawupdate"&&this._target.removeEventListener("pointerrawupdate",this._onMove),this._target.removeEventListener("pointerdown",this._onDown),this.scope==="window"?(globalThis.removeEventListener("pointerout",this._onOut),globalThis.removeEventListener("blur",this._onLeave)):this.canvas.removeEventListener("pointerleave",this._onLeave),this.hideCss(!1)}};Vi=0;ea=2e3,ta=6,ia=1e4;Bi=new WeakMap,Dt=class{constructor(){this.target=null,this._sent=!1,this.last=null}get support(){let e=this.target;if(!Wn(e))return"none";let t=Bi.get(e);return t?t.state:"pending"}get drawing(){return this.support==="runtime"}bind(e){let t=e||null;if(t!==this.target&&(this.withdraw(),this.target=t),!Wn(t))return!1;let i=Bi.get(t);if(i||Bi.set(t,i={state:"pending",probes:0,inFlight:!1,at:0}),i.state==="runtime"||i.inFlight)return i.state==="runtime";let n=globalThis.performance?globalThis.performance.now():Date.now(),s=i.probes<ta?ea:ia;if(i.probes===0||n-i.at>=s){i.probes++,i.at=n,i.inFlight=!0;let o;try{o=Promise.resolve(t.getCursorDepthSupport())}catch(a){o=Promise.reject(a)}o.then(a=>{i.inFlight=!1,i.state=a==="runtime"?"runtime":"none"},()=>{i.inFlight=!1,i.state="none"})}return i.state==="runtime"}report(e,t,i){if(!this.drawing)return!1;let n={u:Hn(e),v:Hn(t)};i!=null&&Number.isFinite(i)&&(n.disparityPx=i);try{this.target.setCursorDepth(n)}catch{return!1}return this._sent=!0,this.last={u:n.u,v:n.v,disparityPx:n.disparityPx??null},!0}withdraw(){if(this.last=null,!!this._sent){this._sent=!1;try{this.target?.setCursorDepth()}catch{}}}dispose(){this.withdraw(),this.target=null}},Hn=r=>r>0?r<1?r:1:0;ee=Object.freeze([[0,0],[0,16],[4.2,12.4],[6.9,18.6],[9.5,17.5],[6.8,11.5],[11.7,11.5]].map(([r,e])=>Object.freeze([r/19,e/19]))),Tl=Object.freeze([[0,1,2],[0,2,5],[0,5,6],[2,3,4],[2,4,5]]);Nn=Object.freeze(Tt(1/19).map(r=>Object.freeze(r))),Hi=Object.freeze([1,1,1,1]),$i=Object.freeze([.02,.02,.03,1]);qe=Object.freeze([Object.freeze([0,0,1]),...[[.5,0],[-.5,0],[0,.5],[0,-.5]].map(([r,e])=>Object.freeze([r,e,.7066])),...Array.from({length:8},(r,e)=>{let t=.3927+.7854*e;return Object.freeze([Math.cos(t),Math.sin(t),.2494])})]),Ol=Object.freeze([Math.max(...ee.map(r=>r[0])),Math.max(...ee.map(r=>r[1]))]);Dl=Object.freeze({declarations:`#ifdef PICK_PASS
3
3
  #include "pickPS"
4
4
  #endif
@@ -0,0 +1,270 @@
1
+ # Content lifecycle rules: release what a swap replaces
2
+
3
+ Companion to [`authoring-inline-3d.md`](authoring-inline-3d.md) and
4
+ [`woven-canvas-rules.md`](woven-canvas-rules.md). Those pages keep a woven canvas alive and
5
+ joined. This one covers what happens to the GPU over the life of that canvas, when the page swaps
6
+ content in and out of it: a product carousel, a catalogue, a gallery that loads glTF after glTF.
7
+
8
+ The symptom is slow and silent. Nothing on the page looks wrong. The browser's **GPU process**
9
+ grows by a few megabytes a minute for as long as the page is open, until the machine runs short
10
+ of memory. The rules below come from a production page that grew about **8 MB/min** this way on a
11
+ Linux laptop. They were then checked against the PlayCanvas 2.22.3 and three.js 0.180 sources, and
12
+ measured against this SDK's own swap paths (§5).
13
+
14
+ ---
15
+
16
+ ## 1. What leaks, and why it costs so much
17
+
18
+ Every distinct material set a page draws makes the engine **link a WebGL program**: one per
19
+ material variant (lighting, skinning, maps, pass). A linked program is not just a handle. The
20
+ driver keeps its compiled form for as long as the program exists. On one Linux driver (Mesa, integrated
21
+ graphics) that was **about 5 MB per live program**, measured with an allocation tracer on the
22
+ browser's GPU process: the outstanding allocations were the linked program's IR, freed only by
23
+ `glDeleteProgram`. The production page linked about four new programs per product and deleted
24
+ none, so each product it showed left 15–25 MB behind for the life of the page.
25
+
26
+ What the page saw as "swapping a product" was, to the GPU, *adding* one. The page destroyed the
27
+ entity and its materials, and the engine kept every program those materials had linked.
28
+
29
+ ## 2. Engine facts
30
+
31
+ ### PlayCanvas (2.22.3)
32
+
33
+ - **`ProgramLibrary` caches every program for the device's life.** Its cache is keyed by the
34
+ variant's options. An entry leaves only when that `Shader` is destroyed (`Shader.destroy()`),
35
+ or when the device is.
36
+ - **`Material.destroy()` destroys no program.** It empties `material.variants` and clears each
37
+ mesh instance's per-pass cache (its bind groups and uniform buffers), and that is all. Once it
38
+ has run, the material no longer even says which programs it had.
39
+ - **`Asset.unload()` on a glTF container** destroys the container's textures, meshes and
40
+ materials (each material through `Material.destroy()`, so no program again). **The registry
41
+ shares one container per URL** between every caller that loads it.
42
+ - **One `Shader` per key, shared.** Two materials with the same options get the same `Shader`
43
+ object from the library. Destroying it under one of them breaks the other.
44
+ - **The compiled shaders outlive their program.** `WebglShader.destroy()` deletes the program and
45
+ nothing else. The vertex and fragment shaders it was linked from stay in a module-private
46
+ `CompiledShaderCache` (keyed by source) until the device is destroyed. So even a page that
47
+ destroys its programs keeps two compiled shaders per program it ever linked.
48
+ - **`app.destroy()` frees everything.** It unloads every asset, clears the program library
49
+ (destroying each program) and destroys the device, which deletes the cached shaders.
50
+
51
+ ### three.js (0.180)
52
+
53
+ - **Programs are reference-counted per material.** The renderer listens for each material's
54
+ `dispose` event. `material.dispose()` releases that material's programs
55
+ (`WebGLPrograms.releaseProgram`), and the last material out deletes the program
56
+ (`gl.deleteProgram`). Materials that compile to the same program share it.
57
+ - **There is no compiled-shader cache to leak.** `WebGLProgram` deletes its two shaders right
58
+ after linking.
59
+ - **Nothing is freed by taking an object out of the scene,** or by dropping the last reference to
60
+ it. Call `geometry.dispose()`, `material.dispose()` and `texture.dispose()` for each texture.
61
+ `material.dispose()` does not dispose the textures it uses.
62
+ - **`renderer.info.programs.length`** is the live program count: the pass test of §4, for free.
63
+
64
+ ## 3. The rules
65
+
66
+ ### 1. Release everything a swap replaces, not just the entity
67
+
68
+ A swap replaces five things: the **entity**, its **materials**, the **programs** those materials
69
+ linked, their **textures**, and the **container** they came from. Destroying the entity releases
70
+ the first. Unloading the container releases the textures, meshes and materials. The engine
71
+ releases the programs for nobody.
72
+
73
+ **On a PlayCanvas tile the SDK created, it releases the programs for you** (from the release
74
+ after 1.47.5): when the page destroys a material, directly or through the container's
75
+ `asset.unload()`, the SDK destroys the programs that material linked one frame later, when
76
+ nothing still in the app uses them (rules 2 and 3, done automatically; see §6). So the page's
77
+ part is the engine's own teardown: **destroy the entity, and destroy or unload what it drew
78
+ with.** `entity.destroy()` alone is not enough. A container that stays loaded keeps its
79
+ materials alive, and a live material keeps its programs on purpose, because the page may show it
80
+ again.
81
+
82
+ On a PlayCanvas tile, content you add under `handle.engine.root` is released in one call:
83
+
84
+ ```js
85
+ const prev = shown; // { entity, asset } of the outgoing product
86
+ const { programs, container } = handle.releaseEntity(prev.entity, { asset: prev.asset });
87
+ ```
88
+
89
+ `releaseEntity` (on `addSplat(…, { engine: 'playcanvas' })` and PlayCanvas `addModel`
90
+ handles) does rules 2 to 4 in that call, including the container's last-user check of rule 4,
91
+ and the SDK does rule 5 on its own devices. Without it, the page unloads the container itself
92
+ (rule 4) and the automatic release above covers the programs. On a three.js scene, traverse and dispose (§2, three.js).
93
+
94
+ ### 2. Collect the programs before you destroy anything
95
+
96
+ `Material.destroy()` empties `material.variants`, and the container's unload destroys its
97
+ materials. Read `variants` of every material the entity's mesh instances use (and each mesh
98
+ instance's own shader cache), and of the container's materials, **first**. Then destroy the entity,
99
+ then unload the container, then destroy the programs.
100
+
101
+ ### 3. Destroy only a program nothing else still draws with
102
+
103
+ Because the library hands one `Shader` to every material with the same key, a program you collected
104
+ may also belong to content that stays. Before `shader.destroy()`, look at what is still in the app:
105
+ every mesh instance under `app.root` (enabled or not), every layer's mesh instances, and every
106
+ material asset still in the registry. Destroy only what none of them holds. `Shader.destroy()` is
107
+ the engine's own release: it deletes the program and takes it out of the library, so the next ask
108
+ for that variant compiles it again rather than returning a dead one.
109
+
110
+ ### 4. Unload a container with its last user
111
+
112
+ The registry shares one container per URL, so two products loaded from the same file share
113
+ textures and materials. Unload it (`app.assets.remove(asset); asset.unload()`) only when nothing
114
+ left in the scene draws from it: refcount your holders, or check that no remaining mesh instance
115
+ uses one of its meshes or materials (what `releaseEntity` does: it reports `container: 'in-use'`
116
+ and leaves it loaded).
117
+
118
+ ### 5. Let the engine give compiled shaders back
119
+
120
+ Rule 3 deletes the programs. The compiled shaders behind them stay in the engine's
121
+ `CompiledShaderCache` (§2), so a page that follows rules 1–4 still keeps two compiled shaders for
122
+ every program it ever linked (measured in §5: shaders 10 → 91 over 24 swaps, programs bounded).
123
+ The fix counts, per cached shader, the programs that use it, and deletes the shader when the last
124
+ one is destroyed. **The SDK applies it to every PlayCanvas device it creates** (see §6). A page that
125
+ runs its own PlayCanvas app outside the SDK has to patch the engine itself: the change is about 20
126
+ lines in `src/platform/graphics/webgl/webgl-shader.js` (a count in `_compileShaderSource`, a
127
+ release in `WebglShader.destroy`, cleared on context loss). `js/inline3d-pc-shader-cache.js` is the
128
+ reference.
129
+
130
+ ### 6. On three.js, dispose explicitly, and textures separately
131
+
132
+ Removing a mesh from the scene frees nothing on the GPU. For each object a swap replaces, call
133
+ `geometry.dispose()`, `material.dispose()`, and `dispose()` on every texture the material uses
134
+ (`map`, `normalMap`, …). The renderer then deletes a program when its last material is disposed.
135
+ Do not dispose a geometry, material or texture that content which stays still uses.
136
+
137
+ ### 7. Fewer variants is cheaper than releasing them
138
+
139
+ Each distinct option set is a program. A catalogue whose items share one material set (the same
140
+ maps, the same lighting) links once and swaps meshes and textures. A catalogue of hand-made
141
+ materials links per item. Releasing keeps the second kind bounded, but every swap then compiles
142
+ again, which costs a frame or more of main-thread time on first draw: warm the incoming content
143
+ before you show it (`handle.warmEntity`, [PlayCanvas adapter § warmEntity](playcanvas-adapter.md#warmentity--a-gltfs-first-draw-without-the-freeze)).
144
+
145
+ ### 8. Removing the tile is not a content swap
146
+
147
+ `handle.remove()` on a PlayCanvas tile destroys the app, and with it every program, shader,
148
+ texture and asset (measured in §5: nothing left). But a new tile is a new canvas for the browser's
149
+ compositor to join, with the raw side-by-side window that brings
150
+ ([woven-canvas rules 2 and 3](woven-canvas-rules.md#2-never-remount-a-woven-canvas-inside-a-screen)).
151
+ Keep the tile and swap what is in it, with rules 1–5.
152
+
153
+ ## 4. The pass test: hook, swap, plateau
154
+
155
+ Count program and shader creation on the context, swap content about ten times, and check that the
156
+ live count (`created − deleted`) **plateaus**. A page that leaks climbs with every new item. A page
157
+ that releases goes up and down around what is on screen.
158
+
159
+ ```js
160
+ // Run before the page creates its WebGL context (inject at document start, e.g. with the
161
+ // DevTools protocol's Page.addScriptToEvaluateOnNewDocument, then reload).
162
+ const live = (globalThis.__glLive = { programs: 0, shaders: 0 });
163
+ for (const Ctx of [WebGLRenderingContext, WebGL2RenderingContext]) {
164
+ const p = Ctx.prototype;
165
+ const count = (name, key, step) => {
166
+ const orig = p[name];
167
+ p[name] = function (...args) {
168
+ const r = orig.apply(this, args);
169
+ if (step > 0 ? r : args[0]) live[key] += step; // a create that returned an object; a delete of one
170
+ return r;
171
+ };
172
+ };
173
+ count('createProgram', 'programs', +1);
174
+ count('deleteProgram', 'programs', -1);
175
+ count('createShader', 'shaders', +1);
176
+ count('deleteShader', 'shaders', -1);
177
+ }
178
+ // Swap content ten times, reading __glLive after each swap: it must level off.
179
+ ```
180
+
181
+ Two things to know when reading it:
182
+ - **Content that repeats plateaus anyway.** The engine reuses a cached program for a variant it has
183
+ already linked, so a carousel of eight products stops growing after the eighth even without a
184
+ release. Test with more distinct items than the page will show at once, or with real catalogue
185
+ data.
186
+ - **The SDK's own transitions compile once.** The first `setSource` with each transition kind
187
+ links its programs, and later swaps reuse them: a few rises, then flat.
188
+
189
+ **`?dxrdiag` does this for you on a PlayCanvas tile.** The diagnostics overlay shows
190
+ `gl programs N live (+created −deleted) shaders N live (…) cache patched`, and the dump
191
+ (`copy(__dxrDiag.dump())`) carries the same under `glObjects`. At every content swap the SDK can
192
+ see (a `setSource`, a glTF container loaded into the tile's registry, a `releaseEntity`), it samples
193
+ the live program count. When the count has not dropped across the last 10 swaps of one kind and
194
+ rose at more than 60% of them, it warns once in the console. It costs nothing when diag is off.
195
+ `?dxrdiag=keepshaders` turns rule 5 off for an A/B (page-wide; set it on the URL before the first
196
+ tile loads). The overlay also shows `release patched (−N)`: programs the automatic release of
197
+ rule 1 destroyed. `?dxrdiag=keepprograms` turns that off for an A/B (per tile). Details: [PlayCanvas adapter § Diagnosing transition stalls](playcanvas-adapter.md#diagnosing-transition-stalls-diag--dxrdiag-36).
198
+
199
+ ## 5. Measured: the SDK's own paths
200
+
201
+ DisplayXR Browser on Linux (Chromium 156, Mesa, integrated graphics), PlayCanvas 2.22.3, the hook of §4,
202
+ Khronos glTF sample models. "Live" is `created − deleted` after each step.
203
+
204
+ | path | before this release | with this release |
205
+ |---|---|---|
206
+ | `addModel` → `remove()` → `addModel` on the same canvas, 10–12 times over 4 models | live 0 programs / 0 shaders after every `remove()` (60 created, 60 deleted) | same: nothing leaks |
207
+ | glTF swapped under `handle.engine.root`, page destroys the entity and unloads the container (rule 1 without the programs), 24 distinct models | programs 5 → 47, shaders 10 → 91; `deleteProgram` never called | same: the page never destroys a program, so nothing the SDK does can free it. `?dxrdiag` warns at the 10th swap |
208
+ | the same swaps with `handle.releaseEntity(entity, { asset })` | n/a | programs between 5 and 16 (the model on screen), shaders exactly twice that; back to 5 / 10 after the last release |
209
+ | the same, `?dxrdiag=keepshaders` (rule 5 off) | n/a | programs bounded (5–16), shaders 10 → 91: the engine's compiled-shader cache alone |
210
+ | `setSource` × 12–16 on one splat (cut, crossfade, wavefront, flip in turn) | +6 programs over the first pass of the four transitions, then flat | same |
211
+ | `setVideo(src)` / `setVideo(null)` × 6 | +1 program on the first, then flat | same |
212
+
213
+ **The automatic program release (rule 1), measured.** DisplayXR Browser on macOS (Chromium 156,
214
+ Apple silicon), PlayCanvas 2.22.3, the hook of §4. A PlayCanvas `addModel` tile; each swap adds a
215
+ product with its own material options (a distinct program) under `handle.engine.root`, and
216
+ destroys the outgoing entity and its material: the engine's own teardown, no `releaseEntity`.
217
+
218
+ | 24 swaps | live programs (created) | live shaders |
219
+ |---|---|---|
220
+ | automatic release (default) | 2 (25 created, 23 deleted) | 4 |
221
+ | `?dxrdiag=keepprograms` (the engine as it ships) | 25 (25 created, 0 deleted) | 50 |
222
+
223
+ The same pattern in a production kiosk app (one persistent PlayCanvas stage, content swapped per
224
+ screen, entities, materials and textures destroyed, no program ever released) went from 0 to 170
225
+ live programs over 28 screen changes before this release.
226
+
227
+ So the SDK's own swaps (`setSource`, `setVideo`, `prepareSource().dispose()`, `remove()`) release
228
+ what they replace, and content a page adds under `handle.engine.root` is the page's, with
229
+ `releaseEntity` to do it. On the production page, releasing the outgoing product's programs and
230
+ container plus the shader-cache fix took the GPU process from **+9.1 MB/min to flat** over a soak,
231
+ with live programs and shaders steady and every product still rendering when it came round again.
232
+
233
+ ## 6. What the SDK does for you, and what the page still owns
234
+
235
+ | | SDK | page |
236
+ |---|---|---|
237
+ | `handle.remove()` (PlayCanvas `./splat`, `./model`) | destroys the app: every entity, asset, program, shader and texture on the tile | nothing |
238
+ | `handle.remove()` (three.js `./model`) | disposes the model's geometries, materials and textures | anything the page added to the scene |
239
+ | `setSource` | the outgoing asset (entity destroyed, asset unloaded a few frames later); its own transition programs are compiled once and reused | nothing |
240
+ | `setVideo(null)`, `prepareSource().dispose()` | the plane / the prepared asset | nothing |
241
+ | compiled shaders (rule 5) | on every PlayCanvas device the SDK creates | a PlayCanvas app the page creates itself |
242
+ | programs of a material the page destroys (rule 1) | on every PlayCanvas app the SDK creates, a frame after the destroy, when nothing else uses them | a PlayCanvas app the page creates itself (rules 2–3 by hand) |
243
+ | content under `handle.engine.root` | `handle.releaseEntity(entity, { asset })` when the page calls it | destroying the entity and destroying or unloading what it drew with (or calling `releaseEntity`) |
244
+ | `addScene` (three.js or raw WebGL) | nothing: the renderer is the page's | rule 6 |
245
+
246
+ **How the shader-cache fix rides along with the engine.** PlayCanvas is the page's own peer
247
+ dependency (an import map, a bundler, a CDN build), never a file this package ships, so the SDK
248
+ cannot patch it on disk. `js/inline3d-pc-shader-cache.js` replaces two methods on the engine's
249
+ `WebglShader` prototype at run time, once, through the first shader the device creates. It
250
+ installs only on the 2.22.x shape (an `_compileShaderSource(device, src, isVertexShader)` reading a
251
+ cache keyed by source, and a `destroy` that does not delete shaders), and otherwise steps aside and
252
+ reports why in `?dxrdiag` (`cache skipped: …`). On an engine bump: re-read `webgl-shader.js`; if it
253
+ still never deletes a compiled shader before device destroy, keep the fix and update its shape
254
+ check and `test/pc-shader-cache.test.mjs` (a copy of the 2.22.3 class shape). If upstream releases
255
+ shaders itself, the check makes the fix a no-op, and it can be deleted.
256
+
257
+ **How the program release rides along with the engine.** `js/inline3d-pc-auto-release.js` wraps
258
+ two methods on the engine's `Material` prototype, once: `getShaderVariant` (the one place a
259
+ material links a program) records the material weakly on its device, and `destroy` collects the
260
+ material's programs before the engine empties `variants`. Both act only on devices the SDK
261
+ created; a PlayCanvas app the page runs itself is untouched. At the app's next `frameend` (a
262
+ timer backs that up for a stopped app) it destroys each collected program that no mesh instance
263
+ in the app, no material asset in the registry and **no still-alive material** holds. The last
264
+ condition is what makes it safe: the library hands one `Shader` to every material with the same
265
+ key, and a material the page keeps off-scene to show again still has it in its `variants`.
266
+ `Shader.destroy()` also drops the program from the library, so content shown again recompiles on
267
+ its first draw (`handle.warmEntity` hides that). It installs only on the 2.22.x shape (a
268
+ `getShaderVariant` and a `destroy` that clears `variants`) and otherwise reports
269
+ `release skipped: …` in `?dxrdiag`. On an engine bump: re-read `material.js` and
270
+ `mesh-instance.js`'s `getShaderInstance`; keep `test/pc-auto-release.test.mjs` in step.
@@ -153,6 +153,9 @@ function addModelUnguarded(wall, canvas, src, opts = {}) {
153
153
  depthFactor: 1,
154
154
  // Before the backend lands: once it has, the backend's own (PlayCanvas); three has none (0).
155
155
  warmEntity: (entity, o) => out.ready.then(() => (out.warmEntity !== stubWarm && typeof out.warmEntity === 'function' ? out.warmEntity(entity, o) : 0)),
156
+ // PlayCanvas replaces this once it has loaded; before that (and on three) there is nothing of
157
+ // the engine's to release.
158
+ releaseEntity: () => ({ programs: 0, container: null }),
156
159
  };
157
160
  const stubWarm = out.warmEntity;
158
161
  out.firstWoven = new Promise((resolve) => {
@@ -40,6 +40,7 @@
40
40
  import { PlayCanvasSplatViewer, pageRigTRS, pageViewAxis, pageViewRig } from './inline3d-splat-playcanvas.js';
41
41
  import { resolveControls, normalizeCameraPose, now, MAX_DT_S, declareViewRig, attachModelStereo, attachComfortDepth, resolveDepthOption, framedSubjectBounds } from './inline3d-splat-shared.js';
42
42
  import { displayRig } from './inline3d-three.js';
43
+ import { resolveDiag, DiagRecorder, startDiagLoop, registerDiag, watchContent } from './inline3d-splat-diag.js';
43
44
  import {
44
45
  NEUTRAL_STUDIO,
45
46
  neutralStudioRadiance,
@@ -479,6 +480,11 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
479
480
  let handle = null;
480
481
  let removed = false;
481
482
  let lastPagePose = null;
483
+ // `?dxrdiag` (or `diag`): the splat handle's recorder, for what a model tile can show — frame
484
+ // cadence, and the live GL objects + content-swap leak watch (./inline3d-splat-diag.js).
485
+ const diagCfg = resolveDiag(opts.diag);
486
+ const diag = diagCfg.on ? new DiagRecorder({ switches: diagCfg.switches, label: 'model' }) : null;
487
+ let diagLoop = null;
482
488
 
483
489
  Object.assign(out, {
484
490
  backend: 'playcanvas',
@@ -514,6 +520,8 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
514
520
  },
515
521
  remove() {
516
522
  removed = true;
523
+ diagLoop?.remove();
524
+ diag?.dispose();
517
525
  depth.dispose();
518
526
  handle?.remove();
519
527
  viewer.dispose(); // app.destroy(): the model, its textures and any page entities go with it
@@ -528,6 +536,14 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
528
536
  await out.ready?.catch?.(() => null);
529
537
  return removed ? 0 : viewer.warmEntity(entity ?? out.model, o);
530
538
  },
539
+ /**
540
+ * Destroy `entity` (content a page added under handle.engine.root) and release the programs
541
+ * its materials linked, and its container `asset` when nothing else draws from it —
542
+ * PlayCanvasSplatViewer.releaseEntity. Returns { programs, container }.
543
+ */
544
+ releaseEntity(entity, o) {
545
+ return removed ? { programs: 0, container: null } : viewer.releaseEntity(entity, o);
546
+ },
531
547
  });
532
548
 
533
549
  // The per-eye buffer scale in force (the request times the device-limit clamp,
@@ -550,8 +566,15 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
550
566
  });
551
567
  if (pageMode) depth.ignore("controls:'page' (the page's camera rig)");
552
568
 
569
+ const onFrame = diag
570
+ ? (views, layer, frame) => {
571
+ const t = performance.now();
572
+ viewer.onFrame(views, layer, frame);
573
+ diag.frame(views, t);
574
+ }
575
+ : viewer.onFrame;
553
576
  if (wall && wall.supported) {
554
- handle = wall.addScene(canvas, viewer.onFrame, {
577
+ handle = wall.addScene(canvas, onFrame, {
555
578
  ...(pageMode
556
579
  ? { viewRig: pageViewRig({ verticalFovDeg: viewer.page.fov, convergence: 2, comfortDepth: ctl.comfortDepth }) }
557
580
  : { virtualDisplayHeight }),
@@ -575,6 +598,12 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
575
598
  delete out._resolveFirstWoven;
576
599
  }
577
600
 
601
+ if (diag) {
602
+ diagLoop = startDiagLoop(diag, { overlay: !diag.has('nooverlay') && !!handle });
603
+ if (diagLoop?.el) handle?.exclude?.(diagLoop.el);
604
+ registerDiag(diag);
605
+ }
606
+
578
607
  for (const [name, args] of pending) {
579
608
  if (name === 'remove') {
580
609
  out.remove();
@@ -637,8 +666,13 @@ export function attachPlayCanvasModel(out, wall, canvas, src, opts, pending = []
637
666
  }
638
667
  const wanted = PC_DECODER_KINDS.filter((k) => (k === 'meshopt' && meshoptDecoder) || (declared ? declared.has(PC_DECODERS[k].ext) : false));
639
668
 
640
- const app = await viewer.attachEngine(pc, { preserveDrawingBuffer, antialias, patchSplats: false });
669
+ // `?dxrdiag=keepshaders`: the engine's shader cache as it ships (A/B; see ./inline3d-pc-shader-cache.js).
670
+ const shaderCacheRelease = !diag?.has('keepshaders');
671
+ // `?dxrdiag=keepprograms`: destroyed materials keep their programs (A/B; ./inline3d-pc-auto-release.js).
672
+ const programRelease = !diag?.has('keepprograms');
673
+ const app = await viewer.attachEngine(pc, { preserveDrawingBuffer, antialias, patchSplats: false, shaderCacheRelease, programRelease });
641
674
  if (!app || removed) return out;
675
+ if (diag) watchContent(diag, app, viewer);
642
676
  out.engine = Object.freeze({ app, root: viewer.content, camera: viewer.eye || null });
643
677
 
644
678
  // Lighting before the model, so the first frame the model is in is lit.