Repository navigation
Conversation
The rect was only computed on a move or transform, using the source's size. A VideoBuffer added to a Group before play() kept an empty rect. A plain buffer target drew nothing until it moved. A framebuffer target still got its frames, but compositing and overlay repaint were wrong. This also clears a stale rect when a source of another size is played again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plain buffer move only repainted the area it left. The new location stayed stale until the next frame repainted it. Mark it dirty as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
on_frame gets a changed rect in source coordinates, and the refresh only repaints that part. NULL means the whole frame changed and the next frame repaints all of it. A rect gives a source a protocol type with no displayio dependency. mipicsi passes NULL. get_refresh_areas takes the landed frame and vacated area in one critical section. A frame landing during a refresh stays for the next one, unless it passed NULL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fit check assumed a contiguous frame of height * row_bytes. A source that writes each row at the framebuffer stride ends (height - 1) strides in, so a narrow frame low in the framebuffer could run past its end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move opening a file by name and reading the GIF header into helpers so another GIF user can share them. OnDiskGif behaves the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A source calls the destination's on_eos when it will write no more frames. VideoBuffer then stops it the next time playing, source or a refresh looks, like audio outputs stop a finished sample, and keeps its last frame shown. A framebuffer target drops that frame when moved or hidden, since the repaint overwrites it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GifDecoder plays an animated GIF into a videocore.VideoBuffer as RGB565 frames, reporting each frame's changed rect. It follows the frame delays and loop count, and reports the end of the stream on the last loop or a read error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 3, 2026
tannewt
requested changes
Nov 3, 2026
tannewt
left a comment
Member
There was a problem hiding this comment.
Let's leave gifio as-is so they are separate. I think ideally we'd replace gifio with this.
Comment on lines
+40
to
+41
| //| color, at each loop and whenever the VideoBuffer showing it directly | ||
| //| on a display is moved or shown again. |
Member
There was a problem hiding this comment.
I don't think it should replay on move or show. I'd think it'd need to have play() called again.
Collaborator
Author
There was a problem hiding this comment.
kk.
Moving or hiding a VideoBuffer will stop the video, and play() starts it again.
tannewt
reviewed
Nov 3, 2026
GifDecoder opens the file itself and closes it on deinit. The GIF open helpers move back out of gifio so the two modules stay separate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once a GIF has shown a frame, a new destination or a resume ends the stream, so playing goes False and play() starts it again. The first destination of a play still starts the GIF. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A framebuffer target passes its byte order to the source with the destination. A plain buffer target gets the source's native byte order and VideoBuffer swaps the bytes as it copies to a display that needs it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tannewt
requested changes
Nov 4, 2026
tannewt
left a comment
Member
There was a problem hiding this comment.
Looking better. Thank you! A couple questions to think about.
When background calls come late, decode up to 3 due frames in one call and redraw them once. Still behind after that, slow down as before. The docstring now says play() always starts at the first frame, that there is no pause and resume, and what hiding and moving do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tannewt
approved these changes
Nov 5, 2026
tannewt
left a comment
Member
There was a problem hiding this comment.
Ok! I'm not sure that's exactly what we'd want from move and hide but it makes sense that we'd want the same buffer area over time. Let's see how it works out in practice. Thanks!
mikeysklar
added a commit
that referenced
this pull request
Nov 5, 2026
epaperdisplay-fast was rebased onto main after #11738 merged, and lag drive now builds with CIRCUITPY_EPAPERDISPLAY_FAST. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
videogif.GifDecoderplays a GIF into avideocore.VideoBuffer.Why
GIFs store which area changed and how long each frame lasts, so slow displays can redraw less.
Part 1 of 3 toward GIF video on e-paper, followed by #11739 and #11740. Merges first, needs no other PR.
p4_7in_gif_plainbuffer_landscape.mp4
Metro ESP32-P4 with Raspberry Pi Touch Display 2, filmed on a dev build of all 3 PRs. The teapot is
teapot_1bit_320.gif(320x240, 120 frames), decoded at 16 bit into a plain buffer.Clip code:
code.pyChanges
play()staying blank.on_framegainschanged,NULLfor the whole frame. The camera passesNULL.OnDiskGif, same behavior.CIRCUITPY_VIDEOCOREandCIRCUITPY_GIFIOare on.playinggoes False after the last loop, a read error ordeinit().bytearray), or framebuffer (FramebufferDisplay, read back to check pixels)Tested
Metro ESP32-P4, Raspberry Pi Touch Display 2,
color_depth=16, Ubuntu host, 3 trials per row.CircuitPython 11.0.0-alpha.2 dev builds of this branch, before the rebase.
video.x = 0playingFalse at the enddeinit()mem_freeOnDiskGifon Metro RP2040, no video, main vs this branchnext_frame, teapot in palette modedeinit()at VM end, ASanplay()after a GIF endedpre-commitpasses on the final branch.Test files
Test GIFs:
make_gifs.pyand the three GIFstg_once.gifplays once,tg_loop2.gifloops twice,tg_inf.gifloops forever.Test code:
g_basicon Metro ESP32-P4 (display setup, helpers, case)Serial output from that run
Limits
audiocore.WaveFile(from source)ValueError, as withOnDiskGifplay()after a GIF endedsourceis NonePanel safety
This PR doesn't change how any display is driven. The fast e-paper updates in #11739 go past panel makers' refresh limits, and its
EPaperDisplaydocs now carry a warning.AI assistance
Written with Claude Code.
🤖 Generated with Claude Code