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