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        let config = wgpu::SurfaceConfiguration {
80            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
81            format,
82            present_mode: caps.present_modes[0],
83            alpha_mode: caps.alpha_modes[0],
84            width,
85            height,
86            desired_maximum_frame_latency: 2,
87            view_formats: vec![],
88        };
89        surface.configure(&device, &config);
90
91        sender.send(WGPUBackend {
92            device,
93            queue,
94            surface,
95            config,
96        });
97    }
98}
99
100pub struct WGPUFrame {
101    encoder: wgpu::CommandEncoder,
102    view: wgpu::TextureView,
103    surface_texture: wgpu::SurfaceTexture,
104}
105
106impl FrameOperations for WGPUFrame {
107    type Context<'a> = wgpu::RenderPass<'a>;
108    type Attachment = wgpu::TextureView;
109    type DepthAttachment = wgpu::TextureView;
110
111    fn begin(&mut self, pass: Pass<'_, Self>) -> Self::Context<'_> {
112        let color_attachments: Vec<_> = pass
113            .colors
114            .iter()
115            .map(|target| {
116                let (view, clear) = match target {
117                    ColorTarget::Default { clear } => (&self.view, clear),
118                    ColorTarget::Custom { attachment, clear } => (*attachment, clear),
119                };
120                Some(wgpu::RenderPassColorAttachment {
121                    view,
122                    depth_slice: None,
123                    resolve_target: None,
124                    ops: wgpu::Operations {
125                        load: clear
126                            .map(|[r, g, b, a]| {
127                                wgpu::LoadOp::Clear(wgpu::Color {
128                                    r: r as f64,
129                                    g: g as f64,
130                                    b: b as f64,
131                                    a: a as f64,
132                                })
133                            })
134                            .unwrap_or(wgpu::LoadOp::Load),
135                        store: wgpu::StoreOp::Store,
136                    },
137                })
138            })
139            .collect();
140
141        let depth_stencil_attachment =
142            pass.depth
143                .as_ref()
144                .map(|d| wgpu::RenderPassDepthStencilAttachment {
145                    view: d.attachment,
146                    depth_ops: Some(wgpu::Operations {
147                        load: d
148                            .clear
149                            .map(wgpu::LoadOp::Clear)
150                            .unwrap_or(wgpu::LoadOp::Load),
151                        store: wgpu::StoreOp::Store,
152                    }),
153                    stencil_ops: None,
154                });
155
156        self.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
157            label: None,
158            color_attachments: &color_attachments,
159            depth_stencil_attachment,
160            timestamp_writes: None,
161            occlusion_query_set: None,
162            multiview_mask: None,
163        })
164    }
165}
166
167impl WGPUFrame {
168    /// Begin a compute pass on this frame's command encoder.
169    pub fn compute_pass(&mut self, label: Option<&str>) -> wgpu::ComputePass<'_> {
170        self.encoder
171            .begin_compute_pass(&wgpu::ComputePassDescriptor {
172                label,
173                timestamp_writes: None,
174            })
175    }
176}
177
178impl Backend for WGPUBackend {
179    type Frame = WGPUFrame;
180
181    fn init(handle: impl GPUSurfaceHandle, width: u32, height: u32, sender: InitSender<Self>) {
182        #[cfg(not(target_arch = "wasm32"))]
183        {
184            pollster::block_on(Self::init_async(handle, width, height, sender));
185        }
186
187        #[cfg(target_arch = "wasm32")]
188        {
189            wasm_bindgen_futures::spawn_local(Self::init_async(handle, width, height, sender));
190        }
191    }
192
193    fn resize(&mut self, width: u32, height: u32) {
194        if width == 0 || height == 0 {
195            return; // minimized — don't reconfigure to a degenerate size
196        }
197        self.config.width = width;
198        self.config.height = height;
199        self.surface.configure(&self.device, &self.config);
200    }
201
202    fn acquire(&mut self) -> Result<Self::Frame, AcquireError> {
203        let surface_texture = match self.surface.get_current_texture() {
204            wgpu::CurrentSurfaceTexture::Success(texture) => texture,
205            wgpu::CurrentSurfaceTexture::Suboptimal(texture) => texture,
206            wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Outdated => {
207                return Err(AcquireError::Transient);
208            }
209            other => {
210                return Err(AcquireError::Fatal(format!(
211                    "unexpected surface state: {other:?}"
212                )));
213            }
214        };
215
216        let view = surface_texture
217            .texture
218            .create_view(&wgpu::TextureViewDescriptor::default());
219        let encoder = self
220            .device
221            .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
222
223        Ok(WGPUFrame {
224            encoder,
225            view,
226            surface_texture,
227        })
228    }
229
230    fn present(&mut self, frame: Self::Frame) {
231        self.queue.submit(std::iter::once(frame.encoder.finish()));
232        frame.surface_texture.present();
233    }
234}
235
236pub struct WGPUPlugin {
237    config: WindowConfig,
238}
239
240impl WGPUPlugin {
241    pub fn new(config: WindowConfig) -> Self {
242        Self { config }
243    }
244}
245
246impl Plugin for WGPUPlugin {
247    fn build(&self, app: &mut App) {
248        app.add_plugin(crate::prelude::WindowPlugin::<WinitWindow>::new(
249            WindowConfig {
250                title: self.config.title,
251                width: self.config.width,
252                height: self.config.height,
253            },
254        ))
255        .add_plugin(crate::prelude::GraphicsPlugin::<WGPUBackend, WinitWindow>::new())
256        .add_plugin(crate::prelude::RenderPlugin::<WGPUBackend>::new())
257        .add_plugin(crate::wgpu::textures::TexturePlugin)
258        .add_plugin(crate::wgpu::texture_array::TextureArrayPlugin)
259        .add_plugin(crate::wgpu::cubemap::CubemapPlugin)
260        .add_plugin(crate::wgpu::mesh::MeshPlugin::new())
261        .add_plugin(crate::wgpu::material::MaterialPlugin::new())
262        .add_plugin(crate::wgpu::material_instance::MaterialInstancePlugin::new())
263        .add_plugin(crate::wgpu::compute::ComputePlugin::new())
264        .add_plugin(crate::prelude::LazyResourcePlugin::<
265            WGPUBackend,
266            crate::wgpu::samplers::GlobalSamplers,
267        >::new());
268    }
269}