From eabc28fd1fe5ba0f9fb7a13ccb071ce3446062d6 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 31 Aug 2026 10:58:58 -0500 Subject: [PATCH 1/6] Add DisplayFrame streaming for device screen mirroring FromRadio gains display_frame (tag 20) carrying chunked 1bpp framebuffer snapshots, bounded to 384 data bytes per chunk to respect the 512-byte MAX_TO_FROM_RADIO_SIZE cap. AdminMessage gains get_display_frame_request (one-shot) and set_display_mirror (continuous) to arm the stream. Complements the existing send_input_event for full remote display/control. Co-Authored-By: Claude Fable 5 --- meshtastic/admin.proto | 14 ++++++++++ meshtastic/mesh.options | 6 ++++ meshtastic/mesh.proto | 61 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/meshtastic/admin.proto b/meshtastic/admin.proto index 0de5779d7..6353cc35c 100644 --- a/meshtastic/admin.proto +++ b/meshtastic/admin.proto @@ -457,6 +457,20 @@ message AdminMessage { */ uint32 toggle_muted_node = 49; + /* + * Request a single frame of the device's display framebuffer. + * The frame is delivered to the local client as FromRadio.display_frame + * chunks (see DisplayFrame in mesh.proto). + */ + bool get_display_frame_request = 50; + + /* + * Enable or disable continuous mirroring of the device display. + * While enabled, the device sends a DisplayFrame after each screen + * redraw that changed the framebuffer, as FromRadio.display_frame chunks. + */ + bool set_display_mirror = 51; + /* * Begins an edit transaction for config, module config, owner, and channel settings changes * This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) diff --git a/meshtastic/mesh.options b/meshtastic/mesh.options index 5a0f2bde8..7680c781c 100644 --- a/meshtastic/mesh.options +++ b/meshtastic/mesh.options @@ -109,6 +109,12 @@ *LoRaPresetGroup.presets max_count:11 *LoRaRegionPresets.group_index int_size:8 +# Display mirroring (FromRadio.display_frame). 384 keeps FromRadio under the +# 512-byte MAX_TO_FROM_RADIO_SIZE cap (data 384+3, six varint fields <=30, oneof +# and submessage headers <=10, FromRadio.id <=6) and splits the common 128x64 +# 1bpp frame (1024 bytes) into three chunks aligned on whole page rows. +*DisplayFrame.data max_size:384 + *MqttClientProxyMessage.topic max_size:60 *MqttClientProxyMessage.data max_size:435 *MqttClientProxyMessage.text max_size:435 diff --git a/meshtastic/mesh.proto b/meshtastic/mesh.proto index 1076d1a5f..b5a158cfc 100644 --- a/meshtastic/mesh.proto +++ b/meshtastic/mesh.proto @@ -2467,9 +2467,70 @@ message FromRadio { * any group carries no constraint info and should not be restricted. */ LoRaRegionPresetMap region_presets = 19; + + /* + * One chunk of the device's display framebuffer, sent while display + * mirroring is active (see AdminMessage.set_display_mirror and + * AdminMessage.get_display_frame_request). + */ + DisplayFrame display_frame = 20; } } +/* + * A chunk of the device's display framebuffer, streamed to the local client + * over BLE/serial/TCP. Frames larger than one chunk are split by byte offset; + * a chunk with offset + data length == total_size completes the frame. + */ +message DisplayFrame { + /* + * Pixel encodings of the framebuffer bytes. + */ + enum Format { + /* + * 1 bit per pixel, vertical LSB-first pages (SSD1306/OLEDDisplay layout): + * byte index = x + (y / 8) * width, bit index = y % 8. + */ + MONO_VLSB = 0; + } + + /* + * Display width in pixels. + */ + uint32 width = 1; + + /* + * Display height in pixels. + */ + uint32 height = 2; + + /* + * Pixel encoding of data. + */ + Format format = 3; + + /* + * Monotonically increasing frame counter, constant across the chunks of + * one frame so the client can detect interleaving or loss. + */ + uint32 frame_id = 4; + + /* + * Byte offset of this chunk within the full frame buffer. + */ + uint32 offset = 5; + + /* + * Total size in bytes of the full frame buffer. + */ + uint32 total_size = 6; + + /* + * The framebuffer bytes for this chunk. + */ + bytes data = 7; +} + /* * Lockdown state report from firmware to client (for hardened builds * with MESHTASTIC_LOCKDOWN). Sent immediately after config_complete_id From 723823bd1588d1226f46731c16655aedfa725b04 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:05:42 -0500 Subject: [PATCH 2/6] Polish DisplayFrame contract from review FORMAT_UNSPECIFIED = 0 per the file's sentinel convention (MONO_VLSB moves to 1); document chunk ordering, frame_id reboot/wraparound semantics, mid-frame disable draining, local-only admin verbs and their ack story; bound width/height with int_size:16; correct the options sizing arithmetic. Co-Authored-By: Claude Fable 5 --- meshtastic/admin.proto | 13 ++++++++++--- meshtastic/mesh.options | 9 ++++++--- meshtastic/mesh.proto | 17 ++++++++++++++--- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/meshtastic/admin.proto b/meshtastic/admin.proto index 6353cc35c..04ef648bd 100644 --- a/meshtastic/admin.proto +++ b/meshtastic/admin.proto @@ -460,14 +460,21 @@ message AdminMessage { /* * Request a single frame of the device's display framebuffer. * The frame is delivered to the local client as FromRadio.display_frame - * chunks (see DisplayFrame in mesh.proto). + * chunks (see DisplayFrame in mesh.proto) — there is no AdminMessage + * response. Local connection only: a node receiving this over the mesh, + * or a build without a display, ignores it. During active mirroring it + * forces one frame on the next redraw even if the screen is unchanged. */ bool get_display_frame_request = 50; /* - * Enable or disable continuous mirroring of the device display. + * Enable (true) or disable (false) continuous mirroring of the device + * display — unlike most bool verbs in this oneof, false is meaningful. * While enabled, the device sends a DisplayFrame after each screen - * redraw that changed the framebuffer, as FromRadio.display_frame chunks. + * redraw that changed the framebuffer, as FromRadio.display_frame + * chunks; the first frame arrives immediately and acts as the + * acknowledgement. Local connection only (see get_display_frame_request) + * and not persisted across reboot. */ bool set_display_mirror = 51; diff --git a/meshtastic/mesh.options b/meshtastic/mesh.options index 7680c781c..a225b1912 100644 --- a/meshtastic/mesh.options +++ b/meshtastic/mesh.options @@ -110,10 +110,13 @@ *LoRaRegionPresets.group_index int_size:8 # Display mirroring (FromRadio.display_frame). 384 keeps FromRadio under the -# 512-byte MAX_TO_FROM_RADIO_SIZE cap (data 384+3, six varint fields <=30, oneof -# and submessage headers <=10, FromRadio.id <=6) and splits the common 128x64 -# 1bpp frame (1024 bytes) into three chunks aligned on whole page rows. +# 512-byte MAX_TO_FROM_RADIO_SIZE cap (data 384+3, five uint32 varints <=30 plus +# format <=2, oneof and submessage headers <=10, FromRadio.id <=6) and splits the +# common 128x64 1bpp frame (1024 bytes) into three chunks aligned on whole page +# rows. width/height are panel dimensions, always < 65536. *DisplayFrame.data max_size:384 +*DisplayFrame.width int_size:16 +*DisplayFrame.height int_size:16 *MqttClientProxyMessage.topic max_size:60 *MqttClientProxyMessage.data max_size:435 diff --git a/meshtastic/mesh.proto b/meshtastic/mesh.proto index b5a158cfc..c07d47cf9 100644 --- a/meshtastic/mesh.proto +++ b/meshtastic/mesh.proto @@ -2481,17 +2481,26 @@ message FromRadio { * A chunk of the device's display framebuffer, streamed to the local client * over BLE/serial/TCP. Frames larger than one chunk are split by byte offset; * a chunk with offset + data length == total_size completes the frame. + * Chunks of one frame arrive contiguously and in offset order (FromRadio is + * a reliable ordered stream), so clients may reassemble into a single buffer + * without reordering. A frame whose streaming has begun is always drained to + * completion, even if mirroring is disabled mid-frame. */ message DisplayFrame { /* * Pixel encodings of the framebuffer bytes. */ enum Format { + /* + * Default; should not be sent. + */ + FORMAT_UNSPECIFIED = 0; + /* * 1 bit per pixel, vertical LSB-first pages (SSD1306/OLEDDisplay layout): * byte index = x + (y / 8) * width, bit index = y % 8. */ - MONO_VLSB = 0; + MONO_VLSB = 1; } /* @@ -2510,8 +2519,10 @@ message DisplayFrame { Format format = 3; /* - * Monotonically increasing frame counter, constant across the chunks of - * one frame so the client can detect interleaving or loss. + * Frame counter, constant across the chunks of one frame so the client + * can detect interleaving or loss. Increments per captured frame, wraps + * at uint32 range, and restarts from 1 on device reboot — treat any + * change as "a new frame", not as an ordering guarantee. */ uint32 frame_id = 4; From 6431aab29ff91a7b2ef3b46cbb41ddc9daf95bd6 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:28:05 -0500 Subject: [PATCH 3/6] Add DisplayInfo device metadata and DisplayFrame partial-update rects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeviceMetadata.display (tag 15) describes the panel during the handshake — dimensions, frame format, PanelClass (OLED/LCD/TFT/EINK/HUB75) and touch — so clients can gate mirroring UI, expect e-ink refresh behavior, and map taps to touch events; absence means no display. DisplayFrame gains rect_x/y/width/height, reserved so future dirty-rect streaming (LVGL UIs) stays additive; firmware sends full frames only for now. Co-Authored-By: Claude Fable 5 --- meshtastic/mesh.options | 6 +++ meshtastic/mesh.proto | 102 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/meshtastic/mesh.options b/meshtastic/mesh.options index a225b1912..5350ca6e5 100644 --- a/meshtastic/mesh.options +++ b/meshtastic/mesh.options @@ -117,6 +117,12 @@ *DisplayFrame.data max_size:384 *DisplayFrame.width int_size:16 *DisplayFrame.height int_size:16 +*DisplayFrame.rect_x int_size:16 +*DisplayFrame.rect_y int_size:16 +*DisplayFrame.rect_width int_size:16 +*DisplayFrame.rect_height int_size:16 +*DisplayInfo.width int_size:16 +*DisplayInfo.height int_size:16 *MqttClientProxyMessage.topic max_size:60 *MqttClientProxyMessage.data max_size:435 diff --git a/meshtastic/mesh.proto b/meshtastic/mesh.proto index c07d47cf9..4a773cd31 100644 --- a/meshtastic/mesh.proto +++ b/meshtastic/mesh.proto @@ -2540,6 +2540,31 @@ message DisplayFrame { * The framebuffer bytes for this chunk. */ bytes data = 7; + + /* + * Optional partial-update rectangle, reserved for richer formats (e.g. + * dirty-rect RGB565 streaming from LVGL-based UIs). When rect_width > 0, + * data carries only the rectangle's pixels and offset/total_size describe + * the rectangle's own buffer; width/height above always remain the full + * display dimensions. Firmware currently sends only full frames (all four + * fields unset). + */ + uint32 rect_x = 8; + + /* + * See rect_x. + */ + uint32 rect_y = 9; + + /* + * See rect_x. + */ + uint32 rect_width = 10; + + /* + * See rect_x. + */ + uint32 rect_height = 11; } /* @@ -2883,6 +2908,83 @@ message DeviceMetadata { * This is a read-only capability and must be false when XEdDSA is not compiled in. */ bool has_xeddsa = 14; + + /* + * Describes the device's screen when one is present; absent on display-less + * builds. Lets clients gate display-mirroring UI (see DisplayFrame) and + * adapt to the panel — e.g. expect slow refresh from EINK, or offer + * tap-to-touch when has_touch is set. + */ + DisplayInfo display = 15; +} + +/* + * Static description of a device's display, sent inside DeviceMetadata + * during the connection handshake. + */ +message DisplayInfo { + /* + * Physical panel technology, as a hint for client rendering and refresh + * expectations. + */ + enum PanelClass { + /* + * Default; should not be sent. + */ + PANEL_CLASS_UNSPECIFIED = 0; + + /* + * Monochrome OLED (SSD1306/SH1106 family). + */ + OLED = 1; + + /* + * Monochrome LCD (ST7567 family). + */ + LCD = 2; + + /* + * Color TFT (currently rendering the 1bpp base UI). + */ + TFT = 3; + + /* + * E-ink panel: refresh is slow and full-screen; clients should prefer + * one-shot frame requests over continuous mirroring. + */ + EINK = 4; + + /* + * LED matrix (HUB75). + */ + HUB75 = 5; + } + + /* + * Display width in pixels. + */ + uint32 width = 1; + + /* + * Display height in pixels. + */ + uint32 height = 2; + + /* + * Pixel encoding that DisplayFrame streams from this device will use. + */ + DisplayFrame.Format format = 3; + + /* + * Physical panel technology. + */ + PanelClass panel_class = 4; + + /* + * True when the panel accepts touch input; clients may map taps on a + * mirrored frame to AdminMessage.send_input_event touch coordinates. + */ + bool has_touch = 5; } /* From 5e75d7a7cf0af9073feb1a498ad20b770134b810 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:45:40 -0500 Subject: [PATCH 4/6] Add DisplayPalette for color-accurate mirroring of 1bpp UIs on color panels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Streams the device's region color table (up to 48 rects with RGB565 on/off pairs plus defaults) as FromRadio.display_palette, chunked by region index and identified by a signature that DisplayFrame.palette_signature references — palettes re-send only when the layout or theme changes, so a color-TFT device mirrors in its true colors at mono bandwidth. Co-Authored-By: Claude Fable 5 --- meshtastic/mesh.options | 14 ++++++ meshtastic/mesh.proto | 94 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/meshtastic/mesh.options b/meshtastic/mesh.options index 5350ca6e5..599edeb29 100644 --- a/meshtastic/mesh.options +++ b/meshtastic/mesh.options @@ -124,6 +124,20 @@ *DisplayInfo.width int_size:16 *DisplayInfo.height int_size:16 +# Colorization palette (FromRadio.display_palette). 16 regions/chunk keeps the +# encoded message comfortably under the 512-byte cap (16 regions x <=26 bytes +# plus <=25 bytes of header fields); the firmware's table caps at 48 regions, +# so a full palette is at most 3 chunks, re-sent only on signature change. +*DisplayPalette.regions max_count:16 +*DisplayPalette.region_offset int_size:8 +*DisplayPalette.region_total int_size:8 +*DisplayPalette.ColorRegion.x int_size:16 +*DisplayPalette.ColorRegion.y int_size:16 +*DisplayPalette.ColorRegion.width int_size:16 +*DisplayPalette.ColorRegion.height int_size:16 +*DisplayPalette.ColorRegion.on_color int_size:16 +*DisplayPalette.ColorRegion.off_color int_size:16 + *MqttClientProxyMessage.topic max_size:60 *MqttClientProxyMessage.data max_size:435 *MqttClientProxyMessage.text max_size:435 diff --git a/meshtastic/mesh.proto b/meshtastic/mesh.proto index 4a773cd31..2b786dfea 100644 --- a/meshtastic/mesh.proto +++ b/meshtastic/mesh.proto @@ -2474,6 +2474,12 @@ message FromRadio { * AdminMessage.get_display_frame_request). */ DisplayFrame display_frame = 20; + + /* + * One chunk of the color palette referenced by display_frame's + * palette_signature (see DisplayPalette). + */ + DisplayPalette display_palette = 21; } } @@ -2565,6 +2571,94 @@ message DisplayFrame { * See rect_x. */ uint32 rect_height = 11; + + /* + * Identity of the DisplayPalette that colorizes this frame, matching + * DisplayPalette.signature. 0 when the device renders monochrome (no + * color panel); clients without the referenced palette yet should render + * monochrome until its chunks arrive. + */ + uint32 palette_signature = 12; +} + +/* + * Colorization palette for DisplayFrame streams from devices that paint the + * 1bpp base UI onto a color panel. The panel applies per-region on/off + * colors at flush time; streaming the same region table lets a client + * render the mirror in the panel's true colors at 1bpp bandwidth. + * Sent as FromRadio.display_palette, split by region index when the table + * exceeds one message; re-sent only when the region layout or theme changes + * (the signature changes with it). Within a chunk regions are ordered by + * their table index; a later region overrides earlier ones where they + * overlap. All colors are RGB565 in logical bit layout (RRRRRGGGGGGBBBBB). + */ +message DisplayPalette { + /* + * One colorized rectangle of the display. + */ + message ColorRegion { + /* + * Region origin and size in pixels. + */ + uint32 x = 1; + + /* + * See x. + */ + uint32 y = 2; + + /* + * See x. + */ + uint32 width = 3; + + /* + * See x. + */ + uint32 height = 4; + + /* + * RGB565 drawn for set (1) pixels inside this region. + */ + uint32 on_color = 5; + + /* + * RGB565 drawn for clear (0) pixels inside this region. + */ + uint32 off_color = 6; + } + + /* + * Identity of this palette; DisplayFrame.palette_signature references it. + * Changes whenever the region table or theme changes. + */ + uint32 signature = 1; + + /* + * RGB565 for set pixels outside all regions. + */ + uint32 default_on_color = 2; + + /* + * RGB565 for clear pixels outside all regions. + */ + uint32 default_off_color = 3; + + /* + * Table index of the first region in this chunk. + */ + uint32 region_offset = 4; + + /* + * Total regions in the complete palette; region_offset + regions length + * == region_total completes it. + */ + uint32 region_total = 5; + + /* + * The regions of this chunk, in table order. + */ + repeated ColorRegion regions = 6; } /* From 9d00824e06bd5c5b3bf38611b0b5e23bddc71c23 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:41:26 -0500 Subject: [PATCH 5/6] Tighten DisplayFrame/DisplayPalette docs from review Restate the options sizing arithmetic for the fields added since it was written (bound still holds at 447 <= 512); make palette override order explicitly global table order across chunk boundaries; allow palette chunks to interleave a frame's chunks; add the stale-partial-palette discard rule; drop the behavior-freezing parenthetical from PanelClass.TFT. Co-Authored-By: Claude Fable 5 --- meshtastic/mesh.options | 10 ++++++---- meshtastic/mesh.proto | 17 ++++++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/meshtastic/mesh.options b/meshtastic/mesh.options index 599edeb29..a7201e71d 100644 --- a/meshtastic/mesh.options +++ b/meshtastic/mesh.options @@ -110,10 +110,12 @@ *LoRaRegionPresets.group_index int_size:8 # Display mirroring (FromRadio.display_frame). 384 keeps FromRadio under the -# 512-byte MAX_TO_FROM_RADIO_SIZE cap (data 384+3, five uint32 varints <=30 plus -# format <=2, oneof and submessage headers <=10, FromRadio.id <=6) and splits the -# common 128x64 1bpp frame (1024 bytes) into three chunks aligned on whole page -# rows. width/height are panel dimensions, always < 65536. +# 512-byte MAX_TO_FROM_RADIO_SIZE cap: data 384+3, nine field varints <=50 +# (width/height and the four rect fields uint16-bounded, palette_signature a +# full uint32), format <=2, oneof and submessage headers <=10, FromRadio.id +# <=6 -> 447. It also splits the common 128x64 1bpp frame (1024 bytes) into +# three chunks aligned on whole page rows. width/height are panel dimensions, +# always < 65536. *DisplayFrame.data max_size:384 *DisplayFrame.width int_size:16 *DisplayFrame.height int_size:16 diff --git a/meshtastic/mesh.proto b/meshtastic/mesh.proto index 2b786dfea..b7766690d 100644 --- a/meshtastic/mesh.proto +++ b/meshtastic/mesh.proto @@ -2487,9 +2487,10 @@ message FromRadio { * A chunk of the device's display framebuffer, streamed to the local client * over BLE/serial/TCP. Frames larger than one chunk are split by byte offset; * a chunk with offset + data length == total_size completes the frame. - * Chunks of one frame arrive contiguously and in offset order (FromRadio is - * a reliable ordered stream), so clients may reassemble into a single buffer - * without reordering. A frame whose streaming has begun is always drained to + * Chunks of one frame arrive contiguously (no other display_frame between + * them; display_palette messages may interleave) and in offset order + * (FromRadio is a reliable ordered stream), so clients may reassemble into + * a single buffer without reordering. A frame whose streaming has begun is always drained to * completion, even if mirroring is disabled mid-frame. */ message DisplayFrame { @@ -2588,9 +2589,11 @@ message DisplayFrame { * render the mirror in the panel's true colors at 1bpp bandwidth. * Sent as FromRadio.display_palette, split by region index when the table * exceeds one message; re-sent only when the region layout or theme changes - * (the signature changes with it). Within a chunk regions are ordered by - * their table index; a later region overrides earlier ones where they - * overlap. All colors are RGB565 in logical bit layout (RRRRRGGGGGGBBBBB). + * (the signature changes with it). Regions are ordered by table index; a + * region with a higher table index overrides lower-indexed ones where they + * overlap, regardless of chunk boundaries. A client holding partial chunks + * of a signature that no longer matches incoming chunks should discard + * them. All colors are RGB565 in logical bit layout (RRRRRGGGGGGBBBBB). */ message DisplayPalette { /* @@ -3038,7 +3041,7 @@ message DisplayInfo { LCD = 2; /* - * Color TFT (currently rendering the 1bpp base UI). + * Color TFT. */ TFT = 3; From 575536388c04966249db9faee7b1b486b1f35aa3 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:44:17 -0500 Subject: [PATCH 6/6] Add DisplayFrame.Format RGB565 for LVGL dirty-rect streaming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Little-endian RGB565, rows tightly packed, carried in the partial-update rect fields — each rectangle completes independently under its own frame_id. Activates the reservation the rect fields were added for. Co-Authored-By: Claude Fable 5 --- meshtastic/mesh.proto | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/meshtastic/mesh.proto b/meshtastic/mesh.proto index b7766690d..b367bd088 100644 --- a/meshtastic/mesh.proto +++ b/meshtastic/mesh.proto @@ -2508,6 +2508,13 @@ message DisplayFrame { * byte index = x + (y / 8) * width, bit index = y % 8. */ MONO_VLSB = 1; + + /* + * 16 bits per pixel, RGB565 in little-endian byte order, rows tightly + * packed. Used with the partial-update rect fields: data covers only + * the rectangle. Streamed by LVGL-based color UIs. + */ + RGB565 = 2; } /* @@ -2549,12 +2556,12 @@ message DisplayFrame { bytes data = 7; /* - * Optional partial-update rectangle, reserved for richer formats (e.g. - * dirty-rect RGB565 streaming from LVGL-based UIs). When rect_width > 0, - * data carries only the rectangle's pixels and offset/total_size describe - * the rectangle's own buffer; width/height above always remain the full - * display dimensions. Firmware currently sends only full frames (all four - * fields unset). + * Optional partial-update rectangle (dirty-rect streaming from LVGL-based + * color UIs, format RGB565). When rect_width > 0, data carries only the + * rectangle's pixels and offset/total_size describe the rectangle's own + * buffer; width/height above always remain the full display dimensions, + * and each rectangle is an independently completed unit under its own + * frame_id. MONO_VLSB frames never set these fields. */ uint32 rect_x = 8;