indicatrix_dispatch/lane.rs
1//! [`WorkerLane`]: one backend that traces sample chunks, and what it returns.
2
3use crate::CancelToken;
4use glam::Vec3;
5use indicatrix_net::SceneState;
6
7/// A contiguous absolute sample range `[first_sample, first_sample + samples)`, named
8/// like the protocol's `RenderRequest` fields.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct SampleRange {
11 /// The first absolute sample index.
12 pub first_sample: u32,
13 /// How many samples, starting at `first_sample`.
14 pub samples: u32,
15}
16
17impl SampleRange {
18 /// `[first_sample, first_sample + samples)`.
19 #[must_use]
20 pub const fn new(first_sample: u32, samples: u32) -> Self {
21 Self {
22 first_sample,
23 samples,
24 }
25 }
26
27 /// One past the last sample index.
28 #[must_use]
29 pub const fn end(self) -> u32 {
30 self.first_sample + self.samples
31 }
32
33 /// Whether the range holds no samples.
34 #[must_use]
35 pub const fn is_empty(self) -> bool {
36 self.samples == 0
37 }
38
39 /// The untraced tail after a valid prefix of `done` samples, `[first_sample +
40 /// done, end)`. `done` larger than the range yields an empty tail.
41 #[must_use]
42 pub const fn after_prefix(self, done: u32) -> Self {
43 let done = if done > self.samples {
44 self.samples
45 } else {
46 done
47 };
48 Self::new(self.first_sample + done, self.samples - done)
49 }
50}
51
52/// What one [`WorkerLane::render_chunk`] call produced.
53///
54/// `done` is always a PREFIX of the assigned range: `sum` holds exactly the samples
55/// `[first_sample, first_sample + done)`, never a gap and never anything past it. This
56/// is the worker protocol's own guarantee (a stream's delivered `FRAME`s cover the
57/// request from its start), so a lane over a real connection gets it for free.
58#[derive(Debug, Clone, PartialEq)]
59pub struct ChunkResult {
60 /// Per-pixel summed CIE XYZ radiance of the `done` traced samples, `width *
61 /// height` long. May be empty when `done == 0`.
62 pub sum: Vec<Vec3>,
63 /// How many samples of the assigned range were traced: the valid prefix.
64 pub done: u32,
65 /// The lane's own steady-state throughput measurement for this chunk, in
66 /// samples per second, excluding one-time costs such as connection setup and scene
67 /// upload (for a remote lane: the marginal rate between the first and last
68 /// progress report). `None` lets the pool fall back to `done / wall time`.
69 pub rate: Option<f64>,
70 /// Why the chunk ended short, when it did. `None` together with `done` short of
71 /// the range is reported as "ended early".
72 pub error: Option<String>,
73}
74
75impl ChunkResult {
76 /// Every sample of the range was traced.
77 #[must_use]
78 pub const fn complete(sum: Vec<Vec3>, done: u32, rate: Option<f64>) -> Self {
79 Self {
80 sum,
81 done,
82 rate,
83 error: None,
84 }
85 }
86
87 /// Only the prefix `done` was traced before `error` ended the chunk.
88 #[must_use]
89 pub const fn partial(sum: Vec<Vec3>, done: u32, error: String) -> Self {
90 Self {
91 sum,
92 done,
93 rate: None,
94 error: Some(error),
95 }
96 }
97
98 /// Nothing was traced.
99 #[must_use]
100 pub const fn failed(error: String) -> Self {
101 Self {
102 sum: Vec::new(),
103 done: 0,
104 rate: None,
105 error: Some(error),
106 }
107 }
108}
109
110/// One backend contributing samples to an image: a joined remote worker, a TLS
111/// connection to a plain worker, or the local CPU/GPU. The [`crate::LanePool`] does
112/// not care which.
113///
114/// Implementations are shared across the pool's lane threads, hence `Send + Sync`;
115/// one lane is only ever asked for one chunk at a time by the pool.
116pub trait WorkerLane: Send + Sync {
117 /// A short human-readable name for notes ("worker gpu-01", "local CPU").
118 fn name(&self) -> &str;
119
120 /// Traces `range` of `scene` at `scene.width x scene.height` and returns the summed
121 /// radiance of the traced prefix.
122 ///
123 /// Must return promptly (with whatever prefix is done) once `cancel` is raised.
124 /// Must never trace or report samples outside `range`: a result whose `done`
125 /// exceeds `range.samples`, or whose `sum` has the wrong length, is discarded and
126 /// counted as a failed chunk.
127 fn render_chunk(
128 &self,
129 scene: &SceneState,
130 range: SampleRange,
131 cancel: &CancelToken,
132 ) -> ChunkResult;
133}