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, scene
67 /// upload and the final frame's encode and upload (for a remote lane: the marginal
68 /// rate between its first and its last progress report that advanced the count).
69 /// `None` lets the pool fall back to `done / wall time`.
70 pub rate: Option<f64>,
71 /// Why the chunk ended short, when it did. `None` together with `done` short of
72 /// the range is reported as "ended early".
73 pub error: Option<String>,
74}
75
76impl ChunkResult {
77 /// Every sample of the range was traced.
78 #[must_use]
79 pub const fn complete(sum: Vec<Vec3>, done: u32, rate: Option<f64>) -> Self {
80 Self {
81 sum,
82 done,
83 rate,
84 error: None,
85 }
86 }
87
88 /// Only the prefix `done` was traced before `error` ended the chunk.
89 #[must_use]
90 pub const fn partial(sum: Vec<Vec3>, done: u32, error: String) -> Self {
91 Self {
92 sum,
93 done,
94 rate: None,
95 error: Some(error),
96 }
97 }
98
99 /// Nothing was traced.
100 #[must_use]
101 pub const fn failed(error: String) -> Self {
102 Self {
103 sum: Vec::new(),
104 done: 0,
105 rate: None,
106 error: Some(error),
107 }
108 }
109}
110
111/// One backend contributing samples to an image: a joined remote worker, a TLS
112/// connection to a plain worker, or the local CPU/GPU. The [`crate::LanePool`] does
113/// not care which.
114///
115/// Implementations are shared across the pool's lane threads, hence `Send + Sync`;
116/// one lane is only ever asked for one chunk at a time by the pool.
117pub trait WorkerLane: Send + Sync {
118 /// A short human-readable name for notes ("worker gpu-01", "local CPU").
119 fn name(&self) -> &str;
120
121 /// Traces `range` of `scene` at `scene.width x scene.height` and returns the summed
122 /// radiance of the traced prefix.
123 ///
124 /// Must return promptly (with whatever prefix is done) once `cancel` is raised.
125 /// Must never trace or report samples outside `range`: a result whose `done`
126 /// exceeds `range.samples`, or whose `sum` has the wrong length, is discarded and
127 /// counted as a failed chunk.
128 fn render_chunk(
129 &self,
130 scene: &SceneState,
131 range: SampleRange,
132 cancel: &CancelToken,
133 ) -> ChunkResult;
134
135 /// Whether this lane's backing resource is gone for good (a joined worker's
136 /// connection closed): the pool then removes the lane from a running epoch instead
137 /// of retrying it through its backoff schedule, provided another lane remains. A lane
138 /// that is the pool's last keeps going through the ordinary failure path, so a lane
139 /// that can replace its resource on its own still gets the chance. Defaults to `false`.
140 fn lost(&self) -> bool {
141 false
142 }
143}