Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 68 additions & 30 deletions locale/circuitpython.pot
Original file line number Diff line number Diff line change
Expand Up @@ -1041,7 +1041,7 @@ msgstr ""
#: ports/espressif/common-hal/sdioio/SDCard.c
#: ports/raspberrypi/common-hal/sdioio/SDCard.c
#: ports/stm/common-hal/sdioio/SDCard.c shared-bindings/floppyio/__init__.c
#: shared-module/sdcardio/SDCard.c
#: shared-bindings/picogame/Canvas.c shared-module/sdcardio/SDCard.c
#, c-format
msgid "Buffer must be a multiple of %d bytes"
msgstr ""
Expand Down Expand Up @@ -1476,6 +1476,7 @@ msgid "Requested resource not found"
msgstr ""

#: ports/espressif/common-hal/espidf/__init__.c
#: ports/raspberrypi/common-hal/picogame/Display.c
msgid "Operation or feature not supported"
msgstr ""

Expand Down Expand Up @@ -1570,6 +1571,14 @@ msgstr ""
msgid "Number of data_pins must be %d or %d, not %d"
msgstr ""

#: ports/espressif/common-hal/picogame/Display.c
msgid "rgb444 fast Display not supported on this port yet"
msgstr ""

#: ports/espressif/common-hal/picogame/Display.c
msgid "fast Display needs a FourWire SPI display"
msgstr ""

#: ports/espressif/common-hal/pulseio/PulseIn.c
msgid "pop from an empty PulseIn"
msgstr ""
Expand Down Expand Up @@ -1682,7 +1691,8 @@ msgstr ""
msgid "Only IPv4 addresses supported"
msgstr ""

#: ports/mimxrt10xx/common-hal/busio/SPI.c shared-bindings/busio/SPI.c
#: ports/mimxrt10xx/common-hal/busio/SPI.c
#: ports/raspberrypi/bindings/wiznet/PIO_SPI.c shared-bindings/busio/SPI.c
msgid "Must provide MISO or MOSI pin"
msgstr ""

Expand Down Expand Up @@ -1813,9 +1823,9 @@ msgstr ""

#: ports/raspberrypi/bindings/cyw43/__init__.c py/argcheck.c py/objexcept.c
#: shared-bindings/bitmapfilter/__init__.c shared-bindings/canio/CAN.c
#: shared-bindings/digitalio/Pull.c shared-bindings/supervisor/__init__.c
#: shared-module/audiofilters/Filter.c shared-module/displayio/__init__.c
#: shared-module/synthio/Synthesizer.c
#: shared-bindings/digitalio/Pull.c shared-bindings/picogame/__init__.c
#: shared-bindings/supervisor/__init__.c shared-module/audiofilters/Filter.c
#: shared-module/displayio/__init__.c shared-module/synthio/Synthesizer.c
msgid "%q must be of type %q or %q, not %q"
msgstr ""

Expand Down Expand Up @@ -1844,6 +1854,17 @@ msgstr ""
msgid "In-buffer elements must be <= 4 bytes long"
msgstr ""

#: ports/raspberrypi/bindings/wiznet/PIO_SPI.c shared-bindings/bitbangio/I2C.c
#: shared-bindings/bitbangio/SPI.c shared-bindings/busio/I2C.c
#: shared-bindings/busio/SPI.c
msgid "Function requires lock"
msgstr ""

#: ports/raspberrypi/bindings/wiznet/PIO_SPI.c shared-bindings/bitbangio/SPI.c
#: shared-bindings/busio/SPI.c
msgid "buffer slices must be of equal length"
msgstr ""

#: ports/raspberrypi/common-hal/alarm/touch/TouchAlarm.c
#: ports/stm/common-hal/alarm/touch/TouchAlarm.c
msgid "Touch alarms not available"
Expand Down Expand Up @@ -1958,6 +1979,13 @@ msgstr ""
msgid "All timers for this pin are in use"
msgstr ""

#: ports/raspberrypi/common-hal/picogame/Display.c py/argcheck.c py/runtime.c
#: shared-bindings/bitmapfilter/__init__.c
#: shared-module/audiodelays/MultiTapDelay.c shared-module/synthio/Note.c
#: shared-module/synthio/__init__.c
msgid "%q must be of type %q, not %q"
msgstr ""

#: ports/raspberrypi/common-hal/rotaryio/IncrementalEncoder.c
msgid "Pins must be sequential GPIO pins"
msgstr ""
Expand Down Expand Up @@ -2292,7 +2320,7 @@ msgid "extra keyword arguments given"
msgstr ""

#: py/argcheck.c shared-bindings/_stage/__init__.c
#: shared-bindings/digitalio/DigitalInOut.c
#: shared-bindings/digitalio/DigitalInOut.c shared-bindings/picogame/__init__.c
msgid "argument num/types mismatch"
msgstr ""

Expand All @@ -2313,12 +2341,6 @@ msgstr ""
msgid "%q must be <= %d"
msgstr ""

#: py/argcheck.c py/runtime.c shared-bindings/bitmapfilter/__init__.c
#: shared-module/audiodelays/MultiTapDelay.c shared-module/synthio/Note.c
#: shared-module/synthio/__init__.c
msgid "%q must be of type %q, not %q"
msgstr ""

#: py/argcheck.c
msgid "%q length must be %d-%d"
msgstr ""
Expand Down Expand Up @@ -2868,7 +2890,9 @@ msgstr ""
msgid "schedule queue full"
msgstr ""

#: py/modstruct.c shared-module/struct/__init__.c
#: py/modstruct.c shared-bindings/picogame/Canvas.c
#: shared-bindings/picogame/__init__.c shared-module/picogame/__init__.c
#: shared-module/struct/__init__.c
msgid "buffer too small"
msgstr ""

Expand Down Expand Up @@ -3732,6 +3756,7 @@ msgid "file must be a file opened in byte mode"
msgstr ""

#: shared-bindings/audiodelays/Chorus.c shared-bindings/audiodelays/Echo.c
#: shared-bindings/audiodelays/Flanger.c
#: shared-bindings/audiodelays/GranularPitchShift.c
#: shared-bindings/audiodelays/MultiTapDelay.c
#: shared-bindings/audiodelays/PitchShift.c
Expand All @@ -3749,10 +3774,6 @@ msgstr ""
msgid "bits_per_sample must be 16"
msgstr ""

#: shared-bindings/audioi2sin/I2SIn.c
msgid "%q requires %q"
msgstr ""

#: shared-bindings/audioi2sin/I2SIn.c
#, c-format
msgid "invalid destination buffer, must be an array of type: %c"
Expand All @@ -3762,15 +3783,6 @@ msgstr ""
msgid "%q and %q must be different"
msgstr ""

#: shared-bindings/bitbangio/I2C.c shared-bindings/bitbangio/SPI.c
#: shared-bindings/busio/I2C.c shared-bindings/busio/SPI.c
msgid "Function requires lock"
msgstr ""

#: shared-bindings/bitbangio/SPI.c shared-bindings/busio/SPI.c
msgid "buffer slices must be of equal length"
msgstr ""

#: shared-bindings/bitmapfilter/__init__.c
msgid ""
"weights must be a sequence with an odd square number of elements (usually 9 "
Expand Down Expand Up @@ -4088,6 +4100,36 @@ msgstr ""
msgid "Specify exactly one of data0 or data_pins"
msgstr ""

#: shared-bindings/picogame/Display.c shared-bindings/picogame/Scene.c
#: shared-bindings/picogame/__init__.c
msgid "expected a BusDisplay"
msgstr ""

#: shared-bindings/picogame/Scene.c
msgid "scene full"
msgstr ""

#: shared-bindings/picogame/Scene.c
msgid "item not in scene"
msgstr ""

#: shared-bindings/picogame/__init__.c shared-module/msgpack/__init__.c
#: supervisor/shared/settings.c
msgid "Invalid format"
msgstr ""

#: shared-bindings/picogame/__init__.c
msgid "PAL8 needs a palette"
msgstr ""

#: shared-bindings/picogame/__init__.c
msgid "palette is empty"
msgstr ""

#: shared-bindings/picogame/__init__.c
msgid "expected a Sprite, Tilemap, Particles, Canvas, StripDraw or Triangles"
msgstr ""

#: shared-bindings/ps2io/Ps2.c
msgid "Failed sending command."
msgstr ""
Expand Down Expand Up @@ -4407,10 +4449,6 @@ msgstr ""
msgid "no default packer"
msgstr ""

#: shared-module/msgpack/__init__.c supervisor/shared/settings.c
msgid "Invalid format"
msgstr ""

#: shared-module/paralleldisplaybus/ParallelBus.c
msgid ""
"This microcontroller only supports data0=, not data_pins=, because it "
Expand Down
153 changes: 153 additions & 0 deletions ports/espressif/common-hal/picogame/Display.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// This file is part of the CircuitPython project: https://circuitpython.org
//
// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka
//
// SPDX-License-Identifier: MIT

#include "common-hal/picogame/Display.h"

#include <string.h>

#include "py/runtime.h"
#include "shared-module/picogame/__init__.h"
#include "shared-module/displayio/display_core.h"
#include "shared-bindings/displayio/__init__.h"
#include "shared-bindings/fourwire/FourWire.h"
#include "common-hal/busio/SPI.h"

#include "driver/spi_master.h"

// The esp-idf SPI device queue holds MAX_SPI_TRANSACTIONS (10) outstanding
// transactions. We keep up to two strips in flight (current transferring while
// the next is blitted), so each strip may use at most this many DMA chunks and
// still leave room: 2 * 5 <= 10. Strips needing more chunks fall back to a
// blocking send (correct, just no overlap for that strip).
#define PICOGAME_MAX_STRIP_CHUNKS 5

void common_hal_picogame_display_construct(picogame_display_obj_t *self,
busdisplay_busdisplay_obj_t *display, bool rgb444) {
self->display = display;
// RGB444 strip packing isn't implemented on this backend yet. Raise rather than silently
// ignore it: a no-op would leave the panel in RGB565 while the caller expects 444 (garbled
// output / wrong byte count). The rpi backend implements it; until this one does, fail loud.
if (rgb444) {
mp_raise_NotImplementedError(MP_ERROR_TEXT("rgb444 fast Display not supported on this port yet"));
}
self->rgb444 = false;

// The fast path queues raw DMA on the display's SPI device; only FourWire
// SPI buses expose one.
if (!mp_obj_is_type(display->bus.bus, &fourwire_fourwire_type)) {
mp_raise_ValueError(MP_ERROR_TEXT("fast Display needs a FourWire SPI display"));
}
fourwire_fourwire_obj_t *fw = MP_OBJ_TO_PTR(display->bus.bus);
self->spi = common_hal_busio_spi_get_device_handle(fw->bus);
}

// Retrieve all outstanding results for *count queued chunks, then zero the count.
static void drain(spi_device_handle_t spi, int *count) {
spi_transaction_t *rtrans;
while (*count > 0) {
spi_device_get_trans_result(spi, &rtrans, portMAX_DELAY);
(*count)--;
}
}

// Queue one strip (nbytes from buf) as up to PICOGAME_MAX_STRIP_CHUNKS DMA
// transactions. Returns the chunk count, or -1 if it would need more chunks.
static int queue_strip(spi_device_handle_t spi, spi_transaction_t *trans,
const uint16_t *buf, size_t nbytes) {
int needed = (int)((nbytes + SPI_MAX_DMA_LEN - 1) / SPI_MAX_DMA_LEN);
if (needed > PICOGAME_MAX_STRIP_CHUNKS) {
return -1;
}
const uint8_t *p = (const uint8_t *)buf;
size_t off = 0;
int n = 0;
while (off < nbytes) {
size_t chunk = nbytes - off;
if (chunk > SPI_MAX_DMA_LEN) {
chunk = SPI_MAX_DMA_LEN;
}
memset(&trans[n], 0, sizeof(spi_transaction_t));
trans[n].length = chunk * 8; // in bits
trans[n].tx_buffer = p + off;
spi_device_queue_trans(spi, &trans[n], portMAX_DELAY);
off += chunk;
n++;
}
return n;
}

void common_hal_picogame_display_render(picogame_display_obj_t *self,
mp_obj_t *items, uint8_t *kinds, size_t n,
uint16_t *buf_a, uint16_t *buf_b, size_t buf_pixels,
int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t background,
int ox, int oy) {

busdisplay_busdisplay_obj_t *display = self->display;
spi_device_handle_t spi = self->spi;

// Open the GRAM window (set region, begin transaction, RAMWR). After the
// first DATA send raises DC, DC stays high for the raw queued strips that
// follow, and CS stays low until end_transaction.
int region_w, strip_h;
int cx0 = x0, cy0 = y0, cx1 = x1, cy1 = y1; // strip_begin clamps these to the panel in place
if (!picogame_strip_begin(display, &cx0, &cy0, &cx1, &cy1, buf_pixels, &region_w, &strip_h)) {
return;
}

uint16_t *bufs[2] = { buf_a, buf_b };
spi_transaction_t trans[2][PICOGAME_MAX_STRIP_CHUNKS];
int inflight[2] = { 0, 0 }; // chunks queued from bufs[i], awaiting result

// A StripDraw callback may latch a BaseException (Ctrl-C / auto-reload). Like the portable
// renderer and the RP backend, re-raise it -- but only AFTER the queued transfers drain and
// the bus transaction closes, so hold it here and propagate below.
mp_obj_t pending = MP_OBJ_NULL;

int cur = 0;
bool first = true;
for (int sy = cy0; sy < cy1; sy += strip_h) {
int sh = picogame_imin(strip_h, cy1 - sy);
size_t nbytes = (size_t)region_w * sh * 2;
uint16_t *buf = bufs[cur];

// This buffer must be free before we overwrite it.
drain(spi, &inflight[cur]);

// Blit this strip. If the OTHER buffer has a strip in flight, its DMA
// transfer overlaps this CPU work -- the whole point of the fast path.
pending = picogame_blit_strip_layers(buf, region_w, sy, sh, cx0, items, kinds, n,
background, ox, oy);

int nch;
if (first) {
// First DATA send goes through the busdisplay so it raises DC.
display->bus.send(display->bus.bus, DISPLAY_DATA,
CHIP_SELECT_UNTOUCHED, (uint8_t *)buf, nbytes);
first = false;
} else if ((nch = queue_strip(spi, trans[cur], buf, nbytes)) >= 0) {
inflight[cur] = nch;
} else {
// Strip too large to keep two in flight: drain everything and send
// it blocking (DC already high, CS untouched).
drain(spi, &inflight[cur ^ 1]);
display->bus.send(display->bus.bus, DISPLAY_DATA,
CHIP_SELECT_UNTOUCHED, (uint8_t *)buf, nbytes);
}
cur ^= 1;
if (pending != MP_OBJ_NULL) { // callback interrupted: this strip is queued, now stop + flush
break;
}
}

drain(spi, &inflight[0]);
drain(spi, &inflight[1]);

displayio_display_bus_end_transaction(&display->bus);

if (pending != MP_OBJ_NULL) { // bus now closed -> safe to re-raise (Ctrl-C / reload)
nlr_raise(MP_OBJ_TO_PTR(pending));
}
}
33 changes: 33 additions & 0 deletions ports/espressif/common-hal/picogame/Display.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// This file is part of the CircuitPython project: https://circuitpython.org
//
// SPDX-FileCopyrightText: Copyright (c) 2026 Vladimir Smitka
//
// SPDX-License-Identifier: MIT
//
// Fast display backend (espressif): wraps an existing busdisplay and streams
// pixels with the esp-idf SPI master's queued DMA, double-buffered so the CPU
// blits the next strip while the current one transfers. Reuses the busdisplay's
// SPI device, window opcodes and dimensions -- controller/resolution agnostic.

#pragma once

#include "py/obj.h"
#include "driver/spi_master.h"
#include "shared-bindings/busdisplay/BusDisplay.h"
#include "shared-module/picogame/Sprite.h"

typedef struct {
mp_obj_base_t base;
busdisplay_busdisplay_obj_t *display;
spi_device_handle_t spi; // the busdisplay's SPI device (raw DMA queueing)
bool rgb444; // RGB444 strip packing (not yet implemented on this backend)
} picogame_display_obj_t;

void common_hal_picogame_display_construct(picogame_display_obj_t *self,
busdisplay_busdisplay_obj_t *display, bool rgb444);

void common_hal_picogame_display_render(picogame_display_obj_t *self,
mp_obj_t *items, uint8_t *kinds, size_t n,
uint16_t *buf_a, uint16_t *buf_b, size_t buf_pixels,
int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t background,
int ox, int oy);
Loading
Loading