1use 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 surface_needs_alpha(window_opacity: f32) -> bool {
93 window_opacity < 1.0
94}
95
96pub fn select_alpha_mode(
105 needs_alpha: bool,
106 available: &[wgpu::CompositeAlphaMode],
107) -> Option<wgpu::CompositeAlphaMode> {
108 let preferred: &[wgpu::CompositeAlphaMode] = if needs_alpha {
109 &[
110 wgpu::CompositeAlphaMode::PreMultiplied,
111 wgpu::CompositeAlphaMode::PostMultiplied,
112 wgpu::CompositeAlphaMode::Auto,
113 ]
114 } else {
115 &[
116 wgpu::CompositeAlphaMode::Opaque,
117 wgpu::CompositeAlphaMode::PreMultiplied,
118 wgpu::CompositeAlphaMode::PostMultiplied,
119 wgpu::CompositeAlphaMode::Auto,
120 ]
121 };
122 preferred
123 .iter()
124 .copied()
125 .find(|m| available.contains(m))
126 .or_else(|| available.first().copied())
127}
128
129pub fn refresh_config_after_display_change(
139 config: &mut wgpu::SurfaceConfiguration,
140 formats: &[wgpu::TextureFormat],
141 present_modes: &[wgpu::PresentMode],
142 alpha_modes: &[wgpu::CompositeAlphaMode],
143 width: u32,
144 height: u32,
145 max_dimension: u32,
146) {
147 if !formats.contains(&config.format) {
148 let fallback = formats
149 .iter()
150 .copied()
151 .find(|f| !f.is_srgb())
152 .or_else(|| formats.first().copied());
153 if let Some(fallback) = fallback {
154 log::warn!(
155 "Surface format {:?} no longer supported after display change; switching to {:?} (render pipelines were built for the old format)",
156 config.format,
157 fallback
158 );
159 config.format = fallback;
160 }
161 }
162
163 if !present_modes.contains(&config.present_mode) {
164 let fallback = if present_modes.contains(&wgpu::PresentMode::Fifo) {
165 Some(wgpu::PresentMode::Fifo)
166 } else {
167 present_modes.first().copied()
168 };
169 if let Some(fallback) = fallback {
170 log::warn!(
171 "Present mode {:?} no longer supported after display change; falling back to {:?}",
172 config.present_mode,
173 fallback
174 );
175 config.present_mode = fallback;
176 }
177 }
178
179 if !alpha_modes.contains(&config.alpha_mode) {
180 let needs_alpha = config.alpha_mode != wgpu::CompositeAlphaMode::Opaque;
181 if let Some(fallback) = select_alpha_mode(needs_alpha, alpha_modes) {
182 log::warn!(
183 "Alpha mode {:?} no longer supported after display change; falling back to {:?}",
184 config.alpha_mode,
185 fallback
186 );
187 config.alpha_mode = fallback;
188 }
189 }
190
191 let (w, h) = clamp_surface_extent(width, height, max_dimension);
192 config.width = w;
193 config.height = h;
194}
195
196impl CellRenderer {
197 pub(crate) fn configure_surface(&self) {
203 self.surface.configure(&self.device, &self.config);
204 #[cfg(target_os = "macos")]
205 self.tag_metal_layer_colorspace();
206 }
207
208 #[cfg(target_os = "macos")]
216 fn tag_metal_layer_colorspace(&self) {
217 use objc2_core_graphics::{CGColorSpace, kCGColorSpaceSRGB};
218
219 let Some(hal_surface) = (unsafe { self.surface.as_hal::<wgpu::hal::api::Metal>() }) else {
224 log::warn!("Surface is not a Metal surface; CAMetalLayer colorspace left untagged");
225 return;
226 };
227 let layer = hal_surface.render_layer().lock();
228 let srgb = CGColorSpace::with_name(Some(unsafe { kCGColorSpaceSRGB }));
230 layer.setColorspace(srgb.as_deref());
231 log::info!(
232 "CAMetalLayer after configure: opaque={} displaySync={} maxDrawables={} colorspace={}",
233 layer.isOpaque(),
234 layer.displaySyncEnabled(),
235 layer.maximumDrawableCount(),
236 if layer.colorspace().is_some() {
237 "sRGB"
238 } else {
239 "none"
240 }
241 );
242 }
243
244 pub(crate) fn sync_alpha_mode_to_opacity(&mut self) {
247 let Some(alpha_mode) = select_alpha_mode(
248 surface_needs_alpha(self.window_opacity),
249 &self.supported_alpha_modes,
250 ) else {
251 return;
252 };
253 if alpha_mode != self.config.alpha_mode {
254 log::info!(
255 "Surface alpha mode {:?} -> {:?} (window_opacity={})",
256 self.config.alpha_mode,
257 alpha_mode,
258 self.window_opacity
259 );
260 self.config.alpha_mode = alpha_mode;
261 self.configure_surface();
262 }
263 }
264
265 pub fn reconfigure_surface(&mut self) {
266 log::info!(
270 "Plain surface reconfigure (same config, {}x{}, {:?})",
271 self.config.width,
272 self.config.height,
273 self.config.present_mode
274 );
275 self.configure_surface();
276 }
277
278 pub fn reconfigure_after_display_change(&mut self, width: u32, height: u32) {
294 let caps = self.surface.get_capabilities(&self.adapter);
295 let mut refreshed = self.config.clone();
296 refresh_config_after_display_change(
297 &mut refreshed,
298 &caps.formats,
299 &caps.present_modes,
300 &caps.alpha_modes,
301 width,
302 height,
303 self.device.limits().max_texture_dimension_2d,
304 );
305 self.supported_present_modes = caps.present_modes.clone();
306 self.supported_alpha_modes = caps.alpha_modes.clone();
307
308 let target = refreshed.present_mode;
309 match alternate_present_mode(target, &self.supported_present_modes) {
310 Some(alternate) => {
311 log::info!(
312 "Display-change heal: cycling present mode {:?} -> {:?}, config {} by fresh capabilities",
313 target,
314 alternate,
315 if refreshed == self.config {
316 "unchanged"
317 } else {
318 "changed"
319 }
320 );
321 self.config = refreshed;
322 self.config.present_mode = alternate;
323 self.configure_surface();
324 self.config.present_mode = target;
325 self.configure_surface();
326 }
327 None if refreshed == self.config => {
328 log::warn!(
332 "Display-change heal: no alternate present mode and fresh capabilities unchanged; skipping configure"
333 );
334 }
335 None => {
336 log::info!(
337 "Display-change heal: no alternate present mode; applying refreshed config"
338 );
339 self.config = refreshed;
340 self.configure_surface();
341 }
342 }
343 }
344
345 pub fn supported_present_modes(&self) -> &[wgpu::PresentMode] {
347 &self.supported_present_modes
348 }
349
350 pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
352 self.supported_present_modes
353 .contains(&mode.to_present_mode())
354 }
355
356 pub fn update_vsync_mode(
359 &mut self,
360 mode: par_term_config::VsyncMode,
361 ) -> (par_term_config::VsyncMode, bool) {
362 let requested = mode.to_present_mode();
363 let current = self.config.present_mode;
364
365 let actual = if self.supported_present_modes.contains(&requested) {
367 requested
368 } else {
369 log::warn!(
370 "Requested present mode {:?} not supported, falling back to Fifo",
371 requested
372 );
373 wgpu::PresentMode::Fifo
374 };
375
376 if actual != current {
378 self.config.present_mode = actual;
379 self.configure_surface();
380 log::info!("VSync mode changed to {:?}", actual);
381 }
382
383 let actual_vsync = match actual {
385 wgpu::PresentMode::Immediate => par_term_config::VsyncMode::Immediate,
386 wgpu::PresentMode::Mailbox => par_term_config::VsyncMode::Mailbox,
387 wgpu::PresentMode::Fifo | wgpu::PresentMode::FifoRelaxed => {
388 par_term_config::VsyncMode::Fifo
389 }
390 _ => par_term_config::VsyncMode::Fifo,
391 };
392
393 (actual_vsync, actual != current)
394 }
395
396 pub fn current_vsync_mode(&self) -> par_term_config::VsyncMode {
398 match self.config.present_mode {
399 wgpu::PresentMode::Immediate => par_term_config::VsyncMode::Immediate,
400 wgpu::PresentMode::Mailbox => par_term_config::VsyncMode::Mailbox,
401 wgpu::PresentMode::Fifo | wgpu::PresentMode::FifoRelaxed => {
402 par_term_config::VsyncMode::Fifo
403 }
404 _ => par_term_config::VsyncMode::Fifo,
405 }
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412
413 #[test]
416 fn oversize_extent_is_clamped_to_the_device_limit() {
417 assert_eq!(clamp_surface_extent(10240, 2822, 8192), (8192, 2822));
418 }
419
420 #[test]
421 fn extent_within_the_limit_is_unchanged() {
422 assert_eq!(clamp_surface_extent(3840, 2160, 8192), (3840, 2160));
423 }
424
425 #[test]
426 fn extent_at_the_limit_is_unchanged() {
427 assert_eq!(clamp_surface_extent(8192, 8192, 8192), (8192, 8192));
428 }
429
430 #[test]
433 fn zero_extent_floors_to_one() {
434 assert_eq!(clamp_surface_extent(0, 0, 8192), (1, 1));
435 }
436
437 #[test]
438 fn texture_limits_raises_only_the_2d_dimension() {
439 assert_eq!(
440 texture_limits(16384),
441 wgpu::Limits {
442 max_texture_dimension_2d: 16384,
443 ..wgpu::Limits::default()
444 }
445 );
446 }
447
448 #[test]
449 fn uncaptured_errors_log_the_first_then_every_1000th() {
450 assert!(should_log_uncaptured_error(1));
451 assert!(!should_log_uncaptured_error(2));
452 assert!(!should_log_uncaptured_error(999));
453 assert!(should_log_uncaptured_error(1000));
454 assert!(!should_log_uncaptured_error(1001));
455 }
456
457 fn surface_config(present_mode: wgpu::PresentMode) -> wgpu::SurfaceConfiguration {
458 wgpu::SurfaceConfiguration {
459 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
460 format: wgpu::TextureFormat::Bgra8Unorm,
461 color_space: wgpu::SurfaceColorSpace::Auto,
462 width: 2560,
463 height: 1440,
464 present_mode,
465 alpha_mode: wgpu::CompositeAlphaMode::PreMultiplied,
466 view_formats: vec![],
467 desired_maximum_frame_latency: 2,
468 }
469 }
470
471 #[test]
472 fn alternate_mode_prefers_the_other_vsync_family() {
473 assert_eq!(
476 alternate_present_mode(
477 wgpu::PresentMode::Fifo,
478 &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Immediate],
479 ),
480 Some(wgpu::PresentMode::Immediate)
481 );
482 assert_eq!(
483 alternate_present_mode(
484 wgpu::PresentMode::Immediate,
485 &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Immediate],
486 ),
487 Some(wgpu::PresentMode::Fifo)
488 );
489 }
490
491 #[test]
492 fn alternate_mode_falls_back_to_any_other_supported_mode() {
493 assert_eq!(
495 alternate_present_mode(
496 wgpu::PresentMode::Fifo,
497 &[wgpu::PresentMode::Fifo, wgpu::PresentMode::FifoRelaxed],
498 ),
499 Some(wgpu::PresentMode::FifoRelaxed)
500 );
501 }
502
503 #[test]
504 fn alternate_mode_is_none_when_target_is_the_only_mode() {
505 assert_eq!(
506 alternate_present_mode(wgpu::PresentMode::Fifo, &[wgpu::PresentMode::Fifo]),
507 None
508 );
509 }
510
511 #[test]
512 fn refresh_keeps_fields_the_new_topology_still_supports() {
513 let mut config = surface_config(wgpu::PresentMode::Fifo);
514 refresh_config_after_display_change(
515 &mut config,
516 &[
517 wgpu::TextureFormat::Bgra8Unorm,
518 wgpu::TextureFormat::Rgba8UnormSrgb,
519 ],
520 &[wgpu::PresentMode::Fifo, wgpu::PresentMode::Immediate],
521 &[
522 wgpu::CompositeAlphaMode::PreMultiplied,
523 wgpu::CompositeAlphaMode::Auto,
524 ],
525 1920,
526 1080,
527 8192,
528 );
529 assert_eq!(config.format, wgpu::TextureFormat::Bgra8Unorm);
530 assert_eq!(config.present_mode, wgpu::PresentMode::Fifo);
531 assert_eq!(config.alpha_mode, wgpu::CompositeAlphaMode::PreMultiplied);
532 assert_eq!((config.width, config.height), (1920, 1080));
533 }
534
535 #[test]
536 fn refresh_replaces_what_the_new_topology_dropped() {
537 let mut config = surface_config(wgpu::PresentMode::Mailbox);
538 refresh_config_after_display_change(
539 &mut config,
540 &[wgpu::TextureFormat::Rgba8Unorm],
541 &[wgpu::PresentMode::Fifo],
542 &[wgpu::CompositeAlphaMode::Opaque],
543 0,
544 0,
545 8192,
546 );
547 assert_eq!(config.format, wgpu::TextureFormat::Rgba8Unorm);
549 assert_eq!(config.present_mode, wgpu::PresentMode::Fifo);
550 assert_eq!(config.alpha_mode, wgpu::CompositeAlphaMode::Opaque);
551 assert_eq!((config.width, config.height), (1, 1));
553 }
554
555 #[test]
556 fn refresh_clamps_oversize_extent() {
557 let mut config = surface_config(wgpu::PresentMode::Fifo);
558 let (format, present_mode, alpha_mode) =
559 (config.format, config.present_mode, config.alpha_mode);
560 refresh_config_after_display_change(
561 &mut config,
562 &[format],
563 &[present_mode],
564 &[alpha_mode],
565 10240,
566 2822,
567 8192,
568 );
569 assert_eq!((config.width, config.height), (8192, 2822));
570 }
571
572 #[test]
573 fn opaque_window_selects_opaque_alpha_mode() {
574 use wgpu::CompositeAlphaMode::*;
575 let available = [Opaque, PostMultiplied];
576 assert_eq!(
577 select_alpha_mode(surface_needs_alpha(1.0), &available),
578 Some(Opaque)
579 );
580 }
581
582 #[test]
583 fn translucent_window_selects_compositing_alpha_mode() {
584 use wgpu::CompositeAlphaMode::*;
585 assert_eq!(
586 select_alpha_mode(surface_needs_alpha(0.9), &[Opaque, PostMultiplied]),
587 Some(PostMultiplied)
588 );
589 assert_eq!(
590 select_alpha_mode(true, &[Opaque, PostMultiplied, PreMultiplied]),
591 Some(PreMultiplied)
592 );
593 }
594
595 #[test]
596 fn alpha_mode_falls_back_to_first_advertised() {
597 use wgpu::CompositeAlphaMode::*;
598 assert_eq!(select_alpha_mode(true, &[Opaque]), Some(Opaque));
599 assert_eq!(select_alpha_mode(false, &[Inherit]), Some(Inherit));
600 assert_eq!(select_alpha_mode(false, &[]), None);
601 }
602}