Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
34b53b9
EPaperDisplay: allow update_refresh_mode to change refresh_sequence
mikeysklar Oct 29, 2026
3fea298
epaperdisplay: add write_previous_ram_command
mikeysklar Oct 29, 2026
819974a
epaperdisplay: add keep_awake to skip reset and sleep between refreshes
mikeysklar Oct 29, 2026
f2ffcb4
epaperdisplay: add lag_frames drive
mikeysklar Nov 1, 2026
539e406
epaperdisplay: add clean() for balanced wipes with lag drive
mikeysklar Nov 1, 2026
8898cc9
displayio: raise before releasing a lag display whose bus stays held
mikeysklar Nov 1, 2026
7316a55
epaperdisplay: try one reset when shutdown finds BUSY stuck
mikeysklar Nov 1, 2026
868602f
videocore: allow 1 bit per pixel sources
mikeysklar Nov 1, 2026
2faca5c
videogif: decode GIFs to 1 bit per pixel
mikeysklar Nov 1, 2026
d922e31
gifio: let a board raise the GIF width limit
mikeysklar Nov 1, 2026
34870bd
videocore: show 1 bit plain buffers on a monochrome display
mikeysklar Oct 31, 2026
7a50f9e
videocore: document that a rotated display raises ValueError
mikeysklar Oct 31, 2026
d88f2af
adafruit_feather_rp2040_thinkink: enable videocore
mikeysklar Oct 31, 2026
2d8485c
epaperdisplay: add CIRCUITPY_EPAPERDISPLAY_FAST, off on atmel-samd
mikeysklar Nov 2, 2026
6fe0e28
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 2, 2026
83c0763
Merge branch 'video-gif' into epaperdisplay-fast
mikeysklar Nov 3, 2026
950ba52
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 3, 2026
3301339
Merge branch 'video-gif' into epaperdisplay-fast
mikeysklar Nov 3, 2026
2db8532
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 3, 2026
8d19a33
Update circuitpython.pot after merging video-gif
mikeysklar Nov 3, 2026
9f6f9b3
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 3, 2026
43ee310
Merge branch 'video-gif' into epaperdisplay-fast
mikeysklar Nov 3, 2026
5b56c73
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 3, 2026
8cbd70e
Warn that fast e-paper updates exceed panel limits
mikeysklar Nov 3, 2026
2622cda
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 3, 2026
a6dbf89
Merge branch 'video-gif' into epaperdisplay-fast
mikeysklar Nov 4, 2026
92d7db6
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 4, 2026
dac704b
videocore: VideoBuffer sets the pixel format, not GifDecoder
mikeysklar Nov 4, 2026
5697d28
epaperdisplay: explain how lag drive relates to FAST
mikeysklar Nov 4, 2026
a3bc4c6
epaperdisplay: same API on every build
mikeysklar Nov 4, 2026
665f918
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 4, 2026
c9f3c2a
epaperdisplay: fast updates on only for espressif and raspberrypi
mikeysklar Nov 4, 2026
39dc1c7
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 4, 2026
949571a
Merge branch 'video-gif' into epaperdisplay-fast
mikeysklar Nov 4, 2026
4d40876
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 4, 2026
fa90d44
epaperdisplay: lag drive on for espressif and raspberrypi
mikeysklar Nov 4, 2026
869d663
epaperdisplay: explain why lag drive stays balanced
mikeysklar Nov 4, 2026
6f15261
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 4, 2026
d14c912
Merge branch 'video-gif' into epaperdisplay-fast
mikeysklar Nov 4, 2026
53b156d
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 4, 2026
bcbe057
Merge branch 'video-gif' into epaperdisplay-fast
mikeysklar Nov 4, 2026
e43ae80
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 4, 2026
3552400
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 5, 2026
f418a7b
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 5, 2026
95af59d
gifio, videogif: size the GIF line buffer to the canvas
mikeysklar Nov 10, 2026
5414376
Merge remote-tracking branch 'origin/epaperdisplay-fast' into epaperd…
mikeysklar Nov 10, 2026
8543606
Merge remote-tracking branch 'origin/epaperdisplay-fast' into epaperd…
mikeysklar Nov 10, 2026
4f72166
Merge remote-tracking branch 'origin/epaperdisplay-fast' into epaperd…
mikeysklar Nov 11, 2026
372ad51
gifio: pass the line buffer to GIF_init
mikeysklar Nov 11, 2026
100c967
gifio: free the line buffer in deinit
mikeysklar Nov 11, 2026
3900f7f
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 11, 2026
d1ac8bc
videocore: rotate 1-bit video on rotated displays
mikeysklar Nov 11, 2026
e84b8d6
Turn off gifio on the makerdiary nRF52840 dongle
mikeysklar Nov 11, 2026
f0b0e92
videocore: restore the FB_GETTER indent
mikeysklar Nov 11, 2026
cb26325
Merge epaperdisplay-fast into epaperdisplay-video
mikeysklar Nov 12, 2026
ddca844
Turn gifio back on for the makerdiary nRF52840 dongle
mikeysklar Nov 12, 2026
3bc5169
Merge epaperdisplay-fast into epaperdisplay-video
mikeysklar Nov 12, 2026
348a84f
Merge branch 'epaperdisplay-fast' into epaperdisplay-video
mikeysklar Nov 12, 2026
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
8 changes: 5 additions & 3 deletions lib/AnimatedGIF/AnimatedGIF_circuitpy.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
#define MAX_CHUNK_SIZE 255
#define LZW_BUF_SIZE (6 * MAX_CHUNK_SIZE)
#define LZW_HIGHWATER (4 * MAX_CHUNK_SIZE)
#define MAX_WIDTH 320
#define FILE_BUF_SIZE 4096

#define PIXEL_FIRST 0
Expand Down Expand Up @@ -140,7 +139,7 @@ typedef struct gif_image_tag
//unsigned char *pFrameBuffer;
unsigned int *pFrameBuffer;
unsigned char *pPixels, *pOldPixels;
unsigned char ucLineBuf[MAX_WIDTH]; // current line
unsigned char *ucLineBuf; // current line, at least iCanvasWidth bytes. Set by GIF_init().
unsigned char ucFileBuf[FILE_BUF_SIZE]; // holds temp data and pixel stack
unsigned short pPalette[384]; // can hold RGB565 or RGB888 - set in begin()
unsigned short pLocalPalette[384]; // color palettes for GIF images
Expand All @@ -166,7 +165,10 @@ int GIF_getComment(GIFIMAGE *pGIF, char *destBuffer);
int GIF_getInfo(GIFIMAGE *pGIF, GIFINFO *pInfo);
int GIF_getLastError(GIFIMAGE *pGIF);
int GIF_getLoopCount(GIFIMAGE *pGIF);
int GIF_init(GIFIMAGE *pGIF);
// Reads the header and fills in the canvas size. pLineBuf holds one line of pixels and must be
// at least iCanvasWidth bytes, or this fails with GIF_TOO_WIDE. Pass NULL and 0 to only measure
// the canvas, then call again with a buffer before decoding. Returns 1 on success, 0 on failure.
int GIF_init(GIFIMAGE *pGIF, unsigned char *pLineBuf, int iLineBufSize);
void GIF_setDrawCallback(GIFIMAGE *pGIF, GIF_DRAW_CALLBACK *pfnDraw);
void GIF_scaleHalf(uint16_t *pCurrent, uint16_t *pPrev, int iWidth, int bBigEndian);

Expand Down
21 changes: 15 additions & 6 deletions lib/AnimatedGIF/gif.c
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,16 @@ int GIF_getLastError(GIFIMAGE *pGIF)
return pGIF->iError;
} /* GIF_getLastError() */

int GIF_init(GIFIMAGE *pGIF) {
return GIFInit(pGIF);
int GIF_init(GIFIMAGE *pGIF, unsigned char *pLineBuf, int iLineBufSize) {
pGIF->ucLineBuf = pLineBuf;
if (!GIFInit(pGIF)) {
return 0;
}
if (pLineBuf != NULL && pGIF->iCanvasWidth > iLineBufSize) {
pGIF->iError = GIF_TOO_WIDE;
return 0;
}
return 1;
}

//
Expand Down Expand Up @@ -253,10 +261,6 @@ static int GIFInit(GIFIMAGE *pGIF)
if (!GIFParseInfo(pGIF, 1)) // gather info for the first frame
return 0; // something went wrong; not a GIF file?
(*pGIF->pfnSeek)(&pGIF->GIFFile, 0); // seek back to start of the file
if (pGIF->iCanvasWidth > MAX_WIDTH) { // need to allocate more space
pGIF->iError = GIF_TOO_WIDE;
return 0;
}
return 1;
} /* GIFInit() */

Expand Down Expand Up @@ -445,6 +449,11 @@ static int GIFParseInfo(GIFIMAGE *pPage, int bInfoOnly)
pPage->iWidth = INTELSHORT(&p[iOffset+4]);
pPage->iHeight = INTELSHORT(&p[iOffset+6]);
iOffset += 8;
// The line buffer is sized to the canvas, so a frame must fit inside it.
if (pPage->iX + pPage->iWidth > pPage->iCanvasWidth) {
pPage->iError = GIF_DECODE_ERROR;
return 0;
}

/* Image descriptor
7 6 5 4 3 2 1 0 M=0 - use global color map, ignore pixel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ CHIP_VARIANT = RP2040
CHIP_FAMILY = rp2

EXTERNAL_FLASH_DEVICES = "GD25Q64C,W25Q64JVxQ"

CIRCUITPY_VIDEOCORE = 1
5 changes: 2 additions & 3 deletions py/circuitpy_mpconfig.mk
Original file line number Diff line number Diff line change
Expand Up @@ -868,9 +868,8 @@ CFLAGS += -DCIRCUITPY_ULAB=$(CIRCUITPY_ULAB)
# element-wise array arithmetic roughly 1.5x slower.
CIRCUITPY_ULAB_OPTIMIZE_SIZE ?= 0

# Video pipeline: playing video sources into framebuffers. Requires a port
# with a video source such as mipicsi and a framebuffer output such as
# mipidsi.
# Video pipeline: playing video sources into framebuffers or plain buffers.
# A source can be a port's camera, such as mipicsi, or a GIF file with videogif.
CIRCUITPY_VIDEOCORE ?= 0
CFLAGS += -DCIRCUITPY_VIDEOCORE=$(CIRCUITPY_VIDEOCORE)

Expand Down
5 changes: 2 additions & 3 deletions shared-bindings/gifio/OnDiskGif.c
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,8 @@
//|
//| If the image is too large it will be cropped at the bottom and right when displayed.
//|
//| **Limitations**: The image width is limited to 320 pixels at present. `ValueError`
//| will be raised if the image is too wide. The height
//| is not limited but images that are too large will cause a memory exception.
//| **Limitations**: The width and height are not limited, but images that are too
//| large will cause a memory exception.
//| """
//| ...
//|
Expand Down
46 changes: 34 additions & 12 deletions shared-bindings/videocore/VideoBuffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,18 @@
//| Assigning a Group that contains a framebuffer target as a rotated
//| display's ``root_group`` raises `ValueError`.
//|
//| When the target is a plain buffer, frames are written compactly (row
//| stride equals ``width * bits_per_pixel / 8``) and can be read from
//| Python at any time, for example to process or save camera pixels.
//| When the target is a plain buffer, frames are written with no gap
//| between rows, so each row takes ``(width * bits_per_pixel + 7) // 8``
//| bytes. Python code can read the buffer at any time, for example to
//| process or save camera pixels. 1-bit pixels are packed together, and
//| each row is padded out to a whole byte.
//| A Group shows a plain buffer target only when its pixels are 16 bits
//| and the display uses 16-bit color, or its pixels are 1 bit and the
//| display is monochrome with the leftmost pixel in the top bit of each
//| byte, such as `epaperdisplay.EPaperDisplay`. A 1 bit means white.
//| On a tricolor display with separate black and color RAM, a 1 bit video
//| only draws black and white, and a highlight colored layer below it shows
//| through. Other tricolor displays don't draw it.
//|
//| See the `videocore` module overview for complete examples.
//| """
Expand All @@ -44,41 +53,54 @@
//| framebuffer: Union[
//| circuitpython_typing.FrameBuffer, circuitpython_typing.WriteableBuffer
//| ],
//| *,
//| bits_per_pixel: int = 16,
//| ) -> None:
//| """Create a VideoBuffer that plays video into the given framebuffer
//|
//| :param framebuffer: The framebuffer to copy video frames into, or a
//| writable buffer such as a ``bytearray`` or ``memoryview``.
//| :param int bits_per_pixel: The pixel format of a writable buffer: 16
//| for color (RGB565), or 1 for black and white. A framebuffer uses
//| its own color depth instead.
//| """
//|
static mp_obj_t videocore_videobuffer_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
enum { ARG_framebuffer, NUM_ARGS };
enum { ARG_framebuffer, ARG_bits_per_pixel, NUM_ARGS };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_framebuffer, MP_ARG_REQUIRED | MP_ARG_OBJ },
{ MP_QSTR_bits_per_pixel, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 16} },
};

mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
MP_STATIC_ASSERT(MP_ARRAY_SIZE(allowed_args) == NUM_ARGS);
mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

const mp_int_t bits_per_pixel = args[ARG_bits_per_pixel].u_int;
if (bits_per_pixel != 1 && bits_per_pixel != 16) {
mp_arg_error_invalid(MP_QSTR_bits_per_pixel);
}

videocore_videobuffer_obj_t *self = mp_obj_malloc_with_finaliser(videocore_videobuffer_obj_t, &videocore_videobuffer_type);
common_hal_videocore_videobuffer_construct(self, args[ARG_framebuffer].u_obj);
common_hal_videocore_videobuffer_construct(self, args[ARG_framebuffer].u_obj, bits_per_pixel);
return MP_OBJ_FROM_PTR(self);
}

//| def play(self, source: circuitpython_typing.VideoSource) -> None:
//| """Begin streaming the given video source into the target.
//|
//| The source streams continuously, writing each new frame where the
//| VideoBuffer tells it to, replacing the previous one. The source's
//| frame format must be byte aligned. The source's size and format are
//| then fixed for the duration of the ``play()``; sources that change
//| them require ``stop()`` and a new ``play()``.
//| VideoBuffer tells it to, replacing the previous one. The source can
//| use 1 bit per pixel, or a whole number of bytes per pixel.
//| The source's size and format are then fixed for the duration of the
//| ``play()``; sources that change them require ``stop()`` and a new
//| ``play()``.
//|
//| When the target is a framebuffer, its bits per pixel must match the
//| framebuffer color depth and the whole frame must fit within the
//| framebuffer at the current ``x``/``y`` position. An exception may be
//| raised if the video can't be played directly into the framebuffer.
//| source and be a whole number of bytes, and the whole frame must fit
//| within the framebuffer at the current ``x``/``y`` position. An
//| exception may be raised if the video can't be played directly into
//| the framebuffer.
//|
//| When the target is a plain buffer, it must be large enough for a
//| whole frame.
Expand Down
18 changes: 14 additions & 4 deletions shared-bindings/videogif/GifDecoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,14 @@
//| :param str filename: The name of a GIF file, such as ``"/animation.gif"``.
//| The GifDecoder closes the file when it is deinitialized.
//|
//| Frames are RGB565, in the byte order of the display or framebuffer
//| that shows them. Raises `ValueError` if the file is not a GIF or if
//| the GIF is wider than 320 pixels.
//| The `videocore.VideoBuffer` playing the GIF sets the pixel format.
//| 16-bit frames are RGB565, in the byte order of the display or
//| framebuffer that shows them. 1-bit frames pack eight pixels into
//| each byte, with the leftmost pixel in the most significant bit. A
//| pixel is white (1) when its brightness is at least 128 out of 255,
//| so a GIF that uses only black and white converts exactly.
//|
//| Raises `ValueError` if the file is not a GIF.
//| """
//| ...
//|
Expand Down Expand Up @@ -206,7 +211,11 @@ static mp_int_t videogif_gifdecoder_videosource_get_height(mp_obj_t self_in) {
}

static mp_int_t videogif_gifdecoder_get_bits_per_pixel(mp_obj_t self_in) {
return 16;
return common_hal_videogif_gifdecoder_get_bits_per_pixel(native_gifdecoder(self_in));
}

static bool videogif_gifdecoder_set_bits_per_pixel(mp_obj_t self_in, mp_int_t bits_per_pixel) {
return common_hal_videogif_gifdecoder_set_bits_per_pixel(native_gifdecoder(self_in), bits_per_pixel);
}

static const videosource_p_t videogif_gifdecoder_videosource_protocol = {
Expand All @@ -219,6 +228,7 @@ static const videosource_p_t videogif_gifdecoder_videosource_protocol = {
.get_width = videogif_gifdecoder_videosource_get_width,
.get_height = videogif_gifdecoder_videosource_get_height,
.get_bits_per_pixel = videogif_gifdecoder_get_bits_per_pixel,
.set_bits_per_pixel = videogif_gifdecoder_set_bits_per_pixel,
};

MP_DEFINE_CONST_OBJ_TYPE(
Expand Down
9 changes: 7 additions & 2 deletions shared-bindings/videogif/GifDecoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@
extern const mp_obj_type_t videogif_gifdecoder_type;

// Open the GIF in file, which the constructor opened from its filename. Frames
// are RGB565.
// Raises when the file isn't a GIF or is wider than AnimatedGIF supports.
// are 16-bit until set_bits_per_pixel asks for 1. Raises when the file isn't a
// GIF or is wider than AnimatedGIF supports.
void common_hal_videogif_gifdecoder_construct(videogif_gifdecoder_obj_t *self, mp_obj_t file);
// Stop playing, free the decoder and close the file. Safe to call more than
// once.
void common_hal_videogif_gifdecoder_deinit(videogif_gifdecoder_obj_t *self);
bool common_hal_videogif_gifdecoder_deinited(videogif_gifdecoder_obj_t *self);
mp_int_t common_hal_videogif_gifdecoder_get_width(videogif_gifdecoder_obj_t *self);
mp_int_t common_hal_videogif_gifdecoder_get_height(videogif_gifdecoder_obj_t *self);
mp_int_t common_hal_videogif_gifdecoder_get_bits_per_pixel(videogif_gifdecoder_obj_t *self);
// Decode frames to 1 or 16 bits per pixel from the next play. Returns false
// while playing or for another value. Never raises.
bool common_hal_videogif_gifdecoder_set_bits_per_pixel(videogif_gifdecoder_obj_t *self,
mp_int_t bits_per_pixel);

// Videosource protocol steps, driven by videocore.VideoBuffer. Frames are
// decoded by the supervisor background tick, never in an interrupt. play
Expand Down
21 changes: 11 additions & 10 deletions shared-module/gifio/OnDiskGif.c
Original file line number Diff line number Diff line change
Expand Up @@ -162,16 +162,15 @@ void common_hal_gifio_ondiskgif_construct(gifio_ondiskgif_t *self, mp_obj_t file
}
self->gif.GIFFile.iSize = (int32_t)size;

int result = GIF_init(&self->gif);
if (result != 1) {
switch (self->gif.iError) {
case GIF_TOO_WIDE:
mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be <= %d"), MP_QSTR_width, MAX_WIDTH);
break;
default:
mp_arg_error_invalid(MP_QSTR_file);
break;
}
// Measure the canvas first, then hand the decoder a line buffer of that width.
if (GIF_init(&self->gif, NULL, 0) != 1) {
mp_arg_error_invalid(MP_QSTR_file);
}
// One line of 8-bit pixels. It holds no pointers, so the GC doesn't scan it.
int width = self->gif.iCanvasWidth;
unsigned char *line_buf = m_malloc_without_collect(width);
if (GIF_init(&self->gif, line_buf, width) != 1) {
mp_arg_error_invalid(MP_QSTR_file);
}

int bpp = 16;
Expand Down Expand Up @@ -201,6 +200,8 @@ void common_hal_gifio_ondiskgif_deinit(gifio_ondiskgif_t *self) {
return;
}
self->file = NULL;
m_del(uint8_t, self->gif.ucLineBuf, self->gif.iCanvasWidth);
self->gif.ucLineBuf = NULL;
Comment thread
mikeysklar marked this conversation as resolved.
Resolved
common_hal_displayio_bitmap_deinit(self->bitmap);
self->bitmap = NULL;
self->palette = NULL;
Expand Down
Loading
Loading