···11+**TIC-80 Rust Rewrite — Agent Log**
22+33+- **Owner:** AI Coding Agent (Codex CLI)
44+- **Scope:** Help drive the Rust rewrite forward with tests-first changes, targeted features, and tight parity with TIC-80 behavior.
55+- **Last Updated:** 2025-08-26
66+77+**Context**
88+- **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`.
99+- **Plan Docs:** `RUST_REWRITE.md` defines phased roadmap; `docs/` contains early GUI-first specs and API parity checklist.
1010+- **Prototype Crate:** `tic80_rust` (single crate for now).
1111+ - `gfx::framebuffer`: 240×136 u8-index framebuffer; `cls`, `pix`, `line` (Bresenham), `rect`, `blit_to_rgba`, `print_text` (default font).
1212+ - `script::lua_runner`: `mlua` (Lua 5.4, vendored) binding for `cls/pix/line/rect/print` + `BOOT/TIC` flow.
1313+ - `main.rs`: `winit + pixels` presenter, fixed-step tick (~60 FPS), demo cart.
1414+- **Testing:** Rust unit tests under `tic80_rust/tests/` covering gfx and minimal Lua APIs.
1515+1616+**Build Hygiene (always do this)**
1717+- Fix all compiler warnings before landing changes (treat warnings as errors).
1818+- Run clippy on the crate and keep zero warnings: `cd tic80_rust && cargo clippy --all-targets --all-features -D warnings`.
1919+- Validate with tests: `cd tic80_rust && cargo test` (and run the specific failing test during fixes).
2020+- Keep changes minimal and focused; don’t expand scope while tests are red.
2121+2222+**Decisions (Locked for prototype)**
2323+- **Presentation:** `winit + pixels` with integer scaling (2x/3x/4x), RGBA palette conversion from 16-color default.
2424+- **Lua Engine:** `mlua` with vendored Lua 5.4, behavior aligned to 5.2 where needed (compat noted in docs).
2525+- **Framebuffer:** Single VRAM bank, palette indices in CPU memory; palette map/border/vbank deferred.
2626+2727+**Current Status**
2828+- Minimal end-to-end loop works: window opens, demo script runs (`cls/pix/line/rect/print`).
2929+- Tests run locally; 1 Lua API test currently failing (see below).
3030+- Docs/specs in place for early milestones: GUI + `cls/pix` and Lua + `cls/pix`.
3131+3232+**Failing Test (Next Task)**
3333+- `tic80_rust/tests/lua_api_tests.rs::lua_print_defaults_and_pix_read`
3434+ - 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.
3535+ - Likely Cause: Glyph rendering/metrics mismatch at origin. Candidates:
3636+ - Bit orientation when decoding `src/core/font.inl` (LSB/MSB) may be inverted.
3737+ - Trim logic for variable-width glyphs (`fixed=false`) may misalign the leftmost drawn column vs expected TIC-80 behavior.
3838+ - Row baseline/advance constants (ADV vs actual TIC font metrics) could be off by one.
3939+ - 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.
4040+4141+**Near-Term Backlog**
4242+- Fix `lua_print_defaults_and_pix_read` by aligning `print_text` to TIC-80 semantics.
4343+- Add unit tests for `print_text` width, left trim, and origin pixel behavior (non-Lua) to localize failures.
4444+- Expose `clip` and `rectb` to Lua; add basic tests.
4545+- Confirm palette correctness end-to-end (index→RGBA) against known swatches; keep golden samples.
4646+4747+**Mid-Term Backlog**
4848+- Flesh out `tic-gfx` primitives (`circb/circ/elli/ellib/tri/trib/font/map/spr`) per `docs/api_parity_checklist.md`.
4949+- Start `tic-api` facade layering (separating binding from core) as we add more APIs.
5050+- Add frame-hash snapshot tests for deterministic VRAM state.
5151+- Begin input semantics (`btn/key/keyp/btnp/mouse`) with fixed-step repeat behavior.
5252+5353+**Open Questions**
5454+- Exact bit orientation for `font.inl` (confirm LSB/MSB and row order vs. current implementation).
5555+- Default `print` metrics: width advance, left/right trimming, and baseline rules in TIC-80.
5656+- Small font (`smallfont=true`) parity requirements and when to implement.
5757+5858+**Operating Notes**
5959+- Keep changes surgical and test-driven; don’t expand surface while a test is red.
6060+- Prefer headless tests (VRAM hash or targeted pixel asserts) to validate behavior deterministically.
6161+- Document any intentional deviations from TIC-80 behavior in this file and update `docs/api_parity_checklist.md` as needed.
6262+6363+**Worklog**
6464+- 2025-08-26:
6565+ - Reviewed `RUST_REWRITE.md` and `docs/` (API parity, GUI-first milestones).
6666+ - Surveyed `tic80_rust` crate; confirmed implemented APIs (`cls/pix/line/rect/print`).
6767+ - Ran tests: one failure in Lua print-origin behavior identified; queued as next fix.
6868+ - Reorganized documentation:
6969+ - Moved plan to `docs/roadmap/overview.md` with a root stub.
7070+ - Merged GUI-first docs into `docs/roadmap/gui_first.md`.
7171+ - Renamed API parity to `docs/specs/lua_api_parity.md` and added specs stubs.
7272+ - Added docs index at `docs/README.md`, architecture/testing pages, and ADRs.
7373+7474+**Docs Index**
7575+- Start here: `docs/README.md`
7676+- Roadmap: `docs/roadmap/overview.md`, `docs/roadmap/gui_first.md`
7777+- Specs: `docs/specs/memory_map.md`, `docs/specs/lua_api_parity.md`, `docs/specs/graphics.md`, `docs/specs/audio_fft_vqt.md`
7878+- Architecture: `docs/architecture/workspace.md`, `docs/architecture/runtime.md`
7979+- Testing: `docs/testing/strategy.md`, `docs/testing/frame_hashes.md`
8080+- ADRs: `docs/adr/0001-winit-pixels.md`, `docs/adr/0002-mlua-lua54-compat.md`
+591
MEMORY_MAP.md
···11+# TIC-80 tiny computer
22+1.1.2837 (be42d6f)
33+https://tic80.com (C) 2017-2023
44+55+## Welcome
66+TIC-80 is a fantasy computer for making, playing and sharing tiny games.
77+88+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.
99+In the end, you will get a cartridge file, which can be stored and played on the website.
1010+1111+Also, the game can be packed into a player that works on all popular platforms and distributed as you wish.
1212+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.
1313+1414+## Specification
1515+```
1616+DISPLAY 240x136 pixels, 16 colors palette.
1717+INPUT 4 gamepads with 8 buttons / mouse / keyboard.
1818+SPRITES 256 8x8 tiles and 256 8x8 sprites.
1919+MAP 240x136 cells, 1920x1088 pixels.
2020+SOUND 4 channels with configurable waveforms.
2121+CODE 64KB of lua, ruby, js, moon, fennel, scheme, squirrel, wren, wasm, janet or python.
2222+```
2323+```
2424+2525++-----------------------------------+
2626+| 96KB RAM LAYOUT |
2727++-------+-------------------+-------+
2828+| ADDR | INFO | BYTES |
2929++-------+-------------------+-------+
3030+| 00000 | <VRAM> | 16384 |
3131+| 04000 | TILES | 8192 |
3232+| 06000 | SPRITES | 8192 |
3333+| 08000 | MAP | 32640 |
3434+| 0FF80 | GAMEPADS | 4 |
3535+| 0FF84 | MOUSE | 4 |
3636+| 0FF88 | KEYBOARD | 4 |
3737+| 0FF8C | SFX STATE | 16 |
3838+| 0FF9C | SOUND REGISTERS | 72 |
3939+| 0FFE4 | WAVEFORMS | 256 |
4040+| 100E4 | SFX | 4224 |
4141+| 11164 | MUSIC PATTERNS | 11520 |
4242+| 13E64 | MUSIC TRACKS | 408 |
4343+| 13FFC | MUSIC STATE | 4 |
4444+| 14000 | STEREO VOLUME | 4 |
4545+| 14004 | PERSISTENT MEMORY | 1024 |
4646+| 14404 | SPRITE FLAGS | 512 |
4747+| 14604 | FONT | 1016 |
4848+| 149FC | FONT PARAMS | 8 |
4949+| 14A04 | ALT FONT | 1016 |
5050+| 14DFC | ALT FONT PARAMS | 8 |
5151+| 14E04 | BUTTONS MAPPING | 32 |
5252+| 14E24 | ** RESERVED ** | 12764 |
5353++-------+-------------------+-------+
5454+```
5555+```
5656++-----------------------------------+
5757+| 16KB VRAM LAYOUT |
5858++-------+-------------------+-------+
5959+| ADDR | INFO | BYTES |
6060++-------+-------------------+-------+
6161+| 00000 | SCREEN | 16320 |
6262+| 03FC0 | PALETTE | 48 |
6363+| 03FF0 | PALETTE MAP | 8 |
6464+| 03FF8 | BORDER COLOR | 1 |
6565+| 03FF9 | SCREEN OFFSET | 2 |
6666+| 03FFB | MOUSE CURSOR | 1 |
6767+| 03FFC | BLIT SEGMENT | 1 |
6868+| 03FFD | ... (reserved) | 3 |
6969++-------+-------------------+-------+
7070+```
7171+7272+## Console commands
7373+7474+### cd
7575+change directory.
7676+usage: `
7777+cd <path>
7878+cd /
7979+cd ..`
8080+8181+### cls
8282+clear console screen.
8383+usage: `cls`
8484+8585+### config
8686+edit system configuration cartridge,
8787+use `reset` param to reset current configuration,
8888+use `default` to edit default cart template.
8989+usage: `config [reset|default]`
9090+9191+### del
9292+delete from the filesystem.
9393+usage: `del <file|folder>`
9494+9595+### demo
9696+install demo carts to the current directory.
9797+usage: `demo`
9898+9999+### dir
100100+show list of local files.
101101+usage: `dir`
102102+103103+### edit
104104+open cart editors.
105105+usage: `edit`
106106+107107+### eval
108108+run code provided code.
109109+usage: `eval`
110110+111111+### exit
112112+exit the application.
113113+usage: `exit`
114114+115115+### export
116116+export cart to HTML,
117117+native build (win linux rpi mac),
118118+export sprites/map/... as a .png image or export sfx and music to .wav files.
119119+usage: `
120120+export [win|winxp|linux|rpi|mac|html|binary|tiles|sprites|map|mapimg|sfx|music|screen|help|...] <file> [bank=0 vbank=0 id=0 ...]`
121121+122122+### folder
123123+open working directory in OS.
124124+usage: `folder`
125125+126126+### help
127127+show help info about commands/api/...
128128+usage: `help [<text>|version|welcome|spec|ram|vram|commands|api|keys|buttons|startup|terms|license]`
129129+130130+### import
131131+import code/sprites/map/... from an external file.
132132+usage: `
133133+import [binary|tiles|sprites|map|code|screen|...] <file> [bank=0 x=0 y=0 w=0 h=0 vbank=0 ...]`
134134+135135+### load
136136+load cartridge from the local filesystem(there's no need to type the .tic extension).
137137+you can also load just the section (sprites, map etc) from another cart.
138138+usage: `load <cart> [code|tiles|sprites|map|sfx|music|palette|flags|screen]`
139139+140140+### menu
141141+show game menu where you can setup video, sound and input options.
142142+usage: `menu`
143143+144144+### mkdir
145145+make a directory.
146146+usage: `mkdir <name>`
147147+148148+### new
149149+creates a new `Hello World` cartridge.
150150+usage: `new <lua|ruby|js|moon|fennel|scheme|squirrel|wren|wasm|janet|python>`
151151+152152+### resume
153153+resume last run cart / project.
154154+usage: `resume`
155155+156156+### run
157157+run current cart / project.
158158+usage: `run`
159159+160160+### save
161161+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).
162162+usage: `save <cart>`
163163+164164+### surf
165165+open carts browser.
166166+usage: `surf`
167167+168168+## API functions
169169+170170+### BDR
171171+`BDR(row)`
172172+Allows you to execute code between the drawing of each fullscreen scanline, for example, to manipulate the palette.
173173+174174+### BOOT
175175+`BOOT`
176176+Startup function.
177177+178178+### MENU
179179+`MENU(index)`
180180+Game Menu handler.
181181+182182+### SCN
183183+`SCN(row)`
184184+Allows you to execute code between the drawing of each scanline, for example, to manipulate the palette.
185185+186186+### TIC
187187+`TIC()`
188188+Main function. It's called at 60fps (60 times every second).
189189+190190+### btn
191191+`btn(id) -> pressed`
192192+This function allows you to read the status of one of the buttons attached to TIC.
193193+The function returns true if the key with the supplied id is currently in the pressed state.
194194+It remains true for as long as the key is held down.
195195+If you want to test if a key was just pressed, use `btnp()` instead.
196196+197197+### btnp
198198+`btnp(id hold=-1 period=-1) -> pressed`
199199+This function allows you to read the status of one of TIC's buttons.
200200+It returns true only if the key has been pressed since the last frame.
201201+You can also use the optional hold and period parameters which allow you to check if a button is being held down.
202202+After the time specified by hold has elapsed, btnp will return true each time period is passed if the key is still down.
203203+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).
204204+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.
205205+206206+### circ
207207+`circ(x y radius color)`
208208+This function draws a filled circle of the desired radius and color with its center at x, y.
209209+It uses the Bresenham algorithm.
210210+211211+### circb
212212+`circb(x y radius color)`
213213+Draws the circumference of a circle with its center at x, y using the radius and color requested.
214214+It uses the Bresenham algorithm.
215215+216216+### clip
217217+`clip(x y width height)
218218+clip()`
219219+This function limits drawing to a clipping region or `viewport` defined by x,y,w,h.
220220+Things drawn outside of this area will not be visible.
221221+Calling clip() with no parameters will reset the drawing area to the entire screen.
222222+223223+### cls
224224+`cls(color=0)`
225225+Clear the screen.
226226+When called this function clear all the screen using the color passed as argument.
227227+If no parameter is passed first color (0) is used.
228228+229229+### elli
230230+`elli(x y a b color)`
231231+This function draws a filled ellipse of the desired a, b radiuses and color with its center at x, y.
232232+It uses the Bresenham algorithm.
233233+234234+### ellib
235235+`ellib(x y a b color)`
236236+This function draws an ellipse border with the desired radiuses a b and color with its center at x, y.
237237+It uses the Bresenham algorithm.
238238+239239+### exit
240240+`exit()`
241241+Interrupts program execution and returns to the console when the TIC function ends.
242242+243243+### fget
244244+`fget(sprite_id flag) -> bool`
245245+Returns true if the specified flag of the sprite is set. See `fset()` for more details.
246246+247247+### font
248248+`font(text x y chromakey char_width char_height fixed=false scale=1 alt=false) -> width`
249249+Print string with font defined in foreground sprites.
250250+To simply print to the screen, check out `print()`.
251251+To print to the console, check out `trace()`.
252252+253253+### fset
254254+`fset(sprite_id flag bool)`
255255+Each sprite has eight flags which can be used to store information or signal different conditions.
256256+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.
257257+See algo `fget()`.
258258+259259+### key
260260+`key(code=-1) -> pressed`
261261+The function returns true if the key denoted by keycode is pressed.
262262+263263+### keyp
264264+`keyp(code=-1 hold=-1 period=-1) -> pressed`
265265+This function returns true if the given key is pressed but wasn't pressed in the previous frame.
266266+Refer to `btnp()` for an explanation of the optional hold and period parameters.
267267+268268+### line
269269+`line(x0 y0 x1 y1 color)`
270270+Draws a straight line from point (x0,y0) to point (x1,y1) in the specified color.
271271+272272+### map
273273+`map(x=0 y=0 w=30 h=17 sx=0 sy=0 colorkey=-1 scale=1 remap=nil)`
274274+The map consists of cells of 8x8 pixels, each of which can be filled with a sprite using the map editor.
275275+The map can be up to 240 cells wide by 136 deep.
276276+This function will draw the desired area of the map to a specified screen position.
277277+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).
278278+The map function's last parameter is a powerful callback function for changing how map cells (sprites) are drawn when map is called.
279279+It can be used to rotate, flip and replace sprites while the game is running.
280280+Unlike mset, which saves changes to the map, this special function can be used to create animated tiles or replace them completely.
281281+Some examples include changing sprites to open doorways, hiding sprites used to spawn objects in your game and even to emit the objects themselves.
282282+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.
283283+To set the tile immediately below this we need to write to 0x08000 + 240, ie 0x080F0.
284284+285285+### memcpy
286286+`memcpy(dest source size)`
287287+This function allows you to copy a continuous block of TIC's 96K RAM from one address to another.
288288+Addresses are specified are in hexadecimal format, values are decimal.
289289+290290+### memset
291291+`memset(dest value size)`
292292+This function allows you to set a continuous block of any part of TIC's RAM to the same value.
293293+The address is specified in hexadecimal format, the value in decimal.
294294+295295+### mget
296296+`mget(x y) -> tile_id`
297297+Gets the sprite id at the given x and y map coordinate.
298298+299299+### mouse
300300+`mouse() -> x y left middle right scrollx scrolly`
301301+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.
302302+303303+### mset
304304+`mset(x y tile_id)`
305305+This function will change the tile at the specified map coordinates.
306306+By default, changes made are only kept while the current game is running.
307307+To make permanent changes to the map, see `sync()`.
308308+Related: `map()` `mget()` `sync()`.
309309+310310+### music
311311+`music(track=-1 frame=-1 row=-1 loop=true sustain=false tempo=-1 speed=-1)`
312312+This function starts playing a track created in the Music Editor.
313313+Call without arguments to stop the music.
314314+315315+### peek
316316+`peek(addr bits=8) -> value`
317317+This function allows to read the memory from TIC.
318318+It's useful to access resources created with the integrated tools like sprite, maps, sounds, cartridges data?
319319+Never dream to sound a sprite?
320320+Address are in hexadecimal format but values are decimal.
321321+To write to a memory address, use `poke()`.
322322+`bits` allowed to be 1,2,4,8.
323323+324324+### peek1
325325+`peek1(addr) -> value`
326326+This function enables you to read single bit values from TIC's RAM.
327327+The address is often specified in hexadecimal format.
328328+329329+### peek2
330330+`peek2(addr) -> value`
331331+This function enables you to read two bits values from TIC's RAM.
332332+The address is often specified in hexadecimal format.
333333+334334+### peek4
335335+`peek4(addr) -> value`
336336+This function enables you to read values from TIC's RAM.
337337+The address is often specified in hexadecimal format.
338338+See 'poke4()' for detailed information on how nibble addressing compares with byte addressing.
339339+340340+### pix
341341+`pix(x y color)
342342+pix(x y) -> color`
343343+This function can read or write pixel color values.
344344+When called with a color parameter, the pixel at the specified coordinates is set to that color.
345345+Calling the function without a color parameter returns the color of the pixel at the specified position.
346346+347347+### pmem
348348+`pmem(index value)
349349+pmem(index) -> value`
350350+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.
351351+This is useful for saving high-scores, level advancement or achievements.
352352+The data is stored as unsigned 32-bit integers (from 0 to 4294967295).
353353+354354+Tips:
355355+- pmem depends on the cartridge hash (md5), so don't change your lua script if you want to keep the data.
356356+- Use `saveid:` with a personalized string in the header metadata to override the default MD5 calculation.
357357+This allows the user to update a cart without losing their saved data.
358358+359359+### poke
360360+`poke(addr value bits=8)`
361361+This function allows you to write a single byte to any address in TIC's RAM.
362362+The address should be specified in hexadecimal format, the value in decimal.
363363+`bits` allowed to be 1,2,4,8.
364364+365365+### poke1
366366+`poke1(addr value)`
367367+This function allows you to write single bit values directly to RAM.
368368+The address is often specified in hexadecimal format.
369369+370370+### poke2
371371+`poke2(addr value)`
372372+This function allows you to write two bits values directly to RAM.
373373+The address is often specified in hexadecimal format.
374374+375375+### poke4
376376+`poke4(addr value)`
377377+This function allows you to write directly to RAM.
378378+The address is often specified in hexadecimal format.
379379+For both peek4 and poke4 RAM is addressed in 4 bit segments (nibbles).
380380+Therefore, to access the the RAM at byte address 0x4000
381381+you would need to access both the 0x8000 and 0x8001 nibble addresses.
382382+383383+### print
384384+`print(text x=0 y=0 color=15 fixed=false scale=1 smallfont=false) -> width`
385385+This will simply print text to the screen using the font defined in config.
386386+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.
387387+When fixed width is false, there will be a single space between each character.
388388+389389+Tips:
390390+- To use a custom rastered font, check out `font()`.
391391+- To print to the console, check out `trace()`.
392392+393393+### rect
394394+`rect(x y w h color)`
395395+This function draws a filled rectangle of the desired size and color at the specified position.
396396+If you only need to draw the the border or outline of a rectangle (ie not filled) see `rectb()`.
397397+398398+### rectb
399399+`rectb(x y w h color)`
400400+This function draws a one pixel thick rectangle border at the position requested.
401401+If you need to fill the rectangle with a color, see `rect()` instead.
402402+403403+### reset
404404+`reset()`
405405+Resets the cartridge. To return to the console, see the `exit()`.
406406+407407+### sfx
408408+`sfx(id note=-1 duration=-1 channel=0 volume=15 speed=0)`
409409+This function will play the sound with `id` created in the sfx editor.
410410+Calling the function with id set to -1 will stop playing the channel.
411411+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.
412412+For example, a note value of `14` will play the note `D` in the second octave.
413413+The same note could be specified by the string `D-2`.
414414+Note names consist of two characters, the note itself (in upper case) followed by `-` to represent the natural note or `#` to represent a sharp.
415415+There is no option to indicate flat values.
416416+The available note names are therefore: C-, C#, D-, D#, E-, F-, F#, G-, G#, A-, A#, B-.
417417+The `octave` is specified using a single digit in the range 0 to 8.
418418+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.
419419+A value of -1 will play the sound continuously.
420420+The `channel` parameter indicates which of the four channels to use. Allowed values are 0 to 3.
421421+The `volume` can be between 0 and 15.
422422+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.
423423+424424+### spr
425425+`spr(id x y colorkey=-1 scale=1 flip=0 rotate=0 w=1 h=1)`
426426+Draws the sprite number index at the x and y coordinate.
427427+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.
428428+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.
429429+You can flip the sprite where:
430430+- 0 = No Flip
431431+- 1 = Flip horizontally
432432+- 2 = Flip vertically
433433+- 3 = Flip both vertically and horizontally
434434+When you rotate the sprite, it's rotated clockwise in 90 steps:
435435+- 0 = No rotation
436436+- 1 = 90 rotation
437437+- 2 = 180 rotation
438438+- 3 = 270 rotation
439439+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).
440440+441441+### sync
442442+`sync(mask=0 bank=0 tocart=false)`
443443+The pro version of TIC-80 contains 8 memory banks.
444444+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.
445445+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).
446446+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.
447447+448448+### time
449449+`time() -> ticks`
450450+This function returns the number of milliseconds elapsed since the cartridge began execution.
451451+Useful for keeping track of time, animating items and triggering events.
452452+453453+### trace
454454+`trace(message color=15)`
455455+This is a service function, useful for debugging your code.
456456+It prints the message parameter to the console in the (optional) color specified.
457457+458458+Tips:
459459+- The Lua concatenator for strings is .. (two points).
460460+- Use console cls command to clear the output from trace.
461461+462462+### tri
463463+`tri(x1 y1 x2 y2 x3 y3 color)`
464464+This function draws a triangle filled with color, using the supplied vertices.
465465+466466+### trib
467467+`trib(x1 y1 x2 y2 x3 y3 color)`
468468+This function draws a triangle border with color, using the supplied vertices.
469469+470470+### tstamp
471471+`tstamp() -> timestamp`
472472+This function returns the number of seconds elapsed since January 1st, 1970.
473473+Useful for creating persistent games which evolve over time between plays.
474474+475475+### ttri
476476+`ttri(x1 y1 x2 y2 x3 y3 u1 v1 u2 v2 u3 v3 texsrc=0 chromakey=-1 z1=0 z2=0 z3=0)`
477477+It renders a triangle filled with texture from image ram, map ram or vbank.
478478+Use in 3D graphics.
479479+In particular, if the vertices in the triangle have different 3D depth, you may see some distortion.
480480+These can be thought of as the window inside image ram (sprite sheet), map ram or another vbank.
481481+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.
482482+So for example the top left corner of sprite #2 would be located at u=16, v=0.
483483+484484+### vbank
485485+`vbank(bank) -> prev
486486+vbank() -> prev`
487487+VRAM contains 2x16K memory chips, use vbank(0) or vbank(1) to switch between them.
488488+489489+## Button IDs
490490+```
491491++--------+----+----+----+----+
492492+| ACTION | P1 | P2 | P3 | P4 |
493493++--------+----+----+----+----+
494494+| UP | 0 | 8 | 16 | 24 |
495495+| DOWN | 1 | 9 | 17 | 25 |
496496+| LEFT | 2 | 10 | 18 | 26 |
497497+| RIGHT | 3 | 11 | 19 | 27 |
498498+| A | 4 | 12 | 20 | 28 |
499499+| B | 5 | 13 | 21 | 29 |
500500+| X | 6 | 14 | 22 | 30 |
501501+| Y | 7 | 15 | 23 | 31 |
502502++--------+----+----+----+----+```
503503+504504+## Key IDs
505505+```
506506++----+------------+ +----+------------+
507507+|CODE| KEY | |CODE| KEY |
508508++----+------------+ +----+------------+
509509+| 1 | A | | 48 | SPACE |
510510+| 2 | B | | 49 | TAB |
511511+| 3 | C | | 50 | RETURN |
512512+| 4 | D | | 51 | BACKSPACE |
513513+| 5 | E | | 52 | DELETE |
514514+| 6 | F | | 53 | INSERT |
515515+| 7 | G | | 54 | PAGEUP |
516516+| 8 | H | | 55 | PAGEDOWN |
517517+| 9 | I | | 56 | HOME |
518518+| 10 | J | | 57 | END |
519519+| 11 | K | | 58 | UP |
520520+| 12 | L | | 59 | DOWN |
521521+| 13 | M | | 60 | LEFT |
522522+| 14 | N | | 61 | RIGHT |
523523+| 15 | O | | 62 | CAPSLOCK |
524524+| 16 | P | | 63 | CTRL |
525525+| 17 | Q | | 64 | SHIFT |
526526+| 18 | R | | 65 | ALT |
527527+| 19 | S | | 66 | ESC |
528528+| 20 | T | | 67 | F1 |
529529+| 21 | U | | 68 | F2 |
530530+| 22 | V | | 69 | F3 |
531531+| 23 | W | | 70 | F4 |
532532+| 24 | X | | 71 | F5 |
533533+| 25 | Y | | 72 | F6 |
534534+| 26 | Z | | 73 | F7 |
535535+| 27 | 0 | | 74 | F8 |
536536+| 28 | 1 | | 75 | F9 |
537537+| 29 | 2 | | 76 | F10 |
538538+| 30 | 3 | | 77 | F11 |
539539+| 31 | 4 | | 78 | F12 |
540540+| 32 | 5 | | 79 | NUM0 |
541541+| 33 | 6 | | 80 | NUM1 |
542542+| 34 | 7 | | 81 | NUM2 |
543543+| 35 | 8 | | 82 | NUM3 |
544544+| 36 | 9 | | 83 | NUM4 |
545545+| 37 | MINUS | | 84 | NUM5 |
546546+| 38 | EQUALS | | 85 | NUM6 |
547547+| 39 | LEFTBRACKET| | 86 | NUM7 |
548548+| 40 | RIGHTBRACKT| | 87 | NUM8 |
549549+| 41 | BACKSLASH | | 88 | NUM9 |
550550+| 42 | SEMICOLON | | 89 | NUMPLUS |
551551+| 43 | APOSTROPHE | | 90 | NUMMINUS |
552552+| 44 | GRAVE | | 91 | NUMMULTIPLY|
553553+| 45 | COMMA | | 92 | NUMDIVIDE |
554554+| 46 | PERIOD | | 93 | NUMENTER |
555555+| 47 | SLASH | | 94 | NUMPERIOD |
556556++----+------------+ +----+------------+
557557+```
558558+559559+## Startup options
560560+```
561561+--skip skip startup animation
562562+--volume=<int> global volume value [0-15]
563563+--cli console only output
564564+--fullscreen enable fullscreen mode
565565+--vsync enable VSYNC
566566+--soft use software rendering
567567+--fs=<str> path to the file system folder
568568+--scale=<int> main window scale
569569+--cmd=<str> run commands in the console
570570+--keepcmd re-execute commands on every run
571571+--version print program version
572572+--crt enable CRT monitor effect
573573+```
574574+575575+## Terms of Use
576576+- All cartridges posted on the https://tic80.com website are the property of their authors.
577577+- Do not redistribute the cartridge without permission, directly from the author.
578578+- 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.
579579+- Do not post material that violates copyright, obscenity or any other laws.
580580+- Nesbox reserves the right to remove or filter any material without prior notice.
581581+582582+## Privacy Policy
583583+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.
584584+585585+## MIT License
586586+587587+Copyright (c) 2017-2023 Vadim Grigoruk @nesbox // grigoruk@gmail.com
588588+589589+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.
590590+591591+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
···11+# TIC-80 Rust Rewrite — Plan Location Moved
22+33+This plan now lives at `docs/roadmap/overview.md`.
44+55+Quick links:
66+- Roadmap: `docs/roadmap/overview.md`
77+- GUI-first milestones: `docs/roadmap/gui_first.md`
88+- Docs index: `docs/README.md`
99+
+30
docs/README.md
···11+# TIC-80 Rust Rewrite — Docs Index
22+33+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.
44+55+## Roadmap
66+- `docs/roadmap/overview.md`: High-level phased roadmap and goals (moved from RUST_REWRITE.md).
77+- `docs/roadmap/gui_first.md`: Combined GUI-first kickoff + milestones for `winit + pixels` and `cls/pix`.
88+99+## Specs
1010+- `docs/specs/memory_map.md`: Canonical pointer to the root `MEMORY_MAP.md` and usage notes.
1111+- `docs/specs/lua_api_parity.md`: API parity checklist for Lua (name, signature, side effects).
1212+- `docs/specs/graphics.md`: Framebuffer, palette mapping, text/print semantics (stub to be expanded).
1313+- `docs/specs/audio_fft_vqt.md`: FFT/VQT behavior and parameters (points to `CLAUDE.md`).
1414+1515+## Architecture
1616+- `docs/architecture/workspace.md`: Crate layout and module boundaries.
1717+- `docs/architecture/runtime.md`: Fixed-step loop, callbacks, and presentation responsibilities.
1818+1919+## Testing
2020+- `docs/testing/strategy.md`: Testing and validation strategy across API/VRAM/audio.
2121+- `docs/testing/frame_hashes.md`: Conventions for deterministic frame/audio hashing (stub).
2222+2323+## Decisions (ADR)
2424+- `docs/adr/0001-winit-pixels.md`: Windowing/presentation stack decision.
2525+- `docs/adr/0002-mlua-lua54-compat.md`: Lua engine choice and compatibility stance.
2626+2727+Notes
2828+- `MEMORY_MAP.md` at repo root remains the canonical reference for layout. Specs here link to it rather than duplicating.
2929+- `CLAUDE.md` remains at repo root; the spec page links to it for detailed FFT/VQT behavior.
3030+
+15
docs/adr/0001-winit-pixels.md
···11+# ADR 0001: Windowing/Presentation Stack
22+33+Status: Accepted
44+Date: 2025-08-26
55+66+Context
77+- We need a portable, Rust-native way to open a window and present a 240×136 framebuffer with predictable scaling.
88+99+Decision
1010+- Use `winit` for window/events and `pixels` for presenting an RGBA buffer (wgpu-backed) with integer nearest-neighbor scaling.
1111+1212+Consequences
1313+- Desktop-first with a path to Web via wgpu. No SDL runtime dependency.
1414+- Keep an optional SDL2 backend as a future alternative if needed.
1515+
+15
docs/adr/0002-mlua-lua54-compat.md
···11+# ADR 0002: Lua Engine and Compatibility
22+33+Status: Accepted
44+Date: 2025-08-26
55+66+Context
77+- TIC-80 carts rely on Lua semantics aligned with 5.2 behavior; portability and deterministic behavior are priorities.
88+99+Decision
1010+- Use `mlua` with vendored Lua 5.4, enabling behavior that preserves expected 5.2 semantics where applicable (see tests).
1111+1212+Consequences
1313+- Portable across supported platforms; avoids LuaJIT portability trade-offs initially.
1414+- Add tests around numeric semantics and iteration order to guard against drift.
1515+
+14
docs/architecture/runtime.md
···11+# Runtime Architecture
22+33+Fixed-Step Loop
44+- 60 FPS logical tick; present cadence may be vsync-locked or decoupled.
55+- Each tick: process input → run callbacks → render → present.
66+77+Callbacks
88+- `BOOT()` once after cart load; `TIC()` each frame; `SCN(row)`/`BDR(row)` during draw (later).
99+- Error policy: trace and stop or bubble to host; define consistent behavior per phase.
1010+1111+Presentation
1212+- CPU-owned framebuffer as palette indices; `blit_to_rgba` maps to RGBA for window texture.
1313+- Integer scaling to maintain crisp pixels.
1414+
+18
docs/architecture/workspace.md
···11+# Workspace Architecture
22+33+Crates (target layout)
44+- `tic-core`: VM state, memory map, cart model, fixed-step ticker.
55+- `tic-gfx`: CPU rasterizer (`pix/line/rect/...`), VRAM/VRAM banks, palette ops.
66+- `tic-audio`: PSG synth + mixer; later capture buffer for FX.
77+- `tic-io`: Input abstraction (kbd/mouse/gamepad), FS/cart IO, time.
88+- `tic-api`: Language-agnostic API facade matching TIC-80 surface.
99+- `tic-lua`: `mlua` embedding and Lua shims over `tic-api`.
1010+- `tic-fx`: FFT/VQT analysis per CLAUDE.md.
1111+- `tic-winit` (or `tic-sdl`): Platform layer for window/audio/input.
1212+- `tic-runner`: CLI binary to run carts headless or with window.
1313+1414+Data Flow
1515+- Lua (`tic-lua`) calls into `tic-api` → forwards to `tic-core/gfx/audio/io`.
1616+- `tic-gfx` writes to VRAM page(s); presenter converts palette indices to RGBA for display.
1717+- `tic-audio` produces sample blocks; optional capture ring shared with `tic-fx`.
1818+
+38
docs/roadmap/gui_first.md
···11+# GUI-First Track: Window + cls/pix and Lua Wiring
22+33+Objective
44+- Open a window (winit + pixels), present a 240×136 framebuffer with integer scaling, and exercise `cls/pix` end-to-end, then wire Lua callbacks.
55+66+Decisions
77+- Platform: `winit + pixels` (desktop first, portable to Web).
88+- Input/Audio (later): `winit` (kbd/mouse), `gilrs` (gamepad), `cpal` (audio).
99+- Scaling: integer nearest-neighbor (2x/3x/4x). Default 3x. Preserve aspect.
1010+- Pixel format: u8 palette indices → RGBA via 16-color palette from memory map.
1111+1212+Milestone 1 — GUI + cls/pix (no Lua)
1313+- Deliverables
1414+ - Presenter renders a CPU-owned 240×136 index buffer via `pixels` with NN scaling.
1515+ - Minimal gfx spec for `cls/pix` and palette mapping.
1616+ - Verification: unscaled buffer hashes + micro-demos (alternating `cls`, crosshair via `pix`, palette grid).
1717+- Tasks
1818+ - Finalize FB spec and present path; set vsync policy.
1919+ - Implement `cls/pix` and buffer→RGBA blit; add unit tests.
2020+ - Write hash fixtures and demo scripts (host-driven).
2121+2222+Milestone 2 — Lua + cls/pix
2323+- Deliverables
2424+ - `mlua` runner: BOOT once, TIC per frame; expose `cls/pix`.
2525+ - Error policy for unimplemented APIs; argument handling for `pix` read/write modes.
2626+ - Verification: deterministic script exercising `cls/pix` yields known hashes.
2727+- Tasks
2828+ - Bind `cls/pix`; confirm numeric conversions and return values match TIC-80.
2929+ - Add tests for callback flow and pixel state.
3030+3131+Risks & Mitigations
3232+- Color differences: use exact sRGB bytes; disable filtering.
3333+- Timing jitter: fixed 60 FPS tick; decouple present cadence if needed.
3434+- Lua numeric semantics: be explicit about integer vs float at the boundary.
3535+3636+Next
3737+- Extend primitives (`print`, `line`, `rect/rectb`), then `spr/map`, `clip`, and `vbank`.
3838+
+191
docs/roadmap/overview.md
···11+# TIC-80 Rust Rewrite Plan
22+33+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.
44+55+## Goals and Scope
66+77+- Compatibility: Run existing `.tic` cartridges unmodified; preserve memory map, API semantics, timing, and constraints (240×136, 16-color palette, sprites, map, 4 channels, etc.).
88+- Determinism: Per-frame determinism for a given seed/input across platforms.
99+- Performance: Match or exceed C implementation at 60 FPS on desktop targets.
1010+- Portability: Desktop first (macOS, Linux, Windows), then WebAssembly; keep console ports out-of-scope initially.
1111+- Phased Delivery: Start with core runtime + Lua API, defer Studio editors.
1212+1313+## Non-Goals (Initial Phases)
1414+1515+- Rewriting the Studio editors (code/sprite/map/music/sfx UIs) in Phase 1–4.
1616+- Supporting all languages up front; we start with Lua-only to validate the Rust core.
1717+- Feature-creep beyond documented TIC-80 behavior unless explicitly called out as optional.
1818+1919+## Reference Material (from this repo)
2020+2121+- 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`.
2222+- Audio analysis: FFT/VQT covered in CLAUDE.md with precise specs and behavior.
2323+- Build/deps: `CMakeLists.txt`, `cmake/*.cmake` (Lua vendored, SDL, zlib, etc.).
2424+2525+## Architectural Decomposition (Rust)
2626+2727+Crates (workspace):
2828+- `tic-core`: Core VM state, memory map, timing, resource management, cartridge model, save states.
2929+- `tic-gfx`: CPU rasterizer for all primitives (`pix`, `line`, `circ`, `rect`, `spr`, `map`, `clip`, palette ops), VRAM, and draw modes.
3030+- `tic-audio`: PSG-style synth + mixer matching `src/core/sound.c`; later add capture for FFT/VQT.
3131+- `tic-io`: Input abstraction (keyboard/mouse/gamepad), filesystem/cart IO, clipboard, time.
3232+- `tic-api`: Language-agnostic API facade mirroring `api.h` semantics against `tic-core/gfx/audio/io`.
3333+- `tic-lua`: Lua runtime embedding and API shims that expose `tic-api` to Lua scripts.
3434+- `tic-fx`: FFT/VQT analysis (post-capture) with smoothing, peak normalization, whitening.
3535+- `tic-sdl` (or `tic-winit`): Platform layer for windowing, GL or GPU context, audio IO, input events.
3636+- `tic-runner`: CLI binary that loads `.tic`, runs headless or with window; no Studio.
3737+- `tic-studio` (later): Editor UI reimplementation (e.g., egui or immediate-mode toolkit) when core stabilizes.
3838+3939+Rationale: keep concerns isolated, enable headless testing and cross-checking per crate.
4040+4141+## Key Compatibility Requirements
4242+4343+- Cartridge Format: Load/save `.tic` exactly (including zlib packing). Preserve bank semantics and PRO extensions (only when targeted).
4444+- Memory Map: Mirror addresses, sizes, and behavior (RAM/VRAM/persistent). Do not expose Rust internals to scripts.
4545+- 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.
4646+- Timing: 60 FPS tick by default; menu/console interactions deferred until Studio phase; ensure fixed-step logic to match C.
4747+- FFT/VQT: Reproduce CLAUDE.md specs (buffer sizes, smoothing factors, auto-gain, whitening parameters) for visual parity.
4848+4949+## Cross-Cutting Concerns and Decisions
5050+5151+- 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.
5252+- 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.
5353+- 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`.
5454+- Compression: Use `flate2`/`miniz_oxide` for zlib compatible packing/unpacking of carts.
5555+- FFT: Use `rustfft` for 2k (FFT) and 8k (VQT path) with exact binning and smoothing behavior; implement variable-Q kernel generation per CLAUDE.md.
5656+- Testing: Frame-hash snapshots for VRAM, audio block-level comparisons, API-level golden tests. Conformance carts from `demos/`.
5757+5858+## Phased Roadmap
5959+6060+Phase 0 — Discovery and Spec (no code, docs + harness prep)
6161+- Inventory `api.h` and Lua binding coverage in `src/api/luaapi.c` and `src/api/lua.c`.
6262+- Write API parity checklist (functions, signatures, side effects, edge cases). Annotate open questions.
6363+- Map memory layout and constraints from `src/tic.h`, `src/core/*`, and CLAUDE.md into a concise spec.
6464+- Identify platform dependencies to replace (SDL, zlib, audio) and select Rust crates.
6565+- Output: Design doc + checklists; crate layout proposal; risk register.
6666+6767+Phase 1 — Cartridge + Core State (headless)
6868+- Implement `.tic` parsing/loading/saving; banks, metadata, palettes, sprites, map, code segment(s).
6969+- Define `tic-core` state, memory map, and fixed-step ticker. No rendering/audio yet.
7070+- Add basic CLI to inspect carts and dump metadata (headless validation).
7171+- Output: Can load and introspect `.tic`; deterministic tick progression without side effects.
7272+7373+Phase 2 — Lua Runtime + API Shim (headless)
7474+- Embed Lua; load script from cart; call `BOOT()` then per-frame `TIC()`.
7575+- Implement a minimal subset of API in `tic-api` with in-memory effects only: `time()`, `peek/poke` (scoped), `trace`, seed/rng.
7676+- Add harness to run demo carts in headless mode and record API calls.
7777+- Output: Runs simple scripts that don’t draw or play sound; deterministic logs.
7878+7979+Phase 3 — Graphics Primitives (software)
8080+- Implement full raster pipeline in `tic-gfx` (CPU): palette, clip, `pix/line/circ/rect/tri`, text (`print`), sprites (`spr`), map (`map`), blitting rules.
8181+- Hook `tic-api` drawing calls to `tic-gfx`; expose a window via `tic-sdl`/`tic-winit` for presentation only.
8282+- Add VRAM frame hashing and image snapshots; compare against reference C build where feasible.
8383+- Output: Visual carts render correctly; headless tests produce stable hashes.
8484+8585+Phase 4 — Input + Timing
8686+- Keyboard/mouse/gamepad in `tic-io`; map to TIC-80 semantics; ensure edge cases (press/repeat/up/down).
8787+- Verify fixed-step timing at 60 FPS, decoupling present frequency if needed.
8888+- Output: Interactive carts playable; deterministic input handling under recorded streams.
8989+9090+Phase 5 — Audio Synth + Mixing
9191+- Reimplement `src/core/sound.c` behavior: waveforms, SFX, music tracker playback, mixing; ensure bit-exact or perceptually equivalent output.
9292+- Add audio output via `cpal`; implement recordable audio blocks for tests.
9393+- Output: Audio carts sound correct; block hashes/stats match baselines.
9494+9595+Phase 6 — FFT/VQT Feature Parity
9696+- Implement shared capture buffer; 2048-sample FFT (21 fps) and 8192-sample VQT path per CLAUDE.md.
9797+- Implement smoothing, peak normalization, whitening with configurable macros equivalent.
9898+- Expose APIs: `fft/ffts/fftr/fftrs/vqt/vqts/vqtr/vqtrs/vqtw/vqtsw/vqtrw/vqtrsw`.
9999+- Output: Visual analyzers from `demos/` behave identically.
100100+101101+Phase 7 — Studio (Optional, staged)
102102+- Recreate Studio UI gradually (console, code, sprite, map, sfx, music) using immediate-mode UI (e.g., egui) or SDL-rendered IMGUI-like.
103103+- Defer complex UX to later; keep feature set close to original.
104104+- Output: Usable integrated editor after core stabilizes.
105105+106106+Phase 8 — WebAssembly + Additional Platforms
107107+- WASM target (via `wasm32-unknown-unknown` + `wasm-bindgen`/`wasm32-wasi`), web audio, canvas; sandboxed storage.
108108+- Optional mobile ports later.
109109+110110+## Testing and Validation Strategy
111111+112112+- API Goldens: Unit tests per API call against known inputs (including edge cases and errors).
113113+- Frame Hashes: Hash VRAM after each frame to produce stable signatures for demo carts.
114114+- Audio Blocks: Hash mixed audio blocks; tolerance windows for FP differences.
115115+- Conformance Carts: Automate running `demos/` carts headless, comparing output to reference traces.
116116+- Differential Testing: Optional—run identical carts on original C build and Rust build; compare frame hashes and key metrics.
117117+- Fuzzing: Fuzz `.tic` loader and selected APIs; validate against panics and UB.
118118+119119+## Risks and Mitigations
120120+121121+- 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.
122122+- Rendering Differences: Pixel-perfect behavior required. Begin with CPU rasterizer, codify exact blending rules.
123123+- Audio Timing: Latency/jitter from host APIs. Use ring buffers and fixed block sizes; decouple from visual ticks.
124124+- FFT/VQT Performance: 8k FFT + VQT kernel costs. Optimize with plan reuse, sparse kernels, and careful allocation.
125125+- WASM Constraints: No native threads; audio timing quirks. Defer WASM until desktop parity.
126126+127127+## Milestone Criteria (Per Phase)
128128+129129+- Phase 1: `.tic` carts parse and round-trip; metadata and assets intact.
130130+- Phase 2: Headless Lua scripts run; `TIC()` tick loop stable; minimal API callable.
131131+- Phase 3: Visual primitives render; hashes match baselines for selected carts.
132132+- Phase 4: Input tests pass; deterministic playback under recorded inputs.
133133+- Phase 5: Audio tests pass; tracker playback validated.
134134+- Phase 6: FFT/VQT demo carts behavior matches; numeric metrics within tolerances.
135135+- Phase 7/8: Usability and platform acceptance criteria defined separately.
136136+137137+## Work Breakdown for Lua-Only Track (Fast Path)
138138+139139+1) Phase 0 deliverables (API checklist + confirm/align to MEMORY_MAP.md).
140140+2) Cartridge loader + Lua script extraction only; ignore non-Lua carts initially.
141141+3) Headless Lua VM with `BOOT/TIC` and a minimal `tic-api` surface (`time`, `trace`, RNG, `peek/poke` across documented regions).
142142+4) Software `tic-gfx` with presentation via SDL/winit.
143143+5) Input + audio in later increments.
144144+145145+## First Tangible Step
146146+147147+Create an API Parity Inventory and align with MEMORY_MAP.md (no code). Concretely:
148148+- 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).
149149+- Use `MEMORY_MAP.md` as the authoritative memory layout reference; where needed cross-check with `src/tic.h`/`src/core/*` for any inconsistencies.
150150+- 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.
151151+- 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`).
152152+153153+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.
154154+155155+## Open Questions to Resolve Early
156156+157157+- Exact Lua version expectations for cartridges (5.2 compatibility features relied upon?).
158158+- Required bit-exactness for audio vs. perceptual equivalence.
159159+- Which demos form the conformance baseline set and acceptable numeric tolerances.
160160+- Preferred desktop windowing stack (SDL2 vs. winit) for long-term maintenance.
161161+162162+## Appendix: Mapping Guide (C → Rust)
163163+164164+- `src/core/` → `tic-core` (state/tick), `tic-gfx` (draw), `tic-audio` (synth/mix), `tic-io` (io/input)
165165+- `src/api/` → `tic-api` (surface), `tic-lua` (language binding)
166166+- `src/system/sdl/` → `tic-sdl` (platform layer) or `tic-winit`
167167+- `src/cart.c`/`zip.c` → `tic-core` cart loader + `flate2`/`miniz_oxide`
168168+- FFT/VQT files per CLAUDE.md → `tic-fx`
169169+170170+---
171171+172172+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.
173173+174174+## GUI-First Kickoff (Fast Lane)
175175+176176+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:
177177+178178+- 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.
179179+- Audio + input crates to pair: `cpal` (audio output/capture), `gilrs` (gamepad), `winit` (keyboard/mouse), `arboard` (clipboard) as needed later.
180180+- 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.
181181+- Minimal VRAM: Implement only screen memory and palette mapping from MEMORY_MAP.md; defer palette map, border color, and vbank switching until later.
182182+- 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.
183183+- Then wire Lua: Load Lua script, call `BOOT()`/`TIC()`, and plumb `cls`/`pix` through the Lua binding to the framebuffer.
184184+- 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.
185185+186186+Suggested steps (no code yet):
187187+- 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).
188188+- 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).
189189+- Identify 2–3 micro-demos for visual verification (e.g., alternating `cls`, crosshair via `pix`, and a palette sweep).
190190+191191+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
···11+# Audio Analysis (FFT/VQT)
22+33+Detailed behavior, parameters, and equations live in `CLAUDE.md` at the repo root.
44+55+This page will summarize API-facing behavior (bins, windows, smoothing, normalization, whitening) and cross-link into the canonical derivations in `CLAUDE.md`.
66+
+19
docs/specs/graphics.md
···11+# Graphics Spec (Stub)
22+33+Scope
44+- Framebuffer: 240×136, 8-bit palette indices (0..15) per pixel.
55+- Palette: 16 sRGB entries; index→RGBA conversion for presentation; no color-space transforms.
66+- Text: Default font (5×8 advance within 8×8 glyph box), variable-width by trimming empty columns when `fixed=false`.
77+88+Semantics (to expand)
99+- `cls(color=0)`: Fill the framebuffer with palette index (masked to 0..15).
1010+- `pix(x,y[,color])`: Read returns current index or nil when OOB; write masks to 0..15 and ignores OOB.
1111+- `line/rect/rectb`: Integer rasterization; inclusive endpoints for lines; clipping to framebuffer bounds.
1212+- `print(text, x=0, y=0, color=15, fixed=false, scale=1, small=false) -> width`:
1313+ - Uses default font bitmap; top-left of first drawn column is `(x,y)`.
1414+ - Returns drawn width in pixels (pre-scale), consistent with TIC-80.
1515+1616+Open items
1717+- Document exact TIC-80 font bit packing and baseline to ensure parity.
1818+- Add rules for `clip`, `palette map`, `vbank`, `border`, and blitters.
1919+
+195
docs/specs/lua_api_parity.md
···11+# TIC-80 Lua API Parity Checklist
22+33+Authoritative references: MEMORY_MAP.md (root), src/api.h (TIC_API_LIST), src/api/luaapi.c.
44+Goal: ensure Rust implementation preserves names, signatures, return types, side effects, and timing.
55+66+For each API, we list the expected signature (per spec), key side effects, and the Rust subsystem owner.
77+88+## Callbacks
99+1010+- TIC: `TIC()`
1111+ - Effect: Per-frame tick at 60 FPS.
1212+ - Subsystem: tic-core (scheduler), tic-lua (callback wiring).
1313+- BOOT: `BOOT`
1414+ - Effect: One-time init after loading cart before first TIC.
1515+ - Subsystem: tic-core, tic-lua.
1616+- SCN: `SCN(row)`
1717+ - Effect: Scanline callback during draw; palette tricks.
1818+ - Subsystem: tic-gfx (scanline timing), tic-lua.
1919+- BDR: `BDR(row)`
2020+ - Effect: Border scanline callback; palette tricks.
2121+ - Subsystem: tic-gfx, tic-lua.
2222+- MENU: `MENU(index)`
2323+ - Effect: Game menu handler.
2424+ - Subsystem: tic-core (menu routing), tic-lua.
2525+2626+## Drawing
2727+2828+- print: `print(text x=0 y=0 color=15 fixed=false scale=1 smallfont=false) -> width`
2929+ - Effect: Draw text; returns width.
3030+ - Subsystem: tic-gfx.
3131+- cls: `cls(color=0)`
3232+ - Effect: Clear screen to color.
3333+ - Subsystem: tic-gfx.
3434+- pix: `pix(x y color)` / `pix(x y) -> color`
3535+ - Effect: Read or write pixel.
3636+ - Subsystem: tic-gfx (VRAM).
3737+- line: `line(x0 y0 x1 y1 color)`
3838+ - Effect: Draw line.
3939+ - Subsystem: tic-gfx.
4040+- rect: `rect(x y w h color)`
4141+ - Effect: Filled rectangle.
4242+ - Subsystem: tic-gfx.
4343+- rectb: `rectb(x y w h color)`
4444+ - Effect: Rectangle border.
4545+ - Subsystem: tic-gfx.
4646+- circ: `circ(x y radius color)`
4747+ - Effect: Filled circle.
4848+ - Subsystem: tic-gfx.
4949+- circb: `circb(x y radius color)`
5050+ - Effect: Circle border.
5151+ - Subsystem: tic-gfx.
5252+- elli: `elli(x y a b color)`
5353+ - Effect: Filled ellipse.
5454+ - Subsystem: tic-gfx.
5555+- ellib: `ellib(x y a b color)`
5656+ - Effect: Ellipse border.
5757+ - Subsystem: tic-gfx.
5858+- tri: `tri(x1 y1 x2 y2 x3 y3 color)`
5959+ - Effect: Filled triangle.
6060+ - Subsystem: tic-gfx.
6161+- trib: `trib(x1 y1 x2 y2 x3 y3 color)`
6262+ - Effect: Triangle border.
6363+ - Subsystem: tic-gfx.
6464+- 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)`
6565+ - Effect: Textured triangle (tiles or explicit texture), optional chroma table, optional map source.
6666+ - Subsystem: tic-gfx (texturing), tic-core (remap helper).
6767+- paint: `paint(x y color [bordercolor])`
6868+ - Effect: Flood fill (with optional border color).
6969+ - Subsystem: tic-gfx.
7070+- clip: `clip(x y w h)` and `clip()`
7171+ - Effect: Set/reset clipping rectangle.
7272+ - Subsystem: tic-gfx.
7373+- spr: `spr(id x y colorkey=-1 scale=1 flip=0 rotate=0 w=1 h=1)`
7474+ - Effect: Draw sprite or composite sprite region; supports flip/rotate/scale; optional colorkey.
7575+ - Subsystem: tic-gfx (sprite blitter), respects palette map and vbank.
7676+- map: `map(x=0 y=0 w=30 h=17 sx=0 sy=0 colorkey=-1 scale=1 remap=nil)`
7777+ - Effect: Draw map region to screen with optional remap callback.
7878+ - Subsystem: tic-gfx; remap callback via tic-lua.
7979+- font: `font(text x y chromakey char_w char_h fixed=false scale=1 alt=false) -> width`
8080+ - Effect: Draw text using font region in RAM; returns width.
8181+ - Subsystem: tic-gfx.
8282+8383+## Tilemap Access
8484+8585+- mget: `mget(x y) -> tile_id`
8686+ - Effect: Read tile id at map cell (x,y).
8787+ - Subsystem: tic-gfx (map RAM view), tic-core.
8888+- mset: `mset(x y tile_id)`
8989+ - Effect: Write tile id at map cell (x,y); persistent only after `sync()`.
9090+ - Subsystem: tic-gfx (map RAM view), tic-core.
9191+9292+## Input
9393+9494+- btn: `btn(id) -> pressed`
9595+ - Effect: Gamepad button state (held).
9696+ - Subsystem: tic-io (gamepad).
9797+- btnp: `btnp(id hold=-1 period=-1) -> pressed`
9898+ - Effect: Gamepad pressed/auto-repeat.
9999+ - Subsystem: tic-io (edge detection + repeat timing).
100100+- key: `key(code=-1) -> pressed`
101101+ - Effect: Keyboard key state (held); `-1` checks any.
102102+ - Subsystem: tic-io (keyboard).
103103+- keyp: `keyp(code=-1 hold=-1 period=-1) -> pressed`
104104+ - Effect: Keyboard pressed/auto-repeat.
105105+ - Subsystem: tic-io.
106106+- mouse: `mouse() -> x y left middle right scrollx scrolly`
107107+ - Effect: Mouse state.
108108+ - Subsystem: tic-io (mouse).
109109+110110+## Memory and Banks
111111+112112+- peek: `peek(addr bits=8) -> value`
113113+- poke: `poke(addr value bits=8)`
114114+- peek1/peek2/peek4: `peek1(addr)`, `peek2(addr)`, `peek4(addr)`
115115+- poke1/poke2/poke4: `poke1(addr value)`, `poke2(addr value)`, `poke4(addr value)`
116116+ - Effect: Read/write RAM/VRAM according to MEMORY_MAP.md; 1/2/4-bit and nibble addressing semantics preserved.
117117+ - Subsystem: tic-core (memory), tic-gfx (VRAM effects).
118118+- memcpy: `memcpy(dest source size)`
119119+- memset: `memset(dest value size)`
120120+ - Effect: Raw memory block ops across 96KB RAM.
121121+ - Subsystem: tic-core.
122122+- vbank: `vbank(bank) -> prev` or `vbank() -> prev`
123123+ - Effect: Switch active 16KB VRAM bank (0 or 1); returns previous bank.
124124+ - Subsystem: tic-gfx (VRAM pages), tic-core (state).
125125+- sync: `sync(mask=0 bank=0 tocart=false)`
126126+ - Effect: Copy between cart banks and runtime; respects mask (tiles, sprites, map, sfx, music, palette, flags, screen).
127127+ - Subsystem: tic-core (cart/runtime memory), tic-io (persistence).
128128+- pmem: `pmem(index value)` / `pmem(index) -> value`
129129+ - Effect: Read/write 256×u32 persistent slots; cart-hash keyed.
130130+ - Subsystem: tic-core (persistent store).
131131+132132+## Text/Console and System
133133+134134+- trace: `trace(message color=15)`
135135+ - Effect: Print to console (not screen) in color.
136136+ - Subsystem: tic-core (logger).
137137+- time: `time() -> ticks`
138138+ - Effect: Milliseconds since cart start (double); used for animation/timing.
139139+ - Subsystem: tic-core (timer).
140140+- tstamp: `tstamp() -> timestamp`
141141+ - Effect: Seconds since Unix epoch.
142142+ - Subsystem: tic-core (system clock proxy).
143143+- exit: `exit()`
144144+ - Effect: Return to console when TIC ends.
145145+ - Subsystem: tic-core (control flow).
146146+- reset: `reset()`
147147+ - Effect: Reset cart runtime (not process exit).
148148+ - Subsystem: tic-core.
149149+150150+## Audio
151151+152152+- sfx: `sfx(id note=-1 duration=-1 channel=0 volume=15 speed=0)`
153153+ - Effect: Play/stop SFX on channel; supports id -1 to stop; note as int or "C#-4" style.
154154+ - Subsystem: tic-audio (synth/mixer).
155155+- music: `music(track=-1 frame=-1 row=-1 loop=true sustain=false tempo=-1 speed=-1)`
156156+ - Effect: Start/stop music playback; -1 to stop; supports overrides.
157157+ - Subsystem: tic-audio (tracker).
158158+159159+## Analysis (FFT / VQT)
160160+161161+- fft: `fft(start_freq end_freq=-1)`
162162+- ffts: `ffts(start_freq end_freq=-1)`
163163+- fftr: `fftr(start_freq end_freq=-1)`
164164+- fftrs: `fftrs(start_freq end_freq=-1)`
165165+ - Effect: FFT magnitude queries (peak-normalized vs raw; smoothed vs raw) per CLAUDE.md; 1024 bins, 2,048-sample window ~21 FPS.
166166+ - Subsystem: tic-fx (FFT), tic-audio (capture buffer).
167167+- vqt: `vqt(bin)` / vqts/vqtr/vqtrs/vqtw/vqtsw/vqtrw/vqtrsw
168168+ - Effect: VQT magnitude queries (normalized/raw; smoothed/raw; whitened/non); 120 bins, 8,192-sample window ~5.4 FPS.
169169+ - Subsystem: tic-fx (VQT), tic-audio (capture buffer).
170170+171171+## Sprite Flags
172172+173173+- fget: `fget(sprite_id flag) -> bool`
174174+- fset: `fset(sprite_id flag bool)`
175175+ - Effect: Per-sprite flag bits 0..7.
176176+ - Subsystem: tic-gfx (sprite metadata in RAM), tic-core.
177177+178178+## Input Mapping and Menu
179179+180180+- MENU(index): see Callbacks
181181+ - Effect: Handle game menu actions.
182182+ - Subsystem: tic-core (menu controller).
183183+184184+---
185185+186186+Verification plan per API:
187187+- Signature/arity: Match src/api.h docs and Lua binding arity checks.
188188+- Return types: Match numeric vs boolean vs string; e.g., time() returns double.
189189+- Side effects: Test VRAM/memory deltas, audio start/stop, persistent pmem behavior.
190190+- Timing: Respect fixed step; SCN/BDR callbacks invoked with correct rows.
191191+- Memory map: All peek/poke/mem ops constrained to MEMORY_MAP.md ranges; vbank switching affects VRAM offsets.
192192+193193+Notes:
194194+- Deprecated: `textri` exists under BUILD_DEPRECATED; keep optional compat layer if needed.
195195+- Ensure `LUA_COMPAT_5_2`-equivalent behavior to match current build semantics for Lua.
+8
docs/specs/memory_map.md
···11+# Memory Map (Canonical Reference)
22+33+Canonical source: `MEMORY_MAP.md` at the repository root. This page exists only to link to it and add short guidance.
44+55+Notes
66+- Tests and specs must reference the root `MEMORY_MAP.md` to avoid drift.
77+- When documenting APIs that read/write memory (peek/poke/memcpy/memset/vbank/sync), link to the specific sections in `MEMORY_MAP.md`.
88+
+9
docs/testing/frame_hashes.md
···11+# Frame Hashes (Stub)
22+33+Purpose
44+- Provide deterministic signatures of VRAM and audio output for regression tests.
55+66+Conventions (to define)
77+- VRAM: byte order, region included (full 240×136 vs. viewport), palette influence.
88+- Audio: block size, windowing, channel mix policy, endian/format.
99+
+13
docs/testing/strategy.md
···11+# Testing and Validation Strategy
22+33+Layers
44+- API goldens: Unit tests per API including edge cases and errors.
55+- Frame hashes: Deterministic VRAM hashes per frame to compare against baselines.
66+- Audio blocks: Hash mixed audio buffers with tolerances for FP differences.
77+- Conformance carts: Automated headless runs comparing traces/hashes to reference.
88+- Fuzzing: `.tic` loader and selected APIs for robustness.
99+1010+Notes
1111+- Prefer headless tests with minimal dependencies.
1212+- Keep baselines small and documented; record how they are generated.
1313+
···5454 globals.set("rect", rect_fn)?;
55555656 // print(text, x=0, y=0, color=15, fixed=false, scale=1, small=false) -> width
5757- let fb_print = fb.clone();
5858- let print_fn = lua.create_function(move |_, args: MultiValue| {
5959- let mut text = String::new();
6060- let mut x: i32 = 0;
6161- let mut y: i32 = 0;
6262- let mut color: u8 = 15;
6363- let mut fixed = false;
6464- let mut scale: i32 = 1;
6565- let mut small = false;
5757+ #[derive(Default)]
5858+ struct PrintArgs {
5959+ text: String,
6060+ x: i32,
6161+ y: i32,
6262+ color: u8,
6363+ fixed: bool,
6464+ scale: i32,
6565+ small: bool,
6666+ }
66676767- for (i, v) in args.iter().enumerate() {
6868- match (i, v) {
6969- (0, Value::String(s)) => text = s.to_str()?.to_string(),
7070- (1, Value::Integer(n)) => x = *n as i32,
7171- (2, Value::Integer(n)) => y = *n as i32,
7272- (3, Value::Integer(n)) => color = (*n).clamp(0, 255) as u8,
7373- (4, Value::Boolean(b)) => fixed = *b,
7474- (5, Value::Integer(n)) => scale = (*n as i32).max(1),
7575- (6, Value::Boolean(b)) => small = *b,
7676- _ => {}
6868+ impl PrintArgs {
6969+ fn from_lua(args: &MultiValue) -> LuaResult<Self> {
7070+ let mut out = PrintArgs {
7171+ color: 15,
7272+ scale: 1,
7373+ ..Default::default()
7474+ };
7575+ for (i, v) in args.iter().enumerate() {
7676+ match (i, v) {
7777+ (0, Value::String(s)) => out.text = s.to_str()?.to_string(),
7878+ (1, Value::Integer(n)) => out.x = *n as i32,
7979+ (2, Value::Integer(n)) => out.y = *n as i32,
8080+ (3, Value::Integer(n)) => out.color = (*n).clamp(0, 255) as u8,
8181+ (4, Value::Boolean(b)) => out.fixed = *b,
8282+ (5, Value::Integer(n)) => out.scale = (*n as i32).max(1),
8383+ (6, Value::Boolean(b)) => out.small = *b,
8484+ _ => {}
8585+ }
7786 }
8787+ Ok(out)
7888 }
8989+ }
79909191+ let fb_print = fb.clone();
9292+ let print_fn = lua.create_function(move |_, args: MultiValue| {
9393+ let p = PrintArgs::from_lua(&args)?;
8094 let width = fb_print
8195 .borrow_mut()
8282- .print_text(&text, x, y, color, fixed, scale, small);
9696+ .print_text(&p.text, p.x, p.y, p.color, p.fixed, p.scale, p.small);
8397 Ok(width)
8498 })?;
8599 globals.set("print", print_fn)?;
+143
tic80_rust/tests/gfx_framebuffer_tests.rs
···11+use tic80_rust::gfx::framebuffer::{dimensions, Framebuffer};
22+33+fn fb_all_pixels_equal(fb: &mut Framebuffer, expected: u8) -> bool {
44+ let (w, h) = dimensions();
55+ for y in 0..(h as i32) {
66+ for x in 0..(w as i32) {
77+ if fb.pix(x, y, None) != Some(expected & 0x0F) {
88+ return false;
99+ }
1010+ }
1111+ }
1212+ true
1313+}
1414+1515+#[test]
1616+fn cls_fills_entire_buffer() {
1717+ let mut fb = Framebuffer::new();
1818+ fb.cls(7);
1919+ assert!(fb_all_pixels_equal(&mut fb, 7));
2020+}
2121+2222+#[test]
2323+fn pix_read_write_and_bounds() {
2424+ let mut fb = Framebuffer::new();
2525+ // in-bounds set/read
2626+ assert!(fb.set_pixel(0, 0, 0xFF)); // should mask to 0x0F
2727+ assert_eq!(fb.pix(0, 0, None), Some(0x0F));
2828+2929+ // out-of-bounds read
3030+ assert_eq!(fb.pix(-1, 0, None), None);
3131+ assert_eq!(fb.pix(0, -1, None), None);
3232+ let (w, h) = dimensions();
3333+ assert_eq!(fb.pix(w as i32, 0, None), None);
3434+ assert_eq!(fb.pix(0, h as i32, None), None);
3535+3636+ // out-of-bounds write via set_pixel reports false
3737+ assert!(!fb.set_pixel(-1, 0, 1));
3838+ assert!(!fb.set_pixel(0, -1, 1));
3939+ assert!(!fb.set_pixel(w as i32, 0, 1));
4040+ assert!(!fb.set_pixel(0, h as i32, 1));
4141+}
4242+4343+#[test]
4444+fn rect_fill_and_clipping() {
4545+ let mut fb = Framebuffer::new();
4646+ fb.cls(0);
4747+ // A rect that partially lies outside should clip
4848+ fb.rect(-5, -3, 10, 8, 9);
4949+ // Count colored pixels; expected area is clipped to [0,w) x [0,h)
5050+ let (w, h) = dimensions();
5151+ let x0 = 0i32.max(-5);
5252+ let y0 = 0i32.max(-3);
5353+ let x1 = ( -5 + 10).min(w as i32);
5454+ let y1 = ( -3 + 8).min(h as i32);
5555+ let expected = (x1 - x0).max(0) as usize * (y1 - y0).max(0) as usize;
5656+ let mut count = 0usize;
5757+ for y in 0..(h as i32) {
5858+ for x in 0..(w as i32) {
5959+ if fb.pix(x, y, None) == Some(9) {
6060+ count += 1;
6161+ }
6262+ }
6363+ }
6464+ assert_eq!(count, expected);
6565+6666+ // Fully out-of-bounds should do nothing
6767+ let mut fb2 = Framebuffer::new();
6868+ fb2.cls(2);
6969+ fb2.rect(-1000, -1000, 10, 10, 5);
7070+ assert!(fb_all_pixels_equal(&mut fb2, 2));
7171+}
7272+7373+fn count_color(fb: &mut Framebuffer, color: u8) -> usize {
7474+ let (w, h) = dimensions();
7575+ let mut c = 0usize;
7676+ for y in 0..(h as i32) {
7777+ for x in 0..(w as i32) {
7878+ if fb.pix(x, y, None) == Some(color & 0x0F) {
7979+ c += 1;
8080+ }
8181+ }
8282+ }
8383+ c
8484+}
8585+8686+#[test]
8787+fn line_basic_counts_and_endpoints() {
8888+ // Horizontal line
8989+ let mut hfb = Framebuffer::new();
9090+ hfb.cls(0);
9191+ hfb.line(0, 0, 10, 0, 3);
9292+ assert_eq!(hfb.pix(0, 0, None), Some(3));
9393+ assert_eq!(hfb.pix(10, 0, None), Some(3));
9494+ assert_eq!(count_color(&mut hfb, 3), 11);
9595+9696+ // Vertical line
9797+ let mut vfb = Framebuffer::new();
9898+ vfb.cls(0);
9999+ vfb.line(0, 0, 0, 7, 5);
100100+ assert_eq!(vfb.pix(0, 0, None), Some(5));
101101+ assert_eq!(vfb.pix(0, 7, None), Some(5));
102102+ assert_eq!(count_color(&mut vfb, 5), 8);
103103+104104+ // Diagonal-ish line and reverse should have same count = max(dx,dy)+1
105105+ let (x0, y0, x1, y1): (i32, i32, i32, i32) = (0, 0, 10, 7);
106106+ let expected = (x1 - x0).abs().max((y1 - y0).abs()) + 1;
107107+108108+ let mut a = Framebuffer::new();
109109+ a.cls(0);
110110+ a.line(x0, y0, x1, y1, 4);
111111+ assert_eq!(a.pix(x0, y0, None), Some(4));
112112+ assert_eq!(a.pix(x1, y1, None), Some(4));
113113+ assert_eq!(count_color(&mut a, 4) as i32, expected);
114114+115115+ let mut b = Framebuffer::new();
116116+ b.cls(0);
117117+ b.line(x1, y1, x0, y0, 4);
118118+ assert_eq!(b.pix(x0, y0, None), Some(4));
119119+ assert_eq!(b.pix(x1, y1, None), Some(4));
120120+ assert_eq!(count_color(&mut b, 4) as i32, expected);
121121+}
122122+123123+#[test]
124124+fn blit_to_rgba_maps_palette() {
125125+ let mut fb = Framebuffer::new();
126126+ fb.cls(0);
127127+ // set three sample pixels to known colors
128128+ fb.set_pixel(0, 0, 0); // black
129129+ fb.set_pixel(1, 0, 9); // orange
130130+ fb.set_pixel(2, 0, 15); // peach
131131+132132+ let (w, h) = dimensions();
133133+ let mut rgba = vec![0u8; (w * h * 4) as usize];
134134+ fb.blit_to_rgba(&mut rgba);
135135+136136+ // Helpers to read RGBA at (x,y)
137137+ let idx = |x: u32, y: u32| -> usize { ((y * w + x) * 4) as usize };
138138+139139+ // Known palette entries from framebuffer.rs
140140+ assert_eq!(&rgba[idx(0, 0)..idx(0, 0) + 4], &[0x00, 0x00, 0x00, 0xFF]);
141141+ assert_eq!(&rgba[idx(1, 0)..idx(1, 0) + 4], &[0xFF, 0xA3, 0x00, 0xFF]);
142142+ assert_eq!(&rgba[idx(2, 0)..idx(2, 0) + 4], &[0xFF, 0xCC, 0xAA, 0xFF]);
143143+}
+131
tic80_rust/tests/lua_api_tests.rs
···11+use std::cell::RefCell;
22+use std::rc::Rc;
33+44+use tic80_rust::gfx::framebuffer::{dimensions, Framebuffer};
55+use tic80_rust::script::lua_runner::LuaRunner;
66+77+fn run_lua(script: &str, ticks: usize) -> Rc<RefCell<Framebuffer>> {
88+ let fb = Rc::new(RefCell::new(Framebuffer::new()));
99+ let runner = LuaRunner::new(fb.clone(), script).expect("lua init");
1010+ for _ in 0..ticks {
1111+ runner.tick();
1212+ }
1313+ fb
1414+}
1515+1616+#[test]
1717+fn lua_cls_and_pix() {
1818+ let script = r#"
1919+ function BOOT()
2020+ cls(2)
2121+ end
2222+ function TIC()
2323+ pix(1, 1, 9)
2424+ end
2525+ "#;
2626+ let fb = run_lua(script, 1);
2727+ assert_eq!(fb.borrow_mut().pix(1, 1, None), Some(9));
2828+ assert_eq!(fb.borrow_mut().pix(0, 0, None), Some(2));
2929+}
3030+3131+#[test]
3232+fn lua_line_and_rect() {
3333+ let script = r#"
3434+ function BOOT()
3535+ cls(0)
3636+ end
3737+ function TIC()
3838+ line(0, 0, 2, 0, 5)
3939+ rect(0, 1, 3, 1, 6)
4040+ end
4141+ "#;
4242+ let fb = run_lua(script, 1);
4343+ let mut fbm = fb.borrow_mut();
4444+ // line
4545+ assert_eq!(fbm.pix(0, 0, None), Some(5));
4646+ assert_eq!(fbm.pix(1, 0, None), Some(5));
4747+ assert_eq!(fbm.pix(2, 0, None), Some(5));
4848+ // rect one row below
4949+ assert_eq!(fbm.pix(0, 1, None), Some(6));
5050+ assert_eq!(fbm.pix(1, 1, None), Some(6));
5151+ assert_eq!(fbm.pix(2, 1, None), Some(6));
5252+}
5353+5454+#[test]
5555+fn lua_print_width_marker() {
5656+ let script = r#"
5757+ function BOOT()
5858+ cls(0)
5959+ end
6060+ function TIC()
6161+ local w = print("AB", 10, 10, 1, false, 1, false)
6262+ pix(10 + w, 10, 14)
6363+ end
6464+ "#;
6565+ let fb = run_lua(script, 1);
6666+ let mut fbm = fb.borrow_mut();
6767+ // text drew something in the glyph area near (10,10)
6868+ let mut any = false;
6969+ for yy in 10..(10 + 8) {
7070+ for xx in 10..(10 + 8) {
7171+ if fbm.pix(xx, yy, None) == Some(1) {
7272+ any = true;
7373+ break;
7474+ }
7575+ }
7676+ if any { break; }
7777+ }
7878+ assert!(any, "expected some glyph pixels drawn near (10,10)");
7979+8080+ // marker at x+width somewhere on row y=10
8181+ let (w, _) = dimensions();
8282+ let mut found_marker = false;
8383+ for x in 10..(w as i32) {
8484+ if fbm.pix(x, 10, None) == Some(14) { found_marker = true; break; }
8585+ }
8686+ assert!(found_marker, "expected marker pixel with color 14 on row 10");
8787+}
8888+8989+#[test]
9090+fn lua_print_defaults_and_pix_read() {
9191+ let script = r#"
9292+ function BOOT()
9393+ cls(0)
9494+ end
9595+ function TIC()
9696+ local w = print("A")
9797+ -- Strict gating: require that at least one glyph pixel was drawn
9898+ -- near the origin using the default color (15), rather than probing (0,0).
9999+ local drawn = false
100100+ for yy = 0, 7 do
101101+ for xx = 0, 7 do
102102+ if pix(xx, yy) == 15 then drawn = true break end
103103+ end
104104+ if drawn then break end
105105+ end
106106+ if drawn then pix(w, 0, 7) end
107107+ end
108108+ "#;
109109+ let fb = run_lua(script, 1);
110110+ let mut fbm = fb.borrow_mut();
111111+ // default color drew some glyph pixels near origin
112112+ let mut any = false;
113113+ for yy in 0..8 {
114114+ for xx in 0..8 {
115115+ if fbm.pix(xx, yy, None) == Some(15) { any = true; break; }
116116+ }
117117+ if any { break; }
118118+ }
119119+ assert!(any, "expected some glyph pixels drawn near origin");
120120+ // verify the script marked (w,0) with 7 (we don't need to know w here)
121121+ // find first non-zero on row 0 after x=0
122122+ let (w, _) = dimensions();
123123+ let mut found = false;
124124+ for x in 1..(w as i32) {
125125+ if fbm.pix(x, 0, None) == Some(7) {
126126+ found = true;
127127+ break;
128128+ }
129129+ }
130130+ assert!(found, "expected a marker pixel with color 7 on row 0");
131131+}