Skip to main content

kozan_platform/pipeline/
render_loop.rs

1//! Render loop — per-window compositor + vsync loop.
2//!
3//! Chrome: `cc::LayerTreeHostImpl` on the compositor thread.
4//! Runs on its own thread, owns the GPU surface and Compositor.
5//! Handles scroll at vsync rate without view thread involvement.
6
7use std::sync::mpsc;
8
9use kozan_core::compositor::Compositor;
10use kozan_primitives::geometry::{Offset, Point};
11
12use crate::context::FrameOutput;
13use crate::event::ViewEvent;
14use crate::renderer::{RenderParams, RenderSurface, RendererError};
15
16/// Events received by the render loop.
17///
18/// Chrome: tasks posted to the compositor thread's task queue.
19/// Each variant transfers ownership — no shared state.
20pub enum RenderEvent {
21    /// View thread finished painting — commit to compositor.
22    Commit(FrameOutput),
23    /// Surface resized (physical pixels).
24    Resize { width: u32, height: u32 },
25    /// DPI scale factor changed.
26    ScaleFactorChanged(f64),
27    /// Wheel/touch scroll — compositor handles directly.
28    /// Carries cursor position for hit testing which scroller to target.
29    Scroll { delta: Offset, point: Point },
30    /// Clean shutdown.
31    Shutdown,
32}
33
34/// Error callback — called when the GPU surface is lost.
35/// The platform-specific adapter provides this.
36pub type OnSurfaceLost = Box<dyn FnOnce() + Send>;
37
38/// The per-window render loop. Generic over the GPU surface.
39///
40/// Chrome: `LayerTreeHostImpl` + `OutputSurface` + `SchedulerStateMachine`.
41pub(crate) struct RenderLoop<S> {
42    surface: S,
43    compositor: Compositor,
44    view_tx: mpsc::Sender<ViewEvent>,
45    on_surface_lost: Option<OnSurfaceLost>,
46    width: u32,
47    height: u32,
48    scale_factor: f64,
49    queued_scrolls: Vec<(Offset, Point)>,
50}
51
52impl<S: RenderSurface> RenderLoop<S> {
53    pub fn new(
54        surface: S,
55        view_tx: mpsc::Sender<ViewEvent>,
56        on_surface_lost: OnSurfaceLost,
57        width: u32,
58        height: u32,
59        scale_factor: f64,
60    ) -> Self {
61        Self {
62            surface,
63            compositor: Compositor::new(),
64            view_tx,
65            on_surface_lost: Some(on_surface_lost),
66            width,
67            height,
68            scale_factor,
69            queued_scrolls: Vec::new(),
70        }
71    }
72
73    /// The main entry point — runs until Shutdown or channel disconnect.
74    pub fn run(&mut self, rx: mpsc::Receiver<RenderEvent>) {
75        if !self.wait_for_first_commit(&rx) {
76            return;
77        }
78        loop {
79            if !self.drain_events(&rx) {
80                return;
81            }
82            if !self.render_frame() {
83                return;
84            }
85        }
86    }
87
88    fn wait_for_first_commit(&mut self, rx: &mpsc::Receiver<RenderEvent>) -> bool {
89        loop {
90            match rx.recv() {
91                Ok(RenderEvent::Commit(output)) => {
92                    self.commit(output);
93                    self.replay_queued_scrolls();
94                    return true;
95                }
96                Ok(RenderEvent::Shutdown) | Err(_) => return false,
97                Ok(event) => self.handle_event(event),
98            }
99        }
100    }
101
102    fn drain_events(&mut self, rx: &mpsc::Receiver<RenderEvent>) -> bool {
103        loop {
104            match rx.try_recv() {
105                Ok(RenderEvent::Shutdown) => return false,
106                Ok(event) => self.handle_event(event),
107                Err(mpsc::TryRecvError::Empty) => return true,
108                Err(mpsc::TryRecvError::Disconnected) => return false,
109            }
110        }
111    }
112
113    fn render_frame(&mut self) -> bool {
114        if let Some(frame) = self.compositor.produce_frame() {
115            let params = RenderParams {
116                frame: &frame,
117                width: self.width,
118                height: self.height,
119                scale_factor: self.scale_factor,
120            };
121            match self.surface.render(&params) {
122                Ok(()) => return true,
123                Err(RendererError::SurfaceLost) => {
124                    if let Some(cb) = self.on_surface_lost.take() {
125                        cb();
126                    }
127                    return false;
128                }
129                Err(e) => {
130                    eprintln!("kozan: render error: {e}");
131                    return true;
132                }
133            }
134        }
135        // No content — wait for next event.
136        true
137    }
138
139    fn handle_event(&mut self, event: RenderEvent) {
140        match event {
141            RenderEvent::Commit(output) => self.commit(output),
142            RenderEvent::Resize { width, height } => {
143                self.width = width;
144                self.height = height;
145                self.surface.resize(width, height);
146            }
147            RenderEvent::ScaleFactorChanged(sf) => self.scale_factor = sf,
148            RenderEvent::Scroll { delta, point } => self.apply_scroll(delta, point),
149            RenderEvent::Shutdown => {}
150        }
151    }
152
153    fn commit(&mut self, output: FrameOutput) {
154        self.compositor
155            .commit(output.display_list, output.layer_tree, output.scroll_tree);
156    }
157
158    fn apply_scroll(&mut self, delta: Offset, point: Point) {
159        if !self.compositor.has_content() {
160            self.queued_scrolls.push((delta, point));
161            return;
162        }
163
164        // Hit test: find the scrollable container under the cursor.
165        // Chrome: InputHandler::HitTestScrollNode() on compositor thread.
166        let target = self
167            .compositor
168            .hit_test_scroll_target(point)
169            .or_else(|| self.compositor.scroll_tree().root_scroller());
170
171        if let Some(target) = target {
172            if self.compositor.try_scroll(target, delta) {
173                let _ = self.view_tx.send(ViewEvent::ScrollSync(
174                    self.compositor.scroll_offsets().clone(),
175                ));
176            }
177        }
178    }
179
180    fn replay_queued_scrolls(&mut self) {
181        for (delta, point) in std::mem::take(&mut self.queued_scrolls) {
182            self.apply_scroll(delta, point);
183        }
184    }
185}