this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

cleanup, more, whatever

alice 6fa17186 bea9969f

+449 -50
+28 -27
AGENTS.md
··· 8 8 - **Rewrite code location:** All Rust rewrite code lives under `tic80_rust/` (crate root). Tests live in `tic80_rust/tests/`. The windowed demo binary is `tic80_rust/src/main.rs`. 9 9 - **Plan Docs:** `RUST_REWRITE.md` defines phased roadmap; `docs/` contains early GUI-first specs and API parity checklist. 10 10 - **Prototype Crate:** `tic80_rust` (single crate for now). 11 - - `gfx::framebuffer`: 240×136 u8-index framebuffer; `cls`, `pix`, `line` (Bresenham), `rect`, `blit_to_rgba`, `print_text` (default font). 12 - - `script::lua_runner`: `mlua` (Lua 5.4, vendored) binding for `cls/pix/line/rect/print` + `BOOT/TIC` flow. 13 - - `main.rs`: `winit + pixels` presenter, fixed-step tick (~60 FPS), demo cart. 14 - - **Testing:** Rust unit tests under `tic80_rust/tests/` covering gfx and minimal Lua APIs. 11 + - `gfx::framebuffer`: 240×136 palette-index framebuffer and primitives: `cls`, `pix`, `line`, `rect`, `rectb`, `circ`, `circb`, `elli`, `ellib`, `tri`, `trib`, `clip`, `print_text`, `blit_to_rgba`. 12 + - `core::memory`: 96 KB RAM view with `peek/poke` (1/2/4/8‑bit), `memcpy`, `memset`; VRAM screen region bridged to framebuffer. 13 + - `script::lua_runner`: `mlua` (vendored Lua 5.3) binding for `BOOT/TIC` and APIs: `cls/pix/line/rect/rectb/circ/circb/elli/ellib/tri/trib/clip/print` + memory helpers. 14 + - `main.rs`: `winit + pixels` presenter, fixed‑step tick (~60 FPS), demo cart or external `.lua`. 15 + - **Testing:** Tests under `tic80_rust/tests/` cover framebuffer, Lua bridge, and memory; deterministic VRAM hash helpers. 15 16 16 17 **Build Hygiene (always do this)** 17 18 - Fix all compiler warnings before landing changes (treat warnings as errors). ··· 31 32 - **Framebuffer:** Single VRAM bank, palette indices in CPU memory; palette map/border/vbank deferred. 32 33 33 34 **Current Status** 34 - - Minimal end-to-end loop works: window opens, demo script runs (`cls/pix/line/rect/print`). 35 - - Tests run locally; 1 Lua API test currently failing (see below). 36 - - Docs/specs in place for early milestones: GUI + `cls/pix` and Lua + `cls/pix`. 37 - 38 - **Failing Test (Next Task)** 39 - - `tic80_rust/tests/lua_api_tests.rs::lua_print_defaults_and_pix_read` 40 - - Symptom: After `print("A")` at (0,0), script checks `pix(0,0)==15` to place a marker at `(w,0)`. Marker not found on row 0. 41 - - Likely Cause: Glyph rendering/metrics mismatch at origin. Candidates: 42 - - Bit orientation when decoding `src/core/font.inl` (LSB/MSB) may be inverted. 43 - - Trim logic for variable-width glyphs (`fixed=false`) may misalign the leftmost drawn column vs expected TIC-80 behavior. 44 - - Row baseline/advance constants (ADV vs actual TIC font metrics) could be off by one. 45 - - Plan: Verify bit order against TIC font reference, ensure left trim maps the first non-empty glyph column to `x`, confirm row 0 draws the top row of the glyph, adjust ADV/height semantics as needed. 35 + - All tests pass (`cargo test`). 36 + - Clippy clean (`cargo clippy --all-targets --all-features -D warnings`). 37 + - Drawing primitives implemented and exposed to Lua; clipping enforced on writes; OOB reads return `nil` in Lua. 38 + - `print` implemented with default font (variable/fixed width, scale, newline advance) and returns width. 39 + - Memory ops (`peek/poke` 1/2/4/8‑bit, `memcpy`, `memset`) implemented; VRAM updates reflect on screen. 40 + - CLI loads bundled default cart or a provided `.lua` path. 46 41 47 42 **Near-Term Backlog** 48 - - Fix `lua_print_defaults_and_pix_read` by aligning `print_text` to TIC-80 semantics. 49 - - Add unit tests for `print_text` width, left trim, and origin pixel behavior (non-Lua) to localize failures. 50 - - Expose `clip` and `rectb` to Lua; add basic tests. 51 - - Confirm palette correctness end-to-end (index→RGBA) against known swatches; keep golden samples. 43 + - Print edge cases: tests for scale>1 baseline/advance and multi‑line width parity. 44 + - Small font: decide semantics and implement `smallfont=true` in `print` with tests. 45 + - Lua error paths: add type/arity mismatch tests for core APIs (`pix/line/rect/print`). 46 + - Docs: align Lua version references (5.3) and flesh out `docs/specs/graphics.md` text rules. 47 + - Optional: add CI step for `cargo test` + clippy. 52 48 53 49 **Mid-Term Backlog** 54 - - Flesh out `tic-gfx` primitives (`circb/circ/elli/ellib/tri/trib/font/map/spr`) per `docs/api_parity_checklist.md`. 55 - - Start `tic-api` facade layering (separating binding from core) as we add more APIs. 56 - - Add frame-hash snapshot tests for deterministic VRAM state. 57 - - Begin input semantics (`btn/key/keyp/btnp/mouse`) with fixed-step repeat behavior. 50 + - Sprites/tiles: `spr`, `map`, `font`, `paint`, `ttri` (with colorkey/flip/rotate/scale, remap callback). 51 + - Banks/persistence: `vbank`, `sync`, `pmem`; palette map/border color. 52 + - Input/system: `btn/btnp`, `key/keyp`, `mouse`, `time`, `tstamp`, `trace`, `exit`, `reset` (fixed‑step repeat timing). 53 + - Audio: `sfx`, `music` synth/mixer; capture ring for analysis. 54 + - Analysis: `fft/ffts/fftr/fftrs`, `vqt` variants; conformance carts + numeric tolerances. 55 + - Platform: WASM build path; window scaling and UX polish. 58 56 59 57 **Open Questions** 60 - - Exact bit orientation for `font.inl` (confirm LSB/MSB and row order vs. current implementation). 61 - - Default `print` metrics: width advance, left/right trimming, and baseline rules in TIC-80. 62 - - Small font (`smallfont=true`) parity requirements and when to implement. 58 + - Small font (`smallfont=true`) parity: glyph source, advance, scaling, and when to land. 59 + - `print` trimming/width: exact left/right column trimming, newline advance, and width at `scale>1`. 60 + - Numeric types: confirm integer vs float acceptance/rounding for `line/tri` and similar APIs. 61 + - Palette ops: `pal/palt` semantics, palette map/border color timing, and interactions with `clip`/vbank. 62 + - Callback timing: `SCN/BDR` row timing and palette side effects integration into the pipeline. 63 + - Determinism for `time()/tstamp()`: origin/granularity guarantees for tests. 63 64 64 65 **Operating Notes** 65 66 - Keep changes surgical and test-driven; don’t expand surface while a test is red.
+1
docs/README.md
··· 11 11 - `docs/specs/lua_api_parity.md`: API parity checklist for Lua (name, signature, side effects). 12 12 - `docs/specs/graphics.md`: Framebuffer, palette mapping, text/print semantics (stub to be expanded). 13 13 - `docs/specs/audio_fft_vqt.md`: FFT/VQT behavior and parameters (points to `CLAUDE.md`). 14 + - `docs/specs/implementation_status.md`: What’s implemented vs pending, with notes on behavior. 14 15 15 16 ## Architecture 16 17 - `docs/architecture/workspace.md`: Crate layout and module boundaries.
+35 -11
docs/specs/graphics.md
··· 1 - # Graphics Spec (Stub) 1 + # Graphics Spec 2 2 3 3 Scope 4 4 - Framebuffer: 240×136, 8-bit palette indices (0..15) per pixel. 5 5 - Palette: 16 sRGB entries; index→RGBA conversion for presentation; no color-space transforms. 6 - - Text: Default font (5×8 advance within 8×8 glyph box), variable-width by trimming empty columns when `fixed=false`. 6 + - Text: Default font; 6 px advance within an 8×8 glyph box; variable-width by trimming empty columns when `fixed=false`. 7 + - Clip: Active clip rectangle constrains all drawing writes; reads are unaffected. 7 8 8 - Semantics (to expand) 9 - - `cls(color=0)`: Fill the framebuffer with palette index (masked to 0..15). 10 - - `pix(x,y[,color])`: Read returns current index or nil when OOB; write masks to 0..15 and ignores OOB. 11 - - `line/rect/rectb`: Integer rasterization; inclusive endpoints for lines; clipping to framebuffer bounds. 9 + Implemented Semantics 10 + - `cls(color=0)`: Fills framebuffer with palette index (masked to 0..15). Honors clip by design via `set_pixel` usage in higher-level draws; `cls` itself fills full screen (like TIC-80). 11 + - `pix(x,y[,color])`: 12 + - Read mode: returns palette index at `(x,y)` or `nil` (Lua)/`None` (Rust) when OOB. Reads ignore `clip`. 13 + - Write mode: writes masked color if `(x,y)` is in-bounds and inside the current `clip`; otherwise ignored. 14 + - `line(x0,y0,x1,y1,color)`: Integer Bresenham; inclusive endpoints; obeys `clip` through `set_pixel`. 15 + - `rect(x,y,w,h,color)`: Filled rect; clips to viewport and active `clip`. 16 + - `rectb(x,y,w,h,color)`: One-pixel border; inclusive edges; clipped by `clip`. 17 + - `circ(cx,cy,r,color)`: Filled circle via symmetric horizontal spans; r=0 draws a point; obeys `clip`. 18 + - `circb(cx,cy,r,color)`: One-pixel border using midpoint algorithm; r=0 draws a point; obeys `clip`. 19 + - `elli(cx,cy,a,b,color)`: Filled ellipse via horizontal spans; handles degenerate `a=0` (vertical line), `b=0` (horizontal line), and `a=b=0` (point); obeys `clip`. 20 + - `ellib(cx,cy,a,b,color)`: One-pixel border via midpoint; handles `a=b=0` as a point; obeys `clip`. 21 + - `tri(x1,y1,x2,y2,x3,y3,color)`: Filled triangle using edge functions at pixel centers with a top-left fill rule: 22 + - Include pixels on top and left edges; exclude pixels on bottom and right edges. 23 + - Processes CCW orientation internally for consistent edge testing; no gaps when tiling adjacent triangles. 24 + - Obeys `clip` through `set_pixel`. 25 + - `trib(x1,y1,x2,y2,x3,y3,color)`: One-pixel border using three inclusive Bresenham lines; edges meet at vertices; obeys `clip`. 12 26 - `print(text, x=0, y=0, color=15, fixed=false, scale=1, small=false) -> width`: 13 - - Uses default font bitmap; top-left of first drawn column is `(x,y)`. 14 - - Returns drawn width in pixels (pre-scale), consistent with TIC-80. 27 + - Font: default TIC-80 bitmap; bit order LSB-left; 8×8 glyph box; 6 px advance; variable-width trimming when `fixed=false`. 28 + - Origin: top-left of first drawn column is `(x,y)`. 29 + - Scaling: draws scaled glyphs; returned width includes scaling. 30 + - Newlines: advances by 6 px per line (scale applied); `smallfont` currently unused. 31 + 32 + Clip Behavior 33 + - `clip(x,y,w,h)`: Sets active clip rectangle; `clip()` resets to full screen. 34 + - All draw functions respect `clip` via `set_pixel`/`hspan`; `pix` reads ignore `clip`. 15 35 16 - Open items 17 - - Document exact TIC-80 font bit packing and baseline to ensure parity. 18 - - Add rules for `clip`, `palette map`, `vbank`, `border`, and blitters. 36 + Notes 37 + - Triangle parity: top-left rule ensures bit-for-bit agreement with TIC-80 edge inclusion and eliminates gaps with adjacent primitives. 38 + - Ellipse parity: on rows coinciding with axes (e.g., center row), `ellib` perimeter overlaps `elli` endpoints; resulting color depends on draw order (expected in TIC-80 as well). 39 + - `blit_to_rgba`: Converts framebuffer indices to RGBA using the 16-color palette (no gamma adjustments). 19 40 41 + Pending (not implemented yet) 42 + - `spr`, `map`, `ttri`, `paint`, `font` API variant, palette map, border color, `vbank`. 43 + - Blending rules, chroma tables, and remap callbacks for textured triangles and map drawing.
+58
docs/specs/implementation_status.md
··· 1 + # Implementation Status (Rust Rewrite) 2 + 3 + This page tracks which TIC-80 APIs are implemented in the Rust rewrite, notes per function, and what remains. 4 + 5 + Implemented (Lua + Core) 6 + - Drawing 7 + - `cls(color)`: Full-screen clear (no clip). Note: masks color to 0..15. 8 + - `pix(x,y[,color])`: Read returns color or `nil` when OOB; write obeys clip; reads ignore clip. 9 + - `line(x0,y0,x1,y1,color)`: Bresenham with inclusive endpoints; clip respected. 10 + - `rect(x,y,w,h,color)`: Filled; respects viewport and clip. 11 + - `rectb(x,y,w,h,color)`: One-pixel border; inclusive; clip respected. 12 + - `circ(cx,cy,r,color)`: Filled; r=0 is a point; clip respected. 13 + - `circb(cx,cy,r,color)`: Border; r=0 is a point; clip respected. 14 + - `elli(cx,cy,a,b,color)`: Filled; handles degenerate axes; clip respected. 15 + - `ellib(cx,cy,a,b,color)`: Border; clip respected. 16 + - `tri(x1,y1,x2,y2,x3,y3,color)`: Filled triangle with top-left rule; no gaps with adjacent triangles. 17 + - `trib(x1,y1,x2,y2,x3,y3,color)`: Border triangle via lines. 18 + - `print(text,x,y,color=15,fixed=false,scale=1,small=false) -> width`: Default font, variable-width trim, scale applied to return width; newlines advance by 6 px (times scale). 19 + - Clip 20 + - `clip(x,y,w,h)` and `clip()`: Set/reset clip rectangle affecting all draw writes; reads are unaffected. 21 + - Memory 22 + - `peek(addr[,bits=8])`, `poke(addr, value[,bits=8])`: 8/4/2/1-bit addressing across full 96 KB; VRAM screen region mapped to live framebuffer (nibble-packed 2 px/byte). 23 + - `peek1/peek2/peek4`, `poke1/poke2/poke4`: Bit-specific helpers. 24 + - `memcpy(dst, src, size)`, `memset(dst, value, size)`: Byte-wise operations; overlap-safe memcpy; VRAM ops update on-screen pixels immediately. 25 + 26 + Implemented (Runner/CLI) 27 + - `.lua` loader: First CLI arg as a `.lua` path runs external script; fallback to bundled `assets/default.lua`. 28 + - Window title: “rustic”. 29 + 30 + Behavioral Notes 31 + - Triangles: top-left inclusion; CCW orientation enforced internally; half-open bounding box prevents shared-edge double draws. 32 + - Ellipses vs circles: fill then border may overdraw endpoints; order-dependent at axis rows (parity with TIC-80). 33 + - Font: Default TIC-80 bitmap included; LSB-left bits; 6 px advance; trimming for variable width. 34 + 35 + Pending APIs (not implemented yet) 36 + - Texturing/tiles 37 + - `spr`, `map`, `ttri`, `font` (RAM font region), `paint`. 38 + - Chroma/colorkey handling, flips/rotate/scale for sprites, map remap callbacks. 39 + - Memory/banks 40 + - `vbank`, `sync`, `pmem`. 41 + - Input/system 42 + - `btn/btnp`, `key/keyp`, `mouse`, `time`, `tstamp`, `trace`, `exit`, `reset`. 43 + - Audio 44 + - `sfx`, `music`; audio mixer/synth; capture ring for analysis. 45 + - Analysis 46 + - `fft/ffts/fftr/fftrs`, `vqt/vqts/vqtr/vqtrs` and whitening variants; behavior per CLAUDE.md. 47 + - Sprite flags 48 + - `fget`, `fset`. 49 + 50 + Test Coverage (summary) 51 + - Framebuffer unit tests cover: cls/pix/line/rect/rectb/circ/circb/elli/ellib/tri/trib, clip behavior, OOB, print width/newlines, palette blit, triangle edge rules. 52 + - Lua bridge tests cover: cls/pix/line/rect/print/rectb/clip/circ/circb/elli/ellib/tri/trib and cart file loading. 53 + - Determinism: simple VRAM hash helper asserts stable hashes over N ticks for the default cart. 54 + 55 + See also 56 + - Graphics semantics: `docs/specs/graphics.md`. 57 + - API parity checklist: `docs/specs/lua_api_parity.md`. 58 + - Testing strategy and catalog: `docs/testing/strategy.md`, `docs/testing/test_catalog.md`.
+18 -1
docs/testing/test_catalog.md
··· 14 14 - `print_width_fixed_vs_variable_and_newline`: `print_text` width (fixed vs variable), newline row advance, and scale behavior. 15 15 - `clip_affects_pix_write`: `pix(x,y,color)` respects the active clip. 16 16 - `robust_oob_line_and_rectb`: OOB line still draws in-bounds; border rect crossing viewport produces in-bounds edges. 17 + - `circb_cardinals_and_oob`: Circle border’s cardinal points lit; just outside remains background. 18 + - `circ_fill_center_row_and_clip`: Filled center row span and clip restriction. 19 + - `circ_zero_radius_draws_center`: r=0 draws a point for circ/circb. 20 + - `ellib_cardinals_and_fill_center_row`: Ellipse border cardinals; filled center row interior. 21 + - `tri_fill_and_border`: Basic filled triangle and border overlay. 22 + - `tri_top_left_flat_top_inclusion`: Top-left rule on flat-top triangles (endpoints excluded on top edge). 23 + - `tri_top_left_flat_bottom_exclusion`: Bottom edge excluded on flat-bottom triangles. 24 + - `tri_adjacent_rect_no_gaps`: Two triangles tile a rectangle without gaps. 25 + - `tri_degenerate_zero_area_draws_nothing`: Collinear triangles draw nothing. 17 26 18 27 ## Lua Bridge Tests 19 28 - `tic80_rust/tests/lua_api_tests.rs` ··· 25 34 - `lua_runs_alt_cart_file`: Loads `assets/alt.lua`, checks background/marker/square. 26 35 - `lua_pix_oob_read_returns_nil`: OOB `pix` read returns `nil` in Lua. 27 36 - `lua_default_cart_deterministic_hash`: Default cart produces deterministic frame hashes for fixed tick counts. 37 + - `lua_circ_and_circb`: Circle fill and border via Lua. 38 + - `lua_elli_ellib_and_tri_trib`: Ellipse and triangle APIs via Lua. 39 + 40 + ## Memory Tests 41 + - `tic80_rust/tests/memory_tests.rs` 42 + - `poke4_sets_framebuffer_pixel`: 4-bit writes update screen pixels. 43 + - `peek4_reads_back_nibble`: 4-bit reads reflect framebuffer. 44 + - `memcpy_and_memset_affect_vram`: VRAM writes via memcpy/memset reach the screen. 45 + - `peek_poke_bits_general_ram`: 1/4-bit addressing in general RAM behaves correctly. 28 46 29 47 Notes 30 48 - Tests prefer headless framebuffer inspection over image baselines. 31 49 - Hashing uses FNV‑1a over VRAM palette indices for portability and stability. 32 -
+146
tic80_rust/src/core/memory.rs
··· 1 + use crate::gfx::framebuffer::Framebuffer; 2 + use std::cell::RefCell; 3 + use std::rc::Rc; 4 + 5 + const RAM_TOTAL: usize = 96 * 1024; // 96KB 6 + const VRAM_SIZE: usize = 16 * 1024; // first 16KB of RAM is VRAM window 7 + const VRAM_SCREEN_BYTES: usize = 0x3FC0; // 16320 bytes of screen nibble pairs 8 + 9 + pub struct Memory { 10 + ram: Vec<u8>, 11 + fb: Rc<RefCell<Framebuffer>>, // for VRAM screen mapping 12 + } 13 + 14 + impl Memory { 15 + pub fn new(fb: Rc<RefCell<Framebuffer>>) -> Self { 16 + Self { 17 + ram: vec![0; RAM_TOTAL], 18 + fb, 19 + } 20 + } 21 + 22 + // 8-bit read/write with VRAM screen mapping 23 + fn get_byte(&self, addr: usize) -> u8 { 24 + if addr < VRAM_SCREEN_BYTES { 25 + // pack 2 pixels from framebuffer into one byte (low nibble = even pixel) 26 + let p = addr * 2; // pixel index 27 + let mut fb = self.fb.borrow_mut(); 28 + let (w, h) = (Framebuffer::WIDTH as usize, Framebuffer::HEIGHT as usize); 29 + // Framebuffer stores 1 byte per pixel index; map linear order row-major 30 + // pixel p is (x=p%w, y=p/w) 31 + let mut get_px = |pi: usize| -> u8 { 32 + if pi < w * h { 33 + fb.pix((pi % w) as i32, (pi / w) as i32, None).unwrap_or(0) & 0x0F 34 + } else { 0 } 35 + }; 36 + let lo = get_px(p); 37 + let hi = get_px(p + 1); 38 + (lo & 0x0F) | ((hi & 0x0F) << 4) 39 + } else if addr < VRAM_SIZE { 40 + // other VRAM bytes (palette, etc.): just return RAM view for now 41 + self.ram[addr] 42 + } else if addr < RAM_TOTAL { 43 + self.ram[addr] 44 + } else { 45 + 0 46 + } 47 + } 48 + 49 + fn set_byte(&mut self, addr: usize, val: u8) { 50 + if addr < VRAM_SCREEN_BYTES { 51 + // unpack to 2 pixels 52 + let p = addr * 2; 53 + let lo = val & 0x0F; 54 + let hi = (val >> 4) & 0x0F; 55 + let mut fbm = self.fb.borrow_mut(); 56 + let w = Framebuffer::WIDTH as usize; 57 + let set_px = |fb: &mut Framebuffer, pi: usize, v: u8| { 58 + let x = (pi % w) as i32; 59 + let y = (pi / w) as i32; 60 + let _ = fb.pix(x, y, Some(v)); 61 + }; 62 + set_px(&mut fbm, p, lo); 63 + set_px(&mut fbm, p + 1, hi); 64 + // keep RAM mirror in case code inspects it directly 65 + self.ram[addr] = val; 66 + } else if addr < RAM_TOTAL { 67 + self.ram[addr] = val; 68 + // TODO: if palette region is written, we may later plumb it to framebuffer palette 69 + } 70 + } 71 + 72 + pub fn peek(&self, addr: usize) -> u8 { self.get_byte(addr) } 73 + pub fn poke(&mut self, addr: usize, val: u8) { self.set_byte(addr, val); } 74 + 75 + // bit-packed peeks/pokes across entire 96KB (VRAM included) 76 + pub fn peek_bits(&self, addr: usize, bits: u8) -> u8 { 77 + match bits { 78 + 8 => self.peek(addr), 79 + 4 => { 80 + let byte = self.peek(addr >> 1); 81 + if (addr & 1) == 0 { byte & 0x0F } else { (byte >> 4) & 0x0F } 82 + } 83 + 2 => { 84 + let byte = self.peek(addr >> 2); 85 + let shift = (addr & 0b11) * 2; 86 + (byte >> shift) & 0x03 87 + } 88 + 1 => { 89 + let byte = self.peek(addr >> 3); 90 + let shift = addr & 0b111; 91 + (byte >> shift) & 0x01 92 + } 93 + _ => 0, 94 + } 95 + } 96 + 97 + pub fn poke_bits(&mut self, addr: usize, bits: u8, val: u8) { 98 + match bits { 99 + 8 => self.poke(addr, val), 100 + 4 => { 101 + let mut byte = self.peek(addr >> 1); 102 + if (addr & 1) == 0 { 103 + byte = (byte & 0xF0) | (val & 0x0F); 104 + } else { 105 + byte = (byte & 0x0F) | ((val & 0x0F) << 4); 106 + } 107 + self.poke(addr >> 1, byte); 108 + } 109 + 2 => { 110 + let mut byte = self.peek(addr >> 2); 111 + let shift = (addr & 0b11) * 2; 112 + let mask = !(0x03u8 << shift); 113 + byte = (byte & mask) | ((val & 0x03) << shift); 114 + self.poke(addr >> 2, byte); 115 + } 116 + 1 => { 117 + let mut byte = self.peek(addr >> 3); 118 + let shift = addr & 0b111; 119 + let mask = !(1u8 << shift); 120 + byte = (byte & mask) | ((val & 0x01) << shift); 121 + self.poke(addr >> 3, byte); 122 + } 123 + _ => {} 124 + } 125 + } 126 + 127 + pub fn memcpy(&mut self, dst: usize, src: usize, size: usize) { 128 + if size == 0 { return; } 129 + // Handle overlap with correct direction 130 + if src < dst && src + size > dst { 131 + for i in (0..size).rev() { 132 + let b = self.get_byte(src + i); 133 + self.set_byte(dst + i, b); 134 + } 135 + } else { 136 + for i in 0..size { 137 + let b = self.get_byte(src + i); 138 + self.set_byte(dst + i, b); 139 + } 140 + } 141 + } 142 + 143 + pub fn memset(&mut self, dst: usize, value: u8, size: usize) { 144 + for i in 0..size { self.set_byte(dst + i, value); } 145 + } 146 + }
+4 -1
tic80_rust/src/lib.rs
··· 2 2 pub mod framebuffer; 3 3 } 4 4 5 + pub mod core { 6 + pub mod memory; 7 + } 8 + 5 9 pub mod script { 6 10 pub mod lua_runner; 7 11 } 8 -
+3 -1
tic80_rust/src/main.rs
··· 12 12 13 13 use tic80_rust::gfx::framebuffer::{dimensions, Framebuffer}; 14 14 use tic80_rust::script::lua_runner::LuaRunner; 15 + use tic80_rust::core::memory::Memory; 15 16 16 17 // Simple fixed-step ticker at ~60 FPS 17 18 struct Ticker { ··· 56 57 let mut pixels = Pixels::new(width, height, surface_texture)?; 57 58 58 59 let fb = Rc::new(RefCell::new(Framebuffer::new())); 60 + let mem = Rc::new(RefCell::new(Memory::new(fb.clone()))); 59 61 let mut ticker = Ticker::new(); 60 62 // Program selection: first CLI arg as .lua script, else embedded default 61 63 let args: Vec<String> = std::env::args().skip(1).collect(); ··· 74 76 } else { 75 77 DEFAULT_LUA.to_string() 76 78 }; 77 - let lua_runner = LuaRunner::new(fb.clone(), &script).ok(); 79 + let lua_runner = LuaRunner::new(fb.clone(), mem.clone(), &script).ok(); 78 80 79 81 event_loop.run(move |event, _, control_flow| { 80 82 *control_flow = ControlFlow::Poll;
+41 -1
tic80_rust/src/script/lua_runner.rs
··· 4 4 use mlua::{Function, Lua, MultiValue, RegistryKey, Result as LuaResult, Value}; 5 5 6 6 use crate::gfx::framebuffer::Framebuffer; 7 + use crate::core::memory::Memory; 7 8 8 9 pub struct LuaRunner { 9 10 lua: Lua, ··· 11 12 } 12 13 13 14 impl LuaRunner { 14 - pub fn new(fb: Rc<RefCell<Framebuffer>>, script_src: &str) -> LuaResult<Self> { 15 + pub fn new(fb: Rc<RefCell<Framebuffer>>, mem: Rc<RefCell<Memory>>, script_src: &str) -> LuaResult<Self> { 15 16 let lua = Lua::new(); 16 17 let tic_key = { 17 18 let globals = lua.globals(); ··· 139 140 Ok(width) 140 141 })?; 141 142 globals.set("print", print_fn)?; 143 + 144 + // memory: peek/poke + bit variants + memcpy/memset 145 + let mem_peek = mem.clone(); 146 + let peek_fn = lua.create_function(move |_, (addr, bits): (u32, Option<u8>)| { 147 + let a = addr as usize; 148 + let b = bits.unwrap_or(8); 149 + let v = if b == 8 { mem_peek.borrow().peek(a) } else { mem_peek.borrow().peek_bits(a, b) }; 150 + Ok(v as u32) 151 + })?; 152 + globals.set("peek", peek_fn)?; 153 + 154 + let mem_poke = mem.clone(); 155 + let poke_fn = lua.create_function(move |_, (addr, val, bits): (u32, u32, Option<u8>)| { 156 + let a = addr as usize; 157 + let v = val as u8; 158 + let b = bits.unwrap_or(8); 159 + if b == 8 { mem_poke.borrow_mut().poke(a, v); } else { mem_poke.borrow_mut().poke_bits(a, b, v); } 160 + Ok(()) 161 + })?; 162 + globals.set("poke", poke_fn)?; 163 + 164 + let mem_peek1 = mem.clone(); 165 + globals.set("peek1", lua.create_function(move |_, addr: u32| Ok(mem_peek1.borrow().peek_bits(addr as usize, 1) as u32))?)?; 166 + let mem_peek2 = mem.clone(); 167 + globals.set("peek2", lua.create_function(move |_, addr: u32| Ok(mem_peek2.borrow().peek_bits(addr as usize, 2) as u32))?)?; 168 + let mem_peek4 = mem.clone(); 169 + globals.set("peek4", lua.create_function(move |_, addr: u32| Ok(mem_peek4.borrow().peek_bits(addr as usize, 4) as u32))?)?; 170 + 171 + let mem_poke1 = mem.clone(); 172 + globals.set("poke1", lua.create_function(move |_, (addr, val): (u32, u32)| { mem_poke1.borrow_mut().poke_bits(addr as usize, 1, val as u8); Ok(()) })?)?; 173 + let mem_poke2 = mem.clone(); 174 + globals.set("poke2", lua.create_function(move |_, (addr, val): (u32, u32)| { mem_poke2.borrow_mut().poke_bits(addr as usize, 2, val as u8); Ok(()) })?)?; 175 + let mem_poke4 = mem.clone(); 176 + globals.set("poke4", lua.create_function(move |_, (addr, val): (u32, u32)| { mem_poke4.borrow_mut().poke_bits(addr as usize, 4, val as u8); Ok(()) })?)?; 177 + 178 + let mem_memcpy = mem.clone(); 179 + globals.set("memcpy", lua.create_function(move |_, (dst, src, size): (u32, u32, u32)| { mem_memcpy.borrow_mut().memcpy(dst as usize, src as usize, size as usize); Ok(()) })?)?; 180 + let mem_memset = mem.clone(); 181 + globals.set("memset", lua.create_function(move |_, (dst, val, size): (u32, u32, u32)| { mem_memset.borrow_mut().memset(dst as usize, val as u8, size as usize); Ok(()) })?)?; 142 182 143 183 // elli(x, y, a, b, color) 144 184 let fb_elli = fb.clone();
+50 -2
tic80_rust/tests/gfx_framebuffer_tests.rs
··· 48 48 fb.rect(-5, -3, 10, 8, 9); 49 49 // Count colored pixels; expected area is clipped to [0,w) x [0,h) 50 50 let (w, h) = dimensions(); 51 - let x0 = 0i32.max(-5); 52 - let y0 = 0i32.max(-3); 51 + let x0 = 0i32; 52 + let y0 = 0i32; 53 53 let x1 = ( -5 + 10).min(w as i32); 54 54 let y1 = ( -3 + 8).min(h as i32); 55 55 let expected = (x1 - x0).max(0) as usize * (y1 - y0).max(0) as usize; ··· 343 343 assert_eq!(fb.pix(20, 10, None), Some(7)); 344 344 assert_eq!(fb.pix(15, 15, None), Some(7)); 345 345 } 346 + 347 + #[test] 348 + fn tri_top_left_flat_top_inclusion() { 349 + let mut fb = Framebuffer::new(); 350 + fb.cls(0); 351 + // Flat-top triangle: top edge y=10 should be included; rightmost x excluded 352 + fb.tri(10, 10, 20, 10, 15, 15, 6); 353 + // Top scanline: interior x in (10,20) filled; endpoints excluded by top-left rule 354 + assert_eq!(fb.pix(10, 10, None), Some(0)); 355 + for x in 11..20 { assert_eq!(fb.pix(x, 10, None), Some(6)); } 356 + assert_eq!(fb.pix(20, 10, None), Some(0)); 357 + // Bottom row excluded 358 + for x in 10..=20 { assert_eq!(fb.pix(x, 15, None), Some(0)); } 359 + } 360 + 361 + #[test] 362 + fn tri_top_left_flat_bottom_exclusion() { 363 + let mut fb = Framebuffer::new(); 364 + fb.cls(0); 365 + // Flat-bottom triangle: bottom edge y=20 excluded 366 + fb.tri(10, 10, 5, 20, 15, 20, 7); 367 + for x in 5..=15 { assert_eq!(fb.pix(x, 20, None), Some(0)); } 368 + // No assumption on apex inclusion; key check is base exclusion 369 + } 370 + 371 + #[test] 372 + fn tri_adjacent_rect_no_gaps() { 373 + let mut fb = Framebuffer::new(); 374 + fb.cls(0); 375 + // Two right triangles that should fully cover a 10x10 square [0,10) x [0,10) 376 + fb.tri(0, 0, 10, 0, 0, 10, 3); 377 + fb.tri(10, 10, 10, 0, 0, 10, 3); 378 + let mut count = 0usize; 379 + for y in 0..10 { for x in 0..10 { if fb.pix(x, y, None) == Some(3) { count += 1; } } } 380 + assert_eq!(count, 100, "expected full 10x10 coverage without gaps"); 381 + } 382 + 383 + #[test] 384 + fn tri_degenerate_zero_area_draws_nothing() { 385 + let mut fb = Framebuffer::new(); 386 + fb.cls(2); 387 + // Collinear points -> zero area 388 + fb.tri(10, 10, 10, 15, 10, 20, 9); 389 + // No pixels should be colored with 9 390 + let mut any = false; 391 + for y in 10..=20 { if fb.pix(10, y, None) == Some(9) { any = true; break; } } 392 + assert!(!any, "degenerate triangle should not draw"); 393 + }
+7 -6
tic80_rust/tests/lua_api_tests.rs
··· 5 5 6 6 use tic80_rust::gfx::framebuffer::{dimensions, Framebuffer}; 7 7 use tic80_rust::script::lua_runner::LuaRunner; 8 + use tic80_rust::core::memory::Memory; 8 9 9 10 fn run_lua(script: &str, ticks: usize) -> Rc<RefCell<Framebuffer>> { 10 11 let fb = Rc::new(RefCell::new(Framebuffer::new())); 11 - let runner = LuaRunner::new(fb.clone(), script).expect("lua init"); 12 - for _ in 0..ticks { 13 - runner.tick(); 14 - } 12 + let mem = Rc::new(RefCell::new(Memory::new(fb.clone()))); 13 + let runner = LuaRunner::new(fb.clone(), mem, script).expect("lua init"); 14 + for _ in 0..ticks { runner.tick(); } 15 15 fb 16 16 } 17 17 ··· 207 207 208 208 let run_hash = |ticks: usize| -> u64 { 209 209 let fb = Rc::new(RefCell::new(Framebuffer::new())); 210 - let runner = LuaRunner::new(fb.clone(), &script).expect("lua init"); 210 + let mem = Rc::new(RefCell::new(Memory::new(fb.clone()))); 211 + let runner = LuaRunner::new(fb.clone(), mem, &script).expect("lua init"); 211 212 for _ in 0..ticks { runner.tick(); } 212 213 let mut borrowed = fb.borrow_mut(); 213 - fb_hash(&mut *borrowed) 214 + fb_hash(&mut borrowed) 214 215 }; 215 216 216 217 let h1 = run_hash(1);
+58
tic80_rust/tests/memory_tests.rs
··· 1 + use std::cell::RefCell; 2 + use std::rc::Rc; 3 + 4 + use tic80_rust::core::memory::Memory; 5 + use tic80_rust::gfx::framebuffer::Framebuffer; 6 + 7 + #[test] 8 + fn poke4_sets_framebuffer_pixel() { 9 + let fb = Rc::new(RefCell::new(Framebuffer::new())); 10 + let mut mem = Memory::new(fb.clone()); 11 + // Address 0 corresponds to first two pixels (0,0) and (1,0) 12 + mem.poke_bits(0, 4, 0xA); 13 + assert_eq!(fb.borrow_mut().pix(0, 0, None), Some(0xA)); 14 + // High nibble is pixel 1 15 + mem.poke_bits(1, 4, 0x3); 16 + assert_eq!(fb.borrow_mut().pix(1, 0, None), Some(0x3)); 17 + } 18 + 19 + #[test] 20 + fn peek4_reads_back_nibble() { 21 + let fb = Rc::new(RefCell::new(Framebuffer::new())); 22 + let mem = Memory::new(fb.clone()); 23 + // Set two pixels via framebuffer then read nibbles 24 + fb.borrow_mut().pix(0, 0, Some(7)); 25 + fb.borrow_mut().pix(1, 0, Some(12)); 26 + assert_eq!(mem.peek_bits(0, 4), 7); 27 + assert_eq!(mem.peek_bits(1, 4), 12 & 0x0F); 28 + } 29 + 30 + #[test] 31 + fn memcpy_and_memset_affect_vram() { 32 + let fb = Rc::new(RefCell::new(Framebuffer::new())); 33 + let mut mem = Memory::new(fb.clone()); 34 + // memset first 10 bytes of VRAM screen -> sets first 20 pixels (pairs) to (0x5,0x5) 35 + mem.memset(0, 0x55, 10); 36 + for x in 0..20 { assert_eq!(fb.borrow_mut().pix(x, 0, None), Some(0x5)); } 37 + 38 + // prepare source bytes with pattern 0xAB -> (0xB,0xA) on pixels 39 + let src = 40000usize; // within RAM region beyond VRAM 40 + for i in 0..4 { mem.poke(src + i, 0xAB); } 41 + mem.memcpy(0, src, 4); // copy into beginning of VRAM 42 + // First 8 pixels now map from 0xAB pairs 43 + let mut px = vec![]; 44 + for x in 0..8 { px.push(fb.borrow_mut().pix(x, 0, None).unwrap()); } 45 + assert_eq!(&px, &[0xB, 0xA, 0xB, 0xA, 0xB, 0xA, 0xB, 0xA]); 46 + } 47 + 48 + #[test] 49 + fn peek_poke_bits_general_ram() { 50 + let fb = Rc::new(RefCell::new(Framebuffer::new())); 51 + let mut mem = Memory::new(fb); 52 + let base = 90000usize; // inside 96KB 53 + mem.memset(base, 0x00, 2); 54 + mem.poke_bits(base * 2, 4, 0xF); // address in nibbles 55 + assert_eq!(mem.peek(base), 0x0F); 56 + mem.poke_bits(base * 8 + 7, 1, 1); // set MSB of first byte 57 + assert_eq!(mem.peek(base), 0x8F); 58 + }