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