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.
Particle state lives in two flat Float32Arrays — interleaved x,y positions and interleaved velocities — and the position array is the GL upload buffer, so there is zero packing cost before bufferSubData. The flow field is a proper curl field (velocity = ⊥∇ψ, ψ a sum of three drifting sine octaves, so it's divergence-free: no sinks for particles to pool in), evaluated analytically once per frame onto a 91×61 vector lattice (~5.5k vertices, trig hoisted into per-row/per-column tables), and each particle bilinearly samples that lattice — ~40 flops per particle instead of ~8 transcendental calls, which is the difference between 3 ms and 30 ms for the update. Drawing is a single gl.POINTS call of 100,000 one-pixel additive points over a non-cleared backbuffer; trails come from a two-pass fade (multiply dst by k, then subtract 2/255 so quantized ghosts actually die). The expected bottleneck is the CPU update loop — 100k bilinear samples plus integration in single-threaded JS — followed by the 800 KB per-frame re-upload; the GPU is nearly idle (~1.6 MP of fill total).
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>curl / 100,000</title>
<style>
:root{ color-scheme: dark; }
*{ box-sizing:border-box; }
html,body{ height:100%; }
body{
margin:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
gap:10px; background:#07090d; color:#8fa3b5;
font:12px/1.5 ui-monospace,"SF Mono","Cascadia Mono",Consolas,Menlo,monospace;
background-image: radial-gradient(1100px 700px at 50% 36%, #0b1017 0%, #07090d 72%);
}
header{ width:900px; display:flex; justify-content:space-between; align-items:baseline; }
header b{ color:#d7e4ee; font-size:13px; letter-spacing:.08em; }
header span{ opacity:.55; }
#stage{ position:relative; line-height:0; }
canvas{
display:block; width:900px; height:600px; background:#000;
border:1px solid #1d2836;
box-shadow: 0 0 0 1px #0a0f16, 0 24px 80px -24px rgba(20,60,90,.45), 0 2px 24px rgba(0,0,0,.6);
}
#hud{
position:absolute; top:10px; left:12px; line-height:1.35; pointer-events:none;
text-shadow: 0 1px 2px #000, 0 0 8px rgba(0,0,0,.9);
}
#fps{ font-size:22px; font-weight:700; color:#e8f4fb; font-variant-numeric:tabular-nums; letter-spacing:.02em; }
#fps small{ font-size:11px; font-weight:400; color:#7d93a6; margin-left:5px; letter-spacing:.14em; }
#sub{ font-size:11px; color:#93a9bb; font-variant-numeric:tabular-nums; }
#tag{ position:absolute; right:12px; bottom:9px; font-size:10px; letter-spacing:.16em;
color:#3d5064; pointer-events:none; text-transform:uppercase; }
footer{ width:900px; display:flex; justify-content:space-between; opacity:.5; font-size:11px; }
#err{ position:absolute; inset:0; display:none; align-items:center; justify-content:center;
color:#c9d6e2; line-height:1.6; text-align:center; padding:40px; }
</style>
</head>
<body>
<header>
<b>CURL / 100 000</b>
<span>divergence-free advection · handwritten WebGL · no libraries</span>
</header>
<div id="stage">
<canvas id="c" width="900" height="600"></canvas>
<div id="hud">
<div id="fps">—<small>FPS</small></div>
<div id="sub">starting…</div>
</div>
<div id="tag">gl.POINTS · fade: dst×k − ε</div>
<div id="err"></div>
</div>
<footer>
<span>field: curl of Σ sin·sin, 3 octaves, rebuilt each frame on a 91×61 lattice</span>
<span>bufferSubData 800 KB / frame</span>
</footer>
<script>
"use strict";
/* ---------------- constants ---------------- */
const W = 900, H = 600, N = 100000;
const TAU = Math.PI * 2;
const canvas = document.getElementById('c');
const fpsEl = document.getElementById('fps');
const subEl = document.getElementById('sub');
const gl = canvas.getContext('webgl', {
alpha:false, depth:false, stencil:false, antialias:false,
preserveDrawingBuffer:true, // trails need last frame to survive present
powerPreference:'high-performance'
}) || canvas.getContext('experimental-webgl');
if(!gl){
const e = document.getElementById('err');
e.style.display = 'flex';
e.textContent = 'WebGL is unavailable. This demo needs it to draw 100,000 points in a single call.';
throw new Error('no webgl');
}
/* ---------------- shaders / programs ---------------- */
function sh(type, src){
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if(!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
return s;
}
function prog(vs, fs, attribs){
const p = gl.createProgram();
gl.attachShader(p, sh(gl.VERTEX_SHADER, vs));
gl.attachShader(p, sh(gl.FRAGMENT_SHADER, fs));
attribs.forEach((a, i) => gl.bindAttribLocation(p, i, a));
gl.linkProgram(p);
if(!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(p));
return p;
}
const ptProg = prog(
`attribute vec2 aPos;
uniform vec2 uScale;
varying vec2 vPos;
void main(){
vPos = aPos;
gl_Position = vec4(aPos.x*uScale.x - 1.0, aPos.y*uScale.y + 1.0, 0.0, 1.0);
gl_PointSize = 1.0;
}`,
`precision mediump float;
uniform vec3 uCa, uCb;
uniform float uT;
varying vec2 vPos;
void main(){
float m = 0.5 + 0.5*sin(vPos.x*0.0052 + vPos.y*0.0031 + uT*0.22);
gl_FragColor = vec4(mix(uCa, uCb, m), 1.0);
}`,
['aPos']);
const fadeProg = prog(
`attribute vec2 aP;
void main(){ gl_Position = vec4(aP, 0.0, 1.0); }`,
`precision mediump float;
uniform float uK;
void main(){ gl_FragColor = vec4(uK, uK, uK, 1.0); }`,
['aP']);
const uPtScale = gl.getUniformLocation(ptProg, 'uScale');
const uPtT = gl.getUniformLocation(ptProg, 'uT');
const uCa = gl.getUniformLocation(ptProg, 'uCa');
const uCb = gl.getUniformLocation(ptProg, 'uCb');
const uFadeK = gl.getUniformLocation(fadeProg, 'uK');
/* ---------------- buffers ---------------- */
const ptBuf = gl.createBuffer();
const triBuf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, triBuf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW);
/* ---------------- flow field ----------------
psi = sum of drifting sine octaves; v = (dpsi/dy, -dpsi/dx).
Divergence-free by construction, so particles don't pool.
Evaluated on a lattice once per frame; particles bilinear-sample it. */
const CELL = 10, GW = W/CELL, GH = H/CELL, VW = GW+1, VH = GH+1, VN = VW*VH;
const fieldX = new Float32Array(VN), fieldY = new Float32Array(VN);
const OCT = [
{ lx:340, ly:300, s:34, wx: 0.110, wy: 0.163, px:0.0, py:1.7 },
{ lx:150, ly:170, s:22, wx:-0.210, wy: 0.130, px:2.1, py:0.6 },
{ lx: 74, ly: 82, s:11, wx: 0.310, wy:-0.240, px:4.2, py:3.3 }
];
const KA = new Float32Array(3), KB = new Float32Array(3), KAMP = new Float32Array(3);
for(let k=0;k<3;k++){ KA[k]=TAU/OCT[k].lx; KB[k]=TAU/OCT[k].ly; KAMP[k]=OCT[k].s/KB[k]; }
const sxA=[new Float32Array(VW),new Float32Array(VW),new Float32Array(VW)];
const cxA=[new Float32Array(VW),new Float32Array(VW),new Float32Array(VW)];
const syA=[new Float32Array(VH),new Float32Array(VH),new Float32Array(VH)];
const cyA=[new Float32Array(VH),new Float32Array(VH),new Float32Array(VH)];
const vxAmp=new Float32Array(3), vyAmp=new Float32Array(3);
function buildField(t){
const dx = 8*Math.cos(t*0.043) + 3*Math.sin(t*0.110 + 2.0); // slow weather-scale drift
const dy = 6*Math.sin(t*0.037 + 1.0) + 2*Math.cos(t*0.090);
for(let k=0;k<3;k++){
const o=OCT[k], phx=o.px + t*o.wx, phy=o.py + t*o.wy;
const bf = (0.72 + 0.28*Math.sin(t*0.10 + k*2.1)) * KAMP[k]; // breathing amplitude
vxAmp[k] = bf*KB[k]; vyAmp[k] = bf*KA[k];
const a=KA[k], b=KB[k], sx=sxA[k], cx=cxA[k], sy=syA[k], cy=cyA[k];
for(let i=0;i<VW;i++){ const u=a*(i*CELL)+phx; sx[i]=Math.sin(u); cx[i]=Math.cos(u); }
for(let j=0;j<VH;j++){ const v=b*(j*CELL)+phy; sy[j]=Math.sin(v); cy[j]=Math.cos(v); }
}
const s0=sxA[0],s1=sxA[1],s2=sxA[2], c0=cxA[0],c1=cxA[1],c2=cxA[2];
const r0=syA[0],r1=syA[1],r2=syA[2], q0=cyA[0],q1=cyA[1],q2=cyA[2];
const a0=vxAmp[0],a1=vxAmp[1],a2=vxAmp[2], e0=vyAmp[0],e1=vyAmp[1],e2=vyAmp[2];
let n=0;
for(let j=0;j<VH;j++){
const yq0=q0[j],yq1=q1[j],yq2=q2[j], yr0=r0[j],yr1=r1[j],yr2=r2[j];
for(let i=0;i<VW;i++,n++){
fieldX[n] = dx + a0*s0[i]*yq0 + a1*s1[i]*yq1 + a2*s2[i]*yq2;
fieldY[n] = dy - (e0*c0[i]*yr0 + e1*c1[i]*yr1 + e2*c2[i]*yr2);
}
}
}
/* ---------------- particles ---------------- */
const pos = new Float32Array(N*2); // interleaved x,y — uploaded to the GPU verbatim
const vel = new Float32Array(N*2);
buildField(0);
for(let i=0;i<N;i++){
const ix=i*2;
const x=Math.random()*W, y=Math.random()*H;
pos[ix]=x; pos[ix+1]=y;
const gx=x/CELL, gy=y/CELL, ci=gx|0, cj=gy|0, tx=gx-ci, ty=gy-cj;
const b=cj*VW+ci, bw=b+VW;
let a0=fieldX[b]+(fieldX[b+1]-fieldX[b])*tx, a1=fieldX[bw]+(fieldX[bw+1]-fieldX[bw])*tx;
vel[ix] = a0+(a1-a0)*ty;
a0=fieldY[b]+(fieldY[b+1]-fieldY[b])*tx; a1=fieldY[bw]+(fieldY[bw+1]-fieldY[bw])*tx;
vel[ix+1]= a0+(a1-a0)*ty;
}
gl.bindBuffer(gl.ARRAY_BUFFER, ptBuf);
gl.bufferData(gl.ARRAY_BUFFER, pos.byteLength, gl.DYNAMIC_DRAW);
/* one physics step: bilinear field sample, relax velocity toward field, integrate, recycle */
function step(dt, resp){
const p=pos, v=vel, fx=fieldX, fy=fieldY;
const invC=1/CELL, vw=VW, NN=N*2;
for(let ix=0; ix<NN; ix+=2){
let x=p[ix], y=p[ix+1];
const gx=x*invC, gy=y*invC;
const ci=gx|0, cj=gy|0, tx=gx-ci, ty=gy-cj;
const b=cj*vw+ci, bw=b+vw;
const x00=fx[b], x10=fx[b+1], x01=fx[bw], x11=fx[bw+1];
const xa=x00+(x10-x00)*tx, xb=x01+(x11-x01)*tx;
const fvx=xa+(xb-xa)*ty;
const y00=fy[b], y10=fy[b+1], y01=fy[bw], y11=fy[bw+1];
const ya=y00+(y10-y00)*tx, yb=y01+(y11-y01)*tx;
const fvy=ya+(yb-ya)*ty;
let vx=v[ix] +(fvx-v[ix] )*resp;
let vy=v[ix+1]+(fvy-v[ix+1])*resp;
v[ix]=vx; v[ix+1]=vy;
x+=vx*dt; y+=vy*dt;
if(x<0) x+=W; else if(x>=W) x-=W; // recycled: wrap back in, count never drops
if(y<0) y+=H; else if(y>=H) y-=H;
p[ix]=x; p[ix+1]=y;
}
}
/* ---------------- GL state ---------------- */
gl.disable(gl.DEPTH_TEST);
gl.enable(gl.BLEND);
gl.enableVertexAttribArray(0);
gl.viewport(0,0,W,H);
gl.clearColor(0,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(ptProg);
gl.uniform2f(uPtScale, 2/W, -2/H);
gl.uniform3f(uCa, 0.075, 0.11, 0.15); // cool steel
gl.uniform3f(uCb, 0.15, 0.11, 0.065); // warm amber
/* ---------------- main loop ---------------- */
const HALF_LIFE = 0.35; // trail half-life, seconds
const RELAX_TAU = 0.45; // velocity response time, seconds
let last = performance.now(), simT = 0, accMs = 0, frames = 0;
subEl.textContent = (pos.length>>1).toLocaleString('en-US') + ' particles';
function frame(now){
requestAnimationFrame(frame);
let dtMs = now - last; last = now;
if(dtMs > 250){ accMs = 0; frames = 0; dtMs = 16.7; } // tab was hidden; don't poison the average
const dt = Math.min(dtMs, 50) / 1000;
simT += dt;
buildField(simT);
step(dt, 1 - Math.exp(-dt/RELAX_TAU));
/* fade pass 1: dst *= k (exponential trail decay, frame-rate independent) */
gl.useProgram(fadeProg);
gl.bindBuffer(gl.ARRAY_BUFFER, triBuf);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.ZERO, gl.SRC_COLOR);
gl.uniform1f(uFadeK, Math.pow(0.5, Math.min(dtMs,100)/1000/HALF_LIFE));
gl.drawArrays(gl.TRIANGLES, 0, 3);
/* fade pass 2: dst -= 2/255 (drains 8-bit quantization residue so trails truly die) */
gl.blendEquation(gl.FUNC_REVERSE_SUBTRACT);
gl.blendFunc(gl.ONE, gl.ONE);
gl.uniform1f(uFadeK, 2/255);
gl.drawArrays(gl.TRIANGLES, 0, 3);
/* points: one draw call for all of them, additive */
gl.useProgram(ptProg);
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.ONE, gl.ONE);
gl.uniform1f(uPtT, simT);
gl.bindBuffer(gl.ARRAY_BUFFER, ptBuf);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, pos);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.POINTS, 0, N);
/* honest stats: frames / elapsed over a rolling 500 ms window */
accMs += dtMs; frames++;
if(accMs >= 500){
const fps = frames * 1000 / accMs;
fpsEl.innerHTML = fps.toFixed(1) + '<small>FPS</small>';
subEl.textContent = (accMs/frames).toFixed(2) + ' ms/frame · '
+ (pos.length>>1).toLocaleString('en-US') + ' particles · field 91×61';
accMs = 0; frames = 0;
}
}
requestAnimationFrame(frame);
</script>
</body>
</html>
FPS I expect: 60 on a 2023 laptop with integrated graphics and a 60 Hz panel — i.e. vsync-capped. My CPU budget estimate is ~4–6 ms per frame (~3 ms update loop, ~0.7 ms for the 800 KB bufferSubData, ~0.2 ms field rebuild, the rest GL overhead), so there is roughly 3× headroom; the cap, not the code, sets the number. If that laptop has a 120 Hz panel, the same code will read ~120, because nothing in it is 60-bound.
Where the time goes: of the ~5 ms of CPU work, roughly 65–70% is the update loop — 100,000 bilinear lattice samples plus velocity relaxation and integration, memory-streaming 3.2 MB of typed arrays per frame. ~15–20% is the buffer re-upload and driver submission. The field rebuild is almost free (~5.5k vertices, trig hoisted to row/column tables). The GPU is barely awake: 100k one-pixel points plus two fullscreen fade passes is ~1.6 megapixels of fill, well under 1 ms and overlapped with the CPU anyway. So update outspends draw about 3:1 — this demo is CPU-bound by design choice, not fill-rate bound.
The first thing that breaks at a million: the JavaScript update loop. It scales linearly to ~30–40 ms on its own (~25 fps), and the upload grows to 8 MB/frame (~480 MB/s) which adds another few milliseconds and real driver pain. The GPU still wouldn't be the problem — a million 1-px points is only ~1 MP of fill. Rasterization breaks last; the CPU-side sim and the bus break first.
What you'd need to go ten times faster: a GPU-resident simulation — WebGL2 transform feedback (or WebGPU compute) ping-ponging position/velocity buffers, with the draw consuming the same buffer the sim writes. That deletes both costs at once: per-particle CPU work goes to zero and the 800 KB/frame upload disappears, because positions never leave the GPU. The field becomes a small texture sampled in the update pass. That is the standard jump from "a few hundred thousand" to "tens of millions."
Nothing dropped. 100,000 particles from the actual array (pos.length >> 1 is what the HUD prints), a genuinely time-evolving divergence-free curl field, wrap-around recycling, honestly averaged FPS, one file, no libraries, no network, no storage APIs.