dais_document/
render_pipeline.rs1use std::collections::HashSet;
8use std::sync::Arc;
9
10use crossbeam_channel::{Receiver, Sender, TrySendError};
11
12use crate::cache::PageCache;
13use crate::page::{RenderSize, RenderedPage};
14use crate::source::DocumentSource;
15
16struct RenderRequest {
18 page_index: usize,
19 size: RenderSize,
20}
21
22struct RenderResult {
24 page_index: usize,
25 size: RenderSize,
26 page: RenderedPage,
27}
28
29pub struct RenderPipeline {
31 request_tx: Sender<RenderRequest>,
32 result_rx: Receiver<RenderResult>,
33 pending: HashSet<(usize, RenderSize)>,
35}
36
37pub const FALLBACK_RENDER_SIZE: RenderSize = RenderSize { width: 1920, height: 1080 };
41
42impl RenderPipeline {
43 #[allow(clippy::needless_pass_by_value)]
45 pub fn new(doc: Arc<dyn DocumentSource>, num_workers: usize) -> Self {
46 let (request_tx, request_rx) = crossbeam_channel::bounded::<RenderRequest>(64);
48 let (result_tx, result_rx) = crossbeam_channel::unbounded::<RenderResult>();
49
50 for _ in 0..num_workers {
51 let rx = request_rx.clone();
52 let tx = result_tx.clone();
53 let doc = Arc::clone(&doc);
54 std::thread::Builder::new()
55 .name("dais-render".into())
56 .spawn(move || render_worker(doc, rx, tx))
57 .expect("failed to spawn render thread");
58 }
59
60 Self { request_tx, result_rx, pending: HashSet::new() }
61 }
62
63 pub fn poll_results(&mut self, cache: &mut PageCache) {
66 while let Ok(result) = self.result_rx.try_recv() {
67 let key = (result.page_index, result.size);
68 self.pending.remove(&key);
69 cache.insert(result.page_index, result.size, result.page);
70 }
71 }
72
73 pub fn ensure_rendered(&mut self, page_index: usize, size: RenderSize, cache: &mut PageCache) {
76 if cache.get(page_index, size).is_some() {
77 return;
78 }
79 let key = (page_index, size);
80 if self.pending.contains(&key) {
81 return;
82 }
83 match self.request_tx.try_send(RenderRequest { page_index, size }) {
84 Ok(()) => {
85 self.pending.insert(key);
86 }
87 Err(TrySendError::Full(_)) => {
88 }
90 Err(TrySendError::Disconnected(_)) => {
91 tracing::error!("Render pipeline disconnected");
92 }
93 }
94 }
95
96 pub fn prefetch_neighborhood(
98 &mut self,
99 current_page: usize,
100 total_pages: usize,
101 size: RenderSize,
102 cache: &mut PageCache,
103 ) {
104 self.ensure_rendered(current_page, size, cache);
106
107 if current_page + 1 < total_pages {
109 self.ensure_rendered(current_page + 1, size, cache);
110 }
111
112 if current_page > 0 {
114 self.ensure_rendered(current_page - 1, size, cache);
115 }
116
117 if current_page + 2 < total_pages {
119 self.ensure_rendered(current_page + 2, size, cache);
120 }
121 }
122}
123
124#[allow(clippy::needless_pass_by_value)]
125fn render_worker(
126 doc: Arc<dyn DocumentSource>,
127 rx: Receiver<RenderRequest>,
128 tx: Sender<RenderResult>,
129) {
130 while let Ok(req) = rx.recv() {
131 match doc.render_page(req.page_index, req.size) {
132 Ok(page) => {
133 let _ = tx.send(RenderResult { page_index: req.page_index, size: req.size, page });
134 }
135 Err(e) => {
136 tracing::warn!("Background render failed for page {}: {e}", req.page_index);
137 }
138 }
139 }
140}