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