mirror of
https://github.com/imjasonh/nescript
synced 2026-07-21 14:28:18 +00:00
examples/sha256: interactive SHA-256 hasher with on-screen keyboard
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
This commit is contained in:
parent
ee619681ff
commit
ba23f8578a
16 changed files with 1950 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
93
examples/sha256.ne
Normal file
93
examples/sha256.ne
Normal file
|
|
@ -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
|
||||
BIN
examples/sha256.nes
Normal file
BIN
examples/sha256.nes
Normal file
Binary file not shown.
436
examples/sha256/assets.ne
Normal file
436
examples/sha256/assets.ne
Normal file
|
|
@ -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).
|
||||
"........",
|
||||
"........",
|
||||
"........",
|
||||
"........",
|
||||
"........",
|
||||
"........",
|
||||
"........",
|
||||
"........"
|
||||
]
|
||||
}
|
||||
102
examples/sha256/background.ne
Normal file
102
examples/sha256/background.ne
Normal file
|
|
@ -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
|
||||
]
|
||||
}
|
||||
68
examples/sha256/computing_state.ne
Normal file
68
examples/sha256/computing_state.ne
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
190
examples/sha256/constants.ne
Normal file
190
examples/sha256/constants.ne
Normal file
|
|
@ -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
|
||||
126
examples/sha256/entering_state.ne
Normal file
126
examples/sha256/entering_state.ne
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
62
examples/sha256/keyboard.ne
Normal file
62
examples/sha256/keyboard.ne
Normal file
|
|
@ -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
|
||||
}
|
||||
131
examples/sha256/render.ne
Normal file
131
examples/sha256/render.ne
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
648
examples/sha256/sha_core.ne
Normal file
648
examples/sha256/sha_core.ne
Normal file
|
|
@ -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)
|
||||
}
|
||||
39
examples/sha256/showing_state.ne
Normal file
39
examples/sha256/showing_state.ne
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
52
examples/sha256/state.ne
Normal file
52
examples/sha256/state.ne
Normal file
|
|
@ -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
|
||||
1
tests/emulator/goldens/sha256.audio.hash
Normal file
1
tests/emulator/goldens/sha256.audio.hash
Normal file
|
|
@ -0,0 +1 @@
|
|||
a82b6ff5 132084
|
||||
BIN
tests/emulator/goldens/sha256.png
Normal file
BIN
tests/emulator/goldens/sha256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
Loading…
Add table
Add a link
Reference in a new issue