Changelog

for the component elixpo/rv565 v2.0.0
# Changelog

All notable changes to RV565 are listed here. The project follows
[semantic versioning](https://semver.org).

## 2.0.0 — 2026-11-12

Version 2.0.0 introduces RV565 format version 7 (`docs/FORMAT.md`): the
header declares one of nine pixel formats (`rgb565be`, `rgb565le`, `rgb444`,
`rgb666`, `rgb888`, `gray8`, `gray4`, `mono`, `mono_vpage`) and a codec
(stored or zlib), frames are keyframes, exact XOR deltas, partial deltas
or repeats, and a keyframe interval keeps seeking bounded. A partial delta
stores only the bands of rows that changed, each with a shift, so players
decode and send to the display only those rows, and scrolling content costs
a small residual. Version 6 files stay readable everywhere.

- Conformance vectors in `tests/data/v7` (53 valid files with each decoded
  frame's SHA-256 and changed lines, 29 invalid files with the stage that
  must reject them),
  written and checked by `tools/make_conformance.py`. Every implementation
  is tested against them in CI.
- Python package 2.0.0, the reference implementation: `rv565.write()` takes
  `pixel_format`, `codec`, `keyframe_interval` and `version=6`;
  `rv565.convert()` and `rv565.to_rgb888()` handle every pixel format;
  `Clip.frame_type()`, and `Header` gains `version`, `header_size`,
  `pixel_format`, `codec` and `keyframe_interval`. `rv565 encode` and
  `testpattern` take `--format`, `--codec`, `--keyframe-interval` and `--v6`;
  `encode` now writes version 7 with one keyframe per second by default.
  `rv565 info` reports the format, codec and frame types. The Pillow plugin
  opens every pixel format and saves with the same options.
- The reference encoder writes partial deltas (`band_lines`, 8 by default;
  `--band-lines`), searches a shift per changed band (`motion_range`, 8
  pixels; `--motion-range`), and takes a tolerance (`tolerance`,
  `--tolerance`, 0 by default): it keeps a pixel of the previous decoded
  frame when every channel is within T, so the error stays bounded by T.
  `Clip.changed_lines()` and `rv565.frame_lines()` give the rows a frame
  changes; `rv565 info` reports the share of lines sent to the display.
  `tools/measure_v7.py` and `tools/make_ui_clip.py` measure size, PSNR and
  display traffic.
- `docs/RESULTS_V7.md`: sizes, PSNR and display traffic on the paper's film
  windows and a UI clip, against MJPEG and ZMBV. Lossless, version 7 is
  5.5× (W90) and 2.1× (W360) smaller than version 6; at tolerance 8 the W90
  clip is 2.3× smaller than MJPEG q2 at 5.4 dB better; the UI clip is
  lossless at 6% of MJPEG q2's size and sends 44% of the rows. MJPEG stays
  smaller on high-motion film, and ZMBV on the UI clip.
- `benchmarks/esp32s3_v7_player`: version 7 playback on the Oreo Badge
  (ESP32-S3, ST7789 at 40 MHz). Partial deltas cut the mean frame time of
  the UI clip from 31.4 to 14.8 ms dual-core (34.4 to 16.6 ms serial) and of
  the W90 film window by 12%; every clip played every frame on time, and W90
  decodes in 3.6 ms per frame.
- `contrib/ffmpeg`: the FFmpeg decoder, demuxer and muxer read and validate
  partial deltas with shifted bands, and the encoder writes them
  (`-band_lines`, `-motion_range`) and takes `-tolerance`, byte-identical to
  `rv565.write` with the same options; checked against all 53 valid and 29
  invalid conformance vectors and FATE. The Kaitai Struct description gains
  the partial delta frame type and body.

Version 2.0.0 of the C library reads RV565 format version 7
(`docs/FORMAT.md`) and keeps reading version 6.

- Format version 7 in the core (`rv565.h`): nine pixel formats
  (`rv565_pixel_format_t`, `RV565_PIXEL_RGB565BE` … `RV565_PIXEL_MONO_VPAGE`),
  codecs stored and zlib (`rv565_codec_t`), keyframe, delta and repeat
  frames (`rv565_frame_type_t`) and the keyframe interval. Still
  allocation-free; the zlib path still uses the ESP32 ROM tinfl or the
  bundled one.
- `rv565_open()` validates everything the spec checks at open time in its one
  linear scan: reserved pixel format, codec or frame type, `K` = 0, a missing
  keyframe at a multiple of `K`, a repeat with a body, a stored body that is
  not `S` bytes, truncation and trailing bytes. Rates up to 60 FPS in
  version 7 (30 in version 6).
- New: `rv565_decode(clip, i, prev, dst, dst_len, scratch, scratch_len)`
  (keyframe, delta against `prev`, repeat; in place with a scratch buffer
  for zlib deltas, or between two buffers without one), `rv565_seek()`
  (from the keyframe at or before a frame), `rv565_frame_type()`,
  `rv565_keyframe_at_or_before()`, `rv565_row_bytes()`,
  `rv565_bytes_per_pixel()`, and `rv565_scale_stripe_bpp()` for 1-, 2- and
  3-byte pixels (packed formats are not scaled).
- Player: deltas decode into the frame buffer that is not on screen, using
  the presented frame as their base; repeats decode nothing and are not sent
  to the display again; after a pacer skip the player applies the skipped
  records the delta chain needs (at most `K` − 1) without showing them.
  `rv565_frame_stats_t` gains `decoded` and `rows_sent`. Only RGB565
  big-endian clips play (the bundled ST7789 drivers run the panel in
  RGB565); other formats return `RV565_ERR_UNSUPPORTED` from
  `rv565_player_init()`.
- Partial deltas (frame type 3) with band shifts in the core:
  `rv565_open()` validates them in its scan (`RV565_ERR_PARTIAL` for a band
  height of 0, a short mask, no band or a band past the frame, missing
  shifts or a shift of `B` lines or more; `RV565_ERR_RECORD` for stored data
  of the wrong size). `rv565_decode()` builds each changed band as its
  shifted prediction XOR its residual: in place it writes only the changed
  bands and needs no band copy (zlib residuals go through `scratch`, at most
  `S` bytes); between two buffers it writes the whole frame without scratch.
  New: `rv565_changed_lines()` (the changed line runs of a frame, without
  decoding), `rv565_frame_lines()` (`P` and `R`), and, for readers that parse
  the container themselves, `rv565_partial_runs()` and
  `rv565_apply_partial()` on a raw body.
- The player sends only what changed: it tracks the frame on the panel and,
  for a partial delta, transfers only the panel rows scaled from its changed
  lines, one row window per run, through the new optional
  `rv565_display_ops_t.begin_rows`; keyframes and full deltas send the whole
  frame, repeats nothing, and after skips the envelope of the changes is
  sent. The ESP32 and RP2 ST7789 ports implement `begin_rows` with RASET
  (`rv565_st7789_rows()` in `port/st7789_init.h`) and restore the full
  window for the next whole frame; the TFT_eSPI and LovyanGFX examples
  implement it with `setAddrWindow`. Without `begin_rows` every change is a
  whole-frame write. A clip with zlib partial deltas needs `frames[1]`.
- `rv565_check_schedule()` models repeats as costing no decode, and applies
  the cost model to the body length (type byte excluded).
- MicroPython module (`ports/micropython`): `inflate_delta(body, prev, frame[,
  scratch])`, `xor_frame(frame, other)`, `apply_partial(body, frame,
  line_bytes, zlib[, scratch])` (applies a partial delta in place and returns
  its changed line runs) and `FORMAT_VERSION`; `inflate_frame()` is
  unchanged. The module now links `src/rv565.c`.
- Tests: `tests/test_conformance.c` and `tests/check_conformance.py` decode
  every vector in `tests/data/v7` in order (in place and between two
  buffers) and by seeking, compare each frame's SHA-256 with the manifest,
  and check that every invalid file is rejected at the right stage with the
  matching status, and compare `rv565_changed_lines()` with each frame's
  changed lines. `make -C tests` runs them. The player test plays the
  rgb565be vectors and synthetic stored and zlib clips with scattered bands
  and shifts, checks the panel after every frame and the exact number of
  rows sent. CI also runs the tests under ASan/UBSan and checks
  the vectors with `tools/make_conformance.py --check`.

API changes from 1.x (source-incompatible where noted):

- `rv565_header_t` gains `version`, `header_size`, `pixel_format`, `codec`
  and `keyframe_interval`. Code that fills a header by hand must set them.
- `rv565_frame_bytes()` is a function instead of a static inline and follows
  the pixel format.
- `RV565_MAX_FPS` is 60 (version 7); `RV565_MAX_FPS_V6` is 30.
  `RV565_HEADER_SIZE` is replaced by `RV565_HEADER_SIZE_V6` (12) and
  `RV565_HEADER_SIZE_V7` (16, the minimum).
- `rv565_clip_t.index[i]` points just past each record's length: at the type
  byte in version 7. `rv565_payload()` returns the body without the type
  byte (length 0 for a repeat).
- `rv565_decode_frame(clip, i, dst, len)` decodes in place: a keyframe on its
  own, otherwise `dst` must hold frame i−1, and a zlib delta returns
  `RV565_ERR_ARG` (use `rv565_decode()` with scratch or two buffers).
- `rv565_parse_header()` returns `RV565_ERR_TRUNCATED` (not
  `RV565_ERR_ARG`) for fewer than 12 bytes.
- New status codes: `RV565_ERR_FORMAT`, `RV565_ERR_KEYFRAME`,
  `RV565_ERR_RECORD`, `RV565_ERR_UNSUPPORTED`, `RV565_ERR_PARTIAL`.
- `rv565_display_ops_t` gains `begin_rows` (last member, optional).
- `rv565_decoder_ops_t.start` takes the previous frame:
  `start(ctx, index, prev, dst, dst_len)`, and the struct gains an optional
  `decode` hook for the serial path. Backends call `rv565_decode(clip,
  index, prev, dst, dst_len, NULL, 0)`.
- `rv565_player_t`: `frames[1]` is required for clips with zlib deltas or
  zlib partial deltas, even on the serial path; the state fields changed
  (`ready_frame` is gone; `current`, `held[]`, `shown`).

- CircuitPython library 2.0.0 (`ports/circuitpython/`): reads format
  version 7 as well as version 6, with every pixel format, the stored and
  zlib codecs, and keyframe, XOR delta and repeat frames, validated on open
  and indexing as the reference does, and checked in host tests against every
  conformance vector in `tests/data/v7` (in order and by random seeks). It
  decodes into one reused frame buffer, so `Clip.frame()` returns a buffer
  that the next call overwrites, and XORs deltas in 2 KiB chunks through long
  integers. `Player` shows `rgb565be`, `rgb565le`, `gray8`, `gray4` and
  `mono`, and `DirectPlayer` sends `rgb565be` and `rgb666` (COLMOD 0x66);
  both skip repeat frames and name the supported formats for any other clip.
  The version 7 paths have not yet run on a board.
- CircuitPython library: partial delta frames (type 3). The index checks
  each partial delta's band height, band mask, band shifts and stored size;
  decoding inflates only the changed bands, predicts shifted bands from a
  one-band scratch copy, and XORs the residuals in place into the reused
  frame buffer. `Clip.changed_lines(i)` matches the reference and
  `Clip.dirty_lines(since, i)` merges it over skipped frames. `DirectPlayer`
  sends one row window and write per run of changed bands, `Player` copies
  only the changed rows into the bitmap (`readinto` into a scratch band,
  then `bitmaptools.blit`) so `displayio` refreshes only that area, and
  `PlayStats.rows_updated` counts the rows drawn. Host-tested only.
- Experimental CircuitPython library, `ports/circuitpython/`
  (`circuitpython_rv565`): reads version 6 files from a path, file object or
  bytes with a lazily built frame index, inflates with the built-in `zlib`,
  and plays through `displayio` (`Player`) or straight to an ST7789 over its
  bus (`DirectPlayer`), paced to the clip's rate with frame skipping. Host
  tests with CPython and fake `displayio`; not yet run on a board. Expect a
  few FPS ([#30](https://github.com/elixpo/rv565/issues/30)).
- `contrib/`: the FFmpeg patches (now two, `avcodec` and `avformat`) read
  and write format version 7, with every pixel format, stored or zlib frames,
  keyframe/delta/repeat frames and keyframe seeking, and still read and write
  version 6; they pass the `tests/data/v7` conformance vectors and FATE. The
  `file(1)`, shared-mime-info, PRONOM and Kaitai Struct signatures recognise
  version 7 as well as version 6.

## 1.2.0 — 2026-11-04

- RP2350 cost model `RV565_COST_RP2350_180x135` (8.108 ms + 0.2446 µs/byte,
  r² 0.9984), measured on a Pico 2 with the paper's method; results in
  `docs/RESULTS_RP2350.md`. Dual-core playback runs at 24 FPS with zero
  misses on both film windows; the serial path cannot (every frame late).
- The RP2 port is no longer experimental on the RP2350 and joins the 1.x
  compatibility promise; on the RP2040 it compiles but is not yet run.
- Python: `COST_RP2350_180x135`, and `rv565 info --chip esp32s3|rp2350` picks
  the cost model and that chip's measured display transfer.
- Bundled inflate (RP2 and other non-ESP32 targets) keeps its ~11 KB
  decompressor in static memory instead of on the stack. On the RP2040/RP2350
  core 0 has a 2 KB stack, so inflating there (serial mode, or the Pico
  example's single-core path) overflowed it. The ESP32 ROM path is unchanged.
- RP2 port runs on hardware: first bring-up on a Raspberry Pi Pico 2 with a
  2.0" 320x240 ST7789, 24 FPS with zero misses on the test pattern.
  - Command bytes (the init sequence and each frame's RAMWR) now go out at
    10 MHz; at 37.5 MHz the panel misread them and stayed blank. Pixel data
    still runs at the configured rate.
  - SPI mode 3 instead of mode 0: the RP2350's PL022 leaves a gap after every
    byte in mode 0. Measured frame time 39.71 ms → 33.57 ms; the transfer
    now runs at 99.4% of the 37.5 MHz wire time (83.8% before).
- RP2350 benchmark: wiring notes (short wires, a series resistor on SCL) and
  `st7789_test.py`, a MicroPython wiring check.

## 1.1.0 — 2026-11-01

- Any ST7789 module: the ESP32 and RP2 display configs take an
  `rv565_panel_t` (size, controller offset, MADCTL rotation, inversion), with
  presets for 320x240, 240x320, 240x240, 135x240 and 240x135 in
  `rv565_panel.h`. A zeroed panel keeps the 1.0 behaviour (320x240
  landscape), so existing code is unchanged. Only 320x240 is verified on
  hardware so far.
- Examples show where to pick the panel.
- `examples/TFT_eSPI_Player` and `examples/LovyanGFX_Player`: keep your
  display library for the panel and use RV565 for decoding (compiled in CI,
  not yet run on hardware).
- Python package `rv565` (`python/`): read, write, verify and schedule-check
  files, the `rv565` command (which `tools/rv565.py` now wraps), and a Pillow
  plugin (`import rv565.pillow`). Published to PyPI with the same version.
- `contrib/`: file type registration for `.rv565` (`file(1)` magic,
  shared-mime-info `video/x-rv565`, PRONOM fields) and an FFmpeg patch
  (decoder, encoder, demuxer, muxer), prepared for upstream submission.
- `docs/FORMAT.md` gains an identification section (extension, MIME type,
  signature).

## 1.0.0 — 2026-10-31

First stable release. The container format (version 6) and the public API are
now covered by the 1.x compatibility promise; see *Versioning* in the README.

- Core: header parsing and validation, O(1) frame index, exact-length
  inflate, nearest-neighbour stripe scaler, decode-cost model,
  schedulability check, absolute-deadline pacer.
- Player: two-stripe DMA pipeline with optional second-core decoding.
- ESP32 port (ESP-IDF and Arduino-ESP32), verified on the ESP32-S3 badge.
- RP2040/RP2350 port (Pico SDK and Arduino-Pico), experimental: compiled in
  CI, not yet run on hardware.
- `tools/rv565.py`: encode, info, verify, c-array, testpattern.
- Published in the Arduino Library Manager.

## 0.1.0 — 2026-10-31

Initial listing in the Arduino Library Manager.