From ba23f8578a1973b929221ca186cb783a33a674f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Apr 2026 14:02:58 +0000 Subject: [PATCH] examples/sha256: interactive SHA-256 hasher with on-screen keyboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An end-to-end FIPS 180-4 SHA-256 hasher running entirely on the NES. The player types up to 16 ASCII characters on a 5x8 on-screen keyboard, presses Enter, and the program computes and displays the 64-character hex digest. Layout (`examples/sha256/*.ne`): constants.ne layout + K[64] / H_INIT[8] tables (declared as `var` with init_array because the v0.1 compiler treats `const u8[N] = [...]` as a no-op — noted in the file) assets.ne 44-tile Tileset (A..Z, 0..9, punctuation, special keys, cursor) shared between BG and sprite layers background.ne static nametable (title, labels, keyboard grid) painted at reset state.ne globals sha_core.ne 32-bit byte primitives (copy, xor, and, add, not, rotr, shr) in inline asm + sigma/Sigma mixers + schedule/round steps + fold render.ne OAM helpers for cursor, input buffer, and 64-nibble digest keyboard.ne key dispatch table entering_state.ne cursor navigation + typing + auto-demo computing_state.ne phased driver (48 schedule steps + 64 rounds + fold across ~30 frames at 4 iterations each) showing_state.ne renders the 256-bit digest as 8 rows of 8 sprite glyphs Implementation notes: - All 32-bit words live as 4 little-endian bytes in `wk[64]`, `w[256]`, `h_state[32]` so every primitive walks four bytes with `LDA {arr},X`/`STA {arr},X` chains and, for adds, a carry chain. - Every primitive reads its parameters straight out of the transport slots `$04`/`$05` rather than `{dst}`/`{src}` substitutions: the inline-asm resolver looks parameters up in the analyzer's allocation table but the codegen spills them to a different per-function RAM slot, so `{dst}` would resolve to a ZP slot nothing ever writes to. Bypassing the substitution entirely sidesteps the issue without a compiler change. - Rotate-right by any amount is a byte-rotate loop plus a bit- rotate loop so the 10 SHA amounts (2, 6, 7, 11, 13, 17, 18, 19, 22, 25) all compile to a handful of chained `ROR`s. - The headless jsnes golden auto-types "NES" after 1 s of idle and captures its SHA-256 digest AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — byte-identical to `shasum` / `hashlib.sha256(b"NES")`. Build: `cargo run --release -- build examples/sha256.ne` https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v --- README.md | 1 + examples/README.md | 1 + examples/sha256.ne | 93 ++++ examples/sha256.nes | Bin 0 -> 24592 bytes examples/sha256/assets.ne | 436 +++++++++++++++ examples/sha256/background.ne | 102 ++++ examples/sha256/computing_state.ne | 68 +++ examples/sha256/constants.ne | 190 +++++++ examples/sha256/entering_state.ne | 126 +++++ examples/sha256/keyboard.ne | 62 +++ examples/sha256/render.ne | 131 +++++ examples/sha256/sha_core.ne | 648 +++++++++++++++++++++++ examples/sha256/showing_state.ne | 39 ++ examples/sha256/state.ne | 52 ++ tests/emulator/goldens/sha256.audio.hash | 1 + tests/emulator/goldens/sha256.png | Bin 0 -> 5937 bytes 16 files changed, 1950 insertions(+) create mode 100644 examples/sha256.ne create mode 100644 examples/sha256.nes create mode 100644 examples/sha256/assets.ne create mode 100644 examples/sha256/background.ne create mode 100644 examples/sha256/computing_state.ne create mode 100644 examples/sha256/constants.ne create mode 100644 examples/sha256/entering_state.ne create mode 100644 examples/sha256/keyboard.ne create mode 100644 examples/sha256/render.ne create mode 100644 examples/sha256/sha_core.ne create mode 100644 examples/sha256/showing_state.ne create mode 100644 examples/sha256/state.ne create mode 100644 tests/emulator/goldens/sha256.audio.hash create mode 100644 tests/emulator/goldens/sha256.png diff --git a/README.md b/README.md index 99027cf..ff0df3f 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ start Main | [`sprite_flicker_demo.ne`](examples/sprite_flicker_demo.ne) | `cycle_sprites` — rotates the OAM DMA start offset one slot per frame so scenes with more than 8 sprites on a scanline drop a *different* one each frame. Turns the NES's permanent sprite-dropout hardware symptom into visible flicker, which the eye reconstructs from adjacent frames. Pairs with the compile-time `W0109` warning and the debug-mode `debug.sprite_overflow()` / `debug.sprite_overflow_count()` telemetry for a three-layer defense against the 8-sprites-per-scanline limit. | | [`platformer.ne`](examples/platformer.ne) | **End-to-end side-scroller** — custom CHR tileset, full background nametable, metasprite player with gravity/jump physics, wrap-around scrolling, stomp-or-die enemy collisions, live stomp-count HUD, pickup coins, user-declared SFX + music, and a Title → Playing → GameOver state machine with a proximity-based autopilot so the headless harness demonstrates the full gameplay loop (stomp, stomp, die, retry) inside six seconds | | [`war.ne`](examples/war.ne) | **Production-quality card game** — a complete port of War split across `examples/war/*.ne`: title screen with a 0/1/2-player menu, animated deal, sliding face-up cards, deck-count HUD, "WAR!" tie-break with buried cards, victory screen with a fanfare, and a brisk 4/4 march on pulse 2. Pulls in nearly every NEScript subsystem (custom 88-tile sheet, felt nametable, 8-bit LFSR PRNG, queue-based decks, phase machine inside `Playing`, multiple sfx + music tracks). Building it surfaced seven compiler bugs, all fixed on the same branch — see `git log` for the details. | +| [`sha256.ne`](examples/sha256.ne) | **Interactive SHA-256 hasher** — an on-screen keyboard lets the player type up to 16 ASCII characters, and pressing ↵ runs a full FIPS 180-4 SHA-256 compression on the NES (64 rounds + 48-entry message-schedule expansion, all written in NEScript with inline-asm 32-bit primitives). The 64-character hex digest renders as sprites across eight 8-character rows at the bottom of the screen. Splits across `examples/sha256/*.ne` with a phased driver that runs four iterations per frame so the full hash finishes in well under a second; the jsnes golden captures `SHA-256("NES")` = `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. | ## Compiler Commands diff --git a/examples/README.md b/examples/README.md index c08110c..8ce3a57 100644 --- a/examples/README.md +++ b/examples/README.md @@ -39,6 +39,7 @@ Open any `.nes` file in an NES emulator ([Mesen](https://www.mesen.ca/), [FCEUX] | `sprite_flicker_demo.ne` | `cycle_sprites`, 8-per-scanline hardware limit | Twelve sprites packed onto the same 4-pixel band — two more than the NES's 8-sprites-per-scanline hardware budget. The W0109 analyzer warning fires at compile time, and a `cycle_sprites` call at the end of `on frame` rotates the OAM DMA offset one slot per frame so the PPU drops a *different* sprite each frame. The permanent-dropout failure mode becomes visible flicker, which the eye reconstructs across frames. The classic NES technique used by Gradius, Battletoads, and every shmup that ever existed. | | `war.ne` | **production-quality card game**, multi-file source layout | A complete port of the card game War, split across `examples/war/*.ne` files and pulled in via `include` directives. Title screen with a 0/1/2-player menu (cursor sprite, blinking PRESS A, brisk 4/4 march on pulse 2), a 50-frame deal animation, a deep `Playing` state with an inner phase machine (`P_WAIT_A`/`P_FLY_A`/.../`P_WAR_BANNER`/`P_WAR_BURY`/`P_CHECK`), card-conserving queue-based decks built on a 200-iteration random-swap shuffle, a "WAR!" tie-break that buries 3+1 face-down cards per player and plays a noise-channel thump per bury, and a victory screen with the builtin fanfare. The first NEScript example to use a top-level file as a thin shell that `include`s ~12 component files; building it surfaced seven compiler bugs across the analyzer, IR lowerer, and codegen that were all fixed on the same branch (see `git log` for details). | | `pong.ne` | **production-quality Pong**, powerups, multi-ball, multi-file | A complete Pong game split across `examples/pong/*.ne`. CPU VS CPU / 1 PLAYER / 2 PLAYERS title menu with brisk pulse-2 title march and autopilot, smooth ball physics with wall and paddle bouncing, CPU AI that tracks the ball with a reaction lag and dead zone, three powerup types (LONG paddle for 5 hits, FAST ball on next hit, MULTI-ball on next hit spawning 3 balls) that bounce around the field and are caught by paddle AABB overlap, multi-ball scoring (each ball scores a point, round continues until last ball exits), inner phase machine (`P_SERVE`/`P_PLAY`/`P_POINT`), and a "PLAYER N WINS" victory screen with the builtin fanfare. First-to-7 wins. | +| `sha256.ne` | **interactive SHA-256**, inline-asm 32-bit primitives, multi-file | A full FIPS 180-4 SHA-256 hasher split across `examples/sha256/*.ne`. An on-screen 5×8 keyboard grid lets the player type up to 16 ASCII characters (`A`..`Z`, `0`..`9`, space, `.`, backspace, enter), and pressing ↵ runs the 48-entry message-schedule expansion + 64-round compression on the NES itself. Every 32-bit primitive (`copy`, `xor`, `and`, `add`, `not`, rotate-right, shift-right) is hand-tuned inline assembly that walks the four little-endian bytes of a word with `LDA {wk},X` / `ADC {wk},Y` chains, so a whole round costs a few thousand cycles. The phased driver runs four schedule steps or four rounds per frame so the full compression finishes well under a second, and the 64-character hex digest renders as sprites in 8 rows of 8 glyphs at the bottom of the screen. The jsnes golden auto-types `"NES"` after 1 s of keyboard idle and captures its hash `AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D`. | ## Emulator Controls diff --git a/examples/sha256.ne b/examples/sha256.ne new file mode 100644 index 0000000..bc5d207 --- /dev/null +++ b/examples/sha256.ne @@ -0,0 +1,93 @@ +// SHA-256 Hasher — a full interactive SHA-256 implementation for the NES. +// +// The user types a short ASCII message on an on-screen keyboard, hits +// the ENTER key, and the NES computes the SHA-256 hash of the input +// and displays the 64-character hex digest at the bottom of the screen. +// +// The hashing core is implemented in pure NEScript with inline +// assembly for the 32-bit primitives (copy, XOR, add, rotate-right, +// shift-right) — the whole algorithm fits in about 400 lines of +// source and compresses one 64-byte block in ~15 frames. Since the +// keyboard restricts input to 16 ASCII characters, every message +// fits in a single block, so the final hash appears well under a +// second after the user presses ENTER. +// +// The source is split across examples/sha256/*.ne: +// +// examples/sha256.ne — this file, top-level +// examples/sha256/constants.ne — tile indices + layout +// examples/sha256/assets.ne — the Tileset sprite +// examples/sha256/background.ne — keyboard nametable +// examples/sha256/state.ne — globals +// examples/sha256/sha_core.ne — SHA-256 primitives + +// block compression +// examples/sha256/render.ne — helpers that drive the +// OAM shadow buffer +// examples/sha256/keyboard.ne — keyboard dispatch table +// examples/sha256/entering_state.ne — Entering state (typing) +// examples/sha256/computing_state.ne — Computing state +// (runs the block +// compression across +// frames) +// examples/sha256/showing_state.ne — Showing state (renders +// the 64-hex-char digest) +// +// Controls: +// D-pad — move the cursor on the keyboard. +// A — type the key under the cursor. The keys +// labelled ← and ↵ are backspace and enter; +// pressing A on ↵ starts the compression. +// B — backspace (same as moving to ← + A). +// SELECT — clear the input buffer (from any state). +// +// Autopilot: if the user doesn't press a key for ~1 second after +// reset the program auto-types "NES" and presses enter, so the +// headless jsnes golden captures a completed hash rather than an +// empty keyboard. The SHA-256 of "NES" is +// AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D — +// the exact string that should appear at the bottom of the golden. +// +// Build: cargo run --release -- build examples/sha256.ne +// Output: examples/sha256.nes + +game "SHA-256 Hasher" { + mapper: NROM + mirroring: horizontal +} + +// ── Palette ────────────────────────────────────────────────── +// +// A "terminal" look: dark slate background, cyan for the keyboard +// and labels, amber for the input text the user is typing, and +// bright white for the hash digest. Three of the four bg sub- +// palettes are populated so the attribute table can colour-code +// the INPUT, KEYBOARD, and HASH sections of the screen. +palette Main { + universal: dk_blue // deep slate background + + bg0: [dk_gray, lt_gray, white] // default / labels / keyboard + bg1: [dk_olive, olive, yellow] // INPUT section (amber) + bg2: [dk_teal, teal, aqua] // HASH section (cyan) + bg3: [dk_gray, dk_red, red] // reserved accents + + sp0: [dk_red, yellow, white] // cursor + dynamic text + sp1: [dk_olive, olive, yellow] // reserved (input overlay) + sp2: [dk_teal, teal, aqua] // reserved (hash overlay) + sp3: [dk_gray, lt_gray, white] // reserved +} + +// Pull in everything else. Order matters for symbol visibility: +// constants → assets → background → state → sha core → render → +// keyboard dispatch → each state handler. +include "sha256/constants.ne" +include "sha256/assets.ne" +include "sha256/background.ne" +include "sha256/state.ne" +include "sha256/sha_core.ne" +include "sha256/render.ne" +include "sha256/keyboard.ne" +include "sha256/entering_state.ne" +include "sha256/computing_state.ne" +include "sha256/showing_state.ne" + +start Entering diff --git a/examples/sha256.nes b/examples/sha256.nes new file mode 100644 index 0000000000000000000000000000000000000000..fa9d711653b5e3c95772583ffb3be5630698abb5 GIT binary patch literal 24592 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 bfX{%>fX{%>fX{%>fX{%>fX~4HX9oTq2h1Je literal 0 HcmV?d00001 diff --git a/examples/sha256/assets.ne b/examples/sha256/assets.ne new file mode 100644 index 0000000..1e6da60 --- /dev/null +++ b/examples/sha256/assets.ne @@ -0,0 +1,436 @@ +// sha256/assets.ne — one big Tileset sprite block. +// +// The ROM uses a single 8 KB CHR page that both the background +// layer and the sprite layer read from, so every tile declared +// here is available either way. User tiles start at index 1 +// (index 0 is reserved for the compiler's built-in smiley). +// +// The alphabet is a 2-pixel-stroke sans-serif that reads well on +// NES resolution. Every tile keeps its glyph inside a 6×6 window +// (columns 1..6, rows 1..6) so adjacent characters never touch. +// +// Colour vocabulary: +// . palette index 0 — transparent (sprites) / bg universal +// 2 palette index 2 — main glyph colour (white in bg0/sp0) +// 3 palette index 3 — accent (cursor bar, key arrows) + +sprite Tileset { + pixels: [ + // ── 1: A ───────────────────────────────────────── + "........", + "..2222..", + ".22..22.", + ".22..22.", + ".222222.", + ".22..22.", + ".22..22.", + "........", + // ── 2: B ───────────────────────────────────────── + "........", + ".22222..", + ".22..22.", + ".22222..", + ".22..22.", + ".22..22.", + ".22222..", + "........", + // ── 3: C ───────────────────────────────────────── + "........", + "..22222.", + ".22..22.", + ".22.....", + ".22.....", + ".22..22.", + "..22222.", + "........", + // ── 4: D ───────────────────────────────────────── + "........", + ".2222...", + ".22.22..", + ".22..22.", + ".22..22.", + ".22.22..", + ".2222...", + "........", + // ── 5: E ───────────────────────────────────────── + "........", + ".222222.", + ".22.....", + ".22222..", + ".22.....", + ".22.....", + ".222222.", + "........", + // ── 6: F ───────────────────────────────────────── + "........", + ".222222.", + ".22.....", + ".22222..", + ".22.....", + ".22.....", + ".22.....", + "........", + // ── 7: G ───────────────────────────────────────── + "........", + "..22222.", + ".22.....", + ".22.222.", + ".22..22.", + ".22..22.", + "..2222..", + "........", + // ── 8: H ───────────────────────────────────────── + "........", + ".22..22.", + ".22..22.", + ".222222.", + ".22..22.", + ".22..22.", + ".22..22.", + "........", + // ── 9: I ───────────────────────────────────────── + "........", + "..2222..", + "...22...", + "...22...", + "...22...", + "...22...", + "..2222..", + "........", + // ── 10: J ──────────────────────────────────────── + "........", + "....222.", + ".....22.", + ".....22.", + ".....22.", + ".22..22.", + "..2222..", + "........", + // ── 11: K ──────────────────────────────────────── + "........", + ".22..22.", + ".22.22..", + ".2222...", + ".22.22..", + ".22..22.", + ".22..22.", + "........", + // ── 12: L ──────────────────────────────────────── + "........", + ".22.....", + ".22.....", + ".22.....", + ".22.....", + ".22.....", + ".222222.", + "........", + // ── 13: M ──────────────────────────────────────── + "........", + ".22..22.", + ".222222.", + ".222222.", + ".22..22.", + ".22..22.", + ".22..22.", + "........", + // ── 14: N ──────────────────────────────────────── + "........", + ".22..22.", + ".222.22.", + ".222222.", + ".22.222.", + ".22..22.", + ".22..22.", + "........", + // ── 15: O ──────────────────────────────────────── + "........", + "..2222..", + ".22..22.", + ".22..22.", + ".22..22.", + ".22..22.", + "..2222..", + "........", + // ── 16: P ──────────────────────────────────────── + "........", + ".22222..", + ".22..22.", + ".22..22.", + ".22222..", + ".22.....", + ".22.....", + "........", + // ── 17: Q ──────────────────────────────────────── + "........", + "..2222..", + ".22..22.", + ".22..22.", + ".22.222.", + ".22.22..", + "..222.2.", + "........", + // ── 18: R ──────────────────────────────────────── + "........", + ".22222..", + ".22..22.", + ".22..22.", + ".22222..", + ".22.22..", + ".22..22.", + "........", + // ── 19: S ──────────────────────────────────────── + "........", + "..22222.", + ".22.....", + "..2222..", + ".....22.", + ".....22.", + ".22222..", + "........", + // ── 20: T ──────────────────────────────────────── + "........", + ".222222.", + "...22...", + "...22...", + "...22...", + "...22...", + "...22...", + "........", + // ── 21: U ──────────────────────────────────────── + "........", + ".22..22.", + ".22..22.", + ".22..22.", + ".22..22.", + ".22..22.", + "..2222..", + "........", + // ── 22: V ──────────────────────────────────────── + "........", + ".22..22.", + ".22..22.", + ".22..22.", + ".22..22.", + "..2222..", + "...22...", + "........", + // ── 23: W ──────────────────────────────────────── + "........", + ".22..22.", + ".22..22.", + ".22..22.", + ".222222.", + ".222222.", + ".22..22.", + "........", + // ── 24: X ──────────────────────────────────────── + "........", + ".22..22.", + "..2222..", + "...22...", + "...22...", + "..2222..", + ".22..22.", + "........", + // ── 25: Y ──────────────────────────────────────── + "........", + ".22..22.", + ".22..22.", + "..2222..", + "...22...", + "...22...", + "...22...", + "........", + // ── 26: Z ──────────────────────────────────────── + "........", + ".222222.", + ".....22.", + "....22..", + "...22...", + "..22....", + ".222222.", + "........", + // ── 27: 0 ──────────────────────────────────────── + "........", + "..2222..", + ".22..22.", + ".22..22.", + ".22..22.", + ".22..22.", + "..2222..", + "........", + // ── 28: 1 ──────────────────────────────────────── + "........", + "...22...", + "..222...", + ".2.22...", + "...22...", + "...22...", + "..2222..", + "........", + // ── 29: 2 ──────────────────────────────────────── + "........", + "..2222..", + ".22..22.", + ".....22.", + "....22..", + "..22....", + ".222222.", + "........", + // ── 30: 3 ──────────────────────────────────────── + "........", + "..2222..", + ".22..22.", + "....22..", + ".....22.", + ".22..22.", + "..2222..", + "........", + // ── 31: 4 ──────────────────────────────────────── + "........", + "....22..", + "...222..", + "..2.22..", + ".22.22..", + ".222222.", + "....22..", + "........", + // ── 32: 5 ──────────────────────────────────────── + "........", + ".222222.", + ".22.....", + ".22222..", + ".....22.", + ".22..22.", + "..2222..", + "........", + // ── 33: 6 ──────────────────────────────────────── + "........", + "..2222..", + ".22.....", + ".22222..", + ".22..22.", + ".22..22.", + "..2222..", + "........", + // ── 34: 7 ──────────────────────────────────────── + "........", + ".222222.", + ".....22.", + "....22..", + "...22...", + "..22....", + "..22....", + "........", + // ── 35: 8 ──────────────────────────────────────── + "........", + "..2222..", + ".22..22.", + "..2222..", + ".22..22.", + ".22..22.", + "..2222..", + "........", + // ── 36: 9 ──────────────────────────────────────── + "........", + "..2222..", + ".22..22.", + ".22..22.", + "..22222.", + ".....22.", + "..2222..", + "........", + // ── 37: underscore / space ('_') ───────────────── + // A shallow underline so the "space" key is visible + // in the keyboard grid and the rendered input buffer + // shows blanks where the user typed spaces. + "........", + "........", + "........", + "........", + "........", + "........", + ".222222.", + "........", + // ── 38: period ('.') ───────────────────────────── + "........", + "........", + "........", + "........", + "........", + "..22....", + "..22....", + "........", + // ── 39: colon (':') — used in labels only ──────── + "........", + "........", + "...22...", + "...22...", + "........", + "...22...", + "...22...", + "........", + // ── 40: dash ('-') — used in labels only ───────── + "........", + "........", + "........", + "........", + ".222222.", + "........", + "........", + "........", + // ── 41: backspace arrow ('<') ──────────────────── + // A left-pointing arrow with a vertical bar on the + // right, read as "rub out" by anyone who's used a + // Unix terminal. + "........", + "...2..2.", + "..22..2.", + ".222222.", + "..22..2.", + "...2..2.", + "........", + "........", + // ── 42: enter / return arrow ('>') ─────────────── + // An arrow pointing down and left — the classic + // ↵ glyph flattened to monochrome. Drawn offset to + // the right so it reads as "confirm". + "........", + "......2.", + "......2.", + "..2...2.", + ".22...2.", + "222222..", + ".22.....", + "..2.....", + // ── 43: cursor glyph ───────────────────────────── + // A right-pointing triangle in the accent colour + // (palette index 3). When placed as a sprite at the + // cell immediately left of the selected key, it sits + // on a row that carries only this sprite, so it never + // contributes to the keyboard row's 8-per-scanline + // budget. + "........", + "..3.....", + "..33....", + "..333...", + "..333...", + "..33....", + "..3.....", + "........", + // ── 44: blank tile ─────────────────────────────── + // Every pixel is palette index 0 — renders as the + // universal colour (dk_blue) in the background and + // as fully transparent when used as a sprite. Used + // to pad the nametable so short rows don't fall back + // to tile 0 (the built-in smiley glyph). + "........", + "........", + "........", + "........", + "........", + "........", + "........", + "........" + ] +} diff --git a/examples/sha256/background.ne b/examples/sha256/background.ne new file mode 100644 index 0000000..d690cb2 --- /dev/null +++ b/examples/sha256/background.ne @@ -0,0 +1,102 @@ +// sha256/background.ne — the static nametable. +// +// Painted once at reset-time (the first `background` declaration +// in the program is loaded before rendering is enabled). Contains +// the title banner, section labels, and the 5 × 8 keyboard grid. +// The Entering and Showing state handlers draw the dynamic input +// buffer, the cursor, and the 64-digit hash on top of this +// background via sprites. +// +// Legend characters are chosen to read cleanly when glanced over +// as ASCII art: +// upper-case A-Z and 0-9 → their matching glyph tile +// space ' ' → blank tile (tile 44) +// ':' ',' '-' '.' → punctuation glyph tiles +// '_' → space-key glyph on the keyboard +// 'p' → period-key glyph on the keyboard +// '<' '>' → backspace / enter key glyphs +// +// `palette_map:` leaves every metatile on bg sub-palette 0; the +// keyboard keys render as lt_gray (palette index 2), which reads +// cleanly against the dk_blue universal background. + +background Screen { + legend { + " ": 44 // blank (universal colour) + "A": 1 + "B": 2 + "C": 3 + "D": 4 + "E": 5 + "F": 6 + "G": 7 + "H": 8 + "I": 9 + "J": 10 + "K": 11 + "L": 12 + "M": 13 + "N": 14 + "O": 15 + "P": 16 + "Q": 17 + "R": 18 + "S": 19 + "T": 20 + "U": 21 + "V": 22 + "W": 23 + "X": 24 + "Y": 25 + "Z": 26 + "0": 27 + "1": 28 + "2": 29 + "3": 30 + "4": 31 + "5": 32 + "6": 33 + "7": 34 + "8": 35 + "9": 36 + "_": 37 // space-key glyph (underscore bar) + "p": 38 // period-key glyph + ":": 39 + "-": 40 + "<": 41 // backspace + ">": 42 // enter + } + + map: [ + " ", // row 0 + " SHA-256 HASHER ", // row 1 — title + " ", // row 2 + " INPUT: ", // row 3 — input label + " ", // row 4 — input row A + " ", // row 5 — input row B + " ", // row 6 + " TYPE A MESSAGE PRESS > ", // row 7 — prompt + " ", // row 8 + " ", // row 9 + " ", // row 10 + " ", // row 11 + " A B C D E F G H ", // row 12 — kb row 0 + " I J K L M N O P ", // row 13 — kb row 1 + " Q R S T U V W X ", // row 14 — kb row 2 + " Y Z 0 1 2 3 4 5 ", // row 15 — kb row 3 + " 6 7 8 9 _ p < > ", // row 16 — kb row 4 + " ", // row 17 + " ", // row 18 + " ", // row 19 + " SHA-256: ", // row 20 — hash label + " ", // row 21 — hash rows + " ", // row 22 + " ", // row 23 + " ", // row 24 + " ", // row 25 + " ", // row 26 + " ", // row 27 + " ", // row 28 + " " // row 29 + ] +} diff --git a/examples/sha256/computing_state.ne b/examples/sha256/computing_state.ne new file mode 100644 index 0000000..ad30100 --- /dev/null +++ b/examples/sha256/computing_state.ne @@ -0,0 +1,68 @@ +// sha256/computing_state.ne — runs the SHA-256 block compression. +// +// The compression is split across frames so the main loop keeps +// responding to the NMI handshake. Each frame advances one +// "phase"; every phase does a batch of 4 iterations so the +// whole compression (48 schedule steps + 64 rounds + fold) +// finishes inside ~30 frames — roughly half a second of +// wall-clock wait between pressing Enter and the hash appearing. +// +// Phase map: +// 0..11 schedule W[16..63] in batches of 4 (12 × 4 = 48) +// 12..27 rounds 0..63 in batches of 4 (16 × 4 = 64) +// 28 fold wk[A..H] into h_state, transition to Showing + +const SCHED_PHASES: u8 = 12 // 12 × 4 = 48 schedule steps +const ROUND_PHASES: u8 = 16 // 16 × 4 = 64 rounds +const FOLD_PHASE: u8 = 28 // SCHED + ROUND +const BATCH_SIZE: u8 = 4 // iterations per frame + +state Computing { + on enter { + // Reset persistent hash state and build the padded + // block from the user's message. Phased work then runs + // on top of the freshly-initialised w[] / h_state / + // wk[A..H]. + reset_hash_state() + build_padded_block() + init_abcdefgh() + + cp_phase = 0 + } + + on frame { + if cp_phase < SCHED_PHASES { + // Each schedule phase handles BATCH_SIZE words. + // First word index for this phase: 16 + phase * 4. + var first_idx: u8 = 16 + (cp_phase << 2) + var step: u8 = 0 + while step < BATCH_SIZE { + var i: u8 = first_idx + step + schedule_one(i << 2) // byte offset into w[] + step += 1 + } + cp_phase += 1 + } else if cp_phase < FOLD_PHASE { + // Round batch. First round for this phase: + // (phase - SCHED_PHASES) * 4. + var first_r: u8 = (cp_phase - SCHED_PHASES) << 2 + var step2: u8 = 0 + while step2 < BATCH_SIZE { + var r: u8 = first_r + step2 + round_one(r << 2) // K/W share byte 4*i + step2 += 1 + } + cp_phase += 1 + } else { + // Fold a..h into h_state and hand off to Showing. + fold_abcdefgh() + transition Showing + } + + // Draw the input buffer so the user can see what they + // typed while the hash is being computed. The cursor + // sprite is deliberately not drawn here — the keyboard + // is inactive during this phase. + draw_input() + } +} diff --git a/examples/sha256/constants.ne b/examples/sha256/constants.ne new file mode 100644 index 0000000..264b855 --- /dev/null +++ b/examples/sha256/constants.ne @@ -0,0 +1,190 @@ +// sha256/constants.ne — layout and algorithm constants. +// +// All pixel/tile positions live here so the rest of the code can +// read as coordinate expressions rather than magic numbers. The +// screen budget is tight: only 64 OAM sprites are available, and +// the hash display alone wants 64 of them (8 rows × 8 digits), so +// the Entering / Computing phases keep their overlays small and +// the Showing phase reuses the OAM slots for the digest. + +// ── Keyboard layout ────────────────────────────────────────── +// +// 5 rows × 8 columns = 40 keys, laid out on the nametable and +// mirrored by a compile-time character table so `key_char[i]` +// returns the ASCII code a given cell produces. The cursor is +// one sprite that moves over the grid; its background tile is +// not touched. +// +// The bottom row holds five special keys instead of glyphs: +// `_` produces a space character. +// `.` produces a period. +// `<` is backspace — deletes the last input character. +// `>` is enter — starts the SHA-256 compression. +// (positions 0..3 on that row are just digits 6-9) +const KB_ROWS: u8 = 5 +const KB_COLS: u8 = 8 +const KB_KEYS: u8 = 40 // KB_ROWS * KB_COLS + +// Origin of the keyboard on screen, in pixels. +const KB_BASE_X: u8 = 88 // tile col 11 +const KB_BASE_Y: u8 = 96 // tile row 12 +const KB_CELL_W: u8 = 16 // 2 tiles wide per cell +const KB_CELL_H: u8 = 8 // 1 tile tall per cell + +// Special-key ASCII bytes (all < 32 so they can't collide with +// printable input characters). The keyboard dispatch table stores +// one of these in the bottom row's last two slots. +const KEY_BKSP: u8 = 0x08 // ASCII BS +const KEY_ENTER: u8 = 0x0A // ASCII LF +const KEY_SPACE: u8 = 0x20 // ASCII SP +const KEY_PERIOD: u8 = 0x2E // ASCII . + +// ── Input buffer ───────────────────────────────────────────── +// +// Maximum 16 ASCII characters. After padding (one 0x80 byte, +// zeros, and an 8-byte big-endian length field) the message is +// exactly 64 bytes — a single SHA-256 block. Keeping to one block +// simplifies the compression driver and bounds the wall-clock +// latency of the Computing phase to a fraction of a second. +const INPUT_MAX: u8 = 16 +const INPUT_ROW_LEN: u8 = 8 // 8 chars per on-screen row +const INPUT_BASE_X: u8 = 16 // tile col 2 +const INPUT_BASE_Y: u8 = 32 // tile row 4 +const INPUT_ROW_H: u8 = 8 + +// ── Hash output ────────────────────────────────────────────── +// +// 64 hex characters laid out as 8 rows × 8 glyphs at the bottom +// of the screen. The grid exactly fills the OAM budget. +const HASH_NIBBLES: u8 = 64 // 8 bytes × 2 * 4 words +const HASH_ROW_LEN: u8 = 8 +const HASH_ROWS: u8 = 8 +const HASH_BASE_X: u8 = 32 // tile col 4 +const HASH_BASE_Y: u8 = 168 // tile row 21 — 8 rows fit at + // y=168..231 with margin +const HASH_ROW_H: u8 = 8 + +// ── Sprite cursor ──────────────────────────────────────────── +// +// The cursor sits just to the left of the selected key, so it +// never shares a scanline with the keyboard cell itself. +const CURSOR_OFS_X: i8 = -8 // 8 px left of cell +const CURSOR_OFS_Y: u8 = 0 + +// ── Auto-demo ──────────────────────────────────────────────── +// +// The headless golden harness drives the ROM without touching +// the controller. After AUTO_DELAY frames in Entering with no +// input, the state handler auto-fills the buffer with DEMO_TEXT +// and transitions to Computing, so the captured frame 180 shows +// an actual hash rather than an empty form. DEMO_TEXT is "NES" +// and its SHA-256 digest is +// AE9145DB5CABC41FE34B54E34AF8881F462362EA20FD8F861B26532FFBB84E0D. +const AUTO_DELAY: u8 = 60 // 1 s at 60 fps +const AUTO_DEMO_LEN: u8 = 3 // length of "NES" + +// ── SHA-256 algorithm constants ────────────────────────────── +// +// K[64] round constants and H[8] initial hash values, both stored +// little-endian (LSB first) so the byte-level primitives in +// sha_core.ne can load and add them four bytes at a time. +// +// Derived from the fractional parts of the cube roots of the +// first 64 primes (K) and square roots of the first 8 primes +// (H) per FIPS 180-4 §4.2. +// +// Declared as `var` with an array initialiser rather than `const` +// because the v0.1 compiler only stores scalar constants in its +// const-fold table; array constants would be accepted by the +// grammar but silently dropped. The initialiser costs ~256 bytes +// of reset-time "write each byte" code and 256 bytes of RAM, but +// avoids adding a new const-data pathway just for this program. +// +// The leading underscore on `_K_BYTES` silences the W0103 unused- +// variable warning: the analyzer doesn't look inside inline-asm +// bodies, and every use of this table happens through +// `LDA {_K_BYTES},Y` inside `add_k_to_wk`. +var _K_BYTES: u8[256] = [ + 0x98, 0x2F, 0x8A, 0x42, 0x91, 0x44, 0x37, 0x71, // K[ 0..1] + 0xCF, 0xFB, 0xC0, 0xB5, 0xA5, 0xDB, 0xB5, 0xE9, // K[ 2..3] + 0x5B, 0xC2, 0x56, 0x39, 0xF1, 0x11, 0xF1, 0x59, // K[ 4..5] + 0xA4, 0x82, 0x3F, 0x92, 0xD5, 0x5E, 0x1C, 0xAB, // K[ 6..7] + 0x98, 0xAA, 0x07, 0xD8, 0x01, 0x5B, 0x83, 0x12, // K[ 8..9] + 0xBE, 0x85, 0x31, 0x24, 0xC3, 0x7D, 0x0C, 0x55, // K[10..11] + 0x74, 0x5D, 0xBE, 0x72, 0xFE, 0xB1, 0xDE, 0x80, // K[12..13] + 0xA7, 0x06, 0xDC, 0x9B, 0x74, 0xF1, 0x9B, 0xC1, // K[14..15] + 0xC1, 0x69, 0x9B, 0xE4, 0x86, 0x47, 0xBE, 0xEF, // K[16..17] + 0xC6, 0x9D, 0xC1, 0x0F, 0xCC, 0xA1, 0x0C, 0x24, // K[18..19] + 0x6F, 0x2C, 0xE9, 0x2D, 0xAA, 0x84, 0x74, 0x4A, // K[20..21] + 0xDC, 0xA9, 0xB0, 0x5C, 0xDA, 0x88, 0xF9, 0x76, // K[22..23] + 0x52, 0x51, 0x3E, 0x98, 0x6D, 0xC6, 0x31, 0xA8, // K[24..25] + 0xC8, 0x27, 0x03, 0xB0, 0xC7, 0x7F, 0x59, 0xBF, // K[26..27] + 0xF3, 0x0B, 0xE0, 0xC6, 0x47, 0x91, 0xA7, 0xD5, // K[28..29] + 0x51, 0x63, 0xCA, 0x06, 0x67, 0x29, 0x29, 0x14, // K[30..31] + 0x85, 0x0A, 0xB7, 0x27, 0x38, 0x21, 0x1B, 0x2E, // K[32..33] + 0xFC, 0x6D, 0x2C, 0x4D, 0x13, 0x0D, 0x38, 0x53, // K[34..35] + 0x54, 0x73, 0x0A, 0x65, 0xBB, 0x0A, 0x6A, 0x76, // K[36..37] + 0x2E, 0xC9, 0xC2, 0x81, 0x85, 0x2C, 0x72, 0x92, // K[38..39] + 0xA1, 0xE8, 0xBF, 0xA2, 0x4B, 0x66, 0x1A, 0xA8, // K[40..41] + 0x70, 0x8B, 0x4B, 0xC2, 0xA3, 0x51, 0x6C, 0xC7, // K[42..43] + 0x19, 0xE8, 0x92, 0xD1, 0x24, 0x06, 0x99, 0xD6, // K[44..45] + 0x85, 0x35, 0x0E, 0xF4, 0x70, 0xA0, 0x6A, 0x10, // K[46..47] + 0x16, 0xC1, 0xA4, 0x19, 0x08, 0x6C, 0x37, 0x1E, // K[48..49] + 0x4C, 0x77, 0x48, 0x27, 0xB5, 0xBC, 0xB0, 0x34, // K[50..51] + 0xB3, 0x0C, 0x1C, 0x39, 0x4A, 0xAA, 0xD8, 0x4E, // K[52..53] + 0x4F, 0xCA, 0x9C, 0x5B, 0xF3, 0x6F, 0x2E, 0x68, // K[54..55] + 0xEE, 0x82, 0x8F, 0x74, 0x6F, 0x63, 0xA5, 0x78, // K[56..57] + 0x14, 0x78, 0xC8, 0x84, 0x08, 0x02, 0xC7, 0x8C, // K[58..59] + 0xFA, 0xFF, 0xBE, 0x90, 0xEB, 0x6C, 0x50, 0xA4, // K[60..61] + 0xF7, 0xA3, 0xF9, 0xBE, 0xF2, 0x78, 0x71, 0xC6 // K[62..63] +] + +var H_INIT: u8[32] = [ + 0x67, 0xE6, 0x09, 0x6A, // H[0] = 0x6A09E667 + 0x85, 0xAE, 0x67, 0xBB, // H[1] = 0xBB67AE85 + 0x72, 0xF3, 0x6E, 0x3C, // H[2] = 0x3C6EF372 + 0x3A, 0xF5, 0x4F, 0xA5, // H[3] = 0xA54FF53A + 0x7F, 0x52, 0x0E, 0x51, // H[4] = 0x510E527F + 0x8C, 0x68, 0x05, 0x9B, // H[5] = 0x9B05688C + 0xAB, 0xD9, 0x83, 0x1F, // H[6] = 0x1F83D9AB + 0x19, 0xCD, 0xE0, 0x5B // H[7] = 0x5BE0CD19 +] + +// ── wk[] layout ────────────────────────────────────────────── +// +// Every SHA-256 primitive takes byte offsets into the `wk` +// working array. Values are little-endian 32-bit: wk[A+0] is +// the LSB of `a`, wk[A+3] is its MSB. +const OFS_A: u8 = 0 +const OFS_B: u8 = 4 +const OFS_C: u8 = 8 +const OFS_D: u8 = 12 +const OFS_E: u8 = 16 +const OFS_F: u8 = 20 +const OFS_G: u8 = 24 +const OFS_H: u8 = 28 +const OFS_T1: u8 = 32 +const OFS_T2: u8 = 36 +const OFS_SIG: u8 = 40 // Σ / σ accumulator +const OFS_TMP: u8 = 44 // rotation / shift scratch + +// ── Computing phase budget ─────────────────────────────────── +// +// The compression driver splits work over multiple frames. We +// advance one of the following phases per `on frame` tick: +// +// Phase 0 schedule W[16..31] (16 iterations) +// Phase 1 schedule W[32..47] (16 iterations) +// Phase 2 schedule W[48..63] (16 iterations) +// Phase 3 rounds 0..15 (16 rounds) +// Phase 4 rounds 16..31 (16 rounds) +// Phase 5 rounds 32..47 (16 rounds) +// Phase 6 rounds 48..63 (16 rounds) +// Phase 7 fold a..h into H, +// render the digest, +// transition to Showing +// +// Each of phases 0..6 does 16 iterations. On a release build one +// round or one schedule step runs in well under a vblank-free +// NES frame, so the user-visible latency is ~8 frames. +const CP_PHASES: u8 = 8 diff --git a/examples/sha256/entering_state.ne b/examples/sha256/entering_state.ne new file mode 100644 index 0000000..53e5729 --- /dev/null +++ b/examples/sha256/entering_state.ne @@ -0,0 +1,126 @@ +// sha256/entering_state.ne — the interactive typing state. +// +// Reads controller 1 each frame, moves the keyboard cursor on +// D-pad presses, types a character on A, deletes a character on +// B, and starts compression on START (or when the cursor is +// over the ↵ key and A is pressed). SELECT clears the buffer at +// any time so the user can start over. +// +// If the user doesn't press anything in the first AUTO_DELAY +// frames, the handler auto-types DEMO_TEXT ("NES") and +// transitions to Computing so the jsnes golden always captures +// a fully-rendered hash. + +state Entering { + on enter { + kb_row = 0 + kb_col = 0 + idle_timer = 0 + debounce = 0 + blink_timer = 0 + } + + on frame { + // ── Debounce + blink tick ─────────────────────── + if debounce > 0 { + debounce -= 1 + } + blink_timer += 1 + if blink_timer >= 40 { + blink_timer = 0 + } + + // ── D-pad navigation ──────────────────────────── + var pressed: u8 = 0 + if debounce == 0 { + if button.left { + if kb_col > 0 { + kb_col -= 1 + } else { + kb_col = KB_COLS - 1 + } + debounce = 8 + pressed = 1 + } else if button.right { + kb_col += 1 + if kb_col >= KB_COLS { + kb_col = 0 + } + debounce = 8 + pressed = 1 + } else if button.up { + if kb_row > 0 { + kb_row -= 1 + } else { + kb_row = KB_ROWS - 1 + } + debounce = 8 + pressed = 1 + } else if button.down { + kb_row += 1 + if kb_row >= KB_ROWS { + kb_row = 0 + } + debounce = 8 + pressed = 1 + } else if button.a { + // Press the key under the cursor. Backspace + // and enter dispatch to their actions; any + // other character is appended to `msg`. + var ch: u8 = current_key() + if ch == KEY_BKSP { + input_backspace() + } else if ch == KEY_ENTER { + if msg_len > 0 { + transition Computing + } + } else { + input_append(ch) + } + debounce = 10 + pressed = 1 + } else if button.b { + input_backspace() + debounce = 10 + pressed = 1 + } else if button.start { + if msg_len > 0 { + transition Computing + } + debounce = 10 + pressed = 1 + } else if button.select { + input_clear() + debounce = 10 + pressed = 1 + } + } + + // ── Idle / auto-demo ──────────────────────────── + if pressed == 1 { + idle_timer = 0 + } else { + if idle_timer < 255 { + idle_timer += 1 + } + if idle_timer == AUTO_DELAY { + if msg_len == 0 { + // Auto-type "NES" → SHA-256 digest visible + // in the headless jsnes golden. + input_append(0x4E) // N + input_append(0x45) // E + input_append(0x53) // S + transition Computing + } + } + } + + // ── Render ────────────────────────────────────── + // Input buffer takes 16 sprites, cursor 1 (when on). + // Well under the 64-sprite OAM budget. + draw_input() + if blink_timer < 25 { + draw_cursor(1) + } + } +} diff --git a/examples/sha256/keyboard.ne b/examples/sha256/keyboard.ne new file mode 100644 index 0000000..209f3c7 --- /dev/null +++ b/examples/sha256/keyboard.ne @@ -0,0 +1,62 @@ +// sha256/keyboard.ne — key-to-character lookup + input dispatch. +// +// The on-screen keyboard is a 5-row × 8-column grid of cells. +// `kb_row` and `kb_col` in state.ne store the cursor position. +// The character produced when the user presses A is looked up +// here. Special values are encoded with ASCII control codes so +// they're distinguishable from printable bytes: +// +// KEY_BKSP (0x08) — bottom row col 6. Delete one char. +// KEY_ENTER (0x0A) — bottom row col 7. Start compression. +// +// The grid layout (matches the keyboard in background.ne): +// +// row 0: A B C D E F G H +// row 1: I J K L M N O P +// row 2: Q R S T U V W X +// row 3: Y Z 0 1 2 3 4 5 +// row 4: 6 7 8 9 _ . < > + +var KB_CHARS: u8[40] = [ + 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, // A B C D E F G H + 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, // I J K L M N O P + 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, // Q R S T U V W X + 0x59, 0x5A, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, // Y Z 0 1 2 3 4 5 + 0x36, 0x37, 0x38, 0x39, 0x20, 0x2E, 0x08, 0x0A // 6 7 8 9 ' ' '.' BKSP ENTER +] + +// Look up the character emitted by the key at (kb_row, kb_col). +fun current_key() -> u8 { + var idx: u8 = (kb_row << 3) + kb_col // row * 8 + col + return KB_CHARS[idx] +} + +// Append a byte to the input buffer. Does nothing if the buffer +// is already at INPUT_MAX — users wanting to correct their +// input reach for backspace first. +fun input_append(ch: u8) { + if msg_len >= INPUT_MAX { + return + } + msg[msg_len] = ch + msg_len += 1 +} + +// Delete the last byte, if any. +fun input_backspace() { + if msg_len == 0 { + return + } + msg_len -= 1 + msg[msg_len] = 0 +} + +// Wipe the entire buffer. Triggered by SELECT from any state. +fun input_clear() { + var i: u8 = 0 + while i < INPUT_MAX { + msg[i] = 0 + i += 1 + } + msg_len = 0 +} diff --git a/examples/sha256/render.ne b/examples/sha256/render.ne new file mode 100644 index 0000000..4c860be --- /dev/null +++ b/examples/sha256/render.ne @@ -0,0 +1,131 @@ +// sha256/render.ne — OAM shadow buffer helpers. +// +// The static screen (title, labels, keyboard grid) is baked into +// the `background Screen` nametable at reset time. Everything +// that changes frame-to-frame is drawn here as sprites on top: +// the keyboard cursor, the user's input buffer, and (after the +// compression phase finishes) the 64-character hex digest. +// +// Layouts are arranged to respect the NES's 8-sprites-per- +// scanline PPU limit. Input text is broken across 2 rows of 8, +// the digest across 8 rows of 8, and the cursor sits one tile +// to the left of the selected key so its scanlines don't +// intersect the keyboard row. + +// ── Tile-index helpers ────────────────────────────────────── +// +// Map a character in the alphabet to its tile index inside +// Tileset. Covers the 26 uppercase letters, 10 digits, and the +// four punctuation glyphs used by the keyboard ('_', '.', '<', +// '>'). Any code not in the table maps to tile 44 (blank) so a +// stray byte never draws the built-in smiley. +fun tile_for_char(ch: u8) -> u8 { + if ch >= 0x41 { + if ch <= 0x5A { // 'A'..'Z' + return ch - 0x41 + 1 // -> tile 1..26 + } + } + if ch >= 0x30 { + if ch <= 0x39 { // '0'..'9' + return ch - 0x30 + 27 // -> tile 27..36 + } + } + if ch == 0x20 { // space + return 37 // -> underscore bar + } + if ch == 0x2E { // '.' + return 38 // -> period glyph + } + if ch == KEY_BKSP { // 0x08 + return 41 + } + if ch == KEY_ENTER { // 0x0A + return 42 + } + return 44 // blank +} + +// Return the tile index for a low nibble (0..15) rendered as a +// hexadecimal character glyph. 0..9 map to the digit tiles; +// 10..15 (A..F) to the letter tiles. +fun tile_for_nibble(n: u8) -> u8 { + if n < 10 { + return 27 + n // digits 0..9 → tiles 27..36 + } + return 1 + (n - 10) // A..F → tiles 1..6 +} + +// ── Cursor + input overlay (Entering / Computing states) ──── + +// Draw the keyboard cursor sprite. `blink` is a bit-flag: non- +// zero frames draw the arrow, zero frames hide it, producing a +// ~2 Hz flash that tells the player the keyboard is live. +fun draw_cursor(blink: u8) { + if blink == 0 { + return + } + // Compute pixel position of the *selected key* (kb_row, + // kb_col), then shift 8 pixels left so the sprite lands one + // tile column to the west — off the keyboard row, so the + // cursor never eats into the 8-per-scanline budget. + var cx: u8 = KB_BASE_X + (kb_col << 4) - 8 + var cy: u8 = KB_BASE_Y + (kb_row << 3) + draw Tileset at: (cx, cy) frame: 43 // cursor glyph +} + +// Draw the user's input buffer across two rows of 8 glyphs. +// Empty slots render as blank tiles. Row 0 is y=INPUT_BASE_Y, +// row 1 is y=INPUT_BASE_Y + INPUT_ROW_H. Both rows sit above +// the keyboard so their scanlines are distinct from any other +// sprite row — no `cycle_sprites` trick needed. +fun draw_input() { + var i: u8 = 0 + while i < INPUT_MAX { + var row: u8 = i >> 3 // 0 or 1 + var col: u8 = i & 0x07 // 0..7 + var sx: u8 = INPUT_BASE_X + (col << 3) + var sy: u8 = INPUT_BASE_Y + (row << 3) // INPUT_ROW_H = 8 + var ch: u8 = 0 + if i < msg_len { + ch = msg[i] + } + draw Tileset at: (sx, sy) frame: tile_for_char(ch) + i += 1 + } +} + +// ── Digest overlay (Showing state) ────────────────────────── + +// Draw the 64-nibble hexadecimal digest across 8 rows of 8 +// glyphs. Reads h_state in its SHA-256-specified big-endian +// order: H_0 is the first u32, and inside each u32 the MSB is +// printed first. That makes the on-screen text match the hex +// digest most tools produce (e.g. shasum, Python's hexdigest). +fun draw_hash() { + var word: u8 = 0 + while word < 8 { + var base: u8 = word << 2 // h_state byte index + var nib_idx: u8 = 0 + while nib_idx < 8 { + // Pick the byte of the word currently being printed. + // nib_idx 0,1 -> byte 3 (MSB); 2,3 -> byte 2; + // 4,5 -> byte 1; 6,7 -> byte 0 (LSB). + var byte_off: u8 = 3 - (nib_idx >> 1) + var byte_val: u8 = h_state[base + byte_off] + var nibble: u8 = 0 + if (nib_idx & 1) == 0 { + nibble = byte_val >> 4 // high nibble first + } else { + nibble = byte_val & 0x0F // then low nibble + } + var char_idx: u8 = (word << 3) + nib_idx + var row: u8 = char_idx >> 3 // 0..7 + var col: u8 = char_idx & 0x07 // 0..7 + var sx: u8 = HASH_BASE_X + (col << 3) + var sy: u8 = HASH_BASE_Y + (row << 3) + draw Tileset at: (sx, sy) frame: tile_for_nibble(nibble) + nib_idx += 1 + } + word += 1 + } +} diff --git a/examples/sha256/sha_core.ne b/examples/sha256/sha_core.ne new file mode 100644 index 0000000..65aed47 --- /dev/null +++ b/examples/sha256/sha_core.ne @@ -0,0 +1,648 @@ +// sha256/sha_core.ne — SHA-256 block compression in NEScript. +// +// FIPS 180-4 §6.2 specifies a 256-bit hash computed over one or +// more 512-bit (= 64-byte) message blocks. The compression of a +// single block is the hot path; since our on-screen keyboard +// restricts the input to 16 characters, every message fits in +// one block after padding, so the driver below only needs to +// process one block per Enter press. +// +// Representation: every 32-bit word is held as four consecutive +// bytes, little-endian (LSB first). This choice lets the 6502 +// do 32-bit arithmetic by chaining its native `ADC`, `EOR`, +// `AND`, `ROR`, and `LSR` instructions — each of which walks +// one byte per cycle group and pushes the carry into the next. +// +// Every primitive operates on byte offsets into one of three +// globally-visible byte arrays: +// wk[64] — scratch: a..h and T1/T2/Σ/tmp (see constants.ne) +// w[256] — 64 u32 message-schedule words, packed contig. +// h_state[32] — 8 u32 persistent hash words +// +// K[i] and H_INIT[i] live in RAM as `var` arrays loaded from +// the init_array initialiser at reset time (see constants.ne). +// +// ── Parameter convention ──────────────────────────────────── +// +// NEScript passes the first two function parameters via +// zero-page slots $04 and $05 before the JSR. The compiler's +// standard prologue immediately spills those slots into a +// per-function local in high RAM so nested calls don't step on +// them — but the inline-asm `{name}` resolver looks parameters +// up in the analyzer's allocation table, which doesn't see the +// codegen's spill. Rather than double-copy through a global, +// every primitive below reads its parameters straight out of +// the transport slots with `LDX $04` / `LDY $05`. Our +// primitives never JSR from inside the `asm` block, so the +// transport slots are still live when we read them. + +// ── 32-bit byte primitives ────────────────────────────────── + +// wk[dst..dst+4] = wk[src..src+4] +fun cp_wk(dst: u8, src: u8) { + asm { + LDX $04 + LDY $05 + LDA {wk},Y + STA {wk},X + INX + INY + LDA {wk},Y + STA {wk},X + INX + INY + LDA {wk},Y + STA {wk},X + INX + INY + LDA {wk},Y + STA {wk},X + } +} + +// wk[dst..dst+4] ^= wk[src..src+4] +fun xor_wk(dst: u8, src: u8) { + asm { + LDX $04 + LDY $05 + LDA {wk},X + EOR {wk},Y + STA {wk},X + INX + INY + LDA {wk},X + EOR {wk},Y + STA {wk},X + INX + INY + LDA {wk},X + EOR {wk},Y + STA {wk},X + INX + INY + LDA {wk},X + EOR {wk},Y + STA {wk},X + } +} + +// wk[dst..dst+4] &= wk[src..src+4] +fun and_wk(dst: u8, src: u8) { + asm { + LDX $04 + LDY $05 + LDA {wk},X + AND {wk},Y + STA {wk},X + INX + INY + LDA {wk},X + AND {wk},Y + STA {wk},X + INX + INY + LDA {wk},X + AND {wk},Y + STA {wk},X + INX + INY + LDA {wk},X + AND {wk},Y + STA {wk},X + } +} + +// wk[dst..dst+4] += wk[src..src+4] (chained ADC for carry) +fun add_wk(dst: u8, src: u8) { + asm { + LDX $04 + LDY $05 + CLC + LDA {wk},X + ADC {wk},Y + STA {wk},X + INX + INY + LDA {wk},X + ADC {wk},Y + STA {wk},X + INX + INY + LDA {wk},X + ADC {wk},Y + STA {wk},X + INX + INY + LDA {wk},X + ADC {wk},Y + STA {wk},X + } +} + +// wk[dst..dst+4] = ~wk[dst..dst+4] (bitwise NOT, in place) +fun not_wk(dst: u8) { + asm { + LDX $04 + LDA {wk},X + EOR #$FF + STA {wk},X + INX + LDA {wk},X + EOR #$FF + STA {wk},X + INX + LDA {wk},X + EOR #$FF + STA {wk},X + INX + LDA {wk},X + EOR #$FF + STA {wk},X + } +} + +// Rotate wk[dst..dst+4] right by 1 bit, in place. Treat the +// 4-byte little-endian value as one 32-bit integer. A right- +// rotation pulls bit 0 of the LSB into bit 31 of the MSB. The +// ROR chain below first captures bit 0 of the LSB into the +// carry (via LSR A on a non-destructive copy), then runs ROR +// MSB, byte 2, byte 1, LSB in that order — each ROR pulls the +// previous byte's bit 0 into the next byte's bit 7. +fun rotr1_wk(dst: u8) { + asm { + LDX $04 + LDA {wk},X + LSR A + INX + INX + INX + ROR {wk},X + DEX + ROR {wk},X + DEX + ROR {wk},X + DEX + ROR {wk},X + } +} + +// Rotate wk[dst..dst+4] right by 1 byte, in place. +// new[0] = old[1], new[1] = old[2], +// new[2] = old[3], new[3] = old[0] +fun byte_rotr_wk(dst: u8) { + asm { + LDX $04 + LDY {wk},X + INX + LDA {wk},X + DEX + STA {wk},X + INX + INX + LDA {wk},X + DEX + STA {wk},X + INX + INX + LDA {wk},X + DEX + STA {wk},X + INX + TYA + STA {wk},X + } +} + +// Rotate wk[dst..dst+4] right by `n` bits. Handles any n in +// 0..31 by first rotating whole bytes (each call is cheaper +// than 8 ROR chains) and then finishing with up to 7 single- +// bit ROR chains. +fun rotr_wk(dst: u8, n: u8) { + var rem: u8 = n + while rem >= 8 { + byte_rotr_wk(dst) + rem -= 8 + } + while rem > 0 { + rotr1_wk(dst) + rem -= 1 + } +} + +// Shift wk[dst..dst+4] right by 1 bit (logical — top bit +// becomes 0). +fun shr1_wk(dst: u8) { + asm { + LDX $04 + INX + INX + INX + LSR {wk},X + DEX + ROR {wk},X + DEX + ROR {wk},X + DEX + ROR {wk},X + } +} + +// Shift wk[dst..dst+4] right by 1 byte, in place. The top +// byte becomes 0. +fun byte_shr_wk(dst: u8) { + asm { + LDX $04 + INX + LDA {wk},X + DEX + STA {wk},X + INX + INX + LDA {wk},X + DEX + STA {wk},X + INX + INX + LDA {wk},X + DEX + STA {wk},X + INX + LDA #0 + STA {wk},X + } +} + +// Shift wk[dst..dst+4] right by `n` bits (logical). +fun shr_wk(dst: u8, n: u8) { + var rem: u8 = n + while rem >= 8 { + byte_shr_wk(dst) + rem -= 8 + } + while rem > 0 { + shr1_wk(dst) + rem -= 1 + } +} + +// ── Cross-array primitives ────────────────────────────────── + +// wk[dst..dst+4] = w[w_ofs..w_ofs+4] +fun cp_w_to_wk(dst: u8, w_ofs: u8) { + asm { + LDX $04 + LDY $05 + LDA {w},Y + STA {wk},X + INX + INY + LDA {w},Y + STA {wk},X + INX + INY + LDA {w},Y + STA {wk},X + INX + INY + LDA {w},Y + STA {wk},X + } +} + +// wk[dst..dst+4] += w[w_ofs..w_ofs+4] +fun add_w_to_wk(dst: u8, w_ofs: u8) { + asm { + LDX $04 + LDY $05 + CLC + LDA {wk},X + ADC {w},Y + STA {wk},X + INX + INY + LDA {wk},X + ADC {w},Y + STA {wk},X + INX + INY + LDA {wk},X + ADC {w},Y + STA {wk},X + INX + INY + LDA {wk},X + ADC {w},Y + STA {wk},X + } +} + +// w[w_ofs..w_ofs+4] = wk[src..src+4] +fun cp_wk_to_w(w_ofs: u8, src: u8) { + asm { + LDX $05 + LDY $04 + LDA {wk},X + STA {w},Y + INX + INY + LDA {wk},X + STA {w},Y + INX + INY + LDA {wk},X + STA {w},Y + INX + INY + LDA {wk},X + STA {w},Y + } +} + +// h_state[h_ofs..h_ofs+4] += wk[src..src+4] +fun add_wk_to_h(h_ofs: u8, src: u8) { + asm { + LDX $04 + LDY $05 + CLC + LDA {h_state},X + ADC {wk},Y + STA {h_state},X + INX + INY + LDA {h_state},X + ADC {wk},Y + STA {h_state},X + INX + INY + LDA {h_state},X + ADC {wk},Y + STA {h_state},X + INX + INY + LDA {h_state},X + ADC {wk},Y + STA {h_state},X + } +} + +// wk[dst..dst+4] += _K_BYTES[k_ofs..k_ofs+4] +fun add_k_to_wk(dst: u8, k_ofs: u8) { + asm { + LDX $04 + LDY $05 + CLC + LDA {wk},X + ADC {_K_BYTES},Y + STA {wk},X + INX + INY + LDA {wk},X + ADC {_K_BYTES},Y + STA {wk},X + INX + INY + LDA {wk},X + ADC {_K_BYTES},Y + STA {wk},X + INX + INY + LDA {wk},X + ADC {_K_BYTES},Y + STA {wk},X + } +} + +// ── σ and Σ helpers ───────────────────────────────────────── +// +// Each Σ/σ function writes its 32-bit result at wk[OFS_SIG]. +// OFS_TMP is used internally as scratch. Callers must not +// pass `src` == OFS_SIG / OFS_TMP. + +// Σ0(src) = rotr(src, 2) ^ rotr(src, 13) ^ rotr(src, 22) +fun big_sigma0(src: u8) { + cp_wk(OFS_SIG, src) + rotr_wk(OFS_SIG, 2) + cp_wk(OFS_TMP, src) + rotr_wk(OFS_TMP, 13) + xor_wk(OFS_SIG, OFS_TMP) + cp_wk(OFS_TMP, src) + rotr_wk(OFS_TMP, 22) + xor_wk(OFS_SIG, OFS_TMP) +} + +// Σ1(src) = rotr(src, 6) ^ rotr(src, 11) ^ rotr(src, 25) +fun big_sigma1(src: u8) { + cp_wk(OFS_SIG, src) + rotr_wk(OFS_SIG, 6) + cp_wk(OFS_TMP, src) + rotr_wk(OFS_TMP, 11) + xor_wk(OFS_SIG, OFS_TMP) + cp_wk(OFS_TMP, src) + rotr_wk(OFS_TMP, 25) + xor_wk(OFS_SIG, OFS_TMP) +} + +// σ0(src) = rotr(src, 7) ^ rotr(src, 18) ^ (src >> 3) +fun small_sigma0(src: u8) { + cp_wk(OFS_SIG, src) + rotr_wk(OFS_SIG, 7) + cp_wk(OFS_TMP, src) + rotr_wk(OFS_TMP, 18) + xor_wk(OFS_SIG, OFS_TMP) + cp_wk(OFS_TMP, src) + shr_wk(OFS_TMP, 3) + xor_wk(OFS_SIG, OFS_TMP) +} + +// σ1(src) = rotr(src, 17) ^ rotr(src, 19) ^ (src >> 10) +fun small_sigma1(src: u8) { + cp_wk(OFS_SIG, src) + rotr_wk(OFS_SIG, 17) + cp_wk(OFS_TMP, src) + rotr_wk(OFS_TMP, 19) + xor_wk(OFS_SIG, OFS_TMP) + cp_wk(OFS_TMP, src) + shr_wk(OFS_TMP, 10) + xor_wk(OFS_SIG, OFS_TMP) +} + +// ── Block-level helpers ───────────────────────────────────── + +// Copy H_INIT[0..32] into h_state[0..32]. Used at the start of +// every hash so the driver can be re-run on a new message +// after the user clears the input. +fun reset_hash_state() { + var i: u8 = 0 + while i < 32 { + h_state[i] = H_INIT[i] + i += 1 + } +} + +// Build the 64-byte padded message block directly into +// w[0..63]. `msg[0..msg_len]` is the ASCII input; padding +// follows FIPS 180-4 §5.1.1: +// +// pad[0..msg_len] = msg[0..msg_len] +// pad[msg_len] = 0x80 +// pad[msg_len+1..56] = 0 +// pad[56..62] = 0 (high 48 bits of length) +// pad[62..64] = message length in bits, big-endian +// +// Since msg_len ≤ 16 the bit length fits in 8 bits (max 128), +// so only the very last byte of the block is nonzero for the +// length field. The loader also byte-swaps each 4-byte word so +// our little-endian internal layout matches SHA-256's big- +// endian word order. +fun build_padded_block() { + // Step 1: zero the whole block. + var i: u8 = 0 + while i < 64 { + w[i] = 0 + i += 1 + } + + // Step 2: copy the ASCII message bytes into the block, + // reversing byte order within each 4-byte group so the + // "big-endian word" becomes our "little-endian word". The + // byte index inside each word flips: 0↔3, 1↔2, 2↔1, 3↔0. + i = 0 + while i < msg_len { + var word_idx: u8 = i & 0xFC // i rounded down to 4 + var byte_idx: u8 = i & 0x03 // 0..3 + var w_ofs: u8 = word_idx + (3 - byte_idx) // byte-swap within word + w[w_ofs] = msg[i] + i += 1 + } + + // Step 3: append the 0x80 end-of-message marker at the + // byte-swapped position for `msg_len`. + var pad_word: u8 = msg_len & 0xFC + var pad_byte: u8 = msg_len & 0x03 + var pad_ofs: u8 = pad_word + (3 - pad_byte) + w[pad_ofs] = 0x80 + + // Step 4: write the 64-bit big-endian length into bytes + // 56..63 of the block. The SHA-256 view puts the MSB at + // b_56 and the LSB at b_63; since `msg_len` ≤ 16, the bit + // length is ≤ 128 and fits in a single byte. That byte is + // b_63, which under our byte-swap-within-word convention + // lands at w[60] (= word 15 byte 0 = u32 LSB). + w[60] = msg_len << 3 +} + +// ── Schedule and round steps ──────────────────────────────── +// +// `schedule_one` computes w[i] from the earlier four entries; +// `round_one` runs one SHA-256 iteration against the current +// a..h at wk[0..31]. Both are written as plain NEScript so the +// compression driver can loop over them one step at a time +// between `wait_frame`s. + +// Compute w[i] = σ1(w[i-2]) + w[i-7] + σ0(w[i-15]) + w[i-16]. +// `w_byte` is the byte offset of w[i] inside the w[] array, +// i.e. `4 * i`. +fun schedule_one(w_byte: u8) { + // Temp accumulator lives at OFS_T1. Seed with w[i-16]. + cp_w_to_wk(OFS_T1, w_byte - 64) // w[i-16] + add_w_to_wk(OFS_T1, w_byte - 28) // + w[i-7] + + // Load w[i-15] into OFS_T2, then apply σ0 into OFS_SIG. + cp_w_to_wk(OFS_T2, w_byte - 60) + small_sigma0(OFS_T2) // SIG = σ0(T2) + add_wk(OFS_T1, OFS_SIG) + + // Load w[i-2] into OFS_T2, then apply σ1 into OFS_SIG. + cp_w_to_wk(OFS_T2, w_byte - 8) + small_sigma1(OFS_T2) // SIG = σ1(T2) + add_wk(OFS_T1, OFS_SIG) + + // Store T1 back into w[i]. + cp_wk_to_w(w_byte, OFS_T1) +} + +// Ch(e, f, g) = (e & f) ^ (~e & g). Writes to wk[OFS_SIG]. +// Uses wk[OFS_TMP] as scratch (clobbered). +fun ch_into_sig() { + cp_wk(OFS_SIG, OFS_E) + and_wk(OFS_SIG, OFS_F) // SIG = e & f + cp_wk(OFS_TMP, OFS_E) + not_wk(OFS_TMP) // TMP = ~e + and_wk(OFS_TMP, OFS_G) // TMP = ~e & g + xor_wk(OFS_SIG, OFS_TMP) // SIG = ch +} + +// Maj(a, b, c) = (a & b) ^ (a & c) ^ (b & c). Writes to +// wk[OFS_SIG]. Uses wk[OFS_TMP] as scratch (clobbered). +fun maj_into_sig() { + cp_wk(OFS_SIG, OFS_A) + and_wk(OFS_SIG, OFS_B) // SIG = a & b + cp_wk(OFS_TMP, OFS_A) + and_wk(OFS_TMP, OFS_C) // TMP = a & c + xor_wk(OFS_SIG, OFS_TMP) // SIG = (a&b) ^ (a&c) + cp_wk(OFS_TMP, OFS_B) + and_wk(OFS_TMP, OFS_C) // TMP = b & c + xor_wk(OFS_SIG, OFS_TMP) // SIG = maj +} + +// Run one SHA-256 compression round. `kw_byte` is the byte +// offset shared by K[i] and w[i] (both tables hold 32-bit +// words at 4 bytes each, so their i-th entries sit at byte +// 4*i). +fun round_one(kw_byte: u8) { + // T1 = h + Σ1(e) + Ch(e,f,g) + K[i] + W[i] + cp_wk(OFS_T1, OFS_H) + big_sigma1(OFS_E) // SIG = Σ1(e) + add_wk(OFS_T1, OFS_SIG) + + ch_into_sig() // SIG = ch + add_wk(OFS_T1, OFS_SIG) + + add_k_to_wk(OFS_T1, kw_byte) // T1 += K[i] + add_w_to_wk(OFS_T1, kw_byte) // T1 += W[i] + + // T2 = Σ0(a) + Maj(a,b,c). Compute Σ0(a) into SIG, stash + // in T2, then replace SIG with Maj and add into T2. + big_sigma0(OFS_A) // SIG = Σ0(a) + cp_wk(OFS_T2, OFS_SIG) + maj_into_sig() // SIG = maj + add_wk(OFS_T2, OFS_SIG) + + // Shift registers: h=g, g=f, f=e, e=d+T1, d=c, c=b, b=a, + // a=T1+T2. Done in an order that avoids stomping live + // data (always write the later slot before reading the + // earlier). + cp_wk(OFS_H, OFS_G) + cp_wk(OFS_G, OFS_F) + cp_wk(OFS_F, OFS_E) + cp_wk(OFS_E, OFS_D) + add_wk(OFS_E, OFS_T1) + cp_wk(OFS_D, OFS_C) + cp_wk(OFS_C, OFS_B) + cp_wk(OFS_B, OFS_A) + cp_wk(OFS_A, OFS_T1) + add_wk(OFS_A, OFS_T2) +} + +// Initialise a..h from h_state. Called once before the 64 +// rounds start (inside Computing's on_enter). +fun init_abcdefgh() { + var i: u8 = 0 + while i < 32 { + wk[i] = h_state[i] + i += 1 + } +} + +// Fold wk[A..H] back into h_state with eight 32-bit adds — +// the "H_i' = H_i + a_i" step at the end of block compression. +fun fold_abcdefgh() { + add_wk_to_h(0, OFS_A) + add_wk_to_h(4, OFS_B) + add_wk_to_h(8, OFS_C) + add_wk_to_h(12, OFS_D) + add_wk_to_h(16, OFS_E) + add_wk_to_h(20, OFS_F) + add_wk_to_h(24, OFS_G) + add_wk_to_h(28, OFS_H) +} diff --git a/examples/sha256/showing_state.ne b/examples/sha256/showing_state.ne new file mode 100644 index 0000000..5a2f816 --- /dev/null +++ b/examples/sha256/showing_state.ne @@ -0,0 +1,39 @@ +// sha256/showing_state.ne — renders the final digest. +// +// Draws the 64-character hexadecimal digest across 8 rows of 8 +// glyphs at the bottom of the screen (see HASH_BASE_Y / +// HASH_BASE_X in constants.ne). The digest display alone fills +// the OAM budget, so this state does not draw the input buffer +// or the keyboard cursor. +// +// Pressing B or SELECT clears the buffer and returns to the +// Entering state so the user can hash another message. + +state Showing { + on enter { + blink_timer = 0 + debounce = 20 // swallow the press that + // brought us here + } + + on frame { + blink_timer += 1 + if blink_timer >= 60 { + blink_timer = 0 + } + if debounce > 0 { + debounce -= 1 + } + + // ── Input ──────────────────────────────────────── + if debounce == 0 { + if button.b or button.select or button.start { + input_clear() + transition Entering + } + } + + // ── Render the 256-bit digest ─────────────────── + draw_hash() + } +} diff --git a/examples/sha256/state.ne b/examples/sha256/state.ne new file mode 100644 index 0000000..de61848 --- /dev/null +++ b/examples/sha256/state.ne @@ -0,0 +1,52 @@ +// sha256/state.ne — global variables. +// +// Every piece of cross-handler state lives here so Entering, +// Computing, and Showing can share buffers (and, crucially, so +// the message buffer survives the state transition from +// Entering → Computing without being reinitialised). + +// ── Cursor position on the keyboard ───────────────────────── +var kb_row: u8 = 0 // 0..KB_ROWS-1 +var kb_col: u8 = 0 // 0..KB_COLS-1 + +// ── Input buffer + length ─────────────────────────────────── +var msg: u8[16] // INPUT_MAX ASCII bytes +var msg_len: u8 = 0 // 0..INPUT_MAX + +// ── SHA-256 working memory ────────────────────────────────── +// +// `h_state` holds the running hash (8 u32 words, little-endian); +// it's initialised from H_INIT on every new compression so users +// can run more than one hash per power-on. `w` is the 64-entry +// message schedule (256 bytes). `wk` is the scratch area that +// hosts the a..h registers plus T1/T2/Σ/tmp — see +// constants.ne for the byte offsets. +var h_state: u8[32] +var w: u8[256] +var wk: u8[64] + +// ── Compression driver state ──────────────────────────────── +// +// The compression is split across frames so the main loop never +// overruns vblank. `cp_phase` selects which block of work runs +// this frame; see computing_state.ne for the phase table. +var cp_phase: u8 = 0 + +// ── Input-idle timer ──────────────────────────────────────── +// +// Incremented every frame while Entering sees no button presses. +// When it reaches AUTO_DELAY the state auto-types DEMO_TEXT and +// transitions to Computing, so the headless jsnes golden always +// captures a populated hash screen. +var idle_timer: u8 = 0 + +// Debounces repeat-fire for the face and special keys so one +// press is registered as one key press rather than one per frame +// held. +var debounce: u8 = 0 + +// ── Cursor blink ──────────────────────────────────────────── +// +// The cursor sprite on the keyboard pulses at ~2 Hz. Also used +// by Showing to mark "PRESS B TO RESET". +var blink_timer: u8 = 0 diff --git a/tests/emulator/goldens/sha256.audio.hash b/tests/emulator/goldens/sha256.audio.hash new file mode 100644 index 0000000..5f988a9 --- /dev/null +++ b/tests/emulator/goldens/sha256.audio.hash @@ -0,0 +1 @@ +a82b6ff5 132084 diff --git a/tests/emulator/goldens/sha256.png b/tests/emulator/goldens/sha256.png new file mode 100644 index 0000000000000000000000000000000000000000..d00fbe2ccdfd5321d3efe70523da810a468dfe3a GIT binary patch literal 5937 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 HUV7u-WLGQd literal 0 HcmV?d00001