1use crate::diff::DiffSkipHint;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct RenderCertificateInputs {
36 pub prev_available: bool,
38 pub dims_changed: bool,
40 pub full_redraw_due: bool,
42 pub dirty_row_count: usize,
44 pub total_rows: u16,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50pub enum RenderCertificateLevel {
51 FullRequired,
53 SkipAll,
55 NarrowToDirty,
57}
58
59impl RenderCertificateLevel {
60 #[must_use]
62 pub const fn label(&self) -> &'static str {
63 match self {
64 Self::FullRequired => "full-required",
65 Self::SkipAll => "skip-all",
66 Self::NarrowToDirty => "narrow-to-dirty",
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct RenderCertificate {
74 pub level: RenderCertificateLevel,
76 pub causes: Vec<&'static str>,
78 pub dirty_rows: Vec<u16>,
80 pub fell_back: bool,
82}
83
84impl RenderCertificate {
85 #[must_use]
87 pub fn to_hint(&self) -> DiffSkipHint {
88 match self.level {
89 RenderCertificateLevel::FullRequired => DiffSkipHint::FullDiff,
90 RenderCertificateLevel::SkipAll => DiffSkipHint::SkipDiff,
91 RenderCertificateLevel::NarrowToDirty => {
92 DiffSkipHint::NarrowToRows(self.dirty_rows.clone())
93 }
94 }
95 }
96
97 #[must_use]
99 pub fn to_evidence_json(&self) -> String {
100 format!(
101 r#"{{"level":"{}","causes":[{}],"narrowed_rows":{},"fell_back":{}}}"#,
102 self.level.label(),
103 self.causes
104 .iter()
105 .map(|c| format!("\"{c}\""))
106 .collect::<Vec<_>>()
107 .join(","),
108 self.dirty_rows.len(),
109 self.fell_back
110 )
111 }
112}
113
114#[must_use]
119pub fn evaluate_render_certificate(
120 inputs: &RenderCertificateInputs,
121 dirty_rows: Vec<u16>,
122) -> RenderCertificate {
123 if !inputs.prev_available {
124 return RenderCertificate {
125 level: RenderCertificateLevel::FullRequired,
126 causes: vec!["no-previous-frame"],
127 dirty_rows: Vec::new(),
128 fell_back: true,
129 };
130 }
131 if inputs.dims_changed {
132 return RenderCertificate {
133 level: RenderCertificateLevel::FullRequired,
134 causes: vec!["viewport-changed"],
135 dirty_rows: Vec::new(),
136 fell_back: true,
137 };
138 }
139 if inputs.full_redraw_due {
140 return RenderCertificate {
141 level: RenderCertificateLevel::FullRequired,
142 causes: vec!["full-redraw-probe-due"],
143 dirty_rows: Vec::new(),
144 fell_back: true,
145 };
146 }
147 if inputs.dirty_row_count == 0 {
148 return RenderCertificate {
149 level: RenderCertificateLevel::SkipAll,
150 causes: vec!["zero-dirty-rows"],
151 dirty_rows: Vec::new(),
152 fell_back: false,
153 };
154 }
155 if dirty_rows.len() != inputs.dirty_row_count
158 || dirty_rows.iter().any(|&row| row >= inputs.total_rows)
159 {
160 return RenderCertificate {
161 level: RenderCertificateLevel::FullRequired,
162 causes: vec!["dirty-row-witness-inconsistent"],
163 dirty_rows: Vec::new(),
164 fell_back: true,
165 };
166 }
167 RenderCertificate {
168 level: RenderCertificateLevel::NarrowToDirty,
169 causes: vec!["dirty-rows-witnessed"],
170 dirty_rows,
171 fell_back: false,
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::buffer::Buffer;
179 use crate::cell::Cell;
180 use crate::diff::BufferDiff;
181
182 fn inputs(dirty: usize, rows: u16) -> RenderCertificateInputs {
183 RenderCertificateInputs {
184 prev_available: true,
185 dims_changed: false,
186 full_redraw_due: false,
187 dirty_row_count: dirty,
188 total_rows: rows,
189 }
190 }
191
192 #[test]
193 fn unprovable_conditions_force_full_work() {
194 let mut no_prev = inputs(3, 10);
195 no_prev.prev_available = false;
196 let cert = evaluate_render_certificate(&no_prev, vec![1, 2, 3]);
197 assert_eq!(cert.level, RenderCertificateLevel::FullRequired);
198 assert!(cert.fell_back);
199 assert_eq!(cert.causes, vec!["no-previous-frame"]);
200
201 let mut resized = inputs(3, 10);
202 resized.dims_changed = true;
203 let cert = evaluate_render_certificate(&resized, vec![1, 2, 3]);
204 assert_eq!(cert.causes, vec!["viewport-changed"]);
205
206 let mut probe = inputs(3, 10);
207 probe.full_redraw_due = true;
208 let cert = evaluate_render_certificate(&probe, vec![1, 2, 3]);
209 assert_eq!(cert.causes, vec!["full-redraw-probe-due"]);
210 }
211
212 #[test]
213 fn zero_dirty_rows_certifies_a_skip() {
214 let cert = evaluate_render_certificate(&inputs(0, 10), Vec::new());
215 assert_eq!(cert.level, RenderCertificateLevel::SkipAll);
216 assert!(!cert.fell_back);
217 assert!(matches!(cert.to_hint(), DiffSkipHint::SkipDiff));
218 }
219
220 #[test]
221 fn dirty_rows_certify_a_narrow_scan() {
222 let cert = evaluate_render_certificate(&inputs(2, 10), vec![3, 7]);
223 assert_eq!(cert.level, RenderCertificateLevel::NarrowToDirty);
224 match cert.to_hint() {
225 DiffSkipHint::NarrowToRows(rows) => assert_eq!(rows, vec![3, 7]),
226 other => panic!("expected narrow hint, got {other:?}"),
227 }
228 }
229
230 #[test]
231 fn inconsistent_witness_falls_back_to_full() {
232 let cert = evaluate_render_certificate(&inputs(2, 10), vec![3]);
234 assert_eq!(cert.level, RenderCertificateLevel::FullRequired);
235 assert!(cert.fell_back);
236 let cert = evaluate_render_certificate(&inputs(1, 10), vec![10]);
238 assert_eq!(cert.level, RenderCertificateLevel::FullRequired);
239 assert_eq!(cert.causes, vec!["dirty-row-witness-inconsistent"]);
240 }
241
242 #[test]
243 fn evidence_json_is_stable_and_named() {
244 let cert = evaluate_render_certificate(&inputs(2, 10), vec![3, 7]);
245 let json = cert.to_evidence_json();
246 assert_eq!(
247 json,
248 r#"{"level":"narrow-to-dirty","causes":["dirty-rows-witnessed"],"narrowed_rows":2,"fell_back":false}"#
249 );
250 }
251
252 #[test]
256 fn certified_changes_equal_dirty_changes_across_generated_frames() {
257 let (w, h) = (24u16, 8u16);
258 let mut seed = 0x9E37_79B9_7F4A_7C15u64;
259 let mut next = move || {
260 seed ^= seed << 13;
261 seed ^= seed >> 7;
262 seed ^= seed << 17;
263 seed
264 };
265
266 for round in 0..50 {
267 let old = Buffer::new(w, h);
268 let mut new = Buffer::new(w, h);
269 new.clear_dirty();
270
271 let mutations = (next() % 20) as usize;
272 for _ in 0..mutations {
273 let x = (next() % u64::from(w)) as u16;
274 let y = (next() % u64::from(h)) as u16;
275 let ch = char::from(b'A' + (next() % 26) as u8);
276 new.set(x, y, Cell::from_char(ch));
277 }
278
279 let dirty_count = new.dirty_row_count();
280 let dirty_rows = new.dirty_row_indices();
281 let cert = evaluate_render_certificate(&inputs(dirty_count, h), dirty_rows);
282 assert!(!cert.fell_back, "round {round}: unexpected fallback");
283
284 let mut certified = BufferDiff::new();
285 certified.compute_certified_into(&old, &new, cert.to_hint());
286 let mut truth = BufferDiff::new();
287 truth.compute_dirty_into(&old, &new);
288 assert_eq!(
289 certified.changes(),
290 truth.changes(),
291 "round {round}: certified path diverged ({} mutations)",
292 mutations
293 );
294 }
295 }
296}