Another project
1use crate::camera::ViewportExtent;
2use crate::gpu::{Capabilities, Gpu};
3use crate::pick::{PickIndex, Picker};
4
5const PICK_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint;
6
7#[derive(Debug, thiserror::Error)]
8pub enum SurfaceError {
9 #[error("surface creation: {0}")]
10 Create(#[from] wgpu::CreateSurfaceError),
11 #[error("no wgpu adapter matched the surface: {0}")]
12 NoAdapter(#[from] wgpu::RequestAdapterError),
13 #[error("wgpu device request failed: {0}")]
14 Device(#[from] wgpu::RequestDeviceError),
15 #[error(
16 "surface exposes no srgb color format; baseline requires one. available: {available:?}"
17 )]
18 NoSrgbFormat { available: Vec<wgpu::TextureFormat> },
19 #[error("viewport dimension is zero")]
20 ZeroExtent,
21}
22
23pub struct SurfaceContext {
24 gpu: Gpu,
25 surface: wgpu::Surface<'static>,
26 config: wgpu::SurfaceConfiguration,
27 pick: wgpu::Texture,
28 pick_staging: wgpu::Buffer,
29 depth: wgpu::Texture,
30 extent: ViewportExtent,
31 reconfigure_pending: bool,
32}
33
34enum AcquireOutcome {
35 Frame(wgpu::SurfaceTexture),
36 Skip,
37 Reconfigure,
38}
39
40impl SurfaceContext {
41 pub async fn new(
42 target: impl Into<wgpu::SurfaceTarget<'static>>,
43 extent: ViewportExtent,
44 ) -> Result<Self, SurfaceError> {
45 if extent.width().value() == 0 || extent.height().value() == 0 {
46 return Err(SurfaceError::ZeroExtent);
47 }
48 let instance =
49 wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
50 let target: wgpu::SurfaceTarget<'static> = target.into();
51 let surface = instance.create_surface(target)?;
52 let adapter = instance
53 .request_adapter(&wgpu::RequestAdapterOptions {
54 power_preference: wgpu::PowerPreference::LowPower,
55 force_fallback_adapter: false,
56 compatible_surface: Some(&surface),
57 })
58 .await?;
59 let (device, queue) = adapter
60 .request_device(&wgpu::DeviceDescriptor {
61 label: Some("bone-render:surface"),
62 required_features: wgpu::Features::empty(),
63 required_limits: wgpu::Limits::downlevel_defaults(),
64 experimental_features: wgpu::ExperimentalFeatures::default(),
65 memory_hints: wgpu::MemoryHints::default(),
66 trace: wgpu::Trace::Off,
67 })
68 .await?;
69 let capabilities = Capabilities::probe(&adapter);
70 let caps = surface.get_capabilities(&adapter);
71 let Some(format) = caps
72 .formats
73 .iter()
74 .copied()
75 .find(wgpu::TextureFormat::is_srgb)
76 else {
77 return Err(SurfaceError::NoSrgbFormat {
78 available: caps.formats.clone(),
79 });
80 };
81 let present_mode = pick_present_mode(&caps.present_modes);
82 let alpha_mode = caps
83 .alpha_modes
84 .first()
85 .copied()
86 .unwrap_or(wgpu::CompositeAlphaMode::Auto);
87 let config = wgpu::SurfaceConfiguration {
88 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
89 format,
90 width: extent.width().value(),
91 height: extent.height().value(),
92 present_mode,
93 alpha_mode,
94 view_formats: vec![],
95 desired_maximum_frame_latency: 1,
96 };
97 surface.configure(&device, &config);
98 let pick = create_pick_texture(&device, extent);
99 let pick_staging = device.create_buffer(&wgpu::BufferDescriptor {
100 label: Some("bone-render:surface-pick-readback"),
101 size: u64::from(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT),
102 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
103 mapped_at_creation: false,
104 });
105 let depth = crate::depth_texture(&device, extent);
106 Ok(Self {
107 gpu: Gpu::from_parts(device, queue, capabilities),
108 surface,
109 config,
110 pick,
111 pick_staging,
112 depth,
113 extent,
114 reconfigure_pending: false,
115 })
116 }
117
118 #[must_use]
119 pub fn gpu(&self) -> &Gpu {
120 &self.gpu
121 }
122
123 #[must_use]
124 pub const fn extent(&self) -> ViewportExtent {
125 self.extent
126 }
127
128 #[must_use]
129 pub const fn color_format(&self) -> wgpu::TextureFormat {
130 self.config.format
131 }
132
133 #[must_use]
134 pub fn picker(&self, index: PickIndex) -> Picker<'_> {
135 Picker::new(
136 &self.gpu,
137 &self.pick,
138 &self.pick_staging,
139 self.extent,
140 index,
141 )
142 }
143
144 pub fn resize(&mut self, extent: ViewportExtent) {
145 if extent.width().value() == 0 || extent.height().value() == 0 {
146 return;
147 }
148 self.extent = extent;
149 self.config.width = extent.width().value();
150 self.config.height = extent.height().value();
151 self.surface.configure(self.gpu.device(), &self.config);
152 self.pick = create_pick_texture(self.gpu.device(), extent);
153 self.depth = crate::depth_texture(self.gpu.device(), extent);
154 self.reconfigure_pending = false;
155 }
156
157 pub fn render<F, G>(&mut self, build_passes: F, pre_present: G)
158 where
159 F: FnOnce(
160 &mut wgpu::CommandEncoder,
161 &wgpu::TextureView,
162 &wgpu::TextureView,
163 &wgpu::TextureView,
164 ),
165 G: FnOnce(),
166 {
167 let Some(frame) = self.acquire_frame() else {
168 return;
169 };
170 let color_view = frame
171 .texture
172 .create_view(&wgpu::TextureViewDescriptor::default());
173 let pick_view = self
174 .pick
175 .create_view(&wgpu::TextureViewDescriptor::default());
176 let depth_view = self
177 .depth
178 .create_view(&wgpu::TextureViewDescriptor::default());
179 let mut encoder =
180 self.gpu
181 .device()
182 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
183 label: Some("bone-render:surface-encoder"),
184 });
185 build_passes(&mut encoder, &color_view, &pick_view, &depth_view);
186 self.gpu.queue().submit(Some(encoder.finish()));
187 pre_present();
188 frame.present();
189 }
190
191 fn acquire_frame(&mut self) -> Option<wgpu::SurfaceTexture> {
192 if self.reconfigure_pending {
193 self.surface.configure(self.gpu.device(), &self.config);
194 self.reconfigure_pending = false;
195 }
196 match self.classify_acquire() {
197 AcquireOutcome::Frame(frame) => Some(frame),
198 AcquireOutcome::Skip => None,
199 AcquireOutcome::Reconfigure => {
200 self.surface.configure(self.gpu.device(), &self.config);
201 match self.classify_acquire() {
202 AcquireOutcome::Frame(frame) => Some(frame),
203 AcquireOutcome::Skip => None,
204 AcquireOutcome::Reconfigure => {
205 tracing::warn!(
206 "surface still outdated/lost after reconfigure; skipping frame"
207 );
208 None
209 }
210 }
211 }
212 }
213 }
214
215 fn classify_acquire(&mut self) -> AcquireOutcome {
216 match self.surface.get_current_texture() {
217 wgpu::CurrentSurfaceTexture::Success(frame) => AcquireOutcome::Frame(frame),
218 wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
219 self.reconfigure_pending = true;
220 AcquireOutcome::Frame(frame)
221 }
222 wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
223 AcquireOutcome::Reconfigure
224 }
225 wgpu::CurrentSurfaceTexture::Timeout => {
226 tracing::warn!("surface frame acquire timed out; skipping frame");
227 AcquireOutcome::Skip
228 }
229 wgpu::CurrentSurfaceTexture::Occluded => {
230 tracing::debug!("surface occluded; skipping frame");
231 AcquireOutcome::Skip
232 }
233 wgpu::CurrentSurfaceTexture::Validation => {
234 tracing::warn!("surface validation error on acquire; skipping frame");
235 AcquireOutcome::Skip
236 }
237 }
238 }
239}
240
241fn create_pick_texture(device: &wgpu::Device, extent: ViewportExtent) -> wgpu::Texture {
242 device.create_texture(&wgpu::TextureDescriptor {
243 label: Some("bone-render:surface-pick"),
244 size: wgpu::Extent3d {
245 width: extent.width().value(),
246 height: extent.height().value(),
247 depth_or_array_layers: 1,
248 },
249 mip_level_count: 1,
250 sample_count: 1,
251 dimension: wgpu::TextureDimension::D2,
252 format: PICK_FORMAT,
253 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
254 view_formats: &[],
255 })
256}
257
258fn pick_present_mode(modes: &[wgpu::PresentMode]) -> wgpu::PresentMode {
259 [
260 wgpu::PresentMode::Mailbox,
261 wgpu::PresentMode::Immediate,
262 wgpu::PresentMode::FifoRelaxed,
263 ]
264 .into_iter()
265 .find(|m| modes.contains(m))
266 .unwrap_or(wgpu::PresentMode::Fifo)
267}