rlvgl_core/cmd.rs
1//! Graphics-language layer: structured drawing commands as data, separable
2//! from execution.
3//!
4//! The [`Renderer`] trait is a *drawing API* — verbs invoked one at a time.
5//! [`Cmd`] + [`CommandList`] define a *graphics language* — a noun
6//! vocabulary where each draw is a value the backend consumes. The shift
7//! lets backends:
8//!
9//! - Inspect the whole frame before executing any of it (occlusion
10//! analysis, opaque-cmd skip, dirty-rect union).
11//! - Coalesce adjacent compatible ops (sequential same-color fills,
12//! batched OBB rasterizations).
13//! - Chain hardware transfers (configure DMA2D for cmd N+1 while N runs).
14//! - Bin commands by tile for cache-resident rendering.
15//! - Replay a recorded list to a different surface (e.g. snapshot tests,
16//! off-screen pre-render of static content).
17//!
18//! See [`Recorder`] for capturing an existing widget's [`Renderer`] calls
19//! into a [`CommandList`], and [`RendererSink`] for the default-dispatch
20//! adapter that any [`Renderer`] can be wrapped in to act as a
21//! [`CommandSink`].
22//!
23//! # Lifetime model
24//!
25//! v1 [`Cmd`] is `Clone` (owns its data — heap-allocated `String` /
26//! `Vec<Color>` for text and pixel ops). This trades alloc per
27//! unstructured op for a simpler, lifetime-free API. Future arena-backed
28//! variant can be added behind a feature flag if alloc cost becomes
29//! material.
30
31use alloc::string::String;
32use alloc::vec::Vec;
33
34use crate::raster::{Obb, PointF};
35use crate::renderer::Renderer;
36use crate::widget::{Color, Rect};
37
38/// Composition mode for a draw command. Backends that have hardware
39/// blend modes can branch on this; software backends may collapse Add /
40/// Multiply down to per-pixel arithmetic.
41#[derive(Copy, Clone, Debug, PartialEq, Eq)]
42pub enum BlendMode {
43 /// Replace destination pixels (alpha-ignoring fast path).
44 Replace,
45 /// Standard source-over with `color.alpha * coverage`.
46 SrcOver,
47 /// Additive blend: dst = clamp(dst + src * alpha).
48 Add,
49 /// Multiplicative blend: dst = dst * src (alpha modulates).
50 Multiply,
51}
52
53/// A single drawing command. Z-order is list position in the
54/// [`CommandList`] that contains it.
55#[derive(Clone, Debug)]
56pub enum Cmd {
57 /// Filled axis-aligned rectangle.
58 FillRect {
59 /// Rectangle bounds.
60 rect: Rect,
61 /// Fill color.
62 color: Color,
63 /// Composition mode.
64 blend: BlendMode,
65 },
66 /// Anti-aliased filled oriented bounding box (rotated rectangle).
67 FillObb {
68 /// Pre-computed OBB geometry.
69 obb: Obb,
70 /// Fill color.
71 color: Color,
72 /// Composition mode.
73 blend: BlendMode,
74 },
75 /// Anti-aliased filled disc (filled circle).
76 FillDisc {
77 /// Sub-pixel center.
78 center: PointF,
79 /// Radius in pixels.
80 radius: f32,
81 /// Fill color.
82 color: Color,
83 /// Composition mode.
84 blend: BlendMode,
85 },
86 /// Anti-aliased annular arc / pie slice. See
87 /// [`crate::raster::rasterize_arc`] for the angle convention.
88 FillArc {
89 /// Sub-pixel center.
90 center: PointF,
91 /// Outer radius in pixels.
92 r_outer: f32,
93 /// Inner radius in pixels (0.0 for a pie slice).
94 r_inner: f32,
95 /// Cosine of the start ray direction.
96 start_cos: f32,
97 /// Sine of the start ray direction.
98 start_sin: f32,
99 /// Cosine of the end ray direction.
100 end_cos: f32,
101 /// Sine of the end ray direction.
102 end_sin: f32,
103 /// Signed angular extent in radians.
104 extent: f32,
105 /// Fill color.
106 color: Color,
107 /// Composition mode.
108 blend: BlendMode,
109 },
110 /// Anti-aliased stroked line of given width (square caps).
111 StrokeLine {
112 /// Line start.
113 a: PointF,
114 /// Line end.
115 b: PointF,
116 /// Line width in pixels.
117 width: f32,
118 /// Stroke color.
119 color: Color,
120 /// Composition mode.
121 blend: BlendMode,
122 },
123 /// Text draw — owned `String` so the cmd can outlive the original
124 /// borrow. See module docs for the alloc trade-off.
125 DrawText {
126 /// Anchor position (baseline origin).
127 position: (i32, i32),
128 /// UTF-8 text.
129 text: String,
130 /// Text color.
131 color: Color,
132 },
133 /// Pixel-buffer blit — owned `Vec<Color>` for the same reason as
134 /// [`Cmd::DrawText`]'s string.
135 DrawPixels {
136 /// Top-left destination position.
137 position: (i32, i32),
138 /// Source pixels, row-major.
139 pixels: Vec<Color>,
140 /// Source width in pixels.
141 width: u32,
142 /// Source height in pixels.
143 height: u32,
144 },
145 /// Clip stack marker. `Some(rect)` pushes a clip; `None` clears.
146 /// Backends should bound subsequent draws to the active clip.
147 SetClip {
148 /// Clip rectangle, or `None` to clear.
149 rect: Option<Rect>,
150 },
151 /// Backend flush boundary (compositor swap, telemetry checkpoint).
152 /// Optimizing backends may not reorder cmds across a Barrier.
153 Barrier,
154}
155
156impl Cmd {
157 /// Dispatch this cmd onto a [`Renderer`], using the matching trait
158 /// method for each variant. This is the default execution semantics
159 /// — backends override [`CommandSink`] to specialize.
160 pub fn dispatch_to<R: Renderer + ?Sized>(&self, r: &mut R) {
161 match self {
162 Cmd::FillRect { rect, color, blend } => match blend {
163 BlendMode::Replace => r.fill_rect(*rect, *color),
164 BlendMode::SrcOver => r.blend_rect(*rect, *color),
165 // Add / Multiply have no Renderer-trait equivalent yet;
166 // fall through to SrcOver for v1. Backends with native
167 // support override CommandSink directly.
168 BlendMode::Add | BlendMode::Multiply => r.blend_rect(*rect, *color),
169 },
170 Cmd::FillObb { obb, color, .. } => r.fill_obb_aa(*obb, *color),
171 Cmd::FillDisc {
172 center,
173 radius,
174 color,
175 ..
176 } => r.fill_disc_aa(*center, *radius, *color),
177 Cmd::FillArc {
178 center,
179 r_outer,
180 r_inner,
181 start_cos,
182 start_sin,
183 end_cos,
184 end_sin,
185 extent,
186 color,
187 ..
188 } => r.fill_arc_aa(
189 *center, *r_outer, *r_inner, *start_cos, *start_sin, *end_cos, *end_sin, *extent,
190 *color,
191 ),
192 Cmd::StrokeLine {
193 a, b, width, color, ..
194 } => r.stroke_line_aa(*a, *b, *width, *color),
195 Cmd::DrawText {
196 position,
197 text,
198 color,
199 } => r.draw_text(*position, text, *color),
200 Cmd::DrawPixels {
201 position,
202 pixels,
203 width,
204 height,
205 } => r.draw_pixels(*position, pixels, *width, *height),
206 // Clip and Barrier are advisory for default dispatch — the
207 // base Renderer trait has no clip stack. Backends that
208 // implement clipping override CommandSink::submit directly.
209 Cmd::SetClip { .. } | Cmd::Barrier => {}
210 }
211 }
212
213 /// AABB of pixels this command might write, in absolute framebuffer
214 /// coords. Returns `None` for non-drawing markers (Barrier, SetClip).
215 pub fn aabb(&self) -> Option<Rect> {
216 match self {
217 Cmd::FillRect { rect, .. } => Some(*rect),
218 Cmd::FillObb { obb, .. } => Some(obb.aabb()),
219 Cmd::FillDisc { center, radius, .. }
220 | Cmd::FillArc {
221 center,
222 r_outer: radius,
223 ..
224 } => {
225 let pad = radius + 1.0;
226 Some(Rect {
227 x: (center.x - pad) as i32 - 1,
228 y: (center.y - pad) as i32 - 1,
229 width: (pad * 2.0) as i32 + 3,
230 height: (pad * 2.0) as i32 + 3,
231 })
232 }
233 Cmd::StrokeLine { a, b, width, .. } => {
234 let pad = width * 0.5 + 1.0;
235 let x0 = (a.x.min(b.x) - pad) as i32 - 1;
236 let y0 = (a.y.min(b.y) - pad) as i32 - 1;
237 let x1 = (a.x.max(b.x) + pad) as i32 + 2;
238 let y1 = (a.y.max(b.y) + pad) as i32 + 2;
239 Some(Rect {
240 x: x0,
241 y: y0,
242 width: x1 - x0,
243 height: y1 - y0,
244 })
245 }
246 Cmd::DrawText { .. } => None, // text bbox depends on font metrics
247 Cmd::DrawPixels {
248 position,
249 width,
250 height,
251 ..
252 } => Some(Rect {
253 x: position.0,
254 y: position.1,
255 width: *width as i32,
256 height: *height as i32,
257 }),
258 Cmd::SetClip { .. } | Cmd::Barrier => None,
259 }
260 }
261}
262
263/// Ordered list of drawing commands. Z-order is list position (later =
264/// on top). Backends consume the list via [`CommandSink::submit`].
265#[derive(Clone, Debug, Default)]
266pub struct CommandList {
267 cmds: Vec<Cmd>,
268}
269
270impl CommandList {
271 /// Create an empty command list.
272 pub fn new() -> Self {
273 Self::default()
274 }
275
276 /// Append a command. Z-order is list position.
277 pub fn push(&mut self, cmd: Cmd) {
278 self.cmds.push(cmd);
279 }
280
281 /// Number of commands in the list.
282 pub fn len(&self) -> usize {
283 self.cmds.len()
284 }
285
286 /// Whether the list contains no commands.
287 pub fn is_empty(&self) -> bool {
288 self.cmds.is_empty()
289 }
290
291 /// Iterate over commands in z-order (back to front).
292 pub fn iter(&self) -> core::slice::Iter<'_, Cmd> {
293 self.cmds.iter()
294 }
295
296 /// Drop all commands. Reuses the internal allocation.
297 pub fn clear(&mut self) {
298 self.cmds.clear();
299 }
300
301 /// Replay every command on `r` via [`Cmd::dispatch_to`]. This is the
302 /// "default-dispatch" execution semantics — backends that want
303 /// optimization implement [`CommandSink`] directly on themselves.
304 pub fn replay<R: Renderer + ?Sized>(&self, r: &mut R) {
305 for cmd in &self.cmds {
306 cmd.dispatch_to(r);
307 }
308 }
309
310 /// Union AABB of all drawing commands' write regions, computed via
311 /// [`Cmd::aabb`]. Returns `None` if the list contains no draw cmds
312 /// or only cmds with unknown bboxes (text).
313 pub fn dirty_union(&self) -> Option<Rect> {
314 let mut union: Option<Rect> = None;
315 for cmd in &self.cmds {
316 if let Some(bb) = cmd.aabb() {
317 union = Some(match union {
318 None => bb,
319 Some(u) => rect_union(u, bb),
320 });
321 }
322 }
323 union
324 }
325}
326
327fn rect_union(a: Rect, b: Rect) -> Rect {
328 let x0 = a.x.min(b.x);
329 let y0 = a.y.min(b.y);
330 let x1 = (a.x + a.width).max(b.x + b.width);
331 let y1 = (a.y + a.height).max(b.y + b.height);
332 Rect {
333 x: x0,
334 y: y0,
335 width: x1 - x0,
336 height: y1 - y0,
337 }
338}
339
340/// Backend trait for executing a [`CommandList`]. The blanket impl is
341/// not provided because that would prevent backends from supplying
342/// optimized implementations; instead, wrap any [`Renderer`] in
343/// [`RendererSink`] for default-dispatch semantics, or implement
344/// `CommandSink` directly for native batching.
345pub trait CommandSink {
346 /// Execute the entire command list. The list is consumed by
347 /// reference — backends that need owned cmds should clone.
348 fn submit(&mut self, list: &CommandList);
349}
350
351/// Default-dispatch adapter: wraps any [`Renderer`] and submits cmds by
352/// calling the matching trait method per command. Use this when you
353/// want command-list capture/replay without per-backend optimization.
354pub struct RendererSink<'a, R: Renderer + ?Sized> {
355 /// Inner renderer.
356 pub inner: &'a mut R,
357}
358
359impl<'a, R: Renderer + ?Sized> RendererSink<'a, R> {
360 /// Wrap a renderer. The result acts as a [`CommandSink`].
361 pub fn new(inner: &'a mut R) -> Self {
362 Self { inner }
363 }
364}
365
366impl<R: Renderer + ?Sized> CommandSink for RendererSink<'_, R> {
367 fn submit(&mut self, list: &CommandList) {
368 list.replay(self.inner);
369 }
370}
371
372/// `Renderer` adapter that records structured AA draws into a
373/// [`CommandList`]. Unstructured ops (`draw_text`, `draw_pixels`) and
374/// `blend_row` forward immediately to a passthrough renderer rather
375/// than recording — see module docs for the lifetime/alloc rationale.
376///
377/// Typical usage:
378///
379/// ```ignore
380/// let mut list = CommandList::new();
381/// {
382/// let mut sink = NullRenderer::default(); // or any "do nothing" renderer
383/// let mut recorder = Recorder::new(&mut list, &mut sink);
384/// widget.draw(&mut recorder);
385/// }
386/// // list now contains the structured draws
387/// ```
388pub struct Recorder<'a> {
389 list: &'a mut CommandList,
390 passthrough: &'a mut dyn Renderer,
391}
392
393impl<'a> Recorder<'a> {
394 /// Create a recorder writing structured draws into `list` and
395 /// forwarding unstructured ops to `passthrough`.
396 pub fn new(list: &'a mut CommandList, passthrough: &'a mut dyn Renderer) -> Self {
397 Self { list, passthrough }
398 }
399}
400
401impl Renderer for Recorder<'_> {
402 fn fill_rect(&mut self, rect: Rect, color: Color) {
403 self.list.push(Cmd::FillRect {
404 rect,
405 color,
406 blend: BlendMode::Replace,
407 });
408 }
409
410 fn blend_rect(&mut self, rect: Rect, color: Color) {
411 self.list.push(Cmd::FillRect {
412 rect,
413 color,
414 blend: BlendMode::SrcOver,
415 });
416 }
417
418 fn fill_obb_aa(&mut self, obb: Obb, color: Color) {
419 self.list.push(Cmd::FillObb {
420 obb,
421 color,
422 blend: BlendMode::SrcOver,
423 });
424 }
425
426 fn fill_disc_aa(&mut self, center: PointF, radius: f32, color: Color) {
427 self.list.push(Cmd::FillDisc {
428 center,
429 radius,
430 color,
431 blend: BlendMode::SrcOver,
432 });
433 }
434
435 #[allow(clippy::too_many_arguments)]
436 fn fill_arc_aa(
437 &mut self,
438 center: PointF,
439 r_outer: f32,
440 r_inner: f32,
441 start_cos: f32,
442 start_sin: f32,
443 end_cos: f32,
444 end_sin: f32,
445 extent: f32,
446 color: Color,
447 ) {
448 self.list.push(Cmd::FillArc {
449 center,
450 r_outer,
451 r_inner,
452 start_cos,
453 start_sin,
454 end_cos,
455 end_sin,
456 extent,
457 color,
458 blend: BlendMode::SrcOver,
459 });
460 }
461
462 fn stroke_line_aa(&mut self, a: PointF, b: PointF, width: f32, color: Color) {
463 self.list.push(Cmd::StrokeLine {
464 a,
465 b,
466 width,
467 color,
468 blend: BlendMode::SrcOver,
469 });
470 }
471
472 fn draw_text(&mut self, position: (i32, i32), text: &str, color: Color) {
473 // Unstructured: forward immediately. The recorded list misses
474 // this op; document the limitation. v2 may add owned-string
475 // capture if profiling shows it matters.
476 self.passthrough.draw_text(position, text, color);
477 }
478
479 fn draw_pixels(&mut self, position: (i32, i32), pixels: &[Color], width: u32, height: u32) {
480 // Same passthrough rationale as draw_text.
481 self.passthrough
482 .draw_pixels(position, pixels, width, height);
483 }
484
485 fn blend_row(&mut self, x: i32, y: i32, color: Color, coverage: &[u8]) {
486 // blend_row is the AA inner-loop primitive. Forward to passthrough
487 // since recording per-row coverage spans would explode the cmd
488 // count and defeat the language layer's batching purpose. Higher
489 // -level methods (fill_obb_aa etc.) record correctly above.
490 self.passthrough.blend_row(x, y, color, coverage);
491 }
492}