perf(esp32): batch SPI to the worker + gate CS crossings (Doom ~30x)
Worker side of the libqemu picsimlab_spi_event_batch / CS-gating change: - _on_spi_batch(): replay a whole SPI transfer in bulk (custom-chip runtime, then ePaper feed, then the spi_batch buffer) instead of one _on_spi_event per byte. Registered as a trailing _SPI_BATCH field of _CallbacksT. - _sync_cs_events(): disable SPI chip-select callbacks for pure-display sims, enable them when an ePaper / custom-chip SPI slave is registered (no-op on older libqemu without qemu_picsimlab_enable_spi_cs_events). - _on_pin_change(): flush the SPI batch before each gpio_change so the byte stream stays ordered against the DC pin now that CS no longer triggers the flush. Backward compatible: an older libqemu never calls the batch callback or the CS setter, so it just keeps the per-byte path. esp32-doom: 0.04 -> ~1.0-1.5 FPS wall-clock (~26-37x), render verified correct.
This commit is contained in:
parent
780b80778c
commit
d3f9f7aa99
|
|
@ -140,6 +140,11 @@ _RMT_EVENT = ctypes.CFUNCTYPE(None, ctypes.c_uint8, ctypes.c_uint32,
|
|||
# burn-in window confirms parity. Requires libqemu-{xtensa,riscv32}
|
||||
# 1.1.0+; older binaries omit the field and the placeholder runs.
|
||||
_GPIO_MATRIX_CB = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_int)
|
||||
# Batched write-only SPI: (id, const uint8_t *mosi, int len). Collapses the
|
||||
# per-byte picsimlab_spi_event ctypes crossing for TFT-style bulk writes.
|
||||
# Trailing field — older libqemu builds (without the C-side batch path) simply
|
||||
# never call it and fall back to per-byte picsimlab_spi_event.
|
||||
_SPI_BATCH = ctypes.CFUNCTYPE(None, ctypes.c_uint8, ctypes.POINTER(ctypes.c_uint8), ctypes.c_int)
|
||||
|
||||
|
||||
class _CallbacksT(ctypes.Structure):
|
||||
|
|
@ -152,6 +157,7 @@ class _CallbacksT(ctypes.Structure):
|
|||
('pinmap', ctypes.c_void_p),
|
||||
('picsimlab_rmt_event', _RMT_EVENT),
|
||||
('picsimlab_gpio_matrix_cb', _GPIO_MATRIX_CB),
|
||||
('picsimlab_spi_event_batch', _SPI_BATCH),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -690,6 +696,14 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
|||
return
|
||||
gpio = int(_PINMAP[slot]) if 1 <= slot <= _GPIO_COUNT else slot
|
||||
_pin_state[gpio] = value & 1
|
||||
# Flush pending SPI bytes BEFORE announcing this pin change so the
|
||||
# frontend processes them under the pin state (e.g. the ILI9341 DC line)
|
||||
# that was in effect when they were sent. With CS events gated off for
|
||||
# pure-display sims, this — plus the buffer cap / timer — is what keeps
|
||||
# the SPI byte stream correctly ordered against the DC gpio_change on the
|
||||
# single WS channel (the per-CS flush used to do it).
|
||||
with _spi_buf_lock:
|
||||
_flush_spi_batch_locked()
|
||||
_emit({'type': 'gpio_change', 'pin': gpio, 'state': value})
|
||||
|
||||
# Dispatch to any custom-chip runtime that has a vx_pin_watch on this
|
||||
|
|
@ -1021,6 +1035,18 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
|||
_emit({'type': 'spi_batch', 'b64': b64})
|
||||
_spi_byte_buf.clear()
|
||||
|
||||
def _sync_cs_events():
|
||||
"""Tell QEMU whether to forward SPI chip-select toggles to us. Only
|
||||
ePaper / custom-chip SPI slaves consume CS; pure-display sims (DC pin +
|
||||
batched data) do not, so turning CS off there removes ~9k C->Python
|
||||
crossings/sec for a TFT redraw. No-op on older libqemu builds without
|
||||
the symbol (CS events stay on, as before)."""
|
||||
try:
|
||||
lib.qemu_picsimlab_enable_spi_cs_events(
|
||||
1 if (_epaper_state or _chip_spi_runtimes) else 0)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _spi_flush_timer_loop():
|
||||
"""Background thread: flushes any pending SPI bytes every
|
||||
_SPI_BATCH_PERIOD_S so partial transactions reach the frontend
|
||||
|
|
@ -1102,6 +1128,52 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
|||
_emit({'type': 'spi_event', 'bus': bus_id, 'event': event, 'response': resp})
|
||||
return resp
|
||||
|
||||
def _on_spi_batch(bus_id: int, mosi_ptr, length: int) -> None:
|
||||
"""Batched write-only SPI transfer — the whole MOSI buffer arrives in a
|
||||
single call instead of one picsimlab_spi_event per byte. libqemu only
|
||||
invokes this for rx==0 (MISO-ignored) transfers on the host SPI shim, so
|
||||
nothing is returned. Mirrors the per-byte _on_spi_event side effects in
|
||||
bulk: custom-chip runtimes first, then ePaper, then the spi_batch buffer.
|
||||
This is the path that removes ~150k C->Python crossings/frame for TFTs."""
|
||||
if length <= 0 or _stopped.is_set():
|
||||
return
|
||||
try:
|
||||
data = ctypes.string_at(mosi_ptr, length)
|
||||
except Exception:
|
||||
return
|
||||
# Custom-chip SPI runtimes get first dibs (replay per byte; the chip's
|
||||
# MISO return is discarded because this transfer is write-only).
|
||||
if _chip_spi_runtimes:
|
||||
rt = _chip_spi_runtimes[0]
|
||||
for mb in data:
|
||||
try:
|
||||
rt.spi_transfer_byte(mb)
|
||||
except Exception as e:
|
||||
_log(f'[custom-chip spi_batch] error: {e!r}')
|
||||
return
|
||||
# ePaper SSD168x: feed each byte under the current DC to every active
|
||||
# slave (DC is constant for a write-only transaction).
|
||||
if _epaper_state:
|
||||
any_active = False
|
||||
for st in _epaper_state.values():
|
||||
if st['cs_low']:
|
||||
any_active = True
|
||||
slave = st['slave']
|
||||
dc = st['dc_high']
|
||||
for mb in data:
|
||||
try:
|
||||
slave.feed(mb, dc)
|
||||
except Exception as e:
|
||||
_log(f'[epaper spi_batch] error: {e!r}')
|
||||
if any_active:
|
||||
return
|
||||
# Fast path: bulk-append to the spi_batch buffer (same buffer/flush the
|
||||
# per-byte path uses, so frontend ordering is unchanged).
|
||||
with _spi_buf_lock:
|
||||
_spi_byte_buf.extend(data)
|
||||
if len(_spi_byte_buf) >= _SPI_BATCH_FLUSH_AT:
|
||||
_flush_spi_batch_locked()
|
||||
|
||||
# Keep callback struct alive (prevent GC from freeing ctypes closures)
|
||||
_cbs_ref = _CallbacksT(
|
||||
picsimlab_write_pin = _WRITE_PIN(_on_pin_change),
|
||||
|
|
@ -1112,6 +1184,7 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
|||
pinmap = ctypes.cast(_PINMAP, ctypes.c_void_p).value,
|
||||
picsimlab_rmt_event = _RMT_EVENT(_on_rmt_event),
|
||||
picsimlab_gpio_matrix_cb = _GPIO_MATRIX_CB(_on_gpio_matrix),
|
||||
picsimlab_spi_event_batch = _SPI_BATCH(_on_spi_batch),
|
||||
)
|
||||
lib.qemu_picsimlab_register_callbacks(ctypes.byref(_cbs_ref))
|
||||
# Log whether the new symbol is present in this libqemu build.
|
||||
|
|
@ -1316,6 +1389,7 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
|||
}
|
||||
_epaper_slaves[comp_id] = slave
|
||||
_epaper_state[comp_id] = state
|
||||
_sync_cs_events()
|
||||
sensor_data['epaper_component_id'] = comp_id
|
||||
_log(f"[epaper:{ctl_family}] registered '{comp_id}' "
|
||||
f"({width}x{height}) "
|
||||
|
|
@ -1423,6 +1497,7 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
|||
_log("[custom-chip] UART chip registered on UART0")
|
||||
if runtime.spi_config is not None:
|
||||
_chip_spi_runtimes.append(runtime)
|
||||
_sync_cs_events()
|
||||
_log("[custom-chip] SPI chip registered")
|
||||
if runtime.has_pin_watches():
|
||||
_chip_pin_watch_runtimes.append(runtime)
|
||||
|
|
@ -1439,6 +1514,9 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
|||
_log(f'_i2c_slaves registered: {list(_i2c_slaves.keys())}')
|
||||
|
||||
_emit({'type': 'system', 'event': 'booted'})
|
||||
# Now that the initial components are registered, tell QEMU whether to
|
||||
# forward SPI CS toggles (only ePaper/custom-chip need them).
|
||||
_sync_cs_events()
|
||||
_log(f'QEMU started: machine={machine} firmware={firmware_path}')
|
||||
_log(f'QEMU args: {[a.decode() for a in args_list]}')
|
||||
|
||||
|
|
@ -1706,6 +1784,7 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker)
|
|||
}
|
||||
_epaper_slaves[comp_id] = slave
|
||||
_epaper_state[comp_id] = state
|
||||
_sync_cs_events()
|
||||
sensor_data['epaper_component_id'] = comp_id
|
||||
_sensors[gpio] = sensor_data
|
||||
_log(f'Sensor {sensor_type} attached on GPIO {gpio}')
|
||||
|
|
|
|||
Loading…
Reference in New Issue