From 93c1fb62e9d21569c5bc3f47a04afd3f3c31ee49 Mon Sep 17 00:00:00 2001 From: Me Here Date: Sat, 30 May 2026 08:25:19 -0500 Subject: [PATCH] PM_K-1 0.0.18: split post-seam redraw -- fast bits now, heavy build_grid deferred 0.0.17 fixed the off-by-one so A plays its full final bar, but B's first beats were still being eaten -- by build_grid + draw_log blocking the main loop for ~hundreds of ms right at the seam (a full grid teardown + ~64-128 vectorio rectangle allocations + draw_log text bitmaps + an SPI refresh). tick() doesn't run during that, so B's first master steps fire only when the catch-up while-loop runs at the next tick, collapsing into one click. Split the post-seam refresh: - Fast pass: draw_meters / draw_bpm / draw_status / draw_train. Cheap (each just swaps one TileGrid). Runs immediately so the new title + bpm + status are correct on screen. - Heavy pass: build_grid + draw_log. Deferred by 0.6s after the seam, gated by self._heavy_redraw_at. B's intro plays clean; the grid catches up half a beat or so into B with a single brief jitter (rather than B's first 1-2 beats being lost). Plus gc.collect() at the start of build_grid AND draw_log. Both allocate enough small objects that a fragmented heap will MemoryError partway through; collecting first gives them a clean run. load() / switch_setlist() clear _heavy_redraw_at so a user-initiated navigation doesn't trigger a second deferred rebuild on top of the immediate one. Co-Authored-By: Claude Opus 4.7 (1M context) --- pico-cp/app.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/pico-cp/app.py b/pico-cp/app.py index 0650c33..ea01650 100644 --- a/pico-cp/app.py +++ b/pico-cp/app.py @@ -18,7 +18,7 @@ import board, busio, digitalio, analogio, pwmio, displayio, vectorio, time, json, gc, os, supervisor supervisor.runtime.autoreload = False # we write our own files (log + pushed programs); never self-restart -APP_VERSION = "0.0.17" # firmware version (the A/B updater pushes/compares this) +APP_VERSION = "0.0.18" # firmware version (the A/B updater pushes/compares this) try: import rtc # set from the editor's clock SysEx so the log has real timestamps except ImportError: @@ -452,6 +452,7 @@ class App: self._dirty = False; self._overlay = None; self._ovbtns = [] # on-device editing: unsaved edits + modal self.continue_on = False; self._advance = False; self._grid = {} # auto-advance + pad hit-test geometry self._next_pending = None; self._seam_t = 0; self._need_redraw = False # gapless seam between tracks + self._heavy_redraw_at = 0 # deferred build_grid + draw_log deadline (so B's intro isn't blocked by SPI/alloc) self._displayed_bpm = -1; self._clock_next = 0 # lazy BPM redraw + MIDI Clock Out tick scheduler self._clock_in_last_t = 0; self._clock_in_avg = 0; self._slaved = False # MIDI Clock In: smoothed tracker + slave flag self.sl = 0; self.rebuild_setlists() # built-in playlists (baked) + user playlists (programs.json) @@ -550,7 +551,7 @@ class App: self.bpm, self.lanes, self.bars, self.ramp, self.trainer = parse_program(prog) self.master = self.lanes[0]; self._ramp_base = self.bpm; self._lastbar = -1; self._muted = False self._dirty = False; self._overlay = None # fresh load -> no unsaved edits - self._next_pending = None; self._need_redraw = False # discard any prepared seam (user navigated away) + self._next_pending = None; self._need_redraw = False; self._heavy_redraw_at = 0 # discard any prepared seam (user navigated away) while len(self.g_overlay): self.g_overlay.pop() # dismiss any open modal self._reset_clock(); self.draw_bpm(); self.draw_status(); self.draw_train() self.build_grid(); self.draw_log() @@ -1097,7 +1098,8 @@ class App: while len(self.g_overlay): self.g_overlay.pop() seam = self._seam_t for L in self.lanes: L['next'] = seam; L['step'] = -1 # NEXT tick fires step 0 of the new track at seam_t - self._need_redraw = True # visuals (grid + draws) catch up on the next refresh + self._need_redraw = True # cheap header bits: meters/bpm/status/train -> next refresh + self._heavy_redraw_at = time.monotonic() + 0.6 # heavy: build_grid + draw_log deferred ~0.6s so B's intro plays unblocked self._seg_start = time.monotonic() # reset the on-screen timer self.led_rest() @@ -1208,6 +1210,7 @@ class App: def build_grid(self): while len(self.g_grid): self.g_grid.pop() self.lane_pads = []; self.lane_lit = [] + gc.collect() # 64-128 vectorio allocs in this fn want a defragmented heap n = min(len(self.lanes), MAXLANES) top = GRID_TOP; rowh = min(40, ((LOG_TOP - 10) - top) // max(1, n)) px0 = 60; usable = WIDTH - 8 - px0 - 12; gridh = n * rowh @@ -1282,6 +1285,7 @@ class App: g = self.g_log while len(g): g.pop() self.log_rows = [] + gc.collect() # several text bitmaps allocated below want a clean heap hdr, w, h = make_text("PRACTICE LOG - THIS TRACK", FONT_S, C_MUTE, C_BG); hdr.x = 10; hdr.y = LOG_TOP; g.append(hdr) rows = [(i, e) for i, e in enumerate(self.log) if e.get("name") == self.name] # current track only if not rows: @@ -1409,10 +1413,12 @@ class App: except OSError: committed = True while True: self.tick(); self.poll() - if self._need_redraw: # post-seam: visuals catch up AFTER the audio swap + if self._need_redraw: # post-seam fast pass: cheap header/status bits, runs immediately self._need_redraw = False - self.draw_bpm(); self.draw_status(); self.draw_train() - self.build_grid(); self.draw_log(); self.draw_meters() + self.draw_bpm(); self.draw_status(); self.draw_train(); self.draw_meters() + if self._heavy_redraw_at and time.monotonic() >= self._heavy_redraw_at: + self._heavy_redraw_at = 0 # post-seam slow pass: build_grid + draw_log are ~hundreds of ms, + self.build_grid(); self.draw_log() # deferred so B's first few beats fire on time tnow = time.monotonic() if tnow >= self._uiNext: # ~4x/s: tick the stopwatch + bar counter self._uiNext = tnow + 0.25; self.draw_meters(); self.draw_bpm() # bpm follows the continuous ramp