max / shop
- Co-Authored-By
- Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 file changed,
+78 insertions,
-16 deletions
| @@ -30,7 +30,7 @@ | |||
| 30 | 30 | use std::collections::VecDeque; | |
| 31 | 31 | use wayland_client::protocol::wl_keyboard; | |
| 32 | 32 | use tracing::{info, warn}; | |
| 33 | - | use wayland_client::protocol::{wl_output, wl_seat, wl_surface}; | |
| 33 | + | use wayland_client::protocol::{wl_callback, wl_output, wl_seat, wl_surface}; | |
| 34 | 34 | ||
| 35 | 35 | const INITIAL: (u32, u32) = (960, 540); | |
| 36 | 36 | /// IosevkaTerm Nerd Font Mono, terminal-optimized variant of Iosevka with | |
| @@ -193,7 +193,9 @@ | |||
| 193 | 193 | modifiers: Modifiers::default(), | |
| 194 | 194 | cursor_phase: true, | |
| 195 | 195 | focused: false, | |
| 196 | - | pending_redraw: true, | |
| 196 | + | qh: qh.clone(), | |
| 197 | + | dirty: true, | |
| 198 | + | awaiting_frame: false, | |
| 197 | 199 | }; | |
| 198 | 200 | app.surface.configure(&app.device, &app.surface_config); | |
| 199 | 201 | app.text | |
| @@ -243,7 +245,7 @@ | |||
| 243 | 245 | app.chrome.xdg_window.set_title(title); | |
| 244 | 246 | } | |
| 245 | 247 | app.cursor_phase = !app.cursor_phase; | |
| 246 | - | app.pending_redraw = true; | |
| 248 | + | app.dirty = true; | |
| 247 | 249 | } | |
| 248 | 250 | Err(e) | |
| 249 | 251 | if matches!( | |
| @@ -295,20 +297,27 @@ | |||
| 295 | 297 | cell_width: (CELL_ADVANCE * s as f32).round() as u16, | |
| 296 | 298 | cell_height: (CELL_HEIGHT * s as f32).round() as u16, | |
| 297 | 299 | }); | |
| 298 | - | app.pending_redraw = true; | |
| 300 | + | app.dirty = true; | |
| 299 | 301 | } | |
| 300 | 302 | Pending::CloseRequested => app.chrome.exit = true, | |
| 301 | 303 | } | |
| 302 | 304 | } | |
| 303 | 305 | ||
| 304 | - | if app.pending_redraw && !app.chrome.exit { | |
| 305 | - | if let Err(e) = draw(&mut app) { | |
| 306 | - | warn!("draw: {e:?}"); | |
| 307 | - | } | |
| 308 | - | app.pending_redraw = false; | |
| 306 | + | // If we have new content and no frame callback is in flight, kick off | |
| 307 | + | // a render. The render function requests the next frame callback and | |
| 308 | + | // sets awaiting_frame; subsequent dirty events piggyback on that | |
| 309 | + | // pending callback and coalesce into one frame per compositor tick. | |
| 310 | + | if app.dirty && !app.awaiting_frame && !app.chrome.exit { | |
| 311 | + | render_now(&mut app); | |
| 309 | 312 | } | |
| 310 | 313 | } | |
| 311 | 314 | ||
| 315 | + | // Drop order matters — the wayland Connection lives inside | |
| 316 | + | // event_loop's WaylandSource; wgpu Surface needs a valid wl_surface (and | |
| 317 | + | // an alive connection) to destroy its Vulkan swapchain. Drop app first | |
| 318 | + | // so Surface is torn down while wayland is still up. | |
| 319 | + | drop(app); | |
| 320 | + | drop(event_loop); | |
| 312 | 321 | Ok(()) | |
| 313 | 322 | } | |
| 314 | 323 | ||
| @@ -456,14 +465,17 @@ | |||
| 456 | 465 | } | |
| 457 | 466 | ||
| 458 | 467 | struct App { | |
| 459 | - | chrome: WaylandChrome, | |
| 468 | + | // Field order matters — Rust drops in declaration order. Anything that | |
| 469 | + | // references wayland handles (wgpu Surface via raw pointers) MUST drop | |
| 470 | + | // before `chrome` releases those handles, else use-after-free on exit. | |
| 460 | 471 | surface: wgpu::Surface<'static>, | |
| 461 | 472 | surface_config: wgpu::SurfaceConfiguration, | |
| 462 | 473 | surface_format: wgpu::TextureFormat, | |
| 463 | - | device: Arc<wgpu::Device>, | |
| 464 | - | queue: Arc<wgpu::Queue>, | |
| 465 | 474 | text: TextRenderer, | |
| 466 | 475 | images: ImageRenderer, | |
| 476 | + | device: Arc<wgpu::Device>, | |
| 477 | + | queue: Arc<wgpu::Queue>, | |
| 478 | + | chrome: WaylandChrome, | |
| 467 | 479 | pty: Pty, | |
| 468 | 480 | grid: Grid, | |
| 469 | 481 | parser: shop_vt::Parser, | |
| @@ -483,7 +495,31 @@ | |||
| 483 | 495 | /// True while our surface has keyboard focus. Cursor dims when unfocused | |
| 484 | 496 | /// so you can tell at a glance which window is receiving input. | |
| 485 | 497 | focused: bool, | |
| 486 | - | pending_redraw: bool, | |
| 498 | + | /// QueueHandle kept so we can request `wl_surface.frame` callbacks from | |
| 499 | + | /// non-dispatch contexts (PTY read, resize handling). | |
| 500 | + | qh: QueueHandle<App>, | |
| 501 | + | /// Content has changed since the last render. | |
| 502 | + | dirty: bool, | |
| 503 | + | /// We've requested a `wl_surface.frame` callback that hasn't fired yet — | |
| 504 | + | /// don't request another one, just mark dirty. The pending callback | |
| 505 | + | /// coalesces all dirty events since the last frame into one render. | |
| 506 | + | awaiting_frame: bool, | |
| 507 | + | } | |
| 508 | + | ||
| 509 | + | /// Render one frame and request the next `wl_surface.frame` callback. All | |
| 510 | + | /// content changes (PTY reads, resize, focus toggle, activity-light phase) | |
| 511 | + | /// go through this — never called directly except by the coalescing paths | |
| 512 | + | /// (main-loop-after-events and CompositorHandler::frame). | |
| 513 | + | fn render_now(app: &mut App) { | |
| 514 | + | // Register the frame callback BEFORE presenting so it becomes active on | |
| 515 | + | // the commit that queue.present issues. `awaiting_frame` blocks further | |
| 516 | + | // renders until the compositor tells us it's ready for another one. | |
| 517 | + | app.chrome.xdg_window.wl_surface().frame(&app.qh, ()); | |
| 518 | + | app.awaiting_frame = true; | |
| 519 | + | app.dirty = false; | |
| 520 | + | if let Err(e) = draw(app) { | |
| 521 | + | warn!("render: {e:?}"); | |
| 522 | + | } | |
| 487 | 523 | } | |
| 488 | 524 | ||
| 489 | 525 | fn draw(app: &mut App) -> anyhow::Result<()> { | |
| @@ -646,7 +682,11 @@ | |||
| 646 | 682 | _: wl_output::Transform, | |
| 647 | 683 | ) { | |
| 648 | 684 | } | |
| 649 | - | fn frame(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &wl_surface::WlSurface, _: u32) {} | |
| 685 | + | fn frame(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &wl_surface::WlSurface, _: u32) { | |
| 686 | + | // SCTK doesn't actually forward the raw wl_callback done event here; | |
| 687 | + | // our own Dispatch<WlCallback, ()> impl below is what drives the | |
| 688 | + | // coalesce loop. Left in place because CompositorHandler requires it. | |
| 689 | + | } | |
| 650 | 690 | fn surface_enter( | |
| 651 | 691 | &mut self, | |
| 652 | 692 | _: &Connection, | |
| @@ -721,7 +761,7 @@ | |||
| 721 | 761 | _: &[Keysym], | |
| 722 | 762 | ) { | |
| 723 | 763 | self.focused = true; | |
| 724 | - | self.pending_redraw = true; | |
| 764 | + | self.dirty = true; | |
| 725 | 765 | } | |
| 726 | 766 | fn leave( | |
| 727 | 767 | &mut self, | |
| @@ -732,7 +772,7 @@ | |||
| 732 | 772 | _: u32, | |
| 733 | 773 | ) { | |
| 734 | 774 | self.focused = false; | |
| 735 | - | self.pending_redraw = true; | |
| 775 | + | self.dirty = true; | |
| 736 | 776 | } | |
| 737 | 777 | fn press_key( | |
| 738 | 778 | &mut self, | |
| @@ -963,3 +1003,25 @@ | |||
| 963 | 1003 | ||
| 964 | 1004 | delegate_registry!(App); | |
| 965 | 1005 | delegate_dispatch2!(App); | |
| 1006 | + | ||
| 1007 | + | /// `wl_surface.frame` done event. Compositor signalling us that it's ready | |
| 1008 | + | /// for the next frame — if the grid has dirtied since we submitted the last | |
| 1009 | + | /// one, render immediately. Otherwise wait for content to arrive (a PTY | |
| 1010 | + | /// read will kick the loop back off via the main-loop dirty-check). | |
| 1011 | + | impl wayland_client::Dispatch<wl_callback::WlCallback, ()> for App { | |
| 1012 | + | fn event( | |
| 1013 | + | state: &mut Self, | |
| 1014 | + | _: &wl_callback::WlCallback, | |
| 1015 | + | event: wl_callback::Event, | |
| 1016 | + | _: &(), | |
| 1017 | + | _: &Connection, | |
| 1018 | + | _: &QueueHandle<Self>, | |
| 1019 | + | ) { | |
| 1020 | + | if let wl_callback::Event::Done { .. } = event { | |
| 1021 | + | state.awaiting_frame = false; | |
| 1022 | + | if state.dirty { | |
| 1023 | + | render_now(state); | |
| 1024 | + | } | |
| 1025 | + | } | |
| 1026 | + | } | |
| 1027 | + | } |