Stored for BlackBox {
+ fn prepare(
+ &self,
+ storage: &mut Storage,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ format: wgpu::TextureFormat,
+ bounds: &Rectangle,
+ viewport: &Viewport,
+ ) {
+ if !storage.has::
() {
+ storage.store::
(P::Pipeline::new(device, queue, format));
+ }
+
+ let renderer = storage
+ .get_mut::
()
+ .expect("renderer should be initialized")
+ .downcast_mut::()
+ .expect("renderer should have the proper type");
+
+ self.primitive
+ .prepare(renderer, device, queue, bounds, viewport);
+ }
+
+ fn draw(
+ &self,
+ storage: &Storage,
+ render_pass: &mut wgpu::RenderPass<'_>,
+ ) -> bool {
+ let renderer = storage
+ .get::()
+ .expect("renderer should be initialized")
+ .downcast_ref::()
+ .expect("renderer should have the proper type");
+
+ self.primitive.draw(renderer, render_pass)
+ }
+
+ fn render(
+ &self,
+ storage: &Storage,
+ encoder: &mut wgpu::CommandEncoder,
+ target: &wgpu::TextureView,
+ clip_bounds: &Rectangle,
+ ) {
+ let renderer = storage
+ .get::()
+ .expect("renderer should be initialized")
+ .downcast_ref::()
+ .expect("renderer should have the proper type");
+
+ self.primitive
+ .render(renderer, encoder, target, clip_bounds);
+ }
+}
+
+#[derive(Debug)]
+/// An instance of a specific [`Primitive`].
+pub struct Instance {
+ /// The bounds of the [`Instance`].
+ pub(crate) bounds: Rectangle,
+
+ /// The [`Primitive`] to render.
+ pub(crate) primitive: Box,
+}
+
+impl Instance {
+ /// Creates a new [`Instance`] with the given [`Primitive`].
+ pub fn new(bounds: Rectangle, primitive: impl Primitive) -> Self {
+ Instance {
+ bounds,
+ primitive: Box::new(BlackBox { primitive }),
+ }
+ }
+}
+
+/// A renderer than can draw custom primitives.
+pub trait Renderer: core::Renderer {
+ /// Draws a custom primitive.
+ fn draw_primitive(&mut self, bounds: Rectangle, primitive: impl Primitive);
+}
+
+/// Stores custom, user-provided types.
+#[derive(Default)]
+pub struct Storage {
+ pipelines: FxHashMap>,
+}
+
+impl Storage {
+ /// Returns `true` if `Storage` contains a type `T`.
+ pub fn has(&self) -> bool {
+ self.pipelines.contains_key(&TypeId::of::())
+ }
+
+ /// Inserts the data `T` in to [`Storage`].
+ pub fn store(&mut self, pipeline: P) {
+ let _ = self.pipelines.insert(TypeId::of::(), Box::new(pipeline));
+ }
+
+ /// Returns a reference to the data with type `T` if it exists in [`Storage`].
+ pub fn get(&self) -> Option<&dyn Any> {
+ self.pipelines
+ .get(&TypeId::of::())
+ .map(|pipeline| pipeline.as_ref() as &dyn Any)
+ }
+
+ /// Returns a mutable reference to the data with type `T` if it exists in [`Storage`].
+ pub fn get_mut(&mut self) -> Option<&mut dyn Any> {
+ self.pipelines
+ .get_mut(&TypeId::of::())
+ .map(|pipeline| pipeline.as_mut() as &mut dyn Any)
+ }
+
+ /// Trims the cache of all the pipelines in the [`Storage`].
+ pub fn trim(&mut self) {
+ for pipeline in self.pipelines.values_mut() {
+ pipeline.trim();
+ }
+ }
+}
diff --git a/crates/iced_wgpu/src/quad.rs b/crates/iced_wgpu/src/quad.rs
new file mode 100644
index 000000000..31bf69cfb
--- /dev/null
+++ b/crates/iced_wgpu/src/quad.rs
@@ -0,0 +1,362 @@
+mod gradient;
+mod solid;
+
+use gradient::Gradient;
+use solid::Solid;
+
+use crate::core::{Background, Rectangle, Transformation};
+use crate::graphics;
+use crate::graphics::color;
+
+use bytemuck::{Pod, Zeroable};
+
+use std::mem;
+
+const INITIAL_INSTANCES: usize = 2_000;
+
+/// The properties of a quad.
+#[derive(Clone, Copy, Debug, Pod, Zeroable)]
+#[repr(C)]
+pub struct Quad {
+ /// The position of the [`Quad`].
+ pub position: [f32; 2],
+
+ /// The size of the [`Quad`].
+ pub size: [f32; 2],
+
+ /// The border color of the [`Quad`], in __linear RGB__.
+ pub border_color: color::Packed,
+
+ /// The border radii of the [`Quad`].
+ pub border_radius: [f32; 4],
+
+ /// The border width of the [`Quad`].
+ pub border_width: f32,
+
+ /// The shadow color of the [`Quad`].
+ pub shadow_color: color::Packed,
+
+ /// The shadow offset of the [`Quad`].
+ pub shadow_offset: [f32; 2],
+
+ /// The shadow blur radius of the [`Quad`].
+ pub shadow_blur_radius: f32,
+
+ /// Whether the [`Quad`] should be snapped to the pixel grid.
+ pub snap: u32,
+}
+
+#[derive(Debug, Clone)]
+pub struct Pipeline {
+ solid: solid::Pipeline,
+ gradient: gradient::Pipeline,
+ constant_layout: wgpu::BindGroupLayout,
+}
+
+#[derive(Default)]
+pub struct State {
+ layers: Vec,
+ prepare_layer: usize,
+}
+
+impl State {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn prepare(
+ &mut self,
+ pipeline: &Pipeline,
+ device: &wgpu::Device,
+ belt: &mut wgpu::util::StagingBelt,
+ encoder: &mut wgpu::CommandEncoder,
+ quads: &Batch,
+ transformation: Transformation,
+ scale: f32,
+ ) {
+ if self.layers.len() <= self.prepare_layer {
+ self.layers
+ .push(Layer::new(device, &pipeline.constant_layout));
+ }
+
+ let layer = &mut self.layers[self.prepare_layer];
+ layer.prepare(device, encoder, belt, quads, transformation, scale);
+
+ self.prepare_layer += 1;
+ }
+
+ pub fn render<'a>(
+ &'a self,
+ pipeline: &'a Pipeline,
+ layer: usize,
+ bounds: Rectangle,
+ quads: &Batch,
+ render_pass: &mut wgpu::RenderPass<'a>,
+ ) {
+ if let Some(layer) = self.layers.get(layer) {
+ render_pass.set_scissor_rect(
+ bounds.x,
+ bounds.y,
+ bounds.width,
+ bounds.height,
+ );
+
+ let mut solid_offset = 0;
+ let mut gradient_offset = 0;
+
+ for (kind, count) in &quads.order {
+ match kind {
+ Kind::Solid => {
+ pipeline.solid.render(
+ render_pass,
+ &layer.constants,
+ &layer.solid,
+ solid_offset..(solid_offset + count),
+ );
+
+ solid_offset += count;
+ }
+ Kind::Gradient => {
+ pipeline.gradient.render(
+ render_pass,
+ &layer.constants,
+ &layer.gradient,
+ gradient_offset..(gradient_offset + count),
+ );
+
+ gradient_offset += count;
+ }
+ }
+ }
+ }
+ }
+
+ pub fn trim(&mut self) {
+ self.prepare_layer = 0;
+ }
+}
+
+impl Pipeline {
+ pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Pipeline {
+ let constant_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("iced_wgpu::quad uniforms layout"),
+ entries: &[wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::VERTEX,
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: wgpu::BufferSize::new(
+ mem::size_of::() as wgpu::BufferAddress,
+ ),
+ },
+ count: None,
+ }],
+ });
+
+ Self {
+ solid: solid::Pipeline::new(device, format, &constant_layout),
+ gradient: gradient::Pipeline::new(device, format, &constant_layout),
+ constant_layout,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct Layer {
+ constants: wgpu::BindGroup,
+ constants_buffer: wgpu::Buffer,
+ solid: solid::Layer,
+ gradient: gradient::Layer,
+}
+
+impl Layer {
+ pub fn new(
+ device: &wgpu::Device,
+ constant_layout: &wgpu::BindGroupLayout,
+ ) -> Self {
+ let constants_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("iced_wgpu::quad uniforms buffer"),
+ size: mem::size_of::() as wgpu::BufferAddress,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ let constants = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu::quad uniforms bind group"),
+ layout: constant_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: constants_buffer.as_entire_binding(),
+ }],
+ });
+
+ Self {
+ constants,
+ constants_buffer,
+ solid: solid::Layer::new(device),
+ gradient: gradient::Layer::new(device),
+ }
+ }
+
+ pub fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
+ quads: &Batch,
+ transformation: Transformation,
+ scale: f32,
+ ) {
+ self.update(device, encoder, belt, transformation, scale);
+
+ if !quads.solids.is_empty() {
+ self.solid.prepare(device, encoder, belt, &quads.solids);
+ }
+
+ if !quads.gradients.is_empty() {
+ self.gradient
+ .prepare(device, encoder, belt, &quads.gradients);
+ }
+ }
+
+ pub fn update(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
+ transformation: Transformation,
+ scale: f32,
+ ) {
+ let uniforms = Uniforms::new(transformation, scale);
+ let bytes = bytemuck::bytes_of(&uniforms);
+
+ belt.write_buffer(
+ encoder,
+ &self.constants_buffer,
+ 0,
+ (bytes.len() as u64).try_into().expect("Sized uniforms"),
+ device,
+ )
+ .copy_from_slice(bytes);
+ }
+}
+
+/// A group of [`Quad`]s rendered together.
+#[derive(Default, Debug)]
+pub struct Batch {
+ /// The solid quads of the [`Layer`].
+ solids: Vec,
+
+ /// The gradient quads of the [`Layer`].
+ gradients: Vec,
+
+ /// The quad order of the [`Layer`].
+ order: Order,
+}
+
+/// The quad order of a [`Layer`]; stored as a tuple of the quad type & its count.
+type Order = Vec<(Kind, usize)>;
+
+impl Batch {
+ /// Returns true if there are no quads of any type in [`Quads`].
+ pub fn is_empty(&self) -> bool {
+ self.solids.is_empty() && self.gradients.is_empty()
+ }
+
+ /// Adds a [`Quad`] with the provided `Background` type to the quad [`Layer`].
+ pub fn add(&mut self, quad: Quad, background: &Background) {
+ let kind = match background {
+ Background::Color(color) => {
+ self.solids.push(Solid {
+ color: color::pack(*color),
+ quad,
+ });
+
+ Kind::Solid
+ }
+ Background::Gradient(gradient) => {
+ self.gradients.push(Gradient {
+ gradient: graphics::gradient::pack(
+ gradient,
+ Rectangle::new(quad.position.into(), quad.size.into()),
+ ),
+ quad,
+ });
+
+ Kind::Gradient
+ }
+ };
+
+ match self.order.last_mut() {
+ Some((last_kind, count)) if kind == *last_kind => {
+ *count += 1;
+ }
+ _ => {
+ self.order.push((kind, 1));
+ }
+ }
+ }
+
+ pub fn clear(&mut self) {
+ self.solids.clear();
+ self.gradients.clear();
+ self.order.clear();
+ }
+
+ pub fn append(&mut self, batch: &mut Batch) {
+ self.solids.append(&mut batch.solids);
+ self.gradients.append(&mut batch.gradients);
+ self.order.append(&mut batch.order);
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+/// The kind of a quad.
+enum Kind {
+ /// A solid quad
+ Solid,
+ /// A gradient quad
+ Gradient,
+}
+
+fn color_target_state(
+ format: wgpu::TextureFormat,
+) -> [Option; 1] {
+ [Some(wgpu::ColorTargetState {
+ format,
+ blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING),
+ write_mask: wgpu::ColorWrites::ALL,
+ })]
+}
+
+#[repr(C)]
+#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
+struct Uniforms {
+ transform: [f32; 16],
+ scale: f32,
+ // Uniforms must be aligned to their largest member,
+ // this uses a mat4x4 which aligns to 16, so align to that
+ _padding: [f32; 3],
+}
+
+impl Uniforms {
+ fn new(transformation: Transformation, scale: f32) -> Uniforms {
+ Self {
+ transform: *transformation.as_ref(),
+ scale,
+ _padding: [0.0; 3],
+ }
+ }
+}
+
+impl Default for Uniforms {
+ fn default() -> Self {
+ Self {
+ transform: *Transformation::IDENTITY.as_ref(),
+ scale: 1.0,
+ _padding: [0.0; 3],
+ }
+ }
+}
diff --git a/crates/iced_wgpu/src/quad/gradient.rs b/crates/iced_wgpu/src/quad/gradient.rs
new file mode 100644
index 000000000..6a5eed8dc
--- /dev/null
+++ b/crates/iced_wgpu/src/quad/gradient.rs
@@ -0,0 +1,187 @@
+use crate::Buffer;
+use crate::graphics::gradient;
+use crate::quad::{self, Quad};
+
+use bytemuck::{Pod, Zeroable};
+use std::ops::Range;
+
+/// A quad filled with interpolated colors.
+#[derive(Clone, Copy, Debug)]
+#[repr(C)]
+pub struct Gradient {
+ /// The background gradient data of the quad.
+ pub gradient: gradient::Packed,
+
+ /// The [`Quad`] data of the [`Gradient`].
+ pub quad: Quad,
+}
+
+#[allow(unsafe_code)]
+unsafe impl Pod for Gradient {}
+
+#[allow(unsafe_code)]
+unsafe impl Zeroable for Gradient {}
+
+#[derive(Debug)]
+pub struct Layer {
+ instances: Buffer,
+ instance_count: usize,
+}
+
+impl Layer {
+ pub fn new(device: &wgpu::Device) -> Self {
+ let instances = Buffer::new(
+ device,
+ "iced_wgpu.quad.gradient.buffer",
+ quad::INITIAL_INSTANCES,
+ wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ );
+
+ Self {
+ instances,
+ instance_count: 0,
+ }
+ }
+
+ pub fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
+ instances: &[Gradient],
+ ) {
+ let _ = self.instances.resize(device, instances.len());
+ let _ = self.instances.write(device, encoder, belt, 0, instances);
+
+ self.instance_count = instances.len();
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Pipeline {
+ #[cfg(not(target_arch = "wasm32"))]
+ pipeline: wgpu::RenderPipeline,
+}
+
+impl Pipeline {
+ #[allow(unused_variables)]
+ pub fn new(
+ device: &wgpu::Device,
+ format: wgpu::TextureFormat,
+ constants_layout: &wgpu::BindGroupLayout,
+ ) -> Self {
+ #[cfg(not(target_arch = "wasm32"))]
+ {
+ let layout = device.create_pipeline_layout(
+ &wgpu::PipelineLayoutDescriptor {
+ label: Some("iced_wgpu.quad.gradient.pipeline"),
+ push_constant_ranges: &[],
+ bind_group_layouts: &[constants_layout],
+ },
+ );
+
+ let shader =
+ device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("iced_wgpu.quad.gradient.shader"),
+ source: wgpu::ShaderSource::Wgsl(
+ std::borrow::Cow::Borrowed(concat!(
+ include_str!("../shader/quad.wgsl"),
+ "\n",
+ include_str!("../shader/vertex.wgsl"),
+ "\n",
+ include_str!("../shader/quad/gradient.wgsl"),
+ "\n",
+ include_str!("../shader/color.wgsl"),
+ "\n",
+ include_str!("../shader/color/linear_rgb.wgsl")
+ )),
+ ),
+ });
+
+ let pipeline = device.create_render_pipeline(
+ &wgpu::RenderPipelineDescriptor {
+ label: Some("iced_wgpu.quad.gradient.pipeline"),
+ layout: Some(&layout),
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: Some("gradient_vs_main"),
+ buffers: &[wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::()
+ as u64,
+ step_mode: wgpu::VertexStepMode::Instance,
+ attributes: &wgpu::vertex_attr_array!(
+ // Colors 1-2
+ 0 => Uint32x4,
+ // Colors 3-4
+ 1 => Uint32x4,
+ // Colors 5-6
+ 2 => Uint32x4,
+ // Colors 7-8
+ 3 => Uint32x4,
+ // Offsets 1-8
+ 4 => Uint32x4,
+ // Direction
+ 5 => Float32x4,
+ // Position & Scale
+ 6 => Float32x4,
+ // Border color
+ 7 => Float32x4,
+ // Border radius
+ 8 => Float32x4,
+ // Border width
+ 9 => Float32,
+ // Snap
+ 10 => Uint32,
+ ),
+ }],
+ compilation_options:
+ wgpu::PipelineCompilationOptions::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: Some("gradient_fs_main"),
+ targets: &quad::color_target_state(format),
+ compilation_options:
+ wgpu::PipelineCompilationOptions::default(),
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ front_face: wgpu::FrontFace::Cw,
+ ..Default::default()
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState {
+ count: 1,
+ mask: !0,
+ alpha_to_coverage_enabled: false,
+ },
+ multiview: None,
+ cache: None,
+ },
+ );
+
+ Self { pipeline }
+ }
+
+ #[cfg(target_arch = "wasm32")]
+ Self {}
+ }
+
+ #[allow(unused_variables)]
+ pub fn render<'a>(
+ &'a self,
+ render_pass: &mut wgpu::RenderPass<'a>,
+ constants: &'a wgpu::BindGroup,
+ layer: &'a Layer,
+ range: Range,
+ ) {
+ #[cfg(not(target_arch = "wasm32"))]
+ {
+ render_pass.set_pipeline(&self.pipeline);
+ render_pass.set_bind_group(0, constants, &[]);
+ render_pass.set_vertex_buffer(0, layer.instances.slice(..));
+
+ render_pass.draw(0..6, range.start as u32..range.end as u32);
+ }
+ }
+}
diff --git a/crates/iced_wgpu/src/quad/solid.rs b/crates/iced_wgpu/src/quad/solid.rs
new file mode 100644
index 000000000..7e48358ed
--- /dev/null
+++ b/crates/iced_wgpu/src/quad/solid.rs
@@ -0,0 +1,162 @@
+use crate::Buffer;
+use crate::graphics::color;
+use crate::quad::{self, Quad};
+
+use bytemuck::{Pod, Zeroable};
+use std::ops::Range;
+
+/// A quad filled with a solid color.
+#[derive(Clone, Copy, Debug, Pod, Zeroable)]
+#[repr(C)]
+pub struct Solid {
+ /// The background color data of the quad.
+ pub color: color::Packed,
+
+ /// The [`Quad`] data of the [`Solid`].
+ pub quad: Quad,
+}
+
+#[derive(Debug)]
+pub struct Layer {
+ instances: Buffer,
+ instance_count: usize,
+}
+
+impl Layer {
+ pub fn new(device: &wgpu::Device) -> Self {
+ let instances = Buffer::new(
+ device,
+ "iced_wgpu.quad.solid.buffer",
+ quad::INITIAL_INSTANCES,
+ wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ );
+
+ Self {
+ instances,
+ instance_count: 0,
+ }
+ }
+
+ pub fn prepare(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ belt: &mut wgpu::util::StagingBelt,
+ instances: &[Solid],
+ ) {
+ let _ = self.instances.resize(device, instances.len());
+ let _ = self.instances.write(device, encoder, belt, 0, instances);
+
+ self.instance_count = instances.len();
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct Pipeline {
+ pipeline: wgpu::RenderPipeline,
+}
+
+impl Pipeline {
+ pub fn new(
+ device: &wgpu::Device,
+ format: wgpu::TextureFormat,
+ constants_layout: &wgpu::BindGroupLayout,
+ ) -> Self {
+ let layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("iced_wgpu.quad.solid.pipeline"),
+ push_constant_ranges: &[],
+ bind_group_layouts: &[constants_layout],
+ });
+
+ let shader =
+ device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("iced_wgpu.quad.solid.shader"),
+ source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
+ concat!(
+ include_str!("../shader/color.wgsl"),
+ "\n",
+ include_str!("../shader/quad.wgsl"),
+ "\n",
+ include_str!("../shader/vertex.wgsl"),
+ "\n",
+ include_str!("../shader/quad/solid.wgsl"),
+ ),
+ )),
+ });
+
+ let pipeline =
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("iced_wgpu.quad.solid.pipeline"),
+ layout: Some(&layout),
+ vertex: wgpu::VertexState {
+ module: &shader,
+ entry_point: Some("solid_vs_main"),
+ buffers: &[wgpu::VertexBufferLayout {
+ array_stride: std::mem::size_of::() as u64,
+ step_mode: wgpu::VertexStepMode::Instance,
+ attributes: &wgpu::vertex_attr_array!(
+ // Color
+ 0 => Float32x4,
+ // Position
+ 1 => Float32x2,
+ // Size
+ 2 => Float32x2,
+ // Border color
+ 3 => Float32x4,
+ // Border radius
+ 4 => Float32x4,
+ // Border width
+ 5 => Float32,
+ // Shadow color
+ 6 => Float32x4,
+ // Shadow offset
+ 7 => Float32x2,
+ // Shadow blur radius
+ 8 => Float32,
+ // Snap
+ 9 => Uint32,
+ ),
+ }],
+ compilation_options:
+ wgpu::PipelineCompilationOptions::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &shader,
+ entry_point: Some("solid_fs_main"),
+ targets: &quad::color_target_state(format),
+ compilation_options:
+ wgpu::PipelineCompilationOptions::default(),
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleList,
+ front_face: wgpu::FrontFace::Cw,
+ ..Default::default()
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState {
+ count: 1,
+ mask: !0,
+ alpha_to_coverage_enabled: false,
+ },
+ multiview: None,
+ cache: None,
+ });
+
+ Self { pipeline }
+ }
+
+ pub fn render<'a>(
+ &'a self,
+ render_pass: &mut wgpu::RenderPass<'a>,
+ constants: &'a wgpu::BindGroup,
+ layer: &'a Layer,
+ range: Range,
+ ) {
+ render_pass.set_pipeline(&self.pipeline);
+ render_pass.set_bind_group(0, constants, &[]);
+ render_pass.set_vertex_buffer(0, layer.instances.slice(..));
+
+ render_pass.draw(0..6, range.start as u32..range.end as u32);
+ }
+}
diff --git a/crates/iced_wgpu/src/settings.rs b/crates/iced_wgpu/src/settings.rs
new file mode 100644
index 000000000..646263b7b
--- /dev/null
+++ b/crates/iced_wgpu/src/settings.rs
@@ -0,0 +1,84 @@
+//! Configure a renderer.
+use crate::core::{Font, Pixels};
+use crate::graphics::{self, Antialiasing};
+
+/// The settings of a [`Renderer`].
+///
+/// [`Renderer`]: crate::Renderer
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Settings {
+ /// The present mode of the [`Renderer`].
+ ///
+ /// [`Renderer`]: crate::Renderer
+ pub present_mode: wgpu::PresentMode,
+
+ /// The graphics backends to use.
+ pub backends: wgpu::Backends,
+
+ /// The default [`Font`] to use.
+ pub default_font: Font,
+
+ /// The default size of text.
+ ///
+ /// By default, it will be set to `16.0`.
+ pub default_text_size: Pixels,
+
+ /// The antialiasing strategy that will be used for triangle primitives.
+ ///
+ /// By default, it is `None`.
+ pub antialiasing: Option,
+}
+
+impl Default for Settings {
+ fn default() -> Settings {
+ Settings {
+ present_mode: wgpu::PresentMode::AutoVsync,
+ backends: wgpu::Backends::all(),
+ default_font: Font::default(),
+ default_text_size: Pixels(16.0),
+ antialiasing: None,
+ }
+ }
+}
+
+impl From for Settings {
+ fn from(settings: graphics::Settings) -> Self {
+ Self {
+ present_mode: if settings.vsync {
+ wgpu::PresentMode::AutoVsync
+ } else {
+ wgpu::PresentMode::AutoNoVsync
+ },
+ default_font: settings.default_font,
+ default_text_size: settings.default_text_size,
+ antialiasing: settings.antialiasing,
+ ..Settings::default()
+ }
+ }
+}
+
+/// Obtains a [`wgpu::PresentMode`] from the current environment
+/// configuration, if set.
+///
+/// The value returned by this function can be changed by setting
+/// the `ICED_PRESENT_MODE` env variable. The possible values are:
+///
+/// - `vsync` → [`wgpu::PresentMode::AutoVsync`]
+/// - `no_vsync` → [`wgpu::PresentMode::AutoNoVsync`]
+/// - `immediate` → [`wgpu::PresentMode::Immediate`]
+/// - `fifo` → [`wgpu::PresentMode::Fifo`]
+/// - `fifo_relaxed` → [`wgpu::PresentMode::FifoRelaxed`]
+/// - `mailbox` → [`wgpu::PresentMode::Mailbox`]
+pub fn present_mode_from_env() -> Option {
+ let present_mode = std::env::var("ICED_PRESENT_MODE").ok()?;
+
+ match present_mode.to_lowercase().as_str() {
+ "vsync" => Some(wgpu::PresentMode::AutoVsync),
+ "no_vsync" => Some(wgpu::PresentMode::AutoNoVsync),
+ "immediate" => Some(wgpu::PresentMode::Immediate),
+ "fifo" => Some(wgpu::PresentMode::Fifo),
+ "fifo_relaxed" => Some(wgpu::PresentMode::FifoRelaxed),
+ "mailbox" => Some(wgpu::PresentMode::Mailbox),
+ _ => None,
+ }
+}
diff --git a/crates/iced_wgpu/src/shader/blit.wgsl b/crates/iced_wgpu/src/shader/blit.wgsl
new file mode 100644
index 000000000..5e600de4f
--- /dev/null
+++ b/crates/iced_wgpu/src/shader/blit.wgsl
@@ -0,0 +1,37 @@
+var uvs: array, 6> = array, 6>(
+ vec2(0.0, 0.0),
+ vec2(1.0, 0.0),
+ vec2(1.0, 1.0),
+ vec2(0.0, 0.0),
+ vec2(0.0, 1.0),
+ vec2(1.0, 1.0)
+);
+
+@group(0) @binding(0) var u_sampler: sampler;
+@group(0) @binding(1) var u_ratio: vec4;
+@group(1) @binding(0) var u_texture: texture_2d;
+
+struct VertexInput {
+ @builtin(vertex_index) vertex_index: u32,
+}
+
+struct VertexOutput {
+ @builtin(position) position: vec4,
+ @location(0) uv: vec2,
+}
+
+@vertex
+fn vs_main(input: VertexInput) -> VertexOutput {
+ let uv = uvs[input.vertex_index];
+
+ var out: VertexOutput;
+ out.uv = uv * u_ratio.xy;
+ out.position = vec4(uv * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0);
+
+ return out;
+}
+
+@fragment
+fn fs_main(input: VertexOutput) -> @location(0) vec4 {
+ return textureSample(u_texture, u_sampler, input.uv);
+}
diff --git a/crates/iced_wgpu/src/shader/color.wgsl b/crates/iced_wgpu/src/shader/color.wgsl
new file mode 100644
index 000000000..a80663689
--- /dev/null
+++ b/crates/iced_wgpu/src/shader/color.wgsl
@@ -0,0 +1,14 @@
+fn premultiply(color: vec4) -> vec4 {
+ return vec4(color.xyz * color.a, color.a);
+}
+
+fn unpack_color(data: vec2) -> vec4 {
+ return premultiply(unpack_u32(data));
+}
+
+fn unpack_u32(data: vec2) -> vec4 {
+ let rg: vec2 = unpack2x16float(data.x);
+ let ba: vec2 = unpack2x16float(data.y);
+
+ return vec4(rg.y, rg.x, ba.y, ba.x);
+}
diff --git a/crates/iced_wgpu/src/shader/color/linear_rgb.wgsl b/crates/iced_wgpu/src/shader/color/linear_rgb.wgsl
new file mode 100644
index 000000000..a5cf45d4f
--- /dev/null
+++ b/crates/iced_wgpu/src/shader/color/linear_rgb.wgsl
@@ -0,0 +1,3 @@
+fn interpolate_color(from_: vec4, to_: vec4, factor: f32) -> vec4 {
+ return mix(from_, to_, factor);
+}
diff --git a/crates/iced_wgpu/src/shader/image.wgsl b/crates/iced_wgpu/src/shader/image.wgsl
new file mode 100644
index 000000000..3a237c36a
--- /dev/null
+++ b/crates/iced_wgpu/src/shader/image.wgsl
@@ -0,0 +1,129 @@
+struct Globals {
+ transform: mat4x4,
+ scale_factor: f32,
+}
+
+@group(0) @binding(0) var globals: Globals;
+@group(0) @binding(1) var u_sampler: sampler;
+@group(1) @binding(0) var u_texture: texture_2d_array;
+
+struct VertexInput {
+ @builtin(vertex_index) vertex_index: u32,
+ @location(0) center: vec2,
+ @location(1) clip_bounds: vec4,
+ @location(2) border_radius: vec4,
+ @location(3) tile: vec4,
+ @location(4) rotation: f32,
+ @location(5) opacity: f32,
+ @location(6) atlas_pos: vec2,
+ @location(7) atlas_scale: vec2,
+ @location(8) layer: i32,
+ @location(9) snap: u32,
+}
+
+struct VertexOutput {
+ @builtin(position) position: vec4,
+ @location(0) @interpolate(flat) clip_bounds: vec4,
+ @location(1) @interpolate(flat) border_radius: vec4,
+ @location(2) @interpolate(flat) atlas: vec4,
+ @location(3) @interpolate(flat) layer: i32,
+ @location(4) @interpolate(flat) opacity: f32,
+ @location(5) uv: vec2,
+}
+
+@vertex
+fn vs_main(input: VertexInput) -> VertexOutput {
+ var out: VertexOutput;
+
+ // Generate a vertex position in the range [0, 1] from the vertex index
+ let corner = vertex_position(input.vertex_index);
+
+ let tile = input.tile;
+ let center = input.center;
+
+ // List the unrotated tile corners
+ let corners = array, 4>(
+ tile.xy, // Top left
+ tile.xy + vec2(tile.z, 0.0), // Top right
+ tile.xy + vec2(0.0, tile.w), // Bottom left
+ tile.xy + tile.zw // Bottom right
+ );
+
+ // Rotate tile corners around center
+ let cos_r = cos(-input.rotation); // Clockwise
+ let sin_r = sin(-input.rotation);
+ var rotated = array, 4>();
+
+ for (var i = 0u; i < 4u; i++) {
+ let c = corners[i] - input.center;
+ rotated[i] = vec2(c.x * cos_r - c.y * sin_r, c.x * sin_r + c.y * cos_r) + input.center;
+ }
+
+ // Find bounding box of rotated tile
+ var min_xy = rotated[0];
+ var max_xy = rotated[0];
+ for (var i = 1u; i < 4u; i++) {
+ min_xy = min(min_xy, rotated[i]);
+ max_xy = max(max_xy, rotated[i]);
+ }
+ let rotated_bounds = vec4(min_xy, max_xy - min_xy);
+
+ // Intersect with clip bounds
+ let clip_min = max(rotated_bounds.xy, input.clip_bounds.xy);
+ let clip_max = min(rotated_bounds.xy + rotated_bounds.zw, input.clip_bounds.xy + input.clip_bounds.zw);
+ let clipped_tile = vec4(clip_min, max(vec2(0.0), clip_max - clip_min));
+
+ // Calculate the vertex position
+ let v_pos = clipped_tile.xy + corner * clipped_tile.zw;
+ out.position = vec4(vec2(globals.scale_factor), 1.0, 1.0) * vec4(v_pos, 0.0, 1.0);
+ out.clip_bounds = globals.scale_factor * input.clip_bounds;
+
+ // Calculate rotated UV
+ let uv = input.atlas_pos + (v_pos - tile.xy) / tile.zw * input.atlas_scale;
+ let uv_center = input.atlas_pos + input.atlas_scale / 2.0;
+
+ let d = uv - uv_center;
+ out.uv = vec2(d.x * cos_r - d.y * sin_r, d.x * sin_r + d.y * cos_r) + uv_center;
+
+ // Snap position to the pixel grid
+ if bool(input.snap) {
+ out.position = round(out.position);
+ out.clip_bounds = vec4(
+ round(out.clip_bounds.xy),
+ round(out.clip_bounds.xy + out.clip_bounds.zw) - out.clip_bounds.xy,
+ );
+ }
+
+ out.position = globals.transform * out.position;
+ out.border_radius = globals.scale_factor * min(input.border_radius, vec4(min(input.clip_bounds.z, input.clip_bounds.w) / 2.0));
+ out.atlas = vec4(input.atlas_pos, input.atlas_pos + input.atlas_scale);
+ out.layer = input.layer;
+ out.opacity = input.opacity;
+
+ return out;
+}
+
+@fragment
+fn fs_main(input: VertexOutput) -> @location(0) vec4 {
+ let fragment = input.position.xy;
+ let position = input.clip_bounds.xy;
+ let scale = input.clip_bounds.zw;
+
+ let d = rounded_box_sdf(
+ 2.0 * (fragment - position - scale / 2.0),
+ scale,
+ input.border_radius * 2.0,
+ ) / 2.0;
+
+ let antialias: f32 = clamp(1.0 - d, 0.0, 1.0);
+ let inside = all(input.uv >= input.atlas.xy) && all(input.uv <= input.atlas.zw);
+
+ return textureSample(u_texture, u_sampler, input.uv, input.layer) * vec4(1.0, 1.0, 1.0, antialias * input.opacity * f32(inside));
+}
+
+fn rounded_box_sdf(p: vec2, size: vec2, corners: vec4) -> f32 {
+ var box_half = select(corners.yz, corners.xw, p.x > 0.0);
+ var corner = select(box_half.y, box_half.x, p.y > 0.0);
+ var q = abs(p) - size + corner;
+ return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - corner;
+}
diff --git a/crates/iced_wgpu/src/shader/quad.wgsl b/crates/iced_wgpu/src/shader/quad.wgsl
new file mode 100644
index 000000000..f8258a4ec
--- /dev/null
+++ b/crates/iced_wgpu/src/shader/quad.wgsl
@@ -0,0 +1,13 @@
+struct Globals {
+ transform: mat4x4,
+ scale: f32,
+}
+
+@group(0) @binding(0) var globals: Globals;
+
+fn rounded_box_sdf(p: vec2, size: vec2, corners: vec4) -> f32 {
+ var box_half = select(corners.yz, corners.xw, p.x > 0.0);
+ var corner = select(box_half.y, box_half.x, p.y > 0.0);
+ var q = abs(p) - size + corner;
+ return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - corner;
+}
diff --git a/crates/iced_wgpu/src/shader/quad/gradient.wgsl b/crates/iced_wgpu/src/shader/quad/gradient.wgsl
new file mode 100644
index 000000000..7c7a6e45f
--- /dev/null
+++ b/crates/iced_wgpu/src/shader/quad/gradient.wgsl
@@ -0,0 +1,184 @@
+struct GradientVertexInput {
+ @builtin(vertex_index) vertex_index: u32,
+ @location(0) @interpolate(flat) colors_1: vec4,
+ @location(1) @interpolate(flat) colors_2: vec4,
+ @location(2) @interpolate(flat) colors_3: vec4,
+ @location(3) @interpolate(flat) colors_4: vec4,
+ @location(4) @interpolate(flat) offsets: vec4,
+ @location(5) direction: vec4,
+ @location(6) position_and_scale: vec4,
+ @location(7) border_color: vec4,
+ @location(8) border_radius: vec4,
+ @location(9) border_width: f32,
+ @location(10) snap: u32,
+}
+
+struct GradientVertexOutput {
+ @builtin(position) position: vec4,
+ @location(1) @interpolate(flat) colors_1: vec4,
+ @location(2) @interpolate(flat) colors_2: vec4,
+ @location(3) @interpolate(flat) colors_3: vec4,
+ @location(4) @interpolate(flat) colors_4: vec4,
+ @location(5) @interpolate(flat) offsets: vec4,
+ @location(6) direction: vec4,
+ @location(7) position_and_scale: vec4,
+ @location(8) border_color: vec4,
+ @location(9) border_radius: vec4,
+ @location(10) border_width: f32,
+}
+
+@vertex
+fn gradient_vs_main(input: GradientVertexInput) -> GradientVertexOutput {
+ var out: GradientVertexOutput;
+
+ var pos: vec2 = input.position_and_scale.xy * globals.scale;
+ var scale: vec2 = input.position_and_scale.zw * globals.scale;
+
+ var pos_snap = vec2