From a806bfd3bd8e07ee7c6338741e7ae3229aaaf89c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Apr 2026 22:12:43 +0000 Subject: [PATCH] hud_demo: declare proper digit + heart CHR so the HUD is readable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous version of hud_demo passed `score & 0x0F` and tile index `1` (= Heart) to nt_set / nt_fill_h, but the demo had no Heart sprite declared and tile 1 in CHR was uninitialized garbage. The result was a screen of blue smileys with a tiny red strip in the corner — the buffer mechanism worked, but the visual gave no sense that anything HUD-shaped was happening. This commit makes the HUD actually look like a HUD: - 12 sprite declarations (Bar, Heart, Digit0..9, Ball) that the compiler lays into CHR at known tile indices in declaration order. Tile-index constants (`BAR_TILE`, `HEART_TILE`, `DIGIT_BASE`) match that order so the call sites can use names instead of magic numbers. - bg1 palette restructured to `[red, white, yellow]` so pixel-art characters resolve to visible colours: `#` = red (background fill), `%` = white (heart shape), `@` = yellow (digit strokes). - Background pre-paints row 1 with the solid `Bar` (red) tile via a `legend { "B": 1 }` entry, giving the HUD a uniform red canvas for individual cell writes to land on. - Eight `nt_attr` calls at startup paint the entire top metatile row (4 rows × 32 cols) with sub-palette 1 so the HUD chrome reads as visually distinct from the playfield. The result at frame 180 is unmistakably HUD-shaped: a yellow-on- red status bar at the top of the screen above blue playfield with a yellow ball bouncing around. Per-frame cost still scales with what changed — `last_score` / `last_lives` shadow-compares mean the buffer stays empty on the ~58 of 60 frames where nothing ticks. Tests: 758 pass. Clippy clean. 48/48 emulator goldens match. --- examples/hud_demo.ne | 305 ++++++++++++++++++++++------ examples/hud_demo.nes | Bin 24592 -> 24592 bytes tests/emulator/goldens/hud_demo.png | Bin 37721 -> 37867 bytes 3 files changed, 243 insertions(+), 62 deletions(-) diff --git a/examples/hud_demo.ne b/examples/hud_demo.ne index 02a773e..b60bd72 100644 --- a/examples/hud_demo.ne +++ b/examples/hud_demo.ne @@ -2,26 +2,25 @@ // layout above a scrolling playfield. // // The playfield shows a ball bouncing back and forth; every wall -// hit bumps a score counter that the HUD renders at the top of -// the screen. A "lives" indicator to the left ticks down +// hit bumps a score counter that the HUD renders at the right of +// the status row. A "lives" indicator on the left ticks down // periodically and resets, demonstrating both `nt_set` (for // single-cell updates when state changes) and `nt_fill_h` (for -// repeatedly painting a multi-cell indicator bar). An `nt_attr` -// call at reset gives the HUD row a distinct colour palette so -// it reads as "UI chrome" instead of "gameplay background". +// repeatedly painting a multi-cell indicator). `nt_attr` calls at +// startup paint the top row in a distinct palette so the HUD +// reads as "UI chrome" instead of "gameplay background". // -// The HUD never touches `$2006` / `$2007` directly — user code -// just appends records to the 256-byte ring at `$0400-$04FF` -// and the NMI handler drains them during vblank. `nt_attr` / `nt_set` -// / `nt_fill_h` each cost one buffer entry (4..19 bytes); the -// ~2273-cycle vblank budget drains ~200 single-cell writes, so a -// full HUD refresh fits comfortably. +// User code never touches `$2006` / `$2007` directly — it just +// appends records to the 256-byte ring at `$0400-$04FF` and the +// NMI handler drains them during vblank. Only cells whose backing +// state changed get a buffer entry: the demo tracks `last_score` +// and `last_lives` so the common "state didn't change this frame" +// path appends nothing. // -// Only cells that *change* need a buffer entry: the demo tracks -// `last_score` and `last_lives` so the common "state didn't -// change this frame" path appends nothing. That's the whole -// point of the update buffer — per-frame cost scales with what -// actually changed, not with HUD complexity. +// The visible NES output at native 256×240 is intentionally +// readable rather than flashy — see the table-of-tiles layout +// rather than a polished AAA HUD. Run the ROM in any emulator to +// watch the lives countdown and score tick over time. game "HUD Demo" { mapper: NROM @@ -30,39 +29,227 @@ game "HUD Demo" { palette GameColors { universal: black - bg0: [dk_blue, blue, sky_blue] // playfield background - bg1: [dk_red, red, lt_red] // HUD row + bg0: [dk_blue, blue, sky_blue] // playfield tiles + bg1: [red, white, yellow] // HUD palette bg2: [dk_green, green, lt_green] bg3: [dk_gray, lt_gray, white] - sp0: [dk_blue, blue, sky_blue] + sp0: [black, yellow, white] // ball sprite (yellow) sp1: [red, orange, white] sp2: [dk_teal, teal, lt_teal] sp3: [dk_olive, olive, yellow] } -background Playfield { - legend { ".": 0 } - // Fill the nametable with tile 0 so sprite-0 hit plumbing - // works and the HUD cells have opaque source pixels when we - // overwrite them. The map's three rows zero-pad to the - // full 30-row nametable automatically. - map: [ - "................................", - "................................", - "................................" +// ── HUD tile art. The compiler allocates sprite tiles in +// declaration order starting at tile 1 (tile 0 is the built-in +// smiley used by the playfield). The constants below match +// that order so the call sites can use names instead of magic +// numbers. +// +// `#` = palette index 1; with bg1 = [red, white, yellow] that's +// red. `%` = index 2 = white. `@` = index 3 = yellow. + +// Solid fill (everything index 1 = red) — the "HUD chrome" +// background. Pre-painting row 1 with this tile gives the HUD +// strip a uniform red look so individual cell writes (hearts, +// digits) show up as obviously different content. +sprite Bar { + pixels: [ + "########", + "########", + "########", + "########", + "########", + "########", + "########", + "########" ] } +// White heart-on-red — uses index 2 (white) for the heart shape, +// index 1 (red) for the surrounding fill. Stands out crisply +// against the all-red Bar tiles. +sprite Heart { + pixels: [ + "########", + "#%%##%%#", + "%%%%%%%%", + "%%%%%%%%", + "#%%%%%%#", + "##%%%%##", + "###%%###", + "########" + ] +} + +// Yellow digit glyphs — all use index 3 (yellow) for the strokes +// and index 1 (red) for the surrounding fill. Big enough that the +// digit is readable even at NES resolution. +sprite Digit0 { + pixels: [ + "########", + "##@@@@##", + "#@####@#", + "#@####@#", + "#@####@#", + "#@####@#", + "##@@@@##", + "########" + ] +} +sprite Digit1 { + pixels: [ + "########", + "###@@###", + "##@@@###", + "###@@###", + "###@@###", + "###@@###", + "##@@@@##", + "########" + ] +} +sprite Digit2 { + pixels: [ + "########", + "##@@@@##", + "#@####@#", + "####@@##", + "##@@####", + "#@######", + "#@@@@@@#", + "########" + ] +} +sprite Digit3 { + pixels: [ + "########", + "##@@@@##", + "#@####@#", + "####@@##", + "######@#", + "#@####@#", + "##@@@@##", + "########" + ] +} +sprite Digit4 { + pixels: [ + "########", + "####@@##", + "###@@@##", + "##@##@##", + "#@@@@@@#", + "####@@##", + "####@@##", + "########" + ] +} +sprite Digit5 { + pixels: [ + "########", + "#@@@@@@#", + "#@######", + "#@@@@@##", + "######@#", + "#@####@#", + "##@@@@##", + "########" + ] +} +sprite Digit6 { + pixels: [ + "########", + "##@@@@##", + "#@####@#", + "#@@@@@##", + "#@####@#", + "#@####@#", + "##@@@@##", + "########" + ] +} +sprite Digit7 { + pixels: [ + "########", + "#@@@@@@#", + "######@#", + "#####@##", + "####@###", + "###@####", + "###@####", + "########" + ] +} +sprite Digit8 { + pixels: [ + "########", + "##@@@@##", + "#@####@#", + "##@@@@##", + "#@####@#", + "#@####@#", + "##@@@@##", + "########" + ] +} +sprite Digit9 { + pixels: [ + "########", + "##@@@@##", + "#@####@#", + "##@@@@@#", + "######@#", + "#@####@#", + "##@@@@##", + "########" + ] +} + +// Yellow ball for the playfield, uses sp0 sub-palette +// (index 1 = yellow against universal black). +sprite Ball { + pixels: [ + "..####..", + ".######.", + "########", + "########", + "########", + "########", + ".######.", + "..####.." + ] +} + +background Playfield { + // Pre-paint row 1 (where the HUD lives) with the solid Bar + // tile so single-cell writes (hearts and digits) stand out + // crisply against the uniform red background. Row 0 keeps the + // default smiley so the attribute-painted strip has visible + // chrome above the HUD content. Rows 2+ are zero-padded by + // the parser to fill the rest of the nametable with smileys + // — that's the playfield. + legend { + ".": 0 // smiley (default) + "B": 1 // Bar — declared first sprite, lands at tile 1 + } + map: [ + "................................", // row 0: smiley chrome + "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" // row 1: red Bar canvas for HUD + ] +} + +// Tile-index constants matching the sprite-declaration order. +const BAR_TILE: u8 = 1 +const HEART_TILE: u8 = 2 +const DIGIT_BASE: u8 = 3 // Digit N is at tile DIGIT_BASE + N + // Ball position + velocity. var bx: u8 = 64 var by: u8 = 100 var dx: u8 = 1 // 1 = moving right, 0 = moving left var dy: u8 = 1 // 1 = moving down, 0 = moving up -// HUD state. `score` increments on each wall bounce; `lives` -// ticks down once a second and resets from 5 when it hits zero. -// The `last_*` shadows let us skip the HUD write when nothing -// has changed this frame. +// HUD state. var score: u8 = 0 var lives: u8 = 5 var last_score: u8 = 255 // 255 forces an initial paint on frame 0 @@ -71,14 +258,20 @@ var life_tick: u8 = 0 var attr_set: u8 = 0 // 1 once the HUD attribute has been painted on frame { - // ── One-shot: paint the HUD attribute byte on the first - // frame only. The top metatile group picks up sub-palette - // 1 (the red HUD palette) instead of sub-palette 0 (the - // blue playfield). `0b01010101` means all four 16×16 - // quadrants of the metatile use sub-palette 1. + // ── One-shot: paint all 8 top-row attribute bytes so the + // entire top four rows pick up sub-palette 1 (red HUD + // palette). `0b01010101` means all four 16×16 sub- + // quadrants of each metatile use sub-palette 1. if attr_set == 0 { - nt_attr(0, 0, 0b01010101) attr_set = 1 + nt_attr(0, 0, 0b01010101) + nt_attr(4, 0, 0b01010101) + nt_attr(8, 0, 0b01010101) + nt_attr(12, 0, 0b01010101) + nt_attr(16, 0, 0b01010101) + nt_attr(20, 0, 0b01010101) + nt_attr(24, 0, 0b01010101) + nt_attr(28, 0, 0b01010101) } // ── Playfield: bounce the ball, count bounces as score. ── @@ -87,12 +280,14 @@ on frame { if bx >= 240 { dx = 0 score += 1 + if score >= 10 { score = 0 } } } else { bx -= 1 if bx == 16 { dx = 1 score += 1 + if score >= 10 { score = 0 } } } if dy == 1 { @@ -104,8 +299,7 @@ on frame { } draw Ball at: (bx, by) - // ── HUD: tick the lives timer. One "life" ticks every 60 - // frames (~1 sec); at zero, reset to 5. ── + // ── HUD: tick lives once per second; reset to 5 at zero. ── life_tick += 1 if life_tick >= 60 { life_tick = 0 @@ -117,34 +311,21 @@ on frame { } // ── HUD: rewrite only the cells whose backing state changed. - // When `score` ticks, update the single-digit cell at - // (28, 1) via `nt_set`. When `lives` ticks, repaint - // the whole lives bar via `nt_fill_h` (5 cells - // starting at column 2 of row 1, filled with tile 0 - // for "alive"). A pre-flight shadow-compare keeps - // the buffer empty on the ~58 of 60 frames when - // nothing changed. + // `nt_set` for the score digit, `nt_fill_h` for the + // lives bar (plus a second fill to erase the stale + // tail with the Bar tile so the display "shrinks" + // cleanly). The shadow-compare skips the write on the + // ~58 of 60 frames where nothing changed. if score != last_score { last_score = score - // Low nibble of score → tile index 0..15. A production - // HUD would use CHR-authored digit glyphs; this demo - // relies on the fact that whatever tiles happen to live - // at CHR indices 0..15 will render as *visible* changes - // frame-to-frame, making the update mechanism obvious. - var digit: u8 = score & 0x0F - nt_set(28, 1, digit) + nt_set(28, 1, DIGIT_BASE + score) } if lives != last_lives { last_lives = lives - // Fill `lives` cells at (2, 1) with the smiley tile, then - // clear the stale tail with a second fill of blank cells. - // `nt_fill_h` emits one buffer entry per call, so the - // two-step "draw, then erase" pattern costs two entries - // (~22 bytes) only on the frame the value changes. - nt_fill_h(2, 1, lives, 0) + nt_fill_h(2, 1, lives, HEART_TILE) if lives < 5 { var blanks: u8 = 5 - lives - nt_fill_h(2 + lives, 1, blanks, 1) + nt_fill_h(2 + lives, 1, blanks, BAR_TILE) } } } diff --git a/examples/hud_demo.nes b/examples/hud_demo.nes index 964c28ad389d56c9885381bfbce6e68dc10acde8..bd5e9aebb43e5cac455d6c73a60cac51817f2d19 100644 GIT binary patch literal 24592 zcmeI(L1-gY7zgk-Z<401kx3=3lV&EJbOl@7LvHS2)FB{*5-4&C#o1Yi{P$Jt)TSO--7JVIi=G#s6!WnK$qIzM1*u zWq6mo^Y(k|hQY?|_RpQ*!OKBs_=RF2^FqT5R|940O(|qSmvY_o+nOZB5)F5{Y1?rZL;x55RFYSU(4aCI!qFWGlIOnIk= zUUZ=Yym3J1GN6+fhYX?%$?f_;aB0?=Fn@}ERUc8*=|q_zx=;nWaTN$#eJpay5bGkZ zm~d25xN0a0VuN~~C<;@u!Jl-@q}gKwv-O%KK2V^LAp{wV|IDon|QDi=@Q|zAS zWUhBgi3NQ=UoCY{^cjy5OFA(xa}Sqfu0<37#g;+ypvAegTP8nW`sdG+UeBijw{!td zy76rKq^qYQojY-*^SBV1)$hB~5d~%1dyY65PqH0))VdeXDPvhTyYA?oJeZte(tW%^ zCEh)juGkPw#l=0UAl=Kn|ER(B$it?*r}y`xC!?uKcbEK37gEU^XRg>(+}?9VQ*Md_ zde(hibaPhGy=GkWK<@vmX#A!6fh7j+qdx8E%RRYEdH17>e)auhdL^8tLYDZ@GFFP# zT4~E#%57Whjgn<;m8`W_OV$hd*Q`Z*Yy7I3abP^9%}B#(2teRD5U6}w`Me@|MVb|Q zXPBPiVK$H}gAcZiL2hb+L92$BMhgqMvdsstPc1LvWkog)(94RfURGq~YMqHyT2@f< zT0Ewg6HKglyT|knWHn*d1g|}+eN_vBAsPfA009U<00Izz00bZa0SG_<0uX=z1Rwwb z2tWV=5P$##AOHafKmY;|fB*y_009U<00Izz00bZa0SG_<0uX=z1Rwwb2teRj3v^D| z;q{(hmUm^V?E4LUy@o%!>gb1o!xm$AyT1+DO+9L}@wi{6=3n*g8@6pv^!;+W6f~~J zsm~|l)6Of6boy$%5!{&?_se&t>Nj{|F_XE9_set#Q`dV_=Z~IabpO~x00Izz00bZa n0SG_<0uX=z1Rwwb2tWV=5P$##AOHafKmY;|fB*#k4+4J!aZLkO delta 394 zcmW;GJ4*vW5C`ymT#RYF*yvr7_3}U=C=v^Ug@RgN5&Z-<76HN5!Xi<&l1RcL7(`HU zgiRr8zCiZGys$`TCl)4ZmqN64PncrnKR<>kH*i_ke&ClA=~arc*-70za(ZQ4pnFmZ z#@mfy?+1vLA94aTk2vl2-sOu{%}RTVY$`F27xS1WAkx7@>h7w61y*NxN8KGI5swUF zZb3EuEUecBC9$7D^k1K;fem)2t@Wv|qi#(#{Uok!&9_mouR~y05s|2hiO5kq{pHg* zunyAdL&*SV?w4vZh&@g9sGIg6#;4LIPl^+ooKBu2;k%ss5VQSmUkN4j2pF7%qM=R!w#Y_f$Km#yV>C_;68x&F4AaYQ)K F{sZo}o6G+lpQBCWPT8^n$vb&eN7tW0{tn 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 g2<-HZyd&^>{MWumRXe`2gMXnb!&m&{