@displayxr/inline3d 1.26.0 → 1.28.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 +134 -0
- package/call.d.ts +374 -0
- package/index.d.ts +17 -1
- package/js/call/capture.js +167 -0
- package/js/call/lift.js +314 -0
- package/js/call/qr.js +283 -0
- package/js/call/sdp.js +86 -0
- package/js/call/signaling.js +398 -0
- package/js/call/transport.js +589 -0
- package/js/call/ui.js +146 -0
- package/js/call/wire.js +428 -0
- package/js/inline3d-buffer-limit.js +198 -0
- package/js/inline3d-call.js +1695 -0
- package/js/inline3d-model-playcanvas.js +6 -0
- package/js/inline3d-model.js +6 -0
- package/js/inline3d-player.js +3 -2
- package/js/inline3d-splat-layer-rig.js +79 -7
- package/js/inline3d-splat-playcanvas.js +94 -17
- package/js/inline3d-splat.js +5 -0
- package/js/inline3d-viewer.js +86 -10
- package/js/inline3d.js +127 -14
- package/model.d.ts +10 -1
- package/package.json +9 -1
- package/player.d.ts +3 -2
- package/splat.d.ts +26 -4
- package/viewer.d.ts +12 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// inline3d-buffer-limit.js — keep a woven canvas's backing store inside the device's GL limits.
|
|
2
|
+
//
|
|
3
|
+
// Internal to the SDK (core, ./viewer, ./splat on both engines, ./model on both engines). Not a
|
|
4
|
+
// public entry point; renderer-free and dependency-free, so the core can import it.
|
|
5
|
+
//
|
|
6
|
+
// WHY (measured, an Android 3D tablet on Adreno 740, DisplayXR Browser 154): a full-screen SBS
|
|
7
|
+
// canvas asked for 5120×1348 (2560 per eye) with renderScale 1. The browser reports
|
|
8
|
+
// MAX_TEXTURE_SIZE 4096 there (Chromium's `webgl_or_caps_max_texture_size_limit_4096` driver
|
|
9
|
+
// workaround) and SILENTLY clamps the WebGL drawing buffer to 4096×1348, while `canvas.width` still
|
|
10
|
+
// reads 5120. `XRDisplayLayer.getViewport()` splits `canvas.width` (the attribute), so each eye was
|
|
11
|
+
// drawn 2560 wide into a 4096-wide buffer: the eye boundary landed at 62.5% of the buffer while the
|
|
12
|
+
// weave splits it at 50%, and the panel showed a large double image. Nothing was logged. Windows
|
|
13
|
+
// and Linux report 16384, so the same page was fine there.
|
|
14
|
+
//
|
|
15
|
+
// Two layers of defence, both here:
|
|
16
|
+
// 1. clampEyeBuffer(): shrink the requested per-eye size, UNIFORMLY (the eye aspect is kept), so
|
|
17
|
+
// that the whole store (cols × eyeW by rows × eyeH) fits min(MAX_TEXTURE_SIZE,
|
|
18
|
+
// MAX_RENDERBUFFER_SIZE, MAX_VIEWPORT_DIMS). The browser then never has to clamp.
|
|
19
|
+
// 2. bufferScale(): if the drawing buffer STILL differs from canvas.width/height after a resize
|
|
20
|
+
// (a clamp we did not predict), the caller maps the layer's canvas-space viewports onto the
|
|
21
|
+
// real drawing buffer with these factors — never canvas.width / 2.
|
|
22
|
+
|
|
23
|
+
/** @typedef {{ maxW: number, maxH: number, nameW: string, nameH: string, valueW: number, valueH: number }} BufferLimits */
|
|
24
|
+
|
|
25
|
+
const cache = new WeakMap(); // gl context → BufferLimits
|
|
26
|
+
let probed; // undefined = not tried; null = no WebGL available; else BufferLimits
|
|
27
|
+
|
|
28
|
+
/** Smallest of the named candidates, with its name (the first one wins a tie). */
|
|
29
|
+
function smallest(cands) {
|
|
30
|
+
let best = null;
|
|
31
|
+
for (const [name, v] of cands) {
|
|
32
|
+
if (!(v > 0)) continue;
|
|
33
|
+
if (!best || v < best[1]) best = [name, v];
|
|
34
|
+
}
|
|
35
|
+
return best;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The largest backing store this GL context can hold and render into, queried once per context
|
|
40
|
+
* and cached. Returns null for anything that is not a GL context (or a context lost mid-query).
|
|
41
|
+
*
|
|
42
|
+
* @param {WebGLRenderingContext|WebGL2RenderingContext|null|undefined} gl
|
|
43
|
+
* @returns {BufferLimits|null}
|
|
44
|
+
*/
|
|
45
|
+
export function glBufferLimits(gl) {
|
|
46
|
+
if (!gl || typeof gl.getParameter !== 'function') return null;
|
|
47
|
+
const hit = cache.get(gl);
|
|
48
|
+
if (hit) return hit;
|
|
49
|
+
let tex, rb, vp;
|
|
50
|
+
try {
|
|
51
|
+
tex = gl.getParameter(gl.MAX_TEXTURE_SIZE ?? 0x0d33);
|
|
52
|
+
rb = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE ?? 0x84e8);
|
|
53
|
+
vp = gl.getParameter(gl.MAX_VIEWPORT_DIMS ?? 0x0d3a);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const vpW = vp && vp.length >= 2 ? vp[0] : 0;
|
|
58
|
+
const vpH = vp && vp.length >= 2 ? vp[1] : 0;
|
|
59
|
+
const w = smallest([['MAX_TEXTURE_SIZE', tex], ['MAX_RENDERBUFFER_SIZE', rb], ['MAX_VIEWPORT_DIMS', vpW]]);
|
|
60
|
+
const h = smallest([['MAX_TEXTURE_SIZE', tex], ['MAX_RENDERBUFFER_SIZE', rb], ['MAX_VIEWPORT_DIMS', vpH]]);
|
|
61
|
+
if (!w || !h) return null; // a lost context answers 0/null — don't cache a non-answer
|
|
62
|
+
const lim = { maxW: w[1], maxH: h[1], nameW: w[0], nameH: h[0], valueW: w[1], valueH: h[1] };
|
|
63
|
+
cache.set(gl, lim);
|
|
64
|
+
return lim;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The limits of a throwaway WebGL context, for callers without one of their own yet: the core's
|
|
69
|
+
* image/video windows (2D canvases, which the weave still has to take as a GPU texture) and the
|
|
70
|
+
* PlayCanvas viewer before its engine has booted. Probed once per document, then the context is
|
|
71
|
+
* released. Null where there is no DOM or no WebGL (tests, workers, a GPU-less browser): no
|
|
72
|
+
* clamp is applied then.
|
|
73
|
+
*
|
|
74
|
+
* @returns {BufferLimits|null}
|
|
75
|
+
*/
|
|
76
|
+
export function probeBufferLimits() {
|
|
77
|
+
if (probed !== undefined) return probed;
|
|
78
|
+
probed = null;
|
|
79
|
+
try {
|
|
80
|
+
if (typeof document === 'undefined' || !document || typeof document.createElement !== 'function') return probed;
|
|
81
|
+
const c = document.createElement('canvas');
|
|
82
|
+
const gl = c.getContext('webgl2') || c.getContext('webgl');
|
|
83
|
+
if (!gl) return probed;
|
|
84
|
+
probed = glBufferLimits(gl);
|
|
85
|
+
gl.getExtension?.('WEBGL_lose_context')?.loseContext?.();
|
|
86
|
+
} catch {
|
|
87
|
+
probed = null;
|
|
88
|
+
}
|
|
89
|
+
return probed;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Tests only: forget the probed document limits. */
|
|
93
|
+
export function _resetProbedLimits() {
|
|
94
|
+
probed = undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Fit a requested per-eye size into the device's limits. The store is `cols × eyeW` by
|
|
99
|
+
* `rows × eyeH` (SBS: cols 2, rows 1; mono: cols 1). Both axes shrink by ONE factor, so the eye
|
|
100
|
+
* keeps its aspect; the result is floored so it can never round back over the limit.
|
|
101
|
+
*
|
|
102
|
+
* @param {number} eyeW requested per-eye width, buffer px
|
|
103
|
+
* @param {number} eyeH requested per-eye height, buffer px
|
|
104
|
+
* @param {BufferLimits|null} limits
|
|
105
|
+
* @param {{ cols?: number, rows?: number }} [layout]
|
|
106
|
+
* @returns {{ eyeW: number, eyeH: number, scale: number, clamped: boolean,
|
|
107
|
+
* reqW: number, reqH: number, bufW: number, bufH: number,
|
|
108
|
+
* limitName: string, limitValue: number }}
|
|
109
|
+
*/
|
|
110
|
+
export function clampEyeBuffer(eyeW, eyeH, limits, { cols = 2, rows = 1 } = {}) {
|
|
111
|
+
const reqW = eyeW * cols;
|
|
112
|
+
const reqH = eyeH * rows;
|
|
113
|
+
const base = { eyeW, eyeH, cols, scale: 1, clamped: false, reqW, reqH, bufW: reqW, bufH: reqH, limitName: '', limitValue: 0 };
|
|
114
|
+
if (!limits || !(eyeW > 0) || !(eyeH > 0)) return base;
|
|
115
|
+
const sw = limits.maxW / reqW;
|
|
116
|
+
const sh = limits.maxH / reqH;
|
|
117
|
+
const s = Math.min(sw, sh);
|
|
118
|
+
if (!(s < 1)) return base;
|
|
119
|
+
const byW = sw <= sh;
|
|
120
|
+
// Floor (never round up past the limit), and never above the per-axis cap either way.
|
|
121
|
+
const w = Math.max(1, Math.min(Math.floor(eyeW * s + 1e-9), Math.floor(limits.maxW / cols)));
|
|
122
|
+
const h = Math.max(1, Math.min(Math.floor(eyeH * s + 1e-9), Math.floor(limits.maxH / rows)));
|
|
123
|
+
return {
|
|
124
|
+
eyeW: w,
|
|
125
|
+
eyeH: h,
|
|
126
|
+
cols,
|
|
127
|
+
scale: s,
|
|
128
|
+
clamped: true,
|
|
129
|
+
reqW,
|
|
130
|
+
reqH,
|
|
131
|
+
bufW: w * cols,
|
|
132
|
+
bufH: h * rows,
|
|
133
|
+
limitName: byW ? limits.nameW : limits.nameH,
|
|
134
|
+
limitValue: byW ? limits.valueW : limits.valueH,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Round a scale for a human-readable warning: 0.8, 0.625, never 0.8000000001. */
|
|
139
|
+
export function roundScale(s) {
|
|
140
|
+
return Math.round(s * 1000) / 1000;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The one warning text, so every path names the same things: the store it wanted, the limit, what
|
|
145
|
+
* it rendered at, and (where there is one) the renderScale request vs the effective value.
|
|
146
|
+
*/
|
|
147
|
+
export function clampWarning(tag, c, requestedScale) {
|
|
148
|
+
const rs =
|
|
149
|
+
typeof requestedScale === 'number'
|
|
150
|
+
? ` (renderScale ${roundScale(requestedScale)} → ${roundScale(requestedScale * c.scale)})`
|
|
151
|
+
: ` (scale ${roundScale(c.scale)})`;
|
|
152
|
+
return (
|
|
153
|
+
`${tag} ${c.cols === 1 ? 'buffer' : 'SBS buffer'} ${c.reqW}×${c.reqH} exceeds ${c.limitName} ${c.limitValue} on this device; ` +
|
|
154
|
+
`rendering at ${c.bufW}×${c.bufH}${rs}.`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* How the layer's viewports (canvas-attribute space: XRDisplayLayer.getViewport() splits
|
|
160
|
+
* `canvas.width`) map onto the drawing buffer the GL context really has. {1, 1, false} whenever
|
|
161
|
+
* the two agree, which is every case the clamp above predicted.
|
|
162
|
+
*
|
|
163
|
+
* @param {{ width: number, height: number }} canvas
|
|
164
|
+
* @param {object|null} gl a WebGL context, or null (a 2D canvas: no separate drawing buffer)
|
|
165
|
+
*/
|
|
166
|
+
export function bufferScale(canvas, gl) {
|
|
167
|
+
const cw = canvas?.width || 0;
|
|
168
|
+
const ch = canvas?.height || 0;
|
|
169
|
+
const dw = gl && gl.drawingBufferWidth > 0 ? gl.drawingBufferWidth : cw;
|
|
170
|
+
const dh = gl && gl.drawingBufferHeight > 0 ? gl.drawingBufferHeight : ch;
|
|
171
|
+
const sx = cw > 0 ? dw / cw : 1;
|
|
172
|
+
const sy = ch > 0 ? dh / ch : 1;
|
|
173
|
+
return { sx, sy, w: dw, h: dh, mismatch: sx !== 1 || sy !== 1 };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Scale one {x, y, width, height} viewport by bufferScale()'s factors (identity returns it as is). */
|
|
177
|
+
export function scaleViewport(vp, sx, sy) {
|
|
178
|
+
if (sx === 1 && sy === 1) return vp;
|
|
179
|
+
return {
|
|
180
|
+
x: Math.round(vp.x * sx),
|
|
181
|
+
y: Math.round(vp.y * sy),
|
|
182
|
+
width: Math.max(1, Math.round(vp.width * sx)),
|
|
183
|
+
height: Math.max(1, Math.round(vp.height * sy)),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The warning for a drawing buffer the clamp did not predict. `tail` says what happens next. */
|
|
188
|
+
export function mismatchWarning(
|
|
189
|
+
tag,
|
|
190
|
+
canvas,
|
|
191
|
+
b,
|
|
192
|
+
tail = 'laying the eyes out from the drawing buffer. Lower renderScale to stay inside the device limit.',
|
|
193
|
+
) {
|
|
194
|
+
return (
|
|
195
|
+
`${tag} the browser clamped this canvas's drawing buffer to ${b.w}×${b.h} (canvas.width/height ` +
|
|
196
|
+
`say ${canvas.width}×${canvas.height}); ${tail}`
|
|
197
|
+
);
|
|
198
|
+
}
|