@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.
@@ -34,6 +34,19 @@
34
34
  // located them, and a live outgoing photo goes back on the pre-1.24 node chain (which
35
35
  // scales its disparity by the two rigs' window ratio at the swap). A/B for "the
36
36
  // outgoing photo jumps when the incoming rig is declared".
37
+ // keepshaders — leave the engine's compiled-shader cache as it ships (no per-program release,
38
+ // ./inline3d-pc-shader-cache.js). Page-wide: the engine's shader class is shared.
39
+ // keepprograms — a material the page destroys keeps the programs it linked, as the engine ships
40
+ // (./inline3d-pc-auto-release.js). Per tile.
41
+ //
42
+ // LIVE GL OBJECTS (docs/content-lifecycle-rules.md): programs and shaders created / deleted on the
43
+ // tile's context since diag attached, and the live difference, on the overlay and in the dump.
44
+ // Sampled at every content swap the SDK can see: each setSource, each glTF container the tile's
45
+ // asset registry loads (a page swapping meshes under handle.engine.root), each
46
+ // handle.releaseEntity. When the live program count has not dropped across the last LEAK_SWAPS
47
+ // swaps of one kind and rose at more than 60% of them, one console warning says so. A page that
48
+ // releases what a swap replaces goes up and down, or plateaus once its own transitions are
49
+ // compiled (a few rises, then flat); one that leaks climbs.
37
50
  //
38
51
  // Per frame, `rigAt` / `rigIn` / `rigOut` say which declared rig the views were located for
39
52
  // (the tracker's id; null = none matched), which rig the current photo was drawn through
@@ -52,8 +65,28 @@
52
65
  // - the settle itself: the SDK's settle work in ms, then how long the SAME task kept running
53
66
  // after it (the page's continuation of `await setSource`).
54
67
 
68
+ import { autoReleaseStats, autoReleaseState } from './inline3d-pc-auto-release.js';
69
+
55
70
  /** The switches `diag` / `?dxrdiag` understand, besides the plain on values. */
56
- export const DIAG_SWITCHES = Object.freeze(['norig', 'frozen', 'nowarm', 'cold', 'nooverlay', 'oldpick', 'nolayerrig', 'oldrig']);
71
+ export const DIAG_SWITCHES = Object.freeze(['norig', 'frozen', 'nowarm', 'cold', 'nooverlay', 'oldpick', 'nolayerrig', 'oldrig', 'keepshaders', 'keepprograms']);
72
+ /**
73
+ * The leak watch's window: content swaps of one kind with no drop in live programs, more than 60%
74
+ * of them a rise. Wide enough that a first pass through the transitions (each compiles its own
75
+ * programs once: four rises for cut / crossfade / wavefront / flip) does not trip it.
76
+ */
77
+ export const LEAK_SWAPS = 10;
78
+
79
+ /** The leak watch's rule on one kind's samples (oldest first): the warning's condition. */
80
+ export function looksLikeLeak(samples, n = LEAK_SWAPS) {
81
+ if (samples.length < n + 1) return false;
82
+ const w = samples.slice(-(n + 1));
83
+ let rises = 0;
84
+ for (let i = 1; i < w.length; i++) {
85
+ if (w[i] < w[i - 1]) return false;
86
+ if (w[i] > w[i - 1]) rises++;
87
+ }
88
+ return rises > n * 0.6;
89
+ }
57
90
  const ON_TOKENS = new Set(['1', 'on', 'true', 'yes']);
58
91
 
59
92
  /**
@@ -207,6 +240,12 @@ export class DiagRecorder {
207
240
  this._listeners = new Set();
208
241
  this._lafo = null;
209
242
  this._gl = null; // the wrapped context + its originals, for dispose()
243
+ /** GL object counts on the tile's context (instrumentGl): programs / shaders created + deleted. */
244
+ this.glObjects = { programs: { created: 0, deleted: 0 }, shaders: { created: 0, deleted: 0 } };
245
+ this.shaderCacheState = null; // () => the engine shader-cache patch state (./inline3d-pc-shader-cache.js)
246
+ this.programReleaseState = null; // () => { state, released, … } (./inline3d-pc-auto-release.js)
247
+ this._swapLive = new Map(); // kind → live programs at each content swap of that kind (the leak watch)
248
+ this._leakWarned = false;
210
249
  this._burst = null; // the pick burst of the current task
211
250
  }
212
251
 
@@ -266,6 +305,25 @@ export class DiagRecorder {
266
305
  */
267
306
  instrumentGl(gl) {
268
307
  if (!gl || this._gl) return;
308
+ // Object counts first (no timing): what a content swap leaves behind.
309
+ const objs = this.glObjects;
310
+ const counters = {
311
+ createProgram: (r) => r && objs.programs.created++,
312
+ deleteProgram: (r, a) => a[0] && objs.programs.deleted++,
313
+ createShader: (r) => r && objs.shaders.created++,
314
+ deleteShader: (r, a) => a[0] && objs.shaders.deleted++,
315
+ };
316
+ const counted = [];
317
+ for (const [name, count] of Object.entries(counters)) {
318
+ const orig = gl[name];
319
+ if (typeof orig !== 'function') continue;
320
+ gl[name] = function (...a) {
321
+ const r = orig.apply(this, a);
322
+ count(r, a);
323
+ return r;
324
+ };
325
+ counted.push(name);
326
+ }
269
327
  const LINK = 0x8b82;
270
328
  const COMPLETION = 0x91b1;
271
329
  const kinds = {
@@ -294,7 +352,37 @@ export class DiagRecorder {
294
352
  };
295
353
  saved.push(name);
296
354
  }
297
- this._gl = { gl, saved };
355
+ this._gl = { gl, saved: [...counted, ...saved] };
356
+ }
357
+
358
+ /** Live programs / shaders on the tile's context since instrumentGl (created − deleted). */
359
+ liveGl() {
360
+ const { programs: p, shaders: s } = this.glObjects;
361
+ return { programs: p.created - p.deleted, shaders: s.created - s.deleted };
362
+ }
363
+
364
+ /**
365
+ * A content swap happened (`kind`: 'setSource' at its start, 'container' at a glTF load,
366
+ * 'release' after a releaseEntity): sample the live program count, and warn once when the
367
+ * samples of that kind look like a leak (looksLikeLeak).
368
+ */
369
+ contentSwap(kind) {
370
+ if (!this._gl) return null;
371
+ const live = this.liveGl();
372
+ let w = this._swapLive.get(kind);
373
+ if (!w) this._swapLive.set(kind, (w = []));
374
+ w.push(live.programs);
375
+ if (w.length > LEAK_SWAPS + 1) w.shift();
376
+ this._event('swap', { kind, ...live }, this.now());
377
+ if (!this._leakWarned && looksLikeLeak(w)) {
378
+ this._leakWarned = true;
379
+ console.warn(
380
+ `[inline3d/${this.label.split('#')[0]}] diag: live WebGL programs never dropped across the last ${LEAK_SWAPS} content swaps ` +
381
+ `and rose at most of them (${kind}: ${w.join(' → ')}). Something each swap replaces is not released — see docs/content-lifecycle-rules.md ` +
382
+ '(handle.releaseEntity for content under handle.engine.root). Warned once.',
383
+ );
384
+ }
385
+ return live;
298
386
  }
299
387
 
300
388
  /** One GL call of `kind` that took `ms`, at `t` (instrumentGl's wrappers; tests call it directly). */
@@ -578,6 +666,7 @@ export class DiagRecorder {
578
666
  label: this.label,
579
667
  switches: [...this.switches],
580
668
  rigLocked: this.rigLocked,
669
+ glObjects: { ...this.glObjects, live: this.liveGl(), shaderCache: safe(() => this.shaderCacheState?.()) ?? null, programRelease: safe(() => this.programReleaseState?.()) ?? null },
581
670
  transitions: this.transitions,
582
671
  events: this.events,
583
672
  frames: this.frames,
@@ -794,7 +883,8 @@ export function startDiagLoop(rec, { overlay = true, doc = typeof document !== '
794
883
  `dxr-diag ${rec.label} phase ${rec.phase}` +
795
884
  (Number.isFinite(rec.callAt) ? ` +${Math.round(now - rec.callAt)} ms since setSource` : '') +
796
885
  (rec.switches.size ? ` [${[...rec.switches].join(',')}]` : '') +
797
- (last ? `\nframe dt ${last.dt} ms held ${last.held ? 'YES' : 'no'} img ${last.img}${last.img !== 'none' ? ' ' + last.imgW : ''}` : '');
886
+ (last ? `\nframe dt ${last.dt} ms held ${last.held ? 'YES' : 'no'} img ${last.img}${last.img !== 'none' ? ' ' + last.imgW : ''}` : '') +
887
+ (rec._gl ? `\n${glObjectsLine(rec)}` : '');
798
888
  strip(rec.frames, 0, now - WINDOW_MS, now, rec.callAt);
799
889
  if (frozen && g) {
800
890
  strip(frozen.window.map((f) => ({ ...f })), STRIP_H + 18, -PRE_MS, WINDOW_MS - PRE_MS, 0);
@@ -818,6 +908,37 @@ export function startDiagLoop(rec, { overlay = true, doc = typeof document !== '
818
908
  };
819
909
  }
820
910
 
911
+ /**
912
+ * The live-GL-object watch on a PlayCanvas tile: count the context's programs and shaders, show
913
+ * the engine shader-cache patch state, and sample at the content swaps the SDK can see (a
914
+ * container load in the tile's registry, a releaseEntity). setSource samples itself.
915
+ */
916
+ export function watchContent(rec, app, viewer) {
917
+ rec.instrumentGl(app?.graphicsDevice?.gl);
918
+ rec.shaderCacheState = () => app?.graphicsDevice?.__dxrShaderCache ?? null;
919
+ rec.programReleaseState = () => {
920
+ const state = app?.graphicsDevice?.__dxrAutoRelease ?? null;
921
+ const st = autoReleaseStats(app);
922
+ return state ? { state, ...(st || {}) } : null;
923
+ };
924
+ const ar = autoReleaseState(app);
925
+ if (ar) ar.onSweep = (n) => n && rec.contentSwap('auto-release');
926
+ if (viewer) viewer.onContentReleased = () => rec.contentSwap('release');
927
+ app?.assets?.on?.('load', (a) => a?.type === 'container' && rec.contentSwap('container'));
928
+ }
929
+
930
+ /** The overlay's GL objects line: live programs and shaders (created / deleted), the cache patch. */
931
+ export function glObjectsLine(rec) {
932
+ const { programs: p, shaders: s } = rec.glObjects;
933
+ const cache = safe(() => rec.shaderCacheState?.());
934
+ const rel = safe(() => rec.programReleaseState?.());
935
+ return (
936
+ `gl programs ${p.created - p.deleted} live (+${p.created} −${p.deleted}) shaders ${s.created - s.deleted} live (+${s.created} −${s.deleted})` +
937
+ (cache ? ` cache ${cache}` : '') +
938
+ (rel ? ` release ${rel.state}${rel.released !== undefined ? ` (−${rel.released})` : ''}` : '')
939
+ );
940
+ }
941
+
821
942
  /**
822
943
  * `window.__dxrDiag`: every diag tile on the page. `dump()` is the JSON to paste back;
823
944
  * `last` the most recent tile's recorder; `transitions` every tile's transition summaries.
@@ -839,7 +960,7 @@ export function registerDiag(rec) {
839
960
  },
840
961
  };
841
962
  }
842
- rec.label = `splat#${g.tiles.length + 1}`;
963
+ rec.label = `${rec.label || 'splat'}#${g.tiles.length + 1}`;
843
964
  g.tiles.push(rec);
844
965
  const dispose = rec.dispose.bind(rec);
845
966
  rec.dispose = () => {
@@ -73,7 +73,7 @@ import { viewerEaseFor, frameTrackingState } from './inline3d-viewer-ease.js';
73
73
  import { VideoPlane, validateSetVideo, PAGE_VIDEO_ERROR, eyeSplit, EYE_SPLIT_UNIFORM, makeSbsMaterial } from './inline3d-splat-video.js';
74
74
  import { LayerRigCameras, validateLayerRig, validateLayerRigOptions, DEFAULT_VIEWER_DISTANCE_M } from './inline3d-splat-layer-rig.js';
75
75
  import { RigTracker, remapViews, nodePose, sameRig } from './inline3d-splat-rig-map.js';
76
- import { resolveDiag, DiagRecorder, startDiagLoop, registerDiag, DIAG_SWITCHES } from './inline3d-splat-diag.js';
76
+ import { resolveDiag, DiagRecorder, startDiagLoop, registerDiag, watchContent, DIAG_SWITCHES } from './inline3d-splat-diag.js';
77
77
  import {
78
78
  glBufferLimits,
79
79
  probeBufferLimits,
@@ -127,6 +127,9 @@ import {
127
127
  framedSubjectBounds,
128
128
  } from './inline3d-splat-shared.js';
129
129
  import { resolveCursorOption } from './inline3d-cursor-option.js';
130
+ import { installShaderCacheRelease } from './inline3d-pc-shader-cache.js';
131
+ import { installAutoProgramRelease } from './inline3d-pc-auto-release.js';
132
+ import { releaseEntity as releaseEntityIn } from './inline3d-pc-release.js';
130
133
 
131
134
  /**
132
135
  * The component systems the tile's `AppBase` registers. Camera + GSplat draw the splat; Render,
@@ -1284,6 +1287,18 @@ export class PlayCanvasSplatViewer {
1284
1287
  return shaders.size;
1285
1288
  }
1286
1289
 
1290
+ /**
1291
+ * Destroy `entity` (content a page added under `engine.root`) and release the programs its
1292
+ * materials linked, plus its glTF container (`asset`) when nothing else draws from it —
1293
+ * ./inline3d-pc-release.js, docs/content-lifecycle-rules.md. Returns { programs, container }.
1294
+ */
1295
+ releaseEntity(entity, { asset = null } = {}) {
1296
+ if (!this.app || this._disposed) return { programs: 0, container: null };
1297
+ const r = releaseEntityIn(this.app, entity, { asset });
1298
+ this.onContentReleased?.(r);
1299
+ return r;
1300
+ }
1301
+
1287
1302
  useSceneColor(on = true) {
1288
1303
  this.sceneColor = !!on;
1289
1304
  const cams = [this.eye, ...(this._viewPath === 'cameras' ? this._views : [])];
@@ -1415,7 +1430,7 @@ export class PlayCanvasSplatViewer {
1415
1430
  * @param {object} o.perf the resolved playcanvasPerfSettings().
1416
1431
  * @param {string} [o.viewPath] force 'cameras' | 'renderview' (diagnostics).
1417
1432
  */
1418
- async attachEngine(pc, { preserveDrawingBuffer = false, perf, viewPath, antialias = false, patchSplats = true } = {}) {
1433
+ async attachEngine(pc, { preserveDrawingBuffer = false, perf, viewPath, antialias = false, patchSplats = true, shaderCacheRelease = true, programRelease = true } = {}) {
1419
1434
  this.pc = pc;
1420
1435
  const device = await pc.createGraphicsDevice(this.canvas, {
1421
1436
  deviceTypes: [pc.DEVICETYPE_WEBGL2],
@@ -1431,6 +1446,11 @@ export class PlayCanvasSplatViewer {
1431
1446
  device.destroy?.();
1432
1447
  return null;
1433
1448
  }
1449
+ // The engine keeps every compiled shader until the device dies, even once its program is
1450
+ // destroyed: count them per program and free the last one out (./inline3d-pc-shader-cache.js).
1451
+ // `?dxrdiag=keepshaders` leaves the engine's cache as it ships (A/B; page-wide, first tile wins).
1452
+ if (shaderCacheRelease) installShaderCacheRelease(device);
1453
+ else device.__dxrShaderCache ??= 'off (keepshaders)';
1434
1454
  const opts = new pc.AppOptions();
1435
1455
  opts.graphicsDevice = device;
1436
1456
  // No xr (AppBase constructs XrManager only when asked, and XrManager is what probes and
@@ -1442,6 +1462,11 @@ export class PlayCanvasSplatViewer {
1442
1462
  const app = new pc.AppBase(this.canvas);
1443
1463
  app.init(opts);
1444
1464
  guardDuplicateSystems(app);
1465
+ // A material the page destroys (entity.destroy, a container's unload) keeps every program it
1466
+ // linked for the device's life: release them a frame later, when nothing else uses them
1467
+ // (./inline3d-pc-auto-release.js). `?dxrdiag=keepprograms` leaves the engine as it ships (A/B).
1468
+ if (programRelease) installAutoProgramRelease(app, pc.Material);
1469
+ else device.__dxrAutoRelease ??= 'off (keepprograms)';
1445
1470
  // The SDK sizes the buffer (double-width in 3D, 1:1 in mono), so the engine must never
1446
1471
  // resize it. RESOLUTION_FIXED is AppBase's DEFAULT, and with it `updateCanvasSize()` is a
1447
1472
  // no-op. Deliberately NOT calling setCanvasResolution/setCanvasFillMode: without explicit
@@ -3583,6 +3608,14 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
3583
3608
  await first.catch(() => null);
3584
3609
  return removed ? 0 : viewer.warmEntity(entity, o);
3585
3610
  },
3611
+ /**
3612
+ * Destroy `entity` (content under handle.engine.root) and release the programs its materials
3613
+ * linked, and its container `asset` when nothing else draws from it — viewer.releaseEntity.
3614
+ * Returns { programs, container }.
3615
+ */
3616
+ releaseEntity(entity, o) {
3617
+ return removed ? { programs: 0, container: null } : viewer.releaseEntity(entity, o);
3618
+ },
3586
3619
  setStereo,
3587
3620
  setDepthEnvelope,
3588
3621
  /**
@@ -4745,12 +4778,15 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
4745
4778
  perf: perfResolved,
4746
4779
  viewPath: opts.playcanvasViewPath,
4747
4780
  antialias: antialias === true,
4781
+ shaderCacheRelease: !diag?.has('keepshaders'),
4782
+ programRelease: !diag?.has('keepprograms'),
4748
4783
  });
4749
4784
  if (!app || removed) return null;
4750
4785
  pcModule = pc;
4751
4786
  // diag: count the GL calls that can block (compile, link, status queries, readbacks, syncs),
4752
4787
  // from before the first asset's compile on
4753
- if (diag) diag.instrumentGl(app.graphicsDevice?.gl);
4788
+ // + the live program / shader counts and the content-swap leak watch
4789
+ if (diag) watchContent(diag, app, viewer);
4754
4790
  /**
4755
4791
  * ADVANCED, not covered by the semver promise: the engine objects behind this window.
4756
4792
  * `app` is the tile's `pc.AppBase`; `root` the content root, in the splat's content space
@@ -5335,6 +5371,7 @@ export function attachPlayCanvasSplat(out, wall, canvas, src, opts, pending = []
5335
5371
  const gen = ++sourceGen;
5336
5372
  if (diag) {
5337
5373
  rigLockArmed = true;
5374
+ diag.contentSwap('setSource'); // the previous swap's outgoing asset is released by now
5338
5375
  diag.setPhase('swap', {
5339
5376
  transition: plan.sequence ? `${plan.sequence.name}(${plan.sequence.out.effect}>${plan.sequence.in.effect})` : plan.transition,
5340
5377
  outgoing: plan.sequence ? 'none (one splat)' : plan.outgoing || 'default',
@@ -447,6 +447,8 @@ function addSplatUnguarded(wall, canvas, src, opts = {}) {
447
447
  },
448
448
  /** PlayCanvas-backend feature (its renderer links synchronously); nothing to warm here. */
449
449
  warmEntity: () => Promise.resolve(0),
450
+ /** PlayCanvas-backend feature: three.js content is the page's to dispose (docs/content-lifecycle-rules.md). */
451
+ releaseEntity: () => ({ programs: 0, container: null }),
450
452
  stopEffect() {
451
453
  throw effectsNotOnSpark('stopEffect()');
452
454
  },
package/llms.txt CHANGED
@@ -15,6 +15,7 @@ detail, a cover that always comes off). The SDK's defaults do the rest.
15
15
  ## Reference
16
16
  - docs/authoring-inline-3d.md — every API, both view rigs, comfort maths
17
17
  - docs/woven-canvas-rules.md — what the browser does to a woven canvas; CSS, remount and cover rules
18
+ - docs/content-lifecycle-rules.md — swapping content in a window: release programs, textures, containers (handle.releaseEntity)
18
19
  - docs/porting-three-js-apps.md — moving an existing three.js app over
19
20
  - docs/authoring-motion-and-effects.md, docs/splat-effects.md — motion, reveals, depth envelopes
20
21
  - index.d.ts, three.d.ts, viewer.d.ts, model.d.ts, splat.d.ts — the public API with JSDoc
package/model.d.ts CHANGED
@@ -283,6 +283,18 @@ export interface ModelHandle {
283
283
  * docs/playcanvas-adapter.md §warmEntity.
284
284
  */
285
285
  warmEntity(entity?: unknown, opts?: { timeoutMs?: number }): Promise<number>;
286
+ /**
287
+ * `engine: 'playcanvas'` only (a no-op returning `{ programs: 0, container: null }` on three).
288
+ * Destroy `entity` — content you added under `handle.engine.root` — and release what it held on
289
+ * the GPU: the shader programs its materials linked (PlayCanvas never frees a program by itself:
290
+ * its ProgramLibrary caches every variant for the device's life, and `Material.destroy()` does
291
+ * not destroy them), and, with `asset`, the glTF container it was instantiated from (unloaded
292
+ * and taken out of the registry unless something else in the tile still draws from it). A
293
+ * program another material still uses is kept. Call it for whatever a content swap replaces.
294
+ * Returns the number of programs destroyed and what happened to the container.
295
+ * docs/content-lifecycle-rules.md.
296
+ */
297
+ releaseEntity(entity: unknown, opts?: { asset?: unknown }): { programs: number; container: 'unloaded' | 'in-use' | null };
286
298
  unexclude(el: Element): void;
287
299
  /**
288
300
  * The core window's `TileHandle.firstWoven`: when it is safe to reveal the canvas. Resolves
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@displayxr/inline3d",
3
- "version": "1.47.5",
3
+ "version": "1.47.6",
4
4
  "description": "Turn any HTML <canvas> into a 3D window on a spatial display, inside an ordinary web page, for the DisplayXR Browser. Dependency-free; progressive enhancement (falls back to plain 2D everywhere else).",
5
5
  "type": "module",
6
6
  "types": "./index.d.ts",
@@ -74,6 +74,7 @@
74
74
  "llms.txt",
75
75
  "docs/making-3d-look-right.md",
76
76
  "docs/woven-canvas-rules.md",
77
+ "docs/content-lifecycle-rules.md",
77
78
  "js/inline3d.js",
78
79
  "js/inline3d-undock.js",
79
80
  "js/inline3d-display-metrics.js",
@@ -108,6 +109,9 @@
108
109
  "js/inline3d-model-entry.js",
109
110
  "js/inline3d-model-playcanvas.js",
110
111
  "js/inline3d-pc-look.js",
112
+ "js/inline3d-pc-auto-release.js",
113
+ "js/inline3d-pc-release.js",
114
+ "js/inline3d-pc-shader-cache.js",
111
115
  "js/inline3d-playcanvas-engine.js",
112
116
  "js/inline3d-player.js",
113
117
  "js/inline3d-camera.js",
package/splat.d.ts CHANGED
@@ -1135,6 +1135,18 @@ export interface SplatHandle {
1135
1135
  * it after this resolves. Resolves the number of programs warmed. docs/playcanvas-adapter.md §warmEntity.
1136
1136
  */
1137
1137
  warmEntity(entity: unknown, opts?: { timeoutMs?: number }): Promise<number>;
1138
+ /**
1139
+ * `engine: 'playcanvas'` only (a no-op returning `{ programs: 0, container: null }` on Spark).
1140
+ * Destroy `entity` — content you added under `handle.engine.root` — and release what it held on
1141
+ * the GPU: the shader programs its materials linked (PlayCanvas never frees a program by itself:
1142
+ * its ProgramLibrary caches every variant for the device's life, and `Material.destroy()` does
1143
+ * not destroy them), and, with `asset`, the glTF container it was instantiated from (unloaded
1144
+ * and taken out of the registry unless something else in the tile still draws from it). A
1145
+ * program another material still uses is kept. Call it for whatever a content swap replaces.
1146
+ * Returns the number of programs destroyed and what happened to the container.
1147
+ * docs/content-lifecycle-rules.md.
1148
+ */
1149
+ releaseEntity(entity: unknown, opts?: { asset?: unknown }): { programs: number; container: 'unloaded' | 'in-use' | null };
1138
1150
  setStereo(opts: SplatStereoOptions | null): SplatHandle;
1139
1151
  /**
1140
1152
  * The `depth` option's comfort factor in force, in (0, 1]: what the display rig's ipdFactor and