| 1 | //! The page's own pictures, decoded off the frame thread at the size they show and let go |
| 2 | //! under a budget. |
| 3 | |
| 4 | use super::{SETTLE, Slot, density, queue}; |
| 5 | use draw::RasterImage; |
| 6 | use onestore::ExGuid; |
| 7 | use std::{collections::BTreeMap, sync::Arc, task::Waker, time::Duration}; |
| 8 | use web_time::Instant; |
| 9 | |
| 10 | /// Decoded bytes a scene's pictures keep at once. Pictures in view shrink to fit it, and |
| 11 | /// the rest of the renderer's frame budget stays for template art and the interface. |
| 12 | pub(super) const BUDGET: u64 = draw::MAX_IMAGE_BYTES / 2; |
| 13 | |
| 14 | pub(super) struct Picture { |
| 15 | /// The stored picture, which saving keeps as it is. |
| 16 | bytes: Arc<[u8]>, |
| 17 | /// The stored picture's size in pixels. |
| 18 | native: [u32; 2], |
| 19 | shown: Option<RasterImage>, |
| 20 | /// The one decode under way and the size it makes; replacing it abandons it. |
| 21 | pending: Option<(Slot<Option<RasterImage>>, [u32; 2])>, |
| 22 | /// The stored bytes did not decode, so the picture paints as a placeholder. |
| 23 | failed: bool, |
| 24 | /// When the picture was last near the view; the longest unseen are let go first. |
| 25 | seen: Instant, |
| 26 | } |
| 27 | |
| 28 | impl Picture { |
| 29 | /// Whether both show the same stored bytes. |
| 30 | pub(crate) fn same(&self, other: &Self) -> bool { |
| 31 | self.bytes == other.bytes |
| 32 | } |
| 33 | |
| 34 | /// Reads the size from the picture's header; None when the renderer cannot decode it. |
| 35 | pub fn new(bytes: &Arc<[u8]>) -> Option<Self> { |
| 36 | Some(Self { |
| 37 | native: RasterImage::measure(bytes).ok()?, |
| 38 | bytes: Arc::clone(bytes), |
| 39 | shown: None, |
| 40 | pending: None, |
| 41 | failed: false, |
| 42 | seen: Instant::now(), |
| 43 | }) |
| 44 | } |
| 45 | |
| 46 | /// The latest raster, once one has landed. |
| 47 | pub(crate) fn image(&self) -> Option<&RasterImage> { |
| 48 | self.shown.as_ref() |
| 49 | } |
| 50 | |
| 51 | pub(crate) fn failed(&self) -> bool { |
| 52 | self.failed |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | fn bytes([width, height]: [u32; 2]) -> u64 { |
| 57 | u64::from(width) * u64::from(height) * 4 |
| 58 | } |
| 59 | |
| 60 | /// Takes finished decodes and asks for the pictures `rects` places near `view` at `scale` |
| 61 | /// device pixels per point; `waker` hears when one lands. Pictures in view come first and |
| 62 | /// shrink together to fit the budget, the half view around it decodes ahead with what |
| 63 | /// room is left, and those unseen longest are let go past it. True once every picture in |
| 64 | /// view shows the raster it asked for. |
| 65 | pub(super) fn update( |
| 66 | pictures: &mut BTreeMap<ExGuid, Picture>, |
| 67 | rects: impl IntoIterator<Item = (ExGuid, [f32; 4])>, |
| 68 | view: [f32; 4], |
| 69 | scale: f32, |
| 70 | waker: &Waker, |
| 71 | ) -> bool { |
| 72 | let now = Instant::now(); |
| 73 | for picture in pictures.values_mut() { |
| 74 | let landed = picture |
| 75 | .pending |
| 76 | .as_ref() |
| 77 | .and_then(|(slot, _)| slot.lock().unwrap().take()); |
| 78 | if let Some(image) = landed { |
| 79 | picture.failed = image.is_none(); |
| 80 | picture.shown = image; |
| 81 | picture.pending = None; |
| 82 | } |
| 83 | } |
| 84 | let density = density(scale); |
| 85 | let [x0, y0, x1, y1] = view; |
| 86 | let [ahead_x, ahead_y] = [(x1 - x0) / 2.0, (y1 - y0) / 2.0]; |
| 87 | let near = [x0 - ahead_x, y0 - ahead_y, x1 + ahead_x, y1 + ahead_y]; |
| 88 | let overlaps = |rect: [f32; 4], area: [f32; 4]| { |
| 89 | rect[0] <= area[2] && rect[2] >= area[0] && rect[1] <= area[3] && rect[3] >= area[1] |
| 90 | }; |
| 91 | let mut wanted: Vec<_> = rects |
| 92 | .into_iter() |
| 93 | .filter_map(|(id, rect)| { |
| 94 | let picture = pictures.get(&id)?; |
| 95 | if picture.failed || !overlaps(rect, near) { |
| 96 | return None; |
| 97 | } |
| 98 | let shown = [rect[2] - rect[0], rect[3] - rect[1]]; |
| 99 | let size = [0, 1] |
| 100 | .map(|axis| ((shown[axis] * density).ceil() as u32).clamp(1, picture.native[axis])); |
| 101 | Some((id, size, overlaps(rect, view))) |
| 102 | }) |
| 103 | .collect(); |
| 104 | let visible: u64 = wanted |
| 105 | .iter() |
| 106 | .filter(|(.., visible)| *visible) |
| 107 | .map(|(_, size, _)| bytes(*size)) |
| 108 | .sum(); |
| 109 | let shrink = (BUDGET as f64 / visible as f64).sqrt(); |
| 110 | let mut room = BUDGET.saturating_sub(visible); |
| 111 | wanted.retain_mut(|(_, size, visible)| { |
| 112 | if *visible && shrink < 1.0 { |
| 113 | *size = size.map(|side| ((f64::from(side) * shrink) as u32).max(1)); |
| 114 | } else if !*visible { |
| 115 | if bytes(*size) > room { |
| 116 | return false; |
| 117 | } |
| 118 | room -= bytes(*size); |
| 119 | } |
| 120 | true |
| 121 | }); |
| 122 | let wanted: BTreeMap<_, _> = wanted |
| 123 | .into_iter() |
| 124 | .map(|(id, size, visible)| (id, (size, visible))) |
| 125 | .collect(); |
| 126 | let mut settled = true; |
| 127 | for (id, picture) in pictures.iter_mut() { |
| 128 | let Some(&(size, visible)) = wanted.get(id) else { |
| 129 | picture.pending = None; |
| 130 | continue; |
| 131 | }; |
| 132 | picture.seen = now; |
| 133 | let shown = picture.shown.as_ref().map(RasterImage::size); |
| 134 | if shown != Some(size) && picture.pending.as_ref().map(|(_, made)| *made) != Some(size) { |
| 135 | let encoded = Arc::clone(&picture.bytes); |
| 136 | let start = now |
| 137 | + if shown.is_some() { |
| 138 | SETTLE |
| 139 | } else { |
| 140 | Duration::ZERO |
| 141 | }; |
| 142 | let slot = queue(start, waker, move || { |
| 143 | RasterImage::decode(&encoded, size).ok() |
| 144 | }); |
| 145 | picture.pending = Some((slot, size)); |
| 146 | } |
| 147 | settled &= !visible || shown == Some(size); |
| 148 | } |
| 149 | let mut kept: u64 = pictures |
| 150 | .values() |
| 151 | .filter_map(Picture::image) |
| 152 | .map(|image| bytes(image.size())) |
| 153 | .sum(); |
| 154 | if kept > BUDGET { |
| 155 | let mut spare: Vec<_> = pictures |
| 156 | .iter_mut() |
| 157 | .filter(|(id, picture)| { |
| 158 | picture.shown.as_ref().is_some_and(|image| { |
| 159 | wanted |
| 160 | .get(*id) |
| 161 | .is_none_or(|(size, _)| image.size() != *size) |
| 162 | }) |
| 163 | }) |
| 164 | .collect(); |
| 165 | spare.sort_by_key(|(id, picture)| (wanted.contains_key(*id), picture.seen)); |
| 166 | for (_, picture) in spare { |
| 167 | if kept <= BUDGET { |
| 168 | break; |
| 169 | } |
| 170 | kept -= picture.shown.take().map_or(0, |image| bytes(image.size())); |
| 171 | } |
| 172 | } |
| 173 | settled |
| 174 | } |