Files
gods-eye-view/src/data/sceneOcclusionProbe.js
T
Sameh Khamis 076d264982 Hide street vehicles behind low walls and trees seen from above
In a dense downtown view from about 400 m up, the occlusion margin of
max(3 m, 2% of the range) came to 14 to 20 m, so a wall, awning, tree or
lower facade standing 10 to 20 m in front of a street was read as the
road itself: the vehicle's dot was hidden by the scene, but its bracket and
callout were still drawn. Checked against scene.pickPosition in one such
view, 18 of 470 vehicles in frame fell in that band.

The margin now depends on how steeply the camera looks down on the
contact. A point on the line of sight s metres in front of a vehicle
stands s * sin(elevation) above it, so a surface there hides the vehicle
once that rise passes 2 m. The margin stays at least 3 m and at most 2%
of the range, so grazing and street-level views are unchanged. In the
same views no vehicle with the drawn surface within 3 m in front of it
(its own road) is newly hidden.

From the review of the first change:

- A contact back in view after longer than a retest interval is held back
  and retested like a new one, instead of being drawn from an answer
  given before it left the view. It gets a fresh record, so a test still
  in flight from before cannot end the hold-back either.
- The hold-back for a contact's first answer stretches with the retest
  pace when a crowded view slows it.
- A batch whose control point failed no longer counts as a first answer.
- An unknown log-depth flag reads as "no depth" rather than as standard
  depth, which would have put every surface in front of every contact.
- A shader that fails to build leaves no shader behind and is not built
  again on the same context.
2026-11-10 15:08:26 -08:00

797 lines
28 KiB
JavaScript

/**
* Asks the GPU, per contact, whether the scene drew something in front of it,
* without waiting for the answer.
*
* The scene already keeps the depth of the frame it has just drawn (for
* picking). `scene.pickPosition` reads one texel of it back and waits, a
* fraction of a millisecond to over a millisecond per call; asking once per
* labelled contact turns a 2 ms frame into a 40 ms one. Copying the whole
* depth image back instead is no better where it matters: Chromium answers
* `getBufferSubData` with a round trip to its GPU process even after a sync
* object has signalled, measured here at 1.5 ms on a quiet machine and 15 to
* 25 ms on a busy one.
*
* So nothing is read back at all. Each tested contact becomes one point drawn
* into a private 1x1 target, inside an occlusion query. The vertex shader looks
* up the frame's depth at the contact's pixel, decodes it, and throws the point
* away when the drawn surface is nearer than the contact's threshold (see
* `projectContact`). The query's "did anything draw" bit is the answer, and
* asking whether a query result is in is the one question WebGL 2 lets a page
* put to the GPU without waiting (Chromium publishes results to the page
* through shared memory between frames): `_collect` just looks.
*
* - `beginFrame(scene, now)` collects the answers that have arrived.
* - `add(contact, position)` queues a contact for testing against this frame.
* - `endFrame()` draws the queued points. Answers land a frame or two later.
*
* A WebGL 1 context has no queries. There the probe falls back to reading the
* frame's depth back synchronously, at most every `SYNC_READ_INTERVAL_MS`, and
* answering on the CPU; `mode` then reads `'sync'`. Where the scene keeps no
* pick depth, or is not a 3D perspective view, `mode` reads `'off'`, nothing
* is tested and callers carry on as if no contact were hidden.
*
* Every read of renderer internals is in `readSceneDepthSource` below; the
* rest of the file is plain WebGL on the handles that function returns.
*/
import * as Cesium from 'cesium';
import { DepthSnapshot, projectContact } from './detectionOcclusion.js';
/**
* Depth ranges tested, nearest first. Logarithmic depth draws nearly every
* view in one or two; anything past the fourth is beyond where depth-tested
* contacts are labelled.
*/
export const PROBE_MAX_FRUSTUMS = 4;
/** Contacts tested per frame; the rest wait for the next one. */
export const PROBE_MAX_PER_FRAME = 256;
/** Painted frames a batch of queries may stay unanswered before it is dropped. */
export const PROBE_MAX_PENDING_FRAMES = 120;
/** Batches in a row whose control point failed before the probe stands down. */
export const PROBE_MAX_CONTROL_FAILURES = 3;
/** How long the probe stands down after a failure before trying again. */
export const PROBE_RETRY_AFTER_MS = 10_000;
/** Shortest gap between two synchronous depth reads (WebGL 1 fallback). */
export const SYNC_READ_INTERVAL_MS = 125;
/**
* Everything this module takes from the renderer, in one place.
*
* RENDERER INTERNALS. Checked against cesium 1.138.0 (@cesium/engine 22.3.0);
* re-check on an engine upgrade. Undocumented members used, and what each is
* assumed to be:
*
* - `scene.context._gl`: the WebGL context. `scene.context.webgl2` says which.
* - `scene.view.frustumCommandsList[i].near` / `.far`: the depth ranges the
* last frame was drawn in, nearest first.
* - `scene.view.pickDepths[i].framebuffer`: one framebuffer per depth range,
* refreshed every frame while `scene.useDepthPicking` is on and
* `scene._environmentState.useGlobeDepthFramebuffer` is true, whose first
* colour attachment is an RGBA8 texture holding window-space depth packed
* four bytes to a texel (`czm_packDepth`; see `unpackDepth`).
* `framebuffer._framebuffer` is the raw WebGL framebuffer, and
* `framebuffer.getColorTexture(0)` the texture (`._texture` its raw handle).
* - Opaque geometry in every range but the nearest is drawn with its near
* plane pulled in by `scene.opaqueFrustumNearOffset` (public), exactly as
* `scene.pickPosition` assumes when it decodes the same textures.
* - `scene.frameState.useLogDepth`: whether depth is logarithmic (see
* `depthToViewDistance`). Anything but a boolean there is "no depth".
*
* Any of these missing or shaped differently returns null, which callers treat
* as "no depth".
*
* @param {Cesium.Scene} scene
* @param {Float64Array} [viewScratch] Sixteen numbers to write the view matrix
* into instead of allocating; the result's `view` is then this array.
* @returns {null | {
* gl: WebGLRenderingContext|WebGL2RenderingContext, webgl2: boolean,
* width: number, height: number, framebuffers: WebGLFramebuffer[],
* textures: WebGLTexture[],
* frustums: Array<{near: number, far: number}>, logDepth: boolean,
* view: Float64Array, tanHalfFovX: number, tanHalfFovY: number,
* }}
*/
export function readSceneDepthSource(scene, viewScratch) {
try {
if (!scene || scene.mode !== Cesium.SceneMode.SCENE3D) return null;
if (scene.useDepthPicking !== true) return null;
const context = scene.context;
const gl = context?._gl;
if (!gl || context.depthTexture !== true) return null;
if (scene._environmentState?.useGlobeDepthFramebuffer !== true) return null;
const camera = scene.camera;
const frustum = camera?.frustum;
const fovy = frustum?.fovy;
const aspectRatio = frustum?.aspectRatio;
if (!(fovy > 0 && fovy < Math.PI) || !(aspectRatio > 0)) return null;
if (frustum.xOffset || frustum.yOffset) return null;
const view = scene.view;
const ranges = view?.frustumCommandsList;
const pickDepths = view?.pickDepths;
if (!ranges?.length || !pickDepths?.length) return null;
const count = Math.min(
ranges.length,
pickDepths.length,
PROBE_MAX_FRUSTUMS,
);
const nearOffset = Number(scene.opaqueFrustumNearOffset) || 1;
const framebuffers = [];
const textures = [];
const frustums = [];
let width = 0;
let height = 0;
for (let i = 0; i < count; i++) {
const framebuffer = pickDepths[i]?.framebuffer;
const raw = framebuffer?._framebuffer;
const texture = framebuffer?.getColorTexture?.(0);
const near = ranges[i]?.near * (i === 0 ? 1 : nearOffset);
const far = ranges[i]?.far;
if (!raw || !texture?._texture || !(near > 0) || !(far > near))
return null;
if (i === 0) {
width = texture.width;
height = texture.height;
} else if (texture.width !== width || texture.height !== height) {
return null;
}
framebuffers.push(raw);
textures.push(texture._texture);
frustums.push({ near, far });
}
if (!(width > 0) || !(height > 0)) return null;
// Read as standard depth, logarithmic depth puts every surface a few
// metres from the camera and would hide every contact: unknown is "no".
const logDepth = scene.frameState?.useLogDepth;
if (typeof logDepth !== 'boolean') return null;
const tanHalfFovY = Math.tan(fovy / 2);
return {
gl,
webgl2: context.webgl2 === true,
width,
height,
framebuffers,
textures,
frustums,
logDepth,
view: Cesium.Matrix4.toArray(
camera.viewMatrix,
viewScratch || new Float64Array(16),
),
tanHalfFovX: tanHalfFovY * aspectRatio,
tanHalfFovY,
};
} catch {
return null;
}
}
/**
* One point per contact. `a_contact` is (u, v, threshold): where the contact
* is in the frame, and the view distance a drawn surface must be under to hide
* it. A hidden contact's point is moved outside the clip volume, so nothing is
* drawn and its query reports no samples. A visible one lands on the single
* pixel of the 1x1 target (the viewport is set to match while these draw).
*/
export const PROBE_VERTEX_SHADER = `#version 300 es
precision highp float;
precision highp sampler2D;
layout(location = 0) in vec3 a_contact;
uniform sampler2D u_depth0;
uniform sampler2D u_depth1;
uniform sampler2D u_depth2;
uniform sampler2D u_depth3;
uniform int u_count;
uniform bool u_logDepth;
uniform vec4 u_near;
uniform vec4 u_far;
float viewDistance(sampler2D depthTexture, float near, float far) {
ivec2 size = textureSize(depthTexture, 0);
ivec2 texel = min(ivec2(a_contact.xy * vec2(size)), size - 1);
float depth = dot(
texelFetch(depthTexture, texel, 0),
vec4(1.0, 1.0 / 255.0, 1.0 / 65025.0, 1.0 / 16581375.0));
if (depth <= 0.0 || depth >= 1.0) return -1.0;
if (u_logDepth) return near + exp2(depth * log2(far - near + 1.0)) - 1.0;
return near * far / (far - depth * (far - near));
}
void main() {
float surface = viewDistance(u_depth0, u_near.x, u_far.x);
if (surface < 0.0 && u_count > 1) surface = viewDistance(u_depth1, u_near.y, u_far.y);
if (surface < 0.0 && u_count > 2) surface = viewDistance(u_depth2, u_near.z, u_far.z);
if (surface < 0.0 && u_count > 3) surface = viewDistance(u_depth3, u_near.w, u_far.w);
bool hidden = surface > 0.0 && surface < a_contact.z;
gl_Position = hidden ? vec4(2.0, 2.0, 2.0, 1.0) : vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 1.0;
}
`;
const FRAGMENT_SHADER = `#version 300 es
precision mediump float;
out vec4 color;
void main() { color = vec4(1.0); }
`;
function canQuery(gl) {
return (
typeof gl.createQuery === 'function' &&
typeof gl.beginQuery === 'function' &&
typeof gl.getQueryParameter === 'function' &&
typeof gl.createVertexArray === 'function'
);
}
function compile(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
gl.deleteShader(shader);
return null;
}
return shader;
}
/** Owns the GPU objects and the query batches in flight. */
export class SceneOcclusionProbe {
/**
* @param {{
* now?: () => number,
* readSource?: typeof readSceneDepthSource,
* }} [options]
*/
constructor({
now = () => performance.now(),
readSource = readSceneDepthSource,
} = {}) {
this._clock = now;
this._readSource = readSource;
/** Most tests one frame takes; read by whoever schedules them. */
this.maxPerFrame = PROBE_MAX_PER_FRAME;
/** `'async'` (queries), `'sync'` (blocking read), or `'off'`. */
this.mode = 'off';
/** Milliseconds the last frame spent collecting answers and drawing. */
this.lastFrameMs = 0;
/** Contact tests started in the last frame. */
this.lastFrameTests = 0;
this._scene = null;
this._now = 0;
this._source = undefined;
this._gl = null;
/** A context the probe's shader failed to build on; never retried. */
this._unsupportedGl = null;
this._resources = null;
this._queryPool = [];
this._batches = [];
this._attributes = new Float32Array((PROBE_MAX_PER_FRAME + 1) * 3);
this._queued = [];
this._projected = { u: 0, v: 0, threshold: 0 };
this._controlFailures = 0;
this._retryAt = Number.NEGATIVE_INFINITY;
this._frame = 0;
this._view = new Float64Array(16);
this._snapshot = null;
this._snapshotAt = Number.NEGATIVE_INFINITY;
this._spent = 0;
}
/**
* Open a frame: take in the answers that have arrived. Call after the scene
* has rendered (a post-render listener), before `add`.
* @param {Cesium.Scene} scene
* @param {number} now The caller's clock, in milliseconds. Answers are
* dated by it, so it must be the clock the caller judges their age on.
*/
beginFrame(scene, now) {
const started = this._clock();
this._scene = scene;
this._now = now;
this._frame++;
this._source = undefined;
// A frame that never reached `endFrame` leaves its contacts untested.
this._release(this._queued);
this._queued.length = 0;
this.lastFrameTests = 0;
this._collect();
this._spent = this._clock() - started;
}
/**
* Queue a contact for testing against the frame just drawn.
* @param {{lastHiddenAt: number, pending: boolean, answered: boolean}}
* contact Updated when the answer arrives: `pending` cleared, `answered`
* set and, if the contact was hidden, `lastHiddenAt` set to the time of
* the frame it was tested against.
* @param {{x: number, y: number, z: number}} position
* @returns {boolean} False when the contact cannot be tested now.
*/
add(contact, position) {
if (this._source === undefined) this._openSource();
const source = this._source;
if (!source) return false;
if (this.mode === 'sync') {
const hidden = this._snapshot?.hides(position);
if (hidden === null || hidden === undefined) return false;
contact.answered = true;
if (hidden) contact.lastHiddenAt = this._now;
this.lastFrameTests++;
return true;
}
const queued = this._queued;
if (queued.length >= PROBE_MAX_PER_FRAME) return false;
const projected = this._projected;
if (
!projectContact(
source.view,
source.tanHalfFovX,
source.tanHalfFovY,
position,
projected,
)
)
return false;
// Slot 0 is the control point; contacts start at 1.
const at = (queued.length + 1) * 3;
const attributes = this._attributes;
attributes[at] = projected.u;
attributes[at + 1] = projected.v;
attributes[at + 2] = projected.threshold;
queued.push(contact);
contact.pending = true;
this.lastFrameTests++;
return true;
}
/** Close the frame: draw the queued contacts' queries. */
endFrame() {
const started = this._clock();
if (this._queued.length > 0 && this._source && this.mode === 'async') {
try {
this._draw(this._source);
} catch {
this._release(this._queued);
this._fail();
}
}
this._queued.length = 0;
this._source = undefined;
this.lastFrameMs = this._spent + (this._clock() - started);
}
/**
* Drop every test in flight but keep the GPU objects, so that switching the
* overlay off and on again does not compile the shader a second time.
*/
reset() {
const gl = this._gl;
for (const batch of this._batches) this._release(batch.contacts);
this._release(this._queued);
try {
if (gl && !gl.isContextLost?.()) {
for (const batch of this._batches)
for (let i = batch.cursor; i < batch.queries.length; i++)
this._queryPool.push(batch.queries[i]);
}
} catch {
// A context that is going away has already released them.
}
this._batches.length = 0;
this._queued.length = 0;
this._source = undefined;
this._snapshot = null;
this._snapshotAt = Number.NEGATIVE_INFINITY;
this._controlFailures = 0;
this._retryAt = Number.NEGATIVE_INFINITY;
this.mode = 'off';
}
/** Delete the GPU objects and drop every test in flight. */
destroy() {
this.reset();
const gl = this._gl;
try {
if (gl && !gl.isContextLost?.()) {
for (const query of this._queryPool) gl.deleteQuery(query);
const resources = this._resources;
if (resources) {
gl.deleteProgram(resources.program);
gl.deleteVertexArray(resources.vertexArray);
gl.deleteBuffer(resources.buffer);
gl.deleteFramebuffer(resources.framebuffer);
gl.deleteTexture(resources.texture);
}
}
} catch {
// A context that is going away has already released them.
}
this._queryPool.length = 0;
this._resources = null;
this._gl = null;
this._unsupportedGl = null;
}
/** Tests that will never be answered stop counting as in flight. */
_release(contacts) {
for (const contact of contacts) if (contact) contact.pending = false;
}
/**
* Something in the query path broke: stand down for a while and keep
* everything drawn, then try again in case the cause was passing.
*/
_fail() {
this._retryAt = this._now + PROBE_RETRY_AFTER_MS;
this._controlFailures = 0;
this.mode = 'off';
}
/** Read this frame's depth source, once, when the first contact asks. */
_openSource() {
this._source = null;
if (this._now < this._retryAt) return;
const source = this._readSource(this._scene, this._view);
if (!source) {
this.mode = 'off';
return;
}
if (source.gl !== this._gl) {
// A different context (a rebuilt viewer) orphans whatever was in flight.
this.destroy();
this._gl = source.gl;
}
try {
if (source.gl.isContextLost?.()) {
// Everything made on the lost context went with it.
this.reset();
this._queryPool.length = 0;
this._resources = null;
return;
}
if (source.gl === this._unsupportedGl) {
// The shader did not build on this context: nothing is tested here.
this.mode = 'off';
return;
}
if (source.webgl2 && canQuery(source.gl)) {
if (!this._resources && !this._createResources(source.gl)) {
this._fail();
return;
}
this.mode = 'async';
} else {
this.mode = 'sync';
if (this._now - this._snapshotAt >= SYNC_READ_INTERVAL_MS) {
this._snapshot = this._readSync(source);
this._snapshotAt = this._now;
}
}
this._source = source;
} catch {
this._fail();
}
}
/**
* Compile the shader and make the 1x1 target. This is the one place the
* probe does wait on the GPU process (compile, link and framebuffer status
* are answered synchronously): once per WebGL context, on the first frame a
* depth-tested contact is in view. `reset` keeps the result.
*/
_createResources(gl) {
const vertex = compile(gl, gl.VERTEX_SHADER, PROBE_VERTEX_SHADER);
const fragment = compile(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);
if (!vertex || !fragment) {
if (vertex) gl.deleteShader(vertex);
if (fragment) gl.deleteShader(fragment);
// The same source will not compile on this context next time either.
this._unsupportedGl = gl;
return false;
}
const program = gl.createProgram();
gl.attachShader(program, vertex);
gl.attachShader(program, fragment);
gl.linkProgram(program);
gl.deleteShader(vertex);
gl.deleteShader(fragment);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
gl.deleteProgram(program);
this._unsupportedGl = gl;
return false;
}
const uniform = (name) => gl.getUniformLocation(program, name);
const previousArray = gl.getParameter(gl.VERTEX_ARRAY_BINDING);
const previousBuffer = gl.getParameter(gl.ARRAY_BUFFER_BINDING);
const previousTexture = gl.getParameter(gl.TEXTURE_BINDING_2D);
const previousDraw = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
const vertexArray = gl.createVertexArray();
const buffer = gl.createBuffer();
gl.bindVertexArray(vertexArray);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
gl.bindVertexArray(previousArray);
gl.bindBuffer(gl.ARRAY_BUFFER, previousBuffer);
const texture = gl.createTexture();
const framebuffer = gl.createFramebuffer();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA8, 1, 1);
gl.bindTexture(gl.TEXTURE_2D, previousTexture);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, framebuffer);
gl.framebufferTexture2D(
gl.DRAW_FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
texture,
0,
);
const complete =
gl.checkFramebufferStatus(gl.DRAW_FRAMEBUFFER) ===
gl.FRAMEBUFFER_COMPLETE;
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, previousDraw);
if (!complete) {
// Leave nothing half-made behind, so a later retry starts from scratch.
gl.deleteProgram(program);
gl.deleteVertexArray(vertexArray);
gl.deleteBuffer(buffer);
gl.deleteFramebuffer(framebuffer);
gl.deleteTexture(texture);
return false;
}
this._resources = {
program,
vertexArray,
buffer,
texture,
framebuffer,
depth: [0, 1, 2, 3].map((i) => uniform(`u_depth${i}`)),
count: uniform('u_count'),
logDepth: uniform('u_logDepth'),
near: uniform('u_near'),
far: uniform('u_far'),
};
return true;
}
/**
* Draw one query per queued contact, preceded by a control point that must
* always draw. The renderer caches the framebuffer, viewport and scissor
* state it last set, so those are put back exactly as found, along with
* every binding touched here. Depth, stencil, blending and the colour mask
* need no care: the target has no depth or stencil attachment, and a query
* counts samples whether or not they are written.
*/
_draw(source) {
const gl = source.gl;
const resources = this._resources;
const contacts = this._queued.slice();
const count = contacts.length + 1;
const attributes = this._attributes;
// The control point: mid-frame, with a threshold nothing can be under.
attributes[0] = 0.5;
attributes[1] = 0.5;
attributes[2] = -1;
const layers = source.textures.length;
const frustums = source.frustums;
const previousProgram = gl.getParameter(gl.CURRENT_PROGRAM);
const previousArray = gl.getParameter(gl.VERTEX_ARRAY_BINDING);
const previousBuffer = gl.getParameter(gl.ARRAY_BUFFER_BINDING);
const previousDraw = gl.getParameter(gl.DRAW_FRAMEBUFFER_BINDING);
const previousUnit = gl.getParameter(gl.ACTIVE_TEXTURE);
const previousViewport = gl.getParameter(gl.VIEWPORT);
const previousTextures = [];
const scissor = gl.isEnabled(gl.SCISSOR_TEST);
const queries = [];
const pool = this._queryPool;
let open = false;
try {
for (let i = 0; i < layers; i++) {
gl.activeTexture(gl.TEXTURE0 + i);
previousTextures.push(gl.getParameter(gl.TEXTURE_BINDING_2D));
gl.bindTexture(gl.TEXTURE_2D, source.textures[i]);
}
if (scissor) gl.disable(gl.SCISSOR_TEST);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, resources.framebuffer);
gl.viewport(0, 0, 1, 1);
gl.useProgram(resources.program);
// Samplers past the ranges in use point at the first range's texture:
// every sampler a program declares must be drawable, read or not.
for (let i = 0; i < PROBE_MAX_FRUSTUMS; i++)
gl.uniform1i(resources.depth[i], i < layers ? i : 0);
gl.uniform1i(resources.count, layers);
gl.uniform1i(resources.logDepth, source.logDepth ? 1 : 0);
gl.uniform4f(
resources.near,
frustums[0].near,
frustums[1]?.near ?? 1,
frustums[2]?.near ?? 1,
frustums[3]?.near ?? 1,
);
gl.uniform4f(
resources.far,
frustums[0].far,
frustums[1]?.far ?? 2,
frustums[2]?.far ?? 2,
frustums[3]?.far ?? 2,
);
gl.bindVertexArray(resources.vertexArray);
gl.bindBuffer(gl.ARRAY_BUFFER, resources.buffer);
gl.bufferData(
gl.ARRAY_BUFFER,
attributes.subarray(0, count * 3),
gl.STREAM_DRAW,
);
for (let i = 0; i < count; i++) {
const query = pool.pop() || gl.createQuery();
queries.push(query);
gl.beginQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE, query);
open = true;
gl.drawArrays(gl.POINTS, i, 1);
gl.endQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE);
open = false;
}
} catch (error) {
// Leave no query open and none unaccounted for; the caller stands down.
try {
if (open) gl.endQuery(gl.ANY_SAMPLES_PASSED_CONSERVATIVE);
for (const query of queries) gl.deleteQuery(query);
} catch {
// Nothing more can be done for them.
}
throw error;
} finally {
gl.bindBuffer(gl.ARRAY_BUFFER, previousBuffer);
gl.bindVertexArray(previousArray);
gl.useProgram(previousProgram);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, previousDraw);
if (previousViewport)
gl.viewport(
previousViewport[0],
previousViewport[1],
previousViewport[2],
previousViewport[3],
);
if (scissor) gl.enable(gl.SCISSOR_TEST);
for (let i = previousTextures.length - 1; i >= 0; i--) {
gl.activeTexture(gl.TEXTURE0 + i);
gl.bindTexture(gl.TEXTURE_2D, previousTextures[i]);
}
gl.activeTexture(previousUnit);
}
this._batches.push({
queries,
contacts,
at: this._now,
frame: this._frame,
cursor: 0,
control: false,
});
}
/**
* Take in whatever answers have arrived, oldest batch first, without
* waiting for any that have not. An answer is dated to the frame its test
* was drawn on, so one that arrives late (the overlay was not painting for
* a while) is as stale as it is old.
*/
_collect() {
const batches = this._batches;
if (batches.length === 0) return;
const gl = this._gl;
try {
if (gl.isContextLost?.()) {
// Queries made on a lost context are gone with it.
this.reset();
this._queryPool.length = 0;
this._resources = null;
return;
}
while (batches.length > 0) {
const batch = batches[0];
const { queries, contacts } = batch;
while (batch.cursor < queries.length) {
const query = queries[batch.cursor];
if (!gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE)) break;
const drew = !!gl.getQueryParameter(query, gl.QUERY_RESULT);
if (batch.cursor === 0) {
batch.control = drew;
} else {
const contact = contacts[batch.cursor - 1];
contact.pending = false;
// Without the control point's yes the batch means nothing: not a
// "hidden", and not a first answer that ends a new contact's
// hold-back either.
if (batch.control) {
contact.answered = true;
if (!drew && batch.at > contact.lastHiddenAt)
contact.lastHiddenAt = batch.at;
}
}
this._queryPool.push(query);
batch.cursor++;
}
if (batch.cursor < queries.length) break;
batches.shift();
if (batch.control) {
this._controlFailures = 0;
} else if (++this._controlFailures >= PROBE_MAX_CONTROL_FAILURES) {
this._fail();
}
}
// Whatever is still unanswered after too many frames is dropped,
// wherever it stands in the queue, so its contacts can be tested again.
const overdue = this._frame - PROBE_MAX_PENDING_FRAMES;
for (let i = batches.length - 1; i >= 0; i--) {
const batch = batches[i];
if (batch.frame >= overdue) continue;
for (let k = batch.cursor; k < batch.queries.length; k++)
gl.deleteQuery(batch.queries[k]);
for (
let k = Math.max(0, batch.cursor - 1);
k < batch.contacts.length;
k++
)
batch.contacts[k].pending = false;
batches.splice(i, 1);
}
} catch {
for (const batch of batches) this._release(batch.contacts);
batches.length = 0;
this._fail();
}
}
/**
* No queries (WebGL 1): read each depth range at full size and wait for it.
* Costs milliseconds per read; used only where nothing better exists.
*/
_readSync(source) {
const gl = source.gl;
const { width, height } = source;
const layers = source.framebuffers.length;
const layerBytes = width * height * 4;
const byteLength = layerBytes * layers;
const bytes =
this._snapshot?.bytes.length === byteLength
? this._snapshot.bytes
: new Uint8Array(byteLength);
const previous = gl.getParameter(gl.FRAMEBUFFER_BINDING);
for (let i = 0; i < layers; i++) {
gl.bindFramebuffer(gl.FRAMEBUFFER, source.framebuffers[i]);
gl.readPixels(
0,
0,
width,
height,
gl.RGBA,
gl.UNSIGNED_BYTE,
bytes.subarray(i * layerBytes, (i + 1) * layerBytes),
);
}
gl.bindFramebuffer(gl.FRAMEBUFFER, previous);
return new DepthSnapshot({
width,
height,
bytes,
frustums: source.frustums,
logDepth: source.logDepth,
// The source's matrix is a scratch array the next frame overwrites.
view: Float64Array.from(source.view),
tanHalfFovX: source.tanHalfFovX,
tanHalfFovY: source.tanHalfFovY,
});
}
}