Skip to main content

i_slint_renderer_software/
scene.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! This is the module contain data structures for a scene of items that can be rendered
5
6use super::{
7    Fixed, PhysicalBorderRadius, PhysicalLength, PhysicalPoint, PhysicalRect, PhysicalRegion,
8    PhysicalSize, PremultipliedRgbaColor, RenderingRotation,
9};
10use alloc::rc::Rc;
11use alloc::vec::Vec;
12use euclid::Length;
13use i_slint_core::Color;
14use i_slint_core::graphics::{SharedImageBuffer, TexturePixelFormat};
15use i_slint_core::lengths::{PointLengths as _, SizeLengths as _};
16
17#[derive(Default)]
18pub struct SceneVectors {
19    pub textures: Vec<SceneTexture<'static>>,
20    pub rounded_rectangles: Vec<RoundedRectangle>,
21    pub shared_buffers: Vec<SharedBufferCommand>,
22    pub linear_gradients: Vec<LinearGradientCommand>,
23    pub radial_gradients: Vec<RadialGradientCommand>,
24    pub conic_gradients: Vec<ConicGradientCommand>,
25}
26
27pub struct Scene {
28    /// the next line to be processed
29    pub(super) current_line: PhysicalLength,
30
31    /// The items are sorted like so:
32    /// - `items[future_items_index..]` are the items that have `y > current_line`.
33    ///   They must be sorted by `y` (top to bottom), then by `z` (front to back)
34    /// - `items[..current_items_index]` are the items that overlap with the current_line,
35    ///   sorted by z (front to back)
36    pub(super) items: Vec<SceneItem>,
37
38    pub(super) vectors: SceneVectors,
39
40    pub(super) future_items_index: usize,
41    pub(super) current_items_index: usize,
42
43    pub(super) dirty_region: PhysicalRegion,
44
45    pub(super) current_line_ranges: Vec<core::ops::Range<i16>>,
46    pub(super) range_valid_until_line: PhysicalLength,
47}
48
49impl Scene {
50    pub fn new(
51        mut items: Vec<SceneItem>,
52        vectors: SceneVectors,
53        dirty_region: PhysicalRegion,
54    ) -> Self {
55        let current_line =
56            dirty_region.iter_box().map(|x| x.min.y_length()).min().unwrap_or_default();
57        items.retain(|i| i.pos.y_length() + i.size.height_length() > current_line);
58        items.sort_unstable_by(compare_scene_item);
59        let current_items_index = items.partition_point(|i| i.pos.y_length() <= current_line);
60        items[..current_items_index].sort_unstable_by(|a, b| b.z.cmp(&a.z));
61        let mut r = Self {
62            items,
63            current_line,
64            current_items_index,
65            future_items_index: current_items_index,
66            vectors,
67            dirty_region,
68            current_line_ranges: Default::default(),
69            range_valid_until_line: Default::default(),
70        };
71        r.recompute_ranges();
72        debug_assert_eq!(r.current_line, r.dirty_region.bounding_rect().origin.y_length());
73        r
74    }
75
76    /// Updates `current_items_index` and `future_items_index` to match the invariant
77    pub fn next_line(&mut self) {
78        self.current_line += PhysicalLength::new(1);
79
80        let skipped = self.current_line >= self.range_valid_until_line && self.recompute_ranges();
81
82        // The items array is split in part:
83        // 1. [0..i] are the items that have already been processed, that are on this line
84        // 2. [j..current_items_index] are the items from the previous line that might still be
85        //   valid on this line
86        // 3. [tmp1, tmp2] is a buffer where we swap items so we can make room for the items in [0..i]
87        // 4. [future_items_index..] are the items which might get processed now
88        // 5. [current_items_index..tmp1], [tmp2..future_items_index] and [i..j] is garbage
89        //
90        // At each step, we selecting the item with the higher z from the list 2 or 3 or 4 and take it from
91        // that list. Then we add it to the list [0..i] if it needs more processing. If needed,
92        // we move the first  item from list  2. to list 3. to make some room
93
94        let (mut i, mut j, mut tmp1, mut tmp2) =
95            (0, 0, self.current_items_index, self.current_items_index);
96
97        if skipped {
98            // Merge sort doesn't work in that case.
99            while j < self.current_items_index {
100                let item = self.items[j];
101                if item.pos.y_length() + item.size.height_length() > self.current_line {
102                    self.items[i] = item;
103                    i += 1;
104                }
105                j += 1;
106            }
107            while self.future_items_index < self.items.len() {
108                let item = self.items[self.future_items_index];
109                if item.pos.y_length() > self.current_line {
110                    break;
111                }
112                self.future_items_index += 1;
113                if item.pos.y_length() + item.size.height_length() < self.current_line {
114                    continue;
115                }
116                self.items[i] = item;
117                i += 1;
118            }
119            self.items[0..i].sort_unstable_by(|a, b| b.z.cmp(&a.z));
120            self.current_items_index = i;
121            return;
122        }
123
124        'outer: loop {
125            let future_next_z = self
126                .items
127                .get(self.future_items_index)
128                .filter(|i| i.pos.y_length() <= self.current_line)
129                .map(|i| i.z);
130            let item = loop {
131                if tmp1 != tmp2 {
132                    if future_next_z.map_or(true, |z| self.items[tmp1].z > z) {
133                        let idx = tmp1;
134                        tmp1 += 1;
135                        if tmp1 == tmp2 {
136                            tmp1 = self.current_items_index;
137                            tmp2 = self.current_items_index;
138                        }
139                        break self.items[idx];
140                    }
141                } else if j < self.current_items_index {
142                    let item = &self.items[j];
143                    if item.pos.y_length() + item.size.height_length() <= self.current_line {
144                        j += 1;
145                        continue;
146                    }
147                    if future_next_z.map_or(true, |z| item.z > z) {
148                        j += 1;
149                        break *item;
150                    }
151                }
152                if future_next_z.is_some() {
153                    self.future_items_index += 1;
154                    break self.items[self.future_items_index - 1];
155                }
156                break 'outer;
157            };
158            if i != j {
159                // there is room
160            } else if j >= self.current_items_index && tmp1 == tmp2 {
161                // the current_items list is empty
162                j += 1
163            } else if self.items[j].pos.y_length() + self.items[j].size.height_length()
164                <= self.current_line
165            {
166                // next item in the current_items array is no longer in this line
167                j += 1;
168            } else if tmp2 < self.future_items_index && j < self.current_items_index {
169                // move the next item in current_items
170                let to_move = self.items[j];
171                self.items[tmp2] = to_move;
172                j += 1;
173                tmp2 += 1;
174            } else {
175                debug_assert!(tmp1 >= self.current_items_index);
176                let sort_begin = i;
177                // merge sort doesn't work because we don't have enough tmp space, just bring all items and use a normal sort.
178                while j < self.current_items_index {
179                    let item = self.items[j];
180                    if item.pos.y_length() + item.size.height_length() > self.current_line {
181                        self.items[i] = item;
182                        i += 1;
183                    }
184                    j += 1;
185                }
186                self.items.copy_within(tmp1..tmp2, i);
187                i += tmp2 - tmp1;
188                debug_assert!(i < self.future_items_index);
189                self.items[i] = item;
190                i += 1;
191                while self.future_items_index < self.items.len() {
192                    let item = self.items[self.future_items_index];
193                    if item.pos.y_length() > self.current_line {
194                        break;
195                    }
196                    self.future_items_index += 1;
197                    self.items[i] = item;
198                    i += 1;
199                }
200                self.items[sort_begin..i].sort_unstable_by(|a, b| b.z.cmp(&a.z));
201                break;
202            }
203            self.items[i] = item;
204            i += 1;
205        }
206        self.current_items_index = i;
207        // check that current items are properly sorted
208        debug_assert!(self.items[0..self.current_items_index].windows(2).all(|x| x[0].z >= x[1].z));
209    }
210
211    // return true if lines were skipped
212    fn recompute_ranges(&mut self) -> bool {
213        let validity = super::region_line_ranges(
214            &self.dirty_region,
215            self.current_line.get(),
216            &mut self.current_line_ranges,
217        );
218        if self.current_line_ranges.is_empty() {
219            if let Some(next) = validity {
220                self.current_line = Length::new(next);
221                self.range_valid_until_line = Length::new(
222                    super::region_line_ranges(
223                        &self.dirty_region,
224                        self.current_line.get(),
225                        &mut self.current_line_ranges,
226                    )
227                    .unwrap_or_default(),
228                );
229                return true;
230            }
231        }
232        self.range_valid_until_line = Length::new(validity.unwrap_or_default());
233        false
234    }
235}
236
237#[derive(Clone, Copy, Debug)]
238pub struct SceneItem {
239    pub pos: PhysicalPoint,
240    pub size: PhysicalSize,
241    // this is the order of the item from which it is in the item tree
242    pub z: u16,
243    pub command: SceneCommand,
244}
245
246fn compare_scene_item(a: &SceneItem, b: &SceneItem) -> core::cmp::Ordering {
247    // First, order by line (top to bottom)
248    match a.pos.y.partial_cmp(&b.pos.y) {
249        None | Some(core::cmp::Ordering::Equal) => {}
250        Some(ord) => return ord,
251    }
252    // Then by the reverse z (front to back)
253    match a.z.partial_cmp(&b.z) {
254        None | Some(core::cmp::Ordering::Equal) => {}
255        Some(ord) => return ord.reverse(),
256    }
257
258    // anything else, we don't care
259    core::cmp::Ordering::Equal
260}
261
262#[derive(Clone, Copy, Debug)]
263#[repr(u8)]
264pub enum SceneCommand {
265    Rectangle {
266        color: PremultipliedRgbaColor,
267    },
268    /// texture_index is an index in the [`SceneVectors::textures`] array
269    Texture {
270        texture_index: u16,
271    },
272    /// shared_buffer_index is an index in [`SceneVectors::shared_buffers`]
273    SharedBuffer {
274        shared_buffer_index: u16,
275    },
276    /// rectangle_index is an index in the [`SceneVectors::rounded_rectangle`] array
277    RoundedRectangle {
278        rectangle_index: u16,
279    },
280    /// linear_gradient_index is an index in the [`SceneVectors::linear_gradients`] array
281    LinearGradient {
282        linear_gradient_index: u16,
283    },
284    /// radial_gradient_index is an index in the [`SceneVectors::radial_gradients`] array
285    RadialGradient {
286        radial_gradient_index: u16,
287    },
288    /// conic_gradient_index is an index in the [`SceneVectors::conic_gradients`] array
289    ConicGradient {
290        conic_gradient_index: u16,
291    },
292}
293
294pub struct SceneTexture<'a> {
295    /// This should have a size so that the entire slice is ((height - 1) * pixel_stride + width) * bpp
296    pub data: &'a [u8],
297    pub format: TexturePixelFormat,
298    /// number of pixels between two lines in the source
299    pub pixel_stride: u16,
300
301    pub extra: SceneTextureExtra,
302}
303
304impl<'a> SceneTexture<'a> {
305    pub fn source_size(&self) -> PhysicalSize {
306        let mut len = self.data.len();
307        if self.format == TexturePixelFormat::SignedDistanceField {
308            len -= 1;
309        } else {
310            len /= self.format.bpp();
311        }
312        let stride = self.pixel_stride as usize;
313        let h = len / stride;
314        let w = len % stride;
315        if w == 0 {
316            PhysicalSize::new(stride as _, h as _)
317        } else {
318            PhysicalSize::new(w as _, (h + 1) as _)
319        }
320    }
321
322    pub fn from_target_texture(
323        texture: &'a super::target_pixel_buffer::DrawTextureArgs,
324        clip: &PhysicalRect,
325    ) -> Option<(Self, PhysicalRect)> {
326        let (extra, geometry) = SceneTextureExtra::from_target_texture(texture, clip)?;
327        let source = texture.source();
328        Some((
329            Self {
330                data: source.data,
331                pixel_stride: (source.byte_stride / source.pixel_format.bpp()) as u16,
332                format: source.pixel_format,
333                extra,
334            },
335            geometry,
336        ))
337    }
338}
339
340#[derive(Clone, Copy, Debug)]
341pub struct SceneTextureExtra {
342    /// Delta x: the amount of "image pixel" that we need to skip for each physical pixel in the target buffer
343    pub dx: Fixed<u16, 8>,
344    pub dy: Fixed<u16, 8>,
345    /// Offset which is the coordinate of the "image pixel" which going to be drawn at location SceneItem::pos
346    pub off_x: Fixed<u16, 4>,
347    pub off_y: Fixed<u16, 4>,
348    /// Color to colorize. When not transparent, consider that the image is an alpha map and always use that color.
349    /// The alpha of this color is ignored. (it is supposed to be mixed in `Self::alpha`)
350    pub colorize: Color,
351    pub alpha: u8,
352    pub rotation: RenderingRotation,
353}
354
355impl SceneTextureExtra {
356    pub fn from_target_texture(
357        texture: &super::target_pixel_buffer::DrawTextureArgs,
358        clip: &PhysicalRect,
359    ) -> Option<(Self, PhysicalRect)> {
360        let geometry: PhysicalRect = euclid::rect(
361            texture.dst_x as i16,
362            texture.dst_y as i16,
363            texture.dst_width as i16,
364            texture.dst_height as i16,
365        );
366        let geometry = geometry.to_box2d();
367        let clipped_geometry = geometry.intersection(&clip.to_box2d())?;
368
369        let mut offset = match texture.rotation {
370            RenderingRotation::NoRotation => clipped_geometry.min - geometry.min,
371            RenderingRotation::Rotate90 => euclid::vec2(
372                clipped_geometry.min.y - geometry.min.y,
373                geometry.max.x - clipped_geometry.max.x,
374            ),
375            RenderingRotation::Rotate180 => geometry.max - clipped_geometry.max,
376            RenderingRotation::Rotate270 => euclid::vec2(
377                geometry.max.y - clipped_geometry.max.y,
378                clipped_geometry.min.x - geometry.min.x,
379            ),
380        };
381
382        let source_size = texture.source_size().cast::<i32>();
383        let (dx, dy) = if let Some(tiling) = &texture.tiling {
384            offset -= euclid::vec2(tiling.offset_x, tiling.offset_y).cast();
385
386            // FIXME: gap
387            tiling.gap_x;
388            tiling.gap_y;
389
390            (Fixed::from_f32(tiling.scale_x)?, Fixed::from_f32(tiling.scale_y)?)
391        } else {
392            let (dst_w, dst_h) = if texture.rotation.is_transpose() {
393                (texture.dst_height as i32, texture.dst_width as i32)
394            } else {
395                (texture.dst_width as i32, texture.dst_height as i32)
396            };
397            let dx = Fixed::<i32, 8>::from_fraction(source_size.width, dst_w);
398            let dy = Fixed::<i32, 8>::from_fraction(source_size.height, dst_h);
399            (dx, dy)
400        };
401
402        Some((
403            Self {
404                colorize: texture.colorize.unwrap_or_default(),
405                alpha: texture.alpha,
406                rotation: texture.rotation,
407                dx: Fixed::try_from_fixed(dx).ok()?,
408                dy: Fixed::try_from_fixed(dy).ok()?,
409                off_x: Fixed::try_from_fixed(dx * offset.x as i32).ok()?,
410                off_y: Fixed::try_from_fixed(dy * offset.y as i32).ok()?,
411            },
412            clipped_geometry.to_rect(),
413        ))
414    }
415}
416
417#[derive(Clone)]
418pub enum SharedBufferData {
419    SharedImage(SharedImageBuffer),
420    AlphaMap { data: Rc<[u8]>, width: u16 },
421}
422
423impl SharedBufferData {
424    pub fn width(&self) -> usize {
425        match self {
426            SharedBufferData::SharedImage(image) => image.width() as usize,
427            SharedBufferData::AlphaMap { width, .. } => *width as usize,
428        }
429    }
430    #[allow(unused)]
431    pub fn height(&self) -> usize {
432        match self {
433            SharedBufferData::SharedImage(image) => image.height() as usize,
434            SharedBufferData::AlphaMap { data, width, .. } => data.len() / *width as usize,
435        }
436    }
437}
438
439pub struct SharedBufferCommand {
440    pub buffer: SharedBufferData,
441    /// The source rectangle that is mapped into this command span
442    pub source_rect: PhysicalRect,
443    pub extra: SceneTextureExtra,
444}
445
446impl SharedBufferCommand {
447    pub fn as_texture(&self) -> SceneTexture<'_> {
448        let stride = self.buffer.width();
449        let core::ops::Range { start, end } = compute_range_in_buffer(&self.source_rect, stride);
450
451        match &self.buffer {
452            SharedBufferData::SharedImage(SharedImageBuffer::RGB8(b)) => SceneTexture {
453                data: &b.as_bytes()[start * 3..end * 3],
454                pixel_stride: stride as u16,
455                format: TexturePixelFormat::Rgb,
456                extra: self.extra,
457            },
458            SharedBufferData::SharedImage(SharedImageBuffer::RGBA8(b)) => SceneTexture {
459                data: &b.as_bytes()[start * 4..end * 4],
460                pixel_stride: stride as u16,
461                format: TexturePixelFormat::Rgba,
462                extra: self.extra,
463            },
464            SharedBufferData::SharedImage(SharedImageBuffer::RGBA8Premultiplied(b)) => {
465                SceneTexture {
466                    data: &b.as_bytes()[start * 4..end * 4],
467                    pixel_stride: stride as u16,
468                    format: TexturePixelFormat::RgbaPremultiplied,
469                    extra: self.extra,
470                }
471            }
472            SharedBufferData::AlphaMap { data, width } => SceneTexture {
473                data: &data[start..end],
474                pixel_stride: *width,
475                format: TexturePixelFormat::AlphaMap,
476                extra: self.extra,
477            },
478        }
479    }
480}
481
482/// Given a rectangle of coordinate in a buffer and a stride, compute the range, in pixel
483pub fn compute_range_in_buffer(
484    source_rect: &PhysicalRect,
485    pixel_stride: usize,
486) -> core::ops::Range<usize> {
487    let start = pixel_stride * source_rect.min_y() as usize + source_rect.min_x() as usize;
488    let end = pixel_stride * (source_rect.max_y() - 1) as usize + source_rect.max_x() as usize;
489    start..end
490}
491
492#[derive(Debug)]
493pub struct RoundedRectangle {
494    pub radius: PhysicalBorderRadius,
495    /// the border's width
496    pub width: PhysicalLength,
497    pub border_color: PremultipliedRgbaColor,
498    pub inner_color: PremultipliedRgbaColor,
499    /// The clips is the amount of pixels of the rounded rectangle that is clipped away.
500    /// For example, if left_clip > width, then the left border will not be visible, and
501    /// if left_clip > radius, then no radius will be seen in the left side
502    pub left_clip: PhysicalLength,
503    pub right_clip: PhysicalLength,
504    pub top_clip: PhysicalLength,
505    pub bottom_clip: PhysicalLength,
506}
507
508/// Goes from color 1 to color2
509///
510/// depending of `flags & 0b1`
511///  - if false: on the left side, goes from `start` to 1, on the right side, goes from 0 to `1-start`
512///  - if true: on the left side, goes from 0 to `1-start`, on the right side, goes from `start` to `1`
513#[derive(Debug)]
514pub struct LinearGradientCommand {
515    pub color1: PremultipliedRgbaColor,
516    pub color2: PremultipliedRgbaColor,
517    pub start: u8,
518    /// bit 0: if the slope is positive or negative
519    /// bit 1: if we should fill with color1 on the left side when left_clip is negative (or transparent)
520    /// bit 2: if we should fill with color2 on the left side when right_clip is negative (or transparent)
521    pub flags: u8,
522    /// If positive, the clip has the same meaning as in RoundedRectangle.
523    /// If negative, that means the "stop" is only starting or stopping at that point
524    pub left_clip: PhysicalLength,
525    pub right_clip: PhysicalLength,
526    pub top_clip: PhysicalLength,
527    pub bottom_clip: PhysicalLength,
528}
529
530/// Radial gradient that interpolates colors from the center outward
531///
532/// Unlike LinearGradientCommand, radial gradients don't have clipping fields
533/// because they radiate uniformly in all directions from the center point.
534/// The gradient is naturally clipped by the rectangle bounds during rendering.
535#[derive(Debug)]
536pub struct RadialGradientCommand {
537    /// The gradient stops (colors and positions)
538    pub stops: i_slint_core::SharedVector<i_slint_core::graphics::GradientStop>,
539    /// Center of the gradient relative to the item position
540    pub center_x: PhysicalLength,
541    pub center_y: PhysicalLength,
542}
543
544/// Conic gradient that interpolates colors around a center point
545///
546/// The gradient creates a color transition that rotates around the center of the
547/// rectangle being drawn. The angle positions are specified in the gradient stops,
548/// where 0 = 0 degrees (north) and 1 = 360 degrees. Colors are interpolated based
549/// on the angle from north, going clockwise.
550#[derive(Debug)]
551pub struct ConicGradientCommand {
552    /// The gradient stops (colors and normalized angle positions)
553    /// Position 0 = 0 degrees (north), 1 = 360 degrees
554    pub stops: i_slint_core::SharedVector<i_slint_core::graphics::GradientStop>,
555}