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_view::TextureView,
14        window::WinitWindow,
15    },
16};
17
18/// The `wgpu`-backed [`Backend`] implementation. Inserted as a resource
19/// once [`init`](Self::init) finishes (see the [`Backend`] trait docs for
20/// how that's driven); everything in [`super`] that uploads to the GPU
21/// (`Res<WGPUBackend>` in an [`Asset::upload`](crate::assets::upload::Asset::upload)
22/// impl) reads `device`/`queue` directly off this.
23///
24/// `device`/`queue` are `pub` because some operations genuinely need them
25/// (submitting command encoders, `queue.write_buffer`, resource types
26/// [`wgpu::prelude`](super::prelude) doesn't cover) — but for building a
27/// buffer, bind group layout, or bind group, reach for
28/// [`wgpu::prelude`](super::prelude) first rather than hand-writing a
29/// `wgpu::BufferDescriptor`/`BindGroupLayoutDescriptor`/`BindGroupDescriptor`
30/// against `device` directly.
31pub struct WGPUBackend {
32    pub device: wgpu::Device,
33    pub queue: wgpu::Queue,
34    pub surface: wgpu::Surface<'static>,
35    pub config: wgpu::SurfaceConfiguration,
36}
37
38impl WGPUBackend {
39    async fn init_async(
40        handle: impl GPUSurfaceHandle,
41        width: u32,
42        height: u32,
43        sender: InitSender<Self>,
44    ) {
45        let backends = if cfg!(target_arch = "wasm32") {
46            wgpu::Backends::BROWSER_WEBGPU
47        } else {
48            wgpu::Backends::PRIMARY
49        };
50
51        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
52            display: None,
53            backends,
54            flags: wgpu::InstanceFlags::default(),
55            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
56            backend_options: wgpu::BackendOptions::default(),
57        });
58
59        let surface = instance.create_surface(handle).unwrap();
60
61        let adapter = instance
62            .request_adapter(&wgpu::RequestAdapterOptions {
63                power_preference: wgpu::PowerPreference::HighPerformance,
64                force_fallback_adapter: false,
65                compatible_surface: Some(&surface),
66            })
67            .await
68            .unwrap();
69
70        let (required_features, required_limits) = if cfg!(target_arch = "wasm32") {
71            (wgpu::Features::empty(), wgpu::Limits::defaults())
72        } else {
73            (
74                wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER,
75                wgpu::Limits::default(),
76            )
77        };
78
79        let (device, queue) = adapter
80            .request_device(&wgpu::DeviceDescriptor {
81                label: None,
82                required_features,
83                required_limits,
84                ..Default::default()
85            })
86            .await
87            .unwrap();
88
89        let caps = surface.get_capabilities(&adapter);
90        let format = caps
91            .formats
92            .iter()
93            .copied()
94            .find(|f| f.is_srgb())
95            .unwrap_or(caps.formats[0]);
96
97        // Prefer Fifo (vsync) explicitly rather than trusting caps.present_modes[0] —
98        // its ordering isn't guaranteed to put Fifo first, and an uncapped mode
99        // (Immediate/Mailbox) here would tear and burn GPU cycles for no benefit.
100        let present_mode = caps
101            .present_modes
102            .iter()
103            .copied()
104            .find(|m| *m == wgpu::PresentMode::Fifo)
105            .unwrap_or(caps.present_modes[0]);
106
107        let config = wgpu::SurfaceConfiguration {
108            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
109            format,
110            present_mode,
111            alpha_mode: caps.alpha_modes[0],
112            width,
113            height,
114            desired_maximum_frame_latency: 2,
115            view_formats: vec![],
116        };
117        surface.configure(&device, &config);
118
119        sender.send(WGPUBackend {
120            device,
121            queue,
122            surface,
123            config,
124        });
125    }
126}
127
128pub struct WGPUFrame {
129    encoder: wgpu::CommandEncoder,
130    view: wgpu::TextureView,
131    surface_texture: wgpu::SurfaceTexture,
132}
133
134impl FrameOperations for WGPUFrame {
135    type Context<'a> = RenderPass<'a>;
136    type Attachment = TextureView;
137    type DepthAttachment = TextureView;
138
139    fn begin(&mut self, pass: Pass<'_, Self>) -> Self::Context<'_> {
140        let color_attachments: Vec<_> = pass
141            .colors
142            .iter()
143            .map(|target| {
144                let (view, clear) = match target {
145                    ColorTarget::Default { clear } => (&self.view, clear),
146                    ColorTarget::Custom { attachment, clear } => (attachment.raw(), clear),
147                };
148                Some(wgpu::RenderPassColorAttachment {
149                    view,
150                    depth_slice: None,
151                    resolve_target: None,
152                    ops: wgpu::Operations {
153                        load: clear
154                            .map(|[r, g, b, a]| {
155                                wgpu::LoadOp::Clear(wgpu::Color {
156                                    r: r as f64,
157                                    g: g as f64,
158                                    b: b as f64,
159                                    a: a as f64,
160                                })
161                            })
162                            .unwrap_or(wgpu::LoadOp::Load),
163                        store: wgpu::StoreOp::Store,
164                    },
165                })
166            })
167            .collect();
168
169        let depth_stencil_attachment =
170            pass.depth
171                .as_ref()
172                .map(|d| wgpu::RenderPassDepthStencilAttachment {
173                    view: d.attachment.raw(),
174                    depth_ops: Some(wgpu::Operations {
175                        load: d
176                            .clear
177                            .map(wgpu::LoadOp::Clear)
178                            .unwrap_or(wgpu::LoadOp::Load),
179                        store: wgpu::StoreOp::Store,
180                    }),
181                    stencil_ops: None,
182                });
183
184        let raw = self.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
185            label: None,
186            color_attachments: &color_attachments,
187            depth_stencil_attachment,
188            timestamp_writes: None,
189            occlusion_query_set: None,
190            multiview_mask: None,
191        });
192        RenderPass::new(raw)
193    }
194}
195
196impl WGPUFrame {
197    /// Begin a compute pass on this frame's command encoder.
198    pub fn compute_pass(&mut self, label: Option<&str>) -> ComputePass<'_> {
199        let raw = self.encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
200            label,
201            timestamp_writes: None,
202        });
203        ComputePass::new(raw)
204    }
205}
206
207impl WGPUBackend {
208    /// Starts a command encoder for standalone GPU work not tied to an
209    /// acquired frame — a compute dispatch outside `SystemStage::Render`,
210    /// say. Begin a [`ComputePass`] on it via
211    /// [`CommandEncoder::compute_pass`], then hand it to [`submit`](Self::submit)
212    /// when done. Render passes don't need this —
213    /// [`ActiveFrame::begin_pass`](crate::rendering::active_frame::ActiveFrame::begin_pass)
214    /// manages its own frame-tied encoder internally.
215    pub fn create_command_encoder(&self, label: Option<&str>) -> CommandEncoder {
216        CommandEncoder::new(self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }))
217    }
218
219    /// Finishes and submits `encoder`'s recorded commands to the queue.
220    pub fn submit(&self, encoder: CommandEncoder) {
221        self.queue.submit(std::iter::once(encoder.into_raw().finish()));
222    }
223}
224
225impl Backend for WGPUBackend {
226    type Frame = WGPUFrame;
227
228    /// Native blocks the calling thread on [`init_async`](Self::init_async)
229    /// via `pollster::block_on` (fine here — this only runs once, during
230    /// [`App::build`](crate::app::App::build), and there's no other work
231    /// competing for the thread yet). Web can't block its single thread, so
232    /// it hands `init_async` to `wasm_bindgen_futures::spawn_local` instead
233    /// and returns immediately — [`GraphicsPlugin`](crate::rendering::graphics_plugin::GraphicsPlugin)
234    /// polls the resulting `sender`/[`InitReceiver`](crate::rendering::sync::InitReceiver)
235    /// pair every tick either way, so callers don't need to know which path
236    /// ran. A second [`Backend`] implementation should follow the same
237    /// split if it also needs to run on both targets.
238    fn init(handle: impl GPUSurfaceHandle, width: u32, height: u32, sender: InitSender<Self>) {
239        #[cfg(not(target_arch = "wasm32"))]
240        {
241            pollster::block_on(Self::init_async(handle, width, height, sender));
242        }
243
244        #[cfg(target_arch = "wasm32")]
245        {
246            wasm_bindgen_futures::spawn_local(Self::init_async(handle, width, height, sender));
247        }
248    }
249
250    fn resize(&mut self, width: u32, height: u32) {
251        if width == 0 || height == 0 {
252            return; // minimized — don't reconfigure to a degenerate size
253        }
254        self.config.width = width;
255        self.config.height = height;
256        self.surface.configure(&self.device, &self.config);
257    }
258
259    fn acquire(&mut self) -> Result<Self::Frame, AcquireError> {
260        let surface_texture = match self.surface.get_current_texture() {
261            wgpu::CurrentSurfaceTexture::Success(texture) => texture,
262            wgpu::CurrentSurfaceTexture::Suboptimal(texture) => texture,
263            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Outdated => {
264                return Err(AcquireError::Transient);
265            }
266            other => {
267                return Err(AcquireError::Fatal(format!(
268                    "unexpected surface state: {other:?}"
269                )));
270            }
271        };
272
273        let view = surface_texture
274            .texture
275            .create_view(&wgpu::TextureViewDescriptor::default());
276        let encoder = self
277            .device
278            .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
279
280        Ok(WGPUFrame {
281            encoder,
282            view,
283            surface_texture,
284        })
285    }
286
287    fn present(&mut self, frame: Self::Frame) {
288        self.queue.submit(std::iter::once(frame.encoder.finish()));
289        frame.surface_texture.present();
290    }
291}
292
293pub struct WGPUPlugin {
294    config: WindowConfig,
295}
296
297impl WGPUPlugin {
298    pub fn new(config: WindowConfig) -> Self {
299        Self { config }
300    }
301}
302
303impl Plugin for WGPUPlugin {
304    fn build(&self, app: &mut App) {
305        app.add_plugin(crate::prelude::WindowPlugin::<WinitWindow>::new(
306            WindowConfig {
307                title: self.config.title.clone(),
308                width: self.config.width,
309                height: self.config.height,
310            },
311        ))
312        .add_plugin(crate::prelude::GraphicsPlugin::<WGPUBackend, WinitWindow>::new())
313        .add_plugin(crate::prelude::RenderPlugin::<WGPUBackend>::new())
314        .add_plugin(crate::wgpu::textures::TexturePlugin)
315        .add_plugin(crate::wgpu::texture_array::TextureArrayPlugin)
316        .add_plugin(crate::wgpu::cubemap::CubemapPlugin)
317        .add_plugin(crate::wgpu::mesh::MeshPlugin::new())
318        .add_plugin(crate::wgpu::material::MaterialPlugin::new())
319        .add_plugin(crate::wgpu::instance::MaterialInstancePlugin::new())
320        .add_plugin(crate::wgpu::compute::ComputePlugin::new())
321        .add_plugin(crate::wgpu::instance::ComputeInstancePlugin::new())
322        .add_plugin(crate::prelude::LazyResourcePlugin::<
323            WGPUBackend,
324            crate::wgpu::samplers::GlobalSamplers,
325        >::new());
326    }
327}