diff --git a/lib/AnimatedGIF/AnimatedGIF_circuitpy.h b/lib/AnimatedGIF/AnimatedGIF_circuitpy.h index c1f359194f5..63c71e665e5 100644 --- a/lib/AnimatedGIF/AnimatedGIF_circuitpy.h +++ b/lib/AnimatedGIF/AnimatedGIF_circuitpy.h @@ -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 @@ -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 @@ -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); diff --git a/lib/AnimatedGIF/gif.c b/lib/AnimatedGIF/gif.c index 316fc1ab749..fe800c88962 100644 --- a/lib/AnimatedGIF/gif.c +++ b/lib/AnimatedGIF/gif.c @@ -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; } // @@ -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() */ @@ -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 diff --git a/ports/raspberrypi/boards/adafruit_feather_rp2040_thinkink/mpconfigboard.mk b/ports/raspberrypi/boards/adafruit_feather_rp2040_thinkink/mpconfigboard.mk index 1f71dcb76bf..99df118f311 100644 --- a/ports/raspberrypi/boards/adafruit_feather_rp2040_thinkink/mpconfigboard.mk +++ b/ports/raspberrypi/boards/adafruit_feather_rp2040_thinkink/mpconfigboard.mk @@ -7,3 +7,5 @@ CHIP_VARIANT = RP2040 CHIP_FAMILY = rp2 EXTERNAL_FLASH_DEVICES = "GD25Q64C,W25Q64JVxQ" + +CIRCUITPY_VIDEOCORE = 1 diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index 4b86713a23c..8021a40f3e2 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -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) diff --git a/shared-bindings/gifio/OnDiskGif.c b/shared-bindings/gifio/OnDiskGif.c index 6f78af0becc..f13d740e141 100644 --- a/shared-bindings/gifio/OnDiskGif.c +++ b/shared-bindings/gifio/OnDiskGif.c @@ -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. //| """ //| ... //| diff --git a/shared-bindings/videocore/VideoBuffer.c b/shared-bindings/videocore/VideoBuffer.c index 813d147a96b..0da348e1c93 100644 --- a/shared-bindings/videocore/VideoBuffer.c +++ b/shared-bindings/videocore/VideoBuffer.c @@ -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. //| """ @@ -44,25 +53,36 @@ //| 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); } @@ -70,15 +90,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. diff --git a/shared-bindings/videogif/GifDecoder.c b/shared-bindings/videogif/GifDecoder.c index 57a912bf6d7..11a6b882aca 100644 --- a/shared-bindings/videogif/GifDecoder.c +++ b/shared-bindings/videogif/GifDecoder.c @@ -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. //| """ //| ... //| @@ -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 = { @@ -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( diff --git a/shared-bindings/videogif/GifDecoder.h b/shared-bindings/videogif/GifDecoder.h index d7ac18e7ab7..99146c39fd2 100644 --- a/shared-bindings/videogif/GifDecoder.h +++ b/shared-bindings/videogif/GifDecoder.h @@ -11,8 +11,8 @@ 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. @@ -20,6 +20,11 @@ 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 diff --git a/shared-module/gifio/OnDiskGif.c b/shared-module/gifio/OnDiskGif.c index b12f4a46fa3..1062e146ebd 100644 --- a/shared-module/gifio/OnDiskGif.c +++ b/shared-module/gifio/OnDiskGif.c @@ -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; @@ -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; common_hal_displayio_bitmap_deinit(self->bitmap); self->bitmap = NULL; self->palette = NULL; diff --git a/shared-module/videocore/VideoBuffer.c b/shared-module/videocore/VideoBuffer.c index b9f5565b1d7..a0d4c81f89e 100644 --- a/shared-module/videocore/VideoBuffer.c +++ b/shared-module/videocore/VideoBuffer.c @@ -214,9 +214,15 @@ static void videocore_videobuffer_update_area(videocore_videobuffer_obj_t *self) videocore_videobuffer_source_rect(self, &whole, &self->current_area); } -// 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 @@ -346,7 +352,8 @@ static void videocore_videobuffer_move_dest(videocore_videobuffer_obj_t *self) { } } -void common_hal_videocore_videobuffer_construct(videocore_videobuffer_obj_t *self, mp_obj_t framebuffer) { +void common_hal_videocore_videobuffer_construct(videocore_videobuffer_obj_t *self, mp_obj_t framebuffer, + mp_int_t bits_per_pixel) { // Initialise what the finaliser uses before anything can raise. self->source = mp_const_none; self->stream_base = NULL; @@ -359,12 +366,15 @@ void common_hal_videocore_videobuffer_construct(videocore_videobuffer_obj_t *sel self->framebuffer_protocol->get_height == NULL) { mp_raise_ValueError(MP_ERROR_TEXT("framebuffer missing required functions")); } + self->bits_per_pixel = FB_GETTER(get_color_depth, 16); } else if (!mp_get_buffer(framebuffer, &self->bufinfo, MP_BUFFER_WRITE)) { // Not a framebuffer and not a writable buffer such as a bytearray or // memoryview. mp_raise_TypeError_varg(MP_ERROR_TEXT("%q must be of type %q or %q, not %q"), MP_QSTR_framebuffer, MP_QSTR_buffer, MP_QSTR_framebuffer, mp_obj_get_type(framebuffer)->name); + } else { + self->bits_per_pixel = bits_per_pixel; } self->x = 0; @@ -400,12 +410,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); @@ -532,7 +543,9 @@ void common_hal_videocore_videobuffer_play(videocore_videobuffer_obj_t *self, mp if (self->playing) { mp_raise_RuntimeError(MP_ERROR_TEXT("already playing")); } - videosource_check(source); + if (!videosource_set_bits_per_pixel(source, self->bits_per_pixel)) { + mp_arg_error_invalid(MP_QSTR_bits_per_pixel); + } self->stale_size = false; self->source_ended = false; videocore_videobuffer_forget_frame(self); @@ -664,6 +677,117 @@ 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. +// On a rotated display each screen pixel is walked back to its source pixel, as the 16 bit copy +// does. Whole bytes are copied when the source runs forwards along screen x, 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_area = overlap->x1 - area->x1; + const uint8_t *frame = self->stream_base; + uint8_t *out = (uint8_t *)buffer; + + // While the video isn't shown there is no transform and the orientations match. + const displayio_buffer_transform_t *t = &self->absolute_transform; + const bool shown = t->scale != 0; + const int8_t dx = shown ? t->dx : 1; + const int8_t dy = shown ? t->dy : 1; + const bool transpose = shown && t->transpose_xy; + const mp_int_t origin_x = shown ? t->x : 0; + const mp_int_t origin_y = shown ? t->y : 0; + const bool contiguous = !transpose && dx > 0; + // The first source column of every row when the source runs along screen x. + const mp_int_t first_x = videocore_videobuffer_source_axis(dx, overlap->x1, origin_x, self->x); + + const bool whole_bytes = contiguous && span_px % 8 == 0 && x_in_area % 8 == 0 && first_x % 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; + if (!contiguous) { + // Mirrored or transposed: walk each screen pixel back to its source pixel. Screen x + // steps along a source row, or down a source column on a transposed display. + mp_int_t source_x; + mp_int_t source_y; + if (transpose) { + source_x = videocore_videobuffer_source_axis(dy, y, origin_y, self->x); + source_y = videocore_videobuffer_source_axis(dx, overlap->x1, origin_x, self->y); + } else { + source_x = videocore_videobuffer_source_axis(dx, overlap->x1, origin_x, self->x); + source_y = videocore_videobuffer_source_axis(dy, y, origin_y, self->y); + } + for (size_t i = 0; i < width; i++) { + const size_t p = px + i; + if (mask[p / 32] & (1u << (p % 32))) { + continue; + } + const mp_int_t step = (mp_int_t)i * dx; + const mp_int_t fx = transpose ? source_x : source_x + step; + const mp_int_t fy = transpose ? source_y + step : source_y; + if (frame[(size_t)fy * row_bytes + (size_t)fx / 8] & (0x80 >> (fx % 8))) { + out[p / 8] |= 0x80 >> (p % 8); + } + mask[p / 32] |= 1u << (p % 32); + } + continue; + } + const size_t x_in_frame = first_x; + const mp_int_t source_y = videocore_videobuffer_source_axis(dy, y, origin_y, self->y); + const uint8_t *row = frame + (size_t)source_y * 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. @@ -691,21 +815,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; } @@ -737,6 +847,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: copy the pixels, with 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) { diff --git a/shared-module/videocore/VideoBuffer.h b/shared-module/videocore/VideoBuffer.h index 0371a0684e8..c17124fde20 100644 --- a/shared-module/videocore/VideoBuffer.h +++ b/shared-module/videocore/VideoBuffer.h @@ -32,6 +32,9 @@ typedef struct { mp_int_t source_height; mp_int_t source_bits_per_pixel; size_t source_frame_bytes; + // The pixel format play() asks the source for: the plain buffer's, or + // the framebuffer's color depth. + mp_int_t bits_per_pixel; // Base of the destination the source last accepted, or NULL when there // is none. const void *stream_base; @@ -85,7 +88,8 @@ typedef struct { bool paused; } videocore_videobuffer_obj_t; -void common_hal_videocore_videobuffer_construct(videocore_videobuffer_obj_t *self, mp_obj_t framebuffer); +void common_hal_videocore_videobuffer_construct(videocore_videobuffer_obj_t *self, mp_obj_t framebuffer, + mp_int_t bits_per_pixel); void common_hal_videocore_videobuffer_play(videocore_videobuffer_obj_t *self, mp_obj_t source); void common_hal_videocore_videobuffer_set_position(videocore_videobuffer_obj_t *self, mp_int_t x, mp_int_t y); void common_hal_videocore_videobuffer_stop(videocore_videobuffer_obj_t *self); diff --git a/shared-module/videocore/__init__.c b/shared-module/videocore/__init__.c index 2b261368a9c..412da7ab3ea 100644 --- a/shared-module/videocore/__init__.c +++ b/shared-module/videocore/__init__.c @@ -49,3 +49,11 @@ mp_int_t videosource_get_height(mp_obj_t source) { mp_int_t videosource_get_bits_per_pixel(mp_obj_t source) { return videosource_check(source)->get_bits_per_pixel(source); } + +bool videosource_set_bits_per_pixel(mp_obj_t source, mp_int_t bits_per_pixel) { + const videosource_p_t *protocol = videosource_check(source); + if (protocol->set_bits_per_pixel == NULL) { + return protocol->get_bits_per_pixel(source) == bits_per_pixel; + } + return protocol->set_bits_per_pixel(source, bits_per_pixel); +} diff --git a/shared-module/videocore/__init__.h b/shared-module/videocore/__init__.h index 8d2276892ea..fae86c38272 100644 --- a/shared-module/videocore/__init__.h +++ b/shared-module/videocore/__init__.h @@ -94,6 +94,10 @@ typedef struct _videosource_p_t { mp_int_t (*get_height)(mp_obj_t self); // Bits per pixel of the frames the source streams. mp_int_t (*get_bits_per_pixel)(mp_obj_t self); + // Stream frames with this many bits per pixel from the next play. Called + // by VideoBuffer.play before play(). Returns whether the source can. May be + // NULL when the source only streams get_bits_per_pixel(). + bool (*set_bits_per_pixel)(mp_obj_t self, mp_int_t bits_per_pixel); } videosource_p_t; // Protocol dispatch helpers. play and stop are no-ops when the source doesn't @@ -111,6 +115,8 @@ bool videosource_set_destination(mp_obj_t source, const videocore_destination_t mp_int_t videosource_get_width(mp_obj_t source); mp_int_t videosource_get_height(mp_obj_t source); mp_int_t videosource_get_bits_per_pixel(mp_obj_t source); +// Ask source to stream bits_per_pixel. Returns whether it will. +bool videosource_set_bits_per_pixel(mp_obj_t source, mp_int_t bits_per_pixel); // Raise when the group tree contains a VideoBuffer that can't be shown on // this display: one that streams into a framebuffer other than the one this diff --git a/shared-module/videogif/GifDecoder.c b/shared-module/videogif/GifDecoder.c index 0b264d212b7..a018d58688e 100644 --- a/shared-module/videogif/GifDecoder.c +++ b/shared-module/videogif/GifDecoder.c @@ -11,6 +11,7 @@ #include "py/mpstate.h" #include "py/runtime.h" #include "py/stream.h" +#include "shared-module/displayio/ColorConverter.h" #include "supervisor/port_heap.h" #include "supervisor/shared/tick.h" @@ -24,8 +25,23 @@ // USB and audio for one decode. #define VIDEOGIF_MAX_LATE_FRAMES 3 -static inline void videogif_put_pixel(uint8_t *row, int x, uint16_t value) { - memcpy(row + x * 2, &value, 2); +// 1 bit per pixel: an RGB888 palette entry is white when its luma is at least +// half, so any black and white palette maps exactly. +static bool videogif_is_white(const uint8_t *rgb) { + return displayio_colorconverter_compute_luma((rgb[0] << 16) | (rgb[1] << 8) | rgb[2]) >= 128; +} + +static inline void videogif_put_pixel(videogif_gifdecoder_obj_t *self, uint8_t *row, int x, uint16_t value) { + if (self->bits_per_pixel == 1) { + const uint8_t mask = 0x80 >> (x % 8); + if (value) { + row[x / 8] |= mask; + } else { + row[x / 8] &= ~mask; + } + } else { + memcpy(row + x * 2, &value, 2); + } } static displayio_area_t videogif_canvas(videogif_gifdecoder_obj_t *self) { @@ -40,11 +56,16 @@ static displayio_area_t videogif_canvas(videogif_gifdecoder_obj_t *self) { // screen descriptor names. static void videogif_fill(videogif_gifdecoder_obj_t *self, const displayio_area_t *area) { const uint8_t index = self->gif->ucBackground; - const uint16_t value = self->gif->pPalette[index]; + uint16_t value; + if (self->bits_per_pixel == 1) { + value = videogif_is_white((const uint8_t *)self->gif->pPalette + index * 3); + } else { + value = self->gif->pPalette[index]; + } for (int y = area->y1; y < area->y2; y++) { uint8_t *row = self->destination.base + y * self->destination.stride; for (int x = area->x1; x < area->x2; x++) { - videogif_put_pixel(row, x, value); + videogif_put_pixel(self, row, x, value); } } } @@ -70,6 +91,14 @@ static void videogif_draw_line(GIFDRAW *draw) { if (self->new_frame) { self->new_frame = false; videogif_start_frame(self); + if (self->bits_per_pixel == 1) { + memset(self->white, 0, sizeof(self->white)); + for (int i = 0; i < 256; i++) { + if (videogif_is_white(draw->pPalette24 + i * 3)) { + self->white[i / 8] |= 1 << (i % 8); + } + } + } } const int y = draw->iY + draw->y; @@ -87,7 +116,13 @@ static void videogif_draw_line(GIFDRAW *draw) { if (draw->ucHasTransparency && index == draw->ucTransparent) { continue; } - videogif_put_pixel(row, draw->iX + i, draw->pPalette[index]); + uint16_t value; + if (self->bits_per_pixel == 1) { + value = (self->white[index / 8] >> (index % 8)) & 1; + } else { + value = draw->pPalette[index]; + } + videogif_put_pixel(self, row, draw->iX + i, value); } } @@ -141,10 +176,18 @@ static void videogif_open_gif(GIFIMAGE *gif, mp_obj_t file) { } gif->GIFFile.iSize = (int32_t)size; - if (GIF_init(gif) != 1) { - if (gif->iError == GIF_TOO_WIDE) { - mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be <= %d"), MP_QSTR_width, MAX_WIDTH); - } + // Measure the canvas first, then hand the decoder a line buffer of that width. + if (GIF_init(gif, NULL, 0) != 1) { + mp_arg_error_invalid(MP_QSTR_filename); + } + // One line of 8-bit pixels, sized to the canvas. Outside the VM heap like gif itself, and + // freed with it in deinit. + int width = gif->iCanvasWidth; + unsigned char *line_buf = port_malloc(width, false); + if (line_buf == NULL) { + m_malloc_fail(width); + } + if (GIF_init(gif, line_buf, width) != 1) { mp_arg_error_invalid(MP_QSTR_filename); } } @@ -160,6 +203,7 @@ void common_hal_videogif_gifdecoder_construct(videogif_gifdecoder_obj_t *self, m GIF_begin(self->gif, GIF_PALETTE_RGB565_LE); self->gif->pfnDraw = videogif_draw_line; self->file = file; + self->bits_per_pixel = 16; videogif_open_gif(self->gif, file); } @@ -179,6 +223,9 @@ void common_hal_videogif_gifdecoder_deinit(videogif_gifdecoder_obj_t *self) { videogif_end_stream(self); common_hal_videogif_gifdecoder_stop(self); if (self->gif != NULL) { + if (self->gif->ucLineBuf != NULL) { + port_free(self->gif->ucLineBuf); + } port_free(self->gif); self->gif = NULL; } @@ -197,6 +244,25 @@ mp_int_t common_hal_videogif_gifdecoder_get_height(videogif_gifdecoder_obj_t *se return self->gif != NULL ? self->gif->iCanvasHeight : 0; } +mp_int_t common_hal_videogif_gifdecoder_get_bits_per_pixel(videogif_gifdecoder_obj_t *self) { + return self->bits_per_pixel; +} + +bool common_hal_videogif_gifdecoder_set_bits_per_pixel(videogif_gifdecoder_obj_t *self, + mp_int_t bits_per_pixel) { + if (bits_per_pixel == self->bits_per_pixel) { + return true; + } + if (self->playing || self->gif == NULL || (bits_per_pixel != 1 && bits_per_pixel != 16)) { + return false; + } + self->bits_per_pixel = bits_per_pixel; + // 1 bit per pixel needs the RGB888 palette to compute luma. play() + // restarts, which reads the GIF header again with this palette type. + self->gif->ucPaletteType = bits_per_pixel == 1 ? GIF_PALETTE_RGB888 : GIF_PALETTE_RGB565_LE; + return true; +} + // Keep the supervisor tick on only while a frame can come due. static void videogif_update_tick(videogif_gifdecoder_obj_t *self) { const bool ticking = self->playing && !self->paused && !self->finished; @@ -285,10 +351,12 @@ bool common_hal_videogif_gifdecoder_set_destination(videogif_gifdecoder_obj_t *s if (moved && self->shown) { videogif_finish(self); } else if (moved) { - // Restarting reads the GIF header again, which converts the global - // color table to this byte order. - self->gif->ucPaletteType = destination->reverse_bytes_in_word ? - GIF_PALETTE_RGB565_BE : GIF_PALETTE_RGB565_LE; + if (self->bits_per_pixel == 16) { + // Restarting reads the GIF header again, which converts the + // global color table to this byte order. + self->gif->ucPaletteType = destination->reverse_bytes_in_word ? + GIF_PALETTE_RGB565_BE : GIF_PALETTE_RGB565_LE; + } videogif_restart(self); } return true; diff --git a/shared-module/videogif/GifDecoder.h b/shared-module/videogif/GifDecoder.h index 0acb093fce5..4e6cf9297ee 100644 --- a/shared-module/videogif/GifDecoder.h +++ b/shared-module/videogif/GifDecoder.h @@ -37,6 +37,11 @@ typedef struct videogif_gifdecoder_obj { int32_t repeat_count; int32_t loops_done; uint8_t disposal; + // 1 or 16. + uint8_t bits_per_pixel; + // For 1 bit per pixel: which palette entries are white, one bit each, + // for the palette of the frame being decoded. + uint8_t white[256 / 8]; bool playing; bool paused; // Played its last loop, or stopped on an error: no more frames are due.