Skip to main content

unlit_wgpu/
ui.rs

1//! An [egui](https://egui.rs) backend built on the built-in lit pipeline.
2//!
3//! [`EguiIntegration`] takes what egui produces each frame — tessellated, clipped
4//! meshes plus a set of texture updates — and draws it with the built-in
5//! pipeline as ordinary screen-space geometry. Nothing in the shader knows
6//! about egui: the UI is premultiplied, textured, screen-space vertices, and
7//! egui's clip rectangles become scissor rectangles.
8//!
9//! ```rust
10//! # use unlit_wgpu::globals::Globals;
11//! # use unlit_wgpu::pipeline::{CAMERA_BINDING, FRAME_BINDING, VIEW_BINDING};
12//! # use unlit_wgpu::resources::ResourceGraph;
13//! # use unlit_wgpu::specialize::{SpecializedPipeline, SurfaceKey};
14//! # use unlit_wgpu::ui::{
15//! #     EguiIntegration, ScreenDescriptor, screen_view, screen_viewport, ui_variant,
16//! # };
17//! # use zerocopy::IntoBytes;
18//! # fn frame(device: &wgpu::Device, queue: &wgpu::Queue, ctx: &egui::Context,
19//! #          surface: SurfaceKey) {
20//! // The caller owns the globals: a camera uniform (written every frame with
21//! // `screen_view`), a frame-globals uniform, the view `screen_viewport`
22//! // derives from the same points, and the bind group binding all three.
23//! let variant = ui_variant(/* the target encodes sRGB: */ true, surface);
24//! let pipeline = SpecializedPipeline::create(device, variant);
25//! let camera = uniform_buffer(device, "ui::camera");
26//! let globals = uniform_buffer(device, "ui::globals");
27//! let view = uniform_buffer(device, "ui::view");
28//! let global_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
29//!     label: Some("ui::globals"),
30//!     layout: &pipeline.descriptor().bind_group_layouts(device).global,
31//!     entries: &[
32//!         wgpu::BindGroupEntry {
33//!             binding: CAMERA_BINDING,
34//!             resource: camera.as_entire_binding(),
35//!         },
36//!         wgpu::BindGroupEntry {
37//!             binding: FRAME_BINDING,
38//!             resource: globals.as_entire_binding(),
39//!         },
40//!         wgpu::BindGroupEntry {
41//!             binding: VIEW_BINDING,
42//!             resource: view.as_entire_binding(),
43//!         },
44//!     ],
45//! });
46//!
47//! // The camera and the view are written every frame; the UI reads no clock,
48//! // so its frame globals stay at their default.
49//! queue.write_buffer(&camera, 0, screen_view([256.0, 192.0]).as_bytes());
50//! queue.write_buffer(&view, 0, screen_viewport([256.0, 192.0]).as_bytes());
51//! queue.write_buffer(&globals, 0, Globals::default().as_bytes());
52//!
53//! // The UI's per-texture resources join the caller's ledger.
54//! let mut graph = ResourceGraph::new();
55//! let camera_id = graph.insert(camera, None);
56//! let globals_id = graph.insert(globals, None);
57//! let view_id = graph.insert(view, None);
58//! let group_id = graph.insert(global_group, None);
59//! graph.add_dependency(&group_id, &camera_id);
60//! graph.add_dependency(&group_id, &globals_id);
61//! graph.add_dependency(&group_id, &view_id);
62//!
63//! let mut ui = EguiIntegration::new(device, pipeline);
64//! let mut input = egui::RawInput::default();
65//! input.screen_rect = Some(egui::Rect::from_min_size(
66//!     egui::Pos2::ZERO,
67//!     egui::Vec2::new(256.0, 192.0),
68//! ));
69//! let output = ctx.run_ui(input, |ui| {
70//!     ui.label("hello world");
71//! });
72//! // The frame's encoder carries both the UI's staged vertex and index
73//! // uploads and the pass that reads them; `scene` is recorded into it
74//! // after this call.
75//! let mut encoder = device.create_command_encoder(&Default::default());
76//! // The target's physical size and its density, which the clip rectangles
77//! // are scaled by.
78//! let screen = ScreenDescriptor {
79//!     size_in_pixels: [256, 192],
80//!     pixels_per_point: 1.0,
81//! };
82//! ui.update(&mut graph, queue, &mut encoder, ctx, output, screen);
83//! let scene = ui.scene(&graph, &graph.get(&group_id).expect("the group exists"));
84//! # }
85//! # fn uniform_buffer(device: &wgpu::Device, label: &str) -> wgpu::Buffer {
86//! #     device.create_buffer(&wgpu::BufferDescriptor {
87//! #         label: Some(label),
88//! #         size: 256,
89//! #         usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
90//! #         mapped_at_creation: false,
91//! #     })
92//! # }
93//! ```
94
95use crate::globals::{Camera, View};
96use crate::mesh::{ChannelEncoding, MeshInstance, PositionStreamChannels, SecondaryFlags};
97use crate::pipeline::{
98    BASE_COLOR_SAMPLER_BINDING, BASE_COLOR_TEXTURE_BINDING, GLOBAL_GROUP, INSTANCE_SLOT,
99    LitOptions, LitVariant, LitVertexChannels, MATERIAL_GROUP, PRIMARY_SLOT, SECONDARY_SLOT,
100    SpecializedLitPipeline,
101};
102use crate::resources::{Rebuild, ResHandle, Resource, ResourceGraph, TextureExt, TextureView};
103use crate::scene::{DrawEntry, DrawOrder, DrawRange, Scene, ScissorRect};
104use crate::specialize::SurfaceKey;
105use crate::staging::StagingBuffer;
106use crate::util::Hashed;
107use core::ops::Range;
108use hashbrown::HashMap;
109use zerocopy::IntoBytes;
110
111/// egui's own texture format: gamma-space RGBA, never sRGB-aware.
112///
113/// egui's vertex colors and its font atlas are already gamma-encoded, so a
114/// texture is sampled without conversion and the pipeline's own output
115/// conversion deals with the target instead.
116const TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
117
118/// Bytes one planar-position vertex occupies.
119///
120/// The UI writes positions at full precision and in the plane, so this is
121/// [`ChannelEncoding::PlanarPosition`]'s format: two components, with the
122/// zero third supplied by the shader rather than stored.
123const POSITION_STRIDE: usize = wgpu::VertexFormat::Float32x2.size() as usize;
124
125/// Bytes one secondary vertex occupies: `Float32x2` then `Unorm8x4`.
126const SECONDARY_STRIDE: usize =
127    wgpu::VertexFormat::Float32x2.size() as usize + wgpu::VertexFormat::Unorm8x4.size() as usize;
128
129/// The target a UI frame is drawn into and the density it is drawn at.
130///
131/// The two are needed together and in opposite units, which is the easiest
132/// thing to get wrong here: egui tessellates in **logical points**, so the
133/// projection takes the point size, while a scissor rectangle is in **physical
134/// pixels**, so the clip rectangles are scaled by [`Self::pixels_per_point`].
135#[derive(Clone, Copy, Debug, PartialEq)]
136pub struct ScreenDescriptor {
137    /// The target's size in physical pixels.
138    pub size_in_pixels: [u32; 2],
139    /// How many physical pixels one logical point covers.
140    pub pixels_per_point: f32,
141}
142
143impl ScreenDescriptor {
144    /// The target's size in logical points, which is what the UI's projection
145    /// and its `RawInput::screen_rect` are expressed in.
146    pub fn size_in_points(self) -> [f32; 2] {
147        [
148            self.size_in_pixels[0] as f32 / self.pixels_per_point,
149            self.size_in_pixels[1] as f32 / self.pixels_per_point,
150        ]
151    }
152}
153
154/// A world-to-clip matrix taking egui's tessellated points to clip space.
155///
156/// egui positions its vertices in logical points with the origin at the top
157/// left and Y growing downwards, so the Y coefficient is negated; Z collapses
158/// to zero, since everything the UI draws sits at one depth. The physical
159/// density never enters: the target's format already encodes its pixels.
160///
161/// The caller writes this into their camera uniform, since the camera buffer
162/// is theirs; [`screen_viewport`] is the view to bind beside it.
163pub fn screen_view(viewport_points: [f32; 2]) -> Camera {
164    let [width, height] = viewport_points;
165    Camera::from_clip_from_world(
166        glam::Mat4::from_cols_array(&[
167            2.0 / width,
168            0.0,
169            0.0,
170            0.0,
171            0.0,
172            -2.0 / height,
173            0.0,
174            0.0,
175            0.0,
176            0.0,
177            0.0,
178            0.0,
179            -1.0,
180            1.0,
181            0.0,
182            1.0,
183        ]),
184        glam::Vec3::ZERO,
185    )
186}
187
188/// The view to bind beside [`screen_view`].
189///
190/// The clipping is in points, so the size a shader measures against is the point
191/// size too: that is what keeps the two in one unit.
192pub fn screen_viewport(viewport_points: [f32; 2]) -> View {
193    View::new(viewport_points[0], viewport_points[1])
194}
195
196/// The pipeline variant a UI is drawn with.
197///
198/// `srgb_to_linear_output` is the caller's call, not this function's: it
199/// depends on the target's format, which this function never sees. Set it
200/// when the target encodes sRGB.
201///
202/// Every UI draw has the same shape: full-precision screen-space vertices
203/// carrying a premultiplied color and a texture coordinate, no skinning and no
204/// morph target, and a base-color texture sampled through a material group.
205/// What changes with the target is the surface the variant carries, so this is
206/// the only place a UI variant is built.
207pub fn ui_variant(srgb_to_linear_output: bool, surface: SurfaceKey) -> LitVariant {
208    let mut options = LitOptions::standard();
209    apply_ui_settings(&mut options, srgb_to_linear_output);
210    LitVariant {
211        options: Hashed::new(options),
212        surface,
213        channels: ui_channels(),
214        base_color_texture: true,
215        morph: false,
216        strip_index_format: None,
217    }
218}
219
220/// The per-vertex channels a UI mesh is packed with.
221///
222/// The UI writes positions at full precision and in the plane — two components,
223/// with the shader supplying the zero third — and needs no skinning, and every
224/// vertex carries a texture coordinate and a premultiplied color. A caller
225/// packing a UI mesh builds its streams to match this.
226pub fn ui_channels() -> LitVertexChannels {
227    LitVertexChannels {
228        position: PositionStreamChannels {
229            position: Some(ChannelEncoding::PlanarPosition),
230            joints: false,
231        },
232        secondary: SecondaryFlags::UV | SecondaryFlags::UNCOMPRESSED_UV | SecondaryFlags::COLOR,
233    }
234}
235
236/// Apply the UI's device-independent policy — the output conversion, culling,
237/// blending and the overlaid depth behavior — to a standard options set.
238///
239/// The geometry the UI draws with is not policy and lives in
240/// [`ui_channels`] and the variant: an encoding, a skin or a morph target is
241/// a property of the mesh, not of what the caller picked.
242fn apply_ui_settings(options: &mut LitOptions, srgb_to_linear_output: bool) {
243    options.srgb_to_linear_output = srgb_to_linear_output;
244    // A UI fragment composites what egui tessellated and shades nothing: the
245    // overlay is drawn after the scene, so a light would tint text.
246    options.unlit = true;
247    // No culling: egui does not guarantee a consistent winding order across
248    // the primitives it emits, so neither face can be discarded safely.
249    // (`LitOptions::standard` culls back faces for closed meshes; the UI is
250    // flat, overlay geometry with no inside to hide.)
251    options.primitive.cull_mode = None;
252    // egui tessellates premultiplied colors, so the source's RGB is added to
253    // the destination scaled by the alpha it leaves behind.
254    options.blend = Some(wgpu::BlendState {
255        color: wgpu::BlendComponent {
256            src_factor: wgpu::BlendFactor::One,
257            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
258            operation: wgpu::BlendOperation::Add,
259        },
260        alpha: wgpu::BlendComponent {
261            src_factor: wgpu::BlendFactor::OneMinusDstAlpha,
262            dst_factor: wgpu::BlendFactor::One,
263            operation: wgpu::BlendOperation::Add,
264        },
265    });
266    // The UI overlays whatever the pass holds, so it neither tests nor writes
267    // depth. Whether the target has a depth attachment at all is decided by
268    // the surface the variant carries; the policy only says what a target
269    // with one would run.
270    options.depth_stencil.depth_write_enabled = false;
271    options.depth_stencil.depth_compare = wgpu::CompareFunction::Always;
272}
273
274/// How much geometry a frame holds: the two numbers the packing and the draw
275/// ranges both need, and which must agree between them.
276#[derive(Clone, Copy, Debug, PartialEq, Eq)]
277struct GeometryCounts {
278    vertices: usize,
279    indices: usize,
280}
281
282/// Count the vertices and indices every tessellated primitive in
283/// `primitives` contributes.
284fn measure<'a>(
285    primitives: impl IntoIterator<Item = (Source, &'a egui::ClippedPrimitive)>,
286) -> GeometryCounts {
287    let mut counts = GeometryCounts {
288        vertices: 0,
289        indices: 0,
290    };
291    for (_, primitive) in primitives {
292        if let Some(mesh) = mesh_of(primitive) {
293            counts.vertices += mesh.vertices.len();
294            counts.indices += mesh.indices.len();
295        }
296    }
297    counts
298}
299
300/// Pack `primitives` into `vertices` and `indices`, and fill `draws` with where
301/// each one landed.
302///
303/// Both byte buffers are cleared and resized rather than reallocated, so a
304/// caller that keeps them across frames reuses the allocation. The vertex
305/// buffer holds the primary stream first and the interleaved secondary
306/// stream second: wgpu binds one stride per slot and the two differ.
307///
308/// `counts` must be what [`measure`] returned for `primitives`: it is both how
309/// much is written and where the two vertex streams meet.
310fn pack_geometry<'a>(
311    primitives: impl IntoIterator<Item = (Source, &'a egui::ClippedPrimitive)>,
312    screen: ScreenDescriptor,
313    texture_options: &HashMap<TextureKey, egui::TextureOptions>,
314    counts: GeometryCounts,
315    vertices: &mut Vec<u8>,
316    indices: &mut Vec<u8>,
317    draws: &mut Vec<UiDraw>,
318) {
319    // The split between the two streams is the frame's own vertex count, which
320    // is what `scene` reads back. It is *not* the buffer's capacity: that is
321    // usually larger, and splitting on it would write the UVs and colors
322    // somewhere the draws do not look for them.
323    let secondary_start = counts.vertices * POSITION_STRIDE;
324    let vertex_count = counts.vertices;
325    let index_count = counts.indices;
326
327    vertices.clear();
328    vertices.resize(secondary_start + vertex_count * SECONDARY_STRIDE, 0);
329    indices.clear();
330    indices.resize(index_count * size_of::<u32>(), 0);
331    draws.clear();
332
333    let mut vertex_cursor = 0usize;
334    let mut index_cursor = 0usize;
335    for (source, primitive) in primitives {
336        // A paint callback draws its own way; this renderer handles tessellated
337        // meshes only.
338        let Some(mesh) = mesh_of(primitive) else {
339            continue;
340        };
341        if mesh.vertices.is_empty() || mesh.indices.is_empty() {
342            continue;
343        }
344
345        let first_vertex = vertex_cursor as u32;
346        let first_index = index_cursor as u32;
347
348        for vertex in &mesh.vertices {
349            let position = vertex_cursor * POSITION_STRIDE;
350            // The UI is flat: two components, the shader supplies the zero
351            // third.
352            vertices[position..position + POSITION_STRIDE]
353                .copy_from_slice([vertex.pos.x, vertex.pos.y].as_bytes());
354            let secondary = secondary_start + vertex_cursor * SECONDARY_STRIDE;
355            vertices[secondary..secondary + size_of::<egui::Vec2>()]
356                .copy_from_slice([vertex.uv.x, vertex.uv.y].as_bytes());
357            // egui's colors are premultiplied already, straight to Unorm8.
358            vertices[secondary + size_of::<egui::Vec2>()..secondary + SECONDARY_STRIDE]
359                .copy_from_slice(&vertex.color.to_array());
360            vertex_cursor += 1;
361        }
362
363        let index_start = index_cursor * size_of::<u32>();
364        for (slot, &index) in indices[index_start..]
365            .as_chunks_mut::<{ size_of::<u32>() }>()
366            .0
367            .iter_mut()
368            .zip(&mesh.indices)
369        {
370            // The primitive's indices are local to its own vertices, and the
371            // draw addresses both streams by a pool-global ordinal, so the
372            // primitive's first vertex is added here. Baking it into the
373            // indices rather than passing it as the draw's `base_vertex` costs
374            // nothing — the indices are rewritten every frame either way — and
375            // it is the only form a device without `base_vertex` (WebGL2)
376            // accepts.
377            slot.copy_from_slice((index + first_vertex).as_bytes());
378        }
379        index_cursor += mesh.indices.len();
380
381        let texture = TextureKey {
382            source,
383            id: mesh.texture_id,
384        };
385        draws.push(UiDraw {
386            indices: first_index..index_cursor as u32,
387            texture,
388            // The texture's own sampling options, not a default: a texture egui
389            // asked to sample nearest or to tile would otherwise be sampled
390            // linearly and clamped, and the sampler that matches `options` is
391            // the one the material binds.
392            options: texture_options.get(&texture).copied().unwrap_or_default(),
393            scissor: scissor_rect(primitive.clip_rect, screen),
394        });
395    }
396}
397
398/// One uploaded primitive: where its indices sit, what it samples and how it
399/// is clipped.
400struct UiDraw {
401    /// Index range into the index buffer, counting indices.
402    ///
403    /// The indices themselves already name the primitive's vertices in the
404    /// pool, so a draw needs no `base_vertex` to address them; see
405    /// [`pack_geometry`].
406    indices: Range<u32>,
407    /// The texture this primitive samples, and the context it came from.
408    texture: TextureKey,
409    options: egui::TextureOptions,
410    scissor: ScissorRect,
411}
412
413/// The graph nodes backing one egui texture slot.
414///
415/// A slot needs both: the texture, because egui patches its own atlases with
416/// partial updates and a patch is written into the texture, and the view,
417/// because a material bind group samples the view. They are two nodes with an
418/// edge between them, so a patched texture and everything built from it stay
419/// the same resources.
420#[derive(Clone)]
421struct TextureSlot {
422    /// The texture egui's deltas are written into.
423    texture: ResHandle<wgpu::Texture>,
424    /// The default view over it, which the material samples.
425    view: ResHandle<TextureView>,
426}
427
428/// Draws tessellated egui output with the built-in lit pipeline.
429///
430/// The UI's GPU resources are registered in a [`ResourceGraph`] the caller
431/// supplies — normally the one kept alongside the
432/// [`RenderAttachments`](crate::render_attachments::RenderAttachments) — so
433/// the whole frame shares one ledger. Every method that touches the graph
434/// takes it as an argument: the renderer holds only the bookkeeping (ids,
435/// keys) that maps egui's world onto graph nodes.
436pub struct EguiIntegration {
437    /// Kept to build the resources a frame turns out to need.
438    device: wgpu::Device,
439    pipeline: SpecializedLitPipeline,
440    /// Graph nodes of every allocated texture slot, keyed by origin and id.
441    textures: HashMap<TextureKey, TextureSlot>,
442    /// The sampling options egui last stated for each texture.
443    ///
444    /// Kept separately from the texture itself because every [`ImageDelta`]
445    /// carries the options that apply to the whole texture, and the draw that
446    /// samples it has to name the matching sampler:
447    /// [`Self::sampler`] keys its cache on exactly these options, so a texture
448    /// uploaded as nearest samples nearest.
449    ///
450    /// [`ImageDelta`]: egui::epaint::ImageDelta
451    texture_options: HashMap<TextureKey, egui::TextureOptions>,
452    /// One sampler per distinct set of egui sampling options seen, with its
453    /// graph node.
454    samplers: Vec<(egui::TextureOptions, ResHandle<wgpu::Sampler>)>,
455    /// One material bind group per (texture, options) pair, with its graph
456    /// node; the node depends on the texture's view and the sampler.
457    materials: Vec<(MaterialKey, ResHandle<wgpu::BindGroup>)>,
458    /// Positions, then interleaved UVs and colors.
459    vertices: Option<wgpu::Buffer>,
460    /// `Uint32` indices.
461    indices: Option<wgpu::Buffer>,
462    /// Graph node of the vertex buffer, replaced in place when it grows.
463    vertex_node: Option<ResHandle<wgpu::Buffer>>,
464    /// Graph node of the index buffer, replaced in place when it grows.
465    index_node: Option<ResHandle<wgpu::Buffer>>,
466    /// One default [`MeshInstance`], bound at [`INSTANCE_SLOT`] by every
467    /// draw.
468    ///
469    /// The built-in pipeline always declares the instance stream, and a UI
470    /// draw reads no instance state, so one identity record serves every draw.
471    /// It never changes, so it needs no graph node.
472    instance: wgpu::Buffer,
473    /// Vertices the vertex buffer holds room for.
474    vertex_capacity: usize,
475    /// Indices the index buffer holds room for.
476    index_capacity: usize,
477    /// Vertices the most recently uploaded frame holds. This is where its two
478    /// vertex streams meet, so [`Self::scene`] has to split at the same point
479    /// [`Self::upload_geometry`] packed at — which is not the capacity whenever
480    /// the buffer has room to spare.
481    frame_vertices: usize,
482    /// Staging buffer for the per-frame vertex uploads, reused across frames.
483    vertex_staging: StagingBuffer,
484    /// Staging buffer for the per-frame index uploads, reused across frames.
485    index_staging: StagingBuffer,
486    /// Layout of the frame most recently uploaded.
487    draws: Vec<UiDraw>,
488    /// Packing scratch for the vertex stream — positions then interleaved UVs
489    /// and colors — kept so a steady UI reuses one allocation instead of taking
490    /// a fresh one every frame.
491    packed_vertices: Vec<u8>,
492    /// Packing scratch for the index stream, reused the same way.
493    packed_indices: Vec<u8>,
494}
495
496/// Which context a texture belongs to.
497///
498/// Every egui context allocates its texture ids from zero, so one id names
499/// different images in different contexts. A UI drawn from several contexts —
500/// the world's own panels plus a frame laid out in another world — therefore
501/// keys its textures by this pair rather than by the id alone, which is what
502/// keeps two contexts' atlases apart without rewriting the ids a frame
503/// carries.
504///
505/// The origin of the world's own panels is [`Source::LOCAL`]; every other
506/// publisher picks its own, and the numbers are that publisher's to allocate.
507#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
508pub struct Source(pub u32);
509
510impl Source {
511    /// The context of the world doing the drawing.
512    pub const LOCAL: Self = Self(0);
513
514    /// The context of the world publishing into `slot`.
515    ///
516    /// The number is the slot's plus one, so no publisher collides with the
517    /// drawing world's own panels and each slot has a namespace of its own.
518    #[must_use]
519    pub const fn published(slot: usize) -> Self {
520        Self(slot as u32 + 1)
521    }
522}
523
524/// One texture of one source.
525#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
526struct TextureKey {
527    source: Source,
528    id: egui::TextureId,
529}
530
531/// One source's contribution to a frame: what to upload and what it owns.
532///
533/// The textures are consumed — a delta is applied once, and egui panics when
534/// one is dropped unapplied — while the primitives are borrowed: a frame laid
535/// out in another world is drawn again on every drawn frame until that world
536/// publishes a newer one, so its tessellation has to outlive the upload.
537pub struct UiFrame<'a> {
538    /// Which context this contribution came from.
539    pub source: Source,
540    /// The context's texture updates for this frame.
541    pub textures: egui::TexturesDelta,
542    /// The context's tessellated geometry.
543    pub primitives: &'a [egui::ClippedPrimitive],
544}
545
546/// What one material bind group was built from.
547#[derive(Clone, Copy, PartialEq, Eq)]
548struct MaterialKey {
549    texture: TextureKey,
550    options: egui::TextureOptions,
551}
552
553/// A one-element buffer holding the identity [`MeshInstance`].
554///
555/// The built-in lit pipeline declares the instance stream for every variant,
556/// so a draw that binds nothing at [`INSTANCE_SLOT`] is rejected. A UI draw
557/// reads no instance state — its vertices are already in screen space — so one
558/// default record satisfies the slot for every draw the integration records.
559fn default_instance(device: &wgpu::Device) -> wgpu::Buffer {
560    let instance = MeshInstance::default();
561    let buffer = device.create_buffer(&wgpu::BufferDescriptor {
562        label: Some("ui::instance"),
563        size: size_of::<MeshInstance>() as u64,
564        usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
565        mapped_at_creation: true,
566    });
567    buffer
568        .slice(..)
569        .get_mapped_range_mut()
570        .expect("mapped at creation")
571        .copy_from_slice(instance.as_bytes());
572    buffer.unmap();
573    buffer
574}
575
576impl EguiIntegration {
577    /// Build the UI integration around a caller-supplied `pipeline`,
578    /// registering the UI's per-texture resources in `graph`.
579    ///
580    /// `pipeline` is the built-in lit pipeline the UI draws with, configured
581    /// for the target the pass renders into — its color format, sample count
582    /// and blend state. The caller builds it up front, so the UI does not
583    /// compile a pipeline of its own.
584    ///
585    /// The global bind group — the camera, the frame globals and the view —
586    /// is the caller's, and it is handed to [`Self::scene`] rather than kept
587    /// here: the frame owns the uniforms and builds one group per camera and
588    /// viewport its draws name, so a source can draw the same UI through two
589    /// cameras without owning either group. Everything else the UI overlays is
590    /// the caller's too: the integration owns only egui's textures and the
591    /// geometry they are drawn with. Sharing the caller's graph — usually the
592    /// one kept alongside the
593    /// [`crate::render_attachments::RenderAttachments`] — means those
594    /// resources live in the same ledger as the rest of the frame's, with the
595    /// same dependency tracking.
596    pub fn new(device: &wgpu::Device, pipeline: SpecializedLitPipeline) -> Self {
597        Self {
598            device: device.clone(),
599            pipeline,
600            textures: HashMap::new(),
601            texture_options: HashMap::new(),
602            samplers: Vec::new(),
603            materials: Vec::new(),
604            vertices: None,
605            indices: None,
606            vertex_node: None,
607            index_node: None,
608            instance: default_instance(device),
609            vertex_capacity: 0,
610            index_capacity: 0,
611            frame_vertices: 0,
612            vertex_staging: StagingBuffer::new(),
613            index_staging: StagingBuffer::new(),
614            draws: Vec::new(),
615            packed_vertices: Vec::new(),
616            packed_indices: Vec::new(),
617        }
618    }
619
620    /// Apply `output`'s texture updates and upload its tessellated shapes,
621    /// ready for [`Self::scene`].
622    ///
623    /// The vertex and index bytes change every frame, so they are staged
624    /// through `encoder`: `scene`'s pass is recorded into the same encoder
625    /// afterwards, and one submission carries the upload and the draw that
626    /// reads it. Texture updates still go through `queue`.
627    ///
628    /// `screen` tells egui how to rasterise text and scales the tessellated points
629    /// and their clip rectangles; see [`ScreenDescriptor`].
630    ///
631    /// This tessellates `output` with `ctx`, which is right when the context
632    /// and the drawing share a world. A caller whose shapes were laid out
633    /// elsewhere — in another world, by another context — uses
634    /// [`Self::update_with`] and tessellates where the context lives.
635    pub fn update(
636        &mut self,
637        graph: &mut ResourceGraph,
638        queue: &wgpu::Queue,
639        encoder: &mut wgpu::CommandEncoder,
640        ctx: &egui::Context,
641        mut output: egui::FullOutput,
642        screen: ScreenDescriptor,
643    ) {
644        let primitives = ctx.tessellate(output.shapes, screen.pixels_per_point);
645        let textures = core::mem::take(&mut output.textures_delta);
646        let mut sources = [UiFrame {
647            source: Source::LOCAL,
648            textures,
649            primitives: &primitives,
650        }];
651        self.update_with(graph, queue, encoder, &mut sources, screen);
652    }
653
654    /// Apply texture updates and upload primitives from every source, ready
655    /// for [`Self::scene`].
656    ///
657    /// Tessellation needs the [`egui::Context`] that produced the shapes — its
658    /// font atlas and tessellation options — so a UI laid out in one world and
659    /// drawn in another cannot be tessellated by the drawing side. Such a
660    /// caller tessellates where each context lives and hands each result over
661    /// with the [`Source`] it came from.
662    ///
663    /// A source is a namespace, not an identity: every context allocates its
664    /// texture ids from zero, so the same id names different images in
665    /// different sources. Keys carry the pair, and the ids a frame arrived
666    /// with are the ids it is drawn by — nothing rewrites them.
667    ///
668    /// One call per frame: the integration packs one vertex and index buffer,
669    /// and the two vertex streams meet at a single split point, so a second
670    /// call would overwrite the first.
671    ///
672    /// Each `textures` delta is applied and then cleared, so the caller may
673    /// drop it afterwards: egui panics when a delta is dropped unapplied.
674    pub fn update_with(
675        &mut self,
676        graph: &mut ResourceGraph,
677        queue: &wgpu::Queue,
678        encoder: &mut wgpu::CommandEncoder,
679        sources: &mut [UiFrame<'_>],
680        screen: ScreenDescriptor,
681    ) {
682        for frame in sources.iter_mut() {
683            self.apply_textures(graph, queue, frame.source, &frame.textures);
684            // egui panics if a delta is dropped unapplied, so mark it handled.
685            frame.textures.clear();
686        }
687
688        // The primitives of every source, flattened in place: the sources
689        // keep their own tessellation and this walks it without collecting a
690        // list of borrows first.
691        let primitives = sources.iter().flat_map(|frame| {
692            frame
693                .primitives
694                .iter()
695                .map(move |primitive| (frame.source, primitive))
696        });
697
698        let counts = measure(primitives);
699        if counts.vertices == 0 {
700            self.draws.clear();
701            return;
702        }
703        self.reserve(graph, counts);
704        self.frame_vertices = counts.vertices;
705        let primitives = sources.iter().flat_map(|frame| {
706            frame
707                .primitives
708                .iter()
709                .map(move |primitive| (frame.source, primitive))
710        });
711        self.upload_geometry(encoder, primitives, screen, counts);
712
713        // Build every material the frame needs here, while the graph is still
714        // mutably available: `scene` only reads it, so a material that does
715        // not exist yet by then is one this frame cannot draw.
716        //
717        // The draws are walked by index because `build_material` needs `&mut
718        // self`: collecting the keys first would allocate a `Vec` every frame
719        // for a list that is already `self.draws`.
720        for index in 0..self.draws.len() {
721            let (texture, options) = {
722                let draw = &self.draws[index];
723                (draw.texture, draw.options)
724            };
725            self.build_material(graph, texture, options);
726        }
727    }
728
729    /// Forget every graph node the integration registered, and its bookkeeping.
730    ///
731    /// The ids this drops are the last references the graph holds to the two
732    /// geometry buffers, every texture, view, sampler and material, so the next
733    /// [`ResourceGraph::maintain`] collects them — and, once the material nodes
734    /// go, the views and textures they were the last holders of. No graph is
735    /// needed here: giving a resource up is dropping its id.
736    ///
737    /// After this the integration is back to its freshly built state and may be
738    /// reused against the same graph.
739    pub fn release(&mut self) {
740        self.textures.clear();
741        self.texture_options.clear();
742        self.materials.clear();
743        self.samplers.clear();
744        self.vertex_node = None;
745        self.index_node = None;
746        self.vertices = None;
747        self.indices = None;
748        self.instance = default_instance(&self.device);
749        self.vertex_capacity = 0;
750        self.index_capacity = 0;
751        self.frame_vertices = 0;
752        self.draws.clear();
753    }
754
755    /// Pack `primitives` into the vertex and index buffers, staging the bytes
756    /// through `encoder`, and fill [`Self::draws`] with where each one landed.
757    ///
758    /// The primary and secondary streams go into two regions of the one buffer:
759    /// wgpu binds one stride per slot, and the two streams differ.
760    fn upload_geometry<'a>(
761        &mut self,
762        encoder: &mut wgpu::CommandEncoder,
763        primitives: impl IntoIterator<Item = (Source, &'a egui::ClippedPrimitive)>,
764        screen: ScreenDescriptor,
765        counts: GeometryCounts,
766    ) {
767        // The scratch buffers are taken out of `self` for the duration, so the
768        // draw list can be filled while they are written, then put back: a
769        // steady UI reuses one allocation per stream rather than taking fresh
770        // ones every frame.
771        let mut vertices = core::mem::take(&mut self.packed_vertices);
772        let mut indices = core::mem::take(&mut self.packed_indices);
773        pack_geometry(
774            primitives,
775            screen,
776            &self.texture_options,
777            counts,
778            &mut vertices,
779            &mut indices,
780            &mut self.draws,
781        );
782
783        let device = &self.device;
784        let vertex_buffer = self.vertices.as_ref().expect("just reserved");
785        let index_buffer = self.indices.as_ref().expect("just reserved");
786        self.vertex_staging
787            .write(device, encoder, vertex_buffer, 0, &vertices);
788        self.index_staging
789            .write(device, encoder, index_buffer, 0, &indices);
790        self.packed_vertices = vertices;
791        self.packed_indices = indices;
792    }
793
794    /// The frame most recently uploaded, as a scene.
795    ///
796    /// Every draw sets a scissor rectangle, and a scissor stays set for the
797    /// rest of the pass, so record this after the draws it overlays.
798    ///
799    /// The materials the frame needs were built by [`Self::update`], so this
800    /// only reads the graph — a scene can therefore be taken while the graph is
801    /// shared with whatever else the frame is drawing.
802    ///
803    /// `global` is the caller's global bind group for the camera, the frame
804    /// globals and the view this frame draws through; the UI binds it at
805    /// [`GLOBAL_GROUP`] for every draw.
806    pub fn scene(&self, graph: &ResourceGraph, global: &wgpu::BindGroup) -> Scene {
807        if self.vertices.is_none() || self.indices.is_none() || self.draws.is_empty() {
808            return Scene::new();
809        }
810        let mut scene = Scene::new();
811
812        let (vertices, indices) = (
813            self.vertices.as_ref().expect("checked"),
814            self.indices.as_ref().expect("checked"),
815        );
816        // The position stream is the first half of the buffer, the interleaved
817        // UVs and colors the second, and the two meet at the *frame's* vertex
818        // count — the point `upload_geometry` packed at. The buffer usually has
819        // room to spare, so splitting at `vertex_capacity` instead would bind
820        // the second stream to bytes no vertex was written to.
821        let positions_size = (self.frame_vertices * POSITION_STRIDE) as u64;
822        let secondary_size = (self.frame_vertices * SECONDARY_STRIDE) as u64;
823        let pipeline = &self.pipeline.pipeline;
824        for draw in &self.draws {
825            // Each draw carries its own scissor rectangle, so consecutive
826            // draws sharing one are what the recorder's deduplication is for.
827            let Some(material) = self.material(graph, draw.texture, draw.options) else {
828                // A primitive whose texture was freed this frame has nothing
829                // to sample, so it is skipped rather than bound to a stale view.
830                continue;
831            };
832            // Both slots cover their whole stream, and the primitive's indices
833            // already name its vertices pool-globally, so the draw needs no
834            // base vertex: one range addresses the position, the UV and the
835            // color together.
836            // The trailing tier of the composite: the UI sits above the
837            // depth-sorted pool whatever its depth, and a clipped draw here
838            // is followed by nothing its rectangle could leak into.
839            let entry = DrawEntry::new(pipeline, DrawRange::indexed(draw.indices.clone()))
840                .with_order(DrawOrder::After)
841                .with_bind_group(GLOBAL_GROUP, global)
842                .with_bind_group(MATERIAL_GROUP, material)
843                .with_vertex_buffer(INSTANCE_SLOT, &self.instance)
844                .with_vertex_buffer_range(PRIMARY_SLOT, vertices, 0..positions_size)
845                .with_vertex_buffer_range(
846                    SECONDARY_SLOT,
847                    vertices,
848                    positions_size..positions_size + secondary_size,
849                )
850                .with_index_buffer(indices, wgpu::IndexFormat::Uint32)
851                .with_scissor(draw.scissor);
852            scene.draws.push(entry);
853        }
854        scene
855    }
856
857    /// Reallocate the buffers when they cannot hold `counts`, registering any
858    /// new buffer in `graph`.
859    fn reserve(&mut self, graph: &mut ResourceGraph, counts: GeometryCounts) {
860        let GeometryCounts { vertices, indices } = counts;
861        // Grow geometrically so a UI that grows over a few frames does not
862        // reallocate every frame.
863        if self.vertices.is_none() || self.vertex_capacity < vertices {
864            self.vertex_capacity = vertices
865                .max(self.vertex_capacity + self.vertex_capacity / 2)
866                .max(1);
867            let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
868                label: Some("ui::vertices"),
869                size: (self.vertex_capacity * (POSITION_STRIDE + SECONDARY_STRIDE)) as u64,
870                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
871                mapped_at_creation: false,
872            });
873            self.vertex_node = Some(match self.vertex_node.take() {
874                // A grow replaces the buffer in its existing node, so no stale
875                // node is left strongly holding the old buffer.
876                Some(node) => {
877                    graph
878                        .replace(&node, buffer.clone())
879                        .expect("the vertex node is still registered");
880                    node
881                }
882                None => graph.insert(buffer.clone(), None),
883            });
884            self.vertices = Some(buffer);
885        }
886        if self.indices.is_none() || self.index_capacity < indices {
887            self.index_capacity = indices
888                .max(self.index_capacity + self.index_capacity / 2)
889                .max(1);
890            let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
891                label: Some("ui::indices"),
892                size: (self.index_capacity * size_of::<u32>()) as u64,
893                usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
894                mapped_at_creation: false,
895            });
896            self.index_node = Some(match self.index_node.take() {
897                Some(node) => {
898                    graph
899                        .replace(&node, buffer.clone())
900                        .expect("the index node is still registered");
901                    node
902                }
903                None => graph.insert(buffer.clone(), None),
904            });
905            self.indices = Some(buffer);
906        }
907    }
908
909    /// The material bind group for (`id`, `options`), if it exists.
910    fn material<'a>(
911        &self,
912        graph: &'a ResourceGraph,
913        texture: TextureKey,
914        options: egui::TextureOptions,
915    ) -> Option<&'a wgpu::BindGroup> {
916        let key = MaterialKey { texture, options };
917        let index = self.materials.iter().position(|(seen, _)| *seen == key)?;
918        graph.get(&self.materials[index].1)
919    }
920
921    /// Build the material bind group for (`id`, `options`) if it does not
922    /// exist, registered in the graph as a dependent of its texture's view
923    /// and its sampler.
924    ///
925    /// The group is built once here and rebuilt by
926    /// [`ResourceGraph::maintain`] whenever the view or the sampler is
927    /// replaced: the recipe reads both back out of the graph by id, so a
928    /// texture which patches itself into a new handle is picked up without
929    /// the material being rebuilt by hand.
930    fn build_material(
931        &mut self,
932        graph: &mut ResourceGraph,
933        texture: TextureKey,
934        options: egui::TextureOptions,
935    ) {
936        let key = MaterialKey { texture, options };
937        if self.materials.iter().any(|(seen, _)| *seen == key) {
938            return;
939        }
940        let Some(slot) = self.textures.get(&texture).cloned() else {
941            return;
942        };
943        let sampler_id = self.sampler(graph, options);
944        let layout = self
945            .pipeline
946            .descriptor()
947            .bind_group_layouts(&self.device)
948            .material
949            .expect("the UI variant always samples a base-color texture");
950        let device = self.device.clone();
951        let view_id = slot.view.clone();
952        let sampler_for_build = sampler_id.clone();
953        let build = move |graph: &ResourceGraph| {
954            let view = graph
955                .get(&view_id)
956                .expect("the texture view is in the graph")
957                .view()
958                .clone();
959            let sampler = graph
960                .get(&sampler_for_build)
961                .expect("the sampler is in the graph")
962                .clone();
963            device.create_bind_group(&wgpu::BindGroupDescriptor {
964                label: Some("ui::material"),
965                layout: &layout,
966                entries: &[
967                    wgpu::BindGroupEntry {
968                        binding: BASE_COLOR_TEXTURE_BINDING,
969                        resource: wgpu::BindingResource::TextureView(&view),
970                    },
971                    wgpu::BindGroupEntry {
972                        binding: BASE_COLOR_SAMPLER_BINDING,
973                        resource: wgpu::BindingResource::Sampler(&sampler),
974                    },
975                ],
976            })
977        };
978        let group = build(graph);
979        let rebuild = Rebuild::new(move |graph| Resource::BindGroup(build(graph)));
980        let id = graph.insert(group, Some(rebuild));
981        graph.add_dependency(&id, &slot.view);
982        graph.add_dependency(&id, &sampler_id);
983        self.materials.push((key, id));
984    }
985
986    /// The sampler for `options`, created and registered on first use.
987    fn sampler(
988        &mut self,
989        graph: &mut ResourceGraph,
990        options: egui::TextureOptions,
991    ) -> ResHandle<wgpu::Sampler> {
992        if let Some((_, id)) = self.samplers.iter().find(|(seen, _)| *seen == options) {
993            return id.clone();
994        }
995        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
996            label: Some("ui::sampler"),
997            address_mode_u: address_mode(options.wrap_mode),
998            address_mode_v: address_mode(options.wrap_mode),
999            mag_filter: texture_filter(options.magnification),
1000            min_filter: texture_filter(options.minification),
1001            ..Default::default()
1002        });
1003        let id = graph.insert(sampler, None);
1004        self.samplers.push((options, id.clone()));
1005        id
1006    }
1007
1008    fn apply_textures(
1009        &mut self,
1010        graph: &mut ResourceGraph,
1011        queue: &wgpu::Queue,
1012        source: Source,
1013        delta: &egui::TexturesDelta,
1014    ) {
1015        for (&id, deltas) in &delta.set {
1016            let key = TextureKey { source, id };
1017            for image in deltas {
1018                // Every delta for a texture states its sampling options, and
1019                // the last one wins: they describe the texture as a whole, not
1020                // the patch being written.
1021                self.texture_options.insert(key, image.options);
1022                self.upload_texture(graph, queue, key, image);
1023            }
1024        }
1025        for &id in &delta.free {
1026            // Dropping the slot drops the ids to the texture and its view, so
1027            // the next [`ResourceGraph::maintain`] collects them — and the
1028            // materials that were built from the view, since the view's
1029            // reference count reaches zero with the slot's. The materials'
1030            // own entries go here, before that pass, so egui reusing the
1031            // (texture, options) pair rebuilds a material rather than finding
1032            // a stale one.
1033            //
1034            // The key carries the source: two contexts both free id zero at
1035            // some point, and the other context's texture is not this one's to
1036            // drop.
1037            let key = TextureKey { source, id };
1038            self.textures.remove(&key);
1039            self.texture_options.remove(&key);
1040            self.materials.retain(|(seen, _)| seen.texture != key);
1041        }
1042    }
1043
1044    fn upload_texture(
1045        &mut self,
1046        graph: &mut ResourceGraph,
1047        queue: &wgpu::Queue,
1048        key: TextureKey,
1049        delta: &egui::epaint::ImageDelta,
1050    ) {
1051        let [width, height] = delta.image.size();
1052        // The image's own bytes, borrowed rather than copied into a fresh
1053        // `Vec`: a font atlas can be megabytes, and this is the whole of it.
1054        let pixels: &[u8] = match &delta.image {
1055            egui::ImageData::Color(image) => image.as_raw(),
1056        };
1057        let layout = wgpu::TexelCopyBufferLayout {
1058            offset: 0,
1059            bytes_per_row: Some(width as u32 * texture_bytes()),
1060            rows_per_image: Some(height as u32),
1061        };
1062        let size = wgpu::Extent3d {
1063            width: width as u32,
1064            height: height as u32,
1065            depth_or_array_layers: 1,
1066        };
1067
1068        match delta.pos {
1069            // A patch written into the texture already there.
1070            Some([x, y]) => {
1071                let Some(slot) = self.textures.get(&key) else {
1072                    return;
1073                };
1074                let Some(texture) = graph.get(&slot.texture) else {
1075                    return;
1076                };
1077                queue.write_texture(
1078                    wgpu::TexelCopyTextureInfo {
1079                        texture,
1080                        mip_level: 0,
1081                        origin: wgpu::Origin3d {
1082                            x: x as u32,
1083                            y: y as u32,
1084                            z: 0,
1085                        },
1086                        aspect: wgpu::TextureAspect::All,
1087                    },
1088                    pixels,
1089                    layout,
1090                    size,
1091                );
1092            }
1093            None => {
1094                let texture = self.device.create_texture(&wgpu::TextureDescriptor {
1095                    label: Some("ui::texture"),
1096                    size,
1097                    mip_level_count: 1,
1098                    sample_count: 1,
1099                    dimension: wgpu::TextureDimension::D2,
1100                    format: TEXTURE_FORMAT,
1101                    usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1102                    view_formats: &[],
1103                });
1104                queue.write_texture(texture.as_image_copy(), pixels, layout, size);
1105                let view =
1106                    TextureExt::create_view(&texture, &wgpu::TextureViewDescriptor::default());
1107                let texture_id = graph.insert(texture, None);
1108                let view_id = graph.insert(view, None);
1109                graph.add_dependency(&view_id, &texture_id);
1110                self.textures.insert(
1111                    key,
1112                    TextureSlot {
1113                        texture: texture_id,
1114                        view: view_id,
1115                    },
1116                );
1117            }
1118        }
1119    }
1120}
1121
1122/// Bytes per texel of [`TEXTURE_FORMAT`], derived from the format.
1123fn texture_bytes() -> u32 {
1124    TEXTURE_FORMAT
1125        .block_copy_size(None)
1126        .expect("rgba8 has a known texel size")
1127}
1128
1129fn address_mode(mode: egui::TextureWrapMode) -> wgpu::AddressMode {
1130    match mode {
1131        egui::TextureWrapMode::ClampToEdge => wgpu::AddressMode::ClampToEdge,
1132        egui::TextureWrapMode::Repeat => wgpu::AddressMode::Repeat,
1133        egui::TextureWrapMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat,
1134    }
1135}
1136
1137fn texture_filter(filter: egui::TextureFilter) -> wgpu::FilterMode {
1138    match filter {
1139        egui::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
1140        egui::TextureFilter::Linear => wgpu::FilterMode::Linear,
1141    }
1142}
1143
1144/// The mesh of `primitive`, or `None` for a paint callback.
1145fn mesh_of(primitive: &egui::ClippedPrimitive) -> Option<&egui::epaint::Mesh> {
1146    match &primitive.primitive {
1147        egui::epaint::Primitive::Mesh(mesh) => Some(mesh),
1148        egui::epaint::Primitive::Callback(_) => None,
1149    }
1150}
1151
1152/// egui's clip rectangle as a scissor rectangle, in whole pixels.
1153///
1154/// egui states a clip rectangle in logical points, while a scissor rectangle is
1155/// in physical pixels, so the rectangle is scaled by the density and rounded to
1156/// whole pixels — the same conversion `egui-wgpu` performs. Both edges are then
1157/// clamped to the target: a clip rectangle may extend past it (a panel scrolled
1158/// out of view, or a widget larger than the surface), and a scissor rectangle
1159/// outside the attachment is a validation error rather than a no-op.
1160fn scissor_rect(clip_rect: egui::Rect, screen: ScreenDescriptor) -> ScissorRect {
1161    let ppp = screen.pixels_per_point;
1162    let [width, height] = screen.size_in_pixels;
1163
1164    let min_x = (ppp * clip_rect.min.x).round().clamp(0.0, width as f32) as u32;
1165    let min_y = (ppp * clip_rect.min.y).round().clamp(0.0, height as f32) as u32;
1166    let max_x = (ppp * clip_rect.max.x)
1167        .round()
1168        .clamp(min_x as f32, width as f32) as u32;
1169    let max_y = (ppp * clip_rect.max.y)
1170        .round()
1171        .clamp(min_y as f32, height as f32) as u32;
1172
1173    ScissorRect {
1174        x: min_x,
1175        y: min_y,
1176        width: max_x - min_x,
1177        height: max_y - min_y,
1178    }
1179}
1180
1181#[cfg(test)]
1182mod tests {
1183    use super::*;
1184
1185    /// The UI options for a device-free test. Only the policy these tests
1186    /// assert on matters, so the depth-stencil format is the
1187    /// device-independent placeholder.
1188    fn ui_options_for_tests(srgb_to_linear_output: bool) -> LitOptions {
1189        let mut options = LitOptions::standard();
1190        apply_ui_settings(&mut options, srgb_to_linear_output);
1191        options
1192    }
1193
1194    /// A UI variant over the device-free options, for tests that assert on
1195    /// what a variant derives rather than on a compiled pipeline.
1196    fn ui_variant_for_tests(srgb_to_linear_output: bool) -> LitVariant {
1197        LitVariant {
1198            options: Hashed::new(ui_options_for_tests(srgb_to_linear_output)),
1199            surface: SurfaceKey {
1200                color_format: wgpu::TextureFormat::Rgba8UnormSrgb,
1201                depth_stencil_format: None,
1202                sample_count: 1,
1203            },
1204            channels: ui_channels(),
1205            base_color_texture: true,
1206            morph: false,
1207            strip_index_format: None,
1208        }
1209    }
1210
1211    #[test]
1212    fn ui_variant_is_screen_space_and_uncompressed() {
1213        let variant = ui_variant_for_tests(false);
1214        let channels = variant.channels;
1215        assert_eq!(
1216            channels.position.position,
1217            Some(ChannelEncoding::PlanarPosition),
1218            "screen-space vertices are written at full precision and in the plane"
1219        );
1220        assert!(!channels.position.joints, "a UI mesh is never skinned");
1221        assert_eq!(
1222            channels.secondary,
1223            SecondaryFlags::UV | SecondaryFlags::UNCOMPRESSED_UV | SecondaryFlags::COLOR
1224        );
1225        assert!(variant.base_color_texture);
1226        // Nothing is compressed, so no draw decodes through a metadata index:
1227        // the instance stream carries no per-mesh state the UI would read.
1228        assert!(!variant.needs_metadata());
1229        assert!(!variant.needs_joints());
1230        assert!(!variant.needs_morphs());
1231        // The instance stream is declared for every unlit variant, so a UI
1232        // draw binds the default record.
1233        assert!(variant.vertex_buffer_layouts()[INSTANCE_SLOT as usize].is_some());
1234        let options = &variant.options;
1235        // Overlaid rather than depth-tested: the policy says what a target
1236        // with an attachment would run, and the surface says whether it has
1237        // one.
1238        assert!(!options.depth_stencil.depth_write_enabled);
1239        assert_eq!(
1240            options.depth_stencil.depth_compare,
1241            wgpu::CompareFunction::Always
1242        );
1243        // egui's colors premultiply their own alpha.
1244        let blend = options.blend.expect("the UI variant blends");
1245        assert_eq!(blend.color.src_factor, wgpu::BlendFactor::One);
1246        assert_eq!(blend.color.dst_factor, wgpu::BlendFactor::OneMinusSrcAlpha);
1247    }
1248
1249    /// The conversion flag follows the parameter, since it depends on the
1250    /// target's format, not the UI.
1251    #[test]
1252    fn srgb_flag_follows_the_parameter() {
1253        assert!(!ui_options_for_tests(false).srgb_to_linear_output);
1254        assert!(ui_options_for_tests(true).srgb_to_linear_output);
1255    }
1256
1257    /// The projection maps the viewport's corners onto clip space, with Y
1258    /// flipped: egui's origin is the top left, clip space's the bottom left.
1259    #[test]
1260    fn screen_view_maps_the_viewport_corners() {
1261        let camera = screen_view([256.0, 192.0]);
1262        for (point, expected) in [([0.0, 0.0], [-1.0, 1.0]), ([256.0, 192.0], [1.0, -1.0])] {
1263            let clip = camera.clip_from_world * glam::Vec4::new(point[0], point[1], 0.0, 1.0);
1264            let got = [clip.x / clip.w, clip.y / clip.w];
1265            assert!(
1266                (got[0] - expected[0]).abs() < 1e-5 && (got[1] - expected[1]).abs() < 1e-5,
1267                "point {point:?} projected to {got:?}, expected {expected:?}"
1268            );
1269        }
1270        // The viewport the same points describe is the view bound beside it.
1271        assert_eq!(screen_viewport([256.0, 192.0]), View::new(256.0, 192.0));
1272    }
1273
1274    /// A clip rectangle reaching off the target is clamped rather than wrapping
1275    /// into a huge `u32`, and its far edge never falls below its near edge.
1276    #[test]
1277    fn scissor_rect_clamps_to_the_target() {
1278        let screen = ScreenDescriptor {
1279            size_in_pixels: [64, 48],
1280            pixels_per_point: 1.0,
1281        };
1282        let rect = scissor_rect(
1283            egui::Rect::from_two_pos(egui::Pos2::new(-20.0, -8.0), egui::Pos2::new(200.0, 200.0)),
1284            screen,
1285        );
1286        assert_eq!(rect.x, 0);
1287        assert_eq!(rect.y, 0);
1288        // Both edges are clamped to the target, so a clip rectangle larger
1289        // than it covers exactly the target rather than overrunning it.
1290        assert_eq!(rect.width, 64);
1291        assert_eq!(rect.height, 48);
1292
1293        // A rectangle entirely off the target collapses to nothing instead of
1294        // producing a negative width.
1295        let off = scissor_rect(
1296            egui::Rect::from_two_pos(egui::Pos2::new(-40.0, -40.0), egui::Pos2::new(-10.0, -10.0)),
1297            screen,
1298        );
1299        assert_eq!((off.x, off.y, off.width, off.height), (0, 0, 0, 0));
1300    }
1301
1302    /// A clip rectangle is stated in logical points, so at a density above one
1303    /// it must cover that many physical pixels — the conversion egui-wgpu
1304    /// performs. Getting it wrong clips the UI short of its own bounds.
1305    #[test]
1306    fn scissor_rect_scales_points_to_physical_pixels() {
1307        let screen = ScreenDescriptor {
1308            size_in_pixels: [256, 192],
1309            pixels_per_point: 2.0,
1310        };
1311        // A 16x8-point clip at (8, 4) is 32x16 physical pixels at (16, 8).
1312        let clip = egui::Rect::from_min_size(egui::Pos2::new(8.0, 4.0), egui::Vec2::new(16.0, 8.0));
1313        let rect = scissor_rect(clip, screen);
1314        assert_eq!(
1315            rect,
1316            ScissorRect {
1317                x: 16,
1318                y: 8,
1319                width: 32,
1320                height: 16
1321            }
1322        );
1323
1324        // Where an unscaled conversion would have ended, in pixels: egui's own
1325        // point coordinates taken as pixels. The scaled rectangle extends
1326        // beyond it, so an unscaled clip would have cut that strip away.
1327        let unscaled_end = clip.max.x as u32;
1328        assert!(
1329            unscaled_end < rect.x + rect.width,
1330            "the scaled rectangle must reach past where an unscaled one would \
1331             have ended ({unscaled_end} px), but it ends at {} px",
1332            rect.x + rect.width
1333        );
1334    }
1335
1336    /// Fractional points round to whole pixels rather than truncating, so a
1337    /// clip edge does not drift by up to a pixel.
1338    #[test]
1339    fn scissor_rect_rounds_fractional_points() {
1340        let screen = ScreenDescriptor {
1341            size_in_pixels: [64, 64],
1342            pixels_per_point: 1.0,
1343        };
1344        let rect = scissor_rect(
1345            egui::Rect::from_min_size(egui::Pos2::new(1.4, 2.6), egui::Vec2::new(4.0, 4.0)),
1346            screen,
1347        );
1348        assert_eq!((rect.x, rect.y), (1, 3));
1349
1350        let rounded_up = scissor_rect(
1351            egui::Rect::from_min_size(egui::Pos2::new(1.6, 2.4), egui::Vec2::new(4.0, 4.0)),
1352            screen,
1353        );
1354        assert_eq!((rounded_up.x, rounded_up.y), (2, 2));
1355    }
1356
1357    /// The point size the projection takes is the physical size divided by the
1358    /// density — the opposite conversion from the scissor rectangle's.
1359    #[test]
1360    fn size_in_points_divides_by_the_density() {
1361        let screen = ScreenDescriptor {
1362            size_in_pixels: [256, 192],
1363            pixels_per_point: 2.0,
1364        };
1365        assert_eq!(screen.size_in_points(), [128.0, 96.0]);
1366    }
1367
1368    /// A screen descriptor matching the default test target.
1369    fn test_screen() -> ScreenDescriptor {
1370        ScreenDescriptor {
1371            size_in_pixels: [256, 192],
1372            pixels_per_point: 1.0,
1373        }
1374    }
1375
1376    /// Tag `primitives` with the local source: these tests exercise packing,
1377    /// not the namespaces a frame arrives from.
1378    fn local(primitives: &[egui::ClippedPrimitive]) -> Vec<(Source, &egui::ClippedPrimitive)> {
1379        primitives
1380            .iter()
1381            .map(|primitive| (Source::LOCAL, primitive))
1382            .collect()
1383    }
1384
1385    /// One tessellated primitive carrying `vertices` points, clipped to the
1386    /// whole screen.
1387    fn primitive(texture: egui::TextureId, vertices: usize) -> egui::ClippedPrimitive {
1388        let mut mesh = egui::epaint::Mesh::with_texture(texture);
1389        for index in 0..vertices {
1390            mesh.vertices.push(egui::epaint::Vertex {
1391                pos: egui::pos2(index as f32, index as f32 * 2.0),
1392                uv: egui::pos2(0.25, 0.75),
1393                color: egui::Color32::from_rgba_premultiplied(1, 2, 3, 4),
1394            });
1395        }
1396        // A degenerate triangle per vertex, enough to exercise the index path.
1397        for index in 0..vertices.saturating_sub(2) {
1398            mesh.indices
1399                .extend([index as u32, index as u32 + 1, index as u32 + 2]);
1400        }
1401        egui::ClippedPrimitive {
1402            clip_rect: egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(256.0, 192.0)),
1403            primitive: egui::epaint::Primitive::Mesh(mesh),
1404        }
1405    }
1406
1407    /// The packing lands each primitive's bytes where its draw says, and the
1408    /// two vertex streams hold the values egui put in.
1409    #[test]
1410    fn packing_places_each_primitive_where_its_draw_says() {
1411        let primitives = [primitive(egui::TextureId::default(), 4)];
1412        let mut vertices = Vec::new();
1413        let mut indices = Vec::new();
1414        let mut draws = Vec::new();
1415        let tagged = local(&primitives);
1416        let counts = measure(tagged.iter().copied());
1417        pack_geometry(
1418            tagged.iter().copied(),
1419            test_screen(),
1420            &HashMap::new(),
1421            counts,
1422            &mut vertices,
1423            &mut indices,
1424            &mut draws,
1425        );
1426
1427        assert_eq!(draws.len(), 1, "one mesh is one draw");
1428        let draw = &draws[0];
1429        assert_eq!(draw.indices, 0..6, "four vertices make two triangles");
1430        // This primitive starts at the pool's first vertex, so baking its
1431        // offset in leaves the indices as egui wrote them.
1432        assert_eq!(
1433            packed_indices(&indices),
1434            vec![0, 1, 2, 1, 2, 3],
1435            "the indices address the pool without an offset"
1436        );
1437
1438        // The vertex buffer holds the primary stream then the interleaved
1439        // secondary stream, each entry one stride wide.
1440        let secondary_start = 4 * POSITION_STRIDE;
1441        assert_eq!(vertices.len(), secondary_start + 4 * SECONDARY_STRIDE);
1442        let positions: Vec<f32> = vertices[..secondary_start]
1443            .as_chunks::<{ size_of::<f32>() }>()
1444            .0
1445            .iter()
1446            .map(|word| f32::from_le_bytes(*word))
1447            .collect();
1448        assert_eq!(
1449            positions,
1450            vec![0.0, 0.0, 1.0, 2.0, 2.0, 4.0, 3.0, 6.0],
1451            "each position is x and y; the shader supplies the zero z"
1452        );
1453
1454        // The colors are egui's premultiplied bytes, unchanged.
1455        let first_color = secondary_start + size_of::<egui::Vec2>();
1456        assert_eq!(
1457            &vertices[first_color..first_color + size_of::<egui::Color32>()],
1458            &[1, 2, 3, 4]
1459        );
1460
1461        // The indices are this primitive's, starting at zero for the first.
1462        let indices: Vec<u32> = indices
1463            .as_chunks::<{ size_of::<u32>() }>()
1464            .0
1465            .iter()
1466            .map(|word| u32::from_le_bytes(*word))
1467            .collect();
1468        assert_eq!(indices, vec![0, 1, 2, 1, 2, 3]);
1469    }
1470
1471    /// A second primitive's indices are offset by the first's vertex count, so
1472    /// one base vertex addresses both streams.
1473    #[test]
1474    fn a_later_primitive_is_offset_by_the_earlier_one() {
1475        let primitives = [
1476            primitive(egui::TextureId::default(), 3),
1477            primitive(egui::TextureId::default(), 3),
1478        ];
1479        let mut vertices = Vec::new();
1480        let mut indices = Vec::new();
1481        let mut draws = Vec::new();
1482        let tagged = local(&primitives);
1483        let counts = measure(tagged.iter().copied());
1484        pack_geometry(
1485            tagged.iter().copied(),
1486            test_screen(),
1487            &HashMap::new(),
1488            counts,
1489            &mut vertices,
1490            &mut indices,
1491            &mut draws,
1492        );
1493
1494        assert_eq!(draws.len(), 2);
1495        assert_eq!(draws[0].indices, 0..3);
1496        assert_eq!(draws[1].indices, 3..6);
1497        // The second primitive's vertices start at ordinal 3, so its local
1498        // indices are shifted by 3 — the offset a device without `base_vertex`
1499        // could not take from the draw.
1500        assert_eq!(
1501            packed_indices(&indices),
1502            vec![0, 1, 2, 3, 4, 5],
1503            "the second primitive's indices are offset to the pool"
1504        );
1505    }
1506
1507    /// The indices `pack_geometry` wrote, as `u32`s.
1508    fn packed_indices(bytes: &[u8]) -> Vec<u32> {
1509        bytes
1510            .as_chunks::<{ size_of::<u32>() }>()
1511            .0
1512            .iter()
1513            .map(|word| u32::from_le_bytes(*word))
1514            .collect()
1515    }
1516
1517    /// The byte buffers are cleared and refilled, not reallocated, so a caller
1518    /// that keeps them uploads without allocating.
1519    #[test]
1520    fn packing_reuses_the_vertex_and_index_buffers() {
1521        let big = [primitive(egui::TextureId::default(), 8)];
1522        let small = [primitive(egui::TextureId::default(), 3)];
1523
1524        let mut vertices = Vec::new();
1525        let mut indices = Vec::new();
1526        let mut draws = Vec::new();
1527        let big_tagged = local(&big);
1528        let big_counts = measure(big_tagged.iter().copied());
1529        pack_geometry(
1530            big_tagged.iter().copied(),
1531            test_screen(),
1532            &HashMap::new(),
1533            big_counts,
1534            &mut vertices,
1535            &mut indices,
1536            &mut draws,
1537        );
1538        let vertex_capacity = vertices.capacity();
1539        let index_capacity = indices.capacity();
1540
1541        // A later, smaller frame keeps both allocations rather than taking new
1542        // ones; a fresh `Vec` per frame would have exactly its own length.
1543        let small_tagged = local(&small);
1544        let small_counts = measure(small_tagged.iter().copied());
1545        pack_geometry(
1546            small_tagged.iter().copied(),
1547            test_screen(),
1548            &HashMap::new(),
1549            small_counts,
1550            &mut vertices,
1551            &mut indices,
1552            &mut draws,
1553        );
1554        assert_eq!(
1555            vertices.capacity(),
1556            vertex_capacity,
1557            "the vertex scratch buffer is reused"
1558        );
1559        assert_eq!(
1560            indices.capacity(),
1561            index_capacity,
1562            "the index scratch buffer is reused"
1563        );
1564        assert_eq!(draws.len(), 1, "and the stale second draw is gone");
1565    }
1566}