git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/draw/images.rs (10.4K)

  1 //! The image-id queue. [`upload_rgba`] and its kin queue pixels from any code
  2 //! and hand back an id that is usable in [`ImageQuad`]s at once; whichever
  3 //! renderer the process has drains the queue at its next frame
  4 //! ([`take_pending`]) and frees what [`free_image`] queued. See `vk::image`
  5 //! for the Vulkan side and the reasoning behind the streaming path.
  6 
  7 use std::sync::atomic::{AtomicU32, Ordering};
  8 use std::sync::Mutex;
  9 
 10 /// One image draw in a 2D frame.
 11 pub struct ImageQuad {
 12     /// Id from [`upload_rgba`].
 13     pub image: u32,
 14     /// Destination rect (x, y, w, h) in physical pixels.
 15     pub rect: (f32, f32, f32, f32),
 16     pub alpha: f32,
 17     /// Draw order: this quad renders before the vertex at this index of
 18     /// `Frame2D::verts` (so vertices below it stay below, later ones cover it).
 19     /// Use `u32::MAX` to draw on top of all display-list geometry.
 20     pub z_before: u32,
 21     /// Optional scissor (x, y, w, h) in physical pixels.
 22     pub clip: Option<(u32, u32, u32, u32)>,
 23 }
 24 
 25 /// Byte order of the pixels handed over.
 26 ///
 27 /// Both are sRGB-encoded 8-bit-per-channel; the difference is only which
 28 /// channel comes first in memory, and the hardware handles it on sample. A
 29 /// caller whose source is already BGRA (Wayland's and WebKit's usual order)
 30 /// should say so rather than swizzle on the CPU: at a fullscreen 3840x2400
 31 /// that swizzle measured 7.4 ms per frame, which is most of a frame budget
 32 /// spent rearranging bytes the sampler can read either way.
 33 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
 34 pub enum PixelFormat {
 35     Rgba,
 36     Bgra,
 37 }
 38 
 39 /// One queued change to the image table, drained by the renderer.
 40 pub enum Pending {
 41     Upload { id: u32, pixels: Vec<u8>, width: u32, height: u32, format: PixelFormat, mips: bool },
 42     /// Replace the contents of an image that already exists, keeping its
 43     /// id, its `VkImage` and its descriptor set.
 44     Update { id: u32, pixels: Vec<u8>, width: u32, height: u32, format: PixelFormat },
 45     /// Replace only `regions` of an image that already exists; see
 46     /// [`update_pixel_regions`]. `width`, `height` and `format` are the whole
 47     /// image's, and an image that no longer matches them is left alone.
 48     UpdateRegions {
 49         id: u32,
 50         pixels: Vec<u8>,
 51         width: u32,
 52         height: u32,
 53         format: PixelFormat,
 54         regions: Vec<Region>,
 55     },
 56     Free { id: u32 },
 57 }
 58 
 59 /// A rectangle of an image in texels: `(x, y, width, height)`.
 60 pub type Region = (u32, u32, u32, u32);
 61 
 62 static PENDING: Mutex<Vec<Pending>> = Mutex::new(Vec::new());
 63 /// Ids whose pixels were replaced in place ([`update_pixels`],
 64 /// [`update_pixel_regions`]) since the last frame was built: a frame that
 65 /// draws one of them has changed there even though its display list did not.
 66 static UPDATED: Mutex<Vec<u32>> = Mutex::new(Vec::new());
 67 
 68 /// The ids [`update_pixels`] and [`update_pixel_regions`] touched since the
 69 /// last call — the frame builder's, for its damage.
 70 pub(crate) fn take_updated_ids() -> Vec<u32> {
 71     std::mem::take(&mut *UPDATED.lock().unwrap())
 72 }
 73 static NEXT_ID: AtomicU32 = AtomicU32::new(1);
 74 
 75 /// A fresh image id from the process-wide counter, for a renderer that
 76 /// uploads into its own table directly (`ImageStage::upload_now`) — never
 77 /// one a queued upload also holds.
 78 #[cfg_attr(target_arch = "wasm32", allow(dead_code))] // only the Vulkan renderer uploads directly
 79 pub(crate) fn next_image_id() -> u32 {
 80     NEXT_ID.fetch_add(1, Ordering::Relaxed)
 81 }
 82 /// How many image tables have been built in this process. See
 83 /// [`renderer_epoch`].
 84 static STAGES_BUILT: AtomicU32 = AtomicU32::new(0);
 85 /// Pixel buffers the renderer has finished with, waiting to be refilled.
 86 /// Bounded: a streaming caller needs one or two in flight, and holding more
 87 /// frame-sized buffers than that is just memory.
 88 static RECYCLED: Mutex<Vec<Vec<u8>>> = Mutex::new(Vec::new());
 89 const MAX_RECYCLED: usize = 3;
 90 
 91 /// Queue an RGBA8 image for upload; the id is usable in [`ImageQuad`]s right
 92 /// away (draws before the upload lands are skipped, not errors).
 93 pub fn upload_rgba(pixels: Vec<u8>, width: u32, height: u32) -> u32 {
 94     upload_pixels(pixels, width, height, PixelFormat::Rgba)
 95 }
 96 
 97 /// Queue an image whose bytes are in `format`. [`upload_rgba`] is this with
 98 /// [`PixelFormat::Rgba`].
 99 pub fn upload_pixels(pixels: Vec<u8>, width: u32, height: u32, format: PixelFormat) -> u32 {
100     queue_upload(pixels, width, height, format, false)
101 }
102 
103 /// [`upload_rgba`] for an image that will be drawn much smaller than it is,
104 /// or at a slant: the image gets a full mip chain, built on the GPU, and is
105 /// sampled from the level that matches the size it is drawn at. An update
106 /// ([`update_pixels`]) rebuilds the chain.
107 ///
108 /// On a device that cannot build one by blitting the image is uploaded
109 /// without, as [`upload_rgba`] would have.
110 pub fn upload_rgba_mipmapped(pixels: Vec<u8>, width: u32, height: u32) -> u32 {
111     queue_upload(pixels, width, height, PixelFormat::Rgba, true)
112 }
113 
114 fn queue_upload(pixels: Vec<u8>, width: u32, height: u32, format: PixelFormat, mips: bool) -> u32 {
115     assert_eq!(pixels.len(), (width * height * 4) as usize, "8888 size mismatch");
116     let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
117     PENDING.lock().unwrap().push(Pending::Upload { id, pixels, width, height, format, mips });
118     id
119 }
120 
121 /// Replace what `id` holds, keeping the image itself.
122 ///
123 /// For a caller that redraws the same picture over and over — a page, a video
124 /// frame, a live preview. Nothing is allocated, no descriptor is rewritten and
125 /// no image is destroyed, so none of the per-frame `device_wait_idle` that
126 /// freeing one costs. The size or format changing is allowed and simply falls
127 /// back to a fresh image under the same id, which is what a window resize
128 /// does.
129 ///
130 /// An `id` that does not exist yet is treated as an upload, so a caller can
131 /// take an id from [`upload_pixels`] and update it from the next frame on
132 /// without sequencing the two.
133 pub fn update_pixels(id: u32, pixels: Vec<u8>, width: u32, height: u32, format: PixelFormat) {
134     assert_eq!(pixels.len(), (width * height * 4) as usize, "8888 size mismatch");
135     PENDING.lock().unwrap().push(Pending::Update { id, pixels, width, height, format });
136     UPDATED.lock().unwrap().push(id);
137 }
138 
139 /// Replace only the given rectangles of `id`, keeping the rest of what it
140 /// holds.
141 ///
142 /// For a streaming caller that knows what changed between two frames — a
143 /// page whose engine reports damage. Copying and transferring a scrollbar's
144 /// strip instead of the whole picture is the point: at 3840x2400 a full
145 /// frame is 35 MB, and the strip a fraction of a percent of it.
146 ///
147 /// `pixels` holds each region's texels tightly packed (`width * 4` bytes a
148 /// row, no padding), one region after another in `regions` order. `width`,
149 /// `height` and `format` describe the **whole** image and must match the one
150 /// `id` names when the queue is drained. If they do not — the image was
151 /// never uploaded, was resized, or belongs to a renderer that has since been
152 /// replaced — nothing is written: the caller has lost track of what the
153 /// image holds and must send the whole picture with [`update_pixels`]. That
154 /// is the caller's contract, since only it knows what the rest of the image
155 /// should be.
156 pub fn update_pixel_regions(
157     id: u32,
158     pixels: Vec<u8>,
159     width: u32,
160     height: u32,
161     format: PixelFormat,
162     regions: Vec<Region>,
163 ) {
164     let mut bytes = 0usize;
165     for &(x, y, w, h) in &regions {
166         assert!(w > 0 && h > 0, "empty region");
167         assert!(x + w <= width && y + h <= height, "region outside the image");
168         bytes += (w * h * 4) as usize;
169     }
170     assert_eq!(pixels.len(), bytes, "8888 region size mismatch");
171     if regions.is_empty() {
172         retire_buffer(pixels);
173         return;
174     }
175     PENDING.lock().unwrap().push(Pending::UpdateRegions { id, pixels, width, height, format, regions });
176     UPDATED.lock().unwrap().push(id);
177 }
178 
179 /// A pixel buffer to fill, reusing one the renderer has finished with when
180 /// there is one of at least `len` bytes.
181 ///
182 /// The returned buffer is exactly `len` long and its contents are unspecified
183 /// — a caller is expected to overwrite every byte, which a full-frame readback
184 /// does by definition. Allocating a fresh frame-sized `Vec` instead measured
185 /// 7.4 ms against 2.9 ms at 3840x2400: the cost is not the copy, it is the
186 /// zeroing and the page faults on newly mapped memory.
187 pub fn recycle_buffer(len: usize) -> Vec<u8> {
188     let mut pool = RECYCLED.lock().unwrap();
189     if let Some(index) = pool.iter().position(|b| b.capacity() >= len) {
190         let mut buf = pool.swap_remove(index);
191         buf.clear();
192         buf.resize(len, 0);
193         return buf;
194     }
195     vec![0u8; len]
196 }
197 
198 /// Hand a finished buffer back to the pool (the renderer, once it has
199 /// uploaded what a [`Pending`] carried).
200 pub fn retire_buffer(mut buf: Vec<u8>) {
201     let mut pool = RECYCLED.lock().unwrap();
202     if pool.len() < MAX_RECYCLED {
203         buf.clear();
204         pool.push(buf);
205     }
206 }
207 
208 /// Which renderer's image table the ids handed out right now belong to.
209 ///
210 /// `0` until the first renderer exists, and again for the whole life of that
211 /// first renderer: uploads queued before it was built (from `Application::new`
212 /// and from anything the app did on the way to its first frame) are drained
213 /// into it, so they are that epoch's images, not a previous one's.
214 /// Every later renderer — `window_runner` builds one per session, and a lost
215 /// Wayland transport starts a new session around the same `Application` —
216 /// counts as the next epoch.
217 ///
218 /// This is what lets a long-lived cache of image ids notice that its ids have
219 /// stopped naming anything. It is the cheap half of the contract; the other
220 /// half is the app's, because only the app knows how to produce the pixels
221 /// again (see [`Application::renderer_init`], and `upload_icon` for the
222 /// toolkit's own use of this).
223 ///
224 /// [`Application::renderer_init`]: crate::engine::Application::renderer_init
225 pub fn renderer_epoch() -> u32 {
226     STAGES_BUILT.load(Ordering::Relaxed).saturating_sub(1)
227 }
228 
229 /// Queue an image's GPU resources for destruction.
230 pub fn free_image(id: u32) {
231     PENDING.lock().unwrap().push(Pending::Free { id });
232 }
233 
234 /// Everything queued since the last call, in order. The renderer's to drain.
235 pub fn take_pending() -> Vec<Pending> {
236     std::mem::take(&mut *PENDING.lock().unwrap())
237 }
238 
239 /// A renderer built its image table: from here on [`renderer_epoch`] names it.
240 pub fn image_table_built() {
241     STAGES_BUILT.fetch_add(1, Ordering::Relaxed);
242 }