PM_K-1 0.0.20: per-pad chunking + step grids + smarter refresh
Three changes; one diagnosis: the 0.0.19 freeze + stutter were both symptoms of the predictive refresh skip being too aggressive at fast subdivisions (scanning ALL lanes + a 35ms window meant the next beat is *always* within the skip window at fine subdivisions, so the only refresh was the 0.5s force-after fallback, audible as both a flickery freeze AND a periodic 30ms stutter). PER-PAD CHUNKING (your ask): - _grid_rebuild_step now builds ONE rectangle per loop iter (was one whole lane row). - The first chunk on a lane draws the instrument label + caches the lane's geometry; subsequent chunks each emit one pad rect/circle. - Each chunk is ~5-10ms instead of ~30-50ms, so tick() interleaves at sub-beat resolution and the grid fills in pad-by-pad over ~600ms after the seam without ever blocking long enough to miss a beat at any reasonable BPM. STEP GRIDS: - L['durs'] is a tuple of int ns per step, precomputed once per lane. - tick()'s inner-loop scheduler is now L['next'] += L['durs'][L['step']] -- a tuple index, no method call, no dict lookups, no division. - _rebuild_dur(L) and _rebuild_dur_all() wired into every bpm-change path: set_bpm / load / _do_advance / _prepare_next / _slave_tick (Clock In) / _lane_dirty (the lane editor; rebuilds master-affects-poly cascade if lane 0 changes structurally), + the per-master-step ramp interpolator. The synchronous _step_dur stays as a fallback for paths the inner loop doesn't hit (e.g. start-of-segment calculations elsewhere). SMARTER REFRESH: - Master-lane-only window (other lanes are subdivisions; a few ms of jitter there is imperceptible musically). - 10ms window (down from 35) so refresh runs in the gap between master beats even at fast subdivisions. - 20Hz throttle (down from 30) so the per-refresh blocking budget is bigger. - 200ms force fallback (down from 500) so visuals + titles stay live -- this is the fix for "the track titles don't get reliably updated." DEFER set_bpm DRAWS: - set_bpm no longer calls draw_bpm/draw_meters; the 4Hz UI tick already redraws them. - Joystick spam used to allocate a fresh text bitmap per nudge -> fragmentation pressure + potential GC stutter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
672f892ea1
commit
09144c9892
1 changed files with 81 additions and 38 deletions
119
pico-cp/app.py
119
pico-cp/app.py
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
import board, busio, digitalio, analogio, pwmio, displayio, vectorio, time, json, gc, os, supervisor
|
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
|
supervisor.runtime.autoreload = False # we write our own files (log + pushed programs); never self-restart
|
||||||
APP_VERSION = "0.0.19" # firmware version (the A/B updater pushes/compares this)
|
APP_VERSION = "0.0.20" # firmware version (the A/B updater pushes/compares this)
|
||||||
try:
|
try:
|
||||||
import rtc # set from the editor's clock SysEx so the log has real timestamps
|
import rtc # set from the editor's clock SysEx so the log has real timestamps
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -453,7 +453,8 @@ class App:
|
||||||
self.continue_on = False; self._advance = False; self._grid = {} # auto-advance + pad hit-test geometry
|
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._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._heavy_redraw_at = 0 # deferred build_grid + draw_log deadline (so B's intro isn't blocked by SPI/alloc)
|
||||||
self._grid_li = None; self._grid_n = 0; self._grid_geo = (0, 0, 0, 0) # chunked build_grid progress (1 lane / loop iter)
|
self._grid_li = None; self._grid_n = 0; self._grid_geo = (0, 0, 0, 0) # chunked build_grid progress (1 PAD / loop iter)
|
||||||
|
self._grid_pi = 0; self._grid_lane_st = None; self._grid_pads = [] # per-lane sub-state for sub-pad chunking
|
||||||
self._heavy_log_pending = False
|
self._heavy_log_pending = False
|
||||||
self._beat_ns = 60_000_000_000 // self.bpm # cached: ns per quarter note; refreshed on every bpm change
|
self._beat_ns = 60_000_000_000 // self.bpm # cached: ns per quarter note; refreshed on every bpm change
|
||||||
self._note_buf = bytearray([0x90, 0, 0]) # reused for every Note On (no per-click bytes() alloc)
|
self._note_buf = bytearray([0x90, 0, 0]) # reused for every Note On (no per-click bytes() alloc)
|
||||||
|
|
@ -556,6 +557,7 @@ class App:
|
||||||
self.idx = i % len(items)
|
self.idx = i % len(items)
|
||||||
self.name, prog = items[self.idx]
|
self.name, prog = items[self.idx]
|
||||||
self.bpm, self.lanes, self.bars, self.ramp, self.trainer = parse_program(prog)
|
self.bpm, self.lanes, self.bars, self.ramp, self.trainer = parse_program(prog)
|
||||||
|
self._beat_ns = 60_000_000_000 // max(1, self.bpm); self._rebuild_dur_all() # step grids ready for this lane set
|
||||||
self.master = self.lanes[0]; self._ramp_base = self.bpm; self._lastbar = -1; self._muted = False
|
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._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 # discard any prepared seam (user navigated away)
|
||||||
|
|
@ -721,6 +723,8 @@ class App:
|
||||||
L['levels'] = [(2 if (i // sub) in starts else 1) if i % sub == 0 else 0 for i in range(L['steps'])]
|
L['levels'] = [(2 if (i // sub) in starts else 1) if i % sub == 0 else 0 for i in range(L['steps'])]
|
||||||
def _lane_dirty(self, structural):
|
def _lane_dirty(self, structural):
|
||||||
if structural: self._regen_levels(self.lanes[self._edit_li])
|
if structural: self._regen_levels(self.lanes[self._edit_li])
|
||||||
|
if structural and self._edit_li == 0: self._rebuild_dur_all() # master changed -> polymeter lanes follow
|
||||||
|
else: self._rebuild_dur(self.lanes[self._edit_li])
|
||||||
self.build_grid()
|
self.build_grid()
|
||||||
if not self._dirty: self._dirty = True; self.draw_status()
|
if not self._dirty: self._dirty = True; self.draw_status()
|
||||||
self._draw_laneedit() # refresh the modal with the new values
|
self._draw_laneedit() # refresh the modal with the new values
|
||||||
|
|
@ -947,16 +951,29 @@ class App:
|
||||||
with open("/settings.json", "w") as f: json.dump(d, f)
|
with open("/settings.json", "w") as f: json.dump(d, f)
|
||||||
except OSError: self.can_write = False
|
except OSError: self.can_write = False
|
||||||
|
|
||||||
def _step_dur(self, L, step):
|
def _step_dur(self, L, step): # legacy fallback (still used by _start_play tap-tempo path)
|
||||||
beat = self._beat_ns # cached ns/beat (refreshed on every bpm change + ramp tick)
|
beat = self._beat_ns
|
||||||
if L['poly']: # ~ polymeter: fit this lane's whole cycle into lane 1's bar
|
if L['poly']:
|
||||||
m = self.lanes[0]; master_bar = beat * (m['steps'] // m['sub'])
|
m = self.lanes[0]; master_bar = beat * (m['steps'] // m['sub'])
|
||||||
return master_bar // L['steps']
|
return master_bar // L['steps']
|
||||||
sub = L['sub']
|
sub = L['sub']
|
||||||
if L['swing'] and sub % 2 == 0: # swing even subdivisions: long-short (2:1) pairs
|
if L['swing'] and sub % 2 == 0:
|
||||||
pair = beat // (sub // 2)
|
pair = beat // (sub // 2)
|
||||||
return (pair * 2) // 3 if (step % sub) % 2 == 0 else pair // 3
|
return (pair * 2) // 3 if (step % sub) % 2 == 0 else pair // 3
|
||||||
return beat // sub # straight: a step = one beat / subdivision
|
return beat // sub
|
||||||
|
def _rebuild_dur(self, L): # cache the per-step ns durations into L['durs'] (tuple lookup is ~10us)
|
||||||
|
beat = self._beat_ns
|
||||||
|
sub = max(1, L['sub']); steps = max(1, L['steps'])
|
||||||
|
if L.get('poly') and self.lanes: # polymeter: spread this lane's cycle across master's bar
|
||||||
|
m = self.lanes[0]; master_bar = beat * (m['steps'] // max(1, m['sub']))
|
||||||
|
d = master_bar // steps; L['durs'] = tuple(d for _ in range(steps))
|
||||||
|
elif L.get('swing') and sub % 2 == 0: # swing: long-short pairs
|
||||||
|
pair = beat // max(1, sub // 2); lng = (pair * 2) // 3; sht = pair // 3
|
||||||
|
L['durs'] = tuple(lng if (s % sub) % 2 == 0 else sht for s in range(steps))
|
||||||
|
else: # straight: every step is beat/sub long
|
||||||
|
d = beat // sub; L['durs'] = tuple(d for _ in range(steps))
|
||||||
|
def _rebuild_dur_all(self): # called on bpm change + lane mutation + track swap
|
||||||
|
for L in self.lanes: self._rebuild_dur(L)
|
||||||
def _reset_clock(self):
|
def _reset_clock(self):
|
||||||
now = time.monotonic_ns()
|
now = time.monotonic_ns()
|
||||||
for L in self.lanes:
|
for L in self.lanes:
|
||||||
|
|
@ -1003,8 +1020,9 @@ class App:
|
||||||
def set_bpm(self, v):
|
def set_bpm(self, v):
|
||||||
v = max(5, min(300, v))
|
v = max(5, min(300, v))
|
||||||
if v != self.bpm:
|
if v != self.bpm:
|
||||||
self.bpm = v; self._beat_ns = 60_000_000_000 // v # keep cached beat duration in sync
|
self.bpm = v; self._beat_ns = 60_000_000_000 // v
|
||||||
self.draw_bpm(); self.draw_meters() # total time depends on bpm
|
self._rebuild_dur_all() # step grids follow the new beat duration
|
||||||
|
# Don't draw here -- the 4Hz UI tick redraws bpm/meters; calling per joystick nudge allocated text bitmaps fast enough to trigger GC pauses
|
||||||
def goto(self, i):
|
def goto(self, i):
|
||||||
was = self.running
|
was = self.running
|
||||||
if was: self.running = False; self._log_play() # close out the track that was playing
|
if was: self.running = False; self._log_play() # close out the track that was playing
|
||||||
|
|
@ -1043,14 +1061,16 @@ class App:
|
||||||
bar_pos = self._m_steps / mlen
|
bar_pos = self._m_steps / mlen
|
||||||
seg_bar = (bar_pos % self.bars) if self.bars else bar_pos
|
seg_bar = (bar_pos % self.bars) if self.bars else bar_pos
|
||||||
new_bpm = max(5, min(300, int(self._ramp_base + seg_bar / self.ramp['every'] * self.ramp['amt'])))
|
new_bpm = max(5, min(300, int(self._ramp_base + seg_bar / self.ramp['every'] * self.ramp['amt'])))
|
||||||
if new_bpm != self.bpm: self.bpm = new_bpm; self._beat_ns = 60_000_000_000 // new_bpm
|
if new_bpm != self.bpm:
|
||||||
|
self.bpm = new_bpm; self._beat_ns = 60_000_000_000 // new_bpm
|
||||||
|
self._rebuild_dur_all() # ramp moves bpm -> step grids follow
|
||||||
lvl = 0 if L['mute'] else L['levels'][L['step']]
|
lvl = 0 if L['mute'] else L['levels'][L['step']]
|
||||||
if lvl > 0:
|
if lvl > 0:
|
||||||
p = PRIO.get(lvl, 0)
|
p = PRIO.get(lvl, 0)
|
||||||
if p > fired_prio: fired_prio = p; fired_best = lvl # accent > normal > ghost
|
if p > fired_prio: fired_prio = p; fired_best = lvl # accent > normal > ghost
|
||||||
if not self._muted: # gap trainer: silent during the rest bars
|
if not self._muted: # gap trainer: silent during the rest bars
|
||||||
self.midi_send(SOUND_GM.get(L['sound'], GM_DEFAULT), MIDI_VEL.get(lvl, 90))
|
self.midi_send(SOUND_GM.get(L['sound'], GM_DEFAULT), MIDI_VEL.get(lvl, 90))
|
||||||
L['next'] += self._step_dur(L, L['step']); adv = True
|
L['next'] += L['durs'][L['step']]; adv = True # zero method call, zero dict lookup, just a tuple index
|
||||||
if adv and li < len(self.lane_pads): self._move_playhead(li, L['step'])
|
if adv and li < len(self.lane_pads): self._move_playhead(li, L['step'])
|
||||||
if fired_best and not self._muted:
|
if fired_best and not self._muted:
|
||||||
if not MUTE_SPEAKER and not (SPEAKER_AUTO_MUTE and self.midi_host):
|
if not MUTE_SPEAKER and not (SPEAKER_AUTO_MUTE and self.midi_host):
|
||||||
|
|
@ -1098,6 +1118,17 @@ class App:
|
||||||
bpm, lanes, bars, ramp, trainer = parse_program(prog)
|
bpm, lanes, bars, ramp, trainer = parse_program(prog)
|
||||||
except MemoryError:
|
except MemoryError:
|
||||||
gc.collect(); return # leave _next_pending None -> the segment just loops
|
gc.collect(); return # leave _next_pending None -> the segment just loops
|
||||||
|
beat = 60_000_000_000 // max(1, bpm) # pre-compute B's durs against B's bpm so the seam swap is allocation-free
|
||||||
|
for L in lanes:
|
||||||
|
sub = max(1, L['sub']); steps = max(1, L['steps'])
|
||||||
|
if L.get('poly'):
|
||||||
|
m = lanes[0]; mbar = beat * (m['steps'] // max(1, m['sub']))
|
||||||
|
d = mbar // steps; L['durs'] = tuple(d for _ in range(steps))
|
||||||
|
elif L.get('swing') and sub % 2 == 0:
|
||||||
|
pair = beat // max(1, sub // 2); lng = (pair * 2) // 3; sht = pair // 3
|
||||||
|
L['durs'] = tuple(lng if (s % sub) % 2 == 0 else sht for s in range(steps))
|
||||||
|
else:
|
||||||
|
d = beat // sub; L['durs'] = tuple(d for _ in range(steps))
|
||||||
self._next_pending = {'lanes': lanes, 'bpm': bpm, 'bars': bars, 'ramp': ramp,
|
self._next_pending = {'lanes': lanes, 'bpm': bpm, 'bars': bars, 'ramp': ramp,
|
||||||
'trainer': trainer, 'name': name, 'idx': nxt}
|
'trainer': trainer, 'name': name, 'idx': nxt}
|
||||||
def _do_advance(self): # gapless seam: swap the prepared track in at seam_t
|
def _do_advance(self): # gapless seam: swap the prepared track in at seam_t
|
||||||
|
|
@ -1106,6 +1137,7 @@ class App:
|
||||||
self._next_pending = None
|
self._next_pending = None
|
||||||
self.lanes = n['lanes']; self.bpm = n['bpm']; self.bars = n['bars']
|
self.lanes = n['lanes']; self.bpm = n['bpm']; self.bars = n['bars']
|
||||||
self.ramp = n['ramp']; self.trainer = n['trainer']; self.name = n['name']; self.idx = n['idx']
|
self.ramp = n['ramp']; self.trainer = n['trainer']; self.name = n['name']; self.idx = n['idx']
|
||||||
|
self._beat_ns = 60_000_000_000 // max(1, self.bpm); self._rebuild_dur_all() # B's step grids built at the seam
|
||||||
self._ramp_base = self.bpm; self._lastbar = -1; self._muted = False; self._m_steps = 0
|
self._ramp_base = self.bpm; self._lastbar = -1; self._muted = False; self._m_steps = 0
|
||||||
self._dirty = False; self._overlay = None
|
self._dirty = False; self._overlay = None
|
||||||
while len(self.g_overlay): self.g_overlay.pop()
|
while len(self.g_overlay): self.g_overlay.pop()
|
||||||
|
|
@ -1239,29 +1271,40 @@ class App:
|
||||||
self._grid_n = n
|
self._grid_n = n
|
||||||
self._grid_geo = (top, rowh, px0, usable)
|
self._grid_geo = (top, rowh, px0, usable)
|
||||||
self._grid_li = 0 if n > 0 else None
|
self._grid_li = 0 if n > 0 else None
|
||||||
|
self._grid_pi = 0; self._grid_lane_st = None; self._grid_pads = [] # start at lane 0, pad 0, no lane initialized yet
|
||||||
self.dirty = True
|
self.dirty = True
|
||||||
def _grid_rebuild_step(self): # build ONE lane row; sets _grid_li=None when done
|
def _grid_rebuild_step(self): # PER-PAD chunk: build at most one rectangle, then yield
|
||||||
li = self._grid_li
|
li = self._grid_li
|
||||||
if li is None: return
|
if li is None: return
|
||||||
if li >= self._grid_n or li >= len(self.lanes):
|
if li >= self._grid_n or li >= len(self.lanes):
|
||||||
self._grid_li = None; return # done; main loop falls through to draw_log
|
self._grid_li = None; return # whole rebuild done -> main loop runs draw_log
|
||||||
|
L = self.lanes[li]
|
||||||
top, rowh, px0, usable = self._grid_geo
|
top, rowh, px0, usable = self._grid_geo
|
||||||
L = self.lanes[li]; y = top + li * rowh; cy = y + rowh // 2
|
y = top + li * rowh; cy = y + rowh // 2
|
||||||
tg, w, h = make_text((L.get('sound', '') or '?')[:7], FONT_S, C_MUTE, C_BG)
|
st = self._grid_lane_st
|
||||||
tg.x = 8; tg.y = cy - h // 2; self.g_grid.append(tg)
|
if st is None: # first chunk on this lane: draw the instrument label + cache the geometry
|
||||||
steps = L['steps']; sub = L['sub']; stepw = max(1, usable // steps)
|
tg, w, h = make_text((L.get('sound', '') or '?')[:7], FONT_S, C_MUTE, C_BG)
|
||||||
side = max(5, min(15, stepw - 1, rowh - 6)) # square edge for the main pulse
|
tg.x = 8; tg.y = cy - h // 2; self.g_grid.append(tg)
|
||||||
rad = max(2, min(side // 2, stepw // 2 - 1)) # smaller circle for subdivisions
|
steps = L['steps']; sub = L['sub']; stepw = max(1, usable // steps)
|
||||||
pal = self.pad_pal; pads = []
|
side = max(5, min(15, stepw - 1, rowh - 6)) # square edge for the main pulse
|
||||||
for s in range(steps):
|
rad = max(2, min(side // 2, stepw // 2 - 1)) # smaller circle for subdivisions
|
||||||
cxp = px0 + 6 + (s * usable) // steps # proportional -> beats line up across lanes
|
self._grid_lane_st = (cy, steps, sub, stepw, side, rad)
|
||||||
if s % sub == 0:
|
self._grid_pi = 0; self._grid_pads = []; self.dirty = True
|
||||||
p = vectorio.Rectangle(pixel_shader=pal, width=side, height=side, x=cxp - side // 2, y=cy - side // 2)
|
return # one chunk = "init this lane"; next iter does the first pad
|
||||||
else:
|
cy_, steps, sub, stepw, side, rad = st
|
||||||
p = vectorio.Circle(pixel_shader=pal, radius=rad, x=cxp, y=cy)
|
s = self._grid_pi
|
||||||
p.color_index = self._padbase(L, s); self.g_grid.append(p); pads.append(p)
|
if s >= steps: # this lane finished; commit and advance to next
|
||||||
self.lane_pads.append(pads); self.lane_lit.append(-1)
|
self.lane_pads.append(self._grid_pads); self.lane_lit.append(-1)
|
||||||
self._grid_li = li + 1
|
self._grid_pads = []; self._grid_lane_st = None; self._grid_li = li + 1
|
||||||
|
return
|
||||||
|
cxp = px0 + 6 + (s * usable) // steps # proportional -> beats line up across lanes
|
||||||
|
pal = self.pad_pal
|
||||||
|
if s % sub == 0:
|
||||||
|
p = vectorio.Rectangle(pixel_shader=pal, width=side, height=side, x=cxp - side // 2, y=cy_ - side // 2)
|
||||||
|
else:
|
||||||
|
p = vectorio.Circle(pixel_shader=pal, radius=rad, x=cxp, y=cy_)
|
||||||
|
p.color_index = self._padbase(L, s); self.g_grid.append(p); self._grid_pads.append(p)
|
||||||
|
self._grid_pi = s + 1
|
||||||
self.dirty = True
|
self.dirty = True
|
||||||
def _move_playhead(self, li, step):
|
def _move_playhead(self, li, step):
|
||||||
pads = self.lane_pads[li]; prev = self.lane_lit[li]
|
pads = self.lane_pads[li]; prev = self.lane_lit[li]
|
||||||
|
|
@ -1362,7 +1405,8 @@ class App:
|
||||||
if self._clock_in_avg == 0: self._clock_in_avg = interval
|
if self._clock_in_avg == 0: self._clock_in_avg = interval
|
||||||
else: self._clock_in_avg = (self._clock_in_avg * 7 + interval) // 8 # exponential smoothing, alpha = 1/8
|
else: self._clock_in_avg = (self._clock_in_avg * 7 + interval) // 8 # exponential smoothing, alpha = 1/8
|
||||||
new_bpm = max(5, min(300, int(60_000_000_000 // (self._clock_in_avg * 24))))
|
new_bpm = max(5, min(300, int(60_000_000_000 // (self._clock_in_avg * 24))))
|
||||||
if new_bpm != self.bpm: self.bpm = new_bpm
|
if new_bpm != self.bpm:
|
||||||
|
self.bpm = new_bpm; self._beat_ns = 60_000_000_000 // new_bpm; self._rebuild_dur_all()
|
||||||
self._slaved = True
|
self._slaved = True
|
||||||
def _slave_start(self): # master sent Start (or Continue) -> start playback
|
def _slave_start(self): # master sent Start (or Continue) -> start playback
|
||||||
if not self.running:
|
if not self.running:
|
||||||
|
|
@ -1457,20 +1501,19 @@ class App:
|
||||||
try: os.remove("/trial")
|
try: os.remove("/trial")
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
committed = True
|
committed = True
|
||||||
# Refresh display ~30x/s, BUT skip if a beat is due within ~35ms (refresh() blocks SPI for that long)
|
# Refresh display ~20x/s, skip ONLY when the MASTER lane's next step is within ~10ms (its alignment
|
||||||
# so the click never gets pushed late by the redraw. Force-refresh after 0.5s anyway so visuals stay live.
|
# matters most musically; sub-lanes can take a ~few ms jitter without audible problem). Force-refresh
|
||||||
|
# after 200ms so titles + meters still feel live at fast subdivisions.
|
||||||
if self.dirty and tnow >= self._refreshNext:
|
if self.dirty and tnow >= self._refreshNext:
|
||||||
safe = True
|
safe = True
|
||||||
if self.running and self.lanes:
|
if self.running and self.lanes:
|
||||||
now_ns = time.monotonic_ns(); next_beat = self.lanes[0]['next']
|
nb = self.lanes[0]['next'] # master only -> doesn't starve at fine subdivisions
|
||||||
for L in self.lanes:
|
safe = (nb - time.monotonic_ns()) > 10_000_000 or (tnow - self._lastRefresh) > 0.2
|
||||||
if L['next'] < next_beat: next_beat = L['next']
|
|
||||||
safe = (next_beat - now_ns) > 35_000_000 or (tnow - self._lastRefresh) > 0.5
|
|
||||||
if safe:
|
if safe:
|
||||||
if self.display.refresh(): self.dirty = False
|
if self.display.refresh(): self.dirty = False
|
||||||
self._lastRefresh = tnow; self._refreshNext = tnow + 0.033
|
self._lastRefresh = tnow; self._refreshNext = tnow + 0.05
|
||||||
else:
|
else:
|
||||||
self._refreshNext = tnow + 0.005 # check again soon; don't wait the full 33ms
|
self._refreshNext = tnow + 0.003 # check again very soon; don't wait the 50ms
|
||||||
time.sleep(0.0005)
|
time.sleep(0.0005)
|
||||||
except MemoryError: # surface, gc, keep running (don't crash on a fragmented heap)
|
except MemoryError: # surface, gc, keep running (don't crash on a fragmented heap)
|
||||||
try: print("MemoryError: gc + continue")
|
try: print("MemoryError: gc + continue")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue