Skip to content

Commit c0647bb

Browse files
oiseeclaude
andcommitted
docs: LFSR animation pipeline article
Full writeup covering: pipeline overview, CUDA phase schedule, carrier-payload encoding, carrier catalog (build/query), content analysis (Ёжик в тумане), seed stream compressibility findings, ZX Spectrum tape math, build & run instructions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 500ba05 commit c0647bb

1 file changed

Lines changed: 255 additions & 0 deletions

File tree

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
# LFSR-16 Animation Pipeline: From Video to ZX Spectrum Intro
2+
**Date:** 2026-03-31
3+
4+
> Turn any MP4 into a playable LFSR-16 animation — and understand why the result is nearly incompressible by construction.
5+
6+
---
7+
8+
## What Is This?
9+
10+
A ZX Spectrum demo intro stores a few hundred bytes and generates a recognizable image on screen using a 16-bit LFSR (Linear Feedback Shift Register). The key insight: instead of storing pixels, store the *seed* that causes the LFSR to paint the right pixels.
11+
12+
This project extends that idea to **video**: encode each frame as a sequence of LFSR seeds, play them back in a browser, and get a recognizable animation from a tiny data stream.
13+
14+
---
15+
16+
## The Pipeline
17+
18+
```
19+
MP4 / YouTube URL
20+
21+
▼ ffmpeg (scale 128×96, grayscale, contrast boost)
22+
23+
▼ encode_anim.py
24+
│ ├─ extract frames (--every N)
25+
│ ├─ [optional] OpenCV heatmap weights
26+
│ └─ CUDA brute-force per frame
27+
│ ├─ KEYFRAME: blk=4→2→1, shrinking area
28+
│ └─ DELTA: blk=2→1→1→1, full canvas, from prev result
29+
30+
▼ animation_flat JSON
31+
│ { type, n_frames, frame_starts[], frame_sizes[], frame_types[], seeds[] }
32+
33+
▼ docs/renderer.html (browser)
34+
LFSR-16 replay, frame-by-frame, seed list, GIF export
35+
```
36+
37+
---
38+
39+
## CUDA Search: How One Frame Is Encoded
40+
41+
Each seed covers the canvas with a pattern of **blocks** (8×8, 4×4, 2×2, or 1×1 pixels). The LFSR-16 (poly 0xB400) with AND-N consecutive bits controls which blocks are active — AND-7 gives ~1% density, AND-3 gives ~22%.
42+
43+
The GPU kernel (`searchKernel`) tries all 65,535 seeds simultaneously. For each seed it computes the **signed delta** against the current canvas: how many pixels would improve vs. worsen if this seed were applied. The best seed is selected and applied, then the next seed is searched on the updated canvas.
44+
45+
**Phase schedule (delta frame, budget=256):**
46+
```
47+
blk=2 AND-3 1 seed (coarse bounce, full canvas)
48+
blk=1 AND-4 3 seeds
49+
blk=1 AND-5 20 seeds
50+
blk=1 AND-6 50 seeds
51+
blk=1 AND-7 54 seeds ← 43% of budget here, finest detail
52+
```
53+
54+
**Keyframe** (first frame or scene cut) uses a shrinking-area schedule: coarse seeds scatter everywhere, fine seeds concentrate toward the image center.
55+
56+
---
57+
58+
## Carrier-Payload (CP) Delta Encoding
59+
60+
Standard delta encoding searches the full canvas for each seed. CP introduces a **two-level hierarchy**:
61+
62+
### Level 0: Carrier (blk=8)
63+
64+
One seed at (0,0) with blk=8 covers the entire 128×96 canvas in 192 non-overlapping 8×8 blocks. The carrier seed is chosen to activate blocks that have the most errors — it's a coarse "error map" in one seed.
65+
66+
```
67+
carrier: { type:'cp', cs:3960, cx:0, cy:0, can:6,
68+
ps:[[171,0,0,4,3],[194,32,8,2,4],[199,48,72,1,5]] }
69+
```
70+
71+
### Level 1-3: Payloads (blk=4→2→1)
72+
73+
Payload seeds are scored and applied **only within carrier-active zones** — pixels in 8×8 blocks that the carrier touched. This focuses the remaining budget on areas that actually need fixing.
74+
75+
**Result:** 4 seeds total (1 carrier + 3 payloads) vs. 32 seeds in plain mode — **8× fewer seeds** for comparable quality on sparse content.
76+
77+
### CP JSON format
78+
79+
```json
80+
{
81+
"type": "cp",
82+
"cs": 3960, "cx": 0, "cy": 0, "can": 6,
83+
"ps": [
84+
[171, 0, 0, 4, 3],
85+
[194, 32, 8, 2, 4],
86+
[199, 48, 72, 1, 5]
87+
]
88+
}
89+
```
90+
Each payload: `[seed, ox, oy, blk, and_n]`.
91+
92+
---
93+
94+
## Carrier Catalog: O(1) Carrier Search
95+
96+
For each possible (seed, andN) pair, the LFSR generates the same carrier pattern every time. We can precompute all 65,535 × 6 patterns and store them as **192-bit bitmaps** (one bit per 8×8 block).
97+
98+
**Build once** (236ms on RTX 4060 Ti):
99+
```bash
100+
./cuda/prng_budget_search --cp-build-catalog data/carrier_catalog.bin \
101+
--cp-andN-lo 3 --cp-andN-hi 8
102+
# Output: 9.4MB file, format "CPCT" + andN range + 65535×6×24 bytes
103+
```
104+
105+
**Query per frame** (CPU, ~2ms):
106+
1. Build 192-bit `hot_bits` mask: which 8×8 blocks have errors?
107+
2. Build `hot_px[192]`: error count per block (0-64)
108+
3. Scan all (65535 × 6) catalog entries: `score = Σ(active blocks: 64 - 2×err_count)`
109+
4. Take top-16 candidates, pixel-rescore with actual delta → best wins
110+
111+
**Use in encoding:**
112+
```bash
113+
./cuda/prng_budget_search --cp --target frame.pgm --init-canvas prev.pgm \
114+
--cp-catalog data/carrier_catalog.bin \
115+
--out result.json --out-pgm result.pgm
116+
```
117+
118+
---
119+
120+
## Content Analysis: What Works
121+
122+
| Works great | Struggles |
123+
|-------------|-----------|
124+
| Dark background, bright silhouettes | Full-frame busy content |
125+
| Slow motion, portraits | Fast cuts, camera shake |
126+
| High-contrast edges | Uniform gradients |
127+
| <10% lit pixels/frame | >30% lit pixels/frame |
128+
129+
### Ёжик в тумане (Hedgehog in the Fog)
130+
131+
The 1975 Soviet animated film is near-perfect content: dark silhouettes on misty background, slow movement, high contrast after a simple curves adjustment.
132+
133+
```bash
134+
yt-dlp -f best -o /tmp/yozhik.mkv "https://www.youtube.com/watch?v=Klt8bVaycQw"
135+
ffmpeg -i /tmp/yozhik.mkv \
136+
-vf "scale=128:96,format=gray,curves=all='0/0 0.3/0 0.6/1 1/1'" \
137+
yozhik_contrasted.mp4
138+
python3 cuda/encode_anim.py --input yozhik_contrasted.mp4 \
139+
--out data/yozhik_b256.json --budget 256 --kf-budget 512 --every 5
140+
```
141+
142+
Result: **104 frames, avg delta budget used = 149/256** — the algorithm stops early because the scene is so sparse. Each delta frame changes only a small fraction of the canvas.
143+
144+
---
145+
146+
## Seed Stream Compressibility
147+
148+
A critical question: can we compress the output further?
149+
150+
### Seeds are incompressible by construction
151+
152+
The brute-force search selects the *best* seed from 65,535 candidates. By definition, this looks like a uniform random sample — the found seeds have no predictable structure.
153+
154+
```
155+
budget-128: seed entropy = 11.76 bits (gzip achieves 98% of raw — useless)
156+
budget-64: seed entropy = 10.99 bits
157+
```
158+
159+
Delta coding makes it worse: seed[i] - seed[i-1] has even higher entropy.
160+
161+
### Fields that do compress
162+
163+
| Field | Entropy | gzip ratio |
164+
|-------|---------|-----------|
165+
| `and_n` | 1.6 bits | **1%** — nearly free |
166+
| `blk` | 0.06 bits | **<1%** — 99.3% are blk=1 |
167+
| `ox/8` | 3.6 bits | 47% |
168+
| `oy/8` | 3.3 bits | 42% |
169+
| `seed` | ~12 bits | **98%** — incompressible |
170+
171+
### Optimal binary format
172+
173+
```
174+
4 bytes per seed:
175+
seed u16 (2 bytes)
176+
and_n-3 u3 packed } 1 byte
177+
blk_enc u2 packed }
178+
ox/8 u4 packed } 1 byte
179+
oy/8 u4 packed }
180+
```
181+
182+
No compression needed — the fields are already at entropy. This is the correct on-wire format for any streaming or tape-loading use case.
183+
184+
### ZX Spectrum tape math
185+
186+
At 1200 bps tape speed:
187+
188+
| Encoding | Seeds/frame | Bytes/frame | Load time/frame |
189+
|----------|-------------|-------------|-----------------|
190+
| budget-256 | 250 | 1000B | **6.7 seconds** — impractical |
191+
| budget-64 | 64 | 256B | **1.7 seconds** — marginal |
192+
| CP mode | ~4 | ~16B | **0.1 seconds** — viable! |
193+
194+
**CP mode is the only path to real-time ZX Spectrum playback.** A 4-seed CP frame (1 carrier + 3 payloads) encodes in ~16 bytes — fast enough to load and display at ~10 fps from tape.
195+
196+
---
197+
198+
## Web Player
199+
200+
Open `docs/renderer.html`. Presets include:
201+
202+
- **Ёжик в тумане 🦔** — 104 frames, contrast-boosted, budget 256
203+
- **Che Anima 2 🎬** — 63 frames at budget 256 and 64
204+
- **CP variants** — carrier-payload comparison at low budget
205+
- **Lissajous 〰️** — ideal LFSR content (near-zero error)
206+
207+
Controls: play/pause, frame scrubber, per-layer toggle, GIF export.
208+
209+
---
210+
211+
## Build & Run
212+
213+
```bash
214+
# Build CUDA search binary
215+
nvcc -O3 -o cuda/prng_budget_search cuda/prng_budget_search.cu -lm
216+
217+
# Build carrier catalog (once, ~236ms)
218+
./cuda/prng_budget_search --cp-build-catalog data/carrier_catalog.bin
219+
220+
# Encode any video
221+
python3 cuda/encode_anim.py \
222+
--input your_video.mp4 \
223+
--out data/my_anim.json \
224+
--budget 256 --kf-budget 512 \
225+
--every 3 \
226+
--name "My Animation"
227+
228+
# Encode with CP (ultra-low bitrate)
229+
python3 cuda/encode_anim.py \
230+
--input your_video.mp4 \
231+
--out data/my_anim_cp.json \
232+
--budget 32 --kf-budget 128 \
233+
--cp --cp-catalog data/carrier_catalog.bin \
234+
--every 3
235+
```
236+
237+
---
238+
239+
## Key Numbers
240+
241+
| Metric | Value |
242+
|--------|-------|
243+
| Canvas | 128×96 pixels, 1-bit |
244+
| LFSR | 16-bit, poly 0xB400, 65,535 states |
245+
| Encoding speed | ~2-4s/frame on RTX 4060 Ti |
246+
| Catalog build | 236ms (9.4MB, andN 3-8) |
247+
| Catalog query | ~2ms/frame (CPU popcount) |
248+
| Best error (Che, 1194B) | **15.0%** — segmented quadtree |
249+
| Best error (Cat, 128B) | **4.9%** — dual-layer evolutionary |
250+
| CP mode overhead vs plain | **3× fewer seeds**, ~5% more error |
251+
| Tape-viable format | CP, ~16 bytes/frame |
252+
253+
---
254+
255+
*Inspired by [BB](https://www.pouet.net/prod.php?which=63074) (Introspec, ZX 256b, Multimatograf 2014) and [Mona](https://www.pouet.net/prod.php?which=62917) (Ilmenit, Atari 256b).*

0 commit comments

Comments
 (0)