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 | |
6 | use super::{ |
7 | Fixed, PhysicalBorderRadius, PhysicalLength, PhysicalPoint, PhysicalRect, PhysicalRegion, |
8 | PhysicalSize, PremultipliedRgbaColor, RenderingRotation, |
9 | }; |
10 | use crate::graphics::{SharedImageBuffer, TexturePixelFormat}; |
11 | use crate::lengths::{PointLengths as _, SizeLengths as _}; |
12 | use crate::Color; |
13 | use alloc::rc::Rc; |
14 | use alloc::vec::Vec; |
15 | use euclid::Length; |
16 | |
17 | #[derive (Default)] |
18 | pub struct SceneVectors { |
19 | pub textures: Vec<SceneTexture<'static>>, |
20 | pub rounded_rectangles: Vec<RoundedRectangle>, |
21 | pub shared_buffers: Vec<SharedBufferCommand>, |
22 | pub gradients: Vec<GradientCommand>, |
23 | } |
24 | |
25 | pub struct Scene { |
26 | /// the next line to be processed |
27 | pub(super) current_line: PhysicalLength, |
28 | |
29 | /// The items are sorted like so: |
30 | /// - `items[future_items_index..]` are the items that have `y > current_line`. |
31 | /// They must be sorted by `y` (top to bottom), then by `z` (front to back) |
32 | /// - `items[..current_items_index]` are the items that overlap with the current_line, |
33 | /// sorted by z (front to back) |
34 | pub(super) items: Vec<SceneItem>, |
35 | |
36 | pub(super) vectors: SceneVectors, |
37 | |
38 | pub(super) future_items_index: usize, |
39 | pub(super) current_items_index: usize, |
40 | |
41 | pub(super) dirty_region: PhysicalRegion, |
42 | |
43 | pub(super) current_line_ranges: Vec<core::ops::Range<i16>>, |
44 | pub(super) range_valid_until_line: PhysicalLength, |
45 | } |
46 | |
47 | impl Scene { |
48 | pub fn new( |
49 | mut items: Vec<SceneItem>, |
50 | vectors: SceneVectors, |
51 | dirty_region: PhysicalRegion, |
52 | ) -> Self { |
53 | let current_line = |
54 | dirty_region.iter_box().map(|x| x.min.y_length()).min().unwrap_or_default(); |
55 | items.retain(|i| i.pos.y_length() + i.size.height_length() > current_line); |
56 | items.sort_unstable_by(compare_scene_item); |
57 | let current_items_index = items.partition_point(|i| i.pos.y_length() <= current_line); |
58 | items[..current_items_index].sort_unstable_by(|a, b| b.z.cmp(&a.z)); |
59 | let mut r = Self { |
60 | items, |
61 | current_line, |
62 | current_items_index, |
63 | future_items_index: current_items_index, |
64 | vectors, |
65 | dirty_region, |
66 | current_line_ranges: Default::default(), |
67 | range_valid_until_line: Default::default(), |
68 | }; |
69 | r.recompute_ranges(); |
70 | debug_assert_eq!(r.current_line, r.dirty_region.bounding_rect().origin.y_length()); |
71 | r |
72 | } |
73 | |
74 | /// Updates `current_items_index` and `future_items_index` to match the invariant |
75 | pub fn next_line(&mut self) { |
76 | self.current_line += PhysicalLength::new(1); |
77 | |
78 | let skipped = self.current_line >= self.range_valid_until_line && self.recompute_ranges(); |
79 | |
80 | // The items array is split in part: |
81 | // 1. [0..i] are the items that have already been processed, that are on this line |
82 | // 2. [j..current_items_index] are the items from the previous line that might still be |
83 | // valid on this line |
84 | // 3. [tmp1, tmp2] is a buffer where we swap items so we can make room for the items in [0..i] |
85 | // 4. [future_items_index..] are the items which might get processed now |
86 | // 5. [current_items_index..tmp1], [tmp2..future_items_index] and [i..j] is garbage |
87 | // |
88 | // At each step, we selecting the item with the higher z from the list 2 or 3 or 4 and take it from |
89 | // that list. Then we add it to the list [0..i] if it needs more processing. If needed, |
90 | // we move the first item from list 2. to list 3. to make some room |
91 | |
92 | let (mut i, mut j, mut tmp1, mut tmp2) = |
93 | (0, 0, self.current_items_index, self.current_items_index); |
94 | |
95 | if skipped { |
96 | // Merge sort doesn't work in that case. |
97 | while j < self.current_items_index { |
98 | let item = self.items[j]; |
99 | if item.pos.y_length() + item.size.height_length() > self.current_line { |
100 | self.items[i] = item; |
101 | i += 1; |
102 | } |
103 | j += 1; |
104 | } |
105 | while self.future_items_index < self.items.len() { |
106 | let item = self.items[self.future_items_index]; |
107 | if item.pos.y_length() > self.current_line { |
108 | break; |
109 | } |
110 | self.future_items_index += 1; |
111 | if item.pos.y_length() + item.size.height_length() < self.current_line { |
112 | continue; |
113 | } |
114 | self.items[i] = item; |
115 | i += 1; |
116 | } |
117 | self.items[0..i].sort_unstable_by(|a, b| b.z.cmp(&a.z)); |
118 | self.current_items_index = i; |
119 | return; |
120 | } |
121 | |
122 | 'outer: loop { |
123 | let future_next_z = self |
124 | .items |
125 | .get(self.future_items_index) |
126 | .filter(|i| i.pos.y_length() <= self.current_line) |
127 | .map(|i| i.z); |
128 | let item = loop { |
129 | if tmp1 != tmp2 { |
130 | if future_next_z.map_or(true, |z| self.items[tmp1].z > z) { |
131 | let idx = tmp1; |
132 | tmp1 += 1; |
133 | if tmp1 == tmp2 { |
134 | tmp1 = self.current_items_index; |
135 | tmp2 = self.current_items_index; |
136 | } |
137 | break self.items[idx]; |
138 | } |
139 | } else if j < self.current_items_index { |
140 | let item = &self.items[j]; |
141 | if item.pos.y_length() + item.size.height_length() <= self.current_line { |
142 | j += 1; |
143 | continue; |
144 | } |
145 | if future_next_z.map_or(true, |z| item.z > z) { |
146 | j += 1; |
147 | break *item; |
148 | } |
149 | } |
150 | if future_next_z.is_some() { |
151 | self.future_items_index += 1; |
152 | break self.items[self.future_items_index - 1]; |
153 | } |
154 | break 'outer; |
155 | }; |
156 | if i != j { |
157 | // there is room |
158 | } else if j >= self.current_items_index && tmp1 == tmp2 { |
159 | // the current_items list is empty |
160 | j += 1 |
161 | } else if self.items[j].pos.y_length() + self.items[j].size.height_length() |
162 | <= self.current_line |
163 | { |
164 | // next item in the current_items array is no longer in this line |
165 | j += 1; |
166 | } else if tmp2 < self.future_items_index && j < self.current_items_index { |
167 | // move the next item in current_items |
168 | let to_move = self.items[j]; |
169 | self.items[tmp2] = to_move; |
170 | j += 1; |
171 | tmp2 += 1; |
172 | } else { |
173 | debug_assert!(tmp1 >= self.current_items_index); |
174 | let sort_begin = i; |
175 | // merge sort doesn't work because we don't have enough tmp space, just bring all items and use a normal sort. |
176 | while j < self.current_items_index { |
177 | let item = self.items[j]; |
178 | if item.pos.y_length() + item.size.height_length() > self.current_line { |
179 | self.items[i] = item; |
180 | i += 1; |
181 | } |
182 | j += 1; |
183 | } |
184 | self.items.copy_within(tmp1..tmp2, i); |
185 | i += tmp2 - tmp1; |
186 | debug_assert!(i < self.future_items_index); |
187 | self.items[i] = item; |
188 | i += 1; |
189 | while self.future_items_index < self.items.len() { |
190 | let item = self.items[self.future_items_index]; |
191 | if item.pos.y_length() > self.current_line { |
192 | break; |
193 | } |
194 | self.future_items_index += 1; |
195 | self.items[i] = item; |
196 | i += 1; |
197 | } |
198 | self.items[sort_begin..i].sort_unstable_by(|a, b| b.z.cmp(&a.z)); |
199 | break; |
200 | } |
201 | self.items[i] = item; |
202 | i += 1; |
203 | } |
204 | self.current_items_index = i; |
205 | // check that current items are properly sorted |
206 | debug_assert!(self.items[0..self.current_items_index].windows(2).all(|x| x[0].z >= x[1].z)); |
207 | } |
208 | |
209 | // return true if lines were skipped |
210 | fn recompute_ranges(&mut self) -> bool { |
211 | let validity = super::region_line_ranges( |
212 | &self.dirty_region, |
213 | self.current_line.get(), |
214 | &mut self.current_line_ranges, |
215 | ); |
216 | if self.current_line_ranges.is_empty() { |
217 | if let Some(next) = validity { |
218 | self.current_line = Length::new(next); |
219 | self.range_valid_until_line = Length::new( |
220 | super::region_line_ranges( |
221 | &self.dirty_region, |
222 | self.current_line.get(), |
223 | &mut self.current_line_ranges, |
224 | ) |
225 | .unwrap_or_default(), |
226 | ); |
227 | return true; |
228 | } |
229 | } |
230 | self.range_valid_until_line = Length::new(validity.unwrap_or_default()); |
231 | false |
232 | } |
233 | } |
234 | |
235 | #[derive (Clone, Copy, Debug)] |
236 | pub struct SceneItem { |
237 | pub pos: PhysicalPoint, |
238 | pub size: PhysicalSize, |
239 | // this is the order of the item from which it is in the item tree |
240 | pub z: u16, |
241 | pub command: SceneCommand, |
242 | } |
243 | |
244 | fn compare_scene_item(a: &SceneItem, b: &SceneItem) -> core::cmp::Ordering { |
245 | // First, order by line (top to bottom) |
246 | match a.pos.y.partial_cmp(&b.pos.y) { |
247 | None | Some(core::cmp::Ordering::Equal) => {} |
248 | Some(ord: Ordering) => return ord, |
249 | } |
250 | // Then by the reverse z (front to back) |
251 | match a.z.partial_cmp(&b.z) { |
252 | None | Some(core::cmp::Ordering::Equal) => {} |
253 | Some(ord: Ordering) => return ord.reverse(), |
254 | } |
255 | |
256 | // anything else, we don't care |
257 | core::cmp::Ordering::Equal |
258 | } |
259 | |
260 | #[derive (Clone, Copy, Debug)] |
261 | #[repr (u8)] |
262 | pub enum SceneCommand { |
263 | Rectangle { |
264 | color: PremultipliedRgbaColor, |
265 | }, |
266 | /// texture_index is an index in the [`SceneVectors::textures`] array |
267 | Texture { |
268 | texture_index: u16, |
269 | }, |
270 | /// shared_buffer_index is an index in [`SceneVectors::shared_buffers`] |
271 | SharedBuffer { |
272 | shared_buffer_index: u16, |
273 | }, |
274 | /// rectangle_index is an index in the [`SceneVectors::rounded_rectangle`] array |
275 | RoundedRectangle { |
276 | rectangle_index: u16, |
277 | }, |
278 | /// rectangle_index is an index in the [`SceneVectors::rounded_gradients`] array |
279 | Gradient { |
280 | gradient_index: u16, |
281 | }, |
282 | } |
283 | |
284 | pub struct SceneTexture<'a> { |
285 | /// This should have a size so that the entire slice is ((height - 1) * pixel_stride + width) * bpp |
286 | pub data: &'a [u8], |
287 | pub format: TexturePixelFormat, |
288 | /// number of pixels between two lines in the source |
289 | pub pixel_stride: u16, |
290 | |
291 | pub extra: SceneTextureExtra, |
292 | } |
293 | |
294 | impl SceneTexture<'_> { |
295 | pub fn source_size(&self) -> PhysicalSize { |
296 | let mut len: usize = self.data.len(); |
297 | if self.format == TexturePixelFormat::SignedDistanceField { |
298 | len -= 1; |
299 | } else { |
300 | len /= self.format.bpp(); |
301 | } |
302 | let stride: usize = self.pixel_stride as usize; |
303 | let h: usize = len / stride; |
304 | let w: usize = len % stride; |
305 | if w == 0 { |
306 | PhysicalSize::new(width:stride as _, height:h as _) |
307 | } else { |
308 | PhysicalSize::new(width:w as _, (h + 1) as _) |
309 | } |
310 | } |
311 | } |
312 | |
313 | #[derive (Clone, Copy, Debug)] |
314 | pub struct SceneTextureExtra { |
315 | /// Delta x: the amount of "image pixel" that we need to skip for each physical pixel in the target buffer |
316 | pub dx: Fixed<u16, 8>, |
317 | pub dy: Fixed<u16, 8>, |
318 | /// Offset which is the coordinate of the "image pixel" which going to be drawn at location SceneItem::pos |
319 | pub off_x: Fixed<u16, 4>, |
320 | pub off_y: Fixed<u16, 4>, |
321 | /// Color to colorize. When not transparent, consider that the image is an alpha map and always use that color. |
322 | /// The alpha of this color is ignored. (it is supposed to be mixed in `Self::alpha`) |
323 | pub colorize: Color, |
324 | pub alpha: u8, |
325 | pub rotation: RenderingRotation, |
326 | } |
327 | |
328 | pub enum SharedBufferData { |
329 | SharedImage(SharedImageBuffer), |
330 | AlphaMap { data: Rc<[u8]>, width: u16 }, |
331 | } |
332 | |
333 | impl SharedBufferData { |
334 | fn width(&self) -> usize { |
335 | match self { |
336 | SharedBufferData::SharedImage(image: &SharedImageBuffer) => image.width() as usize, |
337 | SharedBufferData::AlphaMap { width: &u16, .. } => *width as usize, |
338 | } |
339 | } |
340 | } |
341 | |
342 | pub struct SharedBufferCommand { |
343 | pub buffer: SharedBufferData, |
344 | /// The source rectangle that is mapped into this command span |
345 | pub source_rect: PhysicalRect, |
346 | pub extra: SceneTextureExtra, |
347 | } |
348 | |
349 | impl SharedBufferCommand { |
350 | pub fn as_texture(&self) -> SceneTexture<'_> { |
351 | let stride = self.buffer.width(); |
352 | let core::ops::Range { start, end } = compute_range_in_buffer(&self.source_rect, stride); |
353 | |
354 | match &self.buffer { |
355 | SharedBufferData::SharedImage(SharedImageBuffer::RGB8(b)) => SceneTexture { |
356 | data: &b.as_bytes()[start * 3..end * 3], |
357 | pixel_stride: stride as u16, |
358 | format: TexturePixelFormat::Rgb, |
359 | extra: self.extra, |
360 | }, |
361 | SharedBufferData::SharedImage(SharedImageBuffer::RGBA8(b)) => SceneTexture { |
362 | data: &b.as_bytes()[start * 4..end * 4], |
363 | pixel_stride: stride as u16, |
364 | format: TexturePixelFormat::Rgba, |
365 | extra: self.extra, |
366 | }, |
367 | SharedBufferData::SharedImage(SharedImageBuffer::RGBA8Premultiplied(b)) => { |
368 | SceneTexture { |
369 | data: &b.as_bytes()[start * 4..end * 4], |
370 | pixel_stride: stride as u16, |
371 | format: TexturePixelFormat::RgbaPremultiplied, |
372 | extra: self.extra, |
373 | } |
374 | } |
375 | SharedBufferData::AlphaMap { data, width } => SceneTexture { |
376 | data: &data[start..end], |
377 | pixel_stride: *width, |
378 | format: TexturePixelFormat::AlphaMap, |
379 | extra: self.extra, |
380 | }, |
381 | } |
382 | } |
383 | } |
384 | |
385 | /// Given a rectangle of coordinate in a buffer and a stride, compute the range, in pixel |
386 | pub fn compute_range_in_buffer( |
387 | source_rect: &PhysicalRect, |
388 | pixel_stride: usize, |
389 | ) -> core::ops::Range<usize> { |
390 | let start: usize = pixel_stride * source_rect.min_y() as usize + source_rect.min_x() as usize; |
391 | let end: usize = pixel_stride * (source_rect.max_y() - 1) as usize + source_rect.max_x() as usize; |
392 | start..end |
393 | } |
394 | |
395 | #[derive (Debug)] |
396 | pub struct RoundedRectangle { |
397 | pub radius: PhysicalBorderRadius, |
398 | /// the border's width |
399 | pub width: PhysicalLength, |
400 | pub border_color: PremultipliedRgbaColor, |
401 | pub inner_color: PremultipliedRgbaColor, |
402 | /// The clips is the amount of pixels of the rounded rectangle that is clipped away. |
403 | /// For example, if left_clip > width, then the left border will not be visible, and |
404 | /// if left_clip > radius, then no radius will be seen in the left side |
405 | pub left_clip: PhysicalLength, |
406 | pub right_clip: PhysicalLength, |
407 | pub top_clip: PhysicalLength, |
408 | pub bottom_clip: PhysicalLength, |
409 | } |
410 | |
411 | /// Goes from color 1 to color2 |
412 | /// |
413 | /// depending of `flags & 0b1` |
414 | /// - if false: on the left side, goes from `start` to 1, on the right side, goes from 0 to `1-start` |
415 | /// - if true: on the left side, goes from 0 to `1-start`, on the right side, goes from `start` to `1` |
416 | #[derive (Debug)] |
417 | pub struct GradientCommand { |
418 | pub color1: PremultipliedRgbaColor, |
419 | pub color2: PremultipliedRgbaColor, |
420 | pub start: u8, |
421 | /// bit 0: if the slope is positive or negative |
422 | /// bit 1: if we should fill with color1 on the left side when left_clip is negative (or transparent) |
423 | /// bit 2: if we should fill with color2 on the left side when right_clip is negative (or transparent) |
424 | pub flags: u8, |
425 | /// If positive, the clip has the same meaning as in RoundedRectangle. |
426 | /// If negative, that means the "stop" is only starting or stopping at that point |
427 | pub left_clip: PhysicalLength, |
428 | pub right_clip: PhysicalLength, |
429 | pub top_clip: PhysicalLength, |
430 | pub bottom_clip: PhysicalLength, |
431 | } |
432 | |