@displayxr/inline3d 1.47.5 → 1.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +143 -1
- package/README.md +7 -1
- package/call.d.ts +98 -8
- package/camera.d.ts +34 -5
- package/dist/call.js +8 -6
- package/docs/content-lifecycle-rules.md +270 -0
- package/docs/woven-canvas-rules.md +25 -13
- package/js/call/element.js +7 -3
- package/js/call/options.js +20 -3
- package/js/call/transport.js +11 -1
- package/js/call/ui.js +8 -1
- package/js/call/wire.js +163 -6
- package/js/camera/capture.js +107 -20
- package/js/camera/conv-meter.js +152 -0
- package/js/inline3d-call.js +392 -71
- package/js/inline3d-camera.js +29 -6
- package/js/inline3d-model-entry.js +3 -0
- package/js/inline3d-model-playcanvas.js +36 -2
- package/js/inline3d-pc-auto-release.js +191 -0
- package/js/inline3d-pc-release.js +107 -0
- package/js/inline3d-pc-shader-cache.js +172 -0
- package/js/inline3d-playcanvas-engine.js +2 -1
- package/js/inline3d-player.js +8 -4
- package/js/inline3d-splat-deferred.js +2 -0
- package/js/inline3d-splat-diag.js +125 -4
- package/js/inline3d-splat-playcanvas.js +40 -3
- package/js/inline3d-splat.js +2 -0
- package/llms.txt +1 -0
- package/model.d.ts +12 -0
- package/package.json +12 -1
- package/signaling/README.md +289 -0
- package/signaling/dev-server.mjs +320 -0
- package/signaling/gate.mjs +112 -0
- package/signaling/keys.mjs +123 -0
- package/signaling/limits.mjs +221 -0
- package/signaling/room.mjs +322 -0
- package/signaling/turn-budget.mjs +254 -0
- package/splat.d.ts +12 -0
|
@@ -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.
|
|
@@ -196,14 +196,23 @@ tile while its content keeps running (for a video: sound, no picture). To put bl
|
|
|
196
196
|
tile, give it to an element **behind** the canvas. A cover you mean to hide the tile with (rule 5,
|
|
197
197
|
rule 11) is the one deliberate exception, and it comes off on a signal.
|
|
198
198
|
|
|
199
|
-
### 10.
|
|
200
|
-
|
|
201
|
-
Rule 7 covers the canvas and its ancestors. This one covers the **siblings on
|
|
202
|
-
control bar, a caption, a badge, a menu. `backdrop-filter` is defined as a function
|
|
203
|
-
behind the element, and behind it is the interlaced weave.
|
|
204
|
-
and smears them, and the 3D image is
|
|
205
|
-
|
|
206
|
-
|
|
199
|
+
### 10. `backdrop-filter` over the weave: only on DisplayXR Browser 0.1.16+, never over a whole tile
|
|
200
|
+
|
|
201
|
+
Rule 7 covers the canvas and its ancestors, and is unchanged. This one covers the **siblings on
|
|
202
|
+
top of it**: a control bar, a caption, a badge, a menu. `backdrop-filter` is defined as a function
|
|
203
|
+
of what is behind the element, and behind it is the interlaced weave. On browsers before
|
|
204
|
+
DisplayXR Browser 0.1.16 the blur reads the interlaced pixels and smears them, and the 3D image is
|
|
205
|
+
disturbed across the element's **whole box**, not only where it is opaque (seen on the panel, 24
|
|
206
|
+
September). From 0.1.16 the compositor draws the tile **mono inside each frost rect**
|
|
207
|
+
(displayxr-browser#120), so frosted chrome over a tile looks right: the frosted region is flat,
|
|
208
|
+
the rest of the tile stays 3D. [`samples/composition`](../samples/composition/) (its sticky
|
|
209
|
+
header) is the validated case.
|
|
210
|
+
|
|
211
|
+
- Frost a **partial** region only (a bar, a pill). Never frost a whole tile while it is visible:
|
|
212
|
+
the whole tile goes mono under it.
|
|
213
|
+
- If the page must also run on older browsers, use a plain translucent tint
|
|
214
|
+
(`background: rgba(0, 0, 0, 0.55)`) there. The SDK's own chrome (player, call) keeps its
|
|
215
|
+
near-solid tint so one build works on every generation.
|
|
207
216
|
|
|
208
217
|
### 11. Treat a resize as a fresh-canvas moment
|
|
209
218
|
|
|
@@ -331,11 +340,14 @@ for exactly that reason). It cannot check the join. On a DisplayXR display:
|
|
|
331
340
|
Found on hardware in a production photo app after the rules above were written. Each one looked
|
|
332
341
|
like a runtime or browser bug on the panel and turned out to be the page.
|
|
333
342
|
|
|
334
|
-
-
|
|
335
|
-
and its ancestors. A frosted bar or button placed *over* a woven tile is a
|
|
336
|
-
on
|
|
337
|
-
|
|
338
|
-
|
|
343
|
+
- **`backdrop-filter` on chrome that sits over the weave (superseded for 0.1.16+).** Rule 7 covers
|
|
344
|
+
effects on the canvas and its ancestors. A frosted bar or button placed *over* a woven tile is a
|
|
345
|
+
different element; on browsers before DisplayXR Browser 0.1.16 it visibly disturbed the 3D image
|
|
346
|
+
across that element's whole rect on a lenticular laptop panel, and the fix was a plain
|
|
347
|
+
translucent tint. Since 0.1.16 the compositor draws the tile mono inside each frost rect
|
|
348
|
+
(displayxr-browser#120), so partial frosted chrome is fine there (`samples/composition` is the
|
|
349
|
+
validated case); still never frost a whole visible tile ([rule 10](#10-backdrop-filter-over-the-weave-only-on-displayxr-browser-0116-never-over-a-whole-tile)).
|
|
350
|
+
Judge it on the panel, not on a 2D monitor.
|
|
339
351
|
- **Nothing opaque above the woven canvas.** Stacking order still applies: an element with an
|
|
340
352
|
opaque background painted *above* the canvas hides it, and on a draw-order browser that is
|
|
341
353
|
exactly what you get. The classic case is a full-bleed video plane under a page `<main>` with
|
package/js/call/element.js
CHANGED
|
@@ -37,7 +37,7 @@ import { mountCall, dxrSignaling } from '../inline3d-call.js';
|
|
|
37
37
|
|
|
38
38
|
/** Every call event is re-dispatched as `dxr-call:<type>`; `dxr-call:ready` carries the handle. */
|
|
39
39
|
export const CALL_EVENT_PREFIX = 'dxr-call:';
|
|
40
|
-
const CALL_EVENTS = ['joined', 'left', 'peer', 'peerleft', 'state', 'display', 'speaker', 'quality', 'error', 'warning'];
|
|
40
|
+
const CALL_EVENTS = ['joined', 'left', 'peer', 'peerleft', 'state', 'display', 'speaker', 'quality', 'error', 'warning', 'name', 'message', 'signal', 'weave', 'speaking'];
|
|
41
41
|
const TAG = '[inline3d/call] <dxr-call>';
|
|
42
42
|
|
|
43
43
|
/** A boolean attribute: present is true, unless its value spells "false"/"0"/"off"/"no". */
|
|
@@ -71,8 +71,10 @@ const str = (v) => (v === null || v === undefined || String(v).trim() === '' ? u
|
|
|
71
71
|
* | `no-audio` | `audio: false` |
|
|
72
72
|
* | `no-self-view` | `selfView: false` |
|
|
73
73
|
* | `no-auto-converge` | `autoConverge: false` |
|
|
74
|
+
* | `no-send-convergence`| `sendConvergence: false` |
|
|
74
75
|
* | `cursor="depth"` | `cursor: 'depth'` |
|
|
75
|
-
* | `tile-aspect` | `theme.tileAspect` (a number,
|
|
76
|
+
* | `tile-aspect` | `theme.tileAspect` (a number, `w/h`, or `fill`) |
|
|
77
|
+
* | `name` | `name` (your display name) |
|
|
76
78
|
* | `invite-base` | `invite.base` |
|
|
77
79
|
* | `browser-url` | `landing.browserUrl` |
|
|
78
80
|
* | `debug` | `debug: true` |
|
|
@@ -102,9 +104,11 @@ export function attrsToOpts(source) {
|
|
|
102
104
|
if (isOn(get('no-audio'))) o.audio = false;
|
|
103
105
|
if (isOn(get('no-self-view'))) o.selfView = false;
|
|
104
106
|
if (isOn(get('no-auto-converge'))) o.autoConverge = false;
|
|
107
|
+
if (isOn(get('no-send-convergence'))) o.sendConvergence = false;
|
|
105
108
|
if (str(get('cursor')) === 'depth') o.cursor = 'depth';
|
|
106
109
|
// The C2 groups (RFC 0003 §2): the attribute names are the C1 ones, the option shapes are new.
|
|
107
|
-
|
|
110
|
+
put('name', str(get('name')));
|
|
111
|
+
const theme = { accent: str(get('accent')), tileAspect: str(get('tile-aspect')) === 'fill' ? 'fill' : num(get('tile-aspect')) };
|
|
108
112
|
if (theme.accent !== undefined || theme.tileAspect !== undefined) o.theme = Object.fromEntries(Object.entries(theme).filter(([, v]) => v !== undefined));
|
|
109
113
|
if (str(get('invite-base'))) o.invite = { base: str(get('invite-base')) };
|
|
110
114
|
if (str(get('browser-url'))) o.landing = { browserUrl: str(get('browser-url')) };
|
package/js/call/options.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// 1.29 spellings accepted for one release with a warning. Pure (no DOM, no network) — see
|
|
4
4
|
// test/call.test.mjs. Internal: importable by path, not part of the `./call` entry.
|
|
5
5
|
|
|
6
|
-
import { isValidRoomId, parseInviteLink, DEFAULT_MAX_PEERS } from './wire.js';
|
|
6
|
+
import { isValidRoomId, parseInviteLink, DEFAULT_MAX_PEERS, sanitizeName } from './wire.js';
|
|
7
7
|
import { clampMaxPeers } from './transport.js';
|
|
8
8
|
import { normalizeMono3D } from './lift.js';
|
|
9
9
|
import { dxrSignaling } from './signaling.js';
|
|
@@ -53,7 +53,10 @@ export function normalizeCallOptions(opts = {}) {
|
|
|
53
53
|
const ui = opts.ui === undefined ? true : opts.ui === 'tiles' ? 'tiles' : !!opts.ui;
|
|
54
54
|
const chrome = ui === true;
|
|
55
55
|
const tileAspect = theme.tileAspect !== undefined ? theme.tileAspect : opts.tileAspect;
|
|
56
|
-
|
|
56
|
+
// 'fill': each tile's per-eye aspect is its own stage box (the page sizes the tiles).
|
|
57
|
+
const aspect = tileAspect === 'fill' ? 'fill' : typeof tileAspect === 'number' && tileAspect > 0.3 && tileAspect < 4 ? tileAspect : 16 / 9;
|
|
58
|
+
const layout = opts.layout === 'speaker' ? 'speaker' : opts.layout === 'none' ? 'none' : 'grid';
|
|
59
|
+
const tileCover = opts.tileCover === 'auto' || opts.tileCover === 'off' ? opts.tileCover : layout === 'none' ? 'auto' : 'off';
|
|
57
60
|
let room = opts.room === undefined || opts.room === null ? 'auto' : opts.room;
|
|
58
61
|
if (room !== 'auto' && !isValidRoomId(room)) {
|
|
59
62
|
const fromLink = parseInviteLink(room);
|
|
@@ -90,6 +93,10 @@ export function normalizeCallOptions(opts = {}) {
|
|
|
90
93
|
// peer's eyes and shift the eyes so it sits at the display plane. The depth slider stays an
|
|
91
94
|
// offset on top. Off = the pair as sent (plus any `hint`).
|
|
92
95
|
autoConverge: opts.autoConverge === undefined ? true : !!opts.autoConverge,
|
|
96
|
+
// Sender side of auto-convergence (docs/call.md): with a stereo camera, measure that disparity
|
|
97
|
+
// on this side's own raw frames and send it to every peer (`conv` on the data channel), so
|
|
98
|
+
// receivers apply it instead of measuring the compressed frames. Off = receivers measure.
|
|
99
|
+
sendConvergence: opts.sendConvergence === undefined ? true : !!opts.sendConvergence,
|
|
93
100
|
// `cursor: 'depth'` (runtime ADR-046 on a pair): the arrow comes in front of a person who pops
|
|
94
101
|
// out of the screen under it, measured off each stereo tile's two halves; else the plain OS
|
|
95
102
|
// arrow on the plane. Off by default (zero cost).
|
|
@@ -102,7 +109,15 @@ export function normalizeCallOptions(opts = {}) {
|
|
|
102
109
|
// Concurrent lifted tiles (`liftOptions.max`). Default = maxPeers (4): every mono peer can be lifted.
|
|
103
110
|
maxLifted: Number.isFinite(+maxLiftedRaw) && maxLiftedRaw !== null && maxLiftedRaw !== undefined ? Math.max(0, Math.min(DEFAULT_MAX_PEERS, Math.floor(+maxLiftedRaw))) : DEFAULT_MAX_PEERS,
|
|
104
111
|
// 'grid' | 'speaker' | 'none' (the page lays the tiles out; handle.tile(id) finds them).
|
|
105
|
-
layout
|
|
112
|
+
layout,
|
|
113
|
+
// The core's rect cover on every woven tile (and a stereo self view): 'auto' covers a tile
|
|
114
|
+
// across a resize or move until it is woven again. Default 'auto' with layout: 'none' (the
|
|
115
|
+
// page moves and resizes the tiles), 'off' otherwise (the module's own layout).
|
|
116
|
+
tileCover,
|
|
117
|
+
// This side's display name (hello.name), sanitised; null = none.
|
|
118
|
+
name: sanitizeName(opts.name),
|
|
119
|
+
// Devices no camera probe opens (openCamera's `exclude`); false = not even the built-in list.
|
|
120
|
+
cameraExclude: opts.cameraExclude === false ? false : Array.isArray(opts.cameraExclude) ? [...opts.cameraExclude] : undefined,
|
|
106
121
|
// true: full chrome | 'tiles': badges + plates only | false: nothing. `chrome` = the full set.
|
|
107
122
|
ui,
|
|
108
123
|
chrome,
|
|
@@ -124,6 +139,8 @@ export function normalizeCallOptions(opts = {}) {
|
|
|
124
139
|
log: typeof opts.log === 'function' ? opts.log : opts.debug ? (tag, obj) => console.log(`${TAG} ${tag} ${JSON.stringify(obj)}`) : null,
|
|
125
140
|
// Internal (tests): replaces the lift module importer behind `mono3D: 'auto'`.
|
|
126
141
|
_liftImporter: typeof opts._liftImporter === 'function' ? opts._liftImporter : undefined,
|
|
142
|
+
// Internal (tests): replaces the sender's frame grab (camera/conv-meter.js grabLumaFrame).
|
|
143
|
+
_convGrab: typeof opts._convGrab === 'function' ? opts._convGrab : undefined,
|
|
127
144
|
};
|
|
128
145
|
}
|
|
129
146
|
|
package/js/call/transport.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// transport.start({ room, localStream, onPeer, onPeerLeft, onStream, onMessage, onPeerState })
|
|
8
8
|
// transport.send(peerId, msg) / broadcast(msg)
|
|
9
|
-
// transport.
|
|
9
|
+
// transport.replaceTrack(kind, track, stream) / setSendFormat(format)
|
|
10
10
|
// transport.peerConnection(peerId) // for getStats (null on an SFU)
|
|
11
11
|
// transport.stop()
|
|
12
12
|
//
|
|
@@ -501,6 +501,16 @@ export class MeshTransport {
|
|
|
501
501
|
for (const pid of this.peers.keys()) this.send(pid, msg);
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
+
/** An already-serialised message (the app channel: sized before it is sent). */
|
|
505
|
+
sendRaw(pid, json) {
|
|
506
|
+
const rec = this.peers.get(pid);
|
|
507
|
+
if (rec && rec.dc && rec.dc.readyState === 'open') {
|
|
508
|
+
rec.dc.send(json);
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
|
|
504
514
|
peerConnection(pid) {
|
|
505
515
|
const rec = this.peers.get(pid);
|
|
506
516
|
return rec ? rec.pc : null;
|
package/js/call/ui.js
CHANGED
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
// pill, never a plate congruent with the tile (it would match the canvas's own quad on a
|
|
9
9
|
// legacy browser and take the tile out of the weave). The bottom bar and self view live in a
|
|
10
10
|
// footer BELOW the grid, over no tile at all.
|
|
11
|
-
// 2. NO `backdrop-filter
|
|
11
|
+
// 2. NO `backdrop-filter` in the SDK's own chrome. The near-solid `--dxr-shell`
|
|
12
|
+
// (rgba(16,17,22,.92)) is its choice, and it stays. A frosted element OVER a tile is no longer
|
|
13
|
+
// a defect on DisplayXR Browser 0.1.16+ (browser#120: the compositor draws the tile mono
|
|
14
|
+
// inside each frost rect; samples/composition is the validated case) — a page's own chrome may
|
|
15
|
+
// use it. Never frost a WHOLE visible tile, and rule 3 (effects on the canvas or an ancestor)
|
|
16
|
+
// is unchanged.
|
|
12
17
|
// 3. Promotion is `will-change: transform`. No CSS filter / opacity / border-radius / shadow on a
|
|
13
18
|
// woven canvas OR ANY ANCESTOR of one — the tile, the grid and the host stay visually bare.
|
|
14
19
|
// A speaking highlight is a separate bar UNDER the stage, not an outline around it.
|
|
@@ -147,6 +152,8 @@ const CSS = `
|
|
|
147
152
|
aside (display:contents) so each [data-dxr-peer] tile is laid out by the PAGE's own CSS on the
|
|
148
153
|
host. A tile is never moved; it is styled in place (woven-canvas rule 2). */
|
|
149
154
|
.dxr-call-grid[data-layout="none"]{display:contents;}
|
|
155
|
+
/* …and so do the footer and the self slot, so the page places the self tile ([data-dxr-peer="self"]). */
|
|
156
|
+
.dxr-call-host--none .dxr-call-foot,.dxr-call-host--none .dxr-call-self{display:contents;}
|
|
150
157
|
.dxr-call-tile{position:relative;min-width:0;}
|
|
151
158
|
.dxr-call-stage{position:relative;background:#000;}
|
|
152
159
|
.dxr-call-stage>canvas{display:block;width:100%;height:100%;}
|