Skip to content

Freewili2 port#6

Open
evaderkrub wants to merge 76 commits into
freewili:mainfrom
evaderkrub:freewili2-port
Open

Freewili2 port#6
evaderkrub wants to merge 76 commits into
freewili:mainfrom
evaderkrub:freewili2-port

Conversation

@evaderkrub

Copy link
Copy Markdown

ported to freewili2 test hardware. still needs button and usb keyboard support. loads carts from usb for now. basic games included in rom.

evaderkrub and others added 30 commits June 6, 2026 14:40
Takes the wili8c 120-cart benchmark from 58 to 111 passing, zero
regressions. Preprocessor (p8_preprocess.c, all covered by tests in
test_p8_preprocess.c — 123 passing):

- find_rhs_end: statement-keyword stops, digit->letter token boundaries,
  multi-target lists, `;` terminator, string-call statements (sfx8),
  jammed index operands (e+=d and 0or t[n]o+=...), never-empty-RHS guard
- find_lhs_start: segments cannot start with a digit (...+.8d*=k)
- Compound ops: |= &= ^= added; LHS emitted through transform_chars so
  PICO-8 ops inside indexes convert; RHS may continue on the next line
  (line join); ..= excluded from binop scan
- Short forms: mid-line while(c)stmt; condition extension past the close
  paren through binop chains (if (a==1) and b stmt); body-to-EOL with
  else-binding (nested short-ifs claim elses in stack order); `if (c) do`
  -> then with remainder processed; comment-after-cond = multi-line if
- Multi-line long strings: skip_long_bracket sentinel fixed (]] at EOL
  collided with the unterminated case); short-form bodies opening a
  multi-line [[string]] defer their `end` to the first newline outside
  strings (PICO-8's rule)
- if-do post-pass: keyword matching now requires ident boundaries
  (p.onend, return in strings no longer derail the scan); elseif forms
- Single-byte P8SCII glyphs (ROM carts) become the same _PG_XXXX
  identifiers as the UTF-8 path, so button/fill constants resolve;
  glyph bytes are identifier chars throughout the scanners
- `?` / @ peek emissions separate from preceding tokens (4?e, return@n)

Runtime (p8_api.c, p8_cart.c):

- CRASH FIXES: bulk ord()/peek/peek2/peek4 pushed n values without
  luaL_checkstack (stack corruption -> luaD_reallocstack AV);
  reload() on .p8.png ran the text section parser over raw PNG bytes;
  memcpy/memset negative length when dest/src >= 0x8000;
  p8_coresume captured nargs after pushing resume (garbled args)
- PICO-8 leniency: nil->0 / bool->0/1 / non-numeric-string->0 for all
  numeric API args (p8_num); tonum(bool)=1/0; tostr(v,true)=hex;
  split(nil)={}; ord(nil)=nil
- Compat globals: holdframe/_set_fps no-ops, pack/unpack, inext
- Game loop: enter when _update/_draw are assigned INSIDE _init;
  flip() always polls input (title-screen wait loops depend on it)
- lua_parse.c: standalone binary-safe Lua 5.4 parse checker for
  debugging preprocessed cart output

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… doc math

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Loading snapshots vram into the cart ROM; the REPL run (no-arg) executes
the editor text buffer over a ROM copy so unsaved edits are live;
reload() restores from the ROM (file-based path kept for explicit
filenames). Verified: full 120-cart sweep 111/120, failing set identical
to baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…docs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tab switch

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… sheet nav

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gh zoom)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…race

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tab toggles the full-screen 1:1 sheet view (122 of 128 rows visible,
wheel scrolls the 6-row hidden band). Pencil paints at 1:1 with undo;
any other tool click jumps to that sprite and returns to normal view.
Ctrl+Z/Y (undo/redo) work inside the fullscreen branch via ed_kp.
Normal view: Ctrl+Z and Ctrl+Y changed from ed_kp_once to ed_kp for
hold-to-repeat undo/redo (PICO-8 parity).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ear,

selection drag-move (move/stamp parity)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Extend ED_TAB_* enum to 4 tabs (code, gfx, sfx, music)
- Add p8_editor_sfx and p8_editor_music stub modules with placeholder draw
- Wire new modules into p8_editor_shell_enter; shorten cart-name field to 8 chars to clear 4-tab strip
- Add K_B/D/E/G/H/J/M/N/T/U/1-8/SPACE/MINUS/EQUALS/ENTER/COMMA/PERIOD/LBRACKET/RBRACKET keycodes to p8_editor_internal.h
- Call p8_editor_sfx_reset()/p8_editor_music_reset() after p8_editor_gfx_reset() in both p8_cart_load and p8_cart_run

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the Task 3 stub with a full pitch-mode SFX editor: control row
(sfx select, speed, loop markers, play/stop), 32-column pitch lane (drag to
draw/erase notes), volume lane, loop marker lines, live audio playhead, and
keyboard shortcuts. Tracker mode and undo hooks are stubbed for Tasks 4/5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the broken bottom-row special-case formula with a clean
monotonic mapping: vol = (VOL_Y + VOL_H - 1 - my) / 2.  Each
volume step is 2 px; the full 0..7 range is now reachable without
gaps (previously vol=1 was skipped — my=104..105 mapped to 2 then
my=106..107 jumped to 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
32-row two-column tracker view with note/wave/vol/fx columns, piano-key
note entry, digit column editing, row-highlight playhead, and mouse click
cursor. Mode toggle migrated from M to Tab so piano map owns M.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a 32-slot audio undo ring snapshotting the full music+sfx region
(0x3100-0x42FF, 0x1200 bytes) so the music editor (Task 6) can share it.
Push sites: pitch/vol lane drag-start, speed click (LMB+RMB), loop-start
click (LMB+RMB), loop-end click (LMB+RMB), keyboard speed (pitch-mode
K_MINUS/K_EQUALS, tracker-mode K_LBRACKET/K_RBRACKET), tracker piano
entry, tracker digit edit, tracker Del. Ctrl+C/V copy/paste whole SFX;
Ctrl+Z/Y undo/redo. Public API declared in header for Task 6 reuse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the Task 6 stub with the real pattern editor: 4-channel rows
with enable toggle, sfx-index adjust, loop/stop flags, play preview
(syncs ROM→RAM, drives p8_music_play), Enter→sfx tab jump via the new
p8_editor_shell_request_tab() deferred mechanism, and clipboard (Ctrl+C/V
on 4-byte pattern blocks). Undo ring shared with p8_editor_sfx. Shell
gains static requested_tab + p8_editor_shell_request_tab() applied after
topbar_click each frame so switch_tab() is always in scope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Extend ED_TAB_* enum to 5 tabs (add ED_TAB_MAP=4, ED_TAB_COUNT=5)
- Add stub p8_editor_map module (placeholder draw, no-op reset)
- Replace text tab labels with 5x5 icon glyphs in the top bar
- Shorten cart-name truncation to %.5s to clear the wider tab strip
- Wire p8_editor_map_module() and p8_editor_map_reset() into the shell
  and cart-load path (both p8_cart_load and p8_cart_run sites)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the Task 2 stub with the full core: 16×10 tile viewport,
screen-anchor pan, draw/erase with left/right mouse, coordinate
readout, tool strip (draw/pan/fill/select/stamp buttons), and a
64-sprite sheet picker with paging.  State declarations for fill,
select, stamp, and clipboard (Tasks 4-5) included but handlers deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add TOOL_FILL (flood-fill on click) and TOOL_SELECT (drag to define
rectangular region) handlers inside the viewport-tools block, plus
Ctrl+C/X/V clipboard operations using map_read_region/map_write_region.
Cut copies then clears to tile 0; paste lands at cursor cell (cur_cx,cur_cy).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Picker now supports drag-select of an N×M block; TOOL_STAMP paints the
full block onto the map using row-major sprite indexing (cur_tile + y*16 + x).
Viewport shows a colour-10 footprint outline for multi-tile blocks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a top-level release guard at the end of map_input that clears both
sel_dragging and blk_dragging on any left-button release, preventing
state leaks when the mouse exits the active region while held.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
evaderkrub and others added 30 commits June 8, 2026 20:40
wili8c has two print implementations: console_print (REPL, registered at
boot) and p8_print (screen draw, swapped in just before the game loop via
p8_register_print). console_print's screen-draw branch — taken when called
as print(str,x,y,c) — did `gfx_print(str,x,y,c&0xF)` with the RAW color and
no camera, while p8_print applies p8_draw_pal[] and the camera offset.

These usually agree because direct `print(...)` calls in _draw hit the
swapped-in p8_print. But a cart that captures `print` as a VALUE during load
(before the swap) holds the console_print reference forever:

  multi_print = string_caller(print, split)   -- beam4-3

Every multi_print(...) then drew text with pal()/camera() silently ignored.
On beam4-3's start screen the "hOLD / TO sTART" prompt is printed in pen
color 1 under pal(1,7); direct print would show white, but the captured
print showed raw color 1 (dark blue), and any camera offset was dropped too.

Fix: console_print's (str,x,y[,c]) branch now delegates to p8_print (made
non-static, declared in p8_api.h) — one implementation, so captured and
direct print behave identically. Verified: a capture-vs-direct probe cart
(both white under pal(1,7)) and a faithful beam4-3 menu repro (prompt now
white, matching PICO-8).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A fillp() pattern combined with a two-nibble draw color (e.g.
rectfill(0,0,127,127,0x76)) should dither between the low-nibble color (where
the pattern bit is 0) and the high-nibble color (where it is 1). wili8c only
ever drew the primary color and treated pattern bits purely as transparency,
so two-color dithers collapsed to a solid fill — dungeonclicker-5's textured
title background rendered as flat magenta.

Add p8_fill_resolve(x,y,c): picks primary/secondary/transparent per pixel, and
use it in rectfill's pattern path. Also fix the pattern bit order to match
PICO-8 (16-bit pattern read MSB-first, bit 15 = top-left), via a shared
p8_fill_bit() helper now used by p8_fill_check() too.

Verified: dungeonclicker-5's background now renders the two-shade cross-hatch
matching PICO-8 (was solid). (circfill/line/hspan still primary-only; the
secondary color there can follow if a cart needs it.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PICO-8 P8SCII supports scaled and decorated text via \^ commands, which carts
use for stylized titles. wili8c's command table mishandled them: \^w and \^t
consumed a following byte (treated as a "delay" param), so e.g. dungeonclicker-5's
title "\^o8ff\^w\^td\^-tungeon" had \^w eat the \^t prefix, collapsing the whole
overlapping-glyph composition into the literal "8FFTD TUNGEON".

Implement the real semantics (verified against PICO-8 with a grid probe):
  \^w  wide text on  (2x glyph width, set-on, persists across \n)
  \^t  tall text on  (2x glyph height)
  \^o  outline on    (glyph silhouette stamped in the decoration color)
  \^-  move cursor left one char (was incorrectly moving right)
  \^|  move cursor up one row (was unhandled)
  \n   line height scales with tall mode
Add draw_glyph4() to render 4x6 glyphs scaled by (sw,sh) with an optional 8-way
outline; the ASCII path and gfx_text_width() honor the scale. Normal text
(no \^w/\^t) renders byte-for-byte as before, so unscaled text is unaffected.

Result: dungeonclicker-5's title renders as large outlined "DUNGEON CLICKER"
(was garbled literal text). The hand-tuned drop-cap overlap is not yet pixel-
exact vs PICO-8 (exact cursor/anchor math undocumented), but the words read and
the layout matches closely.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dc-4)

The `\^o` outline command takes two arguments — a foreground color hex
digit and a two-hex-digit direction mask — but gfx_print_w's handler
consumed neither. The three parameter characters were then printed as
literal text: `\^od50`/`\^o6ff` rendered the string "d50"/"6ff", which
in the 4x6 font reads as the garbled "DSO" seen across every menu label
in tad_full_release_dc-4 (and "15A", red title overlays, etc. in other
carts). The intended outline colour/direction was also never applied.

Parse the color digit + 2-hex mask in the `\^o` handler and thread the
mask through draw_glyph4 so the silhouette is only stamped in the
selected directions. The mask bits map to the 3x3 neighbourhood in
reading order (bit0 TL .. bit7 BR), matching PICO-8: 0xff = full halo,
0x50 = R|B (down-right bevel), 0x40 = B. draw_glyph4's existing dxs/dys
order already matches this bit ordering.

Font table verified correct against the CC-0 PICO-8 font (yocto-8): the
symbol glyphs were never the problem. tad now pixel-matches the PICO-8
reference; also resolves the literal-param text garbage in
observer_bounceharvest-0 and rock_paper_sweeper-3. Carts not using `\^o`
are byte-for-byte unaffected; rynsfirsttry-0 and utkadd42-0 (prior
known-good `\^o` users) still render cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
p8_tonum gated its pass-through on lua_isnumber(L,1), which is also true
for numeric STRINGS ("1","0.25") under Lua's string->number coercion. So
tonum("1") took that branch and lua_pushvalue returned the original STRING
unchanged, i.e. type(tonum("1"))=="string". Arithmetic silently coerces the
string so most uses looked fine, masking it; it only bites on comparisons
(Lua does NOT coerce there): tonum(x)>0.5 throws "attempt to compare number
with string". Real PICO-8 always returns a number.

Gate the pass-through on the type instead (lua_type==LUA_TNUMBER); numeric
strings then fall through to the existing strtod/strtol path and return real
numbers. Verified with a probe cart (tonum("1") type string->number;
tonum("1")>0.5 now works) and full 120-cart bench sweep (zero regressions).

Root cause of mouse_required-1 rendering fully black: its call[[...]] DSL
stores tonum(token) as slider values; a settings-slider setter does v>0.5
in _init, which threw and aborted the cart before any draw. (The cart's
remaining PICO-8 mismatch is a separate, unimplemented feature: the 64x64
screen mode poke(0x5f2c,3).)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PICO-8 carts recolor via the DISPLAY palette with secret colors 128-143
(pal(tbl,1) or pal(c0,c1,1)). wili8c masked every pal() target & 0xF, so
those colors fell back to wrong indices - e.g. libryinth-10 rendered a solid
pink screen, alienpico_space-6's outlined title was the wrong color.

Add the 16 secret colors as palette[16..31] (RGB565) and let the DISPLAY
palette (p==1 only) accept targets 128-143, mapped to slots 16-31. gfx_flip
already indexes palette[disp_pal[...]], so widening the table is enough on the
display side. The draw palette and the 4-bit framebuffer stay 0-15 - secret
colors are display-time-only - so non-secret carts hit identical code paths.

Verified: alienpico_space-6 now pixel-matches PICO-8; libryinth-10 /
soulbound_recursed-1 / observer_bounceharvest-0 now render their secret colors
(remaining per-cart issues - missing title logos, poke-based screen palette -
are separate). sweep120: 0 regressions (116/120 pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PICO-8's "\^" special-command escape is chr(6); wili8c's preprocessor emits the
equivalent chr(127), but carts that put a raw chr(6) byte in a print string (e.g.
libryinth-10's `print("\14\6x9\6o"..n.."ffstart game")`) hit wili8c's "bytes 1-15
set color" branch, so the command letters (x9, o2ff, ...) printed as literal text.

Exclude chr(6) from the color-byte branch and route it through the same command
handler as chr(127), in both gfx_print_w and gfx_text_width. This matches PICO-8
semantics, so chr(127)-form carts are unaffected.

Result: libryinth-10's "DEMO"/"START GAME" render as clean outlined text (was
"x9o2ffstart game"); rock_paper_sweeper-3 now pixel-matches PICO-8. sweep120: 0
regressions (116/120). libryinth still has a separate oversized-title-art issue
(animated camera/position) tracked in RALPH_QUEUE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes that together render cart-supplied bitmap fonts:

1. poke4 overflow (the blocker the offscreen-font carts actually hit): poke4 did
   `(int32_t)(n*65536)`, which is signed-overflow UB for values whose 16.16 raw
   pattern exceeds INT32 (e.g. cattlecrisis-1's `poke4(0x5f55,0x8180.208)` ->
   0x81802080). It collapsed to 0x80000000, so the cart's draw-target (0x5f55) and
   font-enable (0x5f58) bytes came out wrong and the font/redirect never engaged.
   Now go through int64 and wrap to uint32. Also call apply_draw_target() when a
   poke4 covers 0x5f55 (match p8_poke), needed for offscreen rendering.

2. Custom font: when 0x5f58 bit0 is set, gfx_print_w / gfx_text_width render every
   printable byte from the cart's 0x5600 glyph sheet (header: width @0x5600,
   wide-width @0x5601, height @0x5602, signed x/y offsets @0x5603-4; glyph for char
   c at 0x5600 + c*8, 8 bytes, bit0 = leftmost). gfx reads p8_mem via a new
   gfx_set_p8_mem() hook. Strictly gated on 0x5f58 bit0, so carts not using a custom
   font hit identical code paths (zero regression).

Result: cattlecrisis-1's "CATTLE CRISIS!" logo (rendered offscreen to 0x8000 with
the custom font, then spr-blit) now renders, matching PICO-8. sweep120: 0
regressions (116/120). observer_bounceharvest-0 has a separate screen-palette issue
and is NOT fixed by this.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PICO-8 0x5f2c screen mode 3 = 64x64 resolution: only the top-left 64x64 of the
framebuffer is shown, zoomed 2x to fill the 128x128 display. gfx_flip detects it
via g_p8_mem[0x5f2c] and nearest-scales the source quarter; mode 0 (and the
flip/rotate modes) take the original 1:1 path unchanged, so other carts are
byte-identical.

mouse_required-1 (poke 0x5f2c,3 = "64x64 resolution") now zooms correctly instead
of rendering in the corner. It still needs devkit-mouse (stat 32-34 / 0x5f2d) to
fully match PICO-8's title state — tracked separately. sweep120: 0 regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PICO-8's `\^@<4hex addr><4hex len><len raw bytes>` pokes a byte stream into memory
mid-print. Carts use it to blit pre-baked screens/draw-state without sprite/map
assets. hotel_for_sale-4 draws its whole title this way: 90 pokes (the 6KB title
bitmap to 0x6000, plus display palette to 0x5f10, draw palette, clip, camera).

gfx_print_w parses `@` and calls a new p8_print_poke() in p8_api.c, which writes
p8_mem and re-syncs the cached draw-state registers the write covered (draw palette
0x5f00, display palette 0x5f10 with secret-color 128-143 mapping, clip 0x5f20,
camera 0x5f28, draw-target 0x5f55) — wili8c keeps those in C state, not p8_mem, and
poke() only special-cased 0x5f55. gfx_text_width consumes the command as 0-width.
Gated on chr(6)/chr(127)+'@', so non-\^@ carts are unaffected.

Result: hotel_for_sale-4's hotel scene (building, signs, character, car) now renders
correctly with the right colors, instead of pink noise + hex-address garbage. The
"PRESS X/C TO START" text overlay still mis-positions (separate issue: the P8SCII
cursor commands \^j/chr(3)/chr(4) need correct arg-consuming semantics). sweep120:
0 regressions (116/120).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
- WILI8_TARGET CMake switch (fruitjam default | freewili2); custom board header
- board_init: 250MHz @1.25V, clk_peri re-source, LoRa CS parked, backlight GPIO
- PCAL6524 ioexp: releases shared LCD reset, routes backlight, hub-power helper
- SEGGER RTT diagnostics (USB host-mode -> no CDC/UART stdio)
- main_freewili2: boot + backlight blink liveness

Verified on hardware: builds, flashes via OpenOCD, RTT shows 250MHz + ioexp ACK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
- Vendored st7796.c/.h + font5x7 (flat fw2/ include layout)
- display_fw2: 128x128 -> 256x256 2x scale + big-endian swap, ping-pong DMA strips
- PWM backlight dimming on GPIO25 (board_backlight_set now 0-255)
- main: PICO-8 palette test pattern (colour bars + diagonal X + border)

Verified on hardware: camera shows the test pattern correctly on the panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
- Link gfx.c (engine renderer) + lua54; engine_stubs.c for p8_print_poke/p8_sfx_play
  (only hit by rare P8SCII escapes; real impls land with full engine in Phase 3)
- main draws a scene via gfx_cls/print/rect/circfill/flip, presents dvi buffer
- MADCTL 0xEC (180deg) so text reads naturally; final orientation TBD with user

Verified on hardware: 'ST7796 + GFX OK' + scene render correctly via camera.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
- APS6404 8MB PSRAM (psram_fw2) -> TLSF Lua heap + 4MB FatFS ramdisk
- FatFS over PSRAM ramdisk (diskio_fw2, volume 0:); USB = volume 1: (Phase 4)
- Full portable engine linked (p8_api/cart/sfx/editor/png/preprocess + gfx + input)
- tusb.h shim + host_stubs (tuh_task) so input/editor link w/o TinyUSB
- audio_fw2 silent seam (Phase 6 adds NAU88C10); p8_set_present_cb hook in gameloop
- carts_rom: baked-in demo cart; mkfs ramdisk + write carts at boot
- ffconf: FF_USE_MKFS=1, FF_VOLUMES=2

Verified on hardware (RTT + camera): 8MB PSRAM, ramdisk mkfs+mount, cart loads
and runs _update/_draw; camera shows animated ring (frame-diff 23/255 = live).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
- MADCTL back to reference 0x2C: the panel content is correct for the device;
  the earlier 'upside down' was the board's physical placement in the camera
  (camera captures are rotated 180 in tooling for readability)
- carts_rom: add bounce + plasma demos (3 baked carts total)
- main: auto-cycle ROM carts ~10s each via p8_set_loop_break_cb timebox
  (interim until the touch-driven picker in Phase 5)
- host_stubs: route newlib _write to RTT

Verified on hardware: 3 carts written to ramdisk, cycle demo->bounce->plasma;
camera (rotated) shows 'bounce' right-side-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
- Vendored native RP2350 USB-MSC host stack (usb_core/hcd/hub/msc/parse, no TinyUSB)
- diskio_fw2 pdrv 1 -> usb_msc block API (FatFS volume 1:)
- main: ioexp_hub_power(true) + usb_msc_init + wait-ready + f_mount 1:; scan
  1:/carts and 1:/ for .p8/.p8.png; cart list = USB carts + ROM carts, auto-cycle;
  usb_msc_task() pumped each frame via the loop-break hook (hotplug-safe)
- Build: -Os on the freewili2 target (copy_to_ram keeps image in 520KB SRAM;
  -Os freed ~65KB: text 446->382KB, total 458KB, ~54KB headroom) so USB fits

NOT yet flashed/verified on hardware (device in use). Drive prepared at E:\carts
(3 demo + 5 real carts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
Used to verify Phase 4 USB: after reset the firmware re-enumerates the drive at
boot. Phase 4 confirmed on hardware: USB-MSC drive enumerated (240M blocks),
8 carts found in 1:/carts, real airfutsal-0.p8.png loads from USB and renders.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
- audio_fw2.c: streaming double-buffer DMA + IRQ refill feeding p8_sfx_mix_sample
  to the I2S PIO; free-running RX-drain DMA (duplex PIO autopushes); 22050 Hz
- Vendored codec_nau88c10.c/.h + i2s_duplex.pio (pico_generate_pio_header)
- codec SMPLR set to 24kHz (nearest to wili8's 22050); bring-up order mirrors the
  proven reference (codec init -> dac_mute(false) -> speaker route -> I2S -> DMA)
- ROM carts now first in the cycle (instant boot visuals); audiotest.p8 tone cart

Verified on hardware: user confirms steady tone from the onboard speaker at 22050.
(Webcam mic couldn't pick up the speaker; engine peak=23296 confirmed digital path.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
… (verified)

- Vendored proven usbcamfw FT6336U driver (ft6336.c/.h, I2C1 @0x38)
- touch_input.c: draws on-screen d-pad (left panel) + O/X (right panel); maps a
  single touch to one PICO-8 button via input.c virtual gamepad keycodes 0xF0-0xF5
- polled each frame in the game-loop break hook (before _update reads btn)
- inputtest.p8 ROM cart (cursor moved by d-pad, labels on O/X)

Verified on hardware: user confirms tapping the on-screen d-pad moves the cursor
and O/X respond correctly; no orientation flip needed. FT6336U id=0x64.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
Phase 7.1. A full-screen ST7796 menu (src/fw2/picker.c) lists ROM + USB
carts in a paged, finger-tappable list (title bar, up to 7 rows/page,
PREV / page / NEXT footer). A fresh FT6336U touch on a row launches that
cart; the footer pages. main_freewili2.cpp now loops picker -> run cart ->
return, instead of the 10 s auto-cycle.

Exiting a running cart: touch_input.c gains an on-screen MENU button
(top-left, above the d-pad) and a menu-requested flag; the game-loop break
hook returns true on a MENU tap, dropping back to the picker. draw_controls
is now public (touch_input_draw_controls) so main can repaint the in-game
controls after the picker overwrites the panel.

Verified on hardware (user taps): tap-to-launch, MENU exit, and PREV/NEXT
paging all work; RTT shows 13 carts across 2 pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
Phase 7.3. pico_stdlib --wraps printf/puts/putchar into the SDK's stdio
layer, which bypasses the newlib _write shim in host_stubs.c. With
stdio-usb/uart disabled (USB is host-mode) no SDK driver was registered,
so the engine's printf diagnostics (p8_cart load errors, "Game loop @
Nfps", etc.) were silently dropped.

host_stubs.c now registers a stdio_driver_t whose out_chars writes to
SEGGER_RTT channel 0; main_freewili2.cpp enables it at boot via
fw2_stdio_rtt_init(), alongside SEGGER_RTT_Init(). _write is kept for any
non-wrapped newlib path.

Verified on hardware: launching carts now logs "Loading cart: <path>" and
"Game loop: _update @ 30fps" (engine printf) over RTT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
Phase 7.6 (cosmetic). reg 0x07 SMPLR is set to 0x0004 = 24 kHz (nearest to
wili8's 22050 Hz), but the init DIAG and the file header comment still said
"16 kHz" from an earlier rate. Correct both strings; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
Phase 7.4 (perf/fps pass). Added an RTT frame profiler in present_frame
(FW2_PERF_LOG): every 60 frames it logs avg blit time, avg frame period,
and implied fps.

Measured on hardware:
  - ST7796 blit = ~20.1 ms/frame, constant (128 KB at 62.5 MHz SPI, DMA-
    bound). SPI can't go faster: even-only SSP dividers off the 250 MHz
    clk_peri give 62.5 MHz (125 MHz exceeds the panel's proven 100 MHz max),
    and overlapping the blit would need a 128 KB double-buffer that doesn't
    fit the ~50 KB free SRAM.
  - Typical carts (airfutsal) hold a clean 33.3 ms / 30.0 fps.
  - Heavy carts (cast.p8 raycaster) run 50-100 ms/frame with the SAME 20 ms
    blit -> the cost is Lua _update/_draw, i.e. interpreter-bound, inherent
    to PICO-8-on-RP2350; not a display bottleneck.

Confirmed the earlier input-frame dips were NOT the blit: they persisted
with touch logging off and tracked cart CPU, not touches. The per-frame
touch DIAG (Phase-5 bring-up debug) is now gated behind TOUCH_DEBUG_LOG
(off) so its bursty RTT traffic no longer clutters logs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
Phase 7.5a. The cart list was built once at boot; now the picker re-scans
when the USB drive is plugged or unplugged while the menu is showing.

main_freewili2.cpp: extracted rebuild_cart_list() (ROM carts on 0: always,
then USB carts on 1: if mounted) and cart_list_poll(), which services the
USB host each picker loop and, on a ready-state transition, mounts/unmounts
volume 1: and rebuilds the list, returning true so the picker redraws.
usb_host_boot() keeps the brief boot-time enumeration window.

picker_run() now takes the live count by pointer and a bool-returning poll
hook; when poll reports a change it re-clamps the page and repaints.

Verified on hardware: boot shows 13 carts (5 ROM + 8 USB); unplugging the
drive redraws the picker to 5 ROM carts, replugging restores 13.

Editor-save-to-USB (Task 5b) is deferred: the FW2 has no keyboard to drive
the on-device editor, and the save path already routes through FatFS to the
USB volume when a 1:/ cart is loaded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
Phase 7.2. ROM carts are baked into the copy_to_ram image (520 KB SRAM cap,
~45 KB free), so the built-in set is the offline fallback; real third-party
carts live on the USB drive. Embedded wiliwaves.p8 (the project-original
demo, 5.4 KB) via xxd -i into carts_rom_wiliwaves.h and added it as the
first ROM cart. Dropped the redundant bounce/plasma inline demos; kept the
demo showcase + inputtest/audiotest diagnostics.

The carts/ example set is otherwise mostly zep's PICO-8 demos with no
explicit redistribution license (jelpi is CC-BY-NC-SA but 83 KB; celeste2
is copyrighted), so only the license-clean wiliwaves is baked. The ~100
tested bench carts are .p8.png at 10-40 KB each and belong on USB, not ROM.

Verified on hardware: wiliwaves launches and animates from the picker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
Three intertwined additions (shared touch_input/picker/main files):

1. Eight original FreeWili2 carts baked into ROM (carts_fw2/*.p8 embedded via
   xxd -i -> carts_rom_custom.h): games flap/snake/dodge/whack and demos
   fw2splash/starfield/metaballs/visualizer. All procedural, license-clean,
   ~7 KB total. MAX_CARTS 48 -> 64. Each verified on the SDL build (bench: no
   errors; shot: renders) before flashing.

2. Game-area touch -> PICO-8 mouse (touch_input.c): a touch inside the centered
   256x256 image maps to PICO-8 coords (0..127) and feeds the engine mouse
   (stat 32/33/34) via input_mouse_update with the left button held. Makes whack
   tap-the-mole; unblocks any mouse cart on the device. Side panels still drive
   the d-pad/O/X buttons.

3. On-device cart code viewer (codeview.c/.h): a read-only, touch-scrollable
   Lua source view (top bar + body + UP/DOWN/BACK). Reachable two ways:
   - Picker: each row gets a right-edge "<>" CODE zone; tap it to view that
     cart's source (p8_cart_load, no game loop -> p8_editor_text), tap the body
     to launch. picker_run reports launch-vs-code via an out-param.
   - In-game: a CODE button top-right (mirror of MENU) sets a flag the loop-break
     hook reads; it shows the source inline (cart paused, Lua state intact) and
     resumes in place on BACK.

Verified on hardware (user): whack registers on-screen taps; all 8 carts run;
picker <> and in-game CODE both open the source and return cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
Exiting a cart (MENU) or the code viewer returned to the top of the list.
Now picker_run takes a `focus` index (the last cart tapped, tracked in main):
it opens on that cart's page and highlights the row (teal bg + yellow left
stripe), so you land back where you were instead of page 1. focus<0 = top.

Verified on hardware: launching from a later page and exiting returns to that
page with the cart highlighted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
The picker reuses one lua_State for every cart, so p8_cart_run ->
p8_cleanup_globals is the only thing resetting the global namespace
between carts. It was broken two ways:

1. p8_snapshot_globals(L) was never called in main_freewili2.cpp (only
   in the desktop main.cpp). With no snapshot in the registry,
   p8_cleanup_globals hits its `if (!snapshot) return;` guard and is a
   no-op -- every cart inherited the previous cart's globals.
2. The snapshot stored `true` markers, so cleanup could detect builtins
   but never restore one a cart overwrote. A cart that clobbers a
   builtin (e.g. using `t` as a number shadows builtin t()/time()) left
   the bad value for the next cart.

Symptom: play dino_sort-1 (which does `t = mget(...)`), then run cast ->
cast errors "attempt to call a number value (global 't')" at its t()
call -> game loop breaks -> returns to picker. Not a memory issue; heap
was stable ~3.9 MB.

Fix: call p8_snapshot_globals(L) after all API/module registration in
main_freewili2.cpp; store snapshot[key]=value and restore every builtin
to its snapshot value in p8_cleanup_globals (handles overwrite and nil).

Verified on HW: dino->cast clean; 9-step interleaved sweep
(dino/cast/snake/metaballs/flap) all rc=0, zero errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
The README only covered the Fruit Jam build. Add the FreeWili 2 backend:
dual-target intro, a Targets comparison table (display/audio/input/storage/
UI/flash/build target), a FreeWili 2 build+flash section (tools/fw2 scripts,
SWD via OpenOCD, -DWILI8_TARGET=freewili2), and source-file entries for
src/main_freewili2.cpp and src/fw2/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SArJkymAka5WBypS2mybKj
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant