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 }