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 ®ions {
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 }