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

src/vk/image.rs (42.3K)

  1 //! User images in the 2D pass: upload RGBA pixels once, then draw them as
  2 //! quads interleaved with the display list — 3D previews in graph nodes,
  3 //! thumbnails in the files grid, any raster content in the UI.
  4 //!
  5 //! Upload is decoupled from the renderer because most apps never touch it
  6 //! (the engine runner owns the frame): [`upload_rgba`] queues pixels from any
  7 //! code and returns a stable id usable immediately in draws; the renderer
  8 //! drains the queue at the next frame. [`free_image`] queues destruction the
  9 //! same way.
 10 //!
 11 //! **Two shapes of caller.** Most upload an image once and draw it for the
 12 //! rest of the process: a decoded PNG, a rasterized SVG, a thumbnail. One
 13 //! uploads a *new* image every frame — cce-browser, whose whole page is a
 14 //! readback of what the engine just painted. The one-shot path allocated a
 15 //! fresh `VkImage` per upload and freed the old one behind a
 16 //! `device_wait_idle`, which for a streaming caller meant an allocation, a
 17 //! descriptor set and a full device stall per frame. [`update_pixels`] is the
 18 //! streaming path: same id, same image, same descriptor, contents replaced in
 19 //! place. [`recycle_buffer`] closes the loop on the CPU side by handing back
 20 //! the pixel buffer the renderer has finished with, so a streaming caller
 21 //! refills one buffer instead of allocating a frame-sized `Vec` per frame. Draw ordering comes from [`super::Frame2D::images`]: each
 22 //! [`ImageQuad`] carries the vertex index it sorts before.
 23 
 24 use std::collections::HashMap;
 25 use std::sync::atomic::{AtomicU32, Ordering};
 26 use std::sync::Mutex;
 27 
 28 use ash::vk;
 29 use gpu_allocator::vulkan::{Allocation, AllocationCreateDesc, AllocationScheme, Allocator};
 30 use gpu_allocator::MemoryLocation;
 31 
 32 use super::renderer::{create_cpu_buffer, destroy_cpu_buffer, AllocatedBuffer};
 33 
 34 /// One image draw in a 2D frame.
 35 pub struct ImageQuad {
 36     /// Id from [`upload_rgba`].
 37     pub image: u32,
 38     /// Destination rect (x, y, w, h) in physical pixels.
 39     pub rect: (f32, f32, f32, f32),
 40     pub alpha: f32,
 41     /// Draw order: this quad renders before the vertex at this index of
 42     /// `Frame2D::verts` (so vertices below it stay below, later ones cover it).
 43     /// Use `u32::MAX` to draw on top of all display-list geometry.
 44     pub z_before: u32,
 45     /// Optional scissor (x, y, w, h) in physical pixels.
 46     pub clip: Option<(u32, u32, u32, u32)>,
 47 }
 48 
 49 /// Byte order of the pixels handed over.
 50 ///
 51 /// Both are sRGB-encoded 8-bit-per-channel; the difference is only which
 52 /// channel comes first in memory, and the hardware handles it on sample. A
 53 /// caller whose source is already BGRA (Wayland's and WebKit's usual order)
 54 /// should say so rather than swizzle on the CPU: at a fullscreen 3840x2400
 55 /// that swizzle measured 7.4 ms per frame, which is most of a frame budget
 56 /// spent rearranging bytes the sampler can read either way.
 57 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
 58 pub enum PixelFormat {
 59     Rgba,
 60     Bgra,
 61 }
 62 
 63 impl PixelFormat {
 64     fn vk(self) -> vk::Format {
 65         // SRGB, not UNORM — see the note in `upload`.
 66         match self {
 67             Self::Rgba => vk::Format::R8G8B8A8_SRGB,
 68             Self::Bgra => vk::Format::B8G8R8A8_SRGB,
 69         }
 70     }
 71 }
 72 
 73 enum Pending {
 74     Upload { id: u32, pixels: Vec<u8>, width: u32, height: u32, format: PixelFormat },
 75     /// Replace the contents of an image that already exists, keeping its
 76     /// id, its `VkImage` and its descriptor set.
 77     Update { id: u32, pixels: Vec<u8>, width: u32, height: u32, format: PixelFormat },
 78     Free { id: u32 },
 79 }
 80 
 81 static PENDING: Mutex<Vec<Pending>> = Mutex::new(Vec::new());
 82 static NEXT_ID: AtomicU32 = AtomicU32::new(1);
 83 /// How many image tables have been built in this process. See
 84 /// [`renderer_epoch`].
 85 static STAGES_BUILT: AtomicU32 = AtomicU32::new(0);
 86 /// Pixel buffers the renderer has finished with, waiting to be refilled.
 87 /// Bounded: a streaming caller needs one or two in flight, and holding more
 88 /// frame-sized buffers than that is just memory.
 89 static RECYCLED: Mutex<Vec<Vec<u8>>> = Mutex::new(Vec::new());
 90 const MAX_RECYCLED: usize = 3;
 91 
 92 /// Queue an RGBA8 image for upload; the id is usable in [`ImageQuad`]s right
 93 /// away (draws before the upload lands are skipped, not errors).
 94 pub fn upload_rgba(pixels: Vec<u8>, width: u32, height: u32) -> u32 {
 95     upload_pixels(pixels, width, height, PixelFormat::Rgba)
 96 }
 97 
 98 /// Queue an image whose bytes are in `format`. [`upload_rgba`] is this with
 99 /// [`PixelFormat::Rgba`].
100 pub fn upload_pixels(pixels: Vec<u8>, width: u32, height: u32, format: PixelFormat) -> u32 {
101     assert_eq!(pixels.len(), (width * height * 4) as usize, "8888 size mismatch");
102     let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
103     PENDING.lock().unwrap().push(Pending::Upload { id, pixels, width, height, format });
104     id
105 }
106 
107 /// Replace what `id` holds, keeping the image itself.
108 ///
109 /// For a caller that redraws the same picture over and over — a page, a video
110 /// frame, a live preview. Nothing is allocated, no descriptor is rewritten and
111 /// no image is destroyed, so none of the per-frame `device_wait_idle` that
112 /// freeing one costs. The size or format changing is allowed and simply falls
113 /// back to a fresh image under the same id, which is what a window resize
114 /// does.
115 ///
116 /// An `id` that does not exist yet is treated as an upload, so a caller can
117 /// take an id from [`upload_pixels`] and update it from the next frame on
118 /// without sequencing the two.
119 pub fn update_pixels(id: u32, pixels: Vec<u8>, width: u32, height: u32, format: PixelFormat) {
120     assert_eq!(pixels.len(), (width * height * 4) as usize, "8888 size mismatch");
121     PENDING.lock().unwrap().push(Pending::Update { id, pixels, width, height, format });
122 }
123 
124 /// A pixel buffer to fill, reusing one the renderer has finished with when
125 /// there is one of at least `len` bytes.
126 ///
127 /// The returned buffer is exactly `len` long and its contents are unspecified
128 /// — a caller is expected to overwrite every byte, which a full-frame readback
129 /// does by definition. Allocating a fresh frame-sized `Vec` instead measured
130 /// 7.4 ms against 2.9 ms at 3840x2400: the cost is not the copy, it is the
131 /// zeroing and the page faults on newly mapped memory.
132 pub fn recycle_buffer(len: usize) -> Vec<u8> {
133     let mut pool = RECYCLED.lock().unwrap();
134     if let Some(index) = pool.iter().position(|b| b.capacity() >= len) {
135         let mut buf = pool.swap_remove(index);
136         buf.clear();
137         buf.resize(len, 0);
138         return buf;
139     }
140     vec![0u8; len]
141 }
142 
143 /// Hand a finished buffer back to the pool.
144 fn retire_buffer(mut buf: Vec<u8>) {
145     let mut pool = RECYCLED.lock().unwrap();
146     if pool.len() < MAX_RECYCLED {
147         buf.clear();
148         pool.push(buf);
149     }
150 }
151 
152 /// Which renderer's image table the ids handed out right now belong to.
153 ///
154 /// `0` until the first renderer exists, and again for the whole life of that
155 /// first renderer: uploads queued before it was built (from `Application::new`
156 /// and from anything the app did on the way to its first frame) are drained
157 /// into it, so they are that epoch's images, not a previous one's.
158 /// Every later renderer — `window_runner` builds one per session, and a lost
159 /// Wayland transport starts a new session around the same `Application` —
160 /// counts as the next epoch.
161 ///
162 /// This is what lets a long-lived cache of image ids notice that its ids have
163 /// stopped naming anything. It is the cheap half of the contract; the other
164 /// half is the app's, because only the app knows how to produce the pixels
165 /// again (see [`Application::renderer_init`], and `upload_icon` for the
166 /// toolkit's own use of this).
167 ///
168 /// [`Application::renderer_init`]: crate::engine::Application::renderer_init
169 pub fn renderer_epoch() -> u32 {
170     STAGES_BUILT.load(Ordering::Relaxed).saturating_sub(1)
171 }
172 
173 /// Queue an image's GPU resources for destruction.
174 pub fn free_image(id: u32) {
175     PENDING.lock().unwrap().push(Pending::Free { id });
176 }
177 
178 #[repr(C)]
179 #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
180 /// Must match `GlyphVertex` field-for-field: both pipelines are fed by the same
181 /// glyph shader, whose vertex entry point declares locations 0..=4. Omitting
182 /// `clip_extents` here left location 4 with no `VkVertexInputAttributeDescription`,
183 /// which the validation layer flags (VUID-VkGraphicsPipelineCreateInfo-Input-07904)
184 /// and which reads undefined data without `vertexAttributeRobustness`.
185 struct ImageVertex {
186     position: [f32; 2],
187     uv: [f32; 2],
188     color: [f32; 4],
189     clip_circle: [f32; 3],
190     clip_extents: [f32; 2],
191 }
192 
193 // The pipeline below hardcodes one offset per shader location. Pin the struct to
194 // them so adding, reordering, or resizing a field fails the build instead of
195 // silently feeding the shader mis-aligned attributes — the drift that left
196 // location 4 undescribed. `text.rs` pins `GlyphVertex` to the same layout.
197 const _: () = {
198     assert!(std::mem::size_of::<ImageVertex>() == 52);
199     assert!(std::mem::offset_of!(ImageVertex, position) == 0);
200     assert!(std::mem::offset_of!(ImageVertex, uv) == 8);
201     assert!(std::mem::offset_of!(ImageVertex, color) == 16);
202     assert!(std::mem::offset_of!(ImageVertex, clip_circle) == 32);
203     assert!(std::mem::offset_of!(ImageVertex, clip_extents) == 44);
204 };
205 
206 struct GpuImage {
207     image: vk::Image,
208     view: vk::ImageView,
209     allocation: Option<Allocation>,
210     descriptor_set: vk::DescriptorSet,
211     /// What the image was created as, so an update can tell "same picture,
212     /// new contents" from "different image under the same id".
213     width: u32,
214     height: u32,
215     format: PixelFormat,
216 }
217 
218 const MAX_IMAGES: u32 = 256;
219 /// Idle frames before the staging buffer is handed back — about two seconds
220 /// at 60 Hz. Long enough that a burst of uploads reuses one buffer, short
221 /// enough that a big one-shot upload does not hold its memory.
222 const STAGING_IDLE_FRAMES: u32 = 120;
223 /// Below this an idle staging buffer is simply kept; releasing and remaking a
224 /// small one costs more than it saves.
225 const STAGING_KEEP_BYTES: vk::DeviceSize = 1 << 20;
226 
227 pub(crate) struct ImageStage {
228     pipeline: vk::Pipeline,
229     pipeline_layout: vk::PipelineLayout,
230     descriptor_set_layout: vk::DescriptorSetLayout,
231     descriptor_pool: vk::DescriptorPool,
232     shader_module: vk::ShaderModule,
233     sampler: vk::Sampler,
234     images: HashMap<u32, GpuImage>,
235     /// One host-visible staging buffer, grown to the largest upload and kept.
236     /// Uploads are serialized against each other (each waits for its own copy
237     /// before returning), so one buffer serves them all — and a streaming
238     /// caller stops paying an allocation and a free per frame.
239     ///
240     /// Kept only while it is being used: a one-shot caller that uploads a
241     /// 96 MB photograph should not leave 96 MB of host memory mapped for the
242     /// life of the process, so an idle buffer is released (see
243     /// `STAGING_IDLE_FRAMES`). A streaming caller touches it every frame and
244     /// never reaches that.
245     staging: Option<AllocatedBuffer>,
246     /// Frames since the staging buffer was last used.
247     staging_idle: u32,
248     /// Per frame in flight: this frame's quad vertices (6 per ImageQuad).
249     frame_buffers: Vec<AllocatedBuffer>,
250 }
251 
252 impl ImageStage {
253     pub(crate) fn new(
254         device: &ash::Device,
255         allocator: &mut Allocator,
256         render_pass: vk::RenderPass,
257         frames_in_flight: usize,
258     ) -> Self {
259         // One image table per renderer, so this is the renderer count — see
260         // `renderer_epoch`, which is what tells a cache of ids that its
261         // renderer is gone.
262         STAGES_BUILT.fetch_add(1, Ordering::Relaxed);
263         unsafe {
264             let bindings = [
265                 vk::DescriptorSetLayoutBinding::default()
266                     .binding(0)
267                     .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
268                     .descriptor_count(1)
269                     .stage_flags(vk::ShaderStageFlags::FRAGMENT),
270                 vk::DescriptorSetLayoutBinding::default()
271                     .binding(1)
272                     .descriptor_type(vk::DescriptorType::SAMPLER)
273                     .descriptor_count(1)
274                     .stage_flags(vk::ShaderStageFlags::FRAGMENT),
275             ];
276             let descriptor_set_layout = device
277                 .create_descriptor_set_layout(
278                     &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
279                     None,
280                 )
281                 .expect("Failed to create image descriptor set layout");
282             let set_layouts = [descriptor_set_layout];
283             let pipeline_layout = device
284                 .create_pipeline_layout(
285                     &vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts),
286                     None,
287                 )
288                 .expect("Failed to create image pipeline layout");
289 
290             // Same shader as glyphs: sampled texel * vertex color (+ circle clip).
291             let spirv = super::renderer::glyph_spirv();
292             let shader_module = device
293                 .create_shader_module(&vk::ShaderModuleCreateInfo::default().code(spirv), None)
294                 .expect("Failed to create image shader module");
295             let stages = [
296                 vk::PipelineShaderStageCreateInfo::default()
297                     .stage(vk::ShaderStageFlags::VERTEX)
298                     .module(shader_module)
299                     .name(c"vs_main"),
300                 vk::PipelineShaderStageCreateInfo::default()
301                     .stage(vk::ShaderStageFlags::FRAGMENT)
302                     .module(shader_module)
303                     .name(c"fs_main"),
304             ];
305             let vertex_bindings = [vk::VertexInputBindingDescription::default()
306                 .binding(0)
307                 .stride(std::mem::size_of::<ImageVertex>() as u32)
308                 .input_rate(vk::VertexInputRate::VERTEX)];
309             let vertex_attributes = [
310                 vk::VertexInputAttributeDescription::default()
311                     .location(0)
312                     .binding(0)
313                     .format(vk::Format::R32G32_SFLOAT)
314                     .offset(0),
315                 vk::VertexInputAttributeDescription::default()
316                     .location(1)
317                     .binding(0)
318                     .format(vk::Format::R32G32_SFLOAT)
319                     .offset(8),
320                 vk::VertexInputAttributeDescription::default()
321                     .location(2)
322                     .binding(0)
323                     .format(vk::Format::R32G32B32A32_SFLOAT)
324                     .offset(16),
325                 vk::VertexInputAttributeDescription::default()
326                     .location(3)
327                     .binding(0)
328                     .format(vk::Format::R32G32B32_SFLOAT)
329                     .offset(32),
330                 // Location 4 is declared by the shared glyph shader; without this
331                 // entry the pipeline is invalid and the attribute reads undefined.
332                 vk::VertexInputAttributeDescription::default()
333                     .location(4)
334                     .binding(0)
335                     .format(vk::Format::R32G32_SFLOAT)
336                     .offset(44),
337             ];
338             let vertex_input = vk::PipelineVertexInputStateCreateInfo::default()
339                 .vertex_binding_descriptions(&vertex_bindings)
340                 .vertex_attribute_descriptions(&vertex_attributes);
341             let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::default()
342                 .topology(vk::PrimitiveTopology::TRIANGLE_LIST);
343             let viewport_state = vk::PipelineViewportStateCreateInfo::default()
344                 .viewport_count(1)
345                 .scissor_count(1);
346             let rasterization = vk::PipelineRasterizationStateCreateInfo::default()
347                 .polygon_mode(vk::PolygonMode::FILL)
348                 .cull_mode(vk::CullModeFlags::NONE)
349                 .front_face(vk::FrontFace::COUNTER_CLOCKWISE)
350                 .line_width(1.0);
351             let multisample = vk::PipelineMultisampleStateCreateInfo::default()
352                 .rasterization_samples(vk::SampleCountFlags::TYPE_1);
353             let blend_attachments = [vk::PipelineColorBlendAttachmentState::default()
354                 .blend_enable(true)
355                 .src_color_blend_factor(vk::BlendFactor::SRC_ALPHA)
356                 .dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
357                 .color_blend_op(vk::BlendOp::ADD)
358                 .src_alpha_blend_factor(vk::BlendFactor::ONE)
359                 .dst_alpha_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
360                 .alpha_blend_op(vk::BlendOp::ADD)
361                 .color_write_mask(vk::ColorComponentFlags::RGBA)];
362             let color_blend = vk::PipelineColorBlendStateCreateInfo::default()
363                 .attachments(&blend_attachments);
364             let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR];
365             let dynamic_state =
366                 vk::PipelineDynamicStateCreateInfo::default().dynamic_states(&dynamic_states);
367             let pipeline = device
368                 .create_graphics_pipelines(
369                     vk::PipelineCache::null(),
370                     &[vk::GraphicsPipelineCreateInfo::default()
371                         .stages(&stages)
372                         .vertex_input_state(&vertex_input)
373                         .input_assembly_state(&input_assembly)
374                         .viewport_state(&viewport_state)
375                         .rasterization_state(&rasterization)
376                         .multisample_state(&multisample)
377                         .color_blend_state(&color_blend)
378                         .dynamic_state(&dynamic_state)
379                         .layout(pipeline_layout)
380                         .render_pass(render_pass)
381                         .subpass(0)],
382                     None,
383                 )
384                 .expect("Failed to create image pipeline")[0];
385 
386             // Linear filtering: thumbnails scale smoothly.
387             let sampler = device
388                 .create_sampler(
389                     &vk::SamplerCreateInfo::default()
390                         .mag_filter(vk::Filter::LINEAR)
391                         .min_filter(vk::Filter::LINEAR)
392                         .mipmap_mode(vk::SamplerMipmapMode::NEAREST)
393                         .address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE)
394                         .address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE)
395                         .address_mode_w(vk::SamplerAddressMode::CLAMP_TO_EDGE),
396                     None,
397                 )
398                 .expect("Failed to create image sampler");
399 
400             let pool_sizes = [
401                 vk::DescriptorPoolSize::default()
402                     .ty(vk::DescriptorType::SAMPLED_IMAGE)
403                     .descriptor_count(MAX_IMAGES),
404                 vk::DescriptorPoolSize::default()
405                     .ty(vk::DescriptorType::SAMPLER)
406                     .descriptor_count(MAX_IMAGES),
407             ];
408             let descriptor_pool = device
409                 .create_descriptor_pool(
410                     &vk::DescriptorPoolCreateInfo::default()
411                         .flags(vk::DescriptorPoolCreateFlags::FREE_DESCRIPTOR_SET)
412                         .max_sets(MAX_IMAGES)
413                         .pool_sizes(&pool_sizes),
414                     None,
415                 )
416                 .expect("Failed to create image descriptor pool");
417 
418             let frame_buffers = (0..frames_in_flight)
419                 .map(|_| {
420                     create_cpu_buffer(
421                         device,
422                         allocator,
423                         16 * 1024,
424                         vk::BufferUsageFlags::VERTEX_BUFFER,
425                         "image-quads",
426                     )
427                 })
428                 .collect();
429 
430             ImageStage {
431                 pipeline,
432                 pipeline_layout,
433                 descriptor_set_layout,
434                 descriptor_pool,
435                 shader_module,
436                 sampler,
437                 images: HashMap::new(),
438                 staging: None,
439                 staging_idle: 0,
440                 frame_buffers,
441             }
442         }
443     }
444 
445     /// Drain the global upload/free queue. Uploads are synchronous one-time
446     /// submits (rare: images load once); frees wait for device idle.
447     pub(crate) fn process_pending(
448         &mut self,
449         device: &ash::Device,
450         allocator: &mut Allocator,
451         queue: vk::Queue,
452         command_pool: vk::CommandPool,
453     ) {
454         let pending: Vec<Pending> = std::mem::take(&mut *PENDING.lock().unwrap());
455         if pending.is_empty() {
456             self.staging_idle = self.staging_idle.saturating_add(1);
457             if self.staging_idle > STAGING_IDLE_FRAMES {
458                 if let Some(mut idle) = self
459                     .staging
460                     .take_if(|b| b.size > STAGING_KEEP_BYTES)
461                 {
462                     // Safe without a wait for the same reason `staging_for`
463                     // needs none: every copy waits for itself before
464                     // returning, so nothing is reading this buffer here.
465                     destroy_cpu_buffer(device, allocator, &mut idle);
466                 }
467             }
468             return;
469         }
470         self.staging_idle = 0;
471         for item in pending {
472             match item {
473                 Pending::Upload { id, pixels, width, height, format } => {
474                     self.upload(
475                         device, allocator, queue, command_pool, id, &pixels, width, height, format,
476                     );
477                     retire_buffer(pixels);
478                 }
479                 Pending::Update { id, pixels, width, height, format } => {
480                     // Same picture, new contents: copy into the image that is
481                     // already there. Anything else about it changing (a window
482                     // resize) falls back to building a fresh one under the
483                     // same id.
484                     let reusable = self.images.get(&id).is_some_and(|gpu| {
485                         gpu.width == width && gpu.height == height && gpu.format == format
486                     });
487                     if reusable {
488                         self.write_into(device, allocator, queue, command_pool, id, &pixels);
489                     } else {
490                         self.destroy_image(device, allocator, id);
491                         self.upload(
492                             device, allocator, queue, command_pool, id, &pixels, width, height,
493                             format,
494                         );
495                     }
496                     retire_buffer(pixels);
497                 }
498                 Pending::Free { id } => self.destroy_image(device, allocator, id),
499             }
500         }
501     }
502 
503     /// Tear one image down. Destroying something the GPU may still be reading
504     /// needs the device idle, which is why this is not on the per-frame path
505     /// any more: a streaming caller updates in place and never gets here until
506     /// it is finished with the image for good.
507     fn destroy_image(&mut self, device: &ash::Device, allocator: &mut Allocator, id: u32) {
508         let Some(mut gpu) = self.images.remove(&id) else { return };
509         unsafe {
510             let _ = device.device_wait_idle();
511             device.destroy_image_view(gpu.view, None);
512             device.destroy_image(gpu.image, None);
513             let _ = device.free_descriptor_sets(self.descriptor_pool, &[gpu.descriptor_set]);
514         }
515         if let Some(a) = gpu.allocation.take() {
516             let _ = allocator.free(a);
517         }
518     }
519 
520     /// The shared staging buffer, grown if this upload needs more room.
521     fn staging_for(
522         &mut self,
523         device: &ash::Device,
524         allocator: &mut Allocator,
525         bytes: usize,
526     ) -> &mut AllocatedBuffer {
527         let too_small = self
528             .staging
529             .as_ref()
530             .is_none_or(|b| (b.size as usize) < bytes);
531         if too_small {
532             if let Some(mut old) = self.staging.take() {
533                 // No wait: every copy submitted from here is followed by
534                 // `queue_wait_idle` before its caller returns, so no GPU work
535                 // is referencing the old buffer by the time anything asks for
536                 // a bigger one.
537                 destroy_cpu_buffer(device, allocator, &mut old);
538             }
539             self.staging = Some(create_cpu_buffer(
540                 device,
541                 allocator,
542                 bytes as vk::DeviceSize,
543                 vk::BufferUsageFlags::TRANSFER_SRC,
544                 "image-staging",
545             ));
546         }
547         self.staging.as_mut().expect("staging buffer")
548     }
549 
550     #[allow(clippy::too_many_arguments)]
551     fn upload(
552         &mut self,
553         device: &ash::Device,
554         allocator: &mut Allocator,
555         queue: vk::Queue,
556         command_pool: vk::CommandPool,
557         id: u32,
558         pixels: &[u8],
559         width: u32,
560         height: u32,
561         format: PixelFormat,
562     ) {
563         if self.images.len() as u32 >= MAX_IMAGES {
564             log::error!("image registry full ({MAX_IMAGES}); dropping upload {id}");
565             return;
566         }
567         unsafe {
568             let image = device
569                 .create_image(
570                     &vk::ImageCreateInfo::default()
571                         .image_type(vk::ImageType::TYPE_2D)
572                         // SRGB, not UNORM: uploaded pixels are sRGB-encoded
573                         // (rasterized SVGs, decoded PNGs, Servo page readback),
574                         // and the swapchain is an sRGB format, so the hardware
575                         // encodes shader output on write. Sampling as UNORM
576                         // fed those bytes through as if linear and encoded
577                         // them a second time, lightening every midtone —
578                         // a page's #101010 measured (71,71,71) on screen.
579                         // Decoding on sample makes the round trip exact.
580                         //
581                         // Which channel comes first is the caller's business
582                         // (`PixelFormat`): the sampler reads either order at
583                         // no cost, so a BGRA source never needs a CPU swizzle.
584                         .format(format.vk())
585                         .extent(vk::Extent3D { width, height, depth: 1 })
586                         .mip_levels(1)
587                         .array_layers(1)
588                         .samples(vk::SampleCountFlags::TYPE_1)
589                         .tiling(vk::ImageTiling::OPTIMAL)
590                         .usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST)
591                         .initial_layout(vk::ImageLayout::UNDEFINED),
592                     None,
593                 )
594                 .expect("Failed to create user image");
595             let requirements = device.get_image_memory_requirements(image);
596             let allocation = allocator
597                 .allocate(&AllocationCreateDesc {
598                     name: "user-image",
599                     requirements,
600                     location: MemoryLocation::GpuOnly,
601                     linear: false,
602                     allocation_scheme: AllocationScheme::GpuAllocatorManaged,
603                 })
604                 .expect("Failed to allocate user image memory");
605             device
606                 .bind_image_memory(image, allocation.memory(), allocation.offset())
607                 .expect("Failed to bind user image memory");
608 
609             let staging_buffer = {
610                 let staging = self.staging_for(device, allocator, pixels.len());
611                 staging.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()[..pixels.len()]
612                     .copy_from_slice(pixels);
613                 staging.buffer
614             };
615 
616             let range = vk::ImageSubresourceRange::default()
617                 .aspect_mask(vk::ImageAspectFlags::COLOR)
618                 .level_count(1)
619                 .layer_count(1);
620             let cmd = device
621                 .allocate_command_buffers(
622                     &vk::CommandBufferAllocateInfo::default()
623                         .command_pool(command_pool)
624                         .level(vk::CommandBufferLevel::PRIMARY)
625                         .command_buffer_count(1),
626                 )
627                 .expect("Failed to allocate upload command buffer")[0];
628             device
629                 .begin_command_buffer(
630                     cmd,
631                     &vk::CommandBufferBeginInfo::default()
632                         .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
633                 )
634                 .unwrap();
635             device.cmd_pipeline_barrier(
636                 cmd,
637                 vk::PipelineStageFlags::TOP_OF_PIPE,
638                 vk::PipelineStageFlags::TRANSFER,
639                 vk::DependencyFlags::empty(),
640                 &[],
641                 &[],
642                 &[vk::ImageMemoryBarrier::default()
643                     .src_access_mask(vk::AccessFlags::empty())
644                     .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
645                     .old_layout(vk::ImageLayout::UNDEFINED)
646                     .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
647                     .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
648                     .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
649                     .image(image)
650                     .subresource_range(range)],
651             );
652             device.cmd_copy_buffer_to_image(
653                 cmd,
654                 staging_buffer,
655                 image,
656                 vk::ImageLayout::TRANSFER_DST_OPTIMAL,
657                 &[vk::BufferImageCopy::default()
658                     .buffer_row_length(width)
659                     .buffer_image_height(height)
660                     .image_subresource(
661                         vk::ImageSubresourceLayers::default()
662                             .aspect_mask(vk::ImageAspectFlags::COLOR)
663                             .layer_count(1),
664                     )
665                     .image_extent(vk::Extent3D { width, height, depth: 1 })],
666             );
667             device.cmd_pipeline_barrier(
668                 cmd,
669                 vk::PipelineStageFlags::TRANSFER,
670                 vk::PipelineStageFlags::FRAGMENT_SHADER,
671                 vk::DependencyFlags::empty(),
672                 &[],
673                 &[],
674                 &[vk::ImageMemoryBarrier::default()
675                     .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
676                     .dst_access_mask(vk::AccessFlags::SHADER_READ)
677                     .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
678                     .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
679                     .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
680                     .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
681                     .image(image)
682                     .subresource_range(range)],
683             );
684             device.end_command_buffer(cmd).unwrap();
685             let cmds = [cmd];
686             device
687                 .queue_submit(queue, &[vk::SubmitInfo::default().command_buffers(&cmds)], vk::Fence::null())
688                 .expect("Image upload submit failed");
689             device.queue_wait_idle(queue).expect("Image upload wait failed");
690             device.free_command_buffers(command_pool, &cmds);
691 
692             let view = device
693                 .create_image_view(
694                     &vk::ImageViewCreateInfo::default()
695                         .image(image)
696                         .view_type(vk::ImageViewType::TYPE_2D)
697                         .format(format.vk())
698                         .subresource_range(range),
699                     None,
700                 )
701                 .expect("Failed to create user image view");
702 
703             let set_layouts = [self.descriptor_set_layout];
704             let descriptor_set = device
705                 .allocate_descriptor_sets(
706                     &vk::DescriptorSetAllocateInfo::default()
707                         .descriptor_pool(self.descriptor_pool)
708                         .set_layouts(&set_layouts),
709                 )
710                 .expect("Failed to allocate image descriptor set")[0];
711             let image_infos = [vk::DescriptorImageInfo::default()
712                 .image_view(view)
713                 .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)];
714             let sampler_infos = [vk::DescriptorImageInfo::default().sampler(self.sampler)];
715             device.update_descriptor_sets(
716                 &[
717                     vk::WriteDescriptorSet::default()
718                         .dst_set(descriptor_set)
719                         .dst_binding(0)
720                         .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
721                         .image_info(&image_infos),
722                     vk::WriteDescriptorSet::default()
723                         .dst_set(descriptor_set)
724                         .dst_binding(1)
725                         .descriptor_type(vk::DescriptorType::SAMPLER)
726                         .image_info(&sampler_infos),
727                 ],
728                 &[],
729             );
730 
731             self.images.insert(
732                 id,
733                 GpuImage {
734                     image,
735                     view,
736                     allocation: Some(allocation),
737                     descriptor_set,
738                     width,
739                     height,
740                     format,
741                 },
742             );
743         }
744     }
745 
746     /// Replace an existing image's contents in place.
747     ///
748     /// The whole streaming path. Against `upload` it skips creating an image,
749     /// allocating its memory, allocating and writing a descriptor set, and —
750     /// the expensive one — destroying last frame's image, which needs the
751     /// device idle and so waits for every frame still in flight.
752     ///
753     /// The copy is still its own submission followed by `queue_wait_idle`,
754     /// and that wait is doing real work: the image is one the *previous*
755     /// frame may still be sampling, and two submissions on one queue are not
756     /// ordered against each other by anything weaker. Lifting it means giving
757     /// each streaming image a second buffer to alternate between and
758     /// recording the copy into the frame's own command buffer, which is the
759     /// next step rather than this one.
760     fn write_into(
761         &mut self,
762         device: &ash::Device,
763         allocator: &mut Allocator,
764         queue: vk::Queue,
765         command_pool: vk::CommandPool,
766         id: u32,
767         pixels: &[u8],
768     ) {
769         let Some(&GpuImage { image, width, height, .. }) = self.images.get(&id) else { return };
770         unsafe {
771             let staging_buffer = {
772                 let staging = self.staging_for(device, allocator, pixels.len());
773                 staging.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()[..pixels.len()]
774                     .copy_from_slice(pixels);
775                 staging.buffer
776             };
777             let range = vk::ImageSubresourceRange::default()
778                 .aspect_mask(vk::ImageAspectFlags::COLOR)
779                 .level_count(1)
780                 .layer_count(1);
781             let cmd = device
782                 .allocate_command_buffers(
783                     &vk::CommandBufferAllocateInfo::default()
784                         .command_pool(command_pool)
785                         .level(vk::CommandBufferLevel::PRIMARY)
786                         .command_buffer_count(1),
787                 )
788                 .expect("Failed to allocate update command buffer")[0];
789             device
790                 .begin_command_buffer(
791                     cmd,
792                     &vk::CommandBufferBeginInfo::default()
793                         .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
794                 )
795                 .unwrap();
796             // Unlike a fresh upload this image holds a picture already, and
797             // it is in the layout the shader reads. Every byte is about to be
798             // overwritten, so its old contents need not be preserved — but
799             // the layout transition still has to be spelled out both ways.
800             device.cmd_pipeline_barrier(
801                 cmd,
802                 vk::PipelineStageFlags::FRAGMENT_SHADER,
803                 vk::PipelineStageFlags::TRANSFER,
804                 vk::DependencyFlags::empty(),
805                 &[],
806                 &[],
807                 &[vk::ImageMemoryBarrier::default()
808                     .src_access_mask(vk::AccessFlags::SHADER_READ)
809                     .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
810                     .old_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
811                     .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
812                     .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
813                     .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
814                     .image(image)
815                     .subresource_range(range)],
816             );
817             device.cmd_copy_buffer_to_image(
818                 cmd,
819                 staging_buffer,
820                 image,
821                 vk::ImageLayout::TRANSFER_DST_OPTIMAL,
822                 &[vk::BufferImageCopy::default()
823                     .buffer_row_length(width)
824                     .buffer_image_height(height)
825                     .image_subresource(
826                         vk::ImageSubresourceLayers::default()
827                             .aspect_mask(vk::ImageAspectFlags::COLOR)
828                             .layer_count(1),
829                     )
830                     .image_extent(vk::Extent3D { width, height, depth: 1 })],
831             );
832             device.cmd_pipeline_barrier(
833                 cmd,
834                 vk::PipelineStageFlags::TRANSFER,
835                 vk::PipelineStageFlags::FRAGMENT_SHADER,
836                 vk::DependencyFlags::empty(),
837                 &[],
838                 &[],
839                 &[vk::ImageMemoryBarrier::default()
840                     .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
841                     .dst_access_mask(vk::AccessFlags::SHADER_READ)
842                     .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
843                     .new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
844                     .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
845                     .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
846                     .image(image)
847                     .subresource_range(range)],
848             );
849             device.end_command_buffer(cmd).unwrap();
850             let cmds = [cmd];
851             device
852                 .queue_submit(
853                     queue,
854                     &[vk::SubmitInfo::default().command_buffers(&cmds)],
855                     vk::Fence::null(),
856                 )
857                 .expect("Image update submit failed");
858             device.queue_wait_idle(queue).expect("Image update wait failed");
859             device.free_command_buffers(command_pool, &cmds);
860         }
861     }
862 
863     /// After the frame fence: build this frame's quad vertices (6 per image,
864     /// in `images` order).
865     pub(crate) fn write_frame_buffer(
866         &mut self,
867         device: &ash::Device,
868         allocator: &mut Allocator,
869         frame_index: usize,
870         images: &[ImageQuad],
871         extent: vk::Extent2D,
872     ) {
873         let sw = extent.width as f32;
874         let sh = extent.height as f32;
875         let mut verts: Vec<ImageVertex> = Vec::with_capacity(images.len() * 6);
876         for q in images {
877             let (x, y, w, h) = q.rect;
878             let ndc = |px: f32, py: f32| [(px / sw) * 2.0 - 1.0, 1.0 - (py / sh) * 2.0];
879             let color = [1.0, 1.0, 1.0, q.alpha];
880             let clip_circle = [0.0; 3];
881             // Zero extents = the shader's plain-circle clip degenerate case. Inert
882             // while clip_circle.z is 0 (the clip branch never runs), but it must be
883             // a defined value, not whatever the missing attribute used to read.
884             let clip_extents = [0.0; 2];
885             let tl = ImageVertex { position: ndc(x, y), uv: [0.0, 0.0], color, clip_circle, clip_extents };
886             let tr = ImageVertex { position: ndc(x + w, y), uv: [1.0, 0.0], color, clip_circle, clip_extents };
887             let bl = ImageVertex { position: ndc(x, y + h), uv: [0.0, 1.0], color, clip_circle, clip_extents };
888             let br = ImageVertex { position: ndc(x + w, y + h), uv: [1.0, 1.0], color, clip_circle, clip_extents };
889             verts.extend([tl, tr, bl, tr, br, bl]);
890         }
891         let bytes: &[u8] = bytemuck::cast_slice(&verts);
892         let buf = &mut self.frame_buffers[frame_index];
893         if bytes.len() as vk::DeviceSize > buf.size {
894             let mut old = std::mem::replace(buf, AllocatedBuffer::null());
895             destroy_cpu_buffer(device, allocator, &mut old);
896             *buf = create_cpu_buffer(
897                 device,
898                 allocator,
899                 (bytes.len() as vk::DeviceSize).next_power_of_two(),
900                 vk::BufferUsageFlags::VERTEX_BUFFER,
901                 "image-quads",
902             );
903         }
904         if !bytes.is_empty() {
905             buf.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()[..bytes.len()]
906                 .copy_from_slice(bytes);
907         }
908     }
909 
910     /// Record one image quad (index `i` of this frame's list). The caller
911     /// restores its own pipeline/scissor state afterwards. Returns false if the
912     /// image hasn't finished uploading (draw skipped).
913     pub(crate) fn record_quad(
914         &self,
915         device: &ash::Device,
916         cmd: vk::CommandBuffer,
917         frame_index: usize,
918         i: usize,
919         image_id: u32,
920     ) -> bool {
921         let Some(gpu) = self.images.get(&image_id) else {
922             return false;
923         };
924         unsafe {
925             device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::GRAPHICS, self.pipeline);
926             device.cmd_bind_descriptor_sets(
927                 cmd,
928                 vk::PipelineBindPoint::GRAPHICS,
929                 self.pipeline_layout,
930                 0,
931                 &[gpu.descriptor_set],
932                 &[],
933             );
934             device.cmd_bind_vertex_buffers(cmd, 0, &[self.frame_buffers[frame_index].buffer], &[0]);
935             device.cmd_draw(cmd, 6, 1, (i * 6) as u32, 0);
936         }
937         true
938     }
939 
940     pub(crate) fn destroy(&mut self, device: &ash::Device, allocator: &mut Allocator) {
941         unsafe {
942             for (_, mut gpu) in self.images.drain() {
943                 device.destroy_image_view(gpu.view, None);
944                 device.destroy_image(gpu.image, None);
945                 if let Some(a) = gpu.allocation.take() {
946                     let _ = allocator.free(a);
947                 }
948             }
949             for buf in &mut self.frame_buffers {
950                 let mut b = std::mem::replace(buf, AllocatedBuffer::null());
951                 destroy_cpu_buffer(device, allocator, &mut b);
952             }
953             device.destroy_sampler(self.sampler, None);
954             device.destroy_descriptor_pool(self.descriptor_pool, None);
955             device.destroy_descriptor_set_layout(self.descriptor_set_layout, None);
956             device.destroy_pipeline(self.pipeline, None);
957             device.destroy_pipeline_layout(self.pipeline_layout, None);
958             device.destroy_shader_module(self.shader_module, None);
959         }
960     }
961 }