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::{
11 compute_pass::{CommandEncoder, ComputePass},
12 render_bundle::{RenderBundleEncoder, RenderBundleEncoderDescriptor},
13 render_pass::RenderPass,
14 texture_format::TextureFormat,
15 texture_view::TextureView,
16 window::WinitWindow,
17 },
18};
19
20pub struct WGPUBackend {
33 pub(crate) device: wgpu::Device,
34 pub(crate) queue: wgpu::Queue,
35 pub(crate) surface: wgpu::Surface<'static>,
36 pub(crate) config: wgpu::SurfaceConfiguration,
37 msaa_sample_count: u32,
38 msaa_color: Option<wgpu::TextureView>,
39}
40
41impl WGPUBackend {
42 pub fn surface_width(&self) -> u32 {
44 self.config.width
45 }
46
47 pub fn surface_height(&self) -> u32 {
49 self.config.height
50 }
51
52 pub fn surface_format(&self) -> TextureFormat {
55 self.config.format.into()
56 }
57
58 pub fn sample_count(&self) -> u32 {
64 self.msaa_sample_count
65 }
66
67 pub fn set_msaa(&mut self, sample_count: u32) {
79 self.msaa_sample_count = sample_count;
80 self.rebuild_msaa_color();
81 }
82
83 fn rebuild_msaa_color(&mut self) {
84 if self.msaa_sample_count <= 1 {
85 self.msaa_color = None;
86 return;
87 }
88 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
89 label: Some("pebble-msaa-color"),
90 size: wgpu::Extent3d { width: self.config.width, height: self.config.height, depth_or_array_layers: 1 },
91 mip_level_count: 1,
92 sample_count: self.msaa_sample_count,
93 dimension: wgpu::TextureDimension::D2,
94 format: self.config.format,
95 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
96 view_formats: &[],
97 });
98 self.msaa_color = Some(texture.create_view(&wgpu::TextureViewDescriptor::default()));
99 }
100}
101
102impl WGPUBackend {
103 async fn init_async(
104 handle: impl GPUSurfaceHandle,
105 width: u32,
106 height: u32,
107 sender: InitSender<Self>,
108 ) {
109 let backends = if cfg!(target_arch = "wasm32") {
110 wgpu::Backends::BROWSER_WEBGPU
111 } else {
112 wgpu::Backends::PRIMARY
113 };
114
115 let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
116 display: None,
117 backends,
118 flags: wgpu::InstanceFlags::default(),
119 memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
120 backend_options: wgpu::BackendOptions::default(),
121 });
122
123 let surface = instance.create_surface(handle).unwrap();
124
125 let adapter = instance
126 .request_adapter(&wgpu::RequestAdapterOptions {
127 power_preference: wgpu::PowerPreference::HighPerformance,
128 force_fallback_adapter: false,
129 compatible_surface: Some(&surface),
130 })
131 .await
132 .unwrap();
133
134 let (required_features, required_limits) = if cfg!(target_arch = "wasm32") {
135 (wgpu::Features::empty(), wgpu::Limits::defaults())
136 } else {
137 (
138 wgpu::Features::ADDRESS_MODE_CLAMP_TO_BORDER,
139 wgpu::Limits::default(),
140 )
141 };
142
143 let (device, queue) = adapter
144 .request_device(&wgpu::DeviceDescriptor {
145 label: None,
146 required_features,
147 required_limits,
148 ..Default::default()
149 })
150 .await
151 .unwrap();
152
153 let caps = surface.get_capabilities(&adapter);
154 let format = caps
155 .formats
156 .iter()
157 .copied()
158 .find(|f| f.is_srgb())
159 .unwrap_or(caps.formats[0]);
160
161 let present_mode = caps
165 .present_modes
166 .iter()
167 .copied()
168 .find(|m| *m == wgpu::PresentMode::Fifo)
169 .unwrap_or(caps.present_modes[0]);
170
171 let config = wgpu::SurfaceConfiguration {
172 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
173 format,
174 present_mode,
175 alpha_mode: caps.alpha_modes[0],
176 width,
177 height,
178 desired_maximum_frame_latency: 2,
179 view_formats: vec![],
180 };
181 surface.configure(&device, &config);
182
183 sender.send(WGPUBackend {
184 device,
185 queue,
186 surface,
187 config,
188 msaa_sample_count: 1,
189 msaa_color: None,
190 });
191 }
192}
193
194pub struct WGPUFrame {
195 encoder: wgpu::CommandEncoder,
196 view: wgpu::TextureView,
197 surface_texture: wgpu::SurfaceTexture,
198 msaa_view: Option<wgpu::TextureView>,
203}
204
205impl FrameOperations for WGPUFrame {
206 type Context<'a> = RenderPass<'a>;
207 type Attachment = TextureView;
208 type DepthAttachment = TextureView;
209
210 fn begin(&mut self, pass: Pass<'_, Self>) -> Self::Context<'_> {
211 let color_attachments: Vec<_> = pass
212 .colors
213 .iter()
214 .map(|target| {
215 let (view, resolve_target, clear) = match target {
216 ColorTarget::Default { clear } => match &self.msaa_view {
217 Some(msaa) => (msaa, Some(&self.view), clear),
218 None => (&self.view, None, clear),
219 },
220 ColorTarget::Custom { attachment, clear } => (attachment.raw(), None, clear),
221 };
222 Some(wgpu::RenderPassColorAttachment {
223 view,
224 depth_slice: None,
225 resolve_target,
226 ops: wgpu::Operations {
227 load: clear
228 .map(|[r, g, b, a]| {
229 wgpu::LoadOp::Clear(wgpu::Color {
230 r: r as f64,
231 g: g as f64,
232 b: b as f64,
233 a: a as f64,
234 })
235 })
236 .unwrap_or(wgpu::LoadOp::Load),
237 store: wgpu::StoreOp::Store,
238 },
239 })
240 })
241 .collect();
242
243 let depth_stencil_attachment =
244 pass.depth
245 .as_ref()
246 .map(|d| wgpu::RenderPassDepthStencilAttachment {
247 view: d.attachment.raw(),
248 depth_ops: Some(wgpu::Operations {
249 load: d
250 .clear
251 .map(wgpu::LoadOp::Clear)
252 .unwrap_or(wgpu::LoadOp::Load),
253 store: wgpu::StoreOp::Store,
254 }),
255 stencil_ops: None,
256 });
257
258 let raw = self.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
259 label: None,
260 color_attachments: &color_attachments,
261 depth_stencil_attachment,
262 timestamp_writes: None,
263 occlusion_query_set: None,
264 multiview_mask: None,
265 });
266 RenderPass::new(raw)
267 }
268}
269
270impl WGPUFrame {
271 pub fn compute_pass(&mut self, label: Option<&str>) -> ComputePass<'_> {
273 let raw = self.encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
274 label,
275 timestamp_writes: None,
276 });
277 ComputePass::new(raw)
278 }
279}
280
281impl WGPUBackend {
282 pub fn create_command_encoder(&self, label: Option<&str>) -> CommandEncoder {
290 CommandEncoder::new(self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }))
291 }
292
293 pub fn submit(&self, encoder: CommandEncoder) {
295 self.queue.submit(std::iter::once(encoder.into_raw().finish()));
296 }
297
298 pub fn create_render_bundle_encoder(&self, desc: &RenderBundleEncoderDescriptor) -> RenderBundleEncoder<'_> {
301 let color_formats: Vec<Option<wgpu::TextureFormat>> =
302 desc.color_formats.iter().map(|f| f.map(Into::into)).collect();
303 let depth_stencil = desc.depth_stencil_format.map(|format| wgpu::RenderBundleDepthStencil {
304 format: format.into(),
305 depth_read_only: desc.depth_read_only,
306 stencil_read_only: desc.stencil_read_only,
307 });
308 let raw = self.device.create_render_bundle_encoder(&wgpu::RenderBundleEncoderDescriptor {
309 label: desc.label,
310 color_formats: &color_formats,
311 depth_stencil,
312 sample_count: desc.sample_count,
313 multiview: None,
314 });
315 RenderBundleEncoder::new(raw)
316 }
317}
318
319impl Backend for WGPUBackend {
320 type Frame = WGPUFrame;
321
322 fn init(handle: impl GPUSurfaceHandle, width: u32, height: u32, sender: InitSender<Self>) {
333 #[cfg(not(target_arch = "wasm32"))]
334 {
335 pollster::block_on(Self::init_async(handle, width, height, sender));
336 }
337
338 #[cfg(target_arch = "wasm32")]
339 {
340 wasm_bindgen_futures::spawn_local(Self::init_async(handle, width, height, sender));
341 }
342 }
343
344 fn resize(&mut self, width: u32, height: u32) {
345 if width == 0 || height == 0 {
346 return; }
348 self.config.width = width;
349 self.config.height = height;
350 self.surface.configure(&self.device, &self.config);
351 self.rebuild_msaa_color();
352 }
353
354 fn acquire(&mut self) -> Result<Self::Frame, AcquireError> {
355 let surface_texture = match self.surface.get_current_texture() {
356 wgpu::CurrentSurfaceTexture::Success(texture) => texture,
357 wgpu::CurrentSurfaceTexture::Suboptimal(texture) => texture,
358 wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Outdated => {
359 return Err(AcquireError::Transient);
360 }
361 other => {
362 return Err(AcquireError::Fatal(format!(
363 "unexpected surface state: {other:?}"
364 )));
365 }
366 };
367
368 let view = surface_texture
369 .texture
370 .create_view(&wgpu::TextureViewDescriptor::default());
371 let encoder = self
372 .device
373 .create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
374
375 Ok(WGPUFrame {
376 encoder,
377 view,
378 surface_texture,
379 msaa_view: self.msaa_color.clone(),
380 })
381 }
382
383 fn present(&mut self, frame: Self::Frame) {
384 self.queue.submit(std::iter::once(frame.encoder.finish()));
385 frame.surface_texture.present();
386 }
387}
388
389pub struct WGPUPlugin {
390 config: WindowConfig,
391}
392
393impl WGPUPlugin {
394 pub fn new(config: WindowConfig) -> Self {
395 Self { config }
396 }
397}
398
399impl Plugin for WGPUPlugin {
400 fn build(&self, app: &mut App) {
401 app.add_plugin(crate::prelude::WindowPlugin::<WinitWindow>::new(
402 WindowConfig {
403 title: self.config.title.clone(),
404 width: self.config.width,
405 height: self.config.height,
406 },
407 ))
408 .add_plugin(crate::prelude::GraphicsPlugin::<WGPUBackend, WinitWindow>::new())
409 .add_plugin(crate::prelude::RenderPlugin::<WGPUBackend>::new())
410 .add_plugin(crate::wgpu::textures::TexturePlugin)
411 .add_plugin(crate::wgpu::texture_array::TextureArrayPlugin)
412 .add_plugin(crate::wgpu::cubemap::CubemapPlugin)
413 .add_plugin(crate::wgpu::mesh::MeshPlugin::new())
414 .add_plugin(crate::wgpu::material::MaterialPlugin::new())
415 .add_plugin(crate::wgpu::instance::MaterialInstancePlugin::new())
416 .add_plugin(crate::wgpu::compute::ComputePlugin::new())
417 .add_plugin(crate::wgpu::instance::ComputeInstancePlugin::new())
418 .add_plugin(crate::prelude::LazyResourcePlugin::<
419 WGPUBackend,
420 crate::wgpu::samplers::GlobalSamplers,
421 >::new());
422 }
423}