this repo has no description
0
fork

Configure Feed

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

tests fixed, font copied, looks good?

alice a4423c9c 4419236d

+1644 -80
+2
.gitignore
··· 190 190 /*.png 191 191 192 192 _references 193 + 194 + tic80_rust/target/
+80
AGENTS.md
··· 1 + **TIC-80 Rust Rewrite — Agent Log** 2 + 3 + - **Owner:** AI Coding Agent (Codex CLI) 4 + - **Scope:** Help drive the Rust rewrite forward with tests-first changes, targeted features, and tight parity with TIC-80 behavior. 5 + - **Last Updated:** 2025-08-26 6 + 7 + **Context** 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 + - **Plan Docs:** `RUST_REWRITE.md` defines phased roadmap; `docs/` contains early GUI-first specs and API parity checklist. 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. 15 + 16 + **Build Hygiene (always do this)** 17 + - Fix all compiler warnings before landing changes (treat warnings as errors). 18 + - Run clippy on the crate and keep zero warnings: `cd tic80_rust && cargo clippy --all-targets --all-features -D warnings`. 19 + - Validate with tests: `cd tic80_rust && cargo test` (and run the specific failing test during fixes). 20 + - Keep changes minimal and focused; don’t expand scope while tests are red. 21 + 22 + **Decisions (Locked for prototype)** 23 + - **Presentation:** `winit + pixels` with integer scaling (2x/3x/4x), RGBA palette conversion from 16-color default. 24 + - **Lua Engine:** `mlua` with vendored Lua 5.4, behavior aligned to 5.2 where needed (compat noted in docs). 25 + - **Framebuffer:** Single VRAM bank, palette indices in CPU memory; palette map/border/vbank deferred. 26 + 27 + **Current Status** 28 + - Minimal end-to-end loop works: window opens, demo script runs (`cls/pix/line/rect/print`). 29 + - Tests run locally; 1 Lua API test currently failing (see below). 30 + - Docs/specs in place for early milestones: GUI + `cls/pix` and Lua + `cls/pix`. 31 + 32 + **Failing Test (Next Task)** 33 + - `tic80_rust/tests/lua_api_tests.rs::lua_print_defaults_and_pix_read` 34 + - 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. 35 + - Likely Cause: Glyph rendering/metrics mismatch at origin. Candidates: 36 + - Bit orientation when decoding `src/core/font.inl` (LSB/MSB) may be inverted. 37 + - Trim logic for variable-width glyphs (`fixed=false`) may misalign the leftmost drawn column vs expected TIC-80 behavior. 38 + - Row baseline/advance constants (ADV vs actual TIC font metrics) could be off by one. 39 + - 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. 40 + 41 + **Near-Term Backlog** 42 + - Fix `lua_print_defaults_and_pix_read` by aligning `print_text` to TIC-80 semantics. 43 + - Add unit tests for `print_text` width, left trim, and origin pixel behavior (non-Lua) to localize failures. 44 + - Expose `clip` and `rectb` to Lua; add basic tests. 45 + - Confirm palette correctness end-to-end (index→RGBA) against known swatches; keep golden samples. 46 + 47 + **Mid-Term Backlog** 48 + - Flesh out `tic-gfx` primitives (`circb/circ/elli/ellib/tri/trib/font/map/spr`) per `docs/api_parity_checklist.md`. 49 + - Start `tic-api` facade layering (separating binding from core) as we add more APIs. 50 + - Add frame-hash snapshot tests for deterministic VRAM state. 51 + - Begin input semantics (`btn/key/keyp/btnp/mouse`) with fixed-step repeat behavior. 52 + 53 + **Open Questions** 54 + - Exact bit orientation for `font.inl` (confirm LSB/MSB and row order vs. current implementation). 55 + - Default `print` metrics: width advance, left/right trimming, and baseline rules in TIC-80. 56 + - Small font (`smallfont=true`) parity requirements and when to implement. 57 + 58 + **Operating Notes** 59 + - Keep changes surgical and test-driven; don’t expand surface while a test is red. 60 + - Prefer headless tests (VRAM hash or targeted pixel asserts) to validate behavior deterministically. 61 + - Document any intentional deviations from TIC-80 behavior in this file and update `docs/api_parity_checklist.md` as needed. 62 + 63 + **Worklog** 64 + - 2025-08-26: 65 + - Reviewed `RUST_REWRITE.md` and `docs/` (API parity, GUI-first milestones). 66 + - Surveyed `tic80_rust` crate; confirmed implemented APIs (`cls/pix/line/rect/print`). 67 + - Ran tests: one failure in Lua print-origin behavior identified; queued as next fix. 68 + - Reorganized documentation: 69 + - Moved plan to `docs/roadmap/overview.md` with a root stub. 70 + - Merged GUI-first docs into `docs/roadmap/gui_first.md`. 71 + - Renamed API parity to `docs/specs/lua_api_parity.md` and added specs stubs. 72 + - Added docs index at `docs/README.md`, architecture/testing pages, and ADRs. 73 + 74 + **Docs Index** 75 + - Start here: `docs/README.md` 76 + - Roadmap: `docs/roadmap/overview.md`, `docs/roadmap/gui_first.md` 77 + - Specs: `docs/specs/memory_map.md`, `docs/specs/lua_api_parity.md`, `docs/specs/graphics.md`, `docs/specs/audio_fft_vqt.md` 78 + - Architecture: `docs/architecture/workspace.md`, `docs/architecture/runtime.md` 79 + - Testing: `docs/testing/strategy.md`, `docs/testing/frame_hashes.md` 80 + - ADRs: `docs/adr/0001-winit-pixels.md`, `docs/adr/0002-mlua-lua54-compat.md`
+591
MEMORY_MAP.md
··· 1 + # TIC-80 tiny computer 2 + 1.1.2837 (be42d6f) 3 + https://tic80.com (C) 2017-2023 4 + 5 + ## Welcome 6 + TIC-80 is a fantasy computer for making, playing and sharing tiny games. 7 + 8 + It has built-in tools for development: code, sprites, maps, sound editors and the command line, which is enough to create a mini retro game. 9 + In the end, you will get a cartridge file, which can be stored and played on the website. 10 + 11 + Also, the game can be packed into a player that works on all popular platforms and distributed as you wish. 12 + To make a retro-style game, the whole creation process takes place under some technical limitations: 240x136 pixels display, 16 color palette, 256 8x8 color sprites, 4 channel sound, etc. 13 + 14 + ## Specification 15 + ``` 16 + DISPLAY 240x136 pixels, 16 colors palette. 17 + INPUT 4 gamepads with 8 buttons / mouse / keyboard. 18 + SPRITES 256 8x8 tiles and 256 8x8 sprites. 19 + MAP 240x136 cells, 1920x1088 pixels. 20 + SOUND 4 channels with configurable waveforms. 21 + CODE 64KB of lua, ruby, js, moon, fennel, scheme, squirrel, wren, wasm, janet or python. 22 + ``` 23 + ``` 24 + 25 + +-----------------------------------+ 26 + | 96KB RAM LAYOUT | 27 + +-------+-------------------+-------+ 28 + | ADDR | INFO | BYTES | 29 + +-------+-------------------+-------+ 30 + | 00000 | <VRAM> | 16384 | 31 + | 04000 | TILES | 8192 | 32 + | 06000 | SPRITES | 8192 | 33 + | 08000 | MAP | 32640 | 34 + | 0FF80 | GAMEPADS | 4 | 35 + | 0FF84 | MOUSE | 4 | 36 + | 0FF88 | KEYBOARD | 4 | 37 + | 0FF8C | SFX STATE | 16 | 38 + | 0FF9C | SOUND REGISTERS | 72 | 39 + | 0FFE4 | WAVEFORMS | 256 | 40 + | 100E4 | SFX | 4224 | 41 + | 11164 | MUSIC PATTERNS | 11520 | 42 + | 13E64 | MUSIC TRACKS | 408 | 43 + | 13FFC | MUSIC STATE | 4 | 44 + | 14000 | STEREO VOLUME | 4 | 45 + | 14004 | PERSISTENT MEMORY | 1024 | 46 + | 14404 | SPRITE FLAGS | 512 | 47 + | 14604 | FONT | 1016 | 48 + | 149FC | FONT PARAMS | 8 | 49 + | 14A04 | ALT FONT | 1016 | 50 + | 14DFC | ALT FONT PARAMS | 8 | 51 + | 14E04 | BUTTONS MAPPING | 32 | 52 + | 14E24 | ** RESERVED ** | 12764 | 53 + +-------+-------------------+-------+ 54 + ``` 55 + ``` 56 + +-----------------------------------+ 57 + | 16KB VRAM LAYOUT | 58 + +-------+-------------------+-------+ 59 + | ADDR | INFO | BYTES | 60 + +-------+-------------------+-------+ 61 + | 00000 | SCREEN | 16320 | 62 + | 03FC0 | PALETTE | 48 | 63 + | 03FF0 | PALETTE MAP | 8 | 64 + | 03FF8 | BORDER COLOR | 1 | 65 + | 03FF9 | SCREEN OFFSET | 2 | 66 + | 03FFB | MOUSE CURSOR | 1 | 67 + | 03FFC | BLIT SEGMENT | 1 | 68 + | 03FFD | ... (reserved) | 3 | 69 + +-------+-------------------+-------+ 70 + ``` 71 + 72 + ## Console commands 73 + 74 + ### cd 75 + change directory. 76 + usage: ` 77 + cd <path> 78 + cd / 79 + cd ..` 80 + 81 + ### cls 82 + clear console screen. 83 + usage: `cls` 84 + 85 + ### config 86 + edit system configuration cartridge, 87 + use `reset` param to reset current configuration, 88 + use `default` to edit default cart template. 89 + usage: `config [reset|default]` 90 + 91 + ### del 92 + delete from the filesystem. 93 + usage: `del <file|folder>` 94 + 95 + ### demo 96 + install demo carts to the current directory. 97 + usage: `demo` 98 + 99 + ### dir 100 + show list of local files. 101 + usage: `dir` 102 + 103 + ### edit 104 + open cart editors. 105 + usage: `edit` 106 + 107 + ### eval 108 + run code provided code. 109 + usage: `eval` 110 + 111 + ### exit 112 + exit the application. 113 + usage: `exit` 114 + 115 + ### export 116 + export cart to HTML, 117 + native build (win linux rpi mac), 118 + export sprites/map/... as a .png image or export sfx and music to .wav files. 119 + usage: ` 120 + export [win|winxp|linux|rpi|mac|html|binary|tiles|sprites|map|mapimg|sfx|music|screen|help|...] <file> [bank=0 vbank=0 id=0 ...]` 121 + 122 + ### folder 123 + open working directory in OS. 124 + usage: `folder` 125 + 126 + ### help 127 + show help info about commands/api/... 128 + usage: `help [<text>|version|welcome|spec|ram|vram|commands|api|keys|buttons|startup|terms|license]` 129 + 130 + ### import 131 + import code/sprites/map/... from an external file. 132 + usage: ` 133 + import [binary|tiles|sprites|map|code|screen|...] <file> [bank=0 x=0 y=0 w=0 h=0 vbank=0 ...]` 134 + 135 + ### load 136 + load cartridge from the local filesystem(there's no need to type the .tic extension). 137 + you can also load just the section (sprites, map etc) from another cart. 138 + usage: `load <cart> [code|tiles|sprites|map|sfx|music|palette|flags|screen]` 139 + 140 + ### menu 141 + show game menu where you can setup video, sound and input options. 142 + usage: `menu` 143 + 144 + ### mkdir 145 + make a directory. 146 + usage: `mkdir <name>` 147 + 148 + ### new 149 + creates a new `Hello World` cartridge. 150 + usage: `new <lua|ruby|js|moon|fennel|scheme|squirrel|wren|wasm|janet|python>` 151 + 152 + ### resume 153 + resume last run cart / project. 154 + usage: `resume` 155 + 156 + ### run 157 + run current cart / project. 158 + usage: `run` 159 + 160 + ### save 161 + save cartridge to the local filesystem, use .lua .rb .js .moon .fnl .scm .nut .wren .wasmp .janet .py cart extension to save it in text format (PRO feature). 162 + usage: `save <cart>` 163 + 164 + ### surf 165 + open carts browser. 166 + usage: `surf` 167 + 168 + ## API functions 169 + 170 + ### BDR 171 + `BDR(row)` 172 + Allows you to execute code between the drawing of each fullscreen scanline, for example, to manipulate the palette. 173 + 174 + ### BOOT 175 + `BOOT` 176 + Startup function. 177 + 178 + ### MENU 179 + `MENU(index)` 180 + Game Menu handler. 181 + 182 + ### SCN 183 + `SCN(row)` 184 + Allows you to execute code between the drawing of each scanline, for example, to manipulate the palette. 185 + 186 + ### TIC 187 + `TIC()` 188 + Main function. It's called at 60fps (60 times every second). 189 + 190 + ### btn 191 + `btn(id) -> pressed` 192 + This function allows you to read the status of one of the buttons attached to TIC. 193 + The function returns true if the key with the supplied id is currently in the pressed state. 194 + It remains true for as long as the key is held down. 195 + If you want to test if a key was just pressed, use `btnp()` instead. 196 + 197 + ### btnp 198 + `btnp(id hold=-1 period=-1) -> pressed` 199 + This function allows you to read the status of one of TIC's buttons. 200 + It returns true only if the key has been pressed since the last frame. 201 + You can also use the optional hold and period parameters which allow you to check if a button is being held down. 202 + After the time specified by hold has elapsed, btnp will return true each time period is passed if the key is still down. 203 + For example, to re-examine the state of button `0` after 2 seconds and continue to check its state every 1/10th of a second, you would use btnp(0, 120, 6). 204 + Since time is expressed in ticks and TIC runs at 60 frames per second, we use the value of 120 to wait 2 seconds and 6 ticks (ie 60/10) as the interval for re-checking. 205 + 206 + ### circ 207 + `circ(x y radius color)` 208 + This function draws a filled circle of the desired radius and color with its center at x, y. 209 + It uses the Bresenham algorithm. 210 + 211 + ### circb 212 + `circb(x y radius color)` 213 + Draws the circumference of a circle with its center at x, y using the radius and color requested. 214 + It uses the Bresenham algorithm. 215 + 216 + ### clip 217 + `clip(x y width height) 218 + clip()` 219 + This function limits drawing to a clipping region or `viewport` defined by x,y,w,h. 220 + Things drawn outside of this area will not be visible. 221 + Calling clip() with no parameters will reset the drawing area to the entire screen. 222 + 223 + ### cls 224 + `cls(color=0)` 225 + Clear the screen. 226 + When called this function clear all the screen using the color passed as argument. 227 + If no parameter is passed first color (0) is used. 228 + 229 + ### elli 230 + `elli(x y a b color)` 231 + This function draws a filled ellipse of the desired a, b radiuses and color with its center at x, y. 232 + It uses the Bresenham algorithm. 233 + 234 + ### ellib 235 + `ellib(x y a b color)` 236 + This function draws an ellipse border with the desired radiuses a b and color with its center at x, y. 237 + It uses the Bresenham algorithm. 238 + 239 + ### exit 240 + `exit()` 241 + Interrupts program execution and returns to the console when the TIC function ends. 242 + 243 + ### fget 244 + `fget(sprite_id flag) -> bool` 245 + Returns true if the specified flag of the sprite is set. See `fset()` for more details. 246 + 247 + ### font 248 + `font(text x y chromakey char_width char_height fixed=false scale=1 alt=false) -> width` 249 + Print string with font defined in foreground sprites. 250 + To simply print to the screen, check out `print()`. 251 + To print to the console, check out `trace()`. 252 + 253 + ### fset 254 + `fset(sprite_id flag bool)` 255 + Each sprite has eight flags which can be used to store information or signal different conditions. 256 + For example, flag 0 might be used to indicate that the sprite is invisible, flag 6 might indicate that the flag should be draw scaled etc. 257 + See algo `fget()`. 258 + 259 + ### key 260 + `key(code=-1) -> pressed` 261 + The function returns true if the key denoted by keycode is pressed. 262 + 263 + ### keyp 264 + `keyp(code=-1 hold=-1 period=-1) -> pressed` 265 + This function returns true if the given key is pressed but wasn't pressed in the previous frame. 266 + Refer to `btnp()` for an explanation of the optional hold and period parameters. 267 + 268 + ### line 269 + `line(x0 y0 x1 y1 color)` 270 + Draws a straight line from point (x0,y0) to point (x1,y1) in the specified color. 271 + 272 + ### map 273 + `map(x=0 y=0 w=30 h=17 sx=0 sy=0 colorkey=-1 scale=1 remap=nil)` 274 + The map consists of cells of 8x8 pixels, each of which can be filled with a sprite using the map editor. 275 + The map can be up to 240 cells wide by 136 deep. 276 + This function will draw the desired area of the map to a specified screen position. 277 + For example, map(5,5,12,10,0,0) will draw a 12x10 section of the map, starting from map coordinates (5,5) to screen position (0,0). 278 + The map function's last parameter is a powerful callback function for changing how map cells (sprites) are drawn when map is called. 279 + It can be used to rotate, flip and replace sprites while the game is running. 280 + Unlike mset, which saves changes to the map, this special function can be used to create animated tiles or replace them completely. 281 + Some examples include changing sprites to open doorways, hiding sprites used to spawn objects in your game and even to emit the objects themselves. 282 + The tilemap is laid out sequentially in RAM - writing 1 to 0x08000 will cause tile(sprite) #1 to appear at top left when map() is called. 283 + To set the tile immediately below this we need to write to 0x08000 + 240, ie 0x080F0. 284 + 285 + ### memcpy 286 + `memcpy(dest source size)` 287 + This function allows you to copy a continuous block of TIC's 96K RAM from one address to another. 288 + Addresses are specified are in hexadecimal format, values are decimal. 289 + 290 + ### memset 291 + `memset(dest value size)` 292 + This function allows you to set a continuous block of any part of TIC's RAM to the same value. 293 + The address is specified in hexadecimal format, the value in decimal. 294 + 295 + ### mget 296 + `mget(x y) -> tile_id` 297 + Gets the sprite id at the given x and y map coordinate. 298 + 299 + ### mouse 300 + `mouse() -> x y left middle right scrollx scrolly` 301 + This function returns the mouse coordinates and a boolean value for the state of each mouse button,with true indicating that a button is pressed. 302 + 303 + ### mset 304 + `mset(x y tile_id)` 305 + This function will change the tile at the specified map coordinates. 306 + By default, changes made are only kept while the current game is running. 307 + To make permanent changes to the map, see `sync()`. 308 + Related: `map()` `mget()` `sync()`. 309 + 310 + ### music 311 + `music(track=-1 frame=-1 row=-1 loop=true sustain=false tempo=-1 speed=-1)` 312 + This function starts playing a track created in the Music Editor. 313 + Call without arguments to stop the music. 314 + 315 + ### peek 316 + `peek(addr bits=8) -> value` 317 + This function allows to read the memory from TIC. 318 + It's useful to access resources created with the integrated tools like sprite, maps, sounds, cartridges data? 319 + Never dream to sound a sprite? 320 + Address are in hexadecimal format but values are decimal. 321 + To write to a memory address, use `poke()`. 322 + `bits` allowed to be 1,2,4,8. 323 + 324 + ### peek1 325 + `peek1(addr) -> value` 326 + This function enables you to read single bit values from TIC's RAM. 327 + The address is often specified in hexadecimal format. 328 + 329 + ### peek2 330 + `peek2(addr) -> value` 331 + This function enables you to read two bits values from TIC's RAM. 332 + The address is often specified in hexadecimal format. 333 + 334 + ### peek4 335 + `peek4(addr) -> value` 336 + This function enables you to read values from TIC's RAM. 337 + The address is often specified in hexadecimal format. 338 + See 'poke4()' for detailed information on how nibble addressing compares with byte addressing. 339 + 340 + ### pix 341 + `pix(x y color) 342 + pix(x y) -> color` 343 + This function can read or write pixel color values. 344 + When called with a color parameter, the pixel at the specified coordinates is set to that color. 345 + Calling the function without a color parameter returns the color of the pixel at the specified position. 346 + 347 + ### pmem 348 + `pmem(index value) 349 + pmem(index) -> value` 350 + This function allows you to save and retrieve data in one of the 256 individual 32-bit slots available in the cartridge's persistent memory. 351 + This is useful for saving high-scores, level advancement or achievements. 352 + The data is stored as unsigned 32-bit integers (from 0 to 4294967295). 353 + 354 + Tips: 355 + - pmem depends on the cartridge hash (md5), so don't change your lua script if you want to keep the data. 356 + - Use `saveid:` with a personalized string in the header metadata to override the default MD5 calculation. 357 + This allows the user to update a cart without losing their saved data. 358 + 359 + ### poke 360 + `poke(addr value bits=8)` 361 + This function allows you to write a single byte to any address in TIC's RAM. 362 + The address should be specified in hexadecimal format, the value in decimal. 363 + `bits` allowed to be 1,2,4,8. 364 + 365 + ### poke1 366 + `poke1(addr value)` 367 + This function allows you to write single bit values directly to RAM. 368 + The address is often specified in hexadecimal format. 369 + 370 + ### poke2 371 + `poke2(addr value)` 372 + This function allows you to write two bits values directly to RAM. 373 + The address is often specified in hexadecimal format. 374 + 375 + ### poke4 376 + `poke4(addr value)` 377 + This function allows you to write directly to RAM. 378 + The address is often specified in hexadecimal format. 379 + For both peek4 and poke4 RAM is addressed in 4 bit segments (nibbles). 380 + Therefore, to access the the RAM at byte address 0x4000 381 + you would need to access both the 0x8000 and 0x8001 nibble addresses. 382 + 383 + ### print 384 + `print(text x=0 y=0 color=15 fixed=false scale=1 smallfont=false) -> width` 385 + This will simply print text to the screen using the font defined in config. 386 + When set to true, the fixed width option ensures that each character will be printed in a `box` of the same size, so the character `i` will occupy the same width as the character `w` for example. 387 + When fixed width is false, there will be a single space between each character. 388 + 389 + Tips: 390 + - To use a custom rastered font, check out `font()`. 391 + - To print to the console, check out `trace()`. 392 + 393 + ### rect 394 + `rect(x y w h color)` 395 + This function draws a filled rectangle of the desired size and color at the specified position. 396 + If you only need to draw the the border or outline of a rectangle (ie not filled) see `rectb()`. 397 + 398 + ### rectb 399 + `rectb(x y w h color)` 400 + This function draws a one pixel thick rectangle border at the position requested. 401 + If you need to fill the rectangle with a color, see `rect()` instead. 402 + 403 + ### reset 404 + `reset()` 405 + Resets the cartridge. To return to the console, see the `exit()`. 406 + 407 + ### sfx 408 + `sfx(id note=-1 duration=-1 channel=0 volume=15 speed=0)` 409 + This function will play the sound with `id` created in the sfx editor. 410 + Calling the function with id set to -1 will stop playing the channel. 411 + The note can be supplied as an integer between 0 and 95 (representing 8 octaves of 12 notes each) or as a string giving the note name and octave. 412 + For example, a note value of `14` will play the note `D` in the second octave. 413 + The same note could be specified by the string `D-2`. 414 + Note names consist of two characters, the note itself (in upper case) followed by `-` to represent the natural note or `#` to represent a sharp. 415 + There is no option to indicate flat values. 416 + The available note names are therefore: C-, C#, D-, D#, E-, F-, F#, G-, G#, A-, A#, B-. 417 + The `octave` is specified using a single digit in the range 0 to 8. 418 + The `duration` specifies how many ticks to play the sound for since TIC-80 runs at 60 frames per second, a value of 30 represents half a second. 419 + A value of -1 will play the sound continuously. 420 + The `channel` parameter indicates which of the four channels to use. Allowed values are 0 to 3. 421 + The `volume` can be between 0 and 15. 422 + The `speed` in the range -4 to 3 can be specified and means how many `ticks+1` to play each step, so speed==0 means 1 tick per step. 423 + 424 + ### spr 425 + `spr(id x y colorkey=-1 scale=1 flip=0 rotate=0 w=1 h=1)` 426 + Draws the sprite number index at the x and y coordinate. 427 + You can specify a colorkey in the palette which will be used as the transparent color or use a value of -1 for an opaque sprite. 428 + The sprite can be scaled up by a desired factor. For example, a scale factor of 2 means an 8x8 pixel sprite is drawn to a 16x16 area of the screen. 429 + You can flip the sprite where: 430 + - 0 = No Flip 431 + - 1 = Flip horizontally 432 + - 2 = Flip vertically 433 + - 3 = Flip both vertically and horizontally 434 + When you rotate the sprite, it's rotated clockwise in 90 steps: 435 + - 0 = No rotation 436 + - 1 = 90 rotation 437 + - 2 = 180 rotation 438 + - 3 = 270 rotation 439 + You can draw a composite sprite (consisting of a rectangular region of sprites from the sprite sheet) by specifying the `w` and `h` parameters (which default to 1). 440 + 441 + ### sync 442 + `sync(mask=0 bank=0 tocart=false)` 443 + The pro version of TIC-80 contains 8 memory banks. 444 + To switch between these banks, sync can be used to either load contents from a memory bank to runtime, or save contents from the active runtime to a bank. 445 + The function can only be called once per frame.If you have manipulated the runtime memory (e.g. by using mset), you can reset the active state by calling sync(0,0,false). 446 + This resets the whole runtime memory to the contents of bank 0.Note that sync is not used to load code from banks; this is done automatically. 447 + 448 + ### time 449 + `time() -> ticks` 450 + This function returns the number of milliseconds elapsed since the cartridge began execution. 451 + Useful for keeping track of time, animating items and triggering events. 452 + 453 + ### trace 454 + `trace(message color=15)` 455 + This is a service function, useful for debugging your code. 456 + It prints the message parameter to the console in the (optional) color specified. 457 + 458 + Tips: 459 + - The Lua concatenator for strings is .. (two points). 460 + - Use console cls command to clear the output from trace. 461 + 462 + ### tri 463 + `tri(x1 y1 x2 y2 x3 y3 color)` 464 + This function draws a triangle filled with color, using the supplied vertices. 465 + 466 + ### trib 467 + `trib(x1 y1 x2 y2 x3 y3 color)` 468 + This function draws a triangle border with color, using the supplied vertices. 469 + 470 + ### tstamp 471 + `tstamp() -> timestamp` 472 + This function returns the number of seconds elapsed since January 1st, 1970. 473 + Useful for creating persistent games which evolve over time between plays. 474 + 475 + ### ttri 476 + `ttri(x1 y1 x2 y2 x3 y3 u1 v1 u2 v2 u3 v3 texsrc=0 chromakey=-1 z1=0 z2=0 z3=0)` 477 + It renders a triangle filled with texture from image ram, map ram or vbank. 478 + Use in 3D graphics. 479 + In particular, if the vertices in the triangle have different 3D depth, you may see some distortion. 480 + These can be thought of as the window inside image ram (sprite sheet), map ram or another vbank. 481 + Note that the sprite sheet or map in this case is treated as a single large image, with U and V addressing its pixels directly, rather than by sprite ID. 482 + So for example the top left corner of sprite #2 would be located at u=16, v=0. 483 + 484 + ### vbank 485 + `vbank(bank) -> prev 486 + vbank() -> prev` 487 + VRAM contains 2x16K memory chips, use vbank(0) or vbank(1) to switch between them. 488 + 489 + ## Button IDs 490 + ``` 491 + +--------+----+----+----+----+ 492 + | ACTION | P1 | P2 | P3 | P4 | 493 + +--------+----+----+----+----+ 494 + | UP | 0 | 8 | 16 | 24 | 495 + | DOWN | 1 | 9 | 17 | 25 | 496 + | LEFT | 2 | 10 | 18 | 26 | 497 + | RIGHT | 3 | 11 | 19 | 27 | 498 + | A | 4 | 12 | 20 | 28 | 499 + | B | 5 | 13 | 21 | 29 | 500 + | X | 6 | 14 | 22 | 30 | 501 + | Y | 7 | 15 | 23 | 31 | 502 + +--------+----+----+----+----+``` 503 + 504 + ## Key IDs 505 + ``` 506 + +----+------------+ +----+------------+ 507 + |CODE| KEY | |CODE| KEY | 508 + +----+------------+ +----+------------+ 509 + | 1 | A | | 48 | SPACE | 510 + | 2 | B | | 49 | TAB | 511 + | 3 | C | | 50 | RETURN | 512 + | 4 | D | | 51 | BACKSPACE | 513 + | 5 | E | | 52 | DELETE | 514 + | 6 | F | | 53 | INSERT | 515 + | 7 | G | | 54 | PAGEUP | 516 + | 8 | H | | 55 | PAGEDOWN | 517 + | 9 | I | | 56 | HOME | 518 + | 10 | J | | 57 | END | 519 + | 11 | K | | 58 | UP | 520 + | 12 | L | | 59 | DOWN | 521 + | 13 | M | | 60 | LEFT | 522 + | 14 | N | | 61 | RIGHT | 523 + | 15 | O | | 62 | CAPSLOCK | 524 + | 16 | P | | 63 | CTRL | 525 + | 17 | Q | | 64 | SHIFT | 526 + | 18 | R | | 65 | ALT | 527 + | 19 | S | | 66 | ESC | 528 + | 20 | T | | 67 | F1 | 529 + | 21 | U | | 68 | F2 | 530 + | 22 | V | | 69 | F3 | 531 + | 23 | W | | 70 | F4 | 532 + | 24 | X | | 71 | F5 | 533 + | 25 | Y | | 72 | F6 | 534 + | 26 | Z | | 73 | F7 | 535 + | 27 | 0 | | 74 | F8 | 536 + | 28 | 1 | | 75 | F9 | 537 + | 29 | 2 | | 76 | F10 | 538 + | 30 | 3 | | 77 | F11 | 539 + | 31 | 4 | | 78 | F12 | 540 + | 32 | 5 | | 79 | NUM0 | 541 + | 33 | 6 | | 80 | NUM1 | 542 + | 34 | 7 | | 81 | NUM2 | 543 + | 35 | 8 | | 82 | NUM3 | 544 + | 36 | 9 | | 83 | NUM4 | 545 + | 37 | MINUS | | 84 | NUM5 | 546 + | 38 | EQUALS | | 85 | NUM6 | 547 + | 39 | LEFTBRACKET| | 86 | NUM7 | 548 + | 40 | RIGHTBRACKT| | 87 | NUM8 | 549 + | 41 | BACKSLASH | | 88 | NUM9 | 550 + | 42 | SEMICOLON | | 89 | NUMPLUS | 551 + | 43 | APOSTROPHE | | 90 | NUMMINUS | 552 + | 44 | GRAVE | | 91 | NUMMULTIPLY| 553 + | 45 | COMMA | | 92 | NUMDIVIDE | 554 + | 46 | PERIOD | | 93 | NUMENTER | 555 + | 47 | SLASH | | 94 | NUMPERIOD | 556 + +----+------------+ +----+------------+ 557 + ``` 558 + 559 + ## Startup options 560 + ``` 561 + --skip skip startup animation 562 + --volume=<int> global volume value [0-15] 563 + --cli console only output 564 + --fullscreen enable fullscreen mode 565 + --vsync enable VSYNC 566 + --soft use software rendering 567 + --fs=<str> path to the file system folder 568 + --scale=<int> main window scale 569 + --cmd=<str> run commands in the console 570 + --keepcmd re-execute commands on every run 571 + --version print program version 572 + --crt enable CRT monitor effect 573 + ``` 574 + 575 + ## Terms of Use 576 + - All cartridges posted on the https://tic80.com website are the property of their authors. 577 + - Do not redistribute the cartridge without permission, directly from the author. 578 + - By uploading cartridges to the site, you grant Nesbox the right to freely use and distribute them. All other rights by default remain with the author. 579 + - Do not post material that violates copyright, obscenity or any other laws. 580 + - Nesbox reserves the right to remove or filter any material without prior notice. 581 + 582 + ## Privacy Policy 583 + We store only the user's email and password in encrypted form and will not transfer any personal information to third parties without explicit permission. 584 + 585 + ## MIT License 586 + 587 + Copyright (c) 2017-2023 Vadim Grigoruk @nesbox // grigoruk@gmail.com 588 + 589 + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 590 + 591 + THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+9
RUST_REWRITE.md
··· 1 + # TIC-80 Rust Rewrite — Plan Location Moved 2 + 3 + This plan now lives at `docs/roadmap/overview.md`. 4 + 5 + Quick links: 6 + - Roadmap: `docs/roadmap/overview.md` 7 + - GUI-first milestones: `docs/roadmap/gui_first.md` 8 + - Docs index: `docs/README.md` 9 +
+30
docs/README.md
··· 1 + # TIC-80 Rust Rewrite — Docs Index 2 + 3 + This folder organizes the rewrite plan, specs, architecture notes, testing strategy, and decisions. `AGENTS.md` at the repo root tracks current work and links here. 4 + 5 + ## Roadmap 6 + - `docs/roadmap/overview.md`: High-level phased roadmap and goals (moved from RUST_REWRITE.md). 7 + - `docs/roadmap/gui_first.md`: Combined GUI-first kickoff + milestones for `winit + pixels` and `cls/pix`. 8 + 9 + ## Specs 10 + - `docs/specs/memory_map.md`: Canonical pointer to the root `MEMORY_MAP.md` and usage notes. 11 + - `docs/specs/lua_api_parity.md`: API parity checklist for Lua (name, signature, side effects). 12 + - `docs/specs/graphics.md`: Framebuffer, palette mapping, text/print semantics (stub to be expanded). 13 + - `docs/specs/audio_fft_vqt.md`: FFT/VQT behavior and parameters (points to `CLAUDE.md`). 14 + 15 + ## Architecture 16 + - `docs/architecture/workspace.md`: Crate layout and module boundaries. 17 + - `docs/architecture/runtime.md`: Fixed-step loop, callbacks, and presentation responsibilities. 18 + 19 + ## Testing 20 + - `docs/testing/strategy.md`: Testing and validation strategy across API/VRAM/audio. 21 + - `docs/testing/frame_hashes.md`: Conventions for deterministic frame/audio hashing (stub). 22 + 23 + ## Decisions (ADR) 24 + - `docs/adr/0001-winit-pixels.md`: Windowing/presentation stack decision. 25 + - `docs/adr/0002-mlua-lua54-compat.md`: Lua engine choice and compatibility stance. 26 + 27 + Notes 28 + - `MEMORY_MAP.md` at repo root remains the canonical reference for layout. Specs here link to it rather than duplicating. 29 + - `CLAUDE.md` remains at repo root; the spec page links to it for detailed FFT/VQT behavior. 30 +
+15
docs/adr/0001-winit-pixels.md
··· 1 + # ADR 0001: Windowing/Presentation Stack 2 + 3 + Status: Accepted 4 + Date: 2025-08-26 5 + 6 + Context 7 + - We need a portable, Rust-native way to open a window and present a 240×136 framebuffer with predictable scaling. 8 + 9 + Decision 10 + - Use `winit` for window/events and `pixels` for presenting an RGBA buffer (wgpu-backed) with integer nearest-neighbor scaling. 11 + 12 + Consequences 13 + - Desktop-first with a path to Web via wgpu. No SDL runtime dependency. 14 + - Keep an optional SDL2 backend as a future alternative if needed. 15 +
+15
docs/adr/0002-mlua-lua54-compat.md
··· 1 + # ADR 0002: Lua Engine and Compatibility 2 + 3 + Status: Accepted 4 + Date: 2025-08-26 5 + 6 + Context 7 + - TIC-80 carts rely on Lua semantics aligned with 5.2 behavior; portability and deterministic behavior are priorities. 8 + 9 + Decision 10 + - Use `mlua` with vendored Lua 5.4, enabling behavior that preserves expected 5.2 semantics where applicable (see tests). 11 + 12 + Consequences 13 + - Portable across supported platforms; avoids LuaJIT portability trade-offs initially. 14 + - Add tests around numeric semantics and iteration order to guard against drift. 15 +
+14
docs/architecture/runtime.md
··· 1 + # Runtime Architecture 2 + 3 + Fixed-Step Loop 4 + - 60 FPS logical tick; present cadence may be vsync-locked or decoupled. 5 + - Each tick: process input → run callbacks → render → present. 6 + 7 + Callbacks 8 + - `BOOT()` once after cart load; `TIC()` each frame; `SCN(row)`/`BDR(row)` during draw (later). 9 + - Error policy: trace and stop or bubble to host; define consistent behavior per phase. 10 + 11 + Presentation 12 + - CPU-owned framebuffer as palette indices; `blit_to_rgba` maps to RGBA for window texture. 13 + - Integer scaling to maintain crisp pixels. 14 +
+18
docs/architecture/workspace.md
··· 1 + # Workspace Architecture 2 + 3 + Crates (target layout) 4 + - `tic-core`: VM state, memory map, cart model, fixed-step ticker. 5 + - `tic-gfx`: CPU rasterizer (`pix/line/rect/...`), VRAM/VRAM banks, palette ops. 6 + - `tic-audio`: PSG synth + mixer; later capture buffer for FX. 7 + - `tic-io`: Input abstraction (kbd/mouse/gamepad), FS/cart IO, time. 8 + - `tic-api`: Language-agnostic API facade matching TIC-80 surface. 9 + - `tic-lua`: `mlua` embedding and Lua shims over `tic-api`. 10 + - `tic-fx`: FFT/VQT analysis per CLAUDE.md. 11 + - `tic-winit` (or `tic-sdl`): Platform layer for window/audio/input. 12 + - `tic-runner`: CLI binary to run carts headless or with window. 13 + 14 + Data Flow 15 + - Lua (`tic-lua`) calls into `tic-api` → forwards to `tic-core/gfx/audio/io`. 16 + - `tic-gfx` writes to VRAM page(s); presenter converts palette indices to RGBA for display. 17 + - `tic-audio` produces sample blocks; optional capture ring shared with `tic-fx`. 18 +
+38
docs/roadmap/gui_first.md
··· 1 + # GUI-First Track: Window + cls/pix and Lua Wiring 2 + 3 + Objective 4 + - Open a window (winit + pixels), present a 240×136 framebuffer with integer scaling, and exercise `cls/pix` end-to-end, then wire Lua callbacks. 5 + 6 + Decisions 7 + - Platform: `winit + pixels` (desktop first, portable to Web). 8 + - Input/Audio (later): `winit` (kbd/mouse), `gilrs` (gamepad), `cpal` (audio). 9 + - Scaling: integer nearest-neighbor (2x/3x/4x). Default 3x. Preserve aspect. 10 + - Pixel format: u8 palette indices → RGBA via 16-color palette from memory map. 11 + 12 + Milestone 1 — GUI + cls/pix (no Lua) 13 + - Deliverables 14 + - Presenter renders a CPU-owned 240×136 index buffer via `pixels` with NN scaling. 15 + - Minimal gfx spec for `cls/pix` and palette mapping. 16 + - Verification: unscaled buffer hashes + micro-demos (alternating `cls`, crosshair via `pix`, palette grid). 17 + - Tasks 18 + - Finalize FB spec and present path; set vsync policy. 19 + - Implement `cls/pix` and buffer→RGBA blit; add unit tests. 20 + - Write hash fixtures and demo scripts (host-driven). 21 + 22 + Milestone 2 — Lua + cls/pix 23 + - Deliverables 24 + - `mlua` runner: BOOT once, TIC per frame; expose `cls/pix`. 25 + - Error policy for unimplemented APIs; argument handling for `pix` read/write modes. 26 + - Verification: deterministic script exercising `cls/pix` yields known hashes. 27 + - Tasks 28 + - Bind `cls/pix`; confirm numeric conversions and return values match TIC-80. 29 + - Add tests for callback flow and pixel state. 30 + 31 + Risks & Mitigations 32 + - Color differences: use exact sRGB bytes; disable filtering. 33 + - Timing jitter: fixed 60 FPS tick; decouple present cadence if needed. 34 + - Lua numeric semantics: be explicit about integer vs float at the boundary. 35 + 36 + Next 37 + - Extend primitives (`print`, `line`, `rect/rectb`), then `spr/map`, `clip`, and `vbank`. 38 +
+191
docs/roadmap/overview.md
··· 1 + # TIC-80 Rust Rewrite Plan 2 + 3 + This document outlines a pragmatic, staged plan to reimplement TIC-80 in Rust. It focuses on maintaining cartridge/API compatibility, achieving deterministic behavior, and enabling incremental delivery with measurable checkpoints. It also details an initial track that targets Lua-only support first, then broadens to full parity. 4 + 5 + ## Goals and Scope 6 + 7 + - Compatibility: Run existing `.tic` cartridges unmodified; preserve memory map, API semantics, timing, and constraints (240×136, 16-color palette, sprites, map, 4 channels, etc.). 8 + - Determinism: Per-frame determinism for a given seed/input across platforms. 9 + - Performance: Match or exceed C implementation at 60 FPS on desktop targets. 10 + - Portability: Desktop first (macOS, Linux, Windows), then WebAssembly; keep console ports out-of-scope initially. 11 + - Phased Delivery: Start with core runtime + Lua API, defer Studio editors. 12 + 13 + ## Non-Goals (Initial Phases) 14 + 15 + - Rewriting the Studio editors (code/sprite/map/music/sfx UIs) in Phase 1–4. 16 + - Supporting all languages up front; we start with Lua-only to validate the Rust core. 17 + - Feature-creep beyond documented TIC-80 behavior unless explicitly called out as optional. 18 + 19 + ## Reference Material (from this repo) 20 + 21 + - Architecture: `src/core/` (runtime, draw, sound, io), `src/api/` (language bindings), `src/studio/` (editors), `src/system/` (SDL/libretro/n3ds), `src/cart.c` (cartridge), `src/tic.[ch]`, `src/api/lua*.c`. 22 + - Audio analysis: FFT/VQT covered in CLAUDE.md with precise specs and behavior. 23 + - Build/deps: `CMakeLists.txt`, `cmake/*.cmake` (Lua vendored, SDL, zlib, etc.). 24 + 25 + ## Architectural Decomposition (Rust) 26 + 27 + Crates (workspace): 28 + - `tic-core`: Core VM state, memory map, timing, resource management, cartridge model, save states. 29 + - `tic-gfx`: CPU rasterizer for all primitives (`pix`, `line`, `circ`, `rect`, `spr`, `map`, `clip`, palette ops), VRAM, and draw modes. 30 + - `tic-audio`: PSG-style synth + mixer matching `src/core/sound.c`; later add capture for FFT/VQT. 31 + - `tic-io`: Input abstraction (keyboard/mouse/gamepad), filesystem/cart IO, clipboard, time. 32 + - `tic-api`: Language-agnostic API facade mirroring `api.h` semantics against `tic-core/gfx/audio/io`. 33 + - `tic-lua`: Lua runtime embedding and API shims that expose `tic-api` to Lua scripts. 34 + - `tic-fx`: FFT/VQT analysis (post-capture) with smoothing, peak normalization, whitening. 35 + - `tic-sdl` (or `tic-winit`): Platform layer for windowing, GL or GPU context, audio IO, input events. 36 + - `tic-runner`: CLI binary that loads `.tic`, runs headless or with window; no Studio. 37 + - `tic-studio` (later): Editor UI reimplementation (e.g., egui or immediate-mode toolkit) when core stabilizes. 38 + 39 + Rationale: keep concerns isolated, enable headless testing and cross-checking per crate. 40 + 41 + ## Key Compatibility Requirements 42 + 43 + - Cartridge Format: Load/save `.tic` exactly (including zlib packing). Preserve bank semantics and PRO extensions (only when targeted). 44 + - Memory Map: Mirror addresses, sizes, and behavior (RAM/VRAM/persistent). Do not expose Rust internals to scripts. 45 + - API Semantics: Preserve function names, parameters, return values, side effects, and error behavior. Lua callback flow (`TIC()`, `BOOT()`, `SCN()`, `BDR()`, `MENU()`); outline parsing not required initially. 46 + - Timing: 60 FPS tick by default; menu/console interactions deferred until Studio phase; ensure fixed-step logic to match C. 47 + - FFT/VQT: Reproduce CLAUDE.md specs (buffer sizes, smoothing factors, auto-gain, whitening parameters) for visual parity. 48 + 49 + ## Cross-Cutting Concerns and Decisions 50 + 51 + - Lua engine: Use `mlua` with bundled Lua 5.4 and `LUA_COMPAT_5_2` equivalent behavior to match current build (`LUA_COMPAT_5_2` is defined in cmake). Avoid LuaJIT for portability; revisit later for perf. 52 + - Rendering backend: Start with software rasterizer in `tic-gfx` (deterministic, headless testable). Add a thin SDL2 or winit+pixels presentation layer later. Optional `wgpu` in a later phase. 53 + - Audio IO: Use `cpal` for capture/output. Keep synth/tick deterministic in `tic-audio`. For capture, implement a lock-free ring buffer shared with `tic-fx`. 54 + - Compression: Use `flate2`/`miniz_oxide` for zlib compatible packing/unpacking of carts. 55 + - FFT: Use `rustfft` for 2k (FFT) and 8k (VQT path) with exact binning and smoothing behavior; implement variable-Q kernel generation per CLAUDE.md. 56 + - Testing: Frame-hash snapshots for VRAM, audio block-level comparisons, API-level golden tests. Conformance carts from `demos/`. 57 + 58 + ## Phased Roadmap 59 + 60 + Phase 0 — Discovery and Spec (no code, docs + harness prep) 61 + - Inventory `api.h` and Lua binding coverage in `src/api/luaapi.c` and `src/api/lua.c`. 62 + - Write API parity checklist (functions, signatures, side effects, edge cases). Annotate open questions. 63 + - Map memory layout and constraints from `src/tic.h`, `src/core/*`, and CLAUDE.md into a concise spec. 64 + - Identify platform dependencies to replace (SDL, zlib, audio) and select Rust crates. 65 + - Output: Design doc + checklists; crate layout proposal; risk register. 66 + 67 + Phase 1 — Cartridge + Core State (headless) 68 + - Implement `.tic` parsing/loading/saving; banks, metadata, palettes, sprites, map, code segment(s). 69 + - Define `tic-core` state, memory map, and fixed-step ticker. No rendering/audio yet. 70 + - Add basic CLI to inspect carts and dump metadata (headless validation). 71 + - Output: Can load and introspect `.tic`; deterministic tick progression without side effects. 72 + 73 + Phase 2 — Lua Runtime + API Shim (headless) 74 + - Embed Lua; load script from cart; call `BOOT()` then per-frame `TIC()`. 75 + - Implement a minimal subset of API in `tic-api` with in-memory effects only: `time()`, `peek/poke` (scoped), `trace`, seed/rng. 76 + - Add harness to run demo carts in headless mode and record API calls. 77 + - Output: Runs simple scripts that don’t draw or play sound; deterministic logs. 78 + 79 + Phase 3 — Graphics Primitives (software) 80 + - Implement full raster pipeline in `tic-gfx` (CPU): palette, clip, `pix/line/circ/rect/tri`, text (`print`), sprites (`spr`), map (`map`), blitting rules. 81 + - Hook `tic-api` drawing calls to `tic-gfx`; expose a window via `tic-sdl`/`tic-winit` for presentation only. 82 + - Add VRAM frame hashing and image snapshots; compare against reference C build where feasible. 83 + - Output: Visual carts render correctly; headless tests produce stable hashes. 84 + 85 + Phase 4 — Input + Timing 86 + - Keyboard/mouse/gamepad in `tic-io`; map to TIC-80 semantics; ensure edge cases (press/repeat/up/down). 87 + - Verify fixed-step timing at 60 FPS, decoupling present frequency if needed. 88 + - Output: Interactive carts playable; deterministic input handling under recorded streams. 89 + 90 + Phase 5 — Audio Synth + Mixing 91 + - Reimplement `src/core/sound.c` behavior: waveforms, SFX, music tracker playback, mixing; ensure bit-exact or perceptually equivalent output. 92 + - Add audio output via `cpal`; implement recordable audio blocks for tests. 93 + - Output: Audio carts sound correct; block hashes/stats match baselines. 94 + 95 + Phase 6 — FFT/VQT Feature Parity 96 + - Implement shared capture buffer; 2048-sample FFT (21 fps) and 8192-sample VQT path per CLAUDE.md. 97 + - Implement smoothing, peak normalization, whitening with configurable macros equivalent. 98 + - Expose APIs: `fft/ffts/fftr/fftrs/vqt/vqts/vqtr/vqtrs/vqtw/vqtsw/vqtrw/vqtrsw`. 99 + - Output: Visual analyzers from `demos/` behave identically. 100 + 101 + Phase 7 — Studio (Optional, staged) 102 + - Recreate Studio UI gradually (console, code, sprite, map, sfx, music) using immediate-mode UI (e.g., egui) or SDL-rendered IMGUI-like. 103 + - Defer complex UX to later; keep feature set close to original. 104 + - Output: Usable integrated editor after core stabilizes. 105 + 106 + Phase 8 — WebAssembly + Additional Platforms 107 + - WASM target (via `wasm32-unknown-unknown` + `wasm-bindgen`/`wasm32-wasi`), web audio, canvas; sandboxed storage. 108 + - Optional mobile ports later. 109 + 110 + ## Testing and Validation Strategy 111 + 112 + - API Goldens: Unit tests per API call against known inputs (including edge cases and errors). 113 + - Frame Hashes: Hash VRAM after each frame to produce stable signatures for demo carts. 114 + - Audio Blocks: Hash mixed audio blocks; tolerance windows for FP differences. 115 + - Conformance Carts: Automate running `demos/` carts headless, comparing output to reference traces. 116 + - Differential Testing: Optional—run identical carts on original C build and Rust build; compare frame hashes and key metrics. 117 + - Fuzzing: Fuzz `.tic` loader and selected APIs; validate against panics and UB. 118 + 119 + ## Risks and Mitigations 120 + 121 + - Lua Semantics Drift: Differences across 5.2/5.3/5.4. Mitigate by enabling compat flags and authoring tests around table iteration, integer/float behavior, coroutines. 122 + - Rendering Differences: Pixel-perfect behavior required. Begin with CPU rasterizer, codify exact blending rules. 123 + - Audio Timing: Latency/jitter from host APIs. Use ring buffers and fixed block sizes; decouple from visual ticks. 124 + - FFT/VQT Performance: 8k FFT + VQT kernel costs. Optimize with plan reuse, sparse kernels, and careful allocation. 125 + - WASM Constraints: No native threads; audio timing quirks. Defer WASM until desktop parity. 126 + 127 + ## Milestone Criteria (Per Phase) 128 + 129 + - Phase 1: `.tic` carts parse and round-trip; metadata and assets intact. 130 + - Phase 2: Headless Lua scripts run; `TIC()` tick loop stable; minimal API callable. 131 + - Phase 3: Visual primitives render; hashes match baselines for selected carts. 132 + - Phase 4: Input tests pass; deterministic playback under recorded inputs. 133 + - Phase 5: Audio tests pass; tracker playback validated. 134 + - Phase 6: FFT/VQT demo carts behavior matches; numeric metrics within tolerances. 135 + - Phase 7/8: Usability and platform acceptance criteria defined separately. 136 + 137 + ## Work Breakdown for Lua-Only Track (Fast Path) 138 + 139 + 1) Phase 0 deliverables (API checklist + confirm/align to MEMORY_MAP.md). 140 + 2) Cartridge loader + Lua script extraction only; ignore non-Lua carts initially. 141 + 3) Headless Lua VM with `BOOT/TIC` and a minimal `tic-api` surface (`time`, `trace`, RNG, `peek/poke` across documented regions). 142 + 4) Software `tic-gfx` with presentation via SDL/winit. 143 + 5) Input + audio in later increments. 144 + 145 + ## First Tangible Step 146 + 147 + Create an API Parity Inventory and align with MEMORY_MAP.md (no code). Concretely: 148 + - From `src/api.h`, `src/api/luaapi.c`, and `src/api/lua.c`, enumerate every API function exposed to Lua (name, signature, return, side effects, error cases, and which subsystem it touches). 149 + - Use `MEMORY_MAP.md` as the authoritative memory layout reference; where needed cross-check with `src/tic.h`/`src/core/*` for any inconsistencies. 150 + - Output one short document in the repo (`docs/specs/lua_api_parity.md`) plus a proposed Rust crate layout; reference `MEMORY_MAP.md` instead of duplicating it. 151 + - This gives a precise contract to implement and test against and avoids early rework. It also makes it straightforward to decide Lua compatibility settings in Rust (`LUA_COMPAT_5_2`). 152 + 153 + If you’d prefer a “code-adjacent” first step instead: scaffold the Rust workspace and empty crates with READMEs and CI that only builds the workspace (no runtime code), then add the two spec docs above before implementing anything. 154 + 155 + ## Open Questions to Resolve Early 156 + 157 + - Exact Lua version expectations for cartridges (5.2 compatibility features relied upon?). 158 + - Required bit-exactness for audio vs. perceptual equivalence. 159 + - Which demos form the conformance baseline set and acceptable numeric tolerances. 160 + - Preferred desktop windowing stack (SDL2 vs. winit) for long-term maintenance. 161 + 162 + ## Appendix: Mapping Guide (C → Rust) 163 + 164 + - `src/core/` → `tic-core` (state/tick), `tic-gfx` (draw), `tic-audio` (synth/mix), `tic-io` (io/input) 165 + - `src/api/` → `tic-api` (surface), `tic-lua` (language binding) 166 + - `src/system/sdl/` → `tic-sdl` (platform layer) or `tic-winit` 167 + - `src/cart.c`/`zip.c` → `tic-core` cart loader + `flate2`/`miniz_oxide` 168 + - FFT/VQT files per CLAUDE.md → `tic-fx` 169 + 170 + --- 171 + 172 + This plan aims to de-risk the rewrite by isolating subsystems, proving compatibility early with headless testing, and deferring UI complexities until the core is stable. 173 + 174 + ## GUI-First Kickoff (Fast Lane) 175 + 176 + If early GUI feedback is a priority, we can interleave a minimal presentation path to get a window plus `cls`/`pix` quickly while keeping scope small and deterministic: 177 + 178 + - Chosen platform layer: `winit` + `pixels` (pure Rust). Rationale: mature, cross-platform (desktop + WebAssembly via wgpu), simple pixel buffer presentation, no system SDL deps. We can keep an optional SDL2 backend later if needed. 179 + - Audio + input crates to pair: `cpal` (audio output/capture), `gilrs` (gamepad), `winit` (keyboard/mouse), `arboard` (clipboard) as needed later. 180 + - Framebuffer spec: 240×136 logical surface with 16-color palette; render to an RGBA buffer for the OS window using nearest-neighbor integer scaling (2x/3x/4x) and optional vsync. 181 + - Minimal VRAM: Implement only screen memory and palette mapping from MEMORY_MAP.md; defer palette map, border color, and vbank switching until later. 182 + - Earliest API subset: `cls(color)`, `pix(x,y[,color])`, and `print(...)` optional; expose to a stub demo runner (no Lua yet) to validate the raster pipeline. 183 + - Then wire Lua: Load Lua script, call `BOOT()`/`TIC()`, and plumb `cls`/`pix` through the Lua binding to the framebuffer. 184 + - Acceptance: Static test image hashes at 1x (unscaled), integer-scaling visual inspection at 3x, and a tiny script that alternates `cls()` colors and sets a few pixels. 185 + 186 + Suggested steps (no code yet): 187 + - Document the choice (SDL2 vs winit) and scaling strategy; define pixel format and palette conversion rules (index→RGBA via 16×RGB in MEMORY_MAP.md). 188 + - Specify the minimal structs for framebuffer and the two API calls, and the main loop responsibilities (tick at 60 FPS, present at vsync or unlocked). 189 + - Identify 2–3 micro-demos for visual verification (e.g., alternating `cls`, crosshair via `pix`, and a palette sweep). 190 + 191 + See also: `docs/roadmap/gui_first.md` for concrete milestone tasks and success criteria for GUI + cls/pix and Lua + cls/pix.
+6
docs/specs/audio_fft_vqt.md
··· 1 + # Audio Analysis (FFT/VQT) 2 + 3 + Detailed behavior, parameters, and equations live in `CLAUDE.md` at the repo root. 4 + 5 + This page will summarize API-facing behavior (bins, windows, smoothing, normalization, whitening) and cross-link into the canonical derivations in `CLAUDE.md`. 6 +
+19
docs/specs/graphics.md
··· 1 + # Graphics Spec (Stub) 2 + 3 + Scope 4 + - Framebuffer: 240×136, 8-bit palette indices (0..15) per pixel. 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`. 7 + 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. 12 + - `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. 15 + 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. 19 +
+195
docs/specs/lua_api_parity.md
··· 1 + # TIC-80 Lua API Parity Checklist 2 + 3 + Authoritative references: MEMORY_MAP.md (root), src/api.h (TIC_API_LIST), src/api/luaapi.c. 4 + Goal: ensure Rust implementation preserves names, signatures, return types, side effects, and timing. 5 + 6 + For each API, we list the expected signature (per spec), key side effects, and the Rust subsystem owner. 7 + 8 + ## Callbacks 9 + 10 + - TIC: `TIC()` 11 + - Effect: Per-frame tick at 60 FPS. 12 + - Subsystem: tic-core (scheduler), tic-lua (callback wiring). 13 + - BOOT: `BOOT` 14 + - Effect: One-time init after loading cart before first TIC. 15 + - Subsystem: tic-core, tic-lua. 16 + - SCN: `SCN(row)` 17 + - Effect: Scanline callback during draw; palette tricks. 18 + - Subsystem: tic-gfx (scanline timing), tic-lua. 19 + - BDR: `BDR(row)` 20 + - Effect: Border scanline callback; palette tricks. 21 + - Subsystem: tic-gfx, tic-lua. 22 + - MENU: `MENU(index)` 23 + - Effect: Game menu handler. 24 + - Subsystem: tic-core (menu routing), tic-lua. 25 + 26 + ## Drawing 27 + 28 + - print: `print(text x=0 y=0 color=15 fixed=false scale=1 smallfont=false) -> width` 29 + - Effect: Draw text; returns width. 30 + - Subsystem: tic-gfx. 31 + - cls: `cls(color=0)` 32 + - Effect: Clear screen to color. 33 + - Subsystem: tic-gfx. 34 + - pix: `pix(x y color)` / `pix(x y) -> color` 35 + - Effect: Read or write pixel. 36 + - Subsystem: tic-gfx (VRAM). 37 + - line: `line(x0 y0 x1 y1 color)` 38 + - Effect: Draw line. 39 + - Subsystem: tic-gfx. 40 + - rect: `rect(x y w h color)` 41 + - Effect: Filled rectangle. 42 + - Subsystem: tic-gfx. 43 + - rectb: `rectb(x y w h color)` 44 + - Effect: Rectangle border. 45 + - Subsystem: tic-gfx. 46 + - circ: `circ(x y radius color)` 47 + - Effect: Filled circle. 48 + - Subsystem: tic-gfx. 49 + - circb: `circb(x y radius color)` 50 + - Effect: Circle border. 51 + - Subsystem: tic-gfx. 52 + - elli: `elli(x y a b color)` 53 + - Effect: Filled ellipse. 54 + - Subsystem: tic-gfx. 55 + - ellib: `ellib(x y a b color)` 56 + - Effect: Ellipse border. 57 + - Subsystem: tic-gfx. 58 + - tri: `tri(x1 y1 x2 y2 x3 y3 color)` 59 + - Effect: Filled triangle. 60 + - Subsystem: tic-gfx. 61 + - trib: `trib(x1 y1 x2 y2 x3 y3 color)` 62 + - Effect: Triangle border. 63 + - Subsystem: tic-gfx. 64 + - ttri: `ttri(x1 y1 x2 y2 x3 y3 u1 v1 u2 v2 u3 v3 use_map=false chroma=nil z1=0 z2=0 z3=0)` 65 + - Effect: Textured triangle (tiles or explicit texture), optional chroma table, optional map source. 66 + - Subsystem: tic-gfx (texturing), tic-core (remap helper). 67 + - paint: `paint(x y color [bordercolor])` 68 + - Effect: Flood fill (with optional border color). 69 + - Subsystem: tic-gfx. 70 + - clip: `clip(x y w h)` and `clip()` 71 + - Effect: Set/reset clipping rectangle. 72 + - Subsystem: tic-gfx. 73 + - spr: `spr(id x y colorkey=-1 scale=1 flip=0 rotate=0 w=1 h=1)` 74 + - Effect: Draw sprite or composite sprite region; supports flip/rotate/scale; optional colorkey. 75 + - Subsystem: tic-gfx (sprite blitter), respects palette map and vbank. 76 + - map: `map(x=0 y=0 w=30 h=17 sx=0 sy=0 colorkey=-1 scale=1 remap=nil)` 77 + - Effect: Draw map region to screen with optional remap callback. 78 + - Subsystem: tic-gfx; remap callback via tic-lua. 79 + - font: `font(text x y chromakey char_w char_h fixed=false scale=1 alt=false) -> width` 80 + - Effect: Draw text using font region in RAM; returns width. 81 + - Subsystem: tic-gfx. 82 + 83 + ## Tilemap Access 84 + 85 + - mget: `mget(x y) -> tile_id` 86 + - Effect: Read tile id at map cell (x,y). 87 + - Subsystem: tic-gfx (map RAM view), tic-core. 88 + - mset: `mset(x y tile_id)` 89 + - Effect: Write tile id at map cell (x,y); persistent only after `sync()`. 90 + - Subsystem: tic-gfx (map RAM view), tic-core. 91 + 92 + ## Input 93 + 94 + - btn: `btn(id) -> pressed` 95 + - Effect: Gamepad button state (held). 96 + - Subsystem: tic-io (gamepad). 97 + - btnp: `btnp(id hold=-1 period=-1) -> pressed` 98 + - Effect: Gamepad pressed/auto-repeat. 99 + - Subsystem: tic-io (edge detection + repeat timing). 100 + - key: `key(code=-1) -> pressed` 101 + - Effect: Keyboard key state (held); `-1` checks any. 102 + - Subsystem: tic-io (keyboard). 103 + - keyp: `keyp(code=-1 hold=-1 period=-1) -> pressed` 104 + - Effect: Keyboard pressed/auto-repeat. 105 + - Subsystem: tic-io. 106 + - mouse: `mouse() -> x y left middle right scrollx scrolly` 107 + - Effect: Mouse state. 108 + - Subsystem: tic-io (mouse). 109 + 110 + ## Memory and Banks 111 + 112 + - peek: `peek(addr bits=8) -> value` 113 + - poke: `poke(addr value bits=8)` 114 + - peek1/peek2/peek4: `peek1(addr)`, `peek2(addr)`, `peek4(addr)` 115 + - poke1/poke2/poke4: `poke1(addr value)`, `poke2(addr value)`, `poke4(addr value)` 116 + - Effect: Read/write RAM/VRAM according to MEMORY_MAP.md; 1/2/4-bit and nibble addressing semantics preserved. 117 + - Subsystem: tic-core (memory), tic-gfx (VRAM effects). 118 + - memcpy: `memcpy(dest source size)` 119 + - memset: `memset(dest value size)` 120 + - Effect: Raw memory block ops across 96KB RAM. 121 + - Subsystem: tic-core. 122 + - vbank: `vbank(bank) -> prev` or `vbank() -> prev` 123 + - Effect: Switch active 16KB VRAM bank (0 or 1); returns previous bank. 124 + - Subsystem: tic-gfx (VRAM pages), tic-core (state). 125 + - sync: `sync(mask=0 bank=0 tocart=false)` 126 + - Effect: Copy between cart banks and runtime; respects mask (tiles, sprites, map, sfx, music, palette, flags, screen). 127 + - Subsystem: tic-core (cart/runtime memory), tic-io (persistence). 128 + - pmem: `pmem(index value)` / `pmem(index) -> value` 129 + - Effect: Read/write 256×u32 persistent slots; cart-hash keyed. 130 + - Subsystem: tic-core (persistent store). 131 + 132 + ## Text/Console and System 133 + 134 + - trace: `trace(message color=15)` 135 + - Effect: Print to console (not screen) in color. 136 + - Subsystem: tic-core (logger). 137 + - time: `time() -> ticks` 138 + - Effect: Milliseconds since cart start (double); used for animation/timing. 139 + - Subsystem: tic-core (timer). 140 + - tstamp: `tstamp() -> timestamp` 141 + - Effect: Seconds since Unix epoch. 142 + - Subsystem: tic-core (system clock proxy). 143 + - exit: `exit()` 144 + - Effect: Return to console when TIC ends. 145 + - Subsystem: tic-core (control flow). 146 + - reset: `reset()` 147 + - Effect: Reset cart runtime (not process exit). 148 + - Subsystem: tic-core. 149 + 150 + ## Audio 151 + 152 + - sfx: `sfx(id note=-1 duration=-1 channel=0 volume=15 speed=0)` 153 + - Effect: Play/stop SFX on channel; supports id -1 to stop; note as int or "C#-4" style. 154 + - Subsystem: tic-audio (synth/mixer). 155 + - music: `music(track=-1 frame=-1 row=-1 loop=true sustain=false tempo=-1 speed=-1)` 156 + - Effect: Start/stop music playback; -1 to stop; supports overrides. 157 + - Subsystem: tic-audio (tracker). 158 + 159 + ## Analysis (FFT / VQT) 160 + 161 + - fft: `fft(start_freq end_freq=-1)` 162 + - ffts: `ffts(start_freq end_freq=-1)` 163 + - fftr: `fftr(start_freq end_freq=-1)` 164 + - fftrs: `fftrs(start_freq end_freq=-1)` 165 + - Effect: FFT magnitude queries (peak-normalized vs raw; smoothed vs raw) per CLAUDE.md; 1024 bins, 2,048-sample window ~21 FPS. 166 + - Subsystem: tic-fx (FFT), tic-audio (capture buffer). 167 + - vqt: `vqt(bin)` / vqts/vqtr/vqtrs/vqtw/vqtsw/vqtrw/vqtrsw 168 + - Effect: VQT magnitude queries (normalized/raw; smoothed/raw; whitened/non); 120 bins, 8,192-sample window ~5.4 FPS. 169 + - Subsystem: tic-fx (VQT), tic-audio (capture buffer). 170 + 171 + ## Sprite Flags 172 + 173 + - fget: `fget(sprite_id flag) -> bool` 174 + - fset: `fset(sprite_id flag bool)` 175 + - Effect: Per-sprite flag bits 0..7. 176 + - Subsystem: tic-gfx (sprite metadata in RAM), tic-core. 177 + 178 + ## Input Mapping and Menu 179 + 180 + - MENU(index): see Callbacks 181 + - Effect: Handle game menu actions. 182 + - Subsystem: tic-core (menu controller). 183 + 184 + --- 185 + 186 + Verification plan per API: 187 + - Signature/arity: Match src/api.h docs and Lua binding arity checks. 188 + - Return types: Match numeric vs boolean vs string; e.g., time() returns double. 189 + - Side effects: Test VRAM/memory deltas, audio start/stop, persistent pmem behavior. 190 + - Timing: Respect fixed step; SCN/BDR callbacks invoked with correct rows. 191 + - Memory map: All peek/poke/mem ops constrained to MEMORY_MAP.md ranges; vbank switching affects VRAM offsets. 192 + 193 + Notes: 194 + - Deprecated: `textri` exists under BUILD_DEPRECATED; keep optional compat layer if needed. 195 + - Ensure `LUA_COMPAT_5_2`-equivalent behavior to match current build semantics for Lua.
+8
docs/specs/memory_map.md
··· 1 + # Memory Map (Canonical Reference) 2 + 3 + Canonical source: `MEMORY_MAP.md` at the repository root. This page exists only to link to it and add short guidance. 4 + 5 + Notes 6 + - Tests and specs must reference the root `MEMORY_MAP.md` to avoid drift. 7 + - When documenting APIs that read/write memory (peek/poke/memcpy/memset/vbank/sync), link to the specific sections in `MEMORY_MAP.md`. 8 +
+9
docs/testing/frame_hashes.md
··· 1 + # Frame Hashes (Stub) 2 + 3 + Purpose 4 + - Provide deterministic signatures of VRAM and audio output for regression tests. 5 + 6 + Conventions (to define) 7 + - VRAM: byte order, region included (full 240×136 vs. viewport), palette influence. 8 + - Audio: block size, windowing, channel mix policy, endian/format. 9 +
+13
docs/testing/strategy.md
··· 1 + # Testing and Validation Strategy 2 + 3 + Layers 4 + - API goldens: Unit tests per API including edge cases and errors. 5 + - Frame hashes: Deterministic VRAM hashes per frame to compare against baselines. 6 + - Audio blocks: Hash mixed audio buffers with tolerances for FP differences. 7 + - Conformance carts: Automated headless runs comparing traces/hashes to reference. 8 + - Fuzzing: `.tic` loader and selected APIs for robustness. 9 + 10 + Notes 11 + - Prefer headless tests with minimal dependencies. 12 + - Keep baselines small and documented; record how they are generated. 13 +
+1
tic80_rust/assets/fonts/default_font.inl
··· 1 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0c, 0x0c, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x1f, 0x0a, 0x1f, 0x0a, 0x00, 0x00, 0x00, 0x1e, 0x05, 0x0e, 0x14, 0x0f, 0x00, 0x00, 0x00, 0x11, 0x08, 0x04, 0x02, 0x11, 0x00, 0x00, 0x00, 0x02, 0x05, 0x16, 0x09, 0x16, 0x00, 0x00, 0x00, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x04, 0x04, 0x04, 0x08, 0x00, 0x00, 0x00, 0x02, 0x04, 0x04, 0x04, 0x02, 0x00, 0x00, 0x00, 0x04, 0x15, 0x0e, 0x15, 0x04, 0x00, 0x00, 0x00, 0x00, 0x04, 0x0e, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x04, 0x02, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06, 0x00, 0x00, 0x00, 0x10, 0x08, 0x04, 0x02, 0x01, 0x00, 0x00, 0x00, 0x0e, 0x1b, 0x17, 0x13, 0x0e, 0x00, 0x00, 0x00, 0x0c, 0x0e, 0x0c, 0x0c, 0x1e, 0x00, 0x00, 0x00, 0x0f, 0x18, 0x0e, 0x03, 0x1f, 0x00, 0x00, 0x00, 0x1f, 0x18, 0x0c, 0x19, 0x0e, 0x00, 0x00, 0x00, 0x0c, 0x0e, 0x0b, 0x1f, 0x08, 0x00, 0x00, 0x00, 0x1f, 0x03, 0x0f, 0x18, 0x0f, 0x00, 0x00, 0x00, 0x0e, 0x03, 0x0f, 0x13, 0x0e, 0x00, 0x00, 0x00, 0x1f, 0x18, 0x0c, 0x06, 0x03, 0x00, 0x00, 0x00, 0x0e, 0x13, 0x0e, 0x13, 0x0e, 0x00, 0x00, 0x00, 0x0e, 0x13, 0x1e, 0x10, 0x0e, 0x00, 0x00, 0x00, 0x06, 0x06, 0x00, 0x06, 0x06, 0x00, 0x00, 0x00, 0x06, 0x06, 0x00, 0x06, 0x04, 0x02, 0x00, 0x00, 0x08, 0x04, 0x02, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x02, 0x04, 0x08, 0x04, 0x02, 0x00, 0x00, 0x00, 0x1e, 0x18, 0x0c, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x0e, 0x15, 0x1d, 0x01, 0x0e, 0x00, 0x00, 0x00, 0x0e, 0x13, 0x13, 0x1f, 0x13, 0x00, 0x00, 0x00, 0x0f, 0x13, 0x0f, 0x13, 0x0f, 0x00, 0x00, 0x00, 0x0e, 0x13, 0x03, 0x13, 0x0e, 0x00, 0x00, 0x00, 0x0f, 0x13, 0x13, 0x13, 0x0f, 0x00, 0x00, 0x00, 0x1f, 0x03, 0x0f, 0x03, 0x1f, 0x00, 0x00, 0x00, 0x1f, 0x03, 0x0f, 0x03, 0x03, 0x00, 0x00, 0x00, 0x1e, 0x03, 0x1b, 0x13, 0x1e, 0x00, 0x00, 0x00, 0x13, 0x13, 0x1f, 0x13, 0x13, 0x00, 0x00, 0x00, 0x1e, 0x0c, 0x0c, 0x0c, 0x1e, 0x00, 0x00, 0x00, 0x1f, 0x18, 0x18, 0x1b, 0x0e, 0x00, 0x00, 0x00, 0x13, 0x0b, 0x07, 0x0b, 0x13, 0x00, 0x00, 0x00, 0x03, 0x03, 0x03, 0x03, 0x1f, 0x00, 0x00, 0x00, 0x1b, 0x1f, 0x1f, 0x15, 0x11, 0x00, 0x00, 0x00, 0x13, 0x17, 0x1f, 0x1b, 0x13, 0x00, 0x00, 0x00, 0x0e, 0x13, 0x13, 0x13, 0x0e, 0x00, 0x00, 0x00, 0x0f, 0x13, 0x13, 0x0f, 0x03, 0x00, 0x00, 0x00, 0x0e, 0x13, 0x13, 0x13, 0x0e, 0x10, 0x00, 0x00, 0x0f, 0x13, 0x13, 0x0f, 0x13, 0x00, 0x00, 0x00, 0x1e, 0x07, 0x0e, 0x1c, 0x0f, 0x00, 0x00, 0x00, 0x1e, 0x0c, 0x0c, 0x0c, 0x0c, 0x00, 0x00, 0x00, 0x13, 0x13, 0x13, 0x13, 0x0e, 0x00, 0x00, 0x00, 0x13, 0x13, 0x13, 0x0e, 0x04, 0x00, 0x00, 0x00, 0x11, 0x15, 0x1f, 0x1f, 0x1b, 0x00, 0x00, 0x00, 0x13, 0x13, 0x0e, 0x13, 0x13, 0x00, 0x00, 0x00, 0x16, 0x16, 0x1e, 0x0c, 0x0c, 0x00, 0x00, 0x00, 0x1f, 0x0c, 0x06, 0x03, 0x1f, 0x00, 0x00, 0x00, 0x0c, 0x04, 0x04, 0x04, 0x0c, 0x00, 0x00, 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00, 0x00, 0x06, 0x04, 0x04, 0x04, 0x06, 0x00, 0x00, 0x00, 0x04, 0x0a, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x19, 0x19, 0x1e, 0x00, 0x00, 0x00, 0x03, 0x0f, 0x13, 0x13, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x07, 0x07, 0x1e, 0x00, 0x00, 0x00, 0x18, 0x1e, 0x19, 0x19, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x1b, 0x07, 0x0e, 0x00, 0x00, 0x00, 0x1c, 0x06, 0x1f, 0x06, 0x06, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x19, 0x1f, 0x18, 0x0e, 0x00, 0x00, 0x03, 0x0f, 0x13, 0x13, 0x13, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x0c, 0x0c, 0x0c, 0x00, 0x00, 0x00, 0x18, 0x00, 0x18, 0x18, 0x19, 0x0e, 0x00, 0x00, 0x03, 0x13, 0x0f, 0x13, 0x13, 0x00, 0x00, 0x00, 0x06, 0x06, 0x06, 0x06, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x1f, 0x15, 0x15, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x13, 0x13, 0x13, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x13, 0x13, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x13, 0x13, 0x0f, 0x03, 0x00, 0x00, 0x00, 0x1e, 0x19, 0x19, 0x1e, 0x18, 0x00, 0x00, 0x00, 0x0f, 0x13, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x07, 0x1c, 0x0f, 0x00, 0x00, 0x00, 0x06, 0x1f, 0x06, 0x06, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x13, 0x13, 0x13, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x13, 0x13, 0x0e, 0x04, 0x00, 0x00, 0x00, 0x00, 0x11, 0x15, 0x1f, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x0e, 0x0e, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x19, 0x19, 0x1e, 0x18, 0x0e, 0x00, 0x00, 0x00, 0x1f, 0x0c, 0x06, 0x1f, 0x00, 0x00, 0x00, 0x0c, 0x04, 0x06, 0x04, 0x0c, 0x00, 0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x06, 0x04, 0x0c, 0x04, 0x06, 0x00, 0x00, 0x00, 0x00, 0x14, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00,
+72 -52
tic80_rust/src/gfx/framebuffer.rs
··· 1 1 use std::sync::OnceLock; 2 2 3 3 // Default 16-color TIC-80 palette (sRGB) as RGBA8 4 - const PALETTE: [(u8, u8, u8, u8); 16] = [ 5 - (0x00, 0x00, 0x00, 0xFF), 6 - (0x1D, 0x2B, 0x53, 0xFF), 7 - (0x7E, 0x25, 0x53, 0xFF), 8 - (0x00, 0x87, 0x51, 0xFF), 9 - (0xAB, 0x52, 0x36, 0xFF), 10 - (0x5F, 0x57, 0x4F, 0xFF), 11 - (0xC2, 0xC3, 0xC7, 0xFF), 12 - (0xFF, 0xF1, 0xE8, 0xFF), 13 - (0xFF, 0x00, 0x4D, 0xFF), 14 - (0xFF, 0xA3, 0x00, 0xFF), 15 - (0xFF, 0xEC, 0x27, 0xFF), 16 - (0x00, 0xE4, 0x36, 0xFF), 17 - (0x29, 0xAD, 0xFF, 0xFF), 18 - (0x83, 0x76, 0x9C, 0xFF), 19 - (0xFF, 0x77, 0xA8, 0xFF), 20 - (0xFF, 0xCC, 0xAA, 0xFF), 4 + const PALETTE: [[u8; 4]; 16] = [ 5 + [0x00, 0x00, 0x00, 0xFF], 6 + [0x1D, 0x2B, 0x53, 0xFF], 7 + [0x7E, 0x25, 0x53, 0xFF], 8 + [0x00, 0x87, 0x51, 0xFF], 9 + [0xAB, 0x52, 0x36, 0xFF], 10 + [0x5F, 0x57, 0x4F, 0xFF], 11 + [0xC2, 0xC3, 0xC7, 0xFF], 12 + [0xFF, 0xF1, 0xE8, 0xFF], 13 + [0xFF, 0x00, 0x4D, 0xFF], 14 + [0xFF, 0xA3, 0x00, 0xFF], 15 + [0xFF, 0xEC, 0x27, 0xFF], 16 + [0x00, 0xE4, 0x36, 0xFF], 17 + [0x29, 0xAD, 0xFF, 0xFF], 18 + [0x83, 0x76, 0x9C, 0xFF], 19 + [0xFF, 0x77, 0xA8, 0xFF], 20 + [0xFF, 0xCC, 0xAA, 0xFF], 21 21 ]; 22 22 23 23 const WIDTH: u32 = 240; 24 24 const HEIGHT: u32 = 136; 25 25 26 - // TIC-80 default font bytes as 8 rows per glyph, 1 bit per pixel (LSB is x=0) 27 - static FONT_TEXT: &str = include_str!("../../../src/core/font.inl"); 26 + // TIC-80 default font bytes as 8 rows per glyph, 1 bit per pixel. 27 + // Interpret bits LSB→MSB as columns left→right (bit0 is x=0). 28 + static FONT_TEXT: &str = include_str!("../../assets/fonts/default_font.inl"); 28 29 static FONT_BYTES: OnceLock<Vec<u8>> = OnceLock::new(); 29 30 30 31 fn font_bytes() -> &'static [u8] { ··· 55 56 } 56 57 57 58 impl Framebuffer { 59 + pub const WIDTH: u32 = WIDTH; 60 + pub const HEIGHT: u32 = HEIGHT; 61 + 58 62 pub fn new() -> Self { 59 63 Self { 60 64 idx: vec![0; (WIDTH * HEIGHT) as usize], ··· 68 72 69 73 // pix(x,y[,color]): if Some(color) -> write; else -> read 70 74 pub fn pix(&mut self, x: i32, y: i32, color: Option<u8>) -> Option<u8> { 71 - if x < 0 || y < 0 || x as u32 >= WIDTH || y as u32 >= HEIGHT { 72 - return None; 73 - } 74 - let i = (y as u32 * WIDTH + x as u32) as usize; 75 75 match color { 76 76 Some(c) => { 77 - self.idx[i] = c & 0x0F; 77 + let _ = self.set_pixel(x, y, c); 78 78 None 79 79 } 80 - None => Some(self.idx[i] & 0x0F), 80 + None => { 81 + if x < 0 || y < 0 || x as u32 >= WIDTH || y as u32 >= HEIGHT { 82 + None 83 + } else { 84 + let i = (y as u32 * WIDTH + x as u32) as usize; 85 + Some(self.idx[i] & 0x0F) 86 + } 87 + } 81 88 } 82 89 } 83 90 91 + // Write a single pixel; returns true if in-bounds and written 92 + 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 { 94 + return false; 95 + } 96 + let i = (y as u32 * WIDTH + x as u32) as usize; 97 + self.idx[i] = color & 0x0F; 98 + true 99 + } 100 + 84 101 // Blit to RGBA buffer for pixels 85 102 pub fn blit_to_rgba(&self, rgba: &mut [u8]) { 86 - for (i, idx) in self.idx.iter().copied().enumerate() { 87 - let (r, g, b, a) = PALETTE[(idx & 0x0F) as usize]; 88 - let o = i * 4; 89 - rgba[o] = r; 90 - rgba[o + 1] = g; 91 - rgba[o + 2] = b; 92 - rgba[o + 3] = a; 103 + for (px, idx) in rgba 104 + .chunks_exact_mut(4) 105 + .zip(self.idx.iter().copied()) 106 + { 107 + let pal = &PALETTE[(idx & 0x0F) as usize]; 108 + px.copy_from_slice(pal); 93 109 } 94 110 } 95 111 ··· 104 120 let mut err = dx + dy; 105 121 let c = color & 0x0F; 106 122 loop { 107 - let _ = self.pix(x0, y0, Some(c)); 123 + let _ = self.set_pixel(x0, y0, c); 108 124 if x0 == x1 && y0 == y1 { 109 125 break; 110 126 } ··· 120 136 } 121 137 } 122 138 123 - // Filled rectangle 139 + // Filled rectangle (clipped to framebuffer bounds) 124 140 pub fn rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: u8) { 125 141 if w <= 0 || h <= 0 { 126 142 return; 127 143 } 128 144 let c = color & 0x0F; 129 - for yy in y..y + h { 130 - for xx in x..x + w { 131 - let _ = self.pix(xx, yy, Some(c)); 132 - } 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); 149 + if x1 <= x0 || y1 <= y0 { 150 + return; 151 + } 152 + let width = WIDTH as usize; 153 + for yy in y0..y1 { 154 + let base = (yy as usize) * width; 155 + let start = base + x0 as usize; 156 + let end = base + x1 as usize; 157 + self.idx[start..end].fill(c); 133 158 } 134 159 } 135 160 ··· 176 201 } 177 202 178 203 let (start_col, width_cols) = if !fixed { 204 + // Variable-width: trim empty columns using LSB-left orientation 179 205 let mut left = GLYPH_W; 180 206 let mut right = 0; 181 207 for row in 0..GLYPH_H { 182 208 let mask = font[base + row]; 183 209 if mask != 0 { 210 + // find first 1 from the left (LSB) 184 211 let mut l = 0; 185 - while l < GLYPH_W && ((mask >> l) & 1) == 0 { 186 - l += 1; 187 - } 212 + while l < GLYPH_W && ((mask >> l) & 1) == 0 { l += 1; } 213 + // find last 1 from the left (rightmost set bit + 1) 188 214 let mut r = GLYPH_W; 189 - while r > 0 && ((mask >> (r - 1)) & 1) == 0 { 190 - r -= 1; 191 - } 192 - if l < left { 193 - left = l; 194 - } 195 - if r > right { 196 - right = r; 197 - } 215 + while r > 0 && (((mask >> (r - 1)) & 1) == 0) { r -= 1; } 216 + if l < left { left = l; } 217 + if r > right { right = r; } 198 218 } 199 219 } 200 220 let width = right.saturating_sub(left); ··· 208 228 let mask = font[base + row]; 209 229 for col in 0..width_cols { 210 230 let bit_idx = start_col + col; 211 - if bit_idx < GLYPH_W && ((mask >> bit_idx) & 1) != 0 { 231 + if bit_idx < GLYPH_W && (((mask >> bit_idx) & 1) != 0) { 212 232 let px = pos + (col as i32) * scale; 213 233 let py = y + (row as i32) * scale; 214 234 for sy in 0..scale { ··· 241 261 } 242 262 243 263 pub fn dimensions() -> (u32, u32) { 244 - (WIDTH, HEIGHT) 264 + (Framebuffer::WIDTH, Framebuffer::HEIGHT) 245 265 }
+8
tic80_rust/src/lib.rs
··· 1 + pub mod gfx { 2 + pub mod framebuffer; 3 + } 4 + 5 + pub mod script { 6 + pub mod lua_runner; 7 + } 8 +
+2 -8
tic80_rust/src/main.rs
··· 8 8 use winit::event_loop::{ControlFlow, EventLoop}; 9 9 use winit::window::WindowBuilder; 10 10 11 - mod gfx { 12 - pub mod framebuffer; 13 - } 14 - mod script { 15 - pub mod lua_runner; 16 - } 17 - use gfx::framebuffer::{dimensions, Framebuffer}; 18 - use script::lua_runner::LuaRunner; 11 + use tic80_rust::gfx::framebuffer::{dimensions, Framebuffer}; 12 + use tic80_rust::script::lua_runner::LuaRunner; 19 13 20 14 // Simple fixed-step ticker at ~60 FPS 21 15 struct Ticker {
+34 -20
tic80_rust/src/script/lua_runner.rs
··· 54 54 globals.set("rect", rect_fn)?; 55 55 56 56 // print(text, x=0, y=0, color=15, fixed=false, scale=1, small=false) -> width 57 - let fb_print = fb.clone(); 58 - let print_fn = lua.create_function(move |_, args: MultiValue| { 59 - let mut text = String::new(); 60 - let mut x: i32 = 0; 61 - let mut y: i32 = 0; 62 - let mut color: u8 = 15; 63 - let mut fixed = false; 64 - let mut scale: i32 = 1; 65 - let mut small = false; 57 + #[derive(Default)] 58 + struct PrintArgs { 59 + text: String, 60 + x: i32, 61 + y: i32, 62 + color: u8, 63 + fixed: bool, 64 + scale: i32, 65 + small: bool, 66 + } 66 67 67 - for (i, v) in args.iter().enumerate() { 68 - match (i, v) { 69 - (0, Value::String(s)) => text = s.to_str()?.to_string(), 70 - (1, Value::Integer(n)) => x = *n as i32, 71 - (2, Value::Integer(n)) => y = *n as i32, 72 - (3, Value::Integer(n)) => color = (*n).clamp(0, 255) as u8, 73 - (4, Value::Boolean(b)) => fixed = *b, 74 - (5, Value::Integer(n)) => scale = (*n as i32).max(1), 75 - (6, Value::Boolean(b)) => small = *b, 76 - _ => {} 68 + impl PrintArgs { 69 + fn from_lua(args: &MultiValue) -> LuaResult<Self> { 70 + let mut out = PrintArgs { 71 + color: 15, 72 + scale: 1, 73 + ..Default::default() 74 + }; 75 + for (i, v) in args.iter().enumerate() { 76 + match (i, v) { 77 + (0, Value::String(s)) => out.text = s.to_str()?.to_string(), 78 + (1, Value::Integer(n)) => out.x = *n as i32, 79 + (2, Value::Integer(n)) => out.y = *n as i32, 80 + (3, Value::Integer(n)) => out.color = (*n).clamp(0, 255) as u8, 81 + (4, Value::Boolean(b)) => out.fixed = *b, 82 + (5, Value::Integer(n)) => out.scale = (*n as i32).max(1), 83 + (6, Value::Boolean(b)) => out.small = *b, 84 + _ => {} 85 + } 77 86 } 87 + Ok(out) 78 88 } 89 + } 79 90 91 + let fb_print = fb.clone(); 92 + let print_fn = lua.create_function(move |_, args: MultiValue| { 93 + let p = PrintArgs::from_lua(&args)?; 80 94 let width = fb_print 81 95 .borrow_mut() 82 - .print_text(&text, x, y, color, fixed, scale, small); 96 + .print_text(&p.text, p.x, p.y, p.color, p.fixed, p.scale, p.small); 83 97 Ok(width) 84 98 })?; 85 99 globals.set("print", print_fn)?;
+143
tic80_rust/tests/gfx_framebuffer_tests.rs
··· 1 + use tic80_rust::gfx::framebuffer::{dimensions, Framebuffer}; 2 + 3 + fn fb_all_pixels_equal(fb: &mut Framebuffer, expected: u8) -> bool { 4 + let (w, h) = dimensions(); 5 + for y in 0..(h as i32) { 6 + for x in 0..(w as i32) { 7 + if fb.pix(x, y, None) != Some(expected & 0x0F) { 8 + return false; 9 + } 10 + } 11 + } 12 + true 13 + } 14 + 15 + #[test] 16 + fn cls_fills_entire_buffer() { 17 + let mut fb = Framebuffer::new(); 18 + fb.cls(7); 19 + assert!(fb_all_pixels_equal(&mut fb, 7)); 20 + } 21 + 22 + #[test] 23 + fn pix_read_write_and_bounds() { 24 + let mut fb = Framebuffer::new(); 25 + // in-bounds set/read 26 + assert!(fb.set_pixel(0, 0, 0xFF)); // should mask to 0x0F 27 + assert_eq!(fb.pix(0, 0, None), Some(0x0F)); 28 + 29 + // out-of-bounds read 30 + assert_eq!(fb.pix(-1, 0, None), None); 31 + assert_eq!(fb.pix(0, -1, None), None); 32 + let (w, h) = dimensions(); 33 + assert_eq!(fb.pix(w as i32, 0, None), None); 34 + assert_eq!(fb.pix(0, h as i32, None), None); 35 + 36 + // out-of-bounds write via set_pixel reports false 37 + assert!(!fb.set_pixel(-1, 0, 1)); 38 + assert!(!fb.set_pixel(0, -1, 1)); 39 + assert!(!fb.set_pixel(w as i32, 0, 1)); 40 + assert!(!fb.set_pixel(0, h as i32, 1)); 41 + } 42 + 43 + #[test] 44 + fn rect_fill_and_clipping() { 45 + let mut fb = Framebuffer::new(); 46 + fb.cls(0); 47 + // A rect that partially lies outside should clip 48 + fb.rect(-5, -3, 10, 8, 9); 49 + // Count colored pixels; expected area is clipped to [0,w) x [0,h) 50 + let (w, h) = dimensions(); 51 + let x0 = 0i32.max(-5); 52 + let y0 = 0i32.max(-3); 53 + let x1 = ( -5 + 10).min(w as i32); 54 + let y1 = ( -3 + 8).min(h as i32); 55 + let expected = (x1 - x0).max(0) as usize * (y1 - y0).max(0) as usize; 56 + let mut count = 0usize; 57 + for y in 0..(h as i32) { 58 + for x in 0..(w as i32) { 59 + if fb.pix(x, y, None) == Some(9) { 60 + count += 1; 61 + } 62 + } 63 + } 64 + assert_eq!(count, expected); 65 + 66 + // Fully out-of-bounds should do nothing 67 + let mut fb2 = Framebuffer::new(); 68 + fb2.cls(2); 69 + fb2.rect(-1000, -1000, 10, 10, 5); 70 + assert!(fb_all_pixels_equal(&mut fb2, 2)); 71 + } 72 + 73 + fn count_color(fb: &mut Framebuffer, color: u8) -> usize { 74 + let (w, h) = dimensions(); 75 + let mut c = 0usize; 76 + for y in 0..(h as i32) { 77 + for x in 0..(w as i32) { 78 + if fb.pix(x, y, None) == Some(color & 0x0F) { 79 + c += 1; 80 + } 81 + } 82 + } 83 + c 84 + } 85 + 86 + #[test] 87 + fn line_basic_counts_and_endpoints() { 88 + // Horizontal line 89 + let mut hfb = Framebuffer::new(); 90 + hfb.cls(0); 91 + hfb.line(0, 0, 10, 0, 3); 92 + assert_eq!(hfb.pix(0, 0, None), Some(3)); 93 + assert_eq!(hfb.pix(10, 0, None), Some(3)); 94 + assert_eq!(count_color(&mut hfb, 3), 11); 95 + 96 + // Vertical line 97 + let mut vfb = Framebuffer::new(); 98 + vfb.cls(0); 99 + vfb.line(0, 0, 0, 7, 5); 100 + assert_eq!(vfb.pix(0, 0, None), Some(5)); 101 + assert_eq!(vfb.pix(0, 7, None), Some(5)); 102 + assert_eq!(count_color(&mut vfb, 5), 8); 103 + 104 + // Diagonal-ish line and reverse should have same count = max(dx,dy)+1 105 + let (x0, y0, x1, y1): (i32, i32, i32, i32) = (0, 0, 10, 7); 106 + let expected = (x1 - x0).abs().max((y1 - y0).abs()) + 1; 107 + 108 + let mut a = Framebuffer::new(); 109 + a.cls(0); 110 + a.line(x0, y0, x1, y1, 4); 111 + assert_eq!(a.pix(x0, y0, None), Some(4)); 112 + assert_eq!(a.pix(x1, y1, None), Some(4)); 113 + assert_eq!(count_color(&mut a, 4) as i32, expected); 114 + 115 + let mut b = Framebuffer::new(); 116 + b.cls(0); 117 + b.line(x1, y1, x0, y0, 4); 118 + assert_eq!(b.pix(x0, y0, None), Some(4)); 119 + assert_eq!(b.pix(x1, y1, None), Some(4)); 120 + assert_eq!(count_color(&mut b, 4) as i32, expected); 121 + } 122 + 123 + #[test] 124 + fn blit_to_rgba_maps_palette() { 125 + let mut fb = Framebuffer::new(); 126 + fb.cls(0); 127 + // set three sample pixels to known colors 128 + fb.set_pixel(0, 0, 0); // black 129 + fb.set_pixel(1, 0, 9); // orange 130 + fb.set_pixel(2, 0, 15); // peach 131 + 132 + let (w, h) = dimensions(); 133 + let mut rgba = vec![0u8; (w * h * 4) as usize]; 134 + fb.blit_to_rgba(&mut rgba); 135 + 136 + // Helpers to read RGBA at (x,y) 137 + let idx = |x: u32, y: u32| -> usize { ((y * w + x) * 4) as usize }; 138 + 139 + // Known palette entries from framebuffer.rs 140 + assert_eq!(&rgba[idx(0, 0)..idx(0, 0) + 4], &[0x00, 0x00, 0x00, 0xFF]); 141 + assert_eq!(&rgba[idx(1, 0)..idx(1, 0) + 4], &[0xFF, 0xA3, 0x00, 0xFF]); 142 + assert_eq!(&rgba[idx(2, 0)..idx(2, 0) + 4], &[0xFF, 0xCC, 0xAA, 0xFF]); 143 + }
+131
tic80_rust/tests/lua_api_tests.rs
··· 1 + use std::cell::RefCell; 2 + use std::rc::Rc; 3 + 4 + use tic80_rust::gfx::framebuffer::{dimensions, Framebuffer}; 5 + use tic80_rust::script::lua_runner::LuaRunner; 6 + 7 + fn run_lua(script: &str, ticks: usize) -> Rc<RefCell<Framebuffer>> { 8 + let fb = Rc::new(RefCell::new(Framebuffer::new())); 9 + let runner = LuaRunner::new(fb.clone(), script).expect("lua init"); 10 + for _ in 0..ticks { 11 + runner.tick(); 12 + } 13 + fb 14 + } 15 + 16 + #[test] 17 + fn lua_cls_and_pix() { 18 + let script = r#" 19 + function BOOT() 20 + cls(2) 21 + end 22 + function TIC() 23 + pix(1, 1, 9) 24 + end 25 + "#; 26 + let fb = run_lua(script, 1); 27 + assert_eq!(fb.borrow_mut().pix(1, 1, None), Some(9)); 28 + assert_eq!(fb.borrow_mut().pix(0, 0, None), Some(2)); 29 + } 30 + 31 + #[test] 32 + fn lua_line_and_rect() { 33 + let script = r#" 34 + function BOOT() 35 + cls(0) 36 + end 37 + function TIC() 38 + line(0, 0, 2, 0, 5) 39 + rect(0, 1, 3, 1, 6) 40 + end 41 + "#; 42 + let fb = run_lua(script, 1); 43 + let mut fbm = fb.borrow_mut(); 44 + // line 45 + assert_eq!(fbm.pix(0, 0, None), Some(5)); 46 + assert_eq!(fbm.pix(1, 0, None), Some(5)); 47 + assert_eq!(fbm.pix(2, 0, None), Some(5)); 48 + // rect one row below 49 + assert_eq!(fbm.pix(0, 1, None), Some(6)); 50 + assert_eq!(fbm.pix(1, 1, None), Some(6)); 51 + assert_eq!(fbm.pix(2, 1, None), Some(6)); 52 + } 53 + 54 + #[test] 55 + fn lua_print_width_marker() { 56 + let script = r#" 57 + function BOOT() 58 + cls(0) 59 + end 60 + function TIC() 61 + local w = print("AB", 10, 10, 1, false, 1, false) 62 + pix(10 + w, 10, 14) 63 + end 64 + "#; 65 + let fb = run_lua(script, 1); 66 + let mut fbm = fb.borrow_mut(); 67 + // text drew something in the glyph area near (10,10) 68 + let mut any = false; 69 + for yy in 10..(10 + 8) { 70 + for xx in 10..(10 + 8) { 71 + if fbm.pix(xx, yy, None) == Some(1) { 72 + any = true; 73 + break; 74 + } 75 + } 76 + if any { break; } 77 + } 78 + assert!(any, "expected some glyph pixels drawn near (10,10)"); 79 + 80 + // marker at x+width somewhere on row y=10 81 + let (w, _) = dimensions(); 82 + let mut found_marker = false; 83 + for x in 10..(w as i32) { 84 + if fbm.pix(x, 10, None) == Some(14) { found_marker = true; break; } 85 + } 86 + assert!(found_marker, "expected marker pixel with color 14 on row 10"); 87 + } 88 + 89 + #[test] 90 + fn lua_print_defaults_and_pix_read() { 91 + let script = r#" 92 + function BOOT() 93 + cls(0) 94 + end 95 + function TIC() 96 + local w = print("A") 97 + -- Strict gating: require that at least one glyph pixel was drawn 98 + -- near the origin using the default color (15), rather than probing (0,0). 99 + local drawn = false 100 + for yy = 0, 7 do 101 + for xx = 0, 7 do 102 + if pix(xx, yy) == 15 then drawn = true break end 103 + end 104 + if drawn then break end 105 + end 106 + if drawn then pix(w, 0, 7) end 107 + end 108 + "#; 109 + let fb = run_lua(script, 1); 110 + let mut fbm = fb.borrow_mut(); 111 + // default color drew some glyph pixels near origin 112 + let mut any = false; 113 + for yy in 0..8 { 114 + for xx in 0..8 { 115 + if fbm.pix(xx, yy, None) == Some(15) { any = true; break; } 116 + } 117 + if any { break; } 118 + } 119 + assert!(any, "expected some glyph pixels drawn near origin"); 120 + // verify the script marked (w,0) with 7 (we don't need to know w here) 121 + // find first non-zero on row 0 after x=0 122 + let (w, _) = dimensions(); 123 + let mut found = false; 124 + for x in 1..(w as i32) { 125 + if fbm.pix(x, 0, None) == Some(7) { 126 + found = true; 127 + break; 128 + } 129 + } 130 + assert!(found, "expected a marker pixel with color 7 on row 0"); 131 + }