From a6ff642da5396ccddfc9106832902ac457db0a18 Mon Sep 17 00:00:00 2001 From: Olivier Goffart Date: Fri, 11 Oct 2024 11:56:46 +0200 Subject: [PATCH] refactor: split sofware_renderer.rs in modules --- internal/core/software_renderer.rs | 495 +----------------- .../minimal_software_window.rs | 85 +++ internal/core/software_renderer/scene.rs | 427 +++++++++++++++ 3 files changed, 518 insertions(+), 489 deletions(-) create mode 100644 internal/core/software_renderer/minimal_software_window.rs create mode 100644 internal/core/software_renderer/scene.rs diff --git a/internal/core/software_renderer.rs b/internal/core/software_renderer.rs index 32f6c98ce45..00092689319 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -10,9 +10,13 @@ mod draw_functions; mod fixed; mod fonts; +mod minimal_software_window; +mod scene; use self::fonts::GlyphRenderer; -use crate::api::{PlatformError, Window}; +pub use self::minimal_software_window::MinimalSoftwareWindow; +use self::scene::*; +use crate::api::PlatformError; use crate::graphics::rendering_metrics_collector::{RefreshMode, RenderingMetricsCollector}; use crate::graphics::{ BorderRadius, PixelFormat, Rgba8Pixel, SharedImageBuffer, SharedPixelBuffer, @@ -23,7 +27,7 @@ use crate::lengths::{ LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector, PhysicalPx, PointLengths, RectLengths, ScaleFactor, SizeLengths, }; -use crate::renderer::{Renderer, RendererSealed}; +use crate::renderer::RendererSealed; use crate::textlayout::{AbstractFont, FontMetrics, TextParagraphLayout}; use crate::window::{WindowAdapter, WindowInner}; use crate::{Brush, Color, Coord, ImageInner, StaticTextures}; @@ -997,417 +1001,6 @@ fn render_window_frame_by_line( scene.dirty_region } -#[derive(Default)] -struct SceneVectors { - textures: Vec>, - rounded_rectangles: Vec, - shared_buffers: Vec, - gradients: Vec, -} - -struct Scene { - /// the next line to be processed - current_line: PhysicalLength, - - /// The items are sorted like so: - /// - `items[future_items_index..]` are the items that have `y > current_line`. - /// They must be sorted by `y` (top to bottom), then by `z` (front to back) - /// - `items[..current_items_index]` are the items that overlap with the current_line, - /// sorted by z (front to back) - items: Vec, - - vectors: SceneVectors, - - future_items_index: usize, - current_items_index: usize, - - dirty_region: PhysicalRegion, - - current_line_ranges: Vec>, - range_valid_until_line: PhysicalLength, -} - -impl Scene { - pub fn new( - mut items: Vec, - vectors: SceneVectors, - dirty_region: PhysicalRegion, - ) -> Self { - let current_line = - dirty_region.iter_box().map(|x| x.min.y_length()).min().unwrap_or_default(); - items.retain(|i| i.pos.y_length() + i.size.height_length() > current_line); - items.sort_unstable_by(compare_scene_item); - let current_items_index = items.partition_point(|i| i.pos.y_length() <= current_line); - items[..current_items_index].sort_unstable_by(|a, b| b.z.cmp(&a.z)); - let mut r = Self { - items, - current_line, - current_items_index, - future_items_index: current_items_index, - vectors, - dirty_region, - current_line_ranges: Default::default(), - range_valid_until_line: Default::default(), - }; - r.recompute_ranges(); - debug_assert_eq!(r.current_line, r.dirty_region.bounding_rect().origin.y_length()); - r - } - - /// Updates `current_items_index` and `future_items_index` to match the invariant - pub fn next_line(&mut self) { - self.current_line += PhysicalLength::new(1); - - let skipped = self.current_line >= self.range_valid_until_line && self.recompute_ranges(); - - // The items array is split in part: - // 1. [0..i] are the items that have already been processed, that are on this line - // 2. [j..current_items_index] are the items from the previous line that might still be - // valid on this line - // 3. [tmp1, tmp2] is a buffer where we swap items so we can make room for the items in [0..i] - // 4. [future_items_index..] are the items which might get processed now - // 5. [current_items_index..tmp1], [tmp2..future_items_index] and [i..j] is garbage - // - // At each step, we selecting the item with the higher z from the list 2 or 3 or 4 and take it from - // that list. Then we add it to the list [0..i] if it needs more processing. If needed, - // we move the first item from list 2. to list 3. to make some room - - let (mut i, mut j, mut tmp1, mut tmp2) = - (0, 0, self.current_items_index, self.current_items_index); - - if skipped { - // Merge sort doesn't work in that case. - while j < self.current_items_index { - let item = self.items[j]; - if item.pos.y_length() + item.size.height_length() > self.current_line { - self.items[i] = item; - i += 1; - } - j += 1; - } - while self.future_items_index < self.items.len() { - let item = self.items[self.future_items_index]; - if item.pos.y_length() > self.current_line { - break; - } - self.future_items_index += 1; - if item.pos.y_length() + item.size.height_length() < self.current_line { - continue; - } - self.items[i] = item; - i += 1; - } - self.items[0..i].sort_unstable_by(|a, b| b.z.cmp(&a.z)); - self.current_items_index = i; - return; - } - - 'outer: loop { - let future_next_z = self - .items - .get(self.future_items_index) - .filter(|i| i.pos.y_length() <= self.current_line) - .map(|i| i.z); - let item = loop { - if tmp1 != tmp2 { - if future_next_z.map_or(true, |z| self.items[tmp1].z > z) { - let idx = tmp1; - tmp1 += 1; - if tmp1 == tmp2 { - tmp1 = self.current_items_index; - tmp2 = self.current_items_index; - } - break self.items[idx]; - } - } else if j < self.current_items_index { - let item = &self.items[j]; - if item.pos.y_length() + item.size.height_length() <= self.current_line { - j += 1; - continue; - } - if future_next_z.map_or(true, |z| item.z > z) { - j += 1; - break *item; - } - } - if future_next_z.is_some() { - self.future_items_index += 1; - break self.items[self.future_items_index - 1]; - } - break 'outer; - }; - if i != j { - // there is room - } else if j >= self.current_items_index && tmp1 == tmp2 { - // the current_items list is empty - j += 1 - } else if self.items[j].pos.y_length() + self.items[j].size.height_length() - <= self.current_line - { - // next item in the current_items array is no longer in this line - j += 1; - } else if tmp2 < self.future_items_index && j < self.current_items_index { - // move the next item in current_items - let to_move = self.items[j]; - self.items[tmp2] = to_move; - j += 1; - tmp2 += 1; - } else { - debug_assert!(tmp1 >= self.current_items_index); - let sort_begin = i; - // merge sort doesn't work because we don't have enough tmp space, just bring all items and use a normal sort. - while j < self.current_items_index { - let item = self.items[j]; - if item.pos.y_length() + item.size.height_length() > self.current_line { - self.items[i] = item; - i += 1; - } - j += 1; - } - self.items.copy_within(tmp1..tmp2, i); - i += tmp2 - tmp1; - debug_assert!(i < self.future_items_index); - self.items[i] = item; - i += 1; - while self.future_items_index < self.items.len() { - let item = self.items[self.future_items_index]; - if item.pos.y_length() > self.current_line { - break; - } - self.future_items_index += 1; - self.items[i] = item; - i += 1; - } - self.items[sort_begin..i].sort_unstable_by(|a, b| b.z.cmp(&a.z)); - break; - } - self.items[i] = item; - i += 1; - } - self.current_items_index = i; - // check that current items are properly sorted - debug_assert!(self.items[0..self.current_items_index].windows(2).all(|x| x[0].z >= x[1].z)); - } - - // return true if lines were skipped - fn recompute_ranges(&mut self) -> bool { - let validity = region_line_ranges( - &self.dirty_region, - self.current_line.get(), - &mut self.current_line_ranges, - ); - if self.current_line_ranges.is_empty() { - if let Some(next) = validity { - self.current_line = Length::new(next); - self.range_valid_until_line = Length::new( - region_line_ranges( - &self.dirty_region, - self.current_line.get(), - &mut self.current_line_ranges, - ) - .unwrap_or_default(), - ); - return true; - } - } - self.range_valid_until_line = Length::new(validity.unwrap_or_default()); - false - } -} - -#[derive(Clone, Copy, Debug)] -struct SceneItem { - pos: PhysicalPoint, - size: PhysicalSize, - // this is the order of the item from which it is in the item tree - z: u16, - command: SceneCommand, -} - -fn compare_scene_item(a: &SceneItem, b: &SceneItem) -> core::cmp::Ordering { - // First, order by line (top to bottom) - match a.pos.y.partial_cmp(&b.pos.y) { - None | Some(core::cmp::Ordering::Equal) => {} - Some(ord) => return ord, - } - // Then by the reverse z (front to back) - match a.z.partial_cmp(&b.z) { - None | Some(core::cmp::Ordering::Equal) => {} - Some(ord) => return ord.reverse(), - } - - // anything else, we don't care - core::cmp::Ordering::Equal -} - -#[derive(Clone, Copy, Debug)] -#[repr(u8)] -enum SceneCommand { - Rectangle { - color: PremultipliedRgbaColor, - }, - /// texture_index is an index in the [`SceneVectors::textures`] array - Texture { - texture_index: u16, - }, - /// shared_buffer_index is an index in [`SceneVectors::shared_buffers`] - SharedBuffer { - shared_buffer_index: u16, - }, - /// rectangle_index is an index in the [`SceneVectors::rounded_rectangle`] array - RoundedRectangle { - rectangle_index: u16, - }, - /// rectangle_index is an index in the [`SceneVectors::rounded_gradients`] array - Gradient { - gradient_index: u16, - }, -} - -struct SceneTexture<'a> { - /// This should have a size so that the entire slice is ((height - 1) * pixel_stride + width) * bpp - data: &'a [u8], - format: PixelFormat, - /// number of pixels between two lines in the source - pixel_stride: u16, - - extra: SceneTextureExtra, -} - -impl<'a> SceneTexture<'a> { - fn source_size(&self) -> PhysicalSize { - let len = self.data.len() / self.format.bpp(); - let stride = self.pixel_stride as usize; - let h = len / stride; - let w = len % stride; - if w == 0 { - PhysicalSize::new(stride as _, h as _) - } else { - PhysicalSize::new(w as _, (h + 1) as _) - } - } -} - -#[derive(Clone, Copy, Debug)] -struct SceneTextureExtra { - /// Delta x: the amount of "image pixel" that we need to skip for each physical pixel in the target buffer - dx: Fixed, - dy: Fixed, - /// Offset which is the coordinate of the "image pixel" which going to be drawn at location SceneItem::pos - off_x: Fixed, - off_y: Fixed, - /// Color to colorize. When not transparent, consider that the image is an alpha map and always use that color. - /// The alpha of this color is ignored. (it is supposed to be mixed in `Self::alpha`) - colorize: Color, - alpha: u8, - rotation: RenderingRotation, -} - -enum SharedBufferData { - SharedImage(SharedImageBuffer), - AlphaMap { data: Rc<[u8]>, width: u16 }, -} - -impl SharedBufferData { - fn width(&self) -> usize { - match self { - SharedBufferData::SharedImage(image) => image.width() as usize, - SharedBufferData::AlphaMap { width, .. } => *width as usize, - } - } -} - -struct SharedBufferCommand { - buffer: SharedBufferData, - /// The source rectangle that is mapped into this command span - source_rect: PhysicalRect, - extra: SceneTextureExtra, -} - -impl SharedBufferCommand { - fn as_texture(&self) -> SceneTexture<'_> { - let stride = self.buffer.width(); - let core::ops::Range { start, end } = compute_range_in_buffer(&self.source_rect, stride); - - match &self.buffer { - SharedBufferData::SharedImage(SharedImageBuffer::RGB8(b)) => SceneTexture { - data: &b.as_bytes()[start * 3..end * 3], - pixel_stride: stride as u16, - format: PixelFormat::Rgb, - extra: self.extra, - }, - SharedBufferData::SharedImage(SharedImageBuffer::RGBA8(b)) => SceneTexture { - data: &b.as_bytes()[start * 4..end * 4], - pixel_stride: stride as u16, - format: PixelFormat::Rgba, - extra: self.extra, - }, - SharedBufferData::SharedImage(SharedImageBuffer::RGBA8Premultiplied(b)) => { - SceneTexture { - data: &b.as_bytes()[start * 4..end * 4], - pixel_stride: stride as u16, - format: PixelFormat::RgbaPremultiplied, - extra: self.extra, - } - } - SharedBufferData::AlphaMap { data, width } => SceneTexture { - data: &data[start..end], - pixel_stride: *width, - format: PixelFormat::AlphaMap, - extra: self.extra, - }, - } - } -} - -// Given a rectangle of coordinate in a buffer and a stride, compute the range, in pixel -fn compute_range_in_buffer( - source_rect: &PhysicalRect, - pixel_stride: usize, -) -> core::ops::Range { - let start = pixel_stride * source_rect.min_y() as usize + source_rect.min_x() as usize; - let end = pixel_stride * (source_rect.max_y() - 1) as usize + source_rect.max_x() as usize; - start..end -} - -#[derive(Debug)] -struct RoundedRectangle { - radius: PhysicalBorderRadius, - /// the border's width - width: PhysicalLength, - border_color: PremultipliedRgbaColor, - inner_color: PremultipliedRgbaColor, - /// The clips is the amount of pixels of the rounded rectangle that is clipped away. - /// For example, if left_clip > width, then the left border will not be visible, and - /// if left_clip > radius, then no radius will be seen in the left side - left_clip: PhysicalLength, - right_clip: PhysicalLength, - top_clip: PhysicalLength, - bottom_clip: PhysicalLength, -} - -/// Goes from color 1 to color2 -/// -/// depending of `flags & 0b1` -/// - if false: on the left side, goes from `start` to 1, on the right side, goes from 0 to `1-start` -/// - if true: on the left side, goes from 0 to `1-start`, on the right side, goes from `start` to `1` -#[derive(Debug)] -struct GradientCommand { - color1: PremultipliedRgbaColor, - color2: PremultipliedRgbaColor, - start: u8, - /// bit 0: if the slope is positive or negative - /// bit 1: if we should fill with color1 on the left side when left_clip is negative (or transparent) - /// bit 2: if we should fill with color2 on the left side when right_clip is negative (or transparent) - flags: u8, - /// If positive, the clip has the same meaning as in RoundedRectangle. - /// If negative, that means the "stop" is only starting or stopping at that point - left_clip: PhysicalLength, - right_clip: PhysicalLength, - top_clip: PhysicalLength, - bottom_clip: PhysicalLength, -} - fn prepare_scene( window: &WindowInner, size: PhysicalSize, @@ -2669,79 +2262,3 @@ impl<'a, T: ProcessScene> crate::item_rendering::ItemRenderer for SceneBuilder<' None } } - -/// This is a minimal adapter for a Window that doesn't have any other feature than rendering -/// using the software renderer. -pub struct MinimalSoftwareWindow { - window: Window, - renderer: SoftwareRenderer, - needs_redraw: Cell, - size: Cell, -} - -impl MinimalSoftwareWindow { - /// Instantiate a new MinimalWindowAdaptor - /// - /// The `repaint_buffer_type` parameter specify what kind of buffer are passed to the [`SoftwareRenderer`] - pub fn new(repaint_buffer_type: RepaintBufferType) -> Rc { - Rc::new_cyclic(|w: &Weak| Self { - window: Window::new(w.clone()), - renderer: SoftwareRenderer::new_with_repaint_buffer_type(repaint_buffer_type), - needs_redraw: Default::default(), - size: Default::default(), - }) - } - /// If the window needs to be redrawn, the callback will be called with the - /// [renderer](SoftwareRenderer) that should be used to do the drawing. - /// - /// [`SoftwareRenderer::render()`] or [`SoftwareRenderer::render_by_line()`] should be called - /// in that callback. - /// - /// Return true if something was redrawn. - pub fn draw_if_needed(&self, render_callback: impl FnOnce(&SoftwareRenderer)) -> bool { - if self.needs_redraw.replace(false) || self.renderer.rendering_metrics_collector.is_some() { - render_callback(&self.renderer); - true - } else { - false - } - } - - #[doc(hidden)] - /// Forward to the window through Deref - /// (Before 1.1, WindowAdapter didn't have set_size, so the one from Deref was used. - /// But in Slint 1.1, if one had imported the WindowAdapter trait, the other one would be found) - pub fn set_size(&self, size: impl Into) { - self.window.set_size(size); - } -} - -impl WindowAdapter for MinimalSoftwareWindow { - fn window(&self) -> &Window { - &self.window - } - - fn renderer(&self) -> &dyn Renderer { - &self.renderer - } - - fn size(&self) -> crate::api::PhysicalSize { - self.size.get() - } - fn set_size(&self, size: crate::api::WindowSize) { - self.size.set(size.to_physical(1.)); - self.window - .dispatch_event(crate::platform::WindowEvent::Resized { size: size.to_logical(1.) }) - } - - fn request_redraw(&self) { - self.needs_redraw.set(true); - } -} - -impl core::ops::Deref for MinimalSoftwareWindow { - type Target = Window; - fn deref(&self) -> &Self::Target { - &self.window - } -} diff --git a/internal/core/software_renderer/minimal_software_window.rs b/internal/core/software_renderer/minimal_software_window.rs new file mode 100644 index 00000000000..7f817ff085e --- /dev/null +++ b/internal/core/software_renderer/minimal_software_window.rs @@ -0,0 +1,85 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +use super::{RepaintBufferType, SoftwareRenderer}; +use crate::api::Window; +use crate::platform::Renderer; +use crate::window::WindowAdapter; +use alloc::rc::{Rc, Weak}; +use core::cell::Cell; + +/// This is a minimal adapter for a Window that doesn't have any other feature than rendering +/// using the software renderer. +pub struct MinimalSoftwareWindow { + window: Window, + renderer: SoftwareRenderer, + needs_redraw: Cell, + size: Cell, +} + +impl MinimalSoftwareWindow { + /// Instantiate a new MinimalWindowAdaptor + /// + /// The `repaint_buffer_type` parameter specify what kind of buffer are passed to the [`SoftwareRenderer`] + pub fn new(repaint_buffer_type: RepaintBufferType) -> Rc { + Rc::new_cyclic(|w: &Weak| Self { + window: Window::new(w.clone()), + renderer: SoftwareRenderer::new_with_repaint_buffer_type(repaint_buffer_type), + needs_redraw: Default::default(), + size: Default::default(), + }) + } + /// If the window needs to be redrawn, the callback will be called with the + /// [renderer](SoftwareRenderer) that should be used to do the drawing. + /// + /// [`SoftwareRenderer::render()`] or [`SoftwareRenderer::render_by_line()`] should be called + /// in that callback. + /// + /// Return true if something was redrawn. + pub fn draw_if_needed(&self, render_callback: impl FnOnce(&SoftwareRenderer)) -> bool { + if self.needs_redraw.replace(false) || self.renderer.rendering_metrics_collector.is_some() { + render_callback(&self.renderer); + true + } else { + false + } + } + + #[doc(hidden)] + /// Forward to the window through Deref + /// (Before 1.1, WindowAdapter didn't have set_size, so the one from Deref was used. + /// But in Slint 1.1, if one had imported the WindowAdapter trait, the other one would be found) + pub fn set_size(&self, size: impl Into) { + self.window.set_size(size); + } +} + +impl WindowAdapter for MinimalSoftwareWindow { + fn window(&self) -> &Window { + &self.window + } + + fn renderer(&self) -> &dyn Renderer { + &self.renderer + } + + fn size(&self) -> crate::api::PhysicalSize { + self.size.get() + } + fn set_size(&self, size: crate::api::WindowSize) { + self.size.set(size.to_physical(1.)); + self.window + .dispatch_event(crate::platform::WindowEvent::Resized { size: size.to_logical(1.) }) + } + + fn request_redraw(&self) { + self.needs_redraw.set(true); + } +} + +impl core::ops::Deref for MinimalSoftwareWindow { + type Target = Window; + fn deref(&self) -> &Self::Target { + &self.window + } +} diff --git a/internal/core/software_renderer/scene.rs b/internal/core/software_renderer/scene.rs new file mode 100644 index 00000000000..fae9b70f95c --- /dev/null +++ b/internal/core/software_renderer/scene.rs @@ -0,0 +1,427 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +//! This is the module contain data structures for a scene of items that can be rendered + +use super::{ + Fixed, PhysicalBorderRadius, PhysicalLength, PhysicalPoint, PhysicalRect, PhysicalRegion, + PhysicalSize, PremultipliedRgbaColor, RenderingRotation, +}; +use crate::graphics::{PixelFormat, SharedImageBuffer}; +use crate::lengths::{PointLengths as _, SizeLengths as _}; +use crate::Color; +use alloc::rc::Rc; +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; +use euclid::Length; + +#[derive(Default)] +pub struct SceneVectors { + pub textures: Vec>, + pub rounded_rectangles: Vec, + pub shared_buffers: Vec, + pub gradients: Vec, +} + +pub struct Scene { + /// the next line to be processed + pub(super) current_line: PhysicalLength, + + /// The items are sorted like so: + /// - `items[future_items_index..]` are the items that have `y > current_line`. + /// They must be sorted by `y` (top to bottom), then by `z` (front to back) + /// - `items[..current_items_index]` are the items that overlap with the current_line, + /// sorted by z (front to back) + pub(super) items: Vec, + + pub(super) vectors: SceneVectors, + + pub(super) future_items_index: usize, + pub(super) current_items_index: usize, + + pub(super) dirty_region: PhysicalRegion, + + pub(super) current_line_ranges: Vec>, + pub(super) range_valid_until_line: PhysicalLength, +} + +impl Scene { + pub fn new( + mut items: Vec, + vectors: SceneVectors, + dirty_region: PhysicalRegion, + ) -> Self { + let current_line = + dirty_region.iter_box().map(|x| x.min.y_length()).min().unwrap_or_default(); + items.retain(|i| i.pos.y_length() + i.size.height_length() > current_line); + items.sort_unstable_by(compare_scene_item); + let current_items_index = items.partition_point(|i| i.pos.y_length() <= current_line); + items[..current_items_index].sort_unstable_by(|a, b| b.z.cmp(&a.z)); + let mut r = Self { + items, + current_line, + current_items_index, + future_items_index: current_items_index, + vectors, + dirty_region, + current_line_ranges: Default::default(), + range_valid_until_line: Default::default(), + }; + r.recompute_ranges(); + debug_assert_eq!(r.current_line, r.dirty_region.bounding_rect().origin.y_length()); + r + } + + /// Updates `current_items_index` and `future_items_index` to match the invariant + pub fn next_line(&mut self) { + self.current_line += PhysicalLength::new(1); + + let skipped = self.current_line >= self.range_valid_until_line && self.recompute_ranges(); + + // The items array is split in part: + // 1. [0..i] are the items that have already been processed, that are on this line + // 2. [j..current_items_index] are the items from the previous line that might still be + // valid on this line + // 3. [tmp1, tmp2] is a buffer where we swap items so we can make room for the items in [0..i] + // 4. [future_items_index..] are the items which might get processed now + // 5. [current_items_index..tmp1], [tmp2..future_items_index] and [i..j] is garbage + // + // At each step, we selecting the item with the higher z from the list 2 or 3 or 4 and take it from + // that list. Then we add it to the list [0..i] if it needs more processing. If needed, + // we move the first item from list 2. to list 3. to make some room + + let (mut i, mut j, mut tmp1, mut tmp2) = + (0, 0, self.current_items_index, self.current_items_index); + + if skipped { + // Merge sort doesn't work in that case. + while j < self.current_items_index { + let item = self.items[j]; + if item.pos.y_length() + item.size.height_length() > self.current_line { + self.items[i] = item; + i += 1; + } + j += 1; + } + while self.future_items_index < self.items.len() { + let item = self.items[self.future_items_index]; + if item.pos.y_length() > self.current_line { + break; + } + self.future_items_index += 1; + if item.pos.y_length() + item.size.height_length() < self.current_line { + continue; + } + self.items[i] = item; + i += 1; + } + self.items[0..i].sort_unstable_by(|a, b| b.z.cmp(&a.z)); + self.current_items_index = i; + return; + } + + 'outer: loop { + let future_next_z = self + .items + .get(self.future_items_index) + .filter(|i| i.pos.y_length() <= self.current_line) + .map(|i| i.z); + let item = loop { + if tmp1 != tmp2 { + if future_next_z.map_or(true, |z| self.items[tmp1].z > z) { + let idx = tmp1; + tmp1 += 1; + if tmp1 == tmp2 { + tmp1 = self.current_items_index; + tmp2 = self.current_items_index; + } + break self.items[idx]; + } + } else if j < self.current_items_index { + let item = &self.items[j]; + if item.pos.y_length() + item.size.height_length() <= self.current_line { + j += 1; + continue; + } + if future_next_z.map_or(true, |z| item.z > z) { + j += 1; + break *item; + } + } + if future_next_z.is_some() { + self.future_items_index += 1; + break self.items[self.future_items_index - 1]; + } + break 'outer; + }; + if i != j { + // there is room + } else if j >= self.current_items_index && tmp1 == tmp2 { + // the current_items list is empty + j += 1 + } else if self.items[j].pos.y_length() + self.items[j].size.height_length() + <= self.current_line + { + // next item in the current_items array is no longer in this line + j += 1; + } else if tmp2 < self.future_items_index && j < self.current_items_index { + // move the next item in current_items + let to_move = self.items[j]; + self.items[tmp2] = to_move; + j += 1; + tmp2 += 1; + } else { + debug_assert!(tmp1 >= self.current_items_index); + let sort_begin = i; + // merge sort doesn't work because we don't have enough tmp space, just bring all items and use a normal sort. + while j < self.current_items_index { + let item = self.items[j]; + if item.pos.y_length() + item.size.height_length() > self.current_line { + self.items[i] = item; + i += 1; + } + j += 1; + } + self.items.copy_within(tmp1..tmp2, i); + i += tmp2 - tmp1; + debug_assert!(i < self.future_items_index); + self.items[i] = item; + i += 1; + while self.future_items_index < self.items.len() { + let item = self.items[self.future_items_index]; + if item.pos.y_length() > self.current_line { + break; + } + self.future_items_index += 1; + self.items[i] = item; + i += 1; + } + self.items[sort_begin..i].sort_unstable_by(|a, b| b.z.cmp(&a.z)); + break; + } + self.items[i] = item; + i += 1; + } + self.current_items_index = i; + // check that current items are properly sorted + debug_assert!(self.items[0..self.current_items_index].windows(2).all(|x| x[0].z >= x[1].z)); + } + + // return true if lines were skipped + fn recompute_ranges(&mut self) -> bool { + let validity = super::region_line_ranges( + &self.dirty_region, + self.current_line.get(), + &mut self.current_line_ranges, + ); + if self.current_line_ranges.is_empty() { + if let Some(next) = validity { + self.current_line = Length::new(next); + self.range_valid_until_line = Length::new( + super::region_line_ranges( + &self.dirty_region, + self.current_line.get(), + &mut self.current_line_ranges, + ) + .unwrap_or_default(), + ); + return true; + } + } + self.range_valid_until_line = Length::new(validity.unwrap_or_default()); + false + } +} + +#[derive(Clone, Copy, Debug)] +pub struct SceneItem { + pub pos: PhysicalPoint, + pub size: PhysicalSize, + // this is the order of the item from which it is in the item tree + pub z: u16, + pub command: SceneCommand, +} + +fn compare_scene_item(a: &SceneItem, b: &SceneItem) -> core::cmp::Ordering { + // First, order by line (top to bottom) + match a.pos.y.partial_cmp(&b.pos.y) { + None | Some(core::cmp::Ordering::Equal) => {} + Some(ord) => return ord, + } + // Then by the reverse z (front to back) + match a.z.partial_cmp(&b.z) { + None | Some(core::cmp::Ordering::Equal) => {} + Some(ord) => return ord.reverse(), + } + + // anything else, we don't care + core::cmp::Ordering::Equal +} + +#[derive(Clone, Copy, Debug)] +#[repr(u8)] +pub enum SceneCommand { + Rectangle { + color: PremultipliedRgbaColor, + }, + /// texture_index is an index in the [`SceneVectors::textures`] array + Texture { + texture_index: u16, + }, + /// shared_buffer_index is an index in [`SceneVectors::shared_buffers`] + SharedBuffer { + shared_buffer_index: u16, + }, + /// rectangle_index is an index in the [`SceneVectors::rounded_rectangle`] array + RoundedRectangle { + rectangle_index: u16, + }, + /// rectangle_index is an index in the [`SceneVectors::rounded_gradients`] array + Gradient { + gradient_index: u16, + }, +} + +pub struct SceneTexture<'a> { + /// This should have a size so that the entire slice is ((height - 1) * pixel_stride + width) * bpp + pub data: &'a [u8], + pub format: PixelFormat, + /// number of pixels between two lines in the source + pub pixel_stride: u16, + + pub extra: SceneTextureExtra, +} + +impl<'a> SceneTexture<'a> { + pub fn source_size(&self) -> PhysicalSize { + let len = self.data.len() / self.format.bpp(); + let stride = self.pixel_stride as usize; + let h = len / stride; + let w = len % stride; + if w == 0 { + PhysicalSize::new(stride as _, h as _) + } else { + PhysicalSize::new(w as _, (h + 1) as _) + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct SceneTextureExtra { + /// Delta x: the amount of "image pixel" that we need to skip for each physical pixel in the target buffer + pub dx: Fixed, + pub dy: Fixed, + /// Offset which is the coordinate of the "image pixel" which going to be drawn at location SceneItem::pos + pub off_x: Fixed, + pub off_y: Fixed, + /// Color to colorize. When not transparent, consider that the image is an alpha map and always use that color. + /// The alpha of this color is ignored. (it is supposed to be mixed in `Self::alpha`) + pub colorize: Color, + pub alpha: u8, + pub rotation: RenderingRotation, +} + +pub enum SharedBufferData { + SharedImage(SharedImageBuffer), + AlphaMap { data: Rc<[u8]>, width: u16 }, +} + +impl SharedBufferData { + fn width(&self) -> usize { + match self { + SharedBufferData::SharedImage(image) => image.width() as usize, + SharedBufferData::AlphaMap { width, .. } => *width as usize, + } + } +} + +pub struct SharedBufferCommand { + pub buffer: SharedBufferData, + /// The source rectangle that is mapped into this command span + pub source_rect: PhysicalRect, + pub extra: SceneTextureExtra, +} + +impl SharedBufferCommand { + pub fn as_texture(&self) -> SceneTexture<'_> { + let stride = self.buffer.width(); + let core::ops::Range { start, end } = compute_range_in_buffer(&self.source_rect, stride); + + match &self.buffer { + SharedBufferData::SharedImage(SharedImageBuffer::RGB8(b)) => SceneTexture { + data: &b.as_bytes()[start * 3..end * 3], + pixel_stride: stride as u16, + format: PixelFormat::Rgb, + extra: self.extra, + }, + SharedBufferData::SharedImage(SharedImageBuffer::RGBA8(b)) => SceneTexture { + data: &b.as_bytes()[start * 4..end * 4], + pixel_stride: stride as u16, + format: PixelFormat::Rgba, + extra: self.extra, + }, + SharedBufferData::SharedImage(SharedImageBuffer::RGBA8Premultiplied(b)) => { + SceneTexture { + data: &b.as_bytes()[start * 4..end * 4], + pixel_stride: stride as u16, + format: PixelFormat::RgbaPremultiplied, + extra: self.extra, + } + } + SharedBufferData::AlphaMap { data, width } => SceneTexture { + data: &data[start..end], + pixel_stride: *width, + format: PixelFormat::AlphaMap, + extra: self.extra, + }, + } + } +} + +/// Given a rectangle of coordinate in a buffer and a stride, compute the range, in pixel +pub fn compute_range_in_buffer( + source_rect: &PhysicalRect, + pixel_stride: usize, +) -> core::ops::Range { + let start = pixel_stride * source_rect.min_y() as usize + source_rect.min_x() as usize; + let end = pixel_stride * (source_rect.max_y() - 1) as usize + source_rect.max_x() as usize; + start..end +} + +#[derive(Debug)] +pub struct RoundedRectangle { + pub radius: PhysicalBorderRadius, + /// the border's width + pub width: PhysicalLength, + pub border_color: PremultipliedRgbaColor, + pub inner_color: PremultipliedRgbaColor, + /// The clips is the amount of pixels of the rounded rectangle that is clipped away. + /// For example, if left_clip > width, then the left border will not be visible, and + /// if left_clip > radius, then no radius will be seen in the left side + pub left_clip: PhysicalLength, + pub right_clip: PhysicalLength, + pub top_clip: PhysicalLength, + pub bottom_clip: PhysicalLength, +} + +/// Goes from color 1 to color2 +/// +/// depending of `flags & 0b1` +/// - if false: on the left side, goes from `start` to 1, on the right side, goes from 0 to `1-start` +/// - if true: on the left side, goes from 0 to `1-start`, on the right side, goes from `start` to `1` +#[derive(Debug)] +pub struct GradientCommand { + pub color1: PremultipliedRgbaColor, + pub color2: PremultipliedRgbaColor, + pub start: u8, + /// bit 0: if the slope is positive or negative + /// bit 1: if we should fill with color1 on the left side when left_clip is negative (or transparent) + /// bit 2: if we should fill with color2 on the left side when right_clip is negative (or transparent) + pub flags: u8, + /// If positive, the clip has the same meaning as in RoundedRectangle. + /// If negative, that means the "stop" is only starting or stopping at that point + pub left_clip: PhysicalLength, + pub right_clip: PhysicalLength, + pub top_clip: PhysicalLength, + pub bottom_clip: PhysicalLength, +}