Skip to content
Closed
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
7 changes: 6 additions & 1 deletion lib/AnimatedGIF/AnimatedGIF_circuitpy.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@
#define MAX_CHUNK_SIZE 255
#define LZW_BUF_SIZE (6 * MAX_CHUNK_SIZE)
#define LZW_HIGHWATER (4 * MAX_CHUNK_SIZE)
#define MAX_WIDTH 320
// Make ports set the width from mpconfigboard.mk. gif.c includes only this header, and the
// zephyr-cp build passes no width, so it needs this default.
#ifndef CIRCUITPY_GIFIO_MAX_WIDTH
#define CIRCUITPY_GIFIO_MAX_WIDTH 320
#endif
#define MAX_WIDTH CIRCUITPY_GIFIO_MAX_WIDTH
#define FILE_BUF_SIZE 4096

#define PIXEL_FIRST 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ CHIP_FAMILY = rp2
EXTERNAL_FLASH_DEVICES = "GD25Q64C,W25Q64JVxQ"

CIRCUITPY_EPAPERDISPLAY_LAG = 1
CIRCUITPY_GIFIO_MAX_WIDTH = 800
CIRCUITPY_VIDEOCORE = 1
9 changes: 6 additions & 3 deletions py/circuitpy_mpconfig.mk
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,10 @@ CFLAGS += -DCIRCUITPY_GETPASS=$(CIRCUITPY_GETPASS)
CIRCUITPY_GIFIO ?= $(CIRCUITPY_DISPLAYIO)
CFLAGS += -DCIRCUITPY_GIFIO=$(CIRCUITPY_GIFIO)

# Widest GIF that can be decoded. Each pixel costs a byte per open GIF.
CIRCUITPY_GIFIO_MAX_WIDTH ?= 320
CFLAGS += -DCIRCUITPY_GIFIO_MAX_WIDTH=$(CIRCUITPY_GIFIO_MAX_WIDTH)

CIRCUITPY_GNSS ?= 0
CFLAGS += -DCIRCUITPY_GNSS=$(CIRCUITPY_GNSS)

Expand Down Expand Up @@ -865,9 +869,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
2 changes: 1 addition & 1 deletion shared-bindings/gifio/OnDiskGif.c
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ mp_obj_t gifio_open_file(mp_obj_t file) {
//|
//| 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`
//| **Limitations**: The image width is limited to 320 pixels on most boards. `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.
//| """
Expand Down
34 changes: 24 additions & 10 deletions shared-bindings/videocore/VideoBuffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,21 @@
//| unsupported position directly raises an error, but moving a containing
//| Group may give garbled output instead.
//|
//| 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.
//| A VideoBuffer can only be translated. Showing it on a rotated display, or
//| in a scaled Group, raises `ValueError`.
//|
//| 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 Down Expand Up @@ -67,15 +79,17 @@ static mp_obj_t videocore_videobuffer_make_new(const mp_obj_type_t *type, size_t
//| """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
26 changes: 20 additions & 6 deletions shared-bindings/videogif/GifDecoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -72,35 +72,49 @@
//| self,
//| file: Union[str, typing.BinaryIO],
//| *,
//| bits_per_pixel: int = 16,
//| reverse_bytes_in_word: bool = False,
//| ) -> None:
//| """Open a GIF file so it can be played.
//|
//| :param file: The name of a GIF file, such as ``"/animation.gif"``,
//| or a GIF file opened in binary mode.
//| :param int bits_per_pixel: The pixel format of each frame. Use 16
//| for color pixels (RGB565), or 1 for black and white pixels.
//| With 1, eight pixels are packed 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.
//| :param bool reverse_bytes_in_word: Swap the two bytes of each 16-bit
//| pixel. Use the same value as your display, such as
//| `busdisplay.BusDisplay`'s ``reverse_bytes_in_word``.
//| `busdisplay.BusDisplay`'s ``reverse_bytes_in_word``. It has no
//| effect when ``bits_per_pixel`` is 1.
//|
//| Frames are RGB565. Raises `ValueError` if the file is not a GIF or if
//| the GIF is wider than 320 pixels.
//| Raises `ValueError` if ``bits_per_pixel`` is not 1 or 16, if the
//| file is not a GIF, or if the GIF is wider than the board supports
//| (320 pixels on most boards).
//| """
//| ...
//|
static mp_obj_t videogif_gifdecoder_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
enum { ARG_file, ARG_reverse_bytes_in_word, NUM_ARGS };
enum { ARG_file, ARG_bits_per_pixel, ARG_reverse_bytes_in_word, NUM_ARGS };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_file, MP_ARG_REQUIRED | MP_ARG_OBJ },
{ MP_QSTR_bits_per_pixel, MP_ARG_INT | MP_ARG_KW_ONLY, {.u_int = 16} },
{ MP_QSTR_reverse_bytes_in_word, MP_ARG_BOOL | MP_ARG_KW_ONLY, {.u_bool = false} },
};
MP_STATIC_ASSERT(MP_ARRAY_SIZE(allowed_args) == NUM_ARGS);
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_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);
}
mp_obj_t file = gifio_open_file(args[ARG_file].u_obj);

videogif_gifdecoder_obj_t *self = mp_obj_malloc_with_finaliser(videogif_gifdecoder_obj_t, &videogif_gifdecoder_type);
common_hal_videogif_gifdecoder_construct(self, file, args[ARG_reverse_bytes_in_word].u_bool);
common_hal_videogif_gifdecoder_construct(self, file, bits_per_pixel, args[ARG_reverse_bytes_in_word].u_bool);
return MP_OBJ_FROM_PTR(self);
}

Expand Down Expand Up @@ -206,7 +220,7 @@ 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 const videosource_p_t videogif_gifdecoder_videosource_protocol = {
Expand Down
7 changes: 4 additions & 3 deletions shared-bindings/videogif/GifDecoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@

extern const mp_obj_type_t videogif_gifdecoder_type;

// Open the GIF in file (a file opened in binary mode). Frames are RGB565.
// Raises when the file isn't a GIF or is wider than AnimatedGIF supports.
// Open the GIF in file (a file opened in binary mode). bits_per_pixel is 1 or
// 16. 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,
bool reverse_bytes_in_word);
mp_int_t bits_per_pixel, bool reverse_bytes_in_word);
// Stop playing, free the decoder and drop 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);

// Videosource protocol steps, driven by videocore.VideoBuffer. Frames are
// decoded by the supervisor background tick, never in an interrupt. play
Expand Down
104 changes: 85 additions & 19 deletions shared-module/videocore/VideoBuffer.c
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,15 @@ static void videocore_videobuffer_update_area(videocore_videobuffer_obj_t *self)
self->current_area.next = NULL;
}

// Bytes in one row of a source frame.
// Bytes in one row of a source frame. 1-bit pixels are packed and each row is
// padded to a whole byte. Framebuffer targets always use whole bytes (see
// read_source_format).
static size_t videocore_videobuffer_row_bytes(mp_int_t width, mp_int_t bits_per_pixel) {
return ((size_t)width * bits_per_pixel + 7) / 8;
}

static size_t videocore_videobuffer_source_row_bytes(videocore_videobuffer_obj_t *self) {
return (size_t)self->source_width * self->source_bits_per_pixel / 8;
return videocore_videobuffer_row_bytes(self->source_width, self->source_bits_per_pixel);
}

// Bytes a frame covers in a framebuffer with this row stride, from its first
Expand Down Expand Up @@ -321,12 +327,13 @@ static void videocore_videobuffer_read_source_format(videocore_videobuffer_obj_t
const videosource_p_t *protocol = videosource_check(source);

mp_int_t bits_per_pixel = protocol->get_bits_per_pixel(source);
if (bits_per_pixel % 8 != 0) {
// Whole bytes per pixel, or 1 bit packed into each byte.
if (bits_per_pixel % 8 != 0 && bits_per_pixel != 1) {
mp_raise_ValueError(MP_ERROR_TEXT("video format must be byte aligned"));
}
mp_int_t width = protocol->get_width(source);
mp_int_t height = protocol->get_height(source);
size_t frame_bytes = (size_t)((width * height * bits_per_pixel) / 8);
size_t frame_bytes = (size_t)height * videocore_videobuffer_row_bytes(width, bits_per_pixel);

if (self->framebuffer_protocol != NULL) {
mp_int_t color_depth = FB_GETTER(get_color_depth, 16);
Expand Down Expand Up @@ -585,6 +592,75 @@ static mp_obj_t videocore_videobuffer_draw_protocol_self(mp_obj_t protocol_conta
return protocol_container;
}

// Whether a layer above already claimed a pixel near the overlap. Checks whole words, which may
// include pixels between overlap rows or just outside the overlap. That can only make the caller
// take its slower path; it never misses a claimed pixel.
static bool videocore_videobuffer_overlap_claimed(const uint32_t *mask, const displayio_area_t *area,
const displayio_area_t *overlap) {
const size_t span_px = displayio_area_width(area);
const size_t first_px = (size_t)(overlap->y1 - area->y1) * span_px + (overlap->x1 - area->x1);
const size_t end_px = (size_t)(overlap->y2 - 1 - area->y1) * span_px + (overlap->x2 - area->x1);
for (size_t word = first_px / 32; word <= (end_px - 1) / 32; word++) {
if (mask[word] != 0) {
return true;
}
}
return false;
}

// Copy a 1 bit per pixel plain buffer into a 1 bit display's buffer. Both are MSB first with
// 1 = white, the same as a monochrome EPaperDisplay's first RAM pass before any inversion.
// Whole bytes are copied when the rects line up and nothing above claimed these pixels.
static bool videocore_videobuffer_fill_area_1bit(videocore_videobuffer_obj_t *self,
const _displayio_colorspace_t *colorspace, const displayio_area_t *area, const displayio_area_t *overlap,
bool full_coverage, uint32_t *mask, uint32_t *buffer) {
// Every grayscale bit of a 1 bit pixel is that pixel, so this also serves each grayscale pass.
if (!colorspace->pixels_in_byte_share_row || !colorspace->reverse_pixels_in_byte ||
!colorspace->grayscale) {
return false;
}
const size_t span_px = displayio_area_width(area);
const size_t width = displayio_area_width(overlap);
const size_t row_bytes = videocore_videobuffer_source_row_bytes(self);
const size_t x_in_frame = overlap->x1 - self->current_area.x1;
const size_t x_in_area = overlap->x1 - area->x1;
const uint8_t *frame = self->stream_base;
uint8_t *out = (uint8_t *)buffer;

const bool whole_bytes = span_px % 8 == 0 && x_in_area % 8 == 0 && x_in_frame % 8 == 0 && width % 8 == 0 &&
!videocore_videobuffer_overlap_claimed(mask, area, overlap);
for (int16_t y = overlap->y1; y < overlap->y2; y++) {
const size_t px = (size_t)(y - area->y1) * span_px + x_in_area;
const uint8_t *row = frame + (size_t)(y - self->current_area.y1) * row_bytes;
if (whole_bytes) {
memcpy(out + px / 8, row + x_in_frame / 8, width / 8);
// Full coverage ends the layer walk, so filling the mask once below is enough.
if (!full_coverage) {
for (size_t p = px; p < px + width; p++) {
mask[p / 32] |= 1u << (p % 32);
}
}
continue;
}
for (size_t i = 0; i < width; i++) {
const size_t p = px + i;
if (mask[p / 32] & (1u << (p % 32))) {
continue;
}
const size_t f = x_in_frame + i;
if (row[f / 8] & (0x80 >> (f % 8))) {
out[p / 8] |= 0x80 >> (p % 8);
}
mask[p / 32] |= 1u << (p % 32);
}
}
if (whole_bytes && full_coverage) {
// A FramebufferDisplay writes nothing back when the mask is empty.
memset(mask, 0xff, (displayio_area_size(area) + 31) / 32 * sizeof(uint32_t));
}
return full_coverage;
}

static bool videocore_videobuffer_draw_fill_area(mp_obj_t self_in, const _displayio_colorspace_t *colorspace, const displayio_area_t *area, uint32_t *mask, uint32_t *buffer) {
videocore_videobuffer_obj_t *self = MP_OBJ_TO_PTR(self_in);
// buffer isn't necessarily the framebuffer. It may be a scratch buffer.
Expand Down Expand Up @@ -612,21 +688,7 @@ static bool videocore_videobuffer_draw_fill_area(mp_obj_t self_in, const _displa
const uint8_t *frame = (const uint8_t *)self->stream_base;
uint8_t *out = (uint8_t *)buffer;

// Check whole words. This may include pixels between overlap rows or
// just outside the overlap, which can only make us take the slower
// per-pixel path below; it never misses a claimed pixel.
const size_t first_px = (size_t)(overlap.y1 - area->y1) * span_px +
(overlap.x1 - area->x1);
const size_t end_px = (size_t)(overlap.y2 - 1 - area->y1) * span_px +
(overlap.x2 - area->x1);
bool masked = false;
for (size_t word = first_px / 32; word <= (end_px - 1) / 32; word++) {
if (mask[word] != 0) {
masked = true;
break;
}
}
if (full_coverage && !masked) {
if (full_coverage && !videocore_videobuffer_overlap_claimed(mask, area, &overlap)) {
// Sole coverer: nothing was composed, so the driver skips the copy.
return true;
}
Expand Down Expand Up @@ -658,6 +720,10 @@ static bool videocore_videobuffer_draw_fill_area(mp_obj_t self_in, const _displa
return full_coverage;
}

if (colorspace->depth == 1 && self->source_bits_per_pixel == 1) {
return videocore_videobuffer_fill_area_1bit(self, colorspace, area, &overlap, full_coverage, mask, buffer);
}

// Raw buffer target: bulk copy, no color conversion: the display depth
// must match the video's pixel format byte for byte.
if (colorspace->depth != self->source_bits_per_pixel || colorspace->depth != 16) {
Expand Down
Loading
Loading