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