From a919fa38cbafd37f535909863542fd143491e5ac Mon Sep 17 00:00:00 2001 From: dangrubbb Date: Sat, 24 Oct 2026 10:34:17 -0400 Subject: [PATCH] =?UTF-8?q?orb:=20startup=20self-check=20=E2=80=94=20verif?= =?UTF-8?q?y=20float=20framebuffers=20complete=20and=20the=20simulation=20?= =?UTF-8?q?produces=20a=20sane=20particle,=20else=20fall=20back=20to=20the?= =?UTF-8?q?=20static=20layout=20with=20a=20console=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scripts/orb.js | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/scripts/orb.js b/src/scripts/orb.js index 20933ae..f9965c0 100644 --- a/src/scripts/orb.js +++ b/src/scripts/orb.js @@ -724,7 +724,8 @@ export function startOrb(canvas, { onReady } = {}) { if (!gl) return false; const hasF32 = !!gl.getExtension('EXT_color_buffer_float'); - const interactive = hasF32 || !!gl.getExtension('EXT_color_buffer_half_float'); + // let: dropped to false if the simulation turns out not to work on this GPU/driver (see the checks below) + let interactive = hasF32 || !!gl.getExtension('EXT_color_buffer_half_float'); const stateFmt = hasF32 ? { internal: gl.RGBA32F, type: gl.FLOAT } : { internal: gl.RGBA16F, type: gl.HALF_FLOAT }; @@ -770,6 +771,19 @@ export function startOrb(canvas, { onReady } = {}) { // and the spawn point of the current life, so the costly spawn maths runs once per life const state = interactive ? pingPong(gl, T, T, stateFmt.internal, stateFmt.type, gl.NEAREST, 3) : null; let needInit = true; + const fallBack = (why) => { + if (!interactive) return; + interactive = false; + console.warn('orb: particle simulation unavailable on this GPU/driver, using the static layout.', why); + }; + // some drivers accept float render targets but cannot complete a three-attachment framebuffer + if (state) { + const incomplete = [state.read, state.write].some((t) => { + gl.bindFramebuffer(gl.FRAMEBUFFER, t.fbo); + return gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE; + }); + if (incomplete) fallBack('particle framebuffer incomplete'); + } // fluid fields; rebuilt when the viewport shape changes so the grid stays square-celled let velocity, dye, pressure, divergence, curl, simW = 1, simH = 1; @@ -924,6 +938,23 @@ export function startOrb(canvas, { onReady } = {}) { let simT = reduced ? 8 : 0; let last = performance.now(); let ready = false; + let frameCount = 0; + + // A few frames in, read one particle back: if the simulation produced nothing sane (all zeros, NaN), + // draw the static layout instead of an orb with no particles. + function verifySimulation() { + if (!hasF32) return; // reading half-float targets back as float is not guaranteed + try { + const px = new Float32Array(4); + gl.bindFramebuffer(gl.FRAMEBUFFER, state.read.fbo); + gl.readBuffer(gl.COLOR_ATTACHMENT0); + gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.FLOAT, px); // particle 0: an inner particle, always inside the ball + const r = Math.hypot(px[0], px[1], px[2]); + if (!(Number.isFinite(r) && r > 0.2 && r < 1.0)) fallBack(`particle 0 read back as (${Array.from(px).join(', ')})`); + } catch (e) { + fallBack(String(e)); + } + } function commonUniforms(prog, t) { gl.uniform1f(prog.u.uTime, t); @@ -1004,6 +1035,7 @@ export function startOrb(canvas, { onReady } = {}) { gl.uniform1f(s.u.uDt, dt); gl.uniform1f(s.u.uGain, 0.5); stepState(); + if (++frameCount === 5) verifySimulation(); } gl.bindFramebuffer(gl.FRAMEBUFFER, null);