orb: startup self-check — verify float framebuffers complete and the simulation produces a sane particle, else fall back to the static layout with a console warning
This commit is contained in:
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user