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