Skip to content

jpegio: a CircuitPython-compatible JPEG decoder as a native module, one TJpgDec per firmware #23

Description

@bdbarnett

Brad, 2026-09-03: "I like your idea" — usbif is wiring a UVC camera through board_config.display_drv and today the only JPEG decoder in the workspace is LVGL's, so the camera path needs LVGL in the build. This issue is the plan; pydevices/docs/jpegio-vision.md is the one-page version.

What. Port CircuitPython's jpegio (shared-module/jpegio/JpegDecoder.c, TJpgDec R0.03 underneath) as a native module in displayif: JpegDecoder.open(buffer|file), decode(bitmap, scale=…), same API as CP so CP builds use the native one and MicroPython builds get ours. Camera path becomes MJPEG frame → jpegiodisplay_drv.blit_rect, no LVGL.

One decoder per firmware. jpegio vendors tjpgd.c; its micropython.cmake/.mk compile it only when the lvgl usermod is NOT in the build, and link against LVGL's jd_prepare/jd_decomp when it is — same pinned R0.03 either way. The org-wide TJpgDec config is CircuitPython's (JD_FORMAT 1 RGB565, JD_USE_SCALE 1); LVGL's decoder is made to honour it (lvgl-bindings issue, linked below), which also un-disables TJPGD on the CircuitPython bindings.

Contract, from the usbif session's measurements (Logitech C920e, 320x240, one camera):

  • UVC MJPEG frames are NOT JFIF-first: SOI → DQT → SOF0 → DHT×4 → COM → APP0/JFIF → DRI → SOS. LVGL's is_jpg() (lv_tjpgd.c:302) demands FF D8 FF E0 … "JFIF" in bytes 0..9 and rejects every webcam frame. jpegio must not sniff beyond SOI; hand the buffer to jd_prepare and let it judge (CP's behaviour).
  • This camera sends its DHT tables; many UVC cameras omit them and expect the host to inject the standard ones. Decide explicitly: "valid JPEG in" (something upstream injects tables) or jpegio injects. "Cameras always send DHT" is unproven with n=1.
  • Frames carry DRI (restart intervals). The test frame for any config change must have restart markers, not a clean still.
  • Output: native-order RGB565 (the driver byteswaps itself). Two things worth more than raw speed: block/row-wise output through TJpgDec's output callback so a preview blits incrementally without a full-frame buffer (600 KB not allocated at 640x480), and exposing the 1/2, 1/4, 1/8 scale factor (pydevices has no resampler; the usbif example upscaled by hand).
  • S3 headroom (ESP32_GENERIC_S3, 8 MB PSRAM, 800x480 panel live): ~6.5 MB contiguous free; TJpgDec's ~3 KB work buffer is nothing. Caveat: pooled PSRAM+SRAM number, internal-SRAM headroom not separated, PSRAM decode speed unmeasured.
  • Target: on the S3's full-speed host the camera negotiates 320x240 @ 5 fps (~8 KB/frame) — ~5 decodes/s, undemanding. The P4's high-speed host (unproven bench) and its hardware JPEG decoder are where rate matters: keep the decode backend swappable.
  • No decode rate exists yet (no successful decode on the bench); do not quote one.

Method note worth keeping: the usbif session's first LVGL attempt "worked" only because the header carried its own w:320,h:240 and LVGL echoed them back; zeroing the header exposed that nothing decoded. Any jpegio check starts from a zeroed header.

Order: decoder module + tests (a DRI frame, a table-less frame, scale factors, block callback) → cmods one-copy build rule → usbif consumer → lvgl-bindings JD_FORMAT change and CP TJPGD re-enable. Backend swap for the P4 later.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions