diff --git a/.claude/skills/t27f/SKILL.md b/.claude/skills/t27f/SKILL.md index 4b7f03aacc..553d99ae99 100644 --- a/.claude/skills/t27f/SKILL.md +++ b/.claude/skills/t27f/SKILL.md @@ -408,6 +408,12 @@ of particles, and runnable in the OS itself and on FPGA. Two specs do this: - **`specs/t27f/idea_factory.t27` is the factory**, `(IDEA, PRESET) -> RGB24 frames`. It wraps (at most 9 characters per line, 4 lines per scene, `|` cuts a scene), lays out in glyph57's 5x7 font, assigns particles to glyph sub-slots and the card border, and closes on `CTA`. +- **A batch of ideas: set `PRESET = PRESET_AUTO`** (255). `main` runs `pick_preset(IDEA, PRESET)`, + and AUTO means `preset_of(IDEA)` = FNV-1a 32 of the idea's bytes mod `PRESETS`. The same idea + always gets the same style, and different ideas spread across all five; A..Z hit every preset. + This is Remotion's `random(seed)` applied to style. Any other `PRESET` value is used as given + (gHashTag/t27#10803). Test hashes against the published FNV vectors (`""`, `a`, `b`, `foobar`), + never against a value the spec computes itself. **Keep it FPGA-shaped.** Use Q8 fixed point. The per-step and per-pixel paths use only add, multiply, compare, table lookup (`kt` kernel ROM, `tt` tone ROM) and power-of-two division. diff --git a/.trinity/seals/t27f_idea_factory.json b/.trinity/seals/t27f_idea_factory.json index 4f321c9bb3..90cd33d12e 100644 --- a/.trinity/seals/t27f_idea_factory.json +++ b/.trinity/seals/t27f_idea_factory.json @@ -1,5 +1,5 @@ { - "built_by": "t27c-bootstrap@0.5.2+355d44a39", + "built_by": "t27c-bootstrap@0.5.2+ae19b93cd", "closure": [ { "spec": "specs/t27f/glyph57.t27", @@ -15,23 +15,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:6a6ffcf30791e08b93221c1067abfc7e053f13c23dff90de4d9f7c9e23461479", - "gen_hash_rust": "sha256:d64150e24319f74e2b9413d45cfbd96a7d8bbc0df9d51c9fac0b68c8837b2efb", - "gen_hash_verilog": "sha256:387961ee44ce4ce7f32d4ccb23a4ac58ac6ef688db3b4c8aef38d1c60e41cc18", - "gen_hash_zig": "sha256:115d3f8ff262b6fd65b59d760aeb7eff66500b9f77f56577fb22233fefaa65e7", + "gen_hash_c": "sha256:ddc41e93c87c1268b2d075c6b34fba3208ac77bc2db7992ee9430b219f3aa2f1", + "gen_hash_rust": "sha256:dfb079933e8ffe3343e2360a23e71f9a92bc7c27b20a2e11c6659f95e1562c63", + "gen_hash_verilog": "sha256:029af4d7206071497f23fd5b12d15eb47df379d6f508d2f9a192edfb12337578", + "gen_hash_zig": "sha256:27984b39231077b1f334e7d6fe2f2249667ffb6838f0bf21f4b8e38861e98fd7", "module": "idea_factory", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T02:41:09Z", + "sealed_at": "2026-10-19T06:20:13Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:249909fbd1679b488be14be323c0079735a55895560423934c21daf28c824db9", + "spec_hash": "sha256:26ab3c6cae74c0c62124efb321098e184703f4b05f1e53fc902fa209dd036768", "spec_path": "specs/t27f/idea_factory.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 10, - "total": 10, + "passed": 13, + "total": 13, "vacuous": 0 }, "toolchain": { diff --git a/specs/t27f/idea_factory.t27 b/specs/t27f/idea_factory.t27 index 5c05863743..ca8b8f6ab8 100644 --- a/specs/t27f/idea_factory.t27 +++ b/specs/t27f/idea_factory.t27 @@ -49,6 +49,7 @@ use t27f::scene_raster::red; use t27f::scene_raster::green; use t27f::scene_raster::blue; use t27f::particle_ds::ONE; +use t27f::particle_ds::PRESETS; use t27f::particle_ds::BG; use t27f::particle_ds::KEY_COL; use t27f::particle_ds::GRAIN; @@ -272,6 +273,32 @@ pub fn frame_bytes() -> i32 { return FRAME_W * FRAME_H * 3; } +// ---- style: the preset can come from the idea itself (gHashTag/t27#10803) ---- + +// PRESET = PRESET_AUTO asks the factory to pick the style from the idea's bytes, so a batch of +// ideas spreads over the presets and a re-render of one idea keeps its style. +pub const PRESET_AUTO : usize = 255; +pub const FNV32_BASIS : u32 = 2166136261; +pub const FNV32_PRIME : u32 = 16777619; + +// FNV-1a 32 over the bytes of t (UTF-8 for a Russian idea): xor the byte in, then multiply. +pub fn fnv1a32(t: []const u8) -> u32 { + var h : u32 = FNV32_BASIS; + var i : usize = 0; + while (i < t.len) { + h = (h ^ @as(u32, t[i])) *% FNV32_PRIME; + i = i + 1; + } + return h; +} +pub fn preset_of(t: []const u8) -> usize { + return @as(usize, fnv1a32(t) % @as(u32, PRESETS)); +} +pub fn pick_preset(t: []const u8, p: usize) -> usize { + if (p == PRESET_AUTO) { return preset_of(t); } + return p; +} + // ---- assign ---- // Where card particle m of mm sits on the border of the rectangle (x0, y0)-(x1, y1): what 0 = x, @@ -329,7 +356,7 @@ pub fn main() -> void { var accb : [1080]i32 = undefined; var row : [3240]u8 = undefined; - const p = PRESET; + const p = pick_preset(IDEA, PRESET); const rr = reach(p); var d2 : i32 = 0; while (d2 <= rr * rr) { @@ -633,3 +660,47 @@ test "a frame is 1080x1920 RGB24, 6220800 bytes" { assert(frame_bytes() == 6220800); assert(FPS == 30); } + +// Expected values are the published FNV-1a 32 test vectors, not values this file computes. +test "fnv1a32 gives the published FNV-1a 32 vectors for the empty string, a, b and foobar" { + assert(fnv1a32("") == 2166136261); + assert(fnv1a32("a") == 3826002220); + assert(fnv1a32("b") == 3876335077); + assert(fnv1a32("foobar") == 3214735720); +} + +test "PRESET_AUTO picks the preset from the idea's hash, any other preset is kept" { + assert(PRESET == 0); + assert(PRESET_AUTO >= PRESETS); + assert(preset_of("a") == 0); + assert(preset_of("b") == 2); + assert(pick_preset("b", PRESET_AUTO) == 2); + assert(pick_preset("b", 3) == 3); + assert(pick_preset("a", 4) == 4); + assert(pick_preset(IDEA, PRESET_AUTO) == preset_of(IDEA)); + assert(preset_of(RU_IDEA[0..80]) < PRESETS); +} + +test "the 26 one-letter ideas A to Z between them pick every preset" { + var hit : [5]i32 = undefined; + var k : usize = 0; + while (k < 5) { + hit[k] = 0; + k = k + 1; + } + var b : [1]u8 = undefined; + var c : u8 = 65; + while (c <= 90) { + b[0] = c; + const p = preset_of(b[0..1]); + assert(p < PRESETS); + hit[p] = hit[p] + 1; + c = c + 1; + } + assert(hit[0] > 0); + assert(hit[1] > 0); + assert(hit[2] > 0); + assert(hit[3] > 0); + assert(hit[4] > 0); + assert(hit[0] + hit[1] + hit[2] + hit[3] + hit[4] == 26); +}