From ac574a530fa390eb23c3e68d2c74a2143942f759 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 17:38:40 +0700 Subject: [PATCH] t27f: token_floor renders in 48 shards; frames cached by the seal (Closes #10383) - token_floor main renders shard k of N (TF_SHARD, TF_SHARDS). The physics steps from frame 0, and only the splats and the raster before the range are skipped. The partition is tested to tile [0, FRAMES) for every N from 1 to 64; bad input renders the whole reel. - New specs/t27f/frame_cache.t27 derives the frames' cache key from the token_floor seal. Each sealed digest (the spec and its closure) is checked against sha256sum now, and the key adds the t27c binary, zig and the build flags. A stale seal, failed tests or a missing part refuse the key, which means a miss. It lowers under gen-c, so the lab runs it as a C filter. - Lab: 48 shards in 3.43 s versus 54.0 s in one process. Both give sha256 7e0c6662 byte for byte, and the muxed reel is the v5 file (a18389ed). Tests: token_floor 24/24, frame_cache 8/8, 0 vacuous. Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/t27f_frame_cache.json | 37 +++ .trinity/seals/t27f_token_floor.json | 16 +- specs/t27f/frame_cache.t27 | 366 +++++++++++++++++++++++++++ specs/t27f/token_floor.t27 | 124 ++++++++- 4 files changed, 534 insertions(+), 9 deletions(-) create mode 100644 .trinity/seals/t27f_frame_cache.json create mode 100644 specs/t27f/frame_cache.t27 diff --git a/.trinity/seals/t27f_frame_cache.json b/.trinity/seals/t27f_frame_cache.json new file mode 100644 index 0000000000..c27fdb8d93 --- /dev/null +++ b/.trinity/seals/t27f_frame_cache.json @@ -0,0 +1,37 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+9448ab4be", + "closure": [ + { + "spec": "specs/claw/sarif.t27", + "spec_hash": "sha256:22258cfd518c5b27ad46db8bf5c507d97e783bd79073f83ff26b20549c927db8" + }, + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:b65da69dff231c61a6d3a48558af9e17af42e1acbe3f4763348f1f0c2f118f21" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:b5a27482a11fb4558d9f18d0307b7570dc83dcc452634ddc1fbee676489f263c", + "gen_hash_rust": "sha256:448fbf88283ae169f4461eddfe2c0b3e41a56eeed5a55b036234fed669250bc4", + "gen_hash_verilog": "sha256:2b37d59afe4615aafcca85d1a2a6494d61d9e548dbb08142a3cca3efa42658d7", + "gen_hash_zig": "sha256:0431d790a7061031d1cd9bfec0dfe0fe1ebb8d1af0653108884d6ffeef2c8035", + "module": "frame_cache", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-18T10:35:18Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:07220958b78d97c3523085e3cf493154f563679b9c8d16790fb0b5a262566fc2", + "spec_path": "specs/t27f/frame_cache.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 8, + "total": 8, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27f_token_floor.json b/.trinity/seals/t27f_token_floor.json index 5e2e0ced81..23174dc11c 100644 --- a/.trinity/seals/t27f_token_floor.json +++ b/.trinity/seals/t27f_token_floor.json @@ -19,23 +19,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:1fa93912469188771edb693d7e40d4ece49d22abebe79c31d4cb9077799fdb9c", - "gen_hash_rust": "sha256:892f285d9bc8177684644938f41ec61632f3d6dd29494f200e520367c55f1c99", - "gen_hash_verilog": "sha256:ec9fc178b4408e530d3f341c18082a622aa1c27f40382b17499eed0cba492d72", - "gen_hash_zig": "sha256:c04875378f32a1dbb0df1215ffd9991dc352a6b9f06e0da72c53eff04ed56dff", + "gen_hash_c": "sha256:e4c028a99ae8bd599dd5354eaf27863d0efc200e89f0f8fb37e53d8512f120d9", + "gen_hash_rust": "sha256:c5b98e52ef18fc0c770c6647cbccfd9f9722d6756191ff2d69d35a52ba3b6353", + "gen_hash_verilog": "sha256:1354e19e72192c703c608223106da7260590e8acc472d28adb67a836af7a11a4", + "gen_hash_zig": "sha256:b83c95dd0b5d3cbcaaaed85b476b9205e776667e895351b891345496318da647", "module": "token_floor", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T10:08:27Z", + "sealed_at": "2026-10-18T10:34:26Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:1fd3b3b3d50b2d163cfe2809587ae1b859ac0f79fddf666813602a53d5adf780", + "spec_hash": "sha256:521127101243ad64150d96883e964b392c4035764414427a6f29300473b2259f", "spec_path": "specs/t27f/token_floor.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 20, - "total": 20, + "passed": 24, + "total": 24, "vacuous": 0 }, "toolchain": { diff --git a/specs/t27f/frame_cache.t27 b/specs/t27f/frame_cache.t27 new file mode 100644 index 0000000000..8f3efc1ae9 --- /dev/null +++ b/specs/t27f/frame_cache.t27 @@ -0,0 +1,366 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/t27f/frame_cache.t27 -- the cache key of a reel's rendered frames, taken from the seal t27c +; wrote for the spec that draws them (specs/t27f/token_floor.t27 first). +; -- +; Why: token_floor renders 450 frames in 52 s, and most edits to the reel (captions, voice, music, +; the filter graph in token_floor_mux.t27) never touch a pixel. The frames depend on the spec, on +; the specs it uses, on the compiler that lowers them and on the zig that builds the result, and +; on nothing else. `t27c seal --save` already records the first two as sha256 digests. So the key +; is the seal, checked against the files as they are now, plus the toolchain. +; -- +; The input, one text the caller assembles from tools it already runs: +; t27c (t27c --version prints only the release number) +; zig <`zig version`> +; mode +; (`sha256sum` lines: the spec and every spec it uses; +; extra lines are ignored) +; seal +; +; The output is the canonical key record: +; frame_cache 1 +; dep (one per closure entry, in the seal's order) +; spec +; t27c ... / zig ... / mode ... (the header lines, verbatim) +; and the caller names the cache file by the sha256 of that record. +; -- +; Fail closed. `t27c seal --verify` checks the spec's own digest but not the closure, so this file +; checks every digest itself. Any of these refuses, and a refusal is a cache miss that stores +; nothing: no seal section, a header line missing or empty, a seal without a closure list, a seal +; whose tests did not all pass, a sealed file with no `sha256sum` line, or one whose digest has +; changed since the seal. The refusal is one line starting `refused:`, so a stale seal can never +; hand back frames rendered from other code. +; Every string is counted, as in specs/claw/text.t27, so this file lowers under gen-c and runs as +; a plain C filter with no zig on the path. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module frame_cache; + +use claw::sarif; +use claw::text; + +pub const KEY_HEAD : str = "frame_cache 1\n"; +pub const KEY_HEAD_LEN : usize = 14; + +/// A sha256 digest in hex; `sha256sum` puts two spaces after it. +pub const DIGEST_LEN : usize = 64; + +/// Whether text[a0..a1) and text[b0..b1) hold the same bytes. +fn spans_eq(text : []const u8, a0 : usize, a1 : usize, b0 : usize, b1 : usize) -> bool { + if (a1 < a0 or b1 < b0) { return false; } + if (a1 - a0 != b1 - b0) { return false; } + var i : usize = 0; + while (a0 + i < a1) { + if (text[a0 + i] != text[b0 + i]) { return false; } + i += 1; + } + return true; +} + +/// Copy text[s..e) to dst at w, clipped at cap; returns the new cursor. +fn put_span(dst : []u8, cap : usize, w : usize, text : []const u8, s : usize, e : usize) -> usize { + var x : usize = w; + var i : usize = s; + while (i < e) { + x = put_byte(dst, cap, x, text[i]); + i += 1; + } + return x; +} + +/// The start of the line after the one starting at s. +fn next_line(text : []const u8, n : usize, s : usize) -> usize { + var i : usize = s; + while (i < n and text[i] != '\n') { i += 1; } + return i + 1; +} + +/// The first non-space index in text[s..e). +fn skip_spaces(text : []const u8, s : usize, e : usize) -> usize { + var i : usize = s; + while (i < e and text[i] == ' ') { i += 1; } + return i; +} + +/// The index of the first double quote in text[s..e); e when there is none. +fn quote_at(text : []const u8, s : usize, e : usize) -> usize { + var i : usize = s; + while (i < e and text[i] != 34) { i += 1; } + return i; +} + +/// The start of the line that is exactly `seal`; n when there is none. +fn seal_line(text : []const u8, n : usize) -> usize { + var ls : usize = 0; + while (ls < n) { + const le : usize = line_end(text, n, ls); + if (le == ls + 4 and field_eq(text, ls, le, "seal", 4)) { return ls; } + ls = next_line(text, n, ls); + } + return n; +} + +/// The start of the value of header line `name value` in text[0..he); he when absent. +fn header_at(text : []const u8, he : usize, name : str, nlen : usize) -> usize { + var ls : usize = 0; + while (ls < he) { + const le : usize = line_end(text, he, ls); + if (le > ls + nlen + 1 and field_eq(text, ls, ls + nlen, name, nlen) and text[ls + nlen] == ' ') { + return ls + nlen + 1; + } + ls = next_line(text, he, ls); + } + return he; +} + +/// The start of the digest `sha256sum` printed for the path text[ps..pe), among text[0..he); he +/// when no line names that path. +fn digest_of(text : []const u8, he : usize, ps : usize, pe : usize) -> usize { + var ls : usize = 0; + while (ls < he) { + const le : usize = line_end(text, he, ls); + if (le > ls + DIGEST_LEN + 2 and text[ls + DIGEST_LEN] == ' ' and text[ls + DIGEST_LEN + 1] == ' ') { + if (spans_eq(text, ls + DIGEST_LEN + 2, le, ps, pe)) { return ls; } + } + ls = next_line(text, he, ls); + } + return he; +} + +/// The one refusal line, written from the start of dst; returns its length. +fn refuse(dst : []u8, cap : usize, why : str, why_len : usize) -> usize { + const w : usize = put_str(dst, cap, 0, "refused: ", 9); + return put_byte(dst, cap, put_str(dst, cap, w, why, why_len), '\n'); +} + +/// Whether out[0..m) is a refusal rather than a key. +pub fn is_refused(out : []const u8, m : usize) -> bool { + if (m < 8) { return true; } + return field_eq(out, 0, 8, "refused:", 8); +} + +/// The header line whose value starts at v, verbatim and newline-terminated. +fn put_header(dst : []u8, cap : usize, w : usize, text : []const u8, he : usize, v : usize, nlen : usize) -> usize { + return put_byte(dst, cap, put_span(dst, cap, w, text, v - nlen - 1, line_end(text, he, v)), '\n'); +} + +/// The key record for text[0..n) into out, or one `refused:` line. Returns the bytes written; a +/// result over cap means out was too small and the key must not be used. +pub fn frame_key(text : []const u8, n : usize, out : []u8, cap : usize) -> usize { + const he : usize = seal_line(text, n); + if (he >= n) { return refuse(out, cap, "no seal section", 15); } + const t27c_at : usize = header_at(text, he, "t27c", 4); + const zig_at : usize = header_at(text, he, "zig", 3); + const mode_at : usize = header_at(text, he, "mode", 4); + if (t27c_at >= he or zig_at >= he or mode_at >= he) { + return refuse(out, cap, "a t27c, zig or mode line is missing", 35); + } + var w : usize = put_str(out, cap, 0, KEY_HEAD, KEY_HEAD_LEN); + var closure_seen : bool = false; + var tests_pass : bool = false; + var have_spec : bool = false; + var spec_s : usize = n; + var spec_e : usize = n; + var top_h : usize = n; + var top_ps : usize = n; + var top_pe : usize = n; + var ls : usize = next_line(text, n, he); + while (ls < n) { + const le : usize = line_end(text, n, ls); + const s : usize = skip_spaces(text, ls, le); + var was_spec : bool = false; + if (le > s + 9 and field_eq(text, s, s + 9, "\"spec\": \"", 9)) { + spec_s = s + 9; + spec_e = quote_at(text, spec_s, le); + was_spec = true; + } else if (le > s + 21 + DIGEST_LEN and field_eq(text, s, s + 21, "\"spec_hash\": \"sha256:", 21) and text[s + 21 + DIGEST_LEN] == 34) { + if (have_spec) { + const d : usize = digest_of(text, he, spec_s, spec_e); + if (d >= he) { return refuse(out, cap, "a spec in the closure has no sha256sum line", 43); } + if (spans_eq(text, d, d + DIGEST_LEN, s + 21, s + 21 + DIGEST_LEN) == false) { + return refuse(out, cap, "a spec in the closure changed since the seal", 44); + } + w = put_str(out, cap, w, "dep ", 4); + w = put_span(out, cap, w, text, spec_s, spec_e); + w = put_byte(out, cap, w, ' '); + w = put_span(out, cap, w, text, s + 21, s + 21 + DIGEST_LEN); + w = put_byte(out, cap, w, '\n'); + } else { + top_h = s + 21; + } + } else if (le > s + 14 and field_eq(text, s, s + 14, "\"spec_path\": \"", 14)) { + top_ps = s + 14; + top_pe = quote_at(text, top_ps, le); + } else if (le > s + 10 and field_eq(text, s, s + 10, "\"failed\": ", 10)) { + tests_pass = text[s + 10] == '0' and (le == s + 11 or text[s + 11] == ','); + } else if (le >= s + 11 and field_eq(text, s, s + 11, "\"closure\": ", 11)) { + closure_seen = true; + } + have_spec = was_spec; + ls = next_line(text, n, ls); + } + if (closure_seen == false) { return refuse(out, cap, "the seal has no closure list", 28); } + if (tests_pass == false) { return refuse(out, cap, "the seal does not say every test passed", 39); } + if (top_h >= n or top_ps >= n) { return refuse(out, cap, "the seal has no spec_hash or spec_path", 38); } + const d : usize = digest_of(text, he, top_ps, top_pe); + if (d >= he) { return refuse(out, cap, "the sealed spec has no sha256sum line", 37); } + if (spans_eq(text, d, d + DIGEST_LEN, top_h, top_h + DIGEST_LEN) == false) { + return refuse(out, cap, "the spec changed since the seal", 31); + } + w = put_str(out, cap, w, "spec ", 5); + w = put_span(out, cap, w, text, top_ps, top_pe); + w = put_byte(out, cap, w, ' '); + w = put_span(out, cap, w, text, top_h, top_h + DIGEST_LEN); + w = put_byte(out, cap, w, '\n'); + w = put_header(out, cap, w, text, he, t27c_at, 4); + w = put_header(out, cap, w, text, he, zig_at, 3); + return put_header(out, cap, w, text, he, mode_at, 4); +} + +// ---- test inputs ---- + +/// count copies of b at w. +fn fill(dst : []u8, cap : usize, w : usize, b : u8, count : usize) -> usize { + var x : usize = w; + var i : usize = 0; + while (i < count) { + x = put_byte(dst, cap, x, b); + i += 1; + } + return x; +} + +/// A token_floor-shaped input with one closure spec: the seal says the closure spec's digest is +/// all `a` and the spec's is all `b`; dep_now, top_now and mux_now are what sha256sum says now, +/// t27c the binary's digest byte, zig the last version digit, mode `S` or `F`, failed the digit +/// the seal records for failed tests. +pub fn sample_input(dst : []u8, cap : usize, t27c : u8, zig : u8, mode : u8, dep_now : u8, top_now : u8, mux_now : u8, failed : u8) -> usize { + var w : usize = put_str(dst, cap, 0, "t27c ", 5); + w = put_byte(dst, cap, fill(dst, cap, w, t27c, DIGEST_LEN), '\n'); + w = put_str(dst, cap, w, "zig 0.16.", 9); + w = put_byte(dst, cap, put_byte(dst, cap, w, zig), '\n'); + w = put_str(dst, cap, w, "mode Release", 12); + w = put_str(dst, cap, put_byte(dst, cap, w, mode), " -lc\n", 5); + w = put_str(dst, cap, fill(dst, cap, w, dep_now, DIGEST_LEN), " specs/t27f/particle_engine.t27\n", 33); + w = put_str(dst, cap, fill(dst, cap, w, top_now, DIGEST_LEN), " specs/t27f/token_floor.t27\n", 29); + w = put_str(dst, cap, fill(dst, cap, w, mux_now, DIGEST_LEN), " specs/t27f/token_floor_mux.t27\n", 33); + w = put_str(dst, cap, w, "seal\n{\n \"closure\": [\n {\n \"spec\": \"specs/t27f/particle_engine.t27\",\n", 76); + w = put_str(dst, cap, w, " \"spec_hash\": \"sha256:", 27); + w = put_str(dst, cap, fill(dst, cap, w, 'a', DIGEST_LEN), "\"\n }\n ],\n \"spec_hash\": \"sha256:", 36); + w = put_str(dst, cap, fill(dst, cap, w, 'b', DIGEST_LEN), "\",\n \"spec_path\": \"specs/t27f/token_floor.t27\",\n", 48); + w = put_str(dst, cap, w, " \"tests\": {\n \"failed\": ", 27); + return put_str(dst, cap, put_byte(dst, cap, w, failed), ",\n \"passed\": 20\n }\n}\n", 25); +} + +/// Whether a[0..an) and b[0..bn) are the same bytes. +pub fn same_bytes(a : []const u8, an : usize, b : []const u8, bn : usize) -> bool { + if (an != bn) { return false; } + var i : usize = 0; + while (i < an) { + if (a[i] != b[i]) { return false; } + i += 1; + } + return true; +} + +test a_fresh_seal_gives_the_canonical_key { + var b : [2048]u8 = undefined; + const n : usize = sample_input(&b, 2048, 'e', '0', 'S', 'a', 'b', '9', '0'); + assert n < 2048; + var o : [1024]u8 = undefined; + const m : usize = frame_key(&b, n, &o, 1024); + assert m < 1024; + assert is_refused(&o, m) == false; + assert buf_has(&o, m, "frame_cache 1\ndep specs/t27f/particle_engine.t27 aaaa", 53) == true; + assert buf_has(&o, m, "\nspec specs/t27f/token_floor.t27 bbbb", 37) == true; + assert buf_has(&o, m, "\nzig 0.16.0\nmode ReleaseS -lc\n", 30) == true; + assert buf_has(&o, m, "token_floor_mux", 15) == false; + assert o[m - 1] == '\n'; +} + +test the_key_is_deterministic { + var b : [2048]u8 = undefined; + const n : usize = sample_input(&b, 2048, 'e', '0', 'S', 'a', 'b', '9', '0'); + var o1 : [1024]u8 = undefined; + var o2 : [1024]u8 = undefined; + const m1 : usize = frame_key(&b, n, &o1, 1024); + const m2 : usize = frame_key(&b, n, &o2, 1024); + assert same_bytes(&o1, m1, &o2, m2) == true; +} + +test a_mux_only_edit_keeps_the_key { + var b1 : [2048]u8 = undefined; + var b2 : [2048]u8 = undefined; + const n1 : usize = sample_input(&b1, 2048, 'e', '0', 'S', 'a', 'b', '1', '0'); + const n2 : usize = sample_input(&b2, 2048, 'e', '0', 'S', 'a', 'b', '2', '0'); + var o1 : [1024]u8 = undefined; + var o2 : [1024]u8 = undefined; + const m1 : usize = frame_key(&b1, n1, &o1, 1024); + const m2 : usize = frame_key(&b2, n2, &o2, 1024); + assert is_refused(&o1, m1) == false; + assert same_bytes(&o1, m1, &o2, m2) == true; +} + +test a_new_compiler_zig_or_build_mode_changes_the_key { + var b0 : [2048]u8 = undefined; + var b1 : [2048]u8 = undefined; + var o0 : [1024]u8 = undefined; + var o1 : [1024]u8 = undefined; + const m0 : usize = frame_key(&b0, sample_input(&b0, 2048, 'e', '0', 'S', 'a', 'b', '9', '0'), &o0, 1024); + var m1 : usize = frame_key(&b1, sample_input(&b1, 2048, 'f', '0', 'S', 'a', 'b', '9', '0'), &o1, 1024); + assert is_refused(&o1, m1) == false; + assert same_bytes(&o0, m0, &o1, m1) == false; + m1 = frame_key(&b1, sample_input(&b1, 2048, 'e', '1', 'S', 'a', 'b', '9', '0'), &o1, 1024); + assert is_refused(&o1, m1) == false; + assert same_bytes(&o0, m0, &o1, m1) == false; + m1 = frame_key(&b1, sample_input(&b1, 2048, 'e', '0', 'F', 'a', 'b', '9', '0'), &o1, 1024); + assert is_refused(&o1, m1) == false; + assert same_bytes(&o0, m0, &o1, m1) == false; +} + +test an_edit_to_any_sealed_spec_refuses_the_key { + var b : [2048]u8 = undefined; + var o : [1024]u8 = undefined; + var m : usize = frame_key(&b, sample_input(&b, 2048, 'e', '0', 'S', 'c', 'b', '9', '0'), &o, 1024); + assert is_refused(&o, m) == true; + assert buf_has(&o, m, "closure changed since the seal", 30) == true; + m = frame_key(&b, sample_input(&b, 2048, 'e', '0', 'S', 'a', 'c', '9', '0'), &o, 1024); + assert is_refused(&o, m) == true; + assert buf_has(&o, m, "the spec changed since the seal", 31) == true; +} + +test a_seal_with_a_failed_test_refuses_the_key { + var b : [2048]u8 = undefined; + var o : [1024]u8 = undefined; + const m : usize = frame_key(&b, sample_input(&b, 2048, 'e', '0', 'S', 'a', 'b', '9', '1'), &o, 1024); + assert is_refused(&o, m) == true; + assert buf_has(&o, m, "every test passed", 17) == true; +} + +test a_missing_part_refuses_the_key { + var b : [2048]u8 = undefined; + var o : [1024]u8 = undefined; + const n : usize = sample_input(&b, 2048, 'e', '0', 'S', 'a', 'b', '9', '0'); + // cut before the seal section: there is no seal + var m : usize = frame_key(&b, 100, &o, 1024); + assert is_refused(&o, m) == true; + assert buf_has(&o, m, "no seal section", 15) == true; + // drop the t27c line: the first line becomes `x27c ...` + b[0] = 'x'; + m = frame_key(&b, n, &o, 1024); + assert buf_has(&o, m, "t27c, zig or mode line is missing", 33) == true; + b[0] = 't'; + // rename the spec's sha256sum line to another path + // the three header lines, the closure spec's line, then the digest and ` specs/t27f/token_` + const p : usize = 70 + 11 + 18 + (DIGEST_LEN + 33) + DIGEST_LEN + 2 + 17; + assert b[p] == 'f'; + b[p] = 'g'; + m = frame_key(&b, n, &o, 1024); + assert buf_has(&o, m, "the sealed spec has no sha256sum line", 37) == true; +} + +test an_empty_input_is_refused { + var o : [64]u8 = undefined; + const m : usize = frame_key(&o, 0, &o, 64); + assert is_refused(&o, m) == true; + assert is_refused(&o, 0) == true; +} diff --git a/specs/t27f/token_floor.t27 b/specs/t27f/token_floor.t27 index 5d578683fb..72710eb679 100644 --- a/specs/t27f/token_floor.t27 +++ b/specs/t27f/token_floor.t27 @@ -42,6 +42,13 @@ // 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`). +// One process renders the whole reel in 52 s on one core. To use more, `main` renders shard k of N +// (environment TF_SHARD and TF_SHARDS; unset, 0 of 1, the whole reel): the frames +// [shard_a(k, N), shard_b(k, N)). Frame f's state is the integral from frame 0, so every shard steps +// the physics from frame 0 and skips only the splats and the raster before its range. The shards +// of one N tile [0, FRAMES) in order, so their outputs concatenated are the one-process output +// byte for byte. Unchanged frames need no render at all: specs/t27f/frame_cache.t27 keys them by +// this spec's seal. // // 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 @@ -711,9 +718,66 @@ fn frame_bytes() -> i32 { return FRAME_W * FRAME_H * 3; } +// ---- shards ---- + +// The first frame of shard k of n, for 0 <= k <= n; shard_a(n, n) is FRAMES. Shard k renders +// [shard_a(k, n), shard_a(k + 1, n)), so shard k + 1 starts where shard k stops. +pub fn shard_a(k: i32, n: i32) -> i32 { + return @divTrunc(k * FRAMES, n); +} + +pub fn shard_b(k: i32, n: i32) -> i32 { + return shard_a(k + 1, n); +} + +// The decimal number at the head of s, or dflt when s has no digit first. At most four digits are +// read, so no input overflows. +pub fn count_or(s: [*:0]const u8, dflt: i32) -> i32 { + var v : i32 = 0; + var i : usize = 0; + while (i < 4 && s[i] >= 48 && s[i] <= 57) { + v = v * 10 + @as(i32, @intCast(s[i] - 48)); + i = i + 1; + } + if (i == 0) { + return dflt; + } + return v; +} + +// The shard main renders, from k and n as read: an n out of 1..FRAMES, or a k out of 0..n - 1, +// is the whole reel, never an empty or a partial one. +pub fn shard_n(k: i32, n: i32) -> i32 { + if (n < 1 || n > FRAMES || k < 0 || k >= n) { + return 1; + } + return n; +} + +pub fn shard_k(k: i32, n: i32) -> i32 { + if (shard_n(k, n) == 1) { + return 0; + } + return k; +} + +// The environment variable `name` as a count; dflt when it is unset. Only main calls this, so the +// tests, which never link libc, do not reach getenv. +fn env_count(name: [*:0]const u8, dflt: i32) -> i32 { + const p = std.c.getenv(name); + if (p == null) { + return dflt; + } + return count_or(p.?, dflt); +} + // ---- the engine ---- pub fn main() -> void { + const sk0 = env_count("TF_SHARD", 0); + const sn0 = env_count("TF_SHARDS", 1); + const fa = shard_a(shard_k(sk0, sn0), shard_n(sk0, sn0)); + const fb = shard_b(shard_k(sk0, sn0), shard_n(sk0, sn0)); var by : [20]f64 = undefined; var bv : [20]f64 = undefined; var ba : [20]f64 = undefined; @@ -836,8 +900,9 @@ pub fn main() -> void { } const mir0 = reflect_alpha(FLOOR_PX); + // the physics runs from frame 0 to the shard's last frame; the picture only inside [fa, fb) var frame : i32 = 0; - while (frame < FRAMES) { + while (frame < fb) { // ---- physics: the column, then what its landings start and stop ---- stack_step(&by, &bv, &ba, &landed, &bext, frame); l = 0; @@ -936,6 +1001,10 @@ pub fn main() -> void { } // ---- splats, back to front ---- + if (frame < fa) { + frame = frame + 1; + continue; + } var n : usize = 0; // the tube's image in the floor var w : usize = 0; @@ -1340,3 +1409,56 @@ test "the last ball lands in time for the closing lines to settle" { assert(land_frame(19) + 66 + 90 < FRAMES); assert(release_frame(19) == 258); } + +test "the shards of every n from 1 to 64 tile the reel in order, with no gap and no overlap" { + var n : i32 = 1; + while (n <= 64) { + assert(shard_a(0, n) == 0); + assert(shard_b(n - 1, n) == FRAMES); + var total : i32 = 0; + var k : i32 = 0; + while (k < n) { + assert(shard_b(k, n) > shard_a(k, n)); + if (k > 0) { + assert(shard_a(k, n) == shard_b(k - 1, n)); + } + total = total + shard_b(k, n) - shard_a(k, n); + k = k + 1; + } + assert(total == FRAMES); + n = n + 1; + } +} + +test "48 shards are 9 or 10 frames each, and FRAMES shards are one frame each" { + assert(shard_b(0, 48) - shard_a(0, 48) == 9); + assert(shard_b(47, 48) - shard_a(47, 48) == 10); + var k : i32 = 0; + while (k < FRAMES) { + assert(shard_a(k, FRAMES) == k); + assert(shard_b(k, FRAMES) == k + 1); + k = k + 1; + } +} + +test "a count is read from its leading digits, and anything else falls back" { + assert(count_or("", 7) == 7); + assert(count_or("x3", 7) == 7); + assert(count_or("12", 7) == 12); + assert(count_or("48\n", 7) == 48); + assert(count_or("0", 7) == 0); + assert(count_or("123456", 7) == 1234); +} + +test "a shard that cannot be rendered as asked renders the whole reel" { + assert(shard_n(0, 1) == 1); + assert(shard_n(48, 48) == 1); + assert(shard_n(0, 0) == 1); + assert(shard_n(0 - 1, 4) == 1); + assert(shard_n(0, FRAMES + 1) == 1); + assert(shard_k(48, 48) == 0); + assert(shard_n(3, 48) == 48); + assert(shard_k(3, 48) == 3); + assert(shard_a(shard_k(9, 4), shard_n(9, 4)) == 0); + assert(shard_b(shard_k(9, 4), shard_n(9, 4)) == FRAMES); +}