From a6cf9ff552d5fb593a292750f6e8722e8f32732c Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 15:55:11 +0700 Subject: [PATCH] spec(t27f): token_floor -- an infographic where every particle carries a fact (Closes #10323) One token of a chat reply costs 1.7 J; the Landauer floor for naming it is 4.9e-20 J; the gap is 19 decades. Twenty glass balls drop into a tube on a glossy black floor: the white ball is the floor, each gold ball a factor of ten. Balls move under gravity and Hertz contact (velocity Verlet); every letter is PD-steered dots emitted by the ball it describes; the X10^k counter is one particle pool that re-forms as each gold ball lands. Numbers come from physical_limits, forces from particle_engine, frame helpers from reel_particles; fifteen helper fns there become pub, no behaviour change. 19/19 tests pass, 0 vacuous; eight planted faults all caught; dupe scan clean. Co-Authored-By: Claude Opus 5.5 --- specs/physics/physical_limits.t27 | 8 +- specs/t27f/particle_engine.t27 | 16 +- specs/t27f/reel_particles.t27 | 6 +- specs/t27f/token_floor.t27 | 1284 +++++++++++++++++++++++++++++ 4 files changed, 1299 insertions(+), 15 deletions(-) create mode 100644 specs/t27f/token_floor.t27 diff --git a/specs/physics/physical_limits.t27 b/specs/physics/physical_limits.t27 index e245622882..53cf9a703a 100644 --- a/specs/physics/physical_limits.t27 +++ b/specs/physics/physical_limits.t27 @@ -104,7 +104,7 @@ module physical_limits { } // Landauer: the least energy, in joules, to erase one bit at temperature t kelvin. - fn landauer_j(t: f64) -> f64 { + pub fn landauer_j(t: f64) -> f64 { return K_B * t * LN2; } @@ -161,7 +161,7 @@ module physical_limits { } // Bits needed to name one of n symbols: the smallest k with 2^k >= n. - fn index_bits(n: f64) -> f64 { + pub fn index_bits(n: f64) -> f64 { var k : f64 = 0.0; var cap : f64 = 1.0; while (cap < n) { @@ -172,12 +172,12 @@ module physical_limits { } // The energy of one token, from the per-prompt figure and a reply length. - fn token_energy_j(prompt_j: f64, tokens: f64) -> f64 { + pub fn token_energy_j(prompt_j: f64, tokens: f64) -> f64 { return prompt_j / tokens; } // How many times the energy spent exceeds the Landauer cost of erasing bits bits at t kelvin. - fn gap(e_j: f64, bits: f64, t: f64) -> f64 { + pub fn gap(e_j: f64, bits: f64, t: f64) -> f64 { return e_j / (bits * landauer_j(t)); } diff --git a/specs/t27f/particle_engine.t27 b/specs/t27f/particle_engine.t27 index 72835332ed..3058697f77 100644 --- a/specs/t27f/particle_engine.t27 +++ b/specs/t27f/particle_engine.t27 @@ -272,38 +272,38 @@ module particle_engine { // ---- optics ---- - fn fresnel_r0(n: f64) -> f64 { + pub fn fresnel_r0(n: f64) -> f64 { const a = (n - 1.0) / (n + 1.0); return a * a; } // Schlick reflectance at an angle of incidence whose cosine is c. - fn schlick(c: f64, n: f64) -> f64 { + pub fn schlick(c: f64, n: f64) -> f64 { const r0 = fresnel_r0(n); const k = 1.0 - c; return r0 + (1.0 - r0) * k * k * k * k * k; } // Cosine of the incidence angle at which the camera sees, in the floor, a body at height y. - fn mirror_cos(y: f64) -> f64 { + pub fn mirror_cos(y: f64) -> f64 { const d = CAM_D * CAM_H / (CAM_H + y); return CAM_H / @sqrt(CAM_H * CAM_H + d * d); } - fn mirror_alpha(y: f64, glow: f64) -> f64 { + pub fn mirror_alpha(y: f64, glow: f64) -> f64 { return glow * schlick(mirror_cos(y), N_LACQUER); } - fn glow_of(vx: f64, vy: f64) -> f64 { + pub fn glow_of(vx: f64, vy: f64) -> f64 { return 1.0 + (vx * vx + vy * vy) / (V_REF * V_REF); } - fn gauss(d2: f64, r: f64) -> f64 { + pub fn gauss(d2: f64, r: f64) -> f64 { return @exp(0.0 - d2 / (2.0 * r * r)); } // Exposure: light adds, the film never clips. - fn expose(l: f64) -> f64 { + pub fn expose(l: f64) -> f64 { return 255.0 * (1.0 - @exp(0.0 - l)); } @@ -329,7 +329,7 @@ module particle_engine { // Hertz push between two elastic spheres whose centres are sqrt(r2) apart and touch at d0: // F = K d^(3/2), U = (2/5) K d^(5/2) for an overlap d (H. Hertz, J. reine angew. Math. 92, 156, 1882). - fn contact_f_over_r(r2: f64, d0: f64) -> f64 { + pub fn contact_f_over_r(r2: f64, d0: f64) -> f64 { if (r2 >= d0 * d0) { return 0.0; } diff --git a/specs/t27f/reel_particles.t27 b/specs/t27f/reel_particles.t27 index 59bd1b6e27..77c398755a 100644 --- a/specs/t27f/reel_particles.t27 +++ b/specs/t27f/reel_particles.t27 @@ -118,11 +118,11 @@ pub const K_RING : u8 = 1; // ---- pure pieces, each tested ---- -fn lcg(seed: i64) -> i64 { +pub fn lcg(seed: i64) -> i64 { return @mod(seed * 1103515245 + 12345, 2147483648); } -fn unit(seed: i64) -> f64 { +pub fn unit(seed: i64) -> f64 { return @as(f64, @floatFromInt(seed)) / 2147483648.0; } @@ -339,7 +339,7 @@ fn grain_level(seed: i64) -> f64 { return GRAIN * (unit(seed) - 0.5); } -fn quantize(x: f64) -> u8 { +pub fn quantize(x: f64) -> u8 { var y : f64 = @round(x); if (y < 0.0) { y = 0.0; } if (y > 255.0) { y = 255.0; } diff --git a/specs/t27f/token_floor.t27 b/specs/t27f/token_floor.t27 new file mode 100644 index 0000000000..d94ab99281 --- /dev/null +++ b/specs/t27f/token_floor.t27 @@ -0,0 +1,1284 @@ +// SPDX-License-Identifier: Apache-2.0 +// specs/t27f/token_floor.t27 -- an infographic in which every particle carries a fact: what one +// token of a chat reply costs in energy, set against the least energy physics allows for it. +// +// WHY: the owner, 2026-10-18, on the engine reel: the particles there fly but carry no information; +// where is the infographic built on this technology? This is it. The numbers come from +// specs/physics/physical_limits.t27, the forces from specs/t27f/particle_engine.t27, and the frame +// helpers from specs/t27f/reel_particles.t27. Nothing here restates any of them. +// +// THE FACT, as physical_limits computes it: +// the floor picking one token out of a 128256-token vocabulary names 17 bits; erasing 17 bits +// at 300 K costs at least 17 k T ln 2 = 4.9e-20 J (R. Landauer, IBM J. Res. Dev. 5, +// 183, 1961) +// the bill 0.24 Wh for a median text prompt (Google, 2025, arXiv:2508.15734), over a reply of +// 500 tokens: 1.7 J a token +// the gap 1.7 J / 4.9e-20 J = 3.5e19: 19 whole decades. The bill is engineering, not +// thermodynamics. +// The vocabulary size and the reply length are UNVERIFIED illustrative inputs in physical_limits, +// and the picture says so in its footer. +// +// THE PICTURE: a glass tube stands on a glossy black floor. Twenty glass balls drop into it one by +// one. The first, white, is the floor: 4.9e-20 J. Each gold ball after it multiplies by ten, so the +// column is a logarithmic bar chart that builds itself: 19 gold balls, one per decade of the gap. +// The tube holds the balls in line, so their motion is one-dimensional: gravity g = G_STD, Hertz +// contact between neighbours and with the floor (contact_f_over_r) with the engine's dashpot +// C_CONTACT, integrated by velocity Verlet at STEPS substeps of H per frame. Air drag on a 34 mm +// ball at these speeds is under 5 percent of its weight and is left out. +// Every letter is made of particles. Each lit dot of a 5x7 dot-matrix glyph is a body steered to +// its place by the engine's critically damped PD controller (KP, KD): control enters as a force, +// never as a position. The labels of a ball are emitted by that ball at the frame the integrator +// finds it touching the stack: the floor label from the white ball, the decade tick beside each +// ball from the ball itself. The exponent of the counter "X10^k" is one pool of particles that +// re-forms into k each time gold ball k lands: the dots k needs beyond k - 1 leave ball k, the ones +// it no longer needs go out where they stand. A ball lands every 0.3 s, so the counter steers with +// the same controller made five times as fast, KP 25 and KD 5 times over, still critically damped. +// What the light does is derived from the state, as in the engine: a dot and a ball glow by +// glow_of(v); the floor mirrors everything with the Fresnel reflectance of lacquer at the angle +// the engine's camera sees it (mirror_alpha); light adds and the exposure 1 - exp(-L) never clips. +// +// THE OUTPUT: `main` writes FRAMES frames of FRAME_W x FRAME_H raw RGB24 to stderr, row by row, as +// specs/t27f/reel_particles.t27 does; ffmpeg reads them with plain flags +// (`-f rawvideo -pix_fmt rgb24 -s 1080x1920 -r 30 -i frames.rgb`). +// +// NOT CLAIMED: the ball mass, the drop height, the tube and the light levels are design choices, +// labelled as such. The balls are not a model of any computer; they are a bar chart that obeys +// Newton. +// phi^2 + 1/phi^2 = 3 | TRINITY + +module token_floor; + +use physics::physical_limits::landauer_j; +use physics::physical_limits::index_bits; +use physics::physical_limits::token_energy_j; +use physics::physical_limits::gap; +use physics::physical_limits::PROMPT_J; +use physics::physical_limits::TOKENS_PER_PROMPT; +use physics::physical_limits::VOCAB; +use physics::physical_limits::ROOM_K; +use t27f::particle_engine::G_STD; +use t27f::particle_engine::H; +use t27f::particle_engine::SUB; +use t27f::particle_engine::C_CONTACT; +use t27f::particle_engine::KP; +use t27f::particle_engine::KD; +use t27f::particle_engine::contact_f_over_r; +use t27f::particle_engine::mirror_alpha; +use t27f::particle_engine::schlick; +use t27f::particle_engine::gauss; +use t27f::particle_engine::expose; +use t27f::particle_engine::glow_of; +use t27f::reel_particles::lcg; +use t27f::reel_particles::unit; +use t27f::reel_particles::quantize; + +pub const ID : str = "token-floor"; +pub const VERSION : u8 = 1; + +// ---- frame and clock ---- +pub const FRAME_W : i32 = 1080; +pub const FRAME_H : i32 = 1920; +pub const FPS : i32 = 30; +pub const FRAMES : i32 = 450; // 15.000 s +pub const STEPS : i64 = 16; // two engine frames of SUB substeps of H per 1/30 s + +// ---- the stage, px; 1 px = 1 mm ---- +pub const PXM : f64 = 1000.0; +pub const FLOOR_PX : f64 = 1640.0; +pub const TUBE_X : f64 = 860.0; +pub const TUBE_TOP : f64 = 250.0; +pub const TUBE_HALF : f64 = 38.0; // the inner radius of the tube, px +pub const R_PX : f64 = 34.0; + +// ---- the balls (design choices) ---- +pub const BALLS : usize = 20; +pub const GOLD : i64 = 19; +pub const R_BALL : f64 = 0.034; // m +pub const M_BALL : f64 = 0.2; // kg: heavy enough that the dashpot's explicit step is stable +pub const H_DROP : f64 = 1.75; // m above the floor, just over the top of the frame +pub const REL0 : i32 = 60; // the white ball drops at 2 s +pub const REL1 : i32 = 96; // the first gold ball at 3.2 s +pub const REL_GAP : i32 = 9; // then one every 0.3 s + +// ---- light (design choices), in units the exposure maps to 0..255 ---- +pub const BG : f64 = 0.04; // about #0A0A0A +pub const KEY : f64 = 0.035; +pub const KEY_X : f64 = 760.0; +pub const KEY_Y : f64 = 980.0; +pub const KEY_R : f64 = 560.0; +pub const FLOOR_DIM : f64 = 0.75; +pub const FLOOR_KEY : f64 = 0.16; +pub const GLINT : f64 = 0.12; +pub const GLINT_W : f64 = 1.5; +pub const GLINT_R : f64 = 380.0; +pub const FADE : f64 = 120.0; // a glossy floor's image dies away with depth, px +pub const REFLECT_SPAN : f64 = 360.0; +pub const TINT_R : f64 = 1.0; +pub const TINT_G : f64 = 0.93; +pub const TINT_B : f64 = 0.84; // the key light is warm +pub const WALL_A : f64 = 0.14; +pub const STREAK_A : f64 = 0.05; +pub const AMB : f64 = 0.08; +pub const SPEC : f64 = 2.2; +pub const RIM : f64 = 0.5; +pub const LX : f64 = -0.42; +pub const LY : f64 = -0.56; +pub const LZ : f64 = 0.714; // the key light from up-left, in front +pub const HALO : f64 = 0.10; +pub const HALO_R : f64 = 1.25; + +// ---- the letters ---- +pub const DOT_R : f64 = 0.42; // a dot's radius as a share of the glyph pitch +pub const RAMP : f64 = 6.0; // frames for a new dot to light +pub const OFF_FADE : f64 = 10.0; // frames for a counter dot no longer needed to go out +pub const POOL : usize = 48; // the counter's particles +pub const COUNTER_GAIN : f64 = 5.0; // the counter steers five times as fast as the letters +pub const SCAT_X : f64 = 700.0; +pub const SCAT_Y : f64 = 420.0; +pub const JIT : f64 = 0.8; // dots leave a ball from inside 80 percent of its radius +pub const EXP_X : f64 = 318.0; +pub const EXP_Y : f64 = 668.0; +pub const EXP_PITCH : f64 = 8.0; +pub const TICK_X : f64 = 916.0; +pub const TICK_PITCH : f64 = 4.0; +pub const TICK_DELAY : f64 = 3.0; + +pub const MAX_DOTS : usize = 6144; +pub const MAX_SPLATS : usize = 12416; // every dot, its mirror image, and 128 more +pub const ROW_BYTES : usize = 3240; + +// Splat kinds. +pub const K_DOT : u8 = 0; +pub const K_GLOW : u8 = 1; +pub const K_BALL : u8 = 2; +pub const K_BALLF : u8 = 3; // a ball's mirror image +pub const K_VLINE : u8 = 4; + +// Every word on the screen, one label per segment between bars. Labels 0-13 are placed by LT, 14-32 +// are the counter exponents 1..19, 33-52 the decade ticks E-20..E-1 beside the balls. +pub const TEXT : str = "ONE TOKEN|VS PHYSICS|PHYSICS FLOOR|4.9E-20 J|LANDAUER, 17 BITS, 300 K|X10|1.7 J PER TOKEN|THAT IS 19 DECADES|ABOVE THE FLOOR|THE BILL IS ENGINEERING|NOT THERMODYNAMICS|SOURCE 0.24 WH A PROMPT, GOOGLE 2025|ASSUMED 500 TOKENS A REPLY, VOCAB 128256|T27.AI|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|E-20|E-19|E-18|E-17|E-16|E-15|E-14|E-13|E-12|E-11|E-10|E-9|E-8|E-7|E-6|E-5|E-4|E-3|E-2|E-1"; +pub const LABELS : usize = 53; +pub const FIXED : usize = 14; + +// x, y (top left, px), pitch (px a dot), colour, trigger (0 = the clock, k + 1 = ball k landing), +// delay (frames after the trigger), start (0 = out of the ball, 1 = scattered). +pub const LT : [98]f64 = [ + 60.0, 70.0, 9.0, 0.0, 0.0, 3.0, 1.0, + 60.0, 150.0, 9.0, 1.0, 0.0, 10.0, 1.0, + 60.0, 1440.0, 6.0, 2.0, 1.0, 0.0, 0.0, + 60.0, 1494.0, 9.0, 1.0, 1.0, 5.0, 0.0, + 60.0, 1582.0, 4.0, 2.0, 1.0, 10.0, 0.0, + 60.0, 700.0, 14.0, 0.0, 2.0, 0.0, 0.0, + 60.0, 300.0, 6.0, 0.0, 20.0, 4.0, 0.0, + 60.0, 830.0, 6.0, 1.0, 20.0, 30.0, 1.0, + 60.0, 882.0, 6.0, 1.0, 20.0, 36.0, 1.0, + 60.0, 1000.0, 4.0, 2.0, 20.0, 60.0, 1.0, + 60.0, 1036.0, 4.0, 2.0, 20.0, 66.0, 1.0, + 60.0, 1810.0, 4.0, 3.0, 0.0, 20.0, 1.0, + 60.0, 1842.0, 4.0, 3.0, 0.0, 26.0, 1.0, + 60.0, 1874.0, 4.0, 0.0, 0.0, 32.0, 1.0 +]; + +// Light of the four text colours, r g b: gold, white, grey, dim. +pub const COL : [12]f64 = [2.4, 1.45, 0.35, 2.3, 2.3, 2.3, 1.0, 1.0, 1.0, 0.6, 0.6, 0.6]; +// Base light of the white ball and of a gold ball, r g b. +pub const BALL_COL : [6]f64 = [1.5, 1.5, 1.6, 1.7, 1.0, 0.22]; + +// A 5x7 dot-matrix font, seven rows a glyph, bit 4 the leftmost column: 0-9, A-Z, '.', '-', ',', ' '. +pub const FONT : [280]u8 = [ + 14, 17, 19, 21, 25, 17, 14, + 4, 12, 4, 4, 4, 4, 14, + 14, 17, 1, 2, 4, 8, 31, + 31, 2, 4, 2, 1, 17, 14, + 2, 6, 10, 18, 31, 2, 2, + 31, 16, 30, 1, 1, 17, 14, + 6, 8, 16, 30, 17, 17, 14, + 31, 1, 2, 4, 8, 8, 8, + 14, 17, 17, 14, 17, 17, 14, + 14, 17, 17, 15, 1, 2, 12, + 14, 17, 17, 31, 17, 17, 17, + 30, 17, 17, 30, 17, 17, 30, + 14, 17, 16, 16, 16, 17, 14, + 28, 18, 17, 17, 17, 18, 28, + 31, 16, 16, 30, 16, 16, 31, + 31, 16, 16, 30, 16, 16, 16, + 14, 17, 16, 23, 17, 17, 15, + 17, 17, 17, 31, 17, 17, 17, + 14, 4, 4, 4, 4, 4, 14, + 7, 2, 2, 2, 2, 18, 12, + 17, 18, 20, 24, 20, 18, 17, + 16, 16, 16, 16, 16, 16, 31, + 17, 27, 21, 21, 17, 17, 17, + 17, 17, 25, 21, 19, 17, 17, + 14, 17, 17, 17, 17, 17, 14, + 30, 17, 17, 30, 16, 16, 16, + 14, 17, 17, 17, 21, 18, 13, + 30, 17, 17, 30, 20, 18, 17, + 15, 16, 16, 14, 1, 1, 30, + 31, 4, 4, 4, 4, 4, 4, + 17, 17, 17, 17, 17, 17, 14, + 17, 17, 17, 17, 17, 10, 4, + 17, 17, 17, 21, 21, 21, 10, + 17, 17, 10, 4, 10, 17, 17, + 17, 17, 17, 10, 4, 4, 4, + 31, 1, 2, 4, 8, 16, 31, + 0, 0, 0, 0, 0, 12, 12, + 0, 0, 0, 14, 0, 0, 0, + 0, 0, 0, 0, 12, 4, 8, + 0, 0, 0, 0, 0, 0, 0 +]; + +// ---- the fact ---- + +fn floor_j() -> f64 { + return index_bits(VOCAB) * landauer_j(ROOM_K); +} + +fn token_j() -> f64 { + return token_energy_j(PROMPT_J, TOKENS_PER_PROMPT); +} + +fn log10(x: f64) -> f64 { + return @log(x) / @log(10.0); +} + +// Whole decades between the bill and the floor. +fn decades() -> i64 { + const g = gap(token_j(), index_bits(VOCAB), ROOM_K); + return @as(i64, @intFromFloat(@floor(log10(g)))); +} + +fn floor_exp() -> i64 { + return @as(i64, @intFromFloat(@floor(log10(floor_j())))); +} + +// ---- the text ---- + +fn seg_start(l: usize) -> usize { + var k : usize = 0; + var i : usize = 0; + while (k < l) { + if (TEXT[i] == 124) { + k = k + 1; + } + i = i + 1; + } + return i; +} + +fn seg_len(l: usize) -> usize { + const s = seg_start(l); + var i : usize = s; + while (i < TEXT.len && TEXT[i] != 124) { + i = i + 1; + } + return i - s; +} + +fn label_char(l: usize, c: usize) -> u8 { + return TEXT[seg_start(l) + c]; +} + +// The decimal number written in characters from..from+len-1 of label l. +fn label_int(l: usize, from: usize, len: usize) -> i64 { + var v : i64 = 0; + var c : usize = from; + while (c < from + len) { + v = v * 10 + @as(i64, @intCast(label_char(l, c))) - 48; + c = c + 1; + } + return v; +} + +fn label_count() -> usize { + var n : usize = 1; + var i : usize = 0; + while (i < TEXT.len) { + if (TEXT[i] == 124) { + n = n + 1; + } + i = i + 1; + } + return n; +} + +fn glyph(c: u8) -> i64 { + const v = @as(i64, @intCast(c)); + if (v >= 48 && v <= 57) { return v - 48; } + if (v >= 65 && v <= 90) { return v - 55; } + if (v == 46) { return 36; } + if (v == 45) { return 37; } + if (v == 44) { return 38; } + if (v == 32) { return 39; } + return 0 - 1; +} + +// Is the dot at row `row`, column `col` of character c lit? +fn lit(c: u8, row: i64, col: i64) -> bool { + const g = glyph(c); + if (g < 0) { + return false; + } + const v = @as(i64, @intCast(FONT[@as(usize, @intCast(g * 7 + row))])); + var p : i64 = 16; + var k : i64 = 0; + while (k < col) { + p = @divTrunc(p, 2); + k = k + 1; + } + return @mod(@divTrunc(v, p), 2) == 1; +} + +fn label_dots(l: usize) -> i64 { + const n = seg_len(l); + var d : i64 = 0; + var c : usize = 0; + while (c < n) { + const ch = label_char(l, c); + var r : i64 = 0; + while (r < 7) { + var b : i64 = 0; + while (b < 5) { + if (lit(ch, r, b)) { + d = d + 1; + } + b = b + 1; + } + r = r + 1; + } + c = c + 1; + } + return d; +} + +fn total_dots() -> i64 { + var d : i64 = 0; + var l : usize = 0; + while (l < LABELS) { + d = d + label_dots(l); + l = l + 1; + } + return d; +} + +// ---- where each label goes and what starts it ---- + +// The height of ball k's centre above the floor once all twenty rest: each contact is pressed by +// the weight of every ball above it, and Hertz gives the overlap d = (F / K)^(2/3). +fn hertz_overlap(load: f64) -> f64 { + return @exp(@log(load / 100000.0) * 2.0 / 3.0); +} + +fn rest_y(k: usize) -> f64 { + const w = M_BALL * G_STD; + var y : f64 = R_BALL - hertz_overlap(@as(f64, @floatFromInt(BALLS)) * w); + var j : usize = 1; + while (j <= k) { + y = y + 2.0 * R_BALL - hertz_overlap(@as(f64, @floatFromInt(BALLS - j)) * w); + j = j + 1; + } + return y; +} + +fn rest_px(k: usize) -> f64 { + return FLOOR_PX - rest_y(k) * PXM; +} + +// Field f of label l: 0 x, 1 y, 2 pitch, 3 colour, 4 trigger, 5 delay, 6 start. +fn lbl_f(l: usize, f: usize) -> f64 { + if (l < FIXED) { + return LT[l * 7 + f]; + } + if (l < FIXED + 19) { + const k = @as(f64, @floatFromInt(l - FIXED + 1)); + if (f == 0) { return EXP_X; } + if (f == 1) { return EXP_Y; } + if (f == 2) { return EXP_PITCH; } + if (f == 4) { return k + 1.0; } + return 0.0; + } + const j = l - FIXED - 19; + if (f == 0) { return TICK_X; } + if (f == 1) { return rest_px(j) - 3.5 * TICK_PITCH; } + if (f == 2) { return TICK_PITCH; } + if (f == 3) { + if (j == 0) { return 1.0; } + return 2.0; + } + if (f == 4) { return @as(f64, @floatFromInt(j)) + 1.0; } + if (f == 5) { return TICK_DELAY; } + return 0.0; +} + +// The ball whose landing starts label l, or -1 for the clock. +fn lbl_trig(l: usize) -> i64 { + return @as(i64, @intFromFloat(lbl_f(l, 4))) - 1; +} + +// Labels 14-32 are the counter's exponents: places for the counter's pool, never drawn themselves. +fn is_counter(l: usize) -> bool { + return l >= FIXED && l < FIXED + 19; +} + +// The most dots any exponent needs; the pool holds them all. +fn counter_need() -> i64 { + var m : i64 = 0; + var l : usize = FIXED; + while (l < FIXED + 19) { + if (label_dots(l) > m) { m = label_dots(l); } + l = l + 1; + } + return m; +} + +// ---- the balls ---- + +fn release_frame(i: usize) -> i32 { + if (i == 0) { + return REL0; + } + return REL1 + @as(i32, @intCast(i - 1)) * REL_GAP; +} + +// Hertz push plus the dashpot between two surfaces sqrt(r2) = r apart that touch at d0, closing at +// `closing` m/s. A contact pushes; it never pulls. +fn push(r: f64, closing: f64, d0: f64) -> f64 { + if (r >= d0) { + return 0.0; + } + var f : f64 = contact_f_over_r(r * r, d0) * r + C_CONTACT * closing; + if (f < 0.0) { + f = 0.0; + } + return f; +} + +// One displayed frame of the column: STEPS velocity Verlet substeps under gravity and contact. +// The tube keeps the order, so only the floor and neighbours can touch. A contact can open and +// close inside one frame, so the substeps record the first touch of each ball in `landed` and the +// lowest white ball and the closest pair in ext[0] and ext[1]. +fn stack_step(y: *[20]f64, v: *[20]f64, a: *[20]f64, landed: *[20]i32, ext: *[2]f64, frame: i32) -> void { + var s : i64 = 0; + while (s < STEPS) { + var i : usize = 0; + while (i < BALLS) { + if (frame >= release_frame(i)) { + v[i] = v[i] + 0.5 * a[i] * H; + y[i] = y[i] + v[i] * H; + } + i = i + 1; + } + i = 0; + while (i < BALLS) { + a[i] = 0.0 - G_STD; + i = i + 1; + } + a[0] = a[0] + push(y[0], 0.0 - v[0], R_BALL) / M_BALL; + if (landed[0] < 0 && y[0] < R_BALL) { + landed[0] = frame; + } + if (y[0] < ext[0]) { + ext[0] = y[0]; + } + i = 1; + while (i < BALLS) { + if (frame >= release_frame(i)) { + const gp = y[i] - y[i - 1]; + const f = push(gp, v[i - 1] - v[i], 2.0 * R_BALL) / M_BALL; + a[i] = a[i] + f; + a[i - 1] = a[i - 1] - f; + if (landed[i] < 0 && gp < 2.0 * R_BALL) { + landed[i] = frame; + } + if (gp < ext[1]) { + ext[1] = gp; + } + } + i = i + 1; + } + i = 0; + while (i < BALLS) { + if (frame >= release_frame(i)) { + v[i] = v[i] + 0.5 * a[i] * H; + } + i = i + 1; + } + s = s + 1; + } +} + +// The frame a ball dropped from rest at H_DROP first touches an uncompressed stack, by free fall. +fn land_frame(k: usize) -> i32 { + const h = H_DROP - R_BALL - 2.0 * R_BALL * @as(f64, @floatFromInt(k)); + const t = @sqrt(2.0 * h / G_STD); + return release_frame(k) + @as(i32, @intFromFloat(@ceil(t * @as(f64, @floatFromInt(FPS))))) - 1; +} + +// Run the column for `frames` frames and report: 0 y of ball idx, 1 the largest speed, 2 the lowest +// the white ball ever got, 3 the smallest centre gap between neighbours, 4 the landing frame of +// ball idx, 5 the largest miss of a landing against land_frame, 6 the largest miss of a resting +// height against rest_y. +fn stack_sim(frames: i32, metric: i64, idx: usize) -> f64 { + var y : [20]f64 = undefined; + var v : [20]f64 = undefined; + var a : [20]f64 = undefined; + var landed : [20]i32 = undefined; + var i : usize = 0; + while (i < BALLS) { + y[i] = H_DROP; + v[i] = 0.0; + a[i] = 0.0 - G_STD; + landed[i] = 0 - 1; + i = i + 1; + } + var ext : [2]f64 = [1000000000.0, 1000000000.0]; + var frame : i32 = 0; + while (frame < frames) { + stack_step(&y, &v, &a, &landed, &ext, frame); + frame = frame + 1; + } + if (metric == 0) { + return y[idx]; + } + if (metric == 1) { + var m : f64 = 0.0; + i = 0; + while (i < BALLS) { + if (@abs(v[i]) > m) { m = @abs(v[i]); } + i = i + 1; + } + return m; + } + if (metric == 2) { + return ext[0]; + } + if (metric == 3) { + return ext[1]; + } + if (metric == 4) { + return @as(f64, @floatFromInt(landed[idx])); + } + var m : f64 = 0.0; + i = 0; + while (i < BALLS) { + var e : f64 = 0.0; + if (metric == 5) { + e = @abs(@as(f64, @floatFromInt(landed[i] - land_frame(i)))); + } else { + e = @abs(y[i] - rest_y(i)); + } + if (e > m) { m = e; } + i = i + 1; + } + return m; +} + +// The stiffest mode of the column times H, and the dashpot's explicit step: both must stay under 2 +// for velocity Verlet to be stable. d is the deepest overlap the run reaches. +fn omega_h(d: f64) -> f64 { + const k = 1.5 * 100000.0 * @sqrt(d); + return 2.0 * @sqrt(k / M_BALL) * H; +} + +fn damp_h() -> f64 { + return 4.0 * C_CONTACT / M_BALL * H; +} + +// ---- the letters' controller ---- + +// The engine's controller made g times as fast: KP g^2 and KD g keep it critically damped. +fn pd_accel(x: f64, v: f64, t: f64, g: f64) -> f64 { + return KP * g * g * (t - x) - KD * g * v; +} + +// A dot steered from x0 to 0 at rest; report 0 its position, 1 the lowest it got, 2 its top speed. +fn pd_after(steps: i64, x0: f64, metric: i64, g: f64) -> f64 { + var x : f64 = x0; + var v : f64 = 0.0; + var low : f64 = x0; + var top : f64 = 0.0; + var s : i64 = 0; + while (s < steps) { + v = v + pd_accel(x, v, 0.0, g) * H; + x = x + v * H; + if (x < low) { low = x; } + if (@abs(v) > top) { top = @abs(v); } + s = s + 1; + } + if (metric == 1) { return low; } + if (metric == 2) { return top; } + return x; +} + +// The critically damped path from rest: x(t) = x0 (1 + w t) e^(-w t), w = g sqrt(KP). +fn pd_closed(t: f64, x0: f64, g: f64) -> f64 { + const w = g * @sqrt(KP); + return x0 * (1.0 + w * t) * @exp(0.0 - w * t); +} + +// ---- light ---- + +fn clamp01(x: f64) -> f64 { + if (x < 0.0) { return 0.0; } + if (x > 1.0) { return 1.0; } + return x; +} + +fn key_w(d: f64) -> f64 { + return gauss(d * d, KEY_R); +} + +fn glint_x(d: f64) -> f64 { + return gauss(d * d, GLINT_R); +} + +fn glint_y(d: f64) -> f64 { + return gauss(d * d, GLINT_W); +} + +// The light of the empty scene from its separable factors; below the floor the key light is seen +// in the mirror and the horizon carries a glint. +fn back_light(kx: f64, ky: f64, gx: f64, gy: f64, below: bool) -> f64 { + if (below == false) { + return BG + KEY * kx * ky; + } + return BG * FLOOR_DIM + KEY * FLOOR_KEY * kx * ky + GLINT * gx * gy; +} + +fn backdrop(x: f64, y: f64) -> f64 { + if (y <= FLOOR_PX) { + return back_light(key_w(x - KEY_X), key_w(y - KEY_Y), 0.0, 0.0, false); + } + return back_light(key_w(x - KEY_X), key_w(2.0 * FLOOR_PX - y - KEY_Y), glint_x(x - TUBE_X), glint_y(y - FLOOR_PX), true); +} + +// How strongly the floor mirrors something drawn at screen height y_px. +fn reflect_alpha(y_px: f64) -> f64 { + return mirror_alpha((FLOOR_PX - y_px) / PXM, 1.0); +} + +// The light a glossy ball sends to the camera from the point whose normal is (nx, ny): Lambert +// under the key light, a Blinn highlight, and a Fresnel rim, the lacquer law of the engine. +fn ball_light(nx: f64, ny: f64, base: f64) -> f64 { + var q : f64 = 1.0 - nx * nx - ny * ny; + if (q < 0.0) { q = 0.0; } + const nz = @sqrt(q); + var lam : f64 = nx * LX + ny * LY + nz * LZ; + if (lam < 0.0) { lam = 0.0; } + const hz = LZ + 1.0; + const hn = @sqrt(LX * LX + LY * LY + hz * hz); + var s : f64 = (nx * LX + ny * LY + nz * hz) / hn; + if (s < 0.0) { s = 0.0; } + const s2 = s * s; + const s5 = s2 * s2 * s; + const s10 = s5 * s5; + const s20 = s10 * s10; + const rim = schlick(nz, 1.5) - schlick(1.0, 1.5); + return base * (AMB + (1.0 - AMB) * lam) + SPEC * s20 * s20 + RIM * rim; +} + +fn reel_ms() -> i32 { + return FRAMES * 1000 / FPS; +} + +fn frame_bytes() -> i32 { + return FRAME_W * FRAME_H * 3; +} + +// ---- the engine ---- + +pub fn main() -> void { + var by : [20]f64 = undefined; + var bv : [20]f64 = undefined; + var ba : [20]f64 = undefined; + var landed : [20]i32 = undefined; + var bext : [2]f64 = [1000000000.0, 1000000000.0]; + var i : usize = 0; + while (i < BALLS) { + by[i] = H_DROP; + bv[i] = 0.0; + ba[i] = 0.0 - G_STD; + landed[i] = 0 - 1; + i = i + 1; + } + var lon : [53]i32 = undefined; + var l : usize = 0; + while (l < LABELS) { + lon[l] = 0 - 1; + l = l + 1; + } + + // one particle per lit dot of every label, with its place in the label + var dl : [6144]usize = undefined; + var dtx : [6144]f64 = undefined; + var dty : [6144]f64 = undefined; + var dx : [6144]f64 = undefined; + var dy : [6144]f64 = undefined; + var dvx : [6144]f64 = undefined; + var dvy : [6144]f64 = undefined; + var nd : usize = 0; + var lfirst : [53]usize = undefined; + var lnum : [53]usize = undefined; + l = 0; + while (l < LABELS) { + lfirst[l] = nd; + const st = seg_start(l); + const ln = seg_len(l); + const x0 = lbl_f(l, 0); + const y0 = lbl_f(l, 1); + const p = lbl_f(l, 2); + var c : usize = 0; + while (c < ln) { + const ch = TEXT[st + c]; + var r : i64 = 0; + while (r < 7) { + var b : i64 = 0; + while (b < 5) { + if (lit(ch, r, b)) { + dl[nd] = l; + dtx[nd] = x0 + (@as(f64, @floatFromInt(c)) * 6.0 + @as(f64, @floatFromInt(b)) + 0.5) * p; + dty[nd] = y0 + (@as(f64, @floatFromInt(r)) + 0.5) * p; + dx[nd] = dtx[nd]; + dy[nd] = dty[nd]; + dvx[nd] = 0.0; + dvy[nd] = 0.0; + nd = nd + 1; + } + b = b + 1; + } + r = r + 1; + } + c = c + 1; + } + lnum[l] = nd - lfirst[l]; + l = l + 1; + } + // the counter's exponent is one pool of particles that re-forms into each next number; the + // labels 14-32 only hold the places its dots go + var cx : [48]f64 = undefined; + var cy : [48]f64 = undefined; + var cvx : [48]f64 = undefined; + var cvy : [48]f64 = undefined; + var ctx : [48]f64 = undefined; + var cty : [48]f64 = undefined; + var ca : [48]f64 = undefined; + var cborn : [48]bool = undefined; + var ci0 : usize = 0; + while (ci0 < POOL) { + ca[ci0] = 0.0; + cborn[ci0] = false; + ci0 = ci0 + 1; + } + + var sx : [12416]f64 = undefined; + var sy : [12416]f64 = undefined; + var sr : [12416]f64 = undefined; + var sw : [12416]f64 = undefined; + var sa : [12416]f64 = undefined; + var sc : [12416]f64 = undefined; + var scr : [12416]f64 = undefined; + var scg : [12416]f64 = undefined; + var scb : [12416]f64 = undefined; + var sk : [12416]u8 = undefined; + var sm : [12416]u8 = undefined; + var accr : [1080]f64 = undefined; + var accg : [1080]f64 = undefined; + var accb : [1080]f64 = undefined; + var kxa : [1080]f64 = undefined; + var gxa : [1080]f64 = undefined; + var row : [3240]u8 = undefined; + var px : usize = 0; + while (px < 1080) { + const x = @as(f64, @floatFromInt(px)) + 0.5; + kxa[px] = key_w(x - KEY_X); + gxa[px] = glint_x(x - TUBE_X); + px = px + 1; + } + const mir0 = reflect_alpha(FLOOR_PX); + + var frame : i32 = 0; + while (frame < FRAMES) { + // ---- physics: the column, then what its landings start and stop ---- + stack_step(&by, &bv, &ba, &landed, &bext, frame); + l = 0; + while (l < LABELS) { + const tr = lbl_trig(l); + const dl0 = @as(i32, @intFromFloat(lbl_f(l, 5))); + if (lon[l] < 0) { + if (tr < 0) { + lon[l] = dl0; + } else { + const lt = landed[@as(usize, @intCast(tr))]; + if (lt >= 0) { + lon[l] = lt + dl0; + } + } + } + l = l + 1; + } + + // ---- the letters: each dot is steered to its place by the controller ---- + var d : usize = 0; + while (d < nd) { + const ld = dl[d]; + const on = lon[ld]; + if (is_counter(ld) == false && on >= 0 && frame >= on) { + if (frame == on) { + var seed : i64 = lcg(@as(i64, @intCast(d)) * 7919 + 101); + const u1 = unit(seed); + seed = lcg(seed); + const u2 = unit(seed); + if (lbl_f(ld, 6) < 0.5) { + const src = @as(usize, @intCast(lbl_trig(ld))); + dx[d] = TUBE_X + (u1 - 0.5) * 2.0 * JIT * R_PX; + dy[d] = FLOOR_PX - by[src] * PXM + (u2 - 0.5) * 2.0 * JIT * R_PX; + } else { + dx[d] = dtx[d] + (u1 - 0.5) * SCAT_X; + dy[d] = dty[d] + (u2 - 0.5) * SCAT_Y; + } + dvx[d] = 0.0; + dvy[d] = 0.0; + } + var s : i64 = 0; + while (s < STEPS) { + dvx[d] = dvx[d] + pd_accel(dx[d], dvx[d], dtx[d], 1.0) * H; + dvy[d] = dvy[d] + pd_accel(dy[d], dvy[d], dty[d], 1.0) * H; + dx[d] = dx[d] + dvx[d] * H; + dy[d] = dy[d] + dvy[d] * H; + s = s + 1; + } + } + d = d + 1; + } + // the counter: the newest landed gold ball names the exponent; its dots are taken from the + // pool in order, the ones born now leave that ball, the ones not needed go out where they are + var kc : usize = 0; + var kk0 : usize = 1; + while (kk0 < BALLS) { + if (landed[kk0] >= 0) { kc = kk0; } + kk0 = kk0 + 1; + } + var need : usize = 0; + if (kc > 0) { need = lnum[FIXED + kc - 1]; } + var pi : usize = 0; + while (pi < POOL) { + if (pi < need) { + const td = lfirst[FIXED + kc - 1] + pi; + ctx[pi] = dtx[td]; + cty[pi] = dty[td]; + if (cborn[pi] == false) { + var seed : i64 = lcg(@as(i64, @intCast(pi)) * 6007 + 211); + const u1 = unit(seed); + seed = lcg(seed); + const u2 = unit(seed); + cx[pi] = TUBE_X + (u1 - 0.5) * 2.0 * JIT * R_PX; + cy[pi] = FLOOR_PX - by[kc] * PXM + (u2 - 0.5) * 2.0 * JIT * R_PX; + cvx[pi] = 0.0; + cvy[pi] = 0.0; + cborn[pi] = true; + } + ca[pi] = clamp01(ca[pi] + 1.0 / RAMP); + } else { + ca[pi] = clamp01(ca[pi] - 1.0 / OFF_FADE); + } + if (cborn[pi]) { + var s : i64 = 0; + while (s < STEPS) { + cvx[pi] = cvx[pi] + pd_accel(cx[pi], cvx[pi], ctx[pi], COUNTER_GAIN) * H; + cvy[pi] = cvy[pi] + pd_accel(cy[pi], cvy[pi], cty[pi], COUNTER_GAIN) * H; + cx[pi] = cx[pi] + cvx[pi] * H; + cy[pi] = cy[pi] + cvy[pi] * H; + s = s + 1; + } + } + pi = pi + 1; + } + + // ---- splats, back to front ---- + var n : usize = 0; + // the tube's image in the floor + var w : usize = 0; + while (w < 2) { + sx[n] = TUBE_X - TUBE_HALF + 2.0 * TUBE_HALF * @as(f64, @floatFromInt(w)); + sy[n] = FLOOR_PX; sw[n] = FLOOR_PX + REFLECT_SPAN; sr[n] = 0.8; sa[n] = WALL_A * mir0; + scr[n] = 1.0; scg[n] = 1.0; scb[n] = 1.0; sk[n] = K_VLINE; sm[n] = 1; n = n + 1; + w = w + 1; + } + // the letters and their images + d = 0; + while (d < nd) { + const ld = dl[d]; + const on = lon[ld]; + if (is_counter(ld) == false && on >= 0 && frame >= on) { + const a = clamp01(@as(f64, @floatFromInt(frame - on + 1)) / RAMP); + if (a > 0.0) { + const ci = @as(usize, @intFromFloat(lbl_f(ld, 3))) * 3; + const p = lbl_f(ld, 2); + const gl = glow_of(dvx[d] / PXM, dvy[d] / PXM); + sx[n] = dx[d]; sy[n] = dy[d]; sr[n] = DOT_R * p; sw[n] = 0.0; sa[n] = a * gl; + scr[n] = COL[ci]; scg[n] = COL[ci + 1]; scb[n] = COL[ci + 2]; sk[n] = K_DOT; sm[n] = 0; + n = n + 1; + if (dy[d] < FLOOR_PX && FLOOR_PX - dy[d] < REFLECT_SPAN) { + sx[n] = dx[d]; sy[n] = 2.0 * FLOOR_PX - dy[d]; sr[n] = DOT_R * p; sw[n] = 0.0; + sa[n] = a * gl * reflect_alpha(dy[d]); + scr[n] = COL[ci]; scg[n] = COL[ci + 1]; scb[n] = COL[ci + 2]; sk[n] = K_DOT; sm[n] = 1; + n = n + 1; + } + } + } + d = d + 1; + } + // the counter's pool; it hangs far above the floor's reach, so it has no image + pi = 0; + while (pi < POOL) { + if (cborn[pi] && ca[pi] > 0.0) { + sx[n] = cx[pi]; sy[n] = cy[pi]; sr[n] = DOT_R * EXP_PITCH; sw[n] = 0.0; + sa[n] = ca[pi] * glow_of(cvx[pi] / PXM, cvy[pi] / PXM); + scr[n] = COL[0]; scg[n] = COL[1]; scb[n] = COL[2]; sk[n] = K_DOT; sm[n] = 0; + n = n + 1; + } + pi = pi + 1; + } + // the balls, their halos and their images + i = 0; + while (i < BALLS) { + if (frame >= release_frame(i)) { + var bc : usize = 3; + if (i == 0) { bc = 0; } + const yp = FLOOR_PX - by[i] * PXM; + sx[n] = TUBE_X; sy[n] = yp; sr[n] = HALO_R * R_PX; sw[n] = 0.0; sa[n] = HALO * glow_of(0.0, bv[i]); + scr[n] = BALL_COL[bc]; scg[n] = BALL_COL[bc + 1]; scb[n] = BALL_COL[bc + 2]; sk[n] = K_GLOW; sm[n] = 0; + n = n + 1; + sx[n] = TUBE_X; sy[n] = yp; sr[n] = R_PX; sw[n] = 0.0; sa[n] = 1.0; + scr[n] = BALL_COL[bc]; scg[n] = BALL_COL[bc + 1]; scb[n] = BALL_COL[bc + 2]; sk[n] = K_BALL; sm[n] = 0; + n = n + 1; + if (yp < FLOOR_PX && FLOOR_PX - yp < REFLECT_SPAN) { + sx[n] = TUBE_X; sy[n] = 2.0 * FLOOR_PX - yp; sr[n] = R_PX; sw[n] = 0.0; sa[n] = reflect_alpha(yp); + scr[n] = BALL_COL[bc]; scg[n] = BALL_COL[bc + 1]; scb[n] = BALL_COL[bc + 2]; sk[n] = K_BALLF; sm[n] = 1; + n = n + 1; + } + } + i = i + 1; + } + // the glass walls, in front + w = 0; + while (w < 2) { + sx[n] = TUBE_X - TUBE_HALF + 2.0 * TUBE_HALF * @as(f64, @floatFromInt(w)); + sy[n] = TUBE_TOP; sw[n] = FLOOR_PX; sr[n] = 0.8; sa[n] = WALL_A; + scr[n] = 1.0; scg[n] = 1.0; scb[n] = 1.0; sk[n] = K_VLINE; sm[n] = 0; n = n + 1; + w = w + 1; + } + sx[n] = TUBE_X - TUBE_HALF + 7.0; sy[n] = TUBE_TOP; sw[n] = FLOOR_PX; sr[n] = 1.6; sa[n] = STREAK_A; + scr[n] = 1.0; scg[n] = 1.0; scb[n] = 1.0; sk[n] = K_VLINE; sm[n] = 0; n = n + 1; + + var j : usize = 0; + while (j < n) { + if (sk[j] == K_GLOW) { + sc[j] = 3.0 * sr[j]; + } else { + sc[j] = sr[j] + 1.0; + } + j = j + 1; + } + + // ---- raster: one row at a time ---- + var py : i32 = 0; + while (py < FRAME_H) { + const y = @as(f64, @floatFromInt(py)) + 0.5; + const below = y > FLOOR_PX; + var ky : f64 = key_w(y - KEY_Y); + var gy : f64 = 0.0; + var fr : f64 = 0.0; + if (below) { + ky = key_w(2.0 * FLOOR_PX - y - KEY_Y); + gy = glint_y(y - FLOOR_PX); + fr = @exp(0.0 - (y - FLOOR_PX) / FADE); + } + px = 0; + while (px < 1080) { + const b = back_light(kxa[px], ky, gxa[px], gy, below); + accr[px] = b * TINT_R; + accg[px] = b * TINT_G; + accb[px] = b * TINT_B; + px = px + 1; + } + j = 0; + while (j < n) { + const k = sk[j]; + var on_row : bool = true; + if (sm[j] == 1 && below == false) { on_row = false; } + var dyy : f64 = y - sy[j]; + if (k == K_VLINE) { + if (y < sy[j] || y > sw[j]) { on_row = false; } + dyy = 0.0; + } else { + if (@abs(dyy) > sc[j]) { on_row = false; } + } + if (on_row) { + var mf : f64 = 1.0; + if (sm[j] == 1) { mf = fr; } + var x0 : f64 = @floor(sx[j] - sc[j]); + var x1 : f64 = @ceil(sx[j] + sc[j]); + if (x0 < 0.0) { x0 = 0.0; } + if (x1 > 1079.0) { x1 = 1079.0; } + var q : i32 = @as(i32, @intFromFloat(x0)); + const qe = @as(i32, @intFromFloat(x1)); + while (q <= qe) { + const ddx = @as(f64, @floatFromInt(q)) + 0.5 - sx[j]; + const kk = @as(usize, @intCast(q)); + if (k == K_BALL || k == K_BALLF) { + const dd = @sqrt(ddx * ddx + dyy * dyy); + const cov = clamp01(sr[j] - dd + 0.5) * sa[j]; + if (cov > 0.0) { + const nx = ddx / sr[j]; + var ny : f64 = dyy / sr[j]; + if (k == K_BALLF) { ny = 0.0 - ny; } + const lr = ball_light(nx, ny, scr[j]); + const lg = ball_light(nx, ny, scg[j]); + const lb = ball_light(nx, ny, scb[j]); + if (k == K_BALL) { + accr[kk] = accr[kk] * (1.0 - cov) + lr * cov; + accg[kk] = accg[kk] * (1.0 - cov) + lg * cov; + accb[kk] = accb[kk] * (1.0 - cov) + lb * cov; + } else { + accr[kk] = accr[kk] + lr * cov * mf; + accg[kk] = accg[kk] + lg * cov * mf; + accb[kk] = accb[kk] + lb * cov * mf; + } + } + } else { + var wt : f64 = 0.0; + if (k == K_GLOW) { + wt = gauss(ddx * ddx + dyy * dyy, sr[j]); + } else { + if (k == K_VLINE) { + wt = clamp01(sr[j] - @abs(ddx) + 0.5); + } else { + wt = clamp01(sr[j] - @sqrt(ddx * ddx + dyy * dyy) + 0.5); + } + } + const e = wt * sa[j] * mf; + accr[kk] = accr[kk] + e * scr[j]; + accg[kk] = accg[kk] + e * scg[j]; + accb[kk] = accb[kk] + e * scb[j]; + } + q = q + 1; + } + } + j = j + 1; + } + px = 0; + while (px < 1080) { + row[px * 3] = quantize(expose(accr[px])); + row[px * 3 + 1] = quantize(expose(accg[px])); + row[px * 3 + 2] = quantize(expose(accb[px])); + px = px + 1; + } + std.debug.print("{s}", .{row}); + py = py + 1; + } + frame = frame + 1; + } +} + +// ---- tests ---- + +test "every number on the screen is the number physical_limits computes" { + // 4.9E-20 J: the Landauer cost of the 17 bits that name one token, at 300 K + assert(floor_exp() == 0 - 20); + assert(@round(floor_j() * 1e21) == 49.0); + assert(label_char(3, 0) == 52); + assert(label_char(3, 2) == 57); + assert(label_int(3, 5, 2) == 0 - floor_exp()); + assert(label_int(4, 10, 2) == @as(i64, @intFromFloat(index_bits(VOCAB)))); + assert(label_int(4, 19, 3) == @as(i64, @intFromFloat(ROOM_K))); + // 1.7 J a token + assert(@round(token_j() * 10.0) == 17.0); + assert(label_char(6, 0) == 49); + assert(label_char(6, 2) == 55); + // the footer: 0.24 Wh a prompt, 500 tokens, a 128256-token vocabulary + assert(@abs(PROMPT_J / 3600.0 - 0.24) < 1e-12); + assert(label_int(11, 9, 2) == 24); + assert(label_int(12, 8, 3) == @as(i64, @intFromFloat(TOKENS_PER_PROMPT))); + assert(label_int(12, 34, 6) == @as(i64, @intFromFloat(VOCAB))); +} + +test "the gap is 19 decades: one gold ball, one exponent and one tick per decade" { + assert(decades() == 19); + assert(GOLD == decades()); + assert(@as(i64, @intCast(BALLS)) == GOLD + 1); + assert(label_int(7, 8, 2) == decades()); + // the counter's exponents count 1..19 and each is started by its own gold ball + var k : usize = 1; + while (k <= 19) { + const l = FIXED + k - 1; + assert(label_int(l, 0, seg_len(l)) == @as(i64, @intCast(k))); + assert(lbl_trig(l) == @as(i64, @intCast(k))); + k = k + 1; + } + // the tick beside ball j reads E(j - 20): the white ball is the floor's own decade + var j : usize = 0; + while (j < BALLS) { + const l = FIXED + 19 + j; + assert(label_char(l, 0) == 69); + assert(0 - label_int(l, 2, seg_len(l) - 2) == @as(i64, @intCast(j)) + floor_exp()); + assert(lbl_trig(l) == @as(i64, @intCast(j))); + j = j + 1; + } +} + +test "the counter re-forms between two landings: its pool holds every number, and it settles in 0.3 s" { + assert(counter_need() <= @as(i64, @intCast(POOL))); + assert(counter_need() > 20); + assert(is_counter(FIXED) && is_counter(FIXED + 18)); + assert(is_counter(FIXED - 1) == false); + assert(is_counter(FIXED + 19) == false); + // a dot moving one digit over, 48 px, is inside 1 px after the 9 frames between two landings, + // on the critically damped path, and never overshoots + const steps = @as(i64, @intCast(REL_GAP)) * STEPS; + assert(@abs(pd_after(steps, 48.0, 0, COUNTER_GAIN)) < 1.0); + assert(@abs(pd_after(steps, 48.0, 0, COUNTER_GAIN) - pd_closed(0.3, 48.0, COUNTER_GAIN)) < 0.1); + assert(pd_after(steps, 48.0, 1, COUNTER_GAIN) > 0.0 - 0.01); + // the engine's letters alone could not: after 0.3 s they are still 66 percent of the way out + assert(pd_after(steps, 48.0, 0, 1.0) > 30.0); + // the stiffer controller is still a small step: w H well under 2 + assert(COUNTER_GAIN * @sqrt(KP) * H < 0.1); +} + +test "every character on the screen has a glyph, and there are 53 labels" { + assert(label_count() == LABELS); + var i : usize = 0; + var bad : i64 = 0; + while (i < TEXT.len) { + if (TEXT[i] != 124 && glyph(TEXT[i]) < 0) { + bad = bad + 1; + } + i = i + 1; + } + assert(bad == 0); + assert(glyph(124) < 0); +} + +test "the glyphs: rows are five bits, the 1 has a foot, the E a full top bar" { + var i : usize = 0; + while (i < 280) { + assert(FONT[i] < 32); + i = i + 1; + } + assert(lit(49, 6, 1) && lit(49, 6, 2) && lit(49, 6, 3)); + assert(lit(49, 6, 0) == false); + assert(lit(69, 0, 0) && lit(69, 0, 4)); + assert(lit(48, 0, 0) == false); + assert(lit(48, 0, 1)); + assert(lit(32, 3, 2) == false); + assert(label_dots(5) == 42); +} + +test "every dot and its image fit the buffers" { + assert(total_dots() > 2000); + assert(total_dots() <= @as(i64, @intCast(MAX_DOTS))); + assert(2 * MAX_DOTS + 128 == MAX_SPLATS); +} + +test "a dot reaches its letter on the critically damped path and never overshoots" { + // 0.5 s: the integrated controller is within 0.5 percent of x0 (1 + w t) e^(-w t) + assert(@abs(pd_after(240, 500.0, 0, 1.0) - pd_closed(0.5, 500.0, 1.0)) < 2.5); + assert(@abs(pd_after(480, 500.0, 0, 1.0) - pd_closed(1.0, 500.0, 1.0)) < 2.5); + // it never crosses its target, and it is inside half a pixel after 3 s + assert(pd_after(1440, 500.0, 1, 1.0) > 0.0 - 0.5); + assert(@abs(pd_after(1440, 500.0, 0, 1.0)) < 0.5); + // its top speed is w x0 / e, and that speed is what makes it glow + assert(@abs(pd_after(1440, 500.0, 2, 1.0) - 2000.0 / 2.718281828459045) < 7.4); + assert(glow_of(0.7, 0.0) > 1.45); +} + +test "STEPS substeps of H make one 1/30 s frame, two engine frames" { + var t : f64 = 0.0; + var n : i64 = 0; + while (n < STEPS) { + t = t + H; + n = n + 1; + } + assert(n == 2 * SUB); + assert(@abs(t - 1.0 / @as(f64, @floatFromInt(FPS))) < 1e-14); +} + +test "a dropped ball falls as g says until it touches" { + // 11 frames after its release at frame 60 the white ball is still in the air + const t = 11.0 / 30.0; + assert(@abs(stack_sim(71, 0, 0) - (H_DROP - 0.5 * G_STD * t * t)) < 1e-9); +} + +test "each ball touches the stack within 3 frames of its free-fall time" { + // early only when the ball beneath is still on its rebound + assert(stack_sim(FRAMES, 5, 0) <= 3.0); + assert(stack_sim(FRAMES, 4, 0) == @as(f64, @floatFromInt(land_frame(0)))); + assert(stack_sim(FRAMES, 4, 19) > 0.0); + assert(stack_sim(FRAMES, 4, 19) < 300.0); +} + +test "the column comes to rest where Hertz's law puts every ball" { + // at the end of the reel every ball is within 1.5 px of rest_y, still ringing under 0.6 px a frame + assert(stack_sim(FRAMES, 6, 0) < 0.0015); + assert(stack_sim(FRAMES, 1, 0) < 0.02); + // left for a minute it settles onto rest_y, the weight of the balls above pressing each contact + assert(stack_sim(1800, 6, 0) < 0.000001); + assert(stack_sim(1800, 1, 0) < 0.00001); + // the stack is shorter than 20 diameters by the overlaps, by about 7 cm + const short = 40.0 * R_BALL - R_BALL - rest_y(19); + assert(short > 0.06); + assert(short < 0.08); +} + +test "no ball passes through the floor or through its neighbour" { + assert(stack_sim(FRAMES, 2, 0) > 0.005); + assert(stack_sim(FRAMES, 3, 0) > 0.04); +} + +test "velocity Verlet is stable here: the stiffest mode and the dashpot both stay under 2" { + // the deepest overlap any contact reaches is under 2 R - 0.04 m (the test above) + assert(omega_h(2.0 * R_BALL - 0.04) < 2.0); + assert(damp_h() < 2.0); +} + +test "a replay is bit-identical" { + assert(stack_sim(300, 0, 5) == stack_sim(300, 0, 5)); + assert(lcg(27) == lcg(27)); +} + +test "the floor mirrors with lacquer's Fresnel reflectance: 16 percent at contact, 4 at the top" { + assert(reflect_alpha(FLOOR_PX) > 0.15); + assert(reflect_alpha(FLOOR_PX) < 0.17); + assert(reflect_alpha(rest_px(19)) < 0.05); + assert(reflect_alpha(rest_px(19)) > 0.04); +} + +test "the field is near-black and a lit dot is bright without clipping" { + assert(expose(backdrop(0.5, 0.5)) < 12.0); + assert(expose(backdrop(0.5, 0.5)) > 9.0); + assert(expose(backdrop(KEY_X, KEY_Y)) < 20.0); + assert(expose(backdrop(TUBE_X, FLOOR_PX + 0.5)) > expose(backdrop(TUBE_X, FLOOR_PX + 20.0)) + 10.0); + assert(expose(COL[0]) > 220.0); + assert(expose(COL[0]) < 255.0); + assert(expose(COL[2]) < expose(COL[1])); +} + +test "a ball is lit from up-left, has a rim, and its mirror image is lit from below" { + const hx = -0.23; + const hy = -0.30; + assert(ball_light(hx, hy, 1.0) > 2.0); + assert(ball_light(0.0 - hx, 0.0 - hy, 1.0) < 0.5); + assert(ball_light(0.7, 0.7, 0.0) > ball_light(0.0, 0.0, 0.0)); +} + +test "the reel is 15.000 s of 1080x1920 at 30 fps, 6220800 bytes a frame" { + assert(reel_ms() == 15000); + assert(frame_bytes() == 6220800); + assert(ROW_BYTES == 3240); +} + +test "the last ball lands in time for the closing lines to settle" { + // the closing lines start 66 frames after the last landing and need about 90 frames to settle + assert(land_frame(19) + 66 + 90 < FRAMES); + assert(release_frame(19) == 258); +}