Skip to main content

pebble/wgpu/
backend.rs

1use crate::{
2    app::App,
3    ecs::plugin::Plugin,
4    rendering::{
5        backend::{Backend, ColorTarget, FrameOperations, Pass},
6        errors::AcquireError,
7        sync::InitSender,
8        window::{GPUSurfaceHandle, WindowConfig},
9    },
10    wgpu::{
11        compute_pass::{CommandEncoder, ComputePass},
12        render_pass::RenderPass,
13        texture_format::TextureFormat,
14        texture_view::TextureView,
15        window::WinitWindow,
16    },
17};
18
19/// The `wgpu`-backed [`Backend`] implementation. Inserted as a resource
20/// once [`init`](Self::init) finishes (see the [`Backend`] trait docs for
21/// how that's driven); everything in [`super`] that uploads to the GPU
22/// (`Res<WGPUBackend>` in an [`Asset::upload`](crate::assets::upload::Asset::upload)
23/// impl) reads `device`/`queue` directly off this.
24///
25/// `device`/`queue`/`surface`/`config` are `pub(crate)` — every builder in
26/// [`wgpu::prelude`](super::prelude) that needs the device takes `&WGPUBackend`
27/// directly instead of a raw `&wgpu::Device`. Surface dimensions/format are
28/// exposed via [`surface_width`](Self::surface_width)/[`surface_height`](Self::surface_height)/
29/// [`surface_format`](Self::surface_format) instead of the raw
30/// `wgpu::SurfaceConfiguration`.
31pub struct WGPUBackend {
32    pub(crate) device: wgpu::Device,
33    pub(crate) queue: wgpu::Queue,
34    pub(crate) surface: wgpu::Surface<'static>,
35    pub(crate) config: wgpu::SurfaceConfiguration,
36    msaa_sample_count: u32,
37    msaa_color: Option<wgpu::TextureView>,
38}
39
40impl WGPUBackend {
41    /// Current surface width in pixels.
42    pub fn surface_width(&self) -> u32 {
43        self.config.width
44    }
45
46    /// Current surface height in pixels.
47    pub fn surface_height(&self) -> u32 {
48        self.config.height
49    }
50
51    /// The format the surface was negotiated at (a preferred sRGB format,
52    /// chosen when the backend initializes).
53    pub fn surface_format(&self) -> TextureFormat {
54        self.config.format.into()
55    }
56
57    /// The multisample count [`ColorTarget::Default`]
58    /// rendering (the window surface) currently uses — `1` (no MSAA) until
59    /// [`set_msaa`](Self::set_msaa) is called. A material meant to render
60    /// into the default target needs `Material { sample_count:
61    /// backend.sample_count(), .. }` to match.
62    pub fn sample_count(&self) -> u32 {
63        self.msaa_sample_count
64    }
65
66    /// Turns on (`sample_count > 1`) or off (`sample_count: 1`) multisampled
67    /// rendering into the window surface. Builds — or rebuilds, matching the
68    /// current surface size — an internal multisampled color texture that
69    /// [`ColorTarget::Default`]
70    /// renders into and automatically resolves from into the real surface;
71    /// [`resize`](Backend::resize) keeps it matched to the surface size
72    /// afterward. Call this once at startup (or whenever you want to change
73    /// the sample count), before building any material meant to render into
74    /// the default target — see [`sample_count`](Self::sample_count). Any
75    /// depth attachment used alongside it needs a matching
76    /// [`TextureBuilder::sample_count`](super::texture_view::TextureBuilder::sample_count).
77    pub fn set_msaa(&mut self, sample_count: u32) {
78        self.msaa_sample_count = sample_count;
79        self.rebuild_msaa_color();
80    }
81
82    fn rebuild_msaa_color(&mut self) {
83        if self.msaa_sample_count <= 1 {
84            self.msaa_color = None;
85            return;
86        }
87        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
88            label: Some("pebble-msaa-color"),
89            size: wgpu::Extent3d { width: self.config.width, height: self.config.height, depth_or_array_layers: 1 },
90            mip_level_count: 1,
91            sample_count: self.msaa_sample_count,
92            dimension: wgpu::TextureDimension::D2,
93            format: self.config.format,
94            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
95            view_formats: &[],
96        });
97        self.msaa_color = Some(texture.create_view(&wgpu::TextureViewDescriptor::default()));
98    }
99}
100
101impl WGPUBackend {
102    async fn init_async(
103        handle: impl GPUSurfaceHandle,
104        width: u32,
105        height: u32,
106        sender: InitSender<Self>,
107    ) {
108        let backends = if cfg!(target_arch = "wasm32") {
109            wgpu::Backends::BROWSER_WEBGPU
110        } else {
111            wgpu::Backends::PRIMARY
112        };
113
114        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
115            display: None,
116            backends,
117            flags: wgpu::InstanceFlags::default(),
118            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
119            backend_options: wgpu::BackendOptions::default(),
120        });
121
122        let surface = instance.create_surface(handle).unwrap();
123
124        let adapter = instance
125            .request_adapter(&wgpu::RequestAdapterOptions {
126                power_preference: wgpu::PowerPreference::HighPerformance,
127                force_fallback_adapter: false,
128                compatible_surface: Some(&surface),
129            })
130            .await
131            .unwrap();
132
133        let (required_features, required_limits) = if cfg!(target_arch = "wasm32") {
134            (wgpu::Features::empty(), wgpu::Limits::defaults())
135        } else {
136            (
137                wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER,
138                wgpu::Limits::default(),
139            )
140        };
141
142        let (device, queue) = adapter
143            .request_device(&wgpu::DeviceDescriptor {
144                label: None,
145                required_features,
146                required_limits,
147                ..Default::default()
148            })
149            .await
150            .unwrap();
151
152        let caps = surface.get_capabilities(&adapter);
153        let format = caps
154            .formats
155            .iter()
156            .copied()
157            .find(|f| f.is_srgb())
158            .unwrap_or(caps.formats[0]);
159
160        // Prefer Fifo (vsync) explicitly rather than trusting caps.present_modes[0] —
161        // its ordering isn't guaranteed to put Fifo first, and an uncapped mode
162        // (Immediate/Mailbox) here would tear and burn GPU cycles for no benefit.
163        let present_mode = caps
164            .present_modes
165            .iter()
166            .copied()
167            .find(|m| *m == wgpu::PresentMode::Fifo)
168            .unwrap_or(caps.present_modes[0]);
169
170        let config = wgpu::SurfaceConfiguration {
171            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
172            format,
173            present_mode,
174            alpha_mode: caps.alpha_modes[0],
175            width,
176            height,
177            desired_maximum_frame_latency: 2,
178            view_formats: vec![],
179        };
180        surface.configure(&device, &config);
181
182        sender.send(WGPUBackend {
183            device,
184            queue,
185            surface,
186            config,
187            msaa_sample_count: 1,
188            msaa_color: None,
189        });
190    }
191}
192
193pub struct WGPUFrame {
194    encoder: wgpu::CommandEncoder,
195    view: wgpu::TextureView,
196    surface_texture: wgpu::SurfaceTexture,
197    /// Snapshot of `WGPUBackend::msaa_color` at acquire time — `Some` means
198    /// `ColorTarget::Default` renders into this multisampled texture and
199    /// resolves into `view` (the real surface) instead of rendering to
200    /// `view` directly.
201    msaa_view: Option<wgpu::TextureView>,
202}
203
204impl FrameOperations for WGPUFrame {
205    type Context<'a> = RenderPass<'a>;
206    type Attachment = TextureView;
207    type DepthAttachment = TextureView;
208
209    fn begin(&mut self, pass: Pass<'_, Self>) -> Self::Context<'_> {
210        let color_attachments: Vec<_> = pass
211            .colors
212            .iter()
213            .map(|target| {
214                let (view, resolve_target, clear) = match target {
215                    ColorTarget::Default { clear } => match &self.msaa_view {
216                        Some(msaa) => (msaa, Some(&self.view), clear),
217                        None => (&self.view, None, clear),
218                    },
219                    ColorTarget::Custom { attachment, clear } => (attachment.raw(), None, clear),
220                };
221                Some(wgpu::RenderPassColorAttachment {
222                    view,
223                    depth_slice: None,
224                    resolve_target,
225                    ops: wgpu::Operations {
226                        load: clear
227                            .map(|[r, g, b, a]| {
228                                wgpu::LoadOp::Clear(wgpu::Color {
229                                    r: r as f64,
230                                    g: g as f64,
231                                    b: b as f64,
232                                    a: a as f64,
233                                })
234                            })
235                            .unwrap_or(wgpu::LoadOp::Load),
236                        store: wgpu::StoreOp::Store,
237                    },
238                })
239            })
240            .collect();
241
242        let depth_stencil_attachment =
243            pass.depth
244                .as_ref()
245                .map(|d| wgpu::RenderPassDepthStencilAttachment {
246                    view: d.attachment.raw(),
247                    depth_ops: Some(wgpu::Operations {
248                        load: d
249                            .clear
250                            .map(wgpu::LoadOp::Clear)
251                            .unwrap_or(wgpu::LoadOp::Load),
252                        store: wgpu::StoreOp::Store,
253                    }),
254                    stencil_ops: None,
255                });
256
257        let raw = self.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
258            label: None,
259            color_attachments: &color_attachments,
260            depth_stencil_attachment,
261            timestamp_writes: None,
262            occlusion_query_set: None,
263            multiview_mask: None,
264        });
265        RenderPass::new(raw)
266    }
267}
268
269impl WGPUFrame {
270    /// Begin a compute pass on this frame's command encoder.
271    pub fn compute_pass(&mut self, label: Option<&str>) -> ComputePass<'_> {
272        let raw = self.encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
273            label,
274            timestamp_writes: None,
275        });
276        ComputePass::new(raw)
277    }
278}
279
280impl WGPUBackend {
281    /// Starts a command encoder for standalone GPU work not tied to an
282    /// acquired frame — a compute dispatch outside `SystemStage::Render`,
283    /// say. Begin a [`ComputePass`] on it via
284    /// [`CommandEncoder::compute_pass`], then hand it to [`submit`](Self::submit)
285    /// when done. Render passes don't need this —
286    /// [`ActiveFrame::begin_pass`](crate::rendering::active_frame::ActiveFrame::begin_pass)
287    /// manages its own frame-tied encoder internally.
288    pub fn create_command_encoder(&self, label: Option<&str>) -> CommandEncoder {
289        CommandEncoder::new(self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }))
290    }
291
292    /// Finishes and submits `encoder`'s recorded commands to the queue.
293    pub fn submit(&self, encoder: CommandEncoder) {
294        self.queue.submit(std::iter::once(encoder.into_raw().finish()));
295    }
296}
297
298impl Backend for WGPUBackend {
299    type Frame = WGPUFrame;
300
301    /// Native blocks the calling thread on [`init_async`](Self::init_async)
302    /// via `pollster::block_on` (fine here — this only runs once, during
303    /// [`App::build`](crate::app::App::build), and there's no other work
304    /// competing for the thread yet). Web can't block its single thread, so
305    /// it hands `init_async` to `wasm_bindgen_futures::spawn_local` instead
306    /// and returns immediately — [`GraphicsPlugin`](crate::rendering::graphics_plugin::GraphicsPlugin)
307    /// polls the resulting `sender`/[`InitReceiver`](crate::rendering::sync::InitReceiver)
308    /// pair every tick either way, so callers don't need to know which path
309    /// ran. A second [`Backend`] implementation should follow the same
310    /// split if it also needs to run on both targets.
311    fn init(handle: impl GPUSurfaceHandle, width: u32, height: u32, sender: InitSender<Self>) {
312        #[cfg(not(target_arch = "wasm32"))]
313        {
314            pollster::block_on(Self::init_async(handle, width, height, sender));
315        }
316
317        #[cfg(target_arch = "wasm32")]
318        {
319            wasm_bindgen_futures::spawn_local(Self::init_async(handle, width, height, sender));
320        }
321    }
322
323    fn resize(&mut self, width: u32, height: u32) {
324        if width == 0 || height == 0 {
325            return; // minimized — don't reconfigure to a degenerate size
326        }
327        self.config.width = width;
328        self.config.height = height;
329        self.surface.configure(&self.device, &self.config);
330        self.rebuild_msaa_color();
331    }
332
333    fn acquire(&mut self) -> Result<Self::Frame, AcquireError> {
334        let surface_texture = match self.surface.get_current_texture() {
335            wgpu::CurrentSurfaceTexture::Success(texture) => texture,
336            wgpu::CurrentSurfaceTexture::Suboptimal(texture) => texture,
337            wgpu::CurrentSurfaceTexture::Timeout
338            | wgpu::CurrentSurfaceTexture::Outdated
339            | wgpu::CurrentSurfaceTexture::Occluded => {
340                return Err(AcquireError::Transient);
341            }
342            other => {
343                return Err(AcquireError::Fatal(format!(
344                    "unexpected surface state: {other:?}"
345                )));
346            }
347        };
348
349        let view = surface_texture
350            .texture
351            .create_view(&wgpu::TextureViewDescriptor::default());
352        let encoder = self
353            .device
354            .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
355
356        Ok(WGPUFrame {
357            encoder,
358            view,
359            surface_texture,
360            msaa_view: self.msaa_color.clone(),
361        })
362    }
363
364    fn present(&mut self, frame: Self::Frame) {
365        self.queue.submit(std::iter::once(frame.encoder.finish()));
366        frame.surface_texture.present();
367    }
368}
369
370pub struct WGPUPlugin {
371    config: WindowConfig,
372}
373
374impl WGPUPlugin {
375    pub fn new(config: WindowConfig) -> Self {
376        Self { config }
377    }
378}
379
380impl Plugin for WGPUPlugin {
381    fn build(&self, app: &mut App) {
382        app.add_plugin(crate::prelude::WindowPlugin::<WinitWindow>::new(
383            WindowConfig {
384                title: self.config.title.clone(),
385                width: self.config.width,
386                height: self.config.height,
387            },
388        ))
389        .add_plugin(crate::prelude::GraphicsPlugin::<WGPUBackend, WinitWindow>::new())
390        .add_plugin(crate::prelude::RenderPlugin::<WGPUBackend>::new())
391        .add_plugin(crate::wgpu::textures::TexturePlugin)
392        .add_plugin(crate::wgpu::texture_array::TextureArrayPlugin)
393        .add_plugin(crate::wgpu::cubemap::CubemapPlugin)
394        .add_plugin(crate::wgpu::mesh::MeshPlugin::new())
395        .add_plugin(crate::wgpu::material::MaterialPlugin::new())
396        .add_plugin(crate::wgpu::instance::MaterialInstancePlugin::new())
397        .add_plugin(crate::wgpu::compute::ComputePlugin::new())
398        .add_plugin(crate::wgpu::instance::ComputeInstancePlugin::new())
399        .add_plugin(crate::prelude::LazyResourcePlugin::<
400            WGPUBackend,
401            crate::wgpu::samplers::GlobalSamplers,
402        >::new());
403    }
404}