Skip to main content

egui_wgpu/
renderer.rs

1use core::{num::NonZeroU64, ops::Range};
2use std::borrow::Cow;
3
4use ahash::HashMap;
5use bytemuck::Zeroable as _;
6use epaint::{PaintCallbackInfo, Primitive, Vertex, emath::NumExt as _};
7
8use wgpu::util::DeviceExt as _;
9
10// Only implements Send + Sync on wasm32 in order to allow storing wgpu resources on the type map.
11#[cfg(not(all(
12    target_arch = "wasm32",
13    not(feature = "fragile-send-sync-non-atomic-wasm"),
14)))]
15/// You can use this for storage when implementing [`CallbackTrait`].
16pub type CallbackResources = type_map::concurrent::TypeMap;
17#[cfg(all(
18    target_arch = "wasm32",
19    not(feature = "fragile-send-sync-non-atomic-wasm"),
20))]
21/// You can use this for storage when implementing [`CallbackTrait`].
22pub type CallbackResources = type_map::TypeMap;
23
24/// You can use this to do custom [`wgpu`] rendering in an egui app.
25///
26/// Implement [`CallbackTrait`] and call [`Callback::new_paint_callback`].
27///
28/// This can be turned into a [`epaint::PaintCallback`] and [`epaint::Shape`].
29pub struct Callback(Box<dyn CallbackTrait>);
30
31impl Callback {
32    /// Creates a new [`epaint::PaintCallback`] from a callback trait instance.
33    pub fn new_paint_callback(
34        rect: epaint::emath::Rect,
35        callback: impl CallbackTrait + 'static,
36    ) -> epaint::PaintCallback {
37        epaint::PaintCallback {
38            rect,
39            callback: std::sync::Arc::new(Self(Box::new(callback))),
40        }
41    }
42}
43
44/// A callback trait that can be used to compose an [`epaint::PaintCallback`] via [`Callback`]
45/// for custom WGPU rendering.
46///
47/// Callbacks in [`Renderer`] are done in three steps:
48/// * [`CallbackTrait::prepare`]: called for all registered callbacks before the main egui render pass.
49/// * [`CallbackTrait::finish_prepare`]: called for all registered callbacks after all callbacks finished calling prepare.
50/// * [`CallbackTrait::paint`]: called for all registered callbacks during the main egui render pass.
51///
52/// Each callback has access to an instance of [`CallbackResources`] that is stored in the [`Renderer`].
53/// This can be used to store wgpu resources that need to be accessed during the [`CallbackTrait::paint`] step.
54///
55/// The callbacks implementing [`CallbackTrait`] itself must always be Send + Sync, but resources stored in
56/// [`Renderer::callback_resources`] are not required to implement Send + Sync when building for wasm.
57/// (this is because wgpu stores references to the JS heap in most of its resources which can not be shared with other threads).
58///
59///
60/// # Command submission
61///
62/// ## Command Encoder
63///
64/// The passed-in [`wgpu::CommandEncoder`] is egui's and can be used directly to register
65/// wgpu commands for simple use cases.
66/// This allows reusing the same [`wgpu::CommandEncoder`] for all callbacks and egui
67/// rendering itself.
68///
69/// ## Command Buffers
70///
71/// For more complicated use cases, one can also return a list of arbitrary
72/// [`wgpu::CommandBuffer`]s and have complete control over how they get created and fed.
73/// In particular, this gives an opportunity to parallelize command registration and
74/// prevents a faulty callback from poisoning the main wgpu pipeline.
75///
76/// When using eframe, the main egui command buffer, as well as all user-defined
77/// command buffers returned by this function, are guaranteed to all be submitted
78/// at once in a single call.
79///
80/// Command Buffers returned by [`CallbackTrait::finish_prepare`] will always be issued *after*
81/// those returned by [`CallbackTrait::prepare`].
82/// Order within command buffers returned by [`CallbackTrait::prepare`] is dependent
83/// on the order the respective [`epaint::Shape::Callback`]s were submitted in.
84///
85/// # Example
86///
87/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/main/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
88pub trait CallbackTrait: Send + Sync {
89    fn prepare(
90        &self,
91        _device: &wgpu::Device,
92        _queue: &wgpu::Queue,
93        _screen_descriptor: &ScreenDescriptor,
94        _egui_encoder: &mut wgpu::CommandEncoder,
95        _callback_resources: &mut CallbackResources,
96    ) -> Vec<wgpu::CommandBuffer> {
97        Vec::new()
98    }
99
100    /// Called after all [`CallbackTrait::prepare`] calls are done.
101    fn finish_prepare(
102        &self,
103        _device: &wgpu::Device,
104        _queue: &wgpu::Queue,
105        _egui_encoder: &mut wgpu::CommandEncoder,
106        _callback_resources: &mut CallbackResources,
107    ) -> Vec<wgpu::CommandBuffer> {
108        Vec::new()
109    }
110
111    /// Called after all [`CallbackTrait::finish_prepare`] calls are done.
112    ///
113    /// It is given access to the [`wgpu::RenderPass`] so that it can issue draw commands
114    /// into the same [`wgpu::RenderPass`] that is used for all other egui elements.
115    fn paint(
116        &self,
117        info: PaintCallbackInfo,
118        render_pass: &mut wgpu::RenderPass<'static>,
119        callback_resources: &CallbackResources,
120    );
121}
122
123/// Information about the screen used for rendering.
124pub struct ScreenDescriptor {
125    /// Size of the window in physical pixels.
126    pub size_in_pixels: [u32; 2],
127
128    /// High-DPI scale factor (pixels per point).
129    pub pixels_per_point: f32,
130}
131
132impl ScreenDescriptor {
133    /// size in "logical" points
134    fn screen_size_in_points(&self) -> [f32; 2] {
135        [
136            self.size_in_pixels[0] as f32 / self.pixels_per_point,
137            self.size_in_pixels[1] as f32 / self.pixels_per_point,
138        ]
139    }
140}
141
142/// Uniform buffer used when rendering.
143#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
144#[repr(C)]
145struct UniformBuffer {
146    screen_size_in_points: [f32; 2],
147    dithering: u32,
148
149    /// 1 to do manual filtering for more predictable kittest snapshot images.
150    ///
151    /// See also <https://github.com/emilk/egui/issues/5295>.
152    predictable_texture_filtering: u32,
153}
154
155struct SlicedBuffer {
156    buffer: wgpu::Buffer,
157    slices: Vec<Range<usize>>,
158    capacity: wgpu::BufferAddress,
159}
160
161pub struct Texture {
162    /// The texture may be None if the `TextureId` is just a handle to a user-provided bind-group.
163    pub texture: Option<wgpu::Texture>,
164
165    /// Bindgroup for the texture + sampler.
166    pub bind_group: wgpu::BindGroup,
167
168    /// Options describing the sampler used in the bind group. This may be None if the `TextureId`
169    /// is just a handle to a user-provided bind-group.
170    pub options: Option<epaint::textures::TextureOptions>,
171}
172
173/// Ways to configure [`Renderer`] during creation.
174#[derive(Clone, Copy, Debug)]
175pub struct RendererOptions {
176    /// Set the level of the multisampling anti-aliasing (MSAA).
177    ///
178    /// Must be a power-of-two. Higher = more smooth 3D.
179    ///
180    /// A value of `0` or `1` turns it off (default).
181    ///
182    /// `egui` already performs anti-aliasing via "feathering"
183    /// (controlled by [`egui::epaint::TessellationOptions`]),
184    /// but if you are embedding 3D in egui you may want to turn on multisampling.
185    pub msaa_samples: u32,
186
187    /// What format to use for the depth and stencil buffers,
188    /// e.g. [`wgpu::TextureFormat::Depth32FloatStencil8`].
189    ///
190    /// egui doesn't need depth/stencil, so the default value is `None` (no depth or stancil buffers).
191    pub depth_stencil_format: Option<wgpu::TextureFormat>,
192
193    /// Controls whether to apply dithering to minimize banding artifacts.
194    ///
195    /// Dithering assumes an sRGB output and thus will apply noise to any input value that lies between
196    /// two 8bit values after applying the sRGB OETF function, i.e. if it's not a whole 8bit value in "gamma space".
197    /// This means that only inputs from texture interpolation and vertex colors should be affected in practice.
198    ///
199    /// Defaults to true.
200    pub dithering: bool,
201
202    /// Perform texture filtering in software?
203    ///
204    /// This is useful when you want predictable rendering across
205    /// different hardware, e.g. for kittest snapshots.
206    ///
207    /// Default is `false`.
208    ///
209    /// See also <https://github.com/emilk/egui/issues/5295>.
210    pub predictable_texture_filtering: bool,
211}
212
213impl RendererOptions {
214    /// Set options that produce the most predicatable output.
215    ///
216    /// Useful for image snapshot tests.
217    pub const PREDICTABLE: Self = Self {
218        msaa_samples: 1,
219        depth_stencil_format: None,
220        dithering: false,
221        predictable_texture_filtering: true,
222    };
223}
224
225impl Default for RendererOptions {
226    fn default() -> Self {
227        Self {
228            msaa_samples: 0,
229            depth_stencil_format: None,
230            dithering: true,
231            predictable_texture_filtering: false,
232        }
233    }
234}
235
236/// Renderer for a egui based GUI.
237pub struct Renderer {
238    pipeline: wgpu::RenderPipeline,
239
240    index_buffer: SlicedBuffer,
241    vertex_buffer: SlicedBuffer,
242
243    uniform_buffer: wgpu::Buffer,
244    previous_uniform_buffer_content: UniformBuffer,
245    uniform_bind_group: wgpu::BindGroup,
246    texture_bind_group_layout: wgpu::BindGroupLayout,
247
248    /// Uniform buffers each holding a single `u32` of texture flags
249    /// (see [`texture_flags`]), indexed by that flag value.
250    /// Read by the shader when `predictable_texture_filtering` is on.
251    texture_flag_buffers: [wgpu::Buffer; NUM_TEXTURE_FLAGS],
252
253    /// Map of egui texture IDs to textures and their associated bindgroups (texture view +
254    /// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided
255    /// sampler.
256    textures: HashMap<epaint::TextureId, Texture>,
257    next_user_texture_id: u64,
258    samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,
259
260    options: RendererOptions,
261
262    /// Storage for resources shared with all invocations of [`CallbackTrait`]'s methods.
263    ///
264    /// See also [`CallbackTrait`].
265    pub callback_resources: CallbackResources,
266}
267
268impl Renderer {
269    /// Creates a renderer for a egui UI.
270    ///
271    /// `output_color_format` should preferably be [`wgpu::TextureFormat::Rgba8Unorm`] or
272    /// [`wgpu::TextureFormat::Bgra8Unorm`], i.e. in gamma-space.
273    pub fn new(
274        device: &wgpu::Device,
275        output_color_format: wgpu::TextureFormat,
276        options: RendererOptions,
277    ) -> Self {
278        profiling::function_scope!();
279
280        let shader = wgpu::ShaderModuleDescriptor {
281            label: Some("egui"),
282            source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("egui.wgsl"))),
283        };
284        let module = {
285            profiling::scope!("create_shader_module");
286            device.create_shader_module(shader)
287        };
288
289        let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
290            label: Some("egui_uniform_buffer"),
291            contents: bytemuck::cast_slice(&[UniformBuffer {
292                screen_size_in_points: [0.0, 0.0],
293                dithering: u32::from(options.dithering),
294                predictable_texture_filtering: u32::from(options.predictable_texture_filtering),
295            }]),
296            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
297        });
298
299        let uniform_bind_group_layout = {
300            profiling::scope!("create_bind_group_layout");
301            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
302                label: Some("egui_uniform_bind_group_layout"),
303                entries: &[wgpu::BindGroupLayoutEntry {
304                    binding: 0,
305                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
306                    ty: wgpu::BindingType::Buffer {
307                        has_dynamic_offset: false,
308                        min_binding_size: NonZeroU64::new(
309                            core::mem::size_of::<UniformBuffer>() as _
310                        ),
311                        ty: wgpu::BufferBindingType::Uniform,
312                    },
313                    count: None,
314                }],
315            })
316        };
317
318        let uniform_bind_group = {
319            profiling::scope!("create_bind_group");
320            device.create_bind_group(&wgpu::BindGroupDescriptor {
321                label: Some("egui_uniform_bind_group"),
322                layout: &uniform_bind_group_layout,
323                entries: &[wgpu::BindGroupEntry {
324                    binding: 0,
325                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
326                        buffer: &uniform_buffer,
327                        offset: 0,
328                        size: None,
329                    }),
330                }],
331            })
332        };
333
334        let texture_bind_group_layout = {
335            profiling::scope!("create_bind_group_layout");
336            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
337                label: Some("egui_texture_bind_group_layout"),
338                entries: &[
339                    wgpu::BindGroupLayoutEntry {
340                        binding: 0,
341                        visibility: wgpu::ShaderStages::FRAGMENT,
342                        ty: wgpu::BindingType::Texture {
343                            multisampled: false,
344                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
345                            view_dimension: wgpu::TextureViewDimension::D2,
346                        },
347                        count: None,
348                    },
349                    wgpu::BindGroupLayoutEntry {
350                        binding: 1,
351                        visibility: wgpu::ShaderStages::FRAGMENT,
352                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
353                        count: None,
354                    },
355                    wgpu::BindGroupLayoutEntry {
356                        binding: 2,
357                        visibility: wgpu::ShaderStages::FRAGMENT,
358                        ty: wgpu::BindingType::Buffer {
359                            has_dynamic_offset: false,
360                            min_binding_size: NonZeroU64::new(
361                                (core::mem::size_of::<u32>() * 4) as _,
362                            ),
363                            ty: wgpu::BufferBindingType::Uniform,
364                        },
365                        count: None,
366                    },
367                ],
368            })
369        };
370
371        let texture_flag_buffers = core::array::from_fn::<_, NUM_TEXTURE_FLAGS, _>(|flag| {
372            let flag = flag as u32;
373            device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
374                label: Some(&format!("egui_texture_flags_{flag}")),
375                contents: bytemuck::bytes_of(&[flag, 0, 0, 0]),
376                usage: wgpu::BufferUsages::UNIFORM,
377            })
378        });
379
380        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
381            label: Some("egui_pipeline_layout"),
382            bind_group_layouts: &[
383                Some(&uniform_bind_group_layout),
384                Some(&texture_bind_group_layout),
385            ],
386            immediate_size: 0,
387        });
388
389        let depth_stencil = options
390            .depth_stencil_format
391            .map(|format| wgpu::DepthStencilState {
392                format,
393                depth_write_enabled: Some(false),
394                depth_compare: Some(wgpu::CompareFunction::Always),
395                stencil: wgpu::StencilState::default(),
396                bias: wgpu::DepthBiasState::default(),
397            });
398
399        let pipeline = {
400            profiling::scope!("create_render_pipeline");
401            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
402                label: Some("egui_pipeline"),
403                layout: Some(&pipeline_layout),
404                vertex: wgpu::VertexState {
405                    entry_point: Some("vs_main"),
406                    module: &module,
407                    buffers: &[Some(wgpu::VertexBufferLayout {
408                        array_stride: 5 * 4,
409                        step_mode: wgpu::VertexStepMode::Vertex,
410                        // 0: vec2 position
411                        // 1: vec2 texture coordinates
412                        // 2: uint color
413                        attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32],
414                    })],
415                    compilation_options: wgpu::PipelineCompilationOptions::default()
416                },
417                primitive: wgpu::PrimitiveState {
418                    topology: wgpu::PrimitiveTopology::TriangleList,
419                    unclipped_depth: false,
420                    conservative: false,
421                    cull_mode: None,
422                    front_face: wgpu::FrontFace::default(),
423                    polygon_mode: wgpu::PolygonMode::default(),
424                    strip_index_format: None,
425                },
426                depth_stencil,
427                multisample: wgpu::MultisampleState {
428                    alpha_to_coverage_enabled: false,
429                    count: options.msaa_samples.max(1),
430                    mask: !0,
431                },
432
433                fragment: Some(wgpu::FragmentState {
434                    module: &module,
435                    entry_point: Some(if output_color_format.is_srgb() {
436                        log::warn!("Detected a linear (sRGBA aware) framebuffer {output_color_format:?}. egui prefers Rgba8Unorm or Bgra8Unorm");
437                        "fs_main_linear_framebuffer"
438                    } else {
439                        "fs_main_gamma_framebuffer" // this is what we prefer
440                    }),
441                    targets: &[Some(wgpu::ColorTargetState {
442                        format: output_color_format,
443                        blend: Some(wgpu::BlendState {
444                            color: wgpu::BlendComponent {
445                                src_factor: wgpu::BlendFactor::One,
446                                dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
447                                operation: wgpu::BlendOperation::Add,
448                            },
449                            alpha: wgpu::BlendComponent {
450                                src_factor: wgpu::BlendFactor::OneMinusDstAlpha,
451                                dst_factor: wgpu::BlendFactor::One,
452                                operation: wgpu::BlendOperation::Add,
453                            },
454                        }),
455                        write_mask: wgpu::ColorWrites::ALL,
456                    })],
457                    compilation_options: wgpu::PipelineCompilationOptions::default()
458                }),
459                multiview_mask: None,
460                cache: None,
461            }
462        )
463        };
464
465        const VERTEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
466            (core::mem::size_of::<Vertex>() * 1024) as _;
467        const INDEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
468            (core::mem::size_of::<u32>() * 1024 * 3) as _;
469
470        Self {
471            pipeline,
472            vertex_buffer: SlicedBuffer {
473                buffer: create_vertex_buffer(device, VERTEX_BUFFER_START_CAPACITY),
474                slices: Vec::with_capacity(64),
475                capacity: VERTEX_BUFFER_START_CAPACITY,
476            },
477            index_buffer: SlicedBuffer {
478                buffer: create_index_buffer(device, INDEX_BUFFER_START_CAPACITY),
479                slices: Vec::with_capacity(64),
480                capacity: INDEX_BUFFER_START_CAPACITY,
481            },
482            uniform_buffer,
483            // Buffers on wgpu are zero initialized, so this is indeed its current state!
484            previous_uniform_buffer_content: UniformBuffer::zeroed(),
485            uniform_bind_group,
486            texture_bind_group_layout,
487            texture_flag_buffers,
488            textures: HashMap::default(),
489            next_user_texture_id: 0,
490            samplers: HashMap::default(),
491            options,
492            callback_resources: CallbackResources::default(),
493        }
494    }
495
496    /// Executes the egui renderer onto an existing wgpu renderpass.
497    ///
498    /// Note that the lifetime of `render_pass` is `'static` which requires a call to [`wgpu::RenderPass::forget_lifetime`].
499    /// This allows users to pass resources that live outside of the callback resources to the render pass.
500    /// The render pass internally keeps all referenced resources alive as long as necessary.
501    /// The only consequence of `forget_lifetime` is that any operation on the parent encoder will cause a runtime error
502    /// instead of a compile time error.
503    ///
504    /// # Panic
505    /// Always ensure that [`Renderer::update_buffers`] has been called otherwise calling [`Renderer::render`] will panic!
506    pub fn render(
507        &self,
508        render_pass: &mut wgpu::RenderPass<'static>,
509        paint_jobs: &[epaint::ClippedPrimitive],
510        screen_descriptor: &ScreenDescriptor,
511    ) {
512        profiling::function_scope!();
513
514        let pixels_per_point = screen_descriptor.pixels_per_point;
515        let size_in_pixels = screen_descriptor.size_in_pixels;
516
517        // Whether or not we need to reset the render pass because a paint callback has just
518        // run.
519        let mut needs_reset = true;
520
521        let mut index_buffer_slices = self.index_buffer.slices.iter();
522        let mut vertex_buffer_slices = self.vertex_buffer.slices.iter();
523
524        for epaint::ClippedPrimitive {
525            clip_rect,
526            primitive,
527        } in paint_jobs
528        {
529            if needs_reset {
530                render_pass.set_viewport(
531                    0.0,
532                    0.0,
533                    size_in_pixels[0] as f32,
534                    size_in_pixels[1] as f32,
535                    0.0,
536                    1.0,
537                );
538                render_pass.set_pipeline(&self.pipeline);
539                render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
540                needs_reset = false;
541            }
542
543            {
544                let rect = ScissorRect::new(clip_rect, pixels_per_point, size_in_pixels);
545
546                if rect.width == 0 || rect.height == 0 {
547                    // Skip rendering zero-sized clip areas.
548                    if let Primitive::Mesh(_) = primitive {
549                        // If this is a mesh, we need to advance the index and vertex buffer iterators:
550                        index_buffer_slices
551                            .next()
552                            .expect("You must call .update_buffers() before .render()");
553                        vertex_buffer_slices
554                            .next()
555                            .expect("You must call .update_buffers() before .render()");
556                    }
557                    continue;
558                }
559
560                render_pass.set_scissor_rect(rect.x, rect.y, rect.width, rect.height);
561            }
562
563            match primitive {
564                Primitive::Mesh(mesh) => {
565                    let index_buffer_slice = index_buffer_slices
566                        .next()
567                        .expect("You must call .update_buffers() before .render()");
568                    let vertex_buffer_slice = vertex_buffer_slices
569                        .next()
570                        .expect("You must call .update_buffers() before .render()");
571
572                    if let Some(Texture { bind_group, .. }) = self.textures.get(&mesh.texture_id) {
573                        render_pass.set_bind_group(1, bind_group, &[]);
574                        render_pass.set_index_buffer(
575                            self.index_buffer.buffer.slice(
576                                index_buffer_slice.start as u64..index_buffer_slice.end as u64,
577                            ),
578                            wgpu::IndexFormat::Uint32,
579                        );
580                        render_pass.set_vertex_buffer(
581                            0,
582                            self.vertex_buffer.buffer.slice(
583                                vertex_buffer_slice.start as u64..vertex_buffer_slice.end as u64,
584                            ),
585                        );
586                        render_pass.draw_indexed(0..mesh.indices.len() as u32, 0, 0..1);
587                    } else {
588                        log::warn!("Missing texture: {:?}", mesh.texture_id);
589                    }
590                }
591                Primitive::Callback(callback) => {
592                    let Some(cbfn) = callback.callback.downcast_ref::<Callback>() else {
593                        // We already warned in the `prepare` callback
594                        continue;
595                    };
596
597                    let info = PaintCallbackInfo {
598                        viewport: callback.rect,
599                        clip_rect: *clip_rect,
600                        pixels_per_point,
601                        screen_size_px: size_in_pixels,
602                    };
603
604                    let viewport_px = info.viewport_in_pixels();
605                    if viewport_px.width_px > 0 && viewport_px.height_px > 0 {
606                        profiling::scope!("callback");
607
608                        needs_reset = true;
609
610                        // We're setting a default viewport for the render pass as a
611                        // courtesy for the user, so that they don't have to think about
612                        // it in the simple case where they just want to fill the whole
613                        // paint area.
614                        //
615                        // The user still has the possibility of setting their own custom
616                        // viewport during the paint callback, effectively overriding this
617                        // one.
618                        render_pass.set_viewport(
619                            viewport_px.left_px as f32,
620                            viewport_px.top_px as f32,
621                            viewport_px.width_px as f32,
622                            viewport_px.height_px as f32,
623                            0.0,
624                            1.0,
625                        );
626
627                        cbfn.0.paint(info, render_pass, &self.callback_resources);
628                    }
629                }
630            }
631        }
632
633        render_pass.set_scissor_rect(0, 0, size_in_pixels[0], size_in_pixels[1]);
634    }
635
636    /// Should be called before [`Self::render`].
637    pub fn update_texture(
638        &mut self,
639        device: &wgpu::Device,
640        queue: &wgpu::Queue,
641        id: epaint::TextureId,
642        image_delta: &epaint::ImageDelta,
643    ) {
644        profiling::function_scope!();
645
646        let width = image_delta.image.width() as u32;
647        let height = image_delta.image.height() as u32;
648
649        let size = wgpu::Extent3d {
650            width,
651            height,
652            depth_or_array_layers: 1,
653        };
654
655        let data_color32 = match &image_delta.image {
656            epaint::ImageData::Color(image) => {
657                assert_eq!(
658                    width as usize * height as usize,
659                    image.pixels.len(),
660                    "Mismatch between texture size and texel count"
661                );
662                Cow::Borrowed(&image.pixels)
663            }
664        };
665        let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());
666
667        let queue_write_data_to_texture = |texture, origin| {
668            profiling::scope!("write_texture");
669            queue.write_texture(
670                wgpu::TexelCopyTextureInfo {
671                    texture,
672                    mip_level: 0,
673                    origin,
674                    aspect: wgpu::TextureAspect::All,
675                },
676                data_bytes,
677                wgpu::TexelCopyBufferLayout {
678                    offset: 0,
679                    bytes_per_row: Some(4 * width),
680                    rows_per_image: Some(height),
681                },
682                size,
683            );
684        };
685
686        // Use same label for all resources associated with this texture id (no point in retyping the type)
687        let label_str = format!("egui_texid_{id:?}");
688        let label = Some(label_str.as_str());
689
690        let (texture, origin, bind_group) = if let Some(pos) = image_delta.pos {
691            // update the existing texture
692            let Texture {
693                texture,
694                bind_group,
695                options,
696            } = self
697                .textures
698                .remove(&id)
699                .expect("Tried to update a texture that has not been allocated yet.");
700            let texture = texture.expect("Tried to update user texture.");
701            let options = options.expect("Tried to update user texture.");
702            let origin = wgpu::Origin3d {
703                x: pos[0] as u32,
704                y: pos[1] as u32,
705                z: 0,
706            };
707
708            (
709                texture,
710                origin,
711                // If the TextureOptions are the same as the previous ones, we can reuse the bind group. Otherwise we
712                // have to recreate it.
713                if image_delta.options == options {
714                    Some(bind_group)
715                } else {
716                    None
717                },
718            )
719        } else {
720            // allocate a new texture
721            let texture = {
722                profiling::scope!("create_texture");
723                device.create_texture(&wgpu::TextureDescriptor {
724                    label,
725                    size,
726                    mip_level_count: 1,
727                    sample_count: 1,
728                    dimension: wgpu::TextureDimension::D2,
729                    format: wgpu::TextureFormat::Rgba8Unorm,
730                    usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
731                    view_formats: &[wgpu::TextureFormat::Rgba8Unorm],
732                })
733            };
734            let origin = wgpu::Origin3d::ZERO;
735            (texture, origin, None)
736        };
737
738        let bind_group = bind_group.unwrap_or_else(|| {
739            let nearest =
740                image_delta.options.magnification == epaint::textures::TextureFilter::Nearest;
741            let wrap_mode = wrap_mode_flag(image_delta.options.wrap_mode);
742            let sampler = self
743                .samplers
744                .entry(image_delta.options)
745                .or_insert_with(|| create_sampler(image_delta.options, device));
746            device.create_bind_group(&wgpu::BindGroupDescriptor {
747                label,
748                layout: &self.texture_bind_group_layout,
749                entries: &[
750                    wgpu::BindGroupEntry {
751                        binding: 0,
752                        resource: wgpu::BindingResource::TextureView(
753                            &texture.create_view(&wgpu::TextureViewDescriptor::default()),
754                        ),
755                    },
756                    wgpu::BindGroupEntry {
757                        binding: 1,
758                        resource: wgpu::BindingResource::Sampler(sampler),
759                    },
760                    wgpu::BindGroupEntry {
761                        binding: 2,
762                        resource: self.texture_flag_buffers[texture_flags(nearest, wrap_mode)]
763                            .as_entire_binding(),
764                    },
765                ],
766            })
767        });
768
769        queue_write_data_to_texture(&texture, origin);
770
771        // A full update must (re)create the texture at exactly the delta's size,
772        // or glyph UVs (normalized by the CPU atlas size) will sample the wrong rows.
773        debug_assert!(
774            image_delta.pos.is_some() || [texture.width(), texture.height()] == [width, height],
775            "egui texture {id:?}: GPU texture is {}x{} but full delta is {width}x{height}",
776            texture.width(),
777            texture.height(),
778        );
779
780        self.textures.insert(
781            id,
782            Texture {
783                texture: Some(texture),
784                bind_group,
785                options: Some(image_delta.options),
786            },
787        );
788    }
789
790    pub fn free_texture(&mut self, id: &epaint::TextureId) {
791        if let Some(texture) = self.textures.remove(id).and_then(|t| t.texture) {
792            texture.destroy();
793        }
794    }
795
796    /// Get the WGPU texture and bind group associated to a texture that has been allocated by egui.
797    ///
798    /// This could be used by custom paint hooks to render images that have been added through
799    /// [`epaint::Context::load_texture`](https://docs.rs/egui/latest/egui/struct.Context.html#method.load_texture).
800    pub fn texture(&self, id: &epaint::TextureId) -> Option<&Texture> {
801        self.textures.get(id)
802    }
803
804    /// Registers a [`wgpu::Texture`] with a [`epaint::TextureId`].
805    ///
806    /// This enables the application to reference the texture inside an image ui element.
807    /// This effectively enables off-screen rendering inside the egui UI. Texture must have
808    /// the texture format [`wgpu::TextureFormat::Rgba8Unorm`].
809    pub fn register_native_texture(
810        &mut self,
811        device: &wgpu::Device,
812        texture: &wgpu::TextureView,
813        texture_filter: wgpu::FilterMode,
814    ) -> epaint::TextureId {
815        self.register_native_texture_with_sampler_options(
816            device,
817            texture,
818            wgpu::SamplerDescriptor {
819                label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
820                mag_filter: texture_filter,
821                min_filter: texture_filter,
822                ..Default::default()
823            },
824        )
825    }
826
827    /// Registers a [`wgpu::Texture`] with an existing [`epaint::TextureId`].
828    ///
829    /// This enables applications to reuse [`epaint::TextureId`]s.
830    pub fn update_egui_texture_from_wgpu_texture(
831        &mut self,
832        device: &wgpu::Device,
833        texture: &wgpu::TextureView,
834        texture_filter: wgpu::FilterMode,
835        id: epaint::TextureId,
836    ) {
837        self.update_egui_texture_from_wgpu_texture_with_sampler_options(
838            device,
839            texture,
840            wgpu::SamplerDescriptor {
841                label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
842                mag_filter: texture_filter,
843                min_filter: texture_filter,
844                ..Default::default()
845            },
846            id,
847        );
848    }
849
850    /// Registers a [`wgpu::Texture`] with a [`epaint::TextureId`] while also accepting custom
851    /// [`wgpu::SamplerDescriptor`] options.
852    ///
853    /// This allows applications to specify individual minification/magnification filters as well as
854    /// custom mipmap and tiling options.
855    ///
856    /// The texture must have the format [`wgpu::TextureFormat::Rgba8Unorm`].
857    /// Any compare function supplied in the [`wgpu::SamplerDescriptor`] will be ignored.
858    #[expect(clippy::needless_pass_by_value)] // false positive
859    pub fn register_native_texture_with_sampler_options(
860        &mut self,
861        device: &wgpu::Device,
862        texture: &wgpu::TextureView,
863        sampler_descriptor: wgpu::SamplerDescriptor<'_>,
864    ) -> epaint::TextureId {
865        profiling::function_scope!();
866
867        let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest;
868        let wrap_mode = address_mode_wrap_flag(sampler_descriptor.address_mode_u);
869        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
870            compare: None,
871            ..sampler_descriptor
872        });
873
874        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
875            label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
876            layout: &self.texture_bind_group_layout,
877            entries: &[
878                wgpu::BindGroupEntry {
879                    binding: 0,
880                    resource: wgpu::BindingResource::TextureView(texture),
881                },
882                wgpu::BindGroupEntry {
883                    binding: 1,
884                    resource: wgpu::BindingResource::Sampler(&sampler),
885                },
886                wgpu::BindGroupEntry {
887                    binding: 2,
888                    resource: self.texture_flag_buffers[texture_flags(nearest, wrap_mode)]
889                        .as_entire_binding(),
890                },
891            ],
892        });
893
894        let id = epaint::TextureId::User(self.next_user_texture_id);
895        self.textures.insert(
896            id,
897            Texture {
898                texture: None,
899                bind_group,
900                options: None,
901            },
902        );
903        self.next_user_texture_id += 1;
904
905        id
906    }
907
908    /// Registers a [`wgpu::Texture`] with an existing [`epaint::TextureId`] while also accepting custom
909    /// [`wgpu::SamplerDescriptor`] options.
910    ///
911    /// This allows applications to reuse [`epaint::TextureId`]s created with custom sampler options.
912    #[expect(clippy::needless_pass_by_value)] // false positive
913    pub fn update_egui_texture_from_wgpu_texture_with_sampler_options(
914        &mut self,
915        device: &wgpu::Device,
916        texture: &wgpu::TextureView,
917        sampler_descriptor: wgpu::SamplerDescriptor<'_>,
918        id: epaint::TextureId,
919    ) {
920        profiling::function_scope!();
921
922        let Texture {
923            bind_group: user_texture_binding,
924            ..
925        } = self
926            .textures
927            .get_mut(&id)
928            .expect("Tried to update a texture that has not been allocated yet.");
929
930        let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest;
931        let wrap_mode = address_mode_wrap_flag(sampler_descriptor.address_mode_u);
932        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
933            compare: None,
934            ..sampler_descriptor
935        });
936
937        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
938            label: Some(format!("egui_user_image_{}", self.next_user_texture_id).as_str()),
939            layout: &self.texture_bind_group_layout,
940            entries: &[
941                wgpu::BindGroupEntry {
942                    binding: 0,
943                    resource: wgpu::BindingResource::TextureView(texture),
944                },
945                wgpu::BindGroupEntry {
946                    binding: 1,
947                    resource: wgpu::BindingResource::Sampler(&sampler),
948                },
949                wgpu::BindGroupEntry {
950                    binding: 2,
951                    resource: self.texture_flag_buffers[texture_flags(nearest, wrap_mode)]
952                        .as_entire_binding(),
953                },
954            ],
955        });
956
957        *user_texture_binding = bind_group;
958    }
959
960    /// Uploads the uniform, vertex and index data used by the renderer.
961    /// Should be called before [`Self::render`].
962    ///
963    /// Returns all user-defined command buffers gathered from [`CallbackTrait::prepare`] & [`CallbackTrait::finish_prepare`] callbacks.
964    pub fn update_buffers(
965        &mut self,
966        device: &wgpu::Device,
967        queue: &wgpu::Queue,
968        encoder: &mut wgpu::CommandEncoder,
969        paint_jobs: &[epaint::ClippedPrimitive],
970        screen_descriptor: &ScreenDescriptor,
971    ) -> Vec<wgpu::CommandBuffer> {
972        profiling::function_scope!();
973
974        let screen_size_in_points = screen_descriptor.screen_size_in_points();
975
976        let uniform_buffer_content = UniformBuffer {
977            screen_size_in_points,
978            dithering: u32::from(self.options.dithering),
979            predictable_texture_filtering: u32::from(self.options.predictable_texture_filtering),
980        };
981        if uniform_buffer_content != self.previous_uniform_buffer_content {
982            profiling::scope!("update uniforms");
983            queue.write_buffer(
984                &self.uniform_buffer,
985                0,
986                bytemuck::cast_slice(&[uniform_buffer_content]),
987            );
988            self.previous_uniform_buffer_content = uniform_buffer_content;
989        }
990
991        // Determine how many vertices & indices need to be rendered, and gather prepare callbacks
992        let mut callbacks = Vec::new();
993        let (vertex_count, index_count) = {
994            profiling::scope!("count_vertices_indices");
995            paint_jobs.iter().fold((0, 0), |acc, clipped_primitive| {
996                match &clipped_primitive.primitive {
997                    Primitive::Mesh(mesh) => {
998                        (acc.0 + mesh.vertices.len(), acc.1 + mesh.indices.len())
999                    }
1000                    Primitive::Callback(callback) => {
1001                        if let Some(c) = callback.callback.downcast_ref::<Callback>() {
1002                            callbacks.push(c.0.as_ref());
1003                        } else {
1004                            log::warn!("Unknown paint callback: expected `egui_wgpu::Callback`");
1005                        }
1006                        acc
1007                    }
1008                }
1009            })
1010        };
1011
1012        if index_count > 0 {
1013            profiling::scope!("indices", index_count.to_string().as_str());
1014
1015            self.index_buffer.slices.clear();
1016
1017            let required_index_buffer_size = (core::mem::size_of::<u32>() * index_count) as u64;
1018            if self.index_buffer.capacity < required_index_buffer_size {
1019                // Resize index buffer if needed.
1020                self.index_buffer.capacity =
1021                    (self.index_buffer.capacity * 2).at_least(required_index_buffer_size);
1022                self.index_buffer.buffer = create_index_buffer(device, self.index_buffer.capacity);
1023            }
1024
1025            let index_buffer_staging = queue.write_buffer_with(
1026                &self.index_buffer.buffer,
1027                0,
1028                #[expect(clippy::unwrap_used)] // Checked above
1029                NonZeroU64::new(required_index_buffer_size).unwrap(),
1030            );
1031
1032            let Some(mut index_buffer_staging) = index_buffer_staging else {
1033                panic!(
1034                    "Failed to create staging buffer for index data. Index count: {index_count}. Required index buffer size: {required_index_buffer_size}. Actual size {} and capacity: {} (bytes)",
1035                    self.index_buffer.buffer.size(),
1036                    self.index_buffer.capacity
1037                );
1038            };
1039
1040            let mut index_offset = 0;
1041            for epaint::ClippedPrimitive { primitive, .. } in paint_jobs {
1042                match primitive {
1043                    Primitive::Mesh(mesh) => {
1044                        let size = mesh.indices.len() * core::mem::size_of::<u32>();
1045                        let slice = index_offset..(size + index_offset);
1046                        index_buffer_staging
1047                            .slice(slice.clone())
1048                            .copy_from_slice(bytemuck::cast_slice(&mesh.indices));
1049                        self.index_buffer.slices.push(slice);
1050                        index_offset += size;
1051                    }
1052                    Primitive::Callback(_) => {}
1053                }
1054            }
1055        }
1056        if vertex_count > 0 {
1057            profiling::scope!("vertices", vertex_count.to_string().as_str());
1058
1059            self.vertex_buffer.slices.clear();
1060
1061            let required_vertex_buffer_size =
1062                (core::mem::size_of::<Vertex>() * vertex_count) as u64;
1063            if self.vertex_buffer.capacity < required_vertex_buffer_size {
1064                // Resize vertex buffer if needed.
1065                self.vertex_buffer.capacity =
1066                    (self.vertex_buffer.capacity * 2).at_least(required_vertex_buffer_size);
1067                self.vertex_buffer.buffer =
1068                    create_vertex_buffer(device, self.vertex_buffer.capacity);
1069            }
1070
1071            let vertex_buffer_staging = queue.write_buffer_with(
1072                &self.vertex_buffer.buffer,
1073                0,
1074                #[expect(clippy::unwrap_used)] // Checked above
1075                NonZeroU64::new(required_vertex_buffer_size).unwrap(),
1076            );
1077
1078            let Some(mut vertex_buffer_staging) = vertex_buffer_staging else {
1079                panic!(
1080                    "Failed to create staging buffer for vertex data. Vertex count: {vertex_count}. Required vertex buffer size: {required_vertex_buffer_size}. Actual size {} and capacity: {} (bytes)",
1081                    self.vertex_buffer.buffer.size(),
1082                    self.vertex_buffer.capacity
1083                );
1084            };
1085
1086            let mut vertex_offset = 0;
1087            for epaint::ClippedPrimitive { primitive, .. } in paint_jobs {
1088                match primitive {
1089                    Primitive::Mesh(mesh) => {
1090                        let size = mesh.vertices.len() * core::mem::size_of::<Vertex>();
1091                        let slice = vertex_offset..(size + vertex_offset);
1092                        vertex_buffer_staging
1093                            .slice(slice.clone())
1094                            .copy_from_slice(bytemuck::cast_slice(&mesh.vertices));
1095                        self.vertex_buffer.slices.push(slice);
1096                        vertex_offset += size;
1097                    }
1098                    Primitive::Callback(_) => {}
1099                }
1100            }
1101        }
1102
1103        let mut user_cmd_bufs = Vec::new();
1104        {
1105            profiling::scope!("prepare callbacks");
1106            for callback in &callbacks {
1107                user_cmd_bufs.extend(callback.prepare(
1108                    device,
1109                    queue,
1110                    screen_descriptor,
1111                    encoder,
1112                    &mut self.callback_resources,
1113                ));
1114            }
1115        }
1116        {
1117            profiling::scope!("finish prepare callbacks");
1118            for callback in &callbacks {
1119                user_cmd_bufs.extend(callback.finish_prepare(
1120                    device,
1121                    queue,
1122                    encoder,
1123                    &mut self.callback_resources,
1124                ));
1125            }
1126        }
1127
1128        user_cmd_bufs
1129    }
1130}
1131
1132/// Set in bit 0 of the texture flags if the sampler uses nearest filtering.
1133///
1134/// Must match `TEX_FLAG_NEAREST` in `egui.wgsl`.
1135const TEX_FLAG_NEAREST: u32 = 1;
1136
1137/// Wrap modes, stored in bits 1+ of the texture flags.
1138///
1139/// Must match the `WRAP_MODE_*` constants in `egui.wgsl`.
1140const WRAP_MODE_CLAMP_TO_EDGE: u32 = 0;
1141const WRAP_MODE_REPEAT: u32 = 1;
1142const WRAP_MODE_MIRRORED_REPEAT: u32 = 2;
1143
1144/// Number of distinct values [`texture_flags`] can return.
1145const NUM_TEXTURE_FLAGS: usize = 6;
1146
1147fn wrap_mode_flag(wrap_mode: epaint::textures::TextureWrapMode) -> u32 {
1148    match wrap_mode {
1149        epaint::textures::TextureWrapMode::ClampToEdge => WRAP_MODE_CLAMP_TO_EDGE,
1150        epaint::textures::TextureWrapMode::Repeat => WRAP_MODE_REPEAT,
1151        epaint::textures::TextureWrapMode::MirroredRepeat => WRAP_MODE_MIRRORED_REPEAT,
1152    }
1153}
1154
1155fn address_mode_wrap_flag(address_mode: wgpu::AddressMode) -> u32 {
1156    match address_mode {
1157        wgpu::AddressMode::Repeat => WRAP_MODE_REPEAT,
1158        wgpu::AddressMode::MirrorRepeat => WRAP_MODE_MIRRORED_REPEAT,
1159        wgpu::AddressMode::ClampToEdge | wgpu::AddressMode::ClampToBorder => {
1160            WRAP_MODE_CLAMP_TO_EDGE
1161        }
1162    }
1163}
1164
1165/// Index into [`Renderer::texture_flag_buffers`]: the texture flags
1166/// read by the shader when `predictable_texture_filtering` is on.
1167///
1168/// Bit 0: [`TEX_FLAG_NEAREST`].
1169/// Bits 1+: one of the `WRAP_MODE_*` constants.
1170fn texture_flags(nearest: bool, wrap_mode: u32) -> usize {
1171    let nearest = if nearest { TEX_FLAG_NEAREST } else { 0 };
1172    (nearest | (wrap_mode << 1)) as usize
1173}
1174
1175fn create_sampler(
1176    options: epaint::textures::TextureOptions,
1177    device: &wgpu::Device,
1178) -> wgpu::Sampler {
1179    let mag_filter = match options.magnification {
1180        epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
1181        epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
1182    };
1183    let min_filter = match options.minification {
1184        epaint::textures::TextureFilter::Nearest => wgpu::FilterMode::Nearest,
1185        epaint::textures::TextureFilter::Linear => wgpu::FilterMode::Linear,
1186    };
1187    let address_mode = match options.wrap_mode {
1188        epaint::textures::TextureWrapMode::ClampToEdge => wgpu::AddressMode::ClampToEdge,
1189        epaint::textures::TextureWrapMode::Repeat => wgpu::AddressMode::Repeat,
1190        epaint::textures::TextureWrapMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat,
1191    };
1192    device.create_sampler(&wgpu::SamplerDescriptor {
1193        label: Some(&format!(
1194            "egui sampler (mag: {mag_filter:?}, min {min_filter:?})"
1195        )),
1196        mag_filter,
1197        min_filter,
1198        address_mode_u: address_mode,
1199        address_mode_v: address_mode,
1200        ..Default::default()
1201    })
1202}
1203
1204fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
1205    profiling::function_scope!();
1206    device.create_buffer(&wgpu::BufferDescriptor {
1207        label: Some("egui_vertex_buffer"),
1208        usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1209        size,
1210        mapped_at_creation: false,
1211    })
1212}
1213
1214fn create_index_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer {
1215    profiling::function_scope!();
1216    device.create_buffer(&wgpu::BufferDescriptor {
1217        label: Some("egui_index_buffer"),
1218        usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
1219        size,
1220        mapped_at_creation: false,
1221    })
1222}
1223
1224/// A Rect in physical pixel space, used for setting clipping rectangles.
1225struct ScissorRect {
1226    x: u32,
1227    y: u32,
1228    width: u32,
1229    height: u32,
1230}
1231
1232impl ScissorRect {
1233    fn new(clip_rect: &epaint::Rect, pixels_per_point: f32, target_size: [u32; 2]) -> Self {
1234        // Transform clip rect to physical pixels:
1235        let clip_min_x = pixels_per_point * clip_rect.min.x;
1236        let clip_min_y = pixels_per_point * clip_rect.min.y;
1237        let clip_max_x = pixels_per_point * clip_rect.max.x;
1238        let clip_max_y = pixels_per_point * clip_rect.max.y;
1239
1240        // Round to integer:
1241        let clip_min_x = clip_min_x.round() as u32;
1242        let clip_min_y = clip_min_y.round() as u32;
1243        let clip_max_x = clip_max_x.round() as u32;
1244        let clip_max_y = clip_max_y.round() as u32;
1245
1246        // Clamp:
1247        let clip_min_x = clip_min_x.clamp(0, target_size[0]);
1248        let clip_min_y = clip_min_y.clamp(0, target_size[1]);
1249        let clip_max_x = clip_max_x.clamp(clip_min_x, target_size[0]);
1250        let clip_max_y = clip_max_y.clamp(clip_min_y, target_size[1]);
1251
1252        Self {
1253            x: clip_min_x,
1254            y: clip_min_y,
1255            width: clip_max_x - clip_min_x,
1256            height: clip_max_y - clip_min_y,
1257        }
1258    }
1259}
1260
1261// Compile-time check that `Renderer` is `Send + Sync`.
1262// Deliberately not a `#[test]`: tests never run on wasm, but `cargo check` does.
1263// On wasm this only holds with `fragile-send-sync-non-atomic-wasm` and without threads;
1264// look at the feature flag for an explanation.
1265#[cfg(any(
1266    not(target_arch = "wasm32"),
1267    all(
1268        feature = "fragile-send-sync-non-atomic-wasm",
1269        not(target_feature = "atomics")
1270    ),
1271))]
1272const _: () = {
1273    const fn assert_send_sync<T: Send + Sync>() {}
1274    assert_send_sync::<Renderer>();
1275};