agg_gui/widget/backbuffer.rs
1//! Widget backbuffer types: caching specs, retained state, and compositing layers.
2//!
3//! Any widget can opt into a cached CPU backbuffer by returning `Some(&mut ...)`
4//! from [`Widget::backbuffer_cache_mut`]. The framework's `paint_subtree`
5//! handles caching transparently: when the widget is dirty (or has no bitmap
6//! yet) it allocates a fresh `Framebuffer`, runs `widget.paint` + all children
7//! into it via a software `GfxCtx`, and caches the resulting RGBA8 pixels as a
8//! shared `Arc<Vec<u8>>`. Every subsequent frame that finds the widget clean
9//! just blits the cached pixels through `ctx.draw_image_rgba_arc` — zero AGG
10//! cost in steady state. On the GL backend the `Arc`'s pointer identity keys
11//! the GPU texture cache (see `arc_texture_cache`), so the hardware texture
12//! is also reused across frames and dropped when the bitmap drops.
13//!
14//! LCD subpixel rendering works naturally inside a backbuffer: the widget
15//! paints its own background first (so text has a solid dst) and then any
16//! `fill_text` call composites the per-channel coverage mask onto that
17//! destination. No walk / sample / bg-declaration needed.
18
19use std::sync::atomic::{AtomicU64, Ordering};
20use std::sync::Arc;
21
22use crate::layout_props::Insets;
23
24/// How a widget's backbuffer stores pixels.
25///
26/// The choice controls what the framework allocates as the render
27/// target during `paint_subtree_backbuffered` and how the cached
28/// bitmap is composited back onto the parent.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum BackbufferMode {
31 /// 8-bit straight-alpha RGBA. Standard Porter-Duff `SRC_ALPHA,
32 /// ONE_MINUS_SRC_ALPHA` composite on blit. Works for any widget,
33 /// including ones with transparent areas. Text inside is grayscale
34 /// AA (no LCD subpixel).
35 Rgba,
36 /// 3 bytes-per-pixel **composited opaque RGB** — no alpha channel.
37 /// Every fill (rects, strokes, text, etc.) inside the buffer goes
38 /// through the 3× horizontal supersample + 5-tap filter + per-channel
39 /// src-over pipeline described in `lcd-subpixel-compositing.md`.
40 /// The buffer is blitted as an opaque RGB texture.
41 ///
42 /// **Contract:** the widget is responsible for painting content
43 /// that covers its full bounds with opaque fills (starting with a
44 /// bg rect). Uncovered pixels land as black on the parent because
45 /// there is no alpha channel to carry "no paint here."
46 LcdCoverage,
47}
48
49/// Unified backbuffer target kind requested by a widget.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum BackbufferKind {
52 /// Paint directly into the parent render target.
53 None,
54 /// Retained software RGBA framebuffer.
55 SoftwareRgba,
56 /// Retained software LCD coverage framebuffer.
57 SoftwareLcd,
58 /// Retained GL framebuffer object.
59 GlFbo,
60}
61
62/// Widget-owned backbuffer request. Windows use this for retained GL FBOs,
63/// while existing label/text-field CPU caches map naturally to the software
64/// variants.
65#[derive(Clone, Copy, Debug)]
66pub struct BackbufferSpec {
67 pub kind: BackbufferKind,
68 pub cached: bool,
69 pub alpha: f64,
70 pub outsets: Insets,
71 pub rounded_clip: Option<f64>,
72}
73
74impl BackbufferSpec {
75 pub const fn none() -> Self {
76 Self {
77 kind: BackbufferKind::None,
78 cached: false,
79 alpha: 1.0,
80 outsets: Insets::ZERO,
81 rounded_clip: None,
82 }
83 }
84}
85
86impl Default for BackbufferSpec {
87 fn default() -> Self {
88 Self::none()
89 }
90}
91
92/// A CPU bitmap owned by a widget that opts into backbuffer caching.
93///
94/// The framework re-rasterises when the cache's explicit dirty flag is set or
95/// when global styling epochs change.
96pub struct BackbufferCache {
97 /// In **Rgba** mode: top-row-first RGBA8 pixels, straight alpha.
98 /// Blitted via [`DrawCtx::draw_image_rgba_arc`].
99 ///
100 /// In **LcdCoverage** mode: top-row-first **colour plane** — 3
101 /// bytes/pixel (R_premult, G_premult, B_premult) matching the
102 /// convention of [`crate::lcd_coverage::LcdBuffer::color_plane`]
103 /// flipped to top-down. The companion alpha plane lives in
104 /// [`Self::lcd_alpha`].
105 pub pixels: Option<Arc<Vec<u8>>>,
106 /// `LcdCoverage`-mode companion to `pixels`: top-row-first per-channel
107 /// **alpha plane** (3 bytes/pixel, `(R_alpha, G_alpha, B_alpha)`).
108 /// `None` means this is a plain Rgba cache. When `Some`, the blit
109 /// step uses [`DrawCtx::draw_lcd_backbuffer_arc`] to preserve the
110 /// per-channel subpixel information through to the destination —
111 /// required for LCD chroma to survive the cache round-trip.
112 pub lcd_alpha: Option<Arc<Vec<u8>>>,
113 pub width: u32,
114 pub height: u32,
115 /// When true, the next paint will re-rasterise rather than reusing
116 /// `pixels`. Widgets set this from their mutation paths
117 /// (`set_text`, `set_color`, focus/hover changes, etc.) and the
118 /// framework clears it after a successful re-raster.
119 pub dirty: bool,
120 /// Visuals epoch (see [`crate::theme::current_visuals_epoch`]) recorded
121 /// the last time this cache was populated. `paint_subtree_backbuffered`
122 /// compares it against the live epoch and forces a re-raster on mismatch,
123 /// so widgets whose text/fill colours come from `ctx.visuals()` refresh
124 /// automatically on a dark/light theme flip without needing every widget
125 /// to subscribe to theme-change events.
126 pub theme_epoch: u64,
127 /// Typography epoch (see
128 /// [`crate::font_settings::current_typography_epoch`]) — same
129 /// pattern as `theme_epoch` but for font / size scale / LCD /
130 /// hinting / gamma / width / interval / faux-* globals. Lets a
131 /// slider drag in the System window's typography controls invalidate
132 /// every cached `Label` bitmap without bespoke hooks per widget.
133 pub typography_epoch: u64,
134 /// Async-state epoch (see
135 /// [`crate::animation::async_state_epoch`]) — bumped when an
136 /// off-thread / async source (e.g. an image fetch + decode)
137 /// finishes outside the normal event-dispatch path that would
138 /// otherwise mark widgets dirty. Mismatch forces a re-raster
139 /// so freshly-loaded data lands in newly-laid-out bounds.
140 pub async_state_epoch: u64,
141 /// Retained `LcdCoverage` planes (Y-up) kept alive between paints so an
142 /// over-scan-band widget can *partially* re-raster only the edited line
143 /// strip into the existing pixels instead of rebuilding the whole (up to
144 /// 2×-viewport) buffer on every keystroke. Only populated on the band path
145 /// (see [`crate::widget::Widget::backbuffer_band`]); `None` for every other
146 /// widget, so the extra memory and reuse machinery never touch them.
147 pub lcd_buffer: Option<crate::lcd_coverage::LcdBuffer>,
148 /// Set by the framework immediately before a band widget's `paint`: `true`
149 /// means the retained [`Self::lcd_buffer`] was reused unchanged (same size,
150 /// mode and styling epochs), so the widget may repaint only its dirty line
151 /// strip and leave the rest of the buffer intact. `false` forces a full
152 /// repaint (first raster, resize, re-anchor, theme flip). Transient — the
153 /// widget reads it during `paint` and must not rely on it afterwards.
154 pub partial_allowed: bool,
155 /// Stamped with a globally-unique value (see [`next_content_version`]) on
156 /// every raster that changes the published `pixels` / `lcd_alpha` content.
157 /// It lets backends that cache GPU textures by buffer *identity* detect an
158 /// in-place content change: the dirty-strip path updates the retained planes
159 /// through `Arc::make_mut`, which keeps the byte-buffer address stable, so a
160 /// pointer-keyed texture cache would otherwise miss the edit. Pairing the
161 /// buffer pointer with this version disambiguates. `0` means "never rastered".
162 pub content_version: u64,
163}
164
165impl BackbufferCache {
166 pub fn new() -> Self {
167 Self {
168 pixels: None,
169 lcd_alpha: None,
170 width: 0,
171 height: 0,
172 dirty: true,
173 theme_epoch: 0,
174 typography_epoch: 0,
175 async_state_epoch: 0,
176 lcd_buffer: None,
177 partial_allowed: false,
178 content_version: 0,
179 }
180 }
181
182 /// Mark the cache dirty so the next paint re-rasterises.
183 pub fn invalidate(&mut self) {
184 self.dirty = true;
185 }
186}
187
188/// Monotone, process-global content-revision counter for backbuffer caches.
189///
190/// Every raster that changes a cache's published pixels stamps
191/// [`BackbufferCache::content_version`] with a fresh value from here. Making it
192/// *global* (rather than per-cache) means a `(buffer_ptr, version)` pair can
193/// never collide across surface lifetimes — a freed buffer's address may be
194/// reused by a different cache, but the version it carries will always differ.
195/// That keeps a pointer-keyed GPU texture cache ABA-proof. Starts at 1 so `0`
196/// stays reserved for "never rastered".
197pub(crate) fn next_content_version() -> u64 {
198 static NEXT_CONTENT_VERSION: AtomicU64 = AtomicU64::new(1);
199 NEXT_CONTENT_VERSION.fetch_add(1, Ordering::Relaxed)
200}
201
202impl Default for BackbufferCache {
203 fn default() -> Self {
204 Self::new()
205 }
206}
207
208static NEXT_BACKBUFFER_ID: AtomicU64 = AtomicU64::new(1);
209
210/// Retained widget backbuffer state shared by software and GL implementations.
211pub struct BackbufferState {
212 id: u64,
213 pub cache: BackbufferCache,
214 pub dirty: bool,
215 pub width: u32,
216 pub height: u32,
217 pub spec_kind: BackbufferKind,
218 /// Visuals epoch recorded the last time this retained surface was repainted.
219 /// Retained backend layers compare it against the live theme epoch so a
220 /// dark/light flip rebuilds the window/layer in the shared paint path.
221 pub theme_epoch: u64,
222 /// Typography epoch recorded the last time this retained surface was
223 /// repainted. Without this, a clean parent FBO can keep compositing old
224 /// text after global font/LCD settings change.
225 pub typography_epoch: u64,
226 /// Async-state epoch (see [`crate::animation::async_state_epoch`])
227 /// recorded the last paint. Mismatch forces a re-raster so a
228 /// freshly-arrived async result (image fetch, font load) doesn't
229 /// composite the previous frame's stale FBO contents.
230 pub async_state_epoch: u64,
231 pub repaint_count: u64,
232 pub composite_count: u64,
233}
234
235impl BackbufferState {
236 pub fn new() -> Self {
237 Self {
238 id: NEXT_BACKBUFFER_ID.fetch_add(1, Ordering::Relaxed),
239 cache: BackbufferCache::new(),
240 dirty: true,
241 width: 0,
242 height: 0,
243 spec_kind: BackbufferKind::None,
244 theme_epoch: 0,
245 typography_epoch: 0,
246 async_state_epoch: 0,
247 repaint_count: 0,
248 composite_count: 0,
249 }
250 }
251
252 pub fn id(&self) -> u64 {
253 self.id
254 }
255
256 pub fn invalidate(&mut self) {
257 self.dirty = true;
258 self.cache.invalidate();
259 }
260}
261
262impl Default for BackbufferState {
263 fn default() -> Self {
264 Self::new()
265 }
266}
267
268/// Over-scan band request for a scrolling backbuffered widget.
269///
270/// Returned by [`crate::widget::Widget::backbuffer_band`]. It lets a widget
271/// (today only [`crate::widgets::TextArea`]) raster a *band* of content taller
272/// than its own viewport — the visible rect plus an extra `overscan_top` above
273/// and `overscan_bottom` below — so that ordinary scrolling within the band
274/// becomes a pure blit-offset change instead of a full backbuffer re-raster.
275///
276/// # Contract
277///
278/// * **Units.** `overscan_top` / `overscan_bottom` are extra logical pixels of
279/// content rastered above / below the widget bounds. `blit_dy` is the
280/// vertical blit offset in logical pixels (positive shifts the band's content
281/// *up* in Y-up, revealing lower content). The band is anchored in content
282/// space by the widget; `blit_dy` is the residual between the live scroll
283/// offset and the anchor the band was rastered at.
284/// * **Quantization.** The framework quantizes both the overscan extents and
285/// `blit_dy` to whole *physical* pixels before sizing the buffer, translating
286/// the raster, and translating the blit — so the LCD subpixel structure is
287/// never resampled. The widget may pass raw (unquantized) logical values.
288/// * **Who clips.** The framework clips the band blit to the widget's bounds on
289/// every backend, so the over-scan margins never paint over sibling widgets.
290/// The widget is responsible for painting opaque content across the *whole*
291/// band (including the over-scan margins) so no gap shows through as the band
292/// is scrolled, and for painting any fixed chrome (border) in `paint_overlay`
293/// so it does not scroll with the band.
294/// * **Invalidation.** The widget must keep the band's anchor (and hence the
295/// rastered content) out of its backbuffer-cache signature, so scrolling
296/// within the band does not invalidate the cache. It must re-anchor and
297/// invalidate only when the live offset leaves the band (or the size /
298/// content / style changes).
299///
300/// Returning `None` (the default) opts out entirely and keeps the byte-for-byte
301/// original bounds-sized raster + 1:1 blit path.
302#[derive(Clone, Copy, Debug, PartialEq)]
303pub struct BackbufferBand {
304 /// Extra logical pixels of content rastered above the widget's top edge.
305 pub overscan_top: f64,
306 /// Extra logical pixels of content rastered below the widget's bottom edge.
307 pub overscan_bottom: f64,
308 /// Vertical blit offset in logical pixels (live offset − band anchor).
309 /// Positive shifts the band content up (Y-up), revealing lower content.
310 pub blit_dy: f64,
311 /// Widget-local Y-up logical `(lo, hi)` extent of the rows the widget will
312 /// repaint *if* the framework grants a partial (`partial_allowed`) re-raster
313 /// this paint. `None` means a granted partial would still repaint the whole
314 /// band (no confined strip). The framework uses this to limit the top-down
315 /// plane update to exactly those rows, so it must cover byte-for-byte the
316 /// same extent the widget fills and clips during its strip repaint.
317 pub dirty_strip_y: Option<(f64, f64)>,
318}
319
320/// Offscreen compositing layer requested by a widget for itself and its
321/// descendants.
322#[derive(Clone, Copy, Debug)]
323pub struct CompositingLayer {
324 /// Extra transparent pixels to the left of the widget bounds.
325 pub outset_left: f64,
326 /// Extra transparent pixels below the widget bounds.
327 pub outset_bottom: f64,
328 /// Extra transparent pixels to the right of the widget bounds.
329 pub outset_right: f64,
330 /// Extra transparent pixels above the widget bounds.
331 pub outset_top: f64,
332 /// Whole-layer opacity applied while compositing back to the parent.
333 pub alpha: f64,
334}
335
336impl CompositingLayer {
337 pub const fn new(
338 outset_left: f64,
339 outset_bottom: f64,
340 outset_right: f64,
341 outset_top: f64,
342 alpha: f64,
343 ) -> Self {
344 Self {
345 outset_left,
346 outset_bottom,
347 outset_right,
348 outset_top,
349 alpha,
350 }
351 }
352}