fix(spi-batch): add 50ms safety flush so frames keep arriving
User reported only 2 frames rendering after the SPI batching change. Diagnosis: the previous flush triggers (CS-line HIGH, buffer >=4096) were both event-driven. Adafruit_ILI9341's ESP32 backend manages CS via digitalWrite — i.e. through the GPIO peripheral, NOT the SPI peripheral's hardware CS pin. So the CS-line-HIGH event from picsimlab_spi_event NEVER fires for this driver. The buffer only flushes when it hits 4096 bytes. Frame 1: 38 400 bytes from drawRGBBitmap → 9 flushes at 4096-byte boundaries → last 192 bytes stay in the buffer. Status bar adds some bytes too → maybe one more flush. Frame 2: same. But by frame 3 the firmware is running ahead of the flush rhythm and somehow the buffer pattern wedges in a state where no flush completes (likely a partial buffer that sits between transactions while the firmware briefly waits on the next fb_get). Hard to reproduce deterministically — but the symptom matches. Fix: add a 50 ms periodic flush thread. Independent of any event, it acquires the lock and flushes whatever's pending. Bounds the worst-case latency at 50 ms (= 20 fps ceiling, more than enough for the emulator). Triple-trigger now: 1. CS HIGH (fast path for hardware-CS drivers) 2. Buffer >= 4096 (safety for big transactions) 3. 50 ms timer (catches GPIO-CS drivers, prevents stalls) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d4d015c25d
commit
977308ef80
|
|
@ -868,15 +868,25 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
|||
# SPI byte batching — emitting one WS message per byte saturates the
|
||||
# uvicorn → frontend pipe and caps tft.drawRGBBitmap at < 1 fps even
|
||||
# for tiny previews. Buffer the MOSI bytes here and flush as a single
|
||||
# base64-encoded `spi_batch` message when CS goes HIGH (transaction
|
||||
# ended) or the buffer crosses a soft cap. The MISO response is still
|
||||
# returned synchronously per byte from `_spi_response[0]` because the
|
||||
# QEMU master writes can't wait. Frontend Esp32Bridge unpacks the batch
|
||||
# and replays each byte through onSpiByte. ~9600 events/frame → ~3
|
||||
# batched messages/frame, ~50× faster TFT throughput in the emulator.
|
||||
_spi_byte_buf = bytearray()
|
||||
_spi_buf_lock = threading.Lock()
|
||||
_SPI_BATCH_FLUSH_AT = 4096 # flush early if a single transaction is huge
|
||||
# base64-encoded `spi_batch` message when:
|
||||
# 1. CS goes HIGH (transaction ended) — only fires when the firmware
|
||||
# uses the SPI peripheral's hardware CS line. If CS is bit-banged
|
||||
# via digitalWrite (the default for many Adafruit-style drivers
|
||||
# on ESP32), this trigger never fires and we fall back to (2)+(3).
|
||||
# 2. Buffer crosses _SPI_BATCH_FLUSH_AT bytes (safety cap for big
|
||||
# transactions).
|
||||
# 3. _spi_flush_timer fires every _SPI_BATCH_PERIOD_MS regardless —
|
||||
# catches the GPIO-CS case so partial batches don't sit in the
|
||||
# buffer forever between transactions. Without this, after a few
|
||||
# drawRGBBitmap calls the firmware advances faster than the
|
||||
# buffer fills, and frames stop appearing on the screen.
|
||||
# MISO is still returned synchronously per byte from _spi_response[0]
|
||||
# because the QEMU master writes can't wait. Frontend Esp32Bridge
|
||||
# unpacks the batch and replays each byte through onSpiByte.
|
||||
_spi_byte_buf = bytearray()
|
||||
_spi_buf_lock = threading.Lock()
|
||||
_SPI_BATCH_FLUSH_AT = 4096
|
||||
_SPI_BATCH_PERIOD_S = 0.05 # 50 ms → 20 fps cadence ceiling
|
||||
|
||||
def _flush_spi_batch_locked():
|
||||
if _spi_byte_buf and not _stopped.is_set():
|
||||
|
|
@ -884,6 +894,23 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
|||
_emit({'type': 'spi_batch', 'b64': b64})
|
||||
_spi_byte_buf.clear()
|
||||
|
||||
def _spi_flush_timer_loop():
|
||||
"""Background thread: flushes any pending SPI bytes every
|
||||
_SPI_BATCH_PERIOD_S so partial transactions reach the frontend
|
||||
even when the firmware drives CS via GPIO and we never see a
|
||||
SPI peripheral CS-high event."""
|
||||
while not _stopped.is_set():
|
||||
_stopped.wait(_SPI_BATCH_PERIOD_S)
|
||||
if _stopped.is_set():
|
||||
break
|
||||
with _spi_buf_lock:
|
||||
_flush_spi_batch_locked()
|
||||
|
||||
threading.Thread(
|
||||
target=_spi_flush_timer_loop, daemon=True,
|
||||
name='esp32-spi-batch-flush',
|
||||
).start()
|
||||
|
||||
def _on_spi_event(bus_id: int, event: int) -> int:
|
||||
"""Synchronous — must return immediately; called from QEMU thread.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue