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    threading::SpawnableFuture,
11    wgpu::window::WinitWindow,
12};
13
14/// The `wgpu`-backed [`Backend`] implementation. Inserted as a resource
15/// once [`init`](Self::init) finishes (see the [`Backend`] trait docs for
16/// how that's driven); everything in [`super`] that uploads to the GPU
17/// (`Res<WGPUBackend>` in an [`Asset::upload`](crate::assets::upload::Asset::upload)
18/// impl) reads `device`/`queue` directly off this.
19///
20/// `device`/`queue` are `pub` because some operations genuinely need them
21/// (submitting command encoders, `queue.write_buffer`, resource types
22/// [`wgpu::prelude`](super::prelude) doesn't cover) — but for building a
23/// buffer, bind group layout, or bind group, reach for
24/// [`wgpu::prelude`](super::prelude) first rather than hand-writing a
25/// `wgpu::BufferDescriptor`/`BindGroupLayoutDescriptor`/`BindGroupDescriptor`
26/// against `device` directly.
27pub struct WGPUBackend {
28    pub device: wgpu::Device,
29    pub queue: wgpu::Queue,
30    pub surface: wgpu::Surface<'static>,
31    pub config: wgpu::SurfaceConfiguration,
32}
33
34impl WGPUBackend {
35    async fn init_async(
36        handle: impl GPUSurfaceHandle,
37        width: u32,
38        height: u32,
39        sender: InitSender<Self>,
40    ) {
41        let backends = if cfg!(target_arch = "wasm32") {
42            wgpu::Backends::BROWSER_WEBGPU
43        } else {
44            wgpu::Backends::PRIMARY
45        };
46
47        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
48            display: None,
49            backends,
50            flags: wgpu::InstanceFlags::default(),
51            memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
52            backend_options: wgpu::BackendOptions::default(),
53        });
54
55        let surface = instance.create_surface(handle).unwrap();
56
57        let adapter = instance
58            .request_adapter(&wgpu::RequestAdapterOptions {
59                power_preference: wgpu::PowerPreference::HighPerformance,
60                force_fallback_adapter: false,
61                compatible_surface: Some(&surface),
62            })
63            .await
64            .unwrap();
65
66        let (required_features, required_limits) = if cfg!(target_arch = "wasm32") {
67            (wgpu::Features::empty(), wgpu::Limits::defaults())
68        } else {
69            (
70                wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER,
71                wgpu::Limits::default(),
72            )
73        };
74
75        let (device, queue) = adapter
76            .request_device(&wgpu::DeviceDescriptor {
77                label: None,
78                required_features,
79                required_limits,
80                ..Default::default()
81            })
82            .await
83            .unwrap();
84
85        let caps = surface.get_capabilities(&adapter);
86        let format = caps
87            .formats
88            .iter()
89            .copied()
90            .find(|f| f.is_srgb())
91            .unwrap_or(caps.formats[0]);
92
93        // Prefer Fifo (vsync) explicitly rather than trusting caps.present_modes[0] —
94        // its ordering isn't guaranteed to put Fifo first, and an uncapped mode
95        // (Immediate/Mailbox) here would tear and burn GPU cycles for no benefit.
96        let present_mode = caps
97            .present_modes
98            .iter()
99            .copied()
100            .find(|m| *m == wgpu::PresentMode::Fifo)
101            .unwrap_or(caps.present_modes[0]);
102
103        let config = wgpu::SurfaceConfiguration {
104            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
105            format,
106            present_mode,
107            alpha_mode: caps.alpha_modes[0],
108            width,
109            height,
110            desired_maximum_frame_latency: 2,
111            view_formats: vec![],
112        };
113        surface.configure(&device, &config);
114
115        sender.send(WGPUBackend {
116            device,
117            queue,
118            surface,
119            config,
120        });
121    }
122}
123
124pub struct WGPUFrame {
125    encoder: wgpu::CommandEncoder,
126    view: wgpu::TextureView,
127    surface_texture: wgpu::SurfaceTexture,
128}
129
130impl FrameOperations for WGPUFrame {
131    type Context<'a> = wgpu::RenderPass<'a>;
132    type Attachment = wgpu::TextureView;
133    type DepthAttachment = wgpu::TextureView;
134
135    fn begin(&mut self, pass: Pass<'_, Self>) -> Self::Context<'_> {
136        let color_attachments: Vec<_> = pass
137            .colors
138            .iter()
139            .map(|target| {
140                let (view, clear) = match target {
141                    ColorTarget::Default { clear } => (&self.view, clear),
142                    ColorTarget::Custom { attachment, clear } => (*attachment, clear),
143                };
144                Some(wgpu::RenderPassColorAttachment {
145                    view,
146                    depth_slice: None,
147                    resolve_target: None,
148                    ops: wgpu::Operations {
149                        load: clear
150                            .map(|[r, g, b, a]| {
151                                wgpu::LoadOp::Clear(wgpu::Color {
152                                    r: r as f64,
153                                    g: g as f64,
154                                    b: b as f64,
155                                    a: a as f64,
156                                })
157                            })
158                            .unwrap_or(wgpu::LoadOp::Load),
159                        store: wgpu::StoreOp::Store,
160                    },
161                })
162            })
163            .collect();
164
165        let depth_stencil_attachment =
166            pass.depth
167                .as_ref()
168                .map(|d| wgpu::RenderPassDepthStencilAttachment {
169                    view: d.attachment,
170                    depth_ops: Some(wgpu::Operations {
171                        load: d
172                            .clear
173                            .map(wgpu::LoadOp::Clear)
174                            .unwrap_or(wgpu::LoadOp::Load),
175                        store: wgpu::StoreOp::Store,
176                    }),
177                    stencil_ops: None,
178                });
179
180        self.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
181            label: None,
182            color_attachments: &color_attachments,
183            depth_stencil_attachment,
184            timestamp_writes: None,
185            occlusion_query_set: None,
186            multiview_mask: None,
187        })
188    }
189}
190
191impl WGPUFrame {
192    /// Begin a compute pass on this frame's command encoder.
193    pub fn compute_pass(&mut self, label: Option<&str>) -> wgpu::ComputePass<'_> {
194        self.encoder
195            .begin_compute_pass(&wgpu::ComputePassDescriptor {
196                label,
197                timestamp_writes: None,
198            })
199    }
200}
201
202impl Backend for WGPUBackend {
203    type Frame = WGPUFrame;
204
205    /// Native blocks the calling thread on [`init_async`](Self::init_async)
206    /// via `pollster::block_on` (fine here — this only runs once, during
207    /// [`App::build`](crate::app::App::build), and there's no other work
208    /// competing for the thread yet). Web can't block its single thread, so
209    /// it hands `init_async` to `wasm_bindgen_futures::spawn_local` instead
210    /// and returns immediately — [`GraphicsPlugin`](crate::rendering::graphics_plugin::GraphicsPlugin)
211    /// polls the resulting `sender`/[`InitReceiver`](crate::rendering::sync::InitReceiver)
212    /// pair every tick either way, so callers don't need to know which path
213    /// ran. A second [`Backend`] implementation should follow the same
214    /// split if it also needs to run on both targets.
215    fn init(handle: impl GPUSurfaceHandle, width: u32, height: u32, sender: InitSender<Self>) {
216        #[cfg(not(target_arch = "wasm32"))]
217        {
218            pollster::block_on(Self::init_async(handle, width, height, sender));
219        }
220
221        #[cfg(target_arch = "wasm32")]
222        {
223            wasm_bindgen_futures::spawn_local(Self::init_async(handle, width, height, sender));
224        }
225    }
226
227    fn resize(&mut self, width: u32, height: u32) {
228        if width == 0 || height == 0 {
229            return; // minimized — don't reconfigure to a degenerate size
230        }
231        self.config.width = width;
232        self.config.height = height;
233        self.surface.configure(&self.device, &self.config);
234    }
235
236    fn acquire(&mut self) -> Result<Self::Frame, AcquireError> {
237        let surface_texture = match self.surface.get_current_texture() {
238            wgpu::CurrentSurfaceTexture::Success(texture) => texture,
239            wgpu::CurrentSurfaceTexture::Suboptimal(texture) => texture,
240            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Outdated => {
241                return Err(AcquireError::Transient);
242            }
243            other => {
244                return Err(AcquireError::Fatal(format!(
245                    "unexpected surface state: {other:?}"
246                )));
247            }
248        };
249
250        let view = surface_texture
251            .texture
252            .create_view(&wgpu::TextureViewDescriptor::default());
253        let encoder = self
254            .device
255            .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
256
257        Ok(WGPUFrame {
258            encoder,
259            view,
260            surface_texture,
261        })
262    }
263
264    fn present(&mut self, frame: Self::Frame) {
265        self.queue.submit(std::iter::once(frame.encoder.finish()));
266        frame.surface_texture.present();
267    }
268}
269
270impl WGPUBackend {
271    /// Copies `src` into a temporary staging buffer, begins a GPU readback,
272    /// and returns a future that resolves to the copied bytes once it's
273    /// done.
274    ///
275    /// The copy is submitted eagerly, right away — do not call mid-frame;
276    /// call after `present` or outside of frame encoding. Only the *wait
277    /// for the GPU to finish mapping it* is deferred into the returned
278    /// future.
279    ///
280    /// This doesn't run itself — drive it with
281    /// [`AsyncEventWriter::spawn`](crate::prelude::AsyncEventWriter::spawn) to get the
282    /// result delivered as an event, or
283    /// [`BackgroundTasks::spawn_async`](crate::threading::BackgroundTasks::spawn_async)
284    /// directly if you'd rather hold onto a
285    /// [`TaskHandle`](crate::threading::TaskHandle) and poll it yourself.
286    pub fn readback_buffer(&self, src: &wgpu::Buffer) -> impl SpawnableFuture<Vec<u8>> {
287        use crate::wgpu::buffers::BufferBuilder;
288
289        let size = src.size();
290        let staging = BufferBuilder::new()
291            .usage(wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ)
292            .size(size)
293            .build(&self.device);
294
295        let mut encoder = self
296            .device
297            .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
298        encoder.copy_buffer_to_buffer(src, 0, &staging, 0, size);
299        let idx = self.queue.submit(std::iter::once(encoder.finish()));
300
301        #[cfg(not(target_arch = "wasm32"))]
302        let device = self.device.clone();
303
304        async move {
305            #[cfg(not(target_arch = "wasm32"))]
306            {
307                let (tx, rx) = std::sync::mpsc::channel();
308                staging.slice(..).map_async(wgpu::MapMode::Read, move |r| {
309                    let _ = tx.send(r);
310                });
311                // Native backends need an explicit poll for a queued
312                // map_async callback to ever fire — nothing else drives
313                // that here, so this blocks whichever thread is driving the
314                // future until the mapping lands. Fine: this is meant to
315                // run via `BackgroundTasks::spawn_async`, which already
316                // dedicates a worker thread to exactly this kind of wait.
317                let _ = device.poll(wgpu::PollType::Wait {
318                    submission_index: Some(idx),
319                    timeout: None,
320                });
321                rx.recv().unwrap().unwrap();
322                let data = staging.slice(..).get_mapped_range().to_vec();
323                staging.unmap();
324                data
325            }
326
327            #[cfg(target_arch = "wasm32")]
328            {
329                let _ = idx;
330                let mapped: std::sync::Arc<
331                    std::sync::Mutex<Option<Result<(), wgpu::BufferAsyncError>>>,
332                > = std::sync::Arc::new(std::sync::Mutex::new(None));
333                let waker: std::sync::Arc<std::sync::Mutex<Option<std::task::Waker>>> =
334                    std::sync::Arc::new(std::sync::Mutex::new(None));
335
336                let mapped_cb = mapped.clone();
337                let waker_cb = waker.clone();
338                staging.slice(..).map_async(wgpu::MapMode::Read, move |r| {
339                    *mapped_cb.lock().unwrap() = Some(r);
340                    if let Some(w) = waker_cb.lock().unwrap().take() {
341                        w.wake();
342                    }
343                });
344
345                std::future::poll_fn(|cx| {
346                    let mut guard = mapped.lock().unwrap();
347                    if let Some(r) = guard.take() {
348                        std::task::Poll::Ready(r)
349                    } else {
350                        *waker.lock().unwrap() = Some(cx.waker().clone());
351                        std::task::Poll::Pending
352                    }
353                })
354                .await
355                .unwrap();
356
357                let data = staging.slice(..).get_mapped_range().to_vec();
358                staging.unmap();
359                data
360            }
361        }
362    }
363
364    /// Same as [`readback_buffer`](Self::readback_buffer) but the resolved
365    /// bytes are cast to `T`.
366    pub fn readback_buffer_as<T: bytemuck::Pod + Send + 'static>(
367        &self,
368        src: &wgpu::Buffer,
369    ) -> impl SpawnableFuture<Vec<T>> {
370        let bytes = self.readback_buffer(src);
371        async move {
372            let bytes = bytes.await;
373            bytemuck::cast_slice(&bytes).to_vec()
374        }
375    }
376}
377
378pub struct WGPUPlugin {
379    config: WindowConfig,
380}
381
382impl WGPUPlugin {
383    pub fn new(config: WindowConfig) -> Self {
384        Self { config }
385    }
386}
387
388impl Plugin for WGPUPlugin {
389    fn build(&self, app: &mut App) {
390        app.add_plugin(crate::prelude::WindowPlugin::<WinitWindow>::new(
391            WindowConfig {
392                title: self.config.title.clone(),
393                width: self.config.width,
394                height: self.config.height,
395            },
396        ))
397        .add_plugin(crate::prelude::GraphicsPlugin::<WGPUBackend, WinitWindow>::new())
398        .add_plugin(crate::prelude::RenderPlugin::<WGPUBackend>::new())
399        .add_plugin(crate::wgpu::textures::TexturePlugin)
400        .add_plugin(crate::wgpu::texture_array::TextureArrayPlugin)
401        .add_plugin(crate::wgpu::cubemap::CubemapPlugin)
402        .add_plugin(crate::wgpu::mesh::MeshPlugin::new())
403        .add_plugin(crate::wgpu::material::MaterialPlugin::new())
404        .add_plugin(crate::wgpu::instance::MaterialInstancePlugin::new())
405        .add_plugin(crate::wgpu::compute::ComputePlugin::new())
406        .add_plugin(crate::wgpu::instance::ComputeInstancePlugin::new())
407        .add_plugin(crate::prelude::LazyResourcePlugin::<
408            WGPUBackend,
409            crate::wgpu::samplers::GlobalSamplers,
410        >::new());
411    }
412}