Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .trinity/seals/t27f_token_floor.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,23 @@
}
],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:632fc0ea1caeb3bbed598e2e31b34ed225334135591742f6e38961effe996465",
"gen_hash_rust": "sha256:58c03090b40543238379889a621acf0e621e5bbd74f9782102afaa57f46b3183",
"gen_hash_verilog": "sha256:a3c7af823504d83104bdd66cdf8c98a2cbc6876fe499a0ed9a6079c483ac1f35",
"gen_hash_zig": "sha256:ca0fe77f7b7aea34d4eb0f5ad461fd12dac9b53c532dd8b78c2c306d3bc4f803",
"gen_hash_c": "sha256:4f16c015b460b005332d006930f66dd64bba91477c3c33b7fc7040cc1a6cea34",
"gen_hash_rust": "sha256:b1f0266f0bd7d407b6f3ae20fb7d79b054774604b359c8c4308eec30eeebcce7",
"gen_hash_verilog": "sha256:5f23d216ff552e61ae387df63bbab05d616894d32665ef3232316da20905c0d3",
"gen_hash_zig": "sha256:ba680b8e5c6d54e34d2ce7f4c2ba073a7bc2376b53e7c90d0eed0327fdf7809d",
"module": "token_floor",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-18T11:00:36Z",
"sealed_at": "2026-10-18T11:13:39Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:073d8dfb6e490e426ea4ae1c7df0233f0e9c575a75b15938060ded38d7723bed",
"spec_hash": "sha256:2ac0e9d3046d86cf3d2f10974b1ebb9c474cc4e2d44fa3226a4ec0de3d7bedf3",
"spec_path": "specs/t27f/token_floor.t27",
"tests": {
"failed": 0,
"failing": [],
"forced": false,
"passed": 26,
"total": 26,
"passed": 28,
"total": 28,
"vacuous": 0
},
"toolchain": {
Expand Down
158 changes: 139 additions & 19 deletions specs/t27f/token_floor.t27
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
// were splatted; the raster's own check (touches_row) still decides, so the index only skips rows a
// splat cannot light and the bytes are those of a scan over every splat. Without the index each of
// the 1920 rows checked all ~42k splats and one process took 51 s; with it, 33 s on one core.
// The empty scene is the same in every frame, so its bytes and each row's light factors are
// computed once; a row exposes only the pixels a splat visited and copies the rest, 19 s.
// To use more cores, `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
Expand Down Expand Up @@ -683,6 +685,35 @@ fn back_light(kx: f64, ky: f64, gx: f64, gy: f64, below: bool) -> f64 {
return BG * FLOOR_DIM + KEY * FLOOR_KEY * kx * ky + GLINT * gx * gy;
}

// What the row whose centre is y gives the light of the empty scene, from y alone: the key light's
// vertical factor (seen in the mirror below the floor), the horizon glint's, and the share of a
// mirror image that survives at that depth. Above the floor the glint and the fade are 0.
pub fn row_key(y: f64) -> f64 {
if (y > FLOOR_PX) {
return key_w(2.0 * FLOOR_PX - y - KEY_Y);
}
return key_w(y - KEY_Y);
}

pub fn row_glint(y: f64) -> f64 {
if (y > FLOOR_PX) {
return glint_y(y - FLOOR_PX);
}
return 0.0;
}

pub fn row_fade(y: f64) -> f64 {
if (y > FLOOR_PX) {
return @exp(0.0 - (y - FLOOR_PX) / FADE);
}
return 0.0;
}

// The byte one channel of light l is written as.
pub fn light_byte(l: f64) -> u8 {
return quantize(expose(l));
}

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);
Expand Down Expand Up @@ -981,6 +1012,35 @@ pub fn main() -> void {
gxa[px] = glint_x(x - TUBE_X);
px = px + 1;
}
// the empty scene is the same in every frame: its row factors and its bytes are computed once,
// and a pixel no splat visits in a frame is written from them
var kya : [1920]f64 = undefined;
var gya : [1920]f64 = undefined;
var fra : [1920]f64 = undefined;
var bgq : [6220800]u8 = undefined;
var dirty : [1080]bool = undefined;
var py0 : usize = 0;
while (py0 < 1920) {
const y = @as(f64, @floatFromInt(py0)) + 0.5;
kya[py0] = row_key(y);
gya[py0] = row_glint(y);
fra[py0] = row_fade(y);
px = 0;
while (px < 1080) {
const b = back_light(kxa[px], kya[py0], gxa[px], gya[py0], y > FLOOR_PX);
const o = py0 * ROW_BYTES + px * 3;
bgq[o] = light_byte(b * TINT_R);
bgq[o + 1] = light_byte(b * TINT_G);
bgq[o + 2] = light_byte(b * TINT_B);
px = px + 1;
}
py0 = py0 + 1;
}
px = 0;
while (px < 1080) {
dirty[px] = false;
px = px + 1;
}
const mir0 = reflect_alpha(FLOOR_PX);

// the physics runs from frame 0 to the shard's last frame; the picture only inside [fa, fb)
Expand Down Expand Up @@ -1211,27 +1271,17 @@ pub fn main() -> void {
}

// ---- raster: one row at a time, over the splats its index lists ----
// A pixel takes the empty scene's light the first time a splat visits it in the row and is
// marked dirty; only dirty pixels are exposed, the rest are copied from bgq. A visit that adds
// nothing leaves the light as it was, so a dirty pixel writes the byte bgq holds for it.
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;
}
const pu = @as(usize, @intCast(py));
const ky = kya[pu];
const gy = gya[pu];
const fr = fra[pu];
var ei : usize = roff[pu];
while (ei < roff[pu + 1]) {
j = @as(usize, @intCast(rsp[ei]));
Expand All @@ -1252,6 +1302,13 @@ pub fn main() -> void {
while (q <= qe) {
const ddx = @as(f64, @floatFromInt(q)) + 0.5 - sx[j];
const kk = @as(usize, @intCast(q));
if (dirty[kk] == false) {
const b = back_light(kxa[kk], ky, gxa[kk], gy, below);
accr[kk] = b * TINT_R;
accg[kk] = b * TINT_G;
accb[kk] = b * TINT_B;
dirty[kk] = true;
}
if (k == K_BALL || k == K_BALLF) {
const dd = @sqrt(ddx * ddx + dyy * dyy);
const cov = clamp01(sr[j] - dd + 0.5) * sa[j];
Expand Down Expand Up @@ -1293,11 +1350,19 @@ pub fn main() -> void {
}
ei = ei + 1;
}
const o = pu * ROW_BYTES;
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]));
if (dirty[px]) {
row[px * 3] = light_byte(accr[px]);
row[px * 3 + 1] = light_byte(accg[px]);
row[px * 3 + 2] = light_byte(accb[px]);
dirty[px] = false;
} else {
row[px * 3] = bgq[o + px * 3];
row[px * 3 + 1] = bgq[o + px * 3 + 1];
row[px * 3 + 2] = bgq[o + px * 3 + 2];
}
px = px + 1;
}
std.debug.print("{s}", .{row});
Expand Down Expand Up @@ -1567,6 +1632,61 @@ test "the field is near-black and a lit dot is bright without clipping" {
assert(expose(COL[2]) < expose(COL[1]));
}

test "a row's light comes from its centre alone" {
// on the key light's centre line its factor is exactly 1, and the mirror sees that line as far
// below the floor as the key is above it
assert(row_key(KEY_Y) == 1.0);
assert(row_key(2.0 * FLOOR_PX - KEY_Y) == 1.0);
assert(row_key(FLOOR_PX + 0.5) == key_w(FLOOR_PX - 0.5 - KEY_Y));
// no glint and no image above the floor or on the floor line itself
assert(row_glint(FLOOR_PX) == 0.0);
assert(row_fade(FLOOR_PX) == 0.0);
assert(row_glint(FLOOR_PX - 0.5) == 0.0);
assert(row_fade(FLOOR_PX - 0.5) == 0.0);
// one glint width below the floor the glint is e^-1/2, one fade length below the image e^-1
assert(row_glint(FLOOR_PX + GLINT_W) == @exp(0.0 - 0.5));
assert(row_fade(FLOOR_PX + FADE) == @exp(0.0 - 1.0));
// the raster's row factors give the backdrop, bit for bit, on every 7th row and 13th column
var bad : i64 = 0;
var seen : i64 = 0;
var py : i32 = 0;
while (py < FRAME_H) {
const y = @as(f64, @floatFromInt(py)) + 0.5;
var px : i32 = 0;
while (px < FRAME_W) {
const x = @as(f64, @floatFromInt(px)) + 0.5;
const b = back_light(key_w(x - KEY_X), row_key(y), glint_x(x - TUBE_X), row_glint(y), y > FLOOR_PX);
if (b != backdrop(x, y)) {
bad = bad + 1;
}
seen = seen + 1;
px = px + 13;
}
py = py + 7;
}
assert(bad == 0);
assert(seen == 23100);
}

test "the empty scene's bytes are the ones the reel writes where no splat has been" {
assert(light_byte(0.0) == 0);
assert(light_byte(100.0) == 255);
assert(light_byte(BG) == 10);
// read off frame 0 of the reel, whose pixels here no splat reaches: the corners, the key
// light's centre, and the floor line by the tube, just above it and just below
assert(light_byte(backdrop(0.5, 0.5) * TINT_R) == 11);
assert(light_byte(backdrop(0.5, 0.5) * TINT_G) == 10);
assert(light_byte(backdrop(0.5, 0.5) * TINT_B) == 9);
assert(light_byte(backdrop(1079.5, 0.5) * TINT_R) == 12);
assert(light_byte(backdrop(1079.5, 1919.5) * TINT_G) == 8);
assert(light_byte(backdrop(0.5, 1919.5) * TINT_B) == 7);
assert(light_byte(backdrop(760.5, 979.5) * TINT_R) == 18);
assert(light_byte(backdrop(859.5, 1639.5) * TINT_R) == 14);
assert(light_byte(backdrop(859.5, 1640.5) * TINT_R) == 35);
assert(light_byte(backdrop(859.5, 1640.5) * TINT_G) == 32);
assert(light_byte(backdrop(859.5, 1640.5) * TINT_B) == 29);
}

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;
Expand Down
Loading