Skip to main content

par_term_render/cell_renderer/
surface.rs

1use super::CellRenderer;
2use crate::wgpu_conversions::VsyncModeWgpu;
3use std::sync::atomic::{AtomicU64, Ordering};
4
5/// Clamp a surface extent so `Surface::configure` cannot reject it.
6///
7/// `configure` is a validation boundary: an extent beyond the device's
8/// `max_texture_dimension_2d` is an error, and an uncaptured wgpu error
9/// aborts the process (see [`install_nonfatal_error_handler`]). A window
10/// spanning multiple high-DPI displays can exceed the 8192 default — the
11/// crash of 2026-08-21 was a full-screen tile at 10240×2822 against an 8192
12/// limit. Clamping trades a slightly soft, compositor-upscaled frame on
13/// adapters that genuinely cap at 8192 for not losing every tab.
14pub 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
21/// Device limits with `max_texture_dimension_2d` raised to the adapter's real
22/// maximum instead of the 8192 of [`wgpu::Limits::default`].
23pub 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
30/// Install a non-fatal handler for uncaptured wgpu errors on `device`.
31///
32/// wgpu's default turns any error not caught by an error scope into a panic.
33/// par-term's wgpu calls happen inside AppKit/SkyLight callbacks — resize and
34/// redraw arrive as Objective-C notifications — where a Rust panic cannot
35/// unwind and aborts the process. Logging instead keeps the app alive with a
36/// degraded frame.
37pub 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
47/// Whether occurrence number `count` (1-based) reaches the log: the first,
48/// then every 1000th, so a per-frame error cannot grow the log unbounded.
49fn should_log_uncaptured_error(count: u64) -> bool {
50    count == 1 || count.is_multiple_of(1_000)
51}
52
53impl CellRenderer {
54    pub fn reconfigure_surface(&mut self) {
55        self.surface.configure(&self.device, &self.config);
56    }
57
58    /// Get the list of supported present modes for this surface
59    pub fn supported_present_modes(&self) -> &[wgpu::PresentMode] {
60        &self.supported_present_modes
61    }
62
63    /// Check if a vsync mode is supported
64    pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
65        self.supported_present_modes
66            .contains(&mode.to_present_mode())
67    }
68
69    /// Update the vsync mode. Returns the actual mode applied (may differ if requested mode unsupported).
70    /// Also returns whether the mode was changed.
71    pub fn update_vsync_mode(
72        &mut self,
73        mode: par_term_config::VsyncMode,
74    ) -> (par_term_config::VsyncMode, bool) {
75        let requested = mode.to_present_mode();
76        let current = self.config.present_mode;
77
78        // Determine the actual mode to use
79        let actual = if self.supported_present_modes.contains(&requested) {
80            requested
81        } else {
82            log::warn!(
83                "Requested present mode {:?} not supported, falling back to Fifo",
84                requested
85            );
86            wgpu::PresentMode::Fifo
87        };
88
89        // Only reconfigure if the mode actually changed
90        if actual != current {
91            self.config.present_mode = actual;
92            self.surface.configure(&self.device, &self.config);
93            log::info!("VSync mode changed to {:?}", actual);
94        }
95
96        // Convert back to VsyncMode for return
97        let actual_vsync = match actual {
98            wgpu::PresentMode::Immediate => par_term_config::VsyncMode::Immediate,
99            wgpu::PresentMode::Mailbox => par_term_config::VsyncMode::Mailbox,
100            wgpu::PresentMode::Fifo | wgpu::PresentMode::FifoRelaxed => {
101                par_term_config::VsyncMode::Fifo
102            }
103            _ => par_term_config::VsyncMode::Fifo,
104        };
105
106        (actual_vsync, actual != current)
107    }
108
109    /// Get the current vsync mode
110    pub fn current_vsync_mode(&self) -> par_term_config::VsyncMode {
111        match self.config.present_mode {
112            wgpu::PresentMode::Immediate => par_term_config::VsyncMode::Immediate,
113            wgpu::PresentMode::Mailbox => par_term_config::VsyncMode::Mailbox,
114            wgpu::PresentMode::Fifo | wgpu::PresentMode::FifoRelaxed => {
115                par_term_config::VsyncMode::Fifo
116            }
117            _ => par_term_config::VsyncMode::Fifo,
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    /// The crash of 2026-08-21: a full-screen tile spanning two 5K displays,
127    /// 10240×2822, configured against a device capped at 8192.
128    #[test]
129    fn oversize_extent_is_clamped_to_the_device_limit() {
130        assert_eq!(clamp_surface_extent(10240, 2822, 8192), (8192, 2822));
131    }
132
133    #[test]
134    fn extent_within_the_limit_is_unchanged() {
135        assert_eq!(clamp_surface_extent(3840, 2160, 8192), (3840, 2160));
136    }
137
138    #[test]
139    fn extent_at_the_limit_is_unchanged() {
140        assert_eq!(clamp_surface_extent(8192, 8192, 8192), (8192, 8192));
141    }
142
143    /// `configure` rejects a zero extent as well, so the clamp floors at one
144    /// and a call site cannot trade one validation failure for another.
145    #[test]
146    fn zero_extent_floors_to_one() {
147        assert_eq!(clamp_surface_extent(0, 0, 8192), (1, 1));
148    }
149
150    #[test]
151    fn texture_limits_raises_only_the_2d_dimension() {
152        assert_eq!(
153            texture_limits(16384),
154            wgpu::Limits {
155                max_texture_dimension_2d: 16384,
156                ..wgpu::Limits::default()
157            }
158        );
159    }
160
161    #[test]
162    fn uncaptured_errors_log_the_first_then_every_1000th() {
163        assert!(should_log_uncaptured_error(1));
164        assert!(!should_log_uncaptured_error(2));
165        assert!(!should_log_uncaptured_error(999));
166        assert!(should_log_uncaptured_error(1000));
167        assert!(!should_log_uncaptured_error(1001));
168    }
169}