par_term_render/cell_renderer/
surface.rs1use super::CellRenderer;
2use crate::wgpu_conversions::VsyncModeWgpu;
3use std::sync::atomic::{AtomicU64, Ordering};
4
5pub fn clamp_surface_extent(width: u32, height: u32, max_dimension: u32) -> (u32, u32) {
15 (
16 width.min(max_dimension).max(1),
17 height.min(max_dimension).max(1),
18 )
19}
20
21pub fn texture_limits(max_texture_dimension_2d: u32) -> wgpu::Limits {
24 wgpu::Limits {
25 max_texture_dimension_2d,
26 ..wgpu::Limits::default()
27 }
28}
29
30pub fn install_nonfatal_error_handler(device: &wgpu::Device) {
38 let seen = AtomicU64::new(0);
39 device.on_uncaptured_error(std::sync::Arc::new(move |err: wgpu::Error| {
40 let count = seen.fetch_add(1, Ordering::Relaxed) + 1;
41 if should_log_uncaptured_error(count) {
42 log::error!("wgpu uncaptured error #{count}: {err}");
43 }
44 }));
45}
46
47fn should_log_uncaptured_error(count: u64) -> bool {
50 count == 1 || count.is_multiple_of(1_000)
51}
52
53fn is_vsync_throttled(mode: wgpu::PresentMode) -> bool {
55 matches!(
56 mode,
57 wgpu::PresentMode::Fifo | wgpu::PresentMode::FifoRelaxed
58 )
59}
60
61pub fn alternate_present_mode(
72 target: wgpu::PresentMode,
73 supported: &[wgpu::PresentMode],
74) -> Option<wgpu::PresentMode> {
75 let preferred: &[wgpu::PresentMode] = if is_vsync_throttled(target) {
76 &[wgpu::PresentMode::Immediate, wgpu::PresentMode::Mailbox]
77 } else {
78 &[wgpu::PresentMode::Fifo, wgpu::PresentMode::FifoRelaxed]
79 };
80 preferred
81 .iter()
82 .copied()
83 .find(|m| *m != target && supported.contains(m))
84 .or_else(|| supported.iter().copied().find(|m| *m != target))
85}
86
87pub fn refresh_config_after_display_change(
97 config: &mut wgpu::SurfaceConfiguration,
98 formats: &[wgpu::TextureFormat],
99 present_modes: &[wgpu::PresentMode],
100 alpha_modes: &[wgpu::CompositeAlphaMode],
101 width: u32,
102 height: u32,
103 max_dimension: u32,
104) {
105 if !formats.contains(&config.format) {
106 let fallback = formats
107 .iter()
108 .copied()
109 .find(|f| !f.is_srgb())
110 .or_else(|| formats.first().copied());
111 if let Some(fallback) = fallback {
112 log::warn!(
113 "Surface format {:?} no longer supported after display change; switching to {:?} (render pipelines were built for the old format)",
114 config.format,
115 fallback
116 );
117 config.format = fallback;
118 }
119 }
120
121 if !present_modes.contains(&config.present_mode) {
122 let fallback = if present_modes.contains(&wgpu::PresentMode::Fifo) {
123 Some(wgpu::PresentMode::Fifo)
124 } else {
125 present_modes.first().copied()
126 };
127 if let Some(fallback) = fallback {
128 log::warn!(
129 "Present mode {:?} no longer supported after display change; falling back to {:?}",
130 config.present_mode,
131 fallback
132 );
133 config.present_mode = fallback;
134 }
135 }
136
137 if !alpha_modes.contains(&config.alpha_mode) {
138 let fallback = [
139 wgpu::CompositeAlphaMode::PreMultiplied,
140 wgpu::CompositeAlphaMode::PostMultiplied,
141 wgpu::CompositeAlphaMode::Auto,
142 ]
143 .into_iter()
144 .find(|m| alpha_modes.contains(m))
145 .or_else(|| alpha_modes.first().copied());
146 if let Some(fallback) = fallback {
147 log::warn!(
148 "Alpha mode {:?} no longer supported after display change; falling back to {:?}",
149 config.alpha_mode,
150 fallback
151 );
152 config.alpha_mode = fallback;
153 }
154 }
155
156 let (w, h) = clamp_surface_extent(width, height, max_dimension);
157 config.width = w;
158 config.height = h;
159}
160
161impl CellRenderer {
162 pub fn reconfigure_surface(&mut self) {
163 self.surface.configure(&self.device, &self.config);
164 }
165
166 pub fn reconfigure_after_display_change(&mut self, width: u32, height: u32) {
182 let caps = self.surface.get_capabilities(&self.adapter);
183 let mut refreshed = self.config.clone();
184 refresh_config_after_display_change(
185 &mut refreshed,
186 &caps.formats,
187 &caps.present_modes,
188 &caps.alpha_modes,
189 width,
190 height,
191 self.device.limits().max_texture_dimension_2d,
192 );
193 self.supported_present_modes = caps.present_modes.clone();
194
195 let target = refreshed.present_mode;
196 match alternate_present_mode(target, &self.supported_present_modes) {
197 Some(alternate) => {
198 log::info!(
199 "Display-change heal: cycling present mode {:?} -> {:?}, config {} by fresh capabilities",
200 target,
201 alternate,
202 if refreshed == self.config {
203 "unchanged"
204 } else {
205 "changed"
206 }
207 );
208 self.config = refreshed;
209 self.config.present_mode = alternate;
210 self.surface.configure(&self.device, &self.config);
211 self.config.present_mode = target;
212 self.surface.configure(&self.device, &self.config);
213 }
214 None if refreshed == self.config => {
215 log::warn!(
219 "Display-change heal: no alternate present mode and fresh capabilities unchanged; skipping configure"
220 );
221 }
222 None => {
223 log::info!(
224 "Display-change heal: no alternate present mode; applying refreshed config"
225 );
226 self.config = refreshed;
227 self.surface.configure(&self.device, &self.config);
228 }
229 }
230 }
231
232 pub fn supported_present_modes(&self) -> &[wgpu::PresentMode] {
234 &self.supported_present_modes
235 }
236
237 pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
239 self.supported_present_modes
240 .contains(&mode.to_present_mode())
241 }
242
243 pub fn update_vsync_mode(
246 &mut self,
247 mode: par_term_config::VsyncMode,
248 ) -> (par_term_config::VsyncMode, bool) {
249 let requested = mode.to_present_mode();
250 let current = self.config.present_mode;
251
252 let actual = if self.supported_present_modes.contains(&requested) {
254 requested
255 } else {
256 log::warn!(
257 "Requested present mode {:?} not supported, falling back to Fifo",
258 requested
259 );
260 wgpu::PresentMode::Fifo
261 };
262
263 if actual != current {
265 self.config.present_mode = actual;
266 self.surface.configure(&self.device, &self.config);
267 log::info!("VSync mode changed to {:?}", actual);
268 }
269
270 let actual_vsync = match actual {
272 wgpu::PresentMode::Immediate => par_term_config::VsyncMode::Immediate,
273 wgpu::PresentMode::Mailbox => par_term_config::VsyncMode::Mailbox,
274 wgpu::PresentMode::Fifo | wgpu::PresentMode::FifoRelaxed => {
275 par_term_config::VsyncMode::Fifo
276 }
277 _ => par_term_config::VsyncMode::Fifo,
278 };
279
280 (actual_vsync, actual != current)
281 }
282
283 pub fn current_vsync_mode(&self) -> par_term_config::VsyncMode {
285 match self.config.present_mode {
286 wgpu::PresentMode::Immediate => par_term_config::VsyncMode::Immediate,
287 wgpu::PresentMode::Mailbox => par_term_config::VsyncMode::Mailbox,
288 wgpu::PresentMode::Fifo | wgpu::PresentMode::FifoRelaxed => {
289 par_term_config::VsyncMode::Fifo
290 }
291 _ => par_term_config::VsyncMode::Fifo,
292 }
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
303 fn oversize_extent_is_clamped_to_the_device_limit() {
304 assert_eq!(clamp_surface_extent(10240, 2822, 8192), (8192, 2822));
305 }
306
307 #[test]
308 fn extent_within_the_limit_is_unchanged() {
309 assert_eq!(clamp_surface_extent(3840, 2160, 8192), (3840, 2160));
310 }
311
312 #[test]
313 fn extent_at_the_limit_is_unchanged() {
314 assert_eq!(clamp_surface_extent(8192, 8192, 8192), (8192, 8192));
315 }
316
317 #[test]
320 fn zero_extent_floors_to_one() {
321 assert_eq!(clamp_surface_extent(0, 0, 8192), (1, 1));
322 }
323
324 #[test]
325 fn texture_limits_raises_only_the_2d_dimension() {
326 assert_eq!(
327 texture_limits(16384),
328 wgpu::Limits {
329 max_texture_dimension_2d: 16384,
330 ..wgpu::Limits::default()
331 }
332 );
333 }
334
335 #[test]
336 fn uncaptured_errors_log_the_first_then_every_1000th() {
337 assert!(should_log_uncaptured_error(1));
338 assert!(!should_log_uncaptured_error(2));
339 assert!(!should_log_uncaptured_error(999));
340 assert!(should_log_uncaptured_error(1000));
341 assert!(!should_log_uncaptured_error(1001));
342 }
343
344 fn surface_config(present_mode: wgpu::PresentMode) -> wgpu::SurfaceConfiguration {
345 wgpu::SurfaceConfiguration {
346 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
347 format: wgpu::TextureFormat::Bgra8Unorm,
348 color_space: wgpu::SurfaceColorSpace::Auto,
349 width: 2560,
350 height: 1440,
351 present_mode,
352 alpha_mode: wgpu::CompositeAlphaMode::PreMultiplied,
353 view_formats: vec![],
354 desired_maximum_frame_latency: 2,
355 }
356 }
357
358 #[test]
359 fn alternate_mode_prefers_the_other_vsync_family() {
360 assert_eq!(
363 alternate_present_mode(
364 wgpu::PresentMode::Fifo,
365 &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Immediate],
366 ),
367 Some(wgpu::PresentMode::Immediate)
368 );
369 assert_eq!(
370 alternate_present_mode(
371 wgpu::PresentMode::Immediate,
372 &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Immediate],
373 ),
374 Some(wgpu::PresentMode::Fifo)
375 );
376 }
377
378 #[test]
379 fn alternate_mode_falls_back_to_any_other_supported_mode() {
380 assert_eq!(
382 alternate_present_mode(
383 wgpu::PresentMode::Fifo,
384 &[wgpu::PresentMode::Fifo, wgpu::PresentMode::FifoRelaxed],
385 ),
386 Some(wgpu::PresentMode::FifoRelaxed)
387 );
388 }
389
390 #[test]
391 fn alternate_mode_is_none_when_target_is_the_only_mode() {
392 assert_eq!(
393 alternate_present_mode(wgpu::PresentMode::Fifo, &[wgpu::PresentMode::Fifo]),
394 None
395 );
396 }
397
398 #[test]
399 fn refresh_keeps_fields_the_new_topology_still_supports() {
400 let mut config = surface_config(wgpu::PresentMode::Fifo);
401 refresh_config_after_display_change(
402 &mut config,
403 &[
404 wgpu::TextureFormat::Bgra8Unorm,
405 wgpu::TextureFormat::Rgba8UnormSrgb,
406 ],
407 &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Immediate],
408 &[
409 wgpu::CompositeAlphaMode::PreMultiplied,
410 wgpu::CompositeAlphaMode::Auto,
411 ],
412 1920,
413 1080,
414 8192,
415 );
416 assert_eq!(config.format, wgpu::TextureFormat::Bgra8Unorm);
417 assert_eq!(config.present_mode, wgpu::PresentMode::Fifo);
418 assert_eq!(config.alpha_mode, wgpu::CompositeAlphaMode::PreMultiplied);
419 assert_eq!((config.width, config.height), (1920, 1080));
420 }
421
422 #[test]
423 fn refresh_replaces_what_the_new_topology_dropped() {
424 let mut config = surface_config(wgpu::PresentMode::Mailbox);
425 refresh_config_after_display_change(
426 &mut config,
427 &[wgpu::TextureFormat::Rgba8Unorm],
428 &[wgpu::PresentMode::Fifo],
429 &[wgpu::CompositeAlphaMode::Opaque],
430 0,
431 0,
432 8192,
433 );
434 assert_eq!(config.format, wgpu::TextureFormat::Rgba8Unorm);
436 assert_eq!(config.present_mode, wgpu::PresentMode::Fifo);
437 assert_eq!(config.alpha_mode, wgpu::CompositeAlphaMode::Opaque);
438 assert_eq!((config.width, config.height), (1, 1));
440 }
441
442 #[test]
443 fn refresh_clamps_oversize_extent() {
444 let mut config = surface_config(wgpu::PresentMode::Fifo);
445 let (format, present_mode, alpha_mode) =
446 (config.format, config.present_mode, config.alpha_mode);
447 refresh_config_after_display_change(
448 &mut config,
449 &[format],
450 &[present_mode],
451 &[alpha_mode],
452 10240,
453 2822,
454 8192,
455 );
456 assert_eq!((config.width, config.height), (8192, 2822));
457 }
458}