use renderer_core::RenderBackend;
use super::FRAME_BUDGET;
pub(super) struct FrameMsg {
pub(super) width: u32,
pub(super) height: u32,
pub(super) scale_factor: f32,
pub(super) generation: u64,
pub(super) commands: Vec<renderer_core::DrawCommand>,
pub(super) clear: Option<renderer_core::Color>,
pub(super) timestamp: std::time::Instant,
}
pub(super) fn spawn_render_thread<R>(
renderer: R,
) -> (
std::sync::mpsc::SyncSender<FrameMsg>,
std::sync::mpsc::Receiver<Vec<renderer_core::DrawCommand>>,
std::thread::JoinHandle<R>,
)
where
R: RenderBackend + Send + 'static,
{
let (tx, rx) = std::sync::mpsc::sync_channel::<FrameMsg>(1);
let (ret_tx, ret_rx) = std::sync::mpsc::channel::<Vec<renderer_core::DrawCommand>>();
let join = std::thread::Builder::new()
.name("telar-render".to_string())
.spawn(move || {
let mut renderer = renderer;
renderer.bind_to_render_thread();
let mut current_width = 0u32;
let mut current_height = 0u32;
let mut scale_scratch = renderer_core::ScaleScratch::new();
let scales_itself = renderer.applies_scale_factor();
#[cfg(target_os = "android")]
let hint_session = platform_android::AdpfSession::new(16_666_667, None);
let idle_sweep_after = renderer.idle_sweep_after();
loop {
let msg = match idle_sweep_after {
Some(after) => match rx.recv_timeout(after) {
Ok(msg) => msg,
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
renderer.sweep_idle_caches();
match rx.recv() {
Ok(msg) => msg,
Err(_) => break,
}
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
},
None => match rx.recv() {
Ok(msg) => msg,
Err(_) => break,
},
};
let size_changed = msg.width != current_width || msg.height != current_height;
if !size_changed && msg.timestamp.elapsed() > FRAME_BUDGET {
let _ = ret_tx.send(msg.commands);
continue;
}
#[cfg(target_os = "android")]
let frame_start = std::time::Instant::now();
let began = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
renderer.begin_frame(msg.width, msg.height, msg.scale_factor, msg.generation)
}));
if !matches!(began, Ok(Ok(()))) {
let _ = ret_tx.send(msg.commands);
continue;
}
current_width = msg.width;
current_height = msg.height;
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let commands: &[renderer_core::DrawCommand] =
if scales_itself || msg.scale_factor == 1.0 {
&msg.commands
} else {
scale_scratch.scale_into(&msg.commands, msg.scale_factor)
};
renderer.render_frame(commands, msg.clear)
}));
#[cfg(target_os = "android")]
if let Some(session) = &hint_session {
let duration_ns = frame_start.elapsed().as_nanos() as i64;
session.report(duration_ns);
}
let _ = ret_tx.send(msg.commands);
}
renderer
})
.expect("failed to spawn render thread");
(tx, ret_rx, join)
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::time::{Duration, Instant};
use geometry_core::Rect;
use renderer_core::{DrawCommand, RectStyle, RendererError};
use super::*;
struct StubBackend {
scales_itself: bool,
rendered: Arc<AtomicU32>,
report: std::sync::mpsc::Sender<Drawn>,
size: (u32, u32),
bound: bool,
bound_before_first_frame: Arc<AtomicBool>,
idle_sweep_after: Option<Duration>,
sweeps: Arc<AtomicU32>,
}
impl RenderBackend for StubBackend {
fn applies_scale_factor(&self) -> bool {
self.scales_itself
}
fn bind_to_render_thread(&mut self) {
self.bound = true;
}
fn idle_sweep_after(&self) -> Option<Duration> {
self.idle_sweep_after
}
fn sweep_idle_caches(&mut self) {
self.sweeps.fetch_add(1, Ordering::SeqCst);
}
fn begin_frame(
&mut self,
width: u32,
height: u32,
_scale_factor: f32,
_generation: u64,
) -> Result<(), RendererError> {
if self.rendered.load(Ordering::SeqCst) == 0 {
self.bound_before_first_frame
.store(self.bound, Ordering::SeqCst);
}
self.size = (width, height);
Ok(())
}
fn render_frame(
&mut self,
commands: &[DrawCommand],
_clear: Option<renderer_core::Color>,
) -> Result<(), RendererError> {
self.rendered.fetch_add(1, Ordering::SeqCst);
let _ = self
.report
.send((self.size.0, self.size.1, commands.to_vec()));
Ok(())
}
}
fn rect(x: f32) -> DrawCommand {
DrawCommand::Rect {
rect: Rect::new(x, 0.0, 10.0, 10.0),
style: Arc::new(RectStyle::default()),
}
}
fn frame(width: u32, height: u32, scale_factor: f32, age: Duration) -> FrameMsg {
FrameMsg {
width,
height,
scale_factor,
generation: 0,
commands: vec![rect(20.0)],
clear: None,
timestamp: Instant::now() - age,
}
}
type Drawn = (u32, u32, Vec<DrawCommand>);
fn stub(
scales_itself: bool,
) -> (
StubBackend,
Arc<AtomicU32>,
std::sync::mpsc::Receiver<Drawn>,
) {
let (backend, rendered, seen, _) = stub_watching_bind(scales_itself);
(backend, rendered, seen)
}
fn stub_watching_bind(
scales_itself: bool,
) -> (
StubBackend,
Arc<AtomicU32>,
std::sync::mpsc::Receiver<Drawn>,
Arc<AtomicBool>,
) {
let rendered = Arc::new(AtomicU32::new(0));
let bound_before_first_frame = Arc::new(AtomicBool::new(false));
let (report, seen) = std::sync::mpsc::channel();
(
StubBackend {
scales_itself,
rendered: Arc::clone(&rendered),
report,
size: (0, 0),
bound: false,
bound_before_first_frame: Arc::clone(&bound_before_first_frame),
idle_sweep_after: None,
sweeps: Arc::new(AtomicU32::new(0)),
},
rendered,
seen,
bound_before_first_frame,
)
}
fn x_of(command: &DrawCommand) -> f32 {
match command {
DrawCommand::Rect { rect, .. } => rect.x,
other => panic!("unexpected command: {other:?}"),
}
}
#[test]
fn a_backend_that_does_not_scale_is_handed_scaled_commands() {
let (backend, _rendered, seen) = stub(false);
let (tx, _ret_rx, join) = spawn_render_thread(backend);
tx.send(frame(100, 50, 2.0, Duration::ZERO)).unwrap();
let (_, _, commands) = seen.recv_timeout(Duration::from_secs(5)).unwrap();
assert_eq!(
x_of(&commands[0]),
40.0,
"20px at scale 2 is 40 physical px"
);
drop(tx);
join.join().unwrap();
}
#[test]
fn a_backend_that_scales_itself_is_handed_logical_commands() {
let (backend, _rendered, seen) = stub(true);
let (tx, _ret_rx, join) = spawn_render_thread(backend);
tx.send(frame(100, 50, 2.0, Duration::ZERO)).unwrap();
let (_, _, commands) = seen.recv_timeout(Duration::from_secs(5)).unwrap();
assert_eq!(
x_of(&commands[0]),
20.0,
"left in logical px for the shader"
);
drop(tx);
join.join().unwrap();
}
#[test]
fn a_stale_frame_is_dropped_and_its_buffer_recycled() {
let (backend, rendered, seen) = stub(false);
let (tx, ret_rx, join) = spawn_render_thread(backend);
tx.send(frame(100, 50, 1.0, Duration::ZERO)).unwrap();
seen.recv_timeout(Duration::from_secs(5)).unwrap();
let _ = ret_rx.recv_timeout(Duration::from_secs(5)).unwrap();
tx.send(frame(100, 50, 1.0, Duration::from_millis(500)))
.unwrap();
let recycled = ret_rx.recv_timeout(Duration::from_secs(5)).unwrap();
assert_eq!(recycled.len(), 1, "the buffer comes back for refilling");
assert_eq!(
rendered.load(Ordering::SeqCst),
1,
"a frame older than the budget must not be drawn"
);
drop(tx);
join.join().unwrap();
}
#[test]
fn a_stale_frame_that_resizes_is_drawn_anyway() {
let (backend, rendered, seen) = stub(false);
let (tx, _ret_rx, join) = spawn_render_thread(backend);
tx.send(frame(100, 50, 1.0, Duration::ZERO)).unwrap();
seen.recv_timeout(Duration::from_secs(5)).unwrap();
tx.send(frame(640, 480, 1.0, Duration::from_millis(500)))
.unwrap();
let (w, h, _) = seen.recv_timeout(Duration::from_secs(5)).unwrap();
assert_eq!((w, h), (640, 480));
assert_eq!(rendered.load(Ordering::SeqCst), 2);
drop(tx);
join.join().unwrap();
}
#[test]
fn the_backend_is_bound_to_the_thread_before_the_first_frame() {
let (backend, _rendered, seen, bound_first) = stub_watching_bind(false);
let (tx, _ret_rx, join) = spawn_render_thread(backend);
tx.send(frame(100, 50, 1.0, Duration::ZERO)).unwrap();
seen.recv_timeout(Duration::from_secs(5)).unwrap();
assert!(
bound_first.load(Ordering::SeqCst),
"bind_to_render_thread must run before the first begin_frame"
);
drop(tx);
join.join().unwrap();
}
#[test]
fn an_idle_render_thread_sweeps_its_own_caches_once() {
let (mut backend, _rendered, seen, _) = stub_watching_bind(false);
backend.idle_sweep_after = Some(Duration::from_millis(30));
let sweeps = Arc::clone(&backend.sweeps);
let (tx, _ret_rx, join) = spawn_render_thread(backend);
tx.send(frame(100, 50, 1.0, Duration::ZERO)).unwrap();
seen.recv_timeout(Duration::from_secs(5)).unwrap();
assert_eq!(sweeps.load(Ordering::SeqCst), 0, "not while frames arrive");
std::thread::sleep(Duration::from_millis(300));
assert_eq!(
sweeps.load(Ordering::SeqCst),
1,
"one sweep per idle stretch, not a repeating timer"
);
tx.send(frame(100, 50, 1.0, Duration::ZERO)).unwrap();
seen.recv_timeout(Duration::from_secs(5)).unwrap();
drop(tx);
join.join().unwrap();
}
#[test]
fn joining_hands_the_renderer_back() {
let (backend, _rendered, seen) = stub(false);
let (tx, _ret_rx, join) = spawn_render_thread(backend);
tx.send(frame(320, 240, 1.0, Duration::ZERO)).unwrap();
seen.recv_timeout(Duration::from_secs(5)).unwrap();
drop(tx);
let recovered = join.join().expect("render thread panicked");
assert_eq!(recovered.size, (320, 240), "state survived the join");
}
}