47:51Ranking major U.S. Open-Weights Models. (Where does Ripple 65B fit in?)
Nov 6, 2026 · YouTube
38:58Testing Particle-1: a post-trained DeepSeek V4.1 Flash specializing in coding and efficiency
Oct 28, 2026 · YouTube
29:55Kimi K3.1 Pro Looked Incredible. Then I Kept Testing.
Oct 26, 2026 · YouTube
22:47Mistral Large 4 Is Cheap. But Is It Good Enough to Compete?
Oct 6, 2026 · YouTube
16:54Stealth Union Alpha: Is this another cheap GPT model? (better than Luna, cheaper than Terra?)
Sep 16, 2026 · YouTube
16:45DeepSeek V4.1 Flash Is Shockingly Good for the Price
Sep 10, 2026 · YouTube
17:14How much better is GPT-6 Astra over Claude Opus 5? Real Website and Coding Tests
Sep 5, 2026 · YouTube
29:13Implementing a multiplayer game with GPT-6 Astra (+ Gemini 3.8, Opus 5, Sol Pro)
Sep 5, 2026 · YouTube
19:42Meta Muse Spark 1.3 Review: A Huge Leap Forward
Sep 2, 2026 · YouTube
17:31Testing Gemini 3.8 Flash on Real Coding Challenges (vs. other models)
Sep 2, 2026 · YouTube
You are a graphics engineer who has spent a lot of time finding out why things are slow.
I want a hundred thousand things moving on screen at sixty frames a second, in one HTML file, with no libraries.
data: URIs are fine.localStorage, sessionStorage, cookies or indexedDB — sandboxed frame, they throw.html block.One paragraph, before the file. Your data layout, your draw strategy, and the one thing you expect to be the bottleneck.
The single fenced html block.
This is the section I care most about. Be specific and commit to numbers:
If you could not do 100,000 at 60fps and shipped fewer, or a cheaper effect than a real flow field, say so here, plainly, and say what you shipped instead. Write Nothing dropped. if you delivered the brief in full.
I am going to open the file and read the FPS counter.
Then I am going to compare it against your prediction. A model that says "about 45fps, fill-rate bound" and measures 43 has understood its own code. A model that says 60 and measures 4 has not, however elegant the code is — and that gap is most of what I am testing here. An honest prediction of a mediocre number beats a confident prediction of a good one.
Particles live in one interleaved Float32Array of [x, y, speed²] triples — one buffer, one bufferSubData per frame, one drawArrays(POINTS). The flow field is a divergence-free curl field built from three travelling sine octaves of a stream function; it's evaluated on a 128×96 grid each frame using an angle-addition recurrence (so only 3 cos/sin pairs per row instead of 36k trig calls), then bilinearly sampled per particle on the CPU. Rendering is raw WebGL1: two ping-pong RGBA framebuffers, a fade quad that multiplies the previous frame by 0.94, then 100k additive points on top, then a blit to the canvas. The bottleneck I expect is the CPU particle loop — 100k bilinear samples plus integration plus a 1.2 MB upload every frame — not the GPU, which is only rasterising ~300k tiny fragments into a 900×600 target.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Flow Field — 100,000 particles</title>
<style>
html,body{margin:0;padding:0;height:100%;background:#04060a;overflow:hidden;}
body{display:flex;align-items:center;justify-content:center;}
#stage{position:relative;width:900px;height:600px;box-shadow:0 0 80px rgba(40,120,255,.10);}
canvas{display:block;width:900px;height:600px;background:#04060a;}
#hud{position:absolute;left:12px;top:10px;pointer-events:none;
font:12px/1.6 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
color:#7fd4ff;letter-spacing:.07em;text-shadow:0 0 8px #000,0 0 2px #000;}
#hud b{color:#eafcff;font-weight:600;}
#hud .k{color:#3f7fa8;}
</style>
</head>
<body>
<div id="stage">
<canvas id="c" width="900" height="600"></canvas>
<div id="hud">
<div><span class="k">FPS</span> <b id="fps">--</b></div>
<div><span class="k">PARTICLES</span> <b id="cnt">--</b></div>
</div>
</div>
<script>
(function(){
'use strict';
var W = 900, H = 600;
var N = 100000;
var STRIDE = 3; // x, y, speed^2
var canvas = document.getElementById('c');
var fpsEl = document.getElementById('fps');
var cntEl = document.getElementById('cnt');
var gl = canvas.getContext('webgl', {
antialias: false, alpha: false, depth: false, stencil: false,
premultipliedAlpha: false, preserveDrawingBuffer: false,
powerPreference: 'high-performance'
});
if (!gl) { fpsEl.textContent = 'no webgl'; return; }
/* ---------------------------------------------------------------- data */
var data = new Float32Array(N * STRIDE);
for (var i = 0; i < N; i++) {
data[i*3] = Math.random() * W;
data[i*3+1] = Math.random() * H;
data[i*3+2] = 0;
}
cntEl.textContent = (data.length / STRIDE).toLocaleString('en-US');
/* --------------------------------------------------------------- field */
/* Stream function psi = sum_o A_o * sin(a_o x + b_o y + c_o t + d_o)
u = dpsi/dy = sum_o A_o b_o cos(...)
v = -dpsi/dx = -sum_o A_o a_o cos(...) */
var GW = 128, GH = 96;
var cellW = W / (GW - 1);
var cellH = H / (GH - 1);
var invCellW = 1 / cellW;
var invCellH = 1 / cellH;
var fu = new Float32Array(GW * GH);
var fv = new Float32Array(GW * GH);
var A0 = 5200, a0 = 0.0105, b0 = 0.0072, c0 = 0.29, d0 = 0.0;
var A1 = 2100, a1 = -0.0192, b1 = 0.0158, c1 = -0.47, d1 = 2.1;
var A2 = 760, a2 = 0.0331, b2 = -0.0264, c2 = 0.83, d2 = 4.7;
var U0 = A0*b0, V0 = -A0*a0;
var U1 = A1*b1, V1 = -A1*a1;
var U2 = A2*b2, V2 = -A2*a2;
var cs0 = Math.cos(a0*cellW), ss0 = Math.sin(a0*cellW);
var cs1 = Math.cos(a1*cellW), ss1 = Math.sin(a1*cellW);
var cs2 = Math.cos(a2*cellW), ss2 = Math.sin(a2*cellW);
function updateField(t) {
var k = 0;
for (var gy = 0; gy < GH; gy++) {
var y = gy * cellH;
var p0 = b0*y + c0*t + d0;
var p1 = b1*y + c1*t + d1;
var p2 = b2*y + c2*t + d2;
var C0 = Math.cos(p0), S0 = Math.sin(p0);
var C1 = Math.cos(p1), S1 = Math.sin(p1);
var C2 = Math.cos(p2), S2 = Math.sin(p2);
for (var gx = 0; gx < GW; gx++) {
fu[k] = U0*C0 + U1*C1 + U2*C2;
fv[k] = V0*C0 + V1*C1 + V2*C2;
k++;
var n0 = C0*cs0 - S0*ss0; S0 = S0*cs0 + C0*ss0; C0 = n0;
var n1 = C1*cs1 - S1*ss1; S1 = S1*cs1 + C1*ss1; C1 = n1;
var n2 = C2*cs2 - S2*ss2; S2 = S2*cs2 + C2*ss2; C2 = n2;
}
}
}
/* ----------------------------------------------------------- particles */
var GWm = GW - 1.001;
var GHm = GH - 1.001;
function updateParticles(dt) {
var d = data;
var i = 0;
for (var p = 0; p < N; p++) {
var x = d[i], y = d[i+1];
var fx = x * invCellW; if (fx > GWm) fx = GWm;
var fy = y * invCellH; if (fy > GHm) fy = GHm;
var ix = fx | 0, iy = fy | 0;
var tx = fx - ix, ty = fy - iy;
var k = iy * GW + ix;
var k1 = k + GW;
var f0 = fu[k], f1 = fu[k+1], f2 = fu[k1], f3 = fu[k1+1];
var u = f0 + (f1-f0)*tx + (f2-f0)*ty + (f0-f1-f2+f3)*tx*ty;
var g0 = fv[k], g1 = fv[k+1], g2 = fv[k1], g3 = fv[k1+1];
var v = g0 + (g1-g0)*tx + (g2-g0)*ty + (g0-g1-g2+g3)*tx*ty;
x += u * dt;
y += v * dt;
if (x < 0 || x >= W || y < 0 || y >= H) {
x = Math.random() * W;
y = Math.random() * H;
}
d[i] = x;
d[i+1] = y;
d[i+2] = (u*u + v*v) * 0.00012;
i += 3;
}
}
/* ------------------------------------------------------------- webgl */
function sh(type, src) {
var s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS))
throw new Error(gl.getShaderInfoLog(s) || 'shader');
return s;
}
function prog(vsSrc, fsSrc) {
var p = gl.createProgram();
gl.attachShader(p, sh(gl.VERTEX_SHADER, vsSrc));
gl.attachShader(p, sh(gl.FRAGMENT_SHADER, fsSrc));
gl.bindAttribLocation(p, 0, 'a_pos');
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
throw new Error(gl.getProgramInfoLog(p) || 'link');
return p;
}
var fadeVS = [
'attribute vec2 a_pos;',
'varying vec2 v_uv;',
'void main(){',
' v_uv = a_pos * 0.5 + 0.5;',
' gl_Position = vec4(a_pos, 0.0, 1.0);',
'}'].join('\n');
var fadeFS = [
'precision mediump float;',
'varying vec2 v_uv;',
'uniform sampler2D u_tex;',
'uniform float u_fade;',
'void main(){',
' gl_FragColor = vec4(texture2D(u_tex, v_uv).rgb * u_fade, 1.0);',
'}'].join('\n');
var ptVS = [
'attribute vec3 a_pos;',
'uniform vec2 u_res;',
'varying vec3 v_col;',
'void main(){',
' vec2 q = a_pos.xy / u_res;',
' gl_Position = vec4(q.x * 2.0 - 1.0, 1.0 - q.y * 2.0, 0.0, 1.0);',
' float s = clamp(sqrt(a_pos.z), 0.0, 1.0);',
' gl_PointSize = 1.0 + s * 2.0;',
' v_col = mix(vec3(0.05, 0.18, 0.62), vec3(0.78, 0.96, 1.0), s * s);',
'}'].join('\n');
var ptFS = [
'precision mediump float;',
'varying vec3 v_col;',
'void main(){',
' gl_FragColor = vec4(v_col * 0.28, 1.0);',
'}'].join('\n');
var fadeProg = prog(fadeVS, fadeFS);
var fadeTex = gl.getUniformLocation(fadeProg, 'u_tex');
var fadeAmt = gl.getUniformLocation(fadeProg, 'u_fade');
var ptProg = prog(ptVS, ptFS);
var ptRes = gl.getUniformLocation(ptProg, 'u_res');
var posBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_DRAW);
var quadBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.bufferData(gl.ARRAY_BUFFER,
new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
function makeTarget() {
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, W, H, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
var fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D, tex, 0);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
return { tex: tex, fbo: fbo };
}
var src = makeTarget();
var dst = makeTarget();
var FADE = 0.94;
function render() {
/* fade previous frame into dst */
gl.bindFramebuffer(gl.FRAMEBUFFER, dst.fbo);
gl.viewport(0, 0, W, H);
gl.disable(gl.BLEND);
gl.useProgram(fadeProg);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, src.tex);
gl.uniform1i(fadeTex, 0);
gl.uniform1f(fadeAmt, FADE);
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
/* additive points */
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE);
gl.useProgram(ptProg);
gl.uniform2f(ptRes, W, H);
gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.POINTS, 0, N);
/* blit to screen */
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.disable(gl.BLEND);
gl.useProgram(fadeProg);
gl.bindTexture(gl.TEXTURE_2D, dst.tex);
gl.uniform1f(fadeAmt, 1.0);
gl.bindBuffer(gl.ARRAY_BUFFER, quadBuf);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
var t = src; src = dst; dst = t;
}
/* --------------------------------------------------------------- loop */
var last = performance.now();
var simT = 0;
var acc = 0, frames = 0, warm = 0;
function loop(now) {
var dtms = now - last;
last = now;
var dt = dtms * 0.001;
if (dt > 0.033) dt = 0.033;
if (dt < 0) dt = 0;
simT += dt;
updateField(simT);
updateParticles(dt);
gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, data);
render();
if (warm < 8) {
warm++;
} else {
acc += dtms;
frames++;
if (acc >= 300) {
fpsEl.textContent = (frames * 1000 / acc).toFixed(1);
acc = 0;
frames = 0;
}
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
FPS I expect: 60 on a 2023 laptop with integrated graphics in Chrome. Not because the frame is cheap, but because the raw frame time lands around 7–9 ms and vsync caps it there. If the CPU is a low-power U-series part running on E-cores it could sag to the low 50s; I don't expect that on a normal P-core dispatch.
Where the time goes: roughly 60% update, 25% draw, 15% upload/other. The update is the whole updateField + updateParticles pair — 100k bilinear samples, 100k integrations, 100k writes back into the interleaved array. The draw is two full-screen quads (fade + blit) plus 100k POINTS primitives, which is a trivial amount of fill for any GPU made this decade; the 1.2 MB bufferSubData is a memcpy and costs a few tenths of a millisecond. The GPU is not the problem here — the CPU touching every particle every frame is.
First thing that breaks at a million: the CPU particle loop. It's linear in N, so 10× the work puts the update alone at ~30–40 ms/frame and the whole thing drops to roughly 20–25 fps. The 12 MB/frame bufferSubData also stops being free and starts showing up as a real stall.
What I'd need to go 10× faster: move the simulation itself onto the GPU — WebGL2 transform feedback (or ping-pong float render targets) so particle state lives in VRAM and never round-trips through the CPU. That deletes both the update loop and the upload, which are the two things actually costing time here.
Nothing dropped.