Skip to content
Merged
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
12 changes: 12 additions & 0 deletions ports/atmel-samd/boards/pybadge/mpconfigboard.mk
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ CIRCUITPY_PARALLELDISPLAYBUS= 0
CIRCUITPY_SPITARGET = 0
CIRCUITPY_STAGE = 1

CIRCUITPY_PICOGAME = 1
CIRCUITPY_PICOGAME_FAST_DISPLAY = 1
CIRCUITPY_PICOGAME_RGB444 = 1

# Not enough room for picogame and these; all are only reachable through the Feather header.
CIRCUITPY_BLEIO_HCI = 0
CIRCUITPY_FREQUENCYIO = 0
CIRCUITPY_I2CTARGET = 0
CIRCUITPY_PS2IO = 0
CIRCUITPY_PULSEIO = 0
CIRCUITPY_SDCARDIO = 0

FROZEN_MPY_DIRS += $(TOP)/frozen/circuitpython-stage/pybadge

# We don't have room for the fonts for terminalio for certain languages,
Expand Down
153 changes: 153 additions & 0 deletions ports/atmel-samd/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 "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 "peripherals/samd/dma.h"

// shared_dma_transfer_start truncates the beat count to 16 bits, so one descriptor tops out here.
#define PICOGAME_DMA_MAX_BYTES 65535

void common_hal_picogame_display_construct(picogame_display_obj_t *self,
busdisplay_busdisplay_obj_t *display, bool rgb444) {
self->display = display;
#if CIRCUITPY_PICOGAME_RGB444
self->rgb444 = rgb444;
#else
if (rgb444) {
mp_raise_NotImplementedError(MP_ERROR_TEXT("Operation or feature not supported"));
}
self->rgb444 = false;
#endif

// The fast path needs the raw SERCOM; only FourWire SPI buses are supported.
if (!mp_obj_is_type(display->bus.bus, &fourwire_fourwire_type)) {
mp_raise_TypeError_varg(MP_ERROR_TEXT("%q must be of type %q, not %q"),
MP_QSTR_display, MP_QSTR_FourWire, mp_obj_get_type(display->bus.bus)->name);
}
fourwire_fourwire_obj_t *fw = MP_OBJ_TO_PTR(display->bus.bus);
self->sercom = (Sercom *)fw->bus->spi_desc.dev.prvt;

#if CIRCUITPY_PICOGAME_RGB444
// COLMOD; also resets a panel left in the other format by a previous program.
picogame_set_pixel_format(display, rgb444);
#endif

}

// Strips go through shared_dma_transfer_*, which handles the DMAC start errata and the RX
// overflow a TX-only transfer leaves on the SERCOM.
static void dma_finish(dma_transfer_t *xfer) {
// Bounded wait: a strip takes well under 1 ms, so a timeout tears one frame instead of
// hanging the board. close() releases the channel either way.
for (uint32_t spins = 0; spins < 2000000; spins++) {
if (shared_dma_transfer_finished(xfer)) {
break;
}
}
shared_dma_transfer_close(xfer);
}

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;
Sercom *sercom = self->sercom;

// RGB444 packs 2 px into 3 bytes, so widen the region to even x bounds.
#if CIRCUITPY_PICOGAME_RGB444
if (self->rgb444) {
x0 &= ~1;
x1 = (x1 + 1) & ~1;
if (x1 > display->core.width) {
x1 = display->core.width;
}
}
#endif

// Sets the window and starts the transaction; the strips follow as data.
int region_w, strip_h;
int cx0 = x0, cy0 = y0, cx1 = x1, cy1 = y1; // clamped to the panel by strip_begin
if (!picogame_strip_begin(display, &cx0, &cy0, &cx1, &cy1, buf_pixels, &region_w, &strip_h)) {
return;
}

uint16_t *bufs[2] = { buf_a, buf_b };
int cur = 0;
bool first = true;
dma_transfer_t xfer;
bool xfer_active = false;
#if CIRCUITPY_PICOGAME_RGB444
const bool rgb444 = self->rgb444;
#endif

// An exception from a StripDraw callback is re-raised after the DMA finishes and the
// transaction ends.
mp_obj_t pending = MP_OBJ_NULL;

for (int sy = cy0; sy < cy1; sy += strip_h) {
int sh = picogame_imin(strip_h, cy1 - sy);
uint16_t *buf = bufs[cur];

// Composes into the buffer the in-flight DMA is not reading.
pending = picogame_blit_strip_layers(buf, region_w, sy, sh, cx0, items, kinds, n, background, ox, oy);

// Packed in place, 3/4 the bytes.
#if CIRCUITPY_PICOGAME_RGB444
size_t nbytes = rgb444
? picogame_pack_rgb444(buf, (size_t)region_w * sh)
: (size_t)region_w * sh * 2;
#else
size_t nbytes = (size_t)region_w * sh * 2;
#endif

if (xfer_active) {
dma_finish(&xfer);
xfer_active = false;
}

if (first || nbytes > PICOGAME_DMA_MAX_BYTES) {
// The first strip sets DC for data through busdisplay; the DMA strips that follow
// leave it. Strips over the descriptor limit also go this way (busio splits them).
display->bus.send(display->bus.bus, DISPLAY_DATA,
CHIP_SELECT_UNTOUCHED, (uint8_t *)buf, nbytes);
first = false;
} else {
shared_dma_transfer_start(&xfer, sercom, (const uint8_t *)buf,
&sercom->SPI.DATA.reg, NULL, NULL, nbytes, 0);
if (xfer.failure != 0) { // no channel free
shared_dma_transfer_close(&xfer);
display->bus.send(display->bus.bus, DISPLAY_DATA,
CHIP_SELECT_UNTOUCHED, (uint8_t *)buf, nbytes);
} else {
xfer_active = true;
}
}
cur ^= 1;
if (pending != MP_OBJ_NULL) {
break;
}
}

if (xfer_active) {
dma_finish(&xfer);
}

displayio_display_bus_end_transaction(&display->bus);

if (pending != MP_OBJ_NULL) {
nlr_raise(MP_OBJ_TO_PTR(pending));
}
}
37 changes: 37 additions & 0 deletions ports/atmel-samd/common-hal/picogame/Display.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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 (atmel-samd): wraps an existing busdisplay and streams pixels with a DMAC
// channel of our own, double-buffered so the CPU blits the next strip while the current one is on
// the wire. Reuses the busdisplay's SERCOM, window opcodes and dimensions -- controller and
// resolution agnostic.
//
// Why this pays on a SAMD51: busio already sends through the DMAC, but it busy-waits for each
// transfer to land, so the compose time is added to the wire time instead of hidden under it.

#pragma once

#include "py/obj.h"

#include "include/sam.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;
Sercom *sercom; // the busdisplay's SERCOM; strips go out on the port's shared DMA
bool rgb444;
} 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);
9 changes: 8 additions & 1 deletion shared-bindings/picogame/Canvas.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
#include "shared-module/picogame/pg_compat.h"
#include "shared-bindings/picogame/Canvas.h"
#include "shared-bindings/picogame/Bitmap.h"
#if CIRCUITPY_FONTIO
#include "shared-bindings/fontio/BuiltinFont.h"
#endif
#include "shared-module/picogame/Canvas.h"

//| class Canvas:
Expand Down Expand Up @@ -512,10 +514,12 @@ static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(canvas_frame3d_obj, 7, 7, canvas_fram
//| is drawn; no memory is retained between calls. Only ASCII characters are
//| supported. If ``bg`` is given the glyph background is filled with it,
//| otherwise it is transparent. Inside a `StripDraw` callback the view is a
//| Canvas, so ``view.text(...)`` draws text directly into the frame."""
//| Canvas, so ``view.text(...)`` draws text directly into the frame.
//| Raises :py:class:`NotImplementedError` in builds without `fontio`."""
//| ...
//|
static mp_obj_t canvas_text(size_t n, const mp_obj_t *a) {
#if CIRCUITPY_FONTIO
const char *s = mp_obj_str_get_str(a[3]);
mp_int_t fg = mp_obj_get_int(a[4]);
const void *font = MP_OBJ_TO_PTR(mp_arg_validate_type(a[5], &fontio_builtinfont_type, MP_QSTR_font));
Expand All @@ -524,6 +528,9 @@ static mp_obj_t canvas_text(size_t n, const mp_obj_t *a) {
picogame_canvas_text(cv_self(a[0]), mp_obj_get_int(a[1]), mp_obj_get_int(a[2]),
s, (uint16_t)fg, bg, has_bg, font);
return mp_const_none;
#else
mp_raise_NotImplementedError(MP_ERROR_TEXT("Operation or feature not supported"));
#endif
}
static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(canvas_text_obj, 6, 7, canvas_text);

Expand Down
4 changes: 4 additions & 0 deletions shared-module/picogame/Canvas.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
#include "shared-module/picogame/Canvas.h"
#include "shared-module/picogame/Bitmap.h"
#include "shared-module/picogame/__init__.h"
#if CIRCUITPY_FONTIO
#include "shared-module/fontio/BuiltinFont.h"
#endif
#include "shared-bindings/displayio/Bitmap.h"

// Thin wrappers over the shared int32 accumulator (dx1,dy1,dx2,dy2 are contiguous int32 at the
Expand Down Expand Up @@ -558,6 +560,7 @@ void picogame_blit_canvas(
// font's 1-bit atlas on the fly (no Python glyph cache, no per-call Bitmap/Sprite). Because the
// StripDraw `view` is a Canvas pointing at the live strip buffer, view.text() draws immediate-mode
// text into the frame with zero retained RAM - the same primitive serves retained Canvas screens.
#if CIRCUITPY_FONTIO
void picogame_canvas_text(picogame_canvas_obj_t *cv, int x, int y, const char *text,
uint16_t fg, uint16_t bg, bool has_bg, const void *font) {
const fontio_builtinfont_t *f = font;
Expand Down Expand Up @@ -609,6 +612,7 @@ void picogame_canvas_text(picogame_canvas_obj_t *cv, int x, int y, const char *t
}
mark(cv, x0, y, x, y + fh);
}
#endif

// Fill a screen-space triangle batch with per-triangle band reject - shared by the
// Canvas.fill_triangles binding and the compositor's Triangles layer (one loop, one place).
Expand Down
Loading