this repo has no description
0
fork

Configure Feed

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

more more more

alice 2922149c a4423c9c

+270 -23
+8
docs/README.md
··· 28 28 - `MEMORY_MAP.md` at repo root remains the canonical reference for layout. Specs here link to it rather than duplicating. 29 29 - `CLAUDE.md` remains at repo root; the spec page links to it for detailed FFT/VQT behavior. 30 30 31 + ## Run the Demo 32 + - Default cart: 33 + - From crate dir: `cd tic80_rust && cargo run` 34 + - From repo root: `cargo run --manifest-path tic80_rust/Cargo.toml` 35 + - Load a `.lua` file: 36 + - `cargo run --manifest-path tic80_rust/Cargo.toml -- tic80_rust/assets/alt.lua` 37 + - In crate dir: `cargo run -- assets/alt.lua` 38 +
+10
tic80_rust/assets/alt.lua
··· 1 + -- Alternate cart: draws a small filled square at the origin 2 + function BOOT() 3 + cls(2) 4 + end 5 + 6 + function TIC() 7 + rect(0, 0, 10, 10, 5) 8 + pix(0, 0, 7) -- top-left marker 9 + end 10 +
+69
tic80_rust/assets/default.lua
··· 1 + -- A calmer demo showcasing: cls, pix, line, rect, rectb, clip, print 2 + local t = 0 3 + local noise = {} 4 + local seed = 123456789 -- deterministic noise 5 + 6 + local function lcg() 7 + seed = (seed * 1664525 + 1013904223) % 4294967296 8 + return seed 9 + end 10 + 11 + function BOOT() 12 + cls(1) -- deep blue background 13 + -- Precompute a bunch of random pixel positions to avoid flicker 14 + for i = 1, 400 do 15 + local x = lcg() % 240 16 + local y = lcg() % 136 17 + noise[i] = { x, y } 18 + end 19 + end 20 + 21 + function TIC() 22 + -- Stable background each frame to avoid trails 23 + cls(1) 24 + 25 + -- Crosshair via pix at the center 26 + local cx, cy = 120, 68 27 + for dx = -10, 10 do pix(cx + dx, cy, 7) end 28 + for dy = -10, 10 do pix(cx, cy + dy, 7) end 29 + 30 + -- Title and legend 31 + print("TIC-80 Rust Demo", 8, 6, 15) 32 + print("cls pix line rect rectb clip print", 8, 16, 14) 33 + 34 + -- Static card: rect + rectb 35 + rectb(20, 28, 48, 22, 12) -- border 36 + rect(22, 30, 44, 18, 6) -- fill 37 + 38 + -- Gentle animation: small square drifting horizontally 39 + local ax = 90 + (t % 60) - 30 40 + rectb(ax, 34, 14, 14, 12) 41 + rect(ax + 1, 35, 12, 12, 3) 42 + 43 + -- Clip demo toggles every ~2.5 seconds (150 frames @60 FPS) 44 + local clipped = (t // 150) % 2 == 0 45 + if clipped then 46 + clip(100, 48, 60, 36) 47 + rect(92, 40, 80, 56, 4) -- clipped fill 48 + clip() 49 + rectb(100, 48, 60, 36, 14) 50 + print("clip: ON", 102, 40, 14) 51 + else 52 + clip() 53 + rect(92, 40, 80, 56, 2) -- un-clipped fill (subtle change) 54 + rectb(100, 48, 60, 36, 14) 55 + print("clip: OFF", 102, 40, 14) 56 + end 57 + 58 + -- Subtle diagonal lines 59 + line(0, 0, 239, 135, 13) 60 + line(0, 135, 239, 0, 2) 61 + 62 + -- Sprinkle precomputed random pixels ("stars") 63 + for i = 1, #noise do 64 + local p = noise[i] 65 + pix(p[1], p[2], 13) 66 + end 67 + 68 + t = t + 1 69 + end
+70 -5
tic80_rust/src/gfx/framebuffer.rs
··· 53 53 pub struct Framebuffer { 54 54 // 240x136 palette indices (0..15) 55 55 idx: Vec<u8>, 56 + // Clipping rectangle in inclusive-exclusive coords [x0,x1), [y0,y1) 57 + clip_x0: i32, 58 + clip_y0: i32, 59 + clip_x1: i32, 60 + clip_y1: i32, 56 61 } 57 62 58 63 impl Framebuffer { ··· 62 67 pub fn new() -> Self { 63 68 Self { 64 69 idx: vec![0; (WIDTH * HEIGHT) as usize], 70 + clip_x0: 0, 71 + clip_y0: 0, 72 + clip_x1: WIDTH as i32, 73 + clip_y1: HEIGHT as i32, 65 74 } 66 75 } 67 76 ··· 90 99 91 100 // Write a single pixel; returns true if in-bounds and written 92 101 pub fn set_pixel(&mut self, x: i32, y: i32, color: u8) -> bool { 93 - if x < 0 || y < 0 || x as u32 >= WIDTH || y as u32 >= HEIGHT { 102 + if x < self.clip_x0 103 + || y < self.clip_y0 104 + || x >= self.clip_x1 105 + || y >= self.clip_y1 106 + || x < 0 107 + || y < 0 108 + || x as u32 >= WIDTH 109 + || y as u32 >= HEIGHT 110 + { 94 111 return false; 95 112 } 96 113 let i = (y as u32 * WIDTH + x as u32) as usize; ··· 98 115 true 99 116 } 100 117 118 + // Set clipping rectangle to intersection with framebuffer and provided rect 119 + pub fn clip(&mut self, x: i32, y: i32, w: i32, h: i32) { 120 + if w <= 0 || h <= 0 { 121 + self.clip_x0 = 0; 122 + self.clip_y0 = 0; 123 + self.clip_x1 = 0; 124 + self.clip_y1 = 0; 125 + return; 126 + } 127 + let x0 = x.max(0); 128 + let y0 = y.max(0); 129 + let x1 = (x + w).min(WIDTH as i32); 130 + let y1 = (y + h).min(HEIGHT as i32); 131 + self.clip_x0 = x0.max(0); 132 + self.clip_y0 = y0.max(0); 133 + self.clip_x1 = x1.max(self.clip_x0); 134 + self.clip_y1 = y1.max(self.clip_y0); 135 + } 136 + 137 + pub fn clip_reset(&mut self) { 138 + self.clip_x0 = 0; 139 + self.clip_y0 = 0; 140 + self.clip_x1 = WIDTH as i32; 141 + self.clip_y1 = HEIGHT as i32; 142 + } 143 + 101 144 // Blit to RGBA buffer for pixels 102 145 pub fn blit_to_rgba(&self, rgba: &mut [u8]) { 103 146 for (px, idx) in rgba ··· 142 185 return; 143 186 } 144 187 let c = color & 0x0F; 145 - let x0 = x.max(0); 146 - let y0 = y.max(0); 147 - let x1 = (x + w).min(WIDTH as i32); 148 - let y1 = (y + h).min(HEIGHT as i32); 188 + let x0 = x.max(self.clip_x0).max(0); 189 + let y0 = y.max(self.clip_y0).max(0); 190 + let x1 = (x + w).min(self.clip_x1).min(WIDTH as i32); 191 + let y1 = (y + h).min(self.clip_y1).min(HEIGHT as i32); 149 192 if x1 <= x0 || y1 <= y0 { 150 193 return; 151 194 } ··· 155 198 let start = base + x0 as usize; 156 199 let end = base + x1 as usize; 157 200 self.idx[start..end].fill(c); 201 + } 202 + } 203 + 204 + // Rectangle border (one-pixel thick), obeys clipping 205 + pub fn rectb(&mut self, x: i32, y: i32, w: i32, h: i32, color: u8) { 206 + if w <= 0 || h <= 0 { 207 + return; 208 + } 209 + let c = color & 0x0F; 210 + let x0 = x; 211 + let y0 = y; 212 + let x1 = x + w - 1; 213 + let y1 = y + h - 1; 214 + // Top and bottom edges 215 + for xx in x0..=x1 { 216 + let _ = self.set_pixel(xx, y0, c); 217 + let _ = self.set_pixel(xx, y1, c); 218 + } 219 + // Left and right edges 220 + for yy in y0..=y1 { 221 + let _ = self.set_pixel(x0, yy, c); 222 + let _ = self.set_pixel(x1, yy, c); 158 223 } 159 224 } 160 225
+21 -18
tic80_rust/src/main.rs
··· 1 1 use std::cell::RefCell; 2 + use std::fs; 3 + use std::path::Path; 2 4 use std::rc::Rc; 3 5 use std::time::{Duration, Instant}; 4 6 ··· 35 37 } 36 38 } 37 39 38 - const DEFAULT_LUA: &str = r#" 39 - -- Minimal demo using cls and pix 40 - local t = 0 41 - function BOOT() 42 - cls(0) 43 - end 44 - function TIC() 45 - if t % 30 == 0 then cls(((t // 30) % 16)) end 46 - local cx, cy = 120, 68 47 - for dx = -10, 10 do pix(cx + dx, cy, 15) end 48 - for dy = -10, 10 do pix(cx, cy + dy, 15) end 49 - print("Hello", 10, 10, 15) 50 - line(0,0,239,135, 14) 51 - rect(20, 20, 40, 20, 9) 52 - t = t + 1 53 - end 54 - "#; 40 + const DEFAULT_LUA: &str = include_str!("../assets/default.lua"); 55 41 56 42 fn run() -> Result<(), Error> { 57 43 let event_loop = EventLoop::new(); ··· 71 57 72 58 let fb = Rc::new(RefCell::new(Framebuffer::new())); 73 59 let mut ticker = Ticker::new(); 74 - let lua_runner = LuaRunner::new(fb.clone(), DEFAULT_LUA).ok(); 60 + // Program selection: first CLI arg as .lua script, else embedded default 61 + let args: Vec<String> = std::env::args().skip(1).collect(); 62 + let script = if let Some(first) = args.get(0) { 63 + if first.ends_with(".lua") && Path::new(first).is_file() { 64 + match fs::read_to_string(first) { 65 + Ok(s) => s, 66 + Err(e) => { 67 + eprintln!("Failed to read {}: {}. Falling back to default cart.", first, e); 68 + DEFAULT_LUA.to_string() 69 + } 70 + } 71 + } else { 72 + DEFAULT_LUA.to_string() 73 + } 74 + } else { 75 + DEFAULT_LUA.to_string() 76 + }; 77 + let lua_runner = LuaRunner::new(fb.clone(), &script).ok(); 75 78 76 79 event_loop.run(move |event, _, control_flow| { 77 80 *control_flow = ControlFlow::Poll;
+26
tic80_rust/src/script/lua_runner.rs
··· 53 53 })?; 54 54 globals.set("rect", rect_fn)?; 55 55 56 + // rectb(x,y,w,h,color) 57 + let fb_rectb = fb.clone(); 58 + let rectb_fn = lua.create_function( 59 + move |_, (x, y, w, h, color): (i32, i32, i32, i32, u8)| { 60 + fb_rectb.borrow_mut().rectb(x, y, w, h, color); 61 + Ok(()) 62 + }, 63 + )?; 64 + globals.set("rectb", rectb_fn)?; 65 + 66 + // clip(x,y,w,h) or clip() to reset 67 + let fb_clip = fb.clone(); 68 + let clip_fn = lua.create_function(move |_, args: MultiValue| { 69 + if args.is_empty() { 70 + fb_clip.borrow_mut().clip_reset(); 71 + } else { 72 + let x = match args.get(0) { Some(Value::Integer(n)) => *n as i32, _ => 0 }; 73 + let y = match args.get(1) { Some(Value::Integer(n)) => *n as i32, _ => 0 }; 74 + let w = match args.get(2) { Some(Value::Integer(n)) => *n as i32, _ => 0 }; 75 + let h = match args.get(3) { Some(Value::Integer(n)) => *n as i32, _ => 0 }; 76 + fb_clip.borrow_mut().clip(x, y, w, h); 77 + } 78 + Ok(()) 79 + })?; 80 + globals.set("clip", clip_fn)?; 81 + 56 82 // print(text, x=0, y=0, color=15, fixed=false, scale=1, small=false) -> width 57 83 #[derive(Default)] 58 84 struct PrintArgs {
+30
tic80_rust/tests/gfx_framebuffer_tests.rs
··· 141 141 assert_eq!(&rgba[idx(1, 0)..idx(1, 0) + 4], &[0xFF, 0xA3, 0x00, 0xFF]); 142 142 assert_eq!(&rgba[idx(2, 0)..idx(2, 0) + 4], &[0xFF, 0xCC, 0xAA, 0xFF]); 143 143 } 144 + 145 + #[test] 146 + fn rectb_draws_border() { 147 + let mut fb = Framebuffer::new(); 148 + fb.cls(0); 149 + fb.rectb(1, 1, 3, 3, 5); 150 + // Expected 3x3 border has 8 pixels set 151 + assert_eq!(count_color(&mut fb, 5), 8); 152 + // Interior remains background 153 + assert_eq!(fb.pix(2, 2, None), Some(0)); 154 + } 155 + 156 + #[test] 157 + fn clip_limits_drawing_and_reset() { 158 + let mut fb = Framebuffer::new(); 159 + fb.cls(1); 160 + // Clip to a 1x1 at origin 161 + fb.clip(0, 0, 1, 1); 162 + fb.rect(0, 0, 10, 10, 7); 163 + // Only (0,0) can be changed by rect under this clip 164 + assert_eq!(fb.pix(0, 0, None), Some(7)); 165 + assert_eq!(fb.pix(1, 0, None), Some(1)); 166 + assert_eq!(fb.pix(0, 1, None), Some(1)); 167 + 168 + // Reset clip and draw a rect filling a small area 169 + fb.clip_reset(); 170 + fb.rect(0, 0, 2, 2, 9); 171 + assert_eq!(fb.pix(0, 0, None), Some(9)); 172 + assert_eq!(fb.pix(1, 1, None), Some(9)); 173 + }
+36
tic80_rust/tests/lua_api_tests.rs
··· 1 1 use std::cell::RefCell; 2 2 use std::rc::Rc; 3 + use std::fs; 4 + use std::path::PathBuf; 3 5 4 6 use tic80_rust::gfx::framebuffer::{dimensions, Framebuffer}; 5 7 use tic80_rust::script::lua_runner::LuaRunner; ··· 129 131 } 130 132 assert!(found, "expected a marker pixel with color 7 on row 0"); 131 133 } 134 + 135 + #[test] 136 + fn lua_runs_alt_cart_file() { 137 + // Load the alternate cart file from assets and run one tick 138 + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 139 + path.push("assets/alt.lua"); 140 + let script = fs::read_to_string(&path).expect("read alt.lua"); 141 + let fb = run_lua(&script, 1); 142 + let mut fbm = fb.borrow_mut(); 143 + // Background set in BOOT 144 + assert_eq!(fbm.pix(20, 20, None), Some(2)); 145 + // Marker at top-left and filled square 146 + assert_eq!(fbm.pix(0, 0, None), Some(7)); 147 + assert_eq!(fbm.pix(9, 9, None), Some(5)); 148 + } 149 + 150 + #[test] 151 + fn lua_clip_and_rectb() { 152 + let script = r#" 153 + function BOOT() 154 + cls(1) 155 + clip(0, 0, 1, 1) 156 + end 157 + function TIC() 158 + rectb(0, 0, 3, 3, 7) 159 + end 160 + "#; 161 + let fb = run_lua(script, 1); 162 + let mut fbm = fb.borrow_mut(); 163 + // Only origin affected due to clipping; neighbors unchanged 164 + assert_eq!(fbm.pix(0, 0, None), Some(7)); 165 + assert_eq!(fbm.pix(1, 0, None), Some(1)); 166 + assert_eq!(fbm.pix(0, 1, None), Some(1)); 167 + }