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 log::info!(
167 "Plain surface reconfigure (same config, {}x{}, {:?})",
168 self.config.width,
169 self.config.height,
170 self.config.present_mode
171 );
172 self.surface.configure(&self.device, &self.config);
173 }
174
175 pub fn reconfigure_after_display_change(&mut self, width: u32, height: u32) {
191 let caps = self.surface.get_capabilities(&self.adapter);
192 let mut refreshed = self.config.clone();
193 refresh_config_after_display_change(
194 &mut refreshed,
195 &caps.formats,
196 &caps.present_modes,
197 &caps.alpha_modes,
198 width,
199 height,
200 self.device.limits().max_texture_dimension_2d,
201 );
202 self.supported_present_modes = caps.present_modes.clone();
203
204 let target = refreshed.present_mode;
205 match alternate_present_mode(target, &self.supported_present_modes) {
206 Some(alternate) => {
207 log::info!(
208 "Display-change heal: cycling present mode {:?} -> {:?}, config {} by fresh capabilities",
209 target,
210 alternate,
211 if refreshed == self.config {
212 "unchanged"
213 } else {
214 "changed"
215 }
216 );
217 self.config = refreshed;
218 self.config.present_mode = alternate;
219 self.surface.configure(&self.device, &self.config);
220 self.config.present_mode = target;
221 self.surface.configure(&self.device, &self.config);
222 }
223 None if refreshed == self.config => {
224 log::warn!(
228 "Display-change heal: no alternate present mode and fresh capabilities unchanged; skipping configure"
229 );
230 }
231 None => {
232 log::info!(
233 "Display-change heal: no alternate present mode; applying refreshed config"
234 );
235 self.config = refreshed;
236 self.surface.configure(&self.device, &self.config);
237 }
238 }
239 }
240
241 pub fn supported_present_modes(&self) -> &[wgpu::PresentMode] {
243 &self.supported_present_modes
244 }
245
246 pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
248 self.supported_present_modes
249 .contains(&mode.to_present_mode())
250 }
251
252 pub fn update_vsync_mode(
255 &mut self,
256 mode: par_term_config::VsyncMode,
257 ) -> (par_term_config::VsyncMode, bool) {
258 let requested = mode.to_present_mode();
259 let current = self.config.present_mode;
260
261 let actual = if self.supported_present_modes.contains(&requested) {
263 requested
264 } else {
265 log::warn!(
266 "Requested present mode {:?} not supported, falling back to Fifo",
267 requested
268 );
269 wgpu::PresentMode::Fifo
270 };
271
272 if actual != current {
274 self.config.present_mode = actual;
275 self.surface.configure(&self.device, &self.config);
276 log::info!("VSync mode changed to {:?}", actual);
277 }
278
279 let actual_vsync = match actual {
281 wgpu::PresentMode::Immediate => par_term_config::VsyncMode::Immediate,
282 wgpu::PresentMode::Mailbox => par_term_config::VsyncMode::Mailbox,
283 wgpu::PresentMode::Fifo | wgpu::PresentMode::FifoRelaxed => {
284 par_term_config::VsyncMode::Fifo
285 }
286 _ => par_term_config::VsyncMode::Fifo,
287 };
288
289 (actual_vsync, actual != current)
290 }
291
292 pub fn current_vsync_mode(&self) -> par_term_config::VsyncMode {
294 match self.config.present_mode {
295 wgpu::PresentMode::Immediate => par_term_config::VsyncMode::Immediate,
296 wgpu::PresentMode::Mailbox => par_term_config::VsyncMode::Mailbox,
297 wgpu::PresentMode::Fifo | wgpu::PresentMode::FifoRelaxed => {
298 par_term_config::VsyncMode::Fifo
299 }
300 _ => par_term_config::VsyncMode::Fifo,
301 }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
312 fn oversize_extent_is_clamped_to_the_device_limit() {
313 assert_eq!(clamp_surface_extent(10240, 2822, 8192), (8192, 2822));
314 }
315
316 #[test]
317 fn extent_within_the_limit_is_unchanged() {
318 assert_eq!(clamp_surface_extent(3840, 2160, 8192), (3840, 2160));
319 }
320
321 #[test]
322 fn extent_at_the_limit_is_unchanged() {
323 assert_eq!(clamp_surface_extent(8192, 8192, 8192), (8192, 8192));
324 }
325
326 #[test]
329 fn zero_extent_floors_to_one() {
330 assert_eq!(clamp_surface_extent(0, 0, 8192), (1, 1));
331 }
332
333 #[test]
334 fn texture_limits_raises_only_the_2d_dimension() {
335 assert_eq!(
336 texture_limits(16384),
337 wgpu::Limits {
338 max_texture_dimension_2d: 16384,
339 ..wgpu::Limits::default()
340 }
341 );
342 }
343
344 #[test]
345 fn uncaptured_errors_log_the_first_then_every_1000th() {
346 assert!(should_log_uncaptured_error(1));
347 assert!(!should_log_uncaptured_error(2));
348 assert!(!should_log_uncaptured_error(999));
349 assert!(should_log_uncaptured_error(1000));
350 assert!(!should_log_uncaptured_error(1001));
351 }
352
353 fn surface_config(present_mode: wgpu::PresentMode) -> wgpu::SurfaceConfiguration {
354 wgpu::SurfaceConfiguration {
355 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
356 format: wgpu::TextureFormat::Bgra8Unorm,
357 color_space: wgpu::SurfaceColorSpace::Auto,
358 width: 2560,
359 height: 1440,
360 present_mode,
361 alpha_mode: wgpu::CompositeAlphaMode::PreMultiplied,
362 view_formats: vec![],
363 desired_maximum_frame_latency: 2,
364 }
365 }
366
367 #[test]
368 fn alternate_mode_prefers_the_other_vsync_family() {
369 assert_eq!(
372 alternate_present_mode(
373 wgpu::PresentMode::Fifo,
374 &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Immediate],
375 ),
376 Some(wgpu::PresentMode::Immediate)
377 );
378 assert_eq!(
379 alternate_present_mode(
380 wgpu::PresentMode::Immediate,
381 &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Immediate],
382 ),
383 Some(wgpu::PresentMode::Fifo)
384 );
385 }
386
387 #[test]
388 fn alternate_mode_falls_back_to_any_other_supported_mode() {
389 assert_eq!(
391 alternate_present_mode(
392 wgpu::PresentMode::Fifo,
393 &[wgpu::PresentMode::Fifo, wgpu::PresentMode::FifoRelaxed],
394 ),
395 Some(wgpu::PresentMode::FifoRelaxed)
396 );
397 }
398
399 #[test]
400 fn alternate_mode_is_none_when_target_is_the_only_mode() {
401 assert_eq!(
402 alternate_present_mode(wgpu::PresentMode::Fifo, &[wgpu::PresentMode::Fifo]),
403 None
404 );
405 }
406
407 #[test]
408 fn refresh_keeps_fields_the_new_topology_still_supports() {
409 let mut config = surface_config(wgpu::PresentMode::Fifo);
410 refresh_config_after_display_change(
411 &mut config,
412 &[
413 wgpu::TextureFormat::Bgra8Unorm,
414 wgpu::TextureFormat::Rgba8UnormSrgb,
415 ],
416 &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Immediate],
417 &[
418 wgpu::CompositeAlphaMode::PreMultiplied,
419 wgpu::CompositeAlphaMode::Auto,
420 ],
421 1920,
422 1080,
423 8192,
424 );
425 assert_eq!(config.format, wgpu::TextureFormat::Bgra8Unorm);
426 assert_eq!(config.present_mode, wgpu::PresentMode::Fifo);
427 assert_eq!(config.alpha_mode, wgpu::CompositeAlphaMode::PreMultiplied);
428 assert_eq!((config.width, config.height), (1920, 1080));
429 }
430
431 #[test]
432 fn refresh_replaces_what_the_new_topology_dropped() {
433 let mut config = surface_config(wgpu::PresentMode::Mailbox);
434 refresh_config_after_display_change(
435 &mut config,
436 &[wgpu::TextureFormat::Rgba8Unorm],
437 &[wgpu::PresentMode::Fifo],
438 &[wgpu::CompositeAlphaMode::Opaque],
439 0,
440 0,
441 8192,
442 );
443 assert_eq!(config.format, wgpu::TextureFormat::Rgba8Unorm);
445 assert_eq!(config.present_mode, wgpu::PresentMode::Fifo);
446 assert_eq!(config.alpha_mode, wgpu::CompositeAlphaMode::Opaque);
447 assert_eq!((config.width, config.height), (1, 1));
449 }
450
451 #[test]
452 fn refresh_clamps_oversize_extent() {
453 let mut config = surface_config(wgpu::PresentMode::Fifo);
454 let (format, present_mode, alpha_mode) =
455 (config.format, config.present_mode, config.alpha_mode);
456 refresh_config_after_display_change(
457 &mut config,
458 &[format],
459 &[present_mode],
460 &[alpha_mode],
461 10240,
462 2822,
463 8192,
464 );
465 assert_eq!((config.width, config.height), (8192, 2822));
466 }
467}