use crate::diff::DiffSkipHint;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RenderCertificateInputs {
pub prev_available: bool,
pub dims_changed: bool,
pub full_redraw_due: bool,
pub dirty_row_count: usize,
pub total_rows: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RenderCertificateLevel {
FullRequired,
SkipAll,
NarrowToDirty,
}
impl RenderCertificateLevel {
#[must_use]
pub const fn label(&self) -> &'static str {
match self {
Self::FullRequired => "full-required",
Self::SkipAll => "skip-all",
Self::NarrowToDirty => "narrow-to-dirty",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderCertificate {
pub level: RenderCertificateLevel,
pub causes: Vec<&'static str>,
pub dirty_rows: Vec<u16>,
pub fell_back: bool,
}
impl RenderCertificate {
#[must_use]
pub fn to_hint(&self) -> DiffSkipHint {
match self.level {
RenderCertificateLevel::FullRequired => DiffSkipHint::FullDiff,
RenderCertificateLevel::SkipAll => DiffSkipHint::SkipDiff,
RenderCertificateLevel::NarrowToDirty => {
DiffSkipHint::NarrowToRows(self.dirty_rows.clone())
}
}
}
#[must_use]
pub fn to_evidence_json(&self) -> String {
format!(
r#"{{"level":"{}","causes":[{}],"narrowed_rows":{},"fell_back":{}}}"#,
self.level.label(),
self.causes
.iter()
.map(|c| format!("\"{c}\""))
.collect::<Vec<_>>()
.join(","),
self.dirty_rows.len(),
self.fell_back
)
}
}
#[must_use]
pub fn evaluate_render_certificate(
inputs: &RenderCertificateInputs,
dirty_rows: Vec<u16>,
) -> RenderCertificate {
if !inputs.prev_available {
return RenderCertificate {
level: RenderCertificateLevel::FullRequired,
causes: vec!["no-previous-frame"],
dirty_rows: Vec::new(),
fell_back: true,
};
}
if inputs.dims_changed {
return RenderCertificate {
level: RenderCertificateLevel::FullRequired,
causes: vec!["viewport-changed"],
dirty_rows: Vec::new(),
fell_back: true,
};
}
if inputs.full_redraw_due {
return RenderCertificate {
level: RenderCertificateLevel::FullRequired,
causes: vec!["full-redraw-probe-due"],
dirty_rows: Vec::new(),
fell_back: true,
};
}
if inputs.dirty_row_count == 0 {
return RenderCertificate {
level: RenderCertificateLevel::SkipAll,
causes: vec!["zero-dirty-rows"],
dirty_rows: Vec::new(),
fell_back: false,
};
}
if dirty_rows.len() != inputs.dirty_row_count
|| dirty_rows.iter().any(|&row| row >= inputs.total_rows)
{
return RenderCertificate {
level: RenderCertificateLevel::FullRequired,
causes: vec!["dirty-row-witness-inconsistent"],
dirty_rows: Vec::new(),
fell_back: true,
};
}
RenderCertificate {
level: RenderCertificateLevel::NarrowToDirty,
causes: vec!["dirty-rows-witnessed"],
dirty_rows,
fell_back: false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::buffer::Buffer;
use crate::cell::Cell;
use crate::diff::BufferDiff;
fn inputs(dirty: usize, rows: u16) -> RenderCertificateInputs {
RenderCertificateInputs {
prev_available: true,
dims_changed: false,
full_redraw_due: false,
dirty_row_count: dirty,
total_rows: rows,
}
}
#[test]
fn unprovable_conditions_force_full_work() {
let mut no_prev = inputs(3, 10);
no_prev.prev_available = false;
let cert = evaluate_render_certificate(&no_prev, vec![1, 2, 3]);
assert_eq!(cert.level, RenderCertificateLevel::FullRequired);
assert!(cert.fell_back);
assert_eq!(cert.causes, vec!["no-previous-frame"]);
let mut resized = inputs(3, 10);
resized.dims_changed = true;
let cert = evaluate_render_certificate(&resized, vec![1, 2, 3]);
assert_eq!(cert.causes, vec!["viewport-changed"]);
let mut probe = inputs(3, 10);
probe.full_redraw_due = true;
let cert = evaluate_render_certificate(&probe, vec![1, 2, 3]);
assert_eq!(cert.causes, vec!["full-redraw-probe-due"]);
}
#[test]
fn zero_dirty_rows_certifies_a_skip() {
let cert = evaluate_render_certificate(&inputs(0, 10), Vec::new());
assert_eq!(cert.level, RenderCertificateLevel::SkipAll);
assert!(!cert.fell_back);
assert!(matches!(cert.to_hint(), DiffSkipHint::SkipDiff));
}
#[test]
fn dirty_rows_certify_a_narrow_scan() {
let cert = evaluate_render_certificate(&inputs(2, 10), vec![3, 7]);
assert_eq!(cert.level, RenderCertificateLevel::NarrowToDirty);
match cert.to_hint() {
DiffSkipHint::NarrowToRows(rows) => assert_eq!(rows, vec![3, 7]),
other => panic!("expected narrow hint, got {other:?}"),
}
}
#[test]
fn inconsistent_witness_falls_back_to_full() {
let cert = evaluate_render_certificate(&inputs(2, 10), vec![3]);
assert_eq!(cert.level, RenderCertificateLevel::FullRequired);
assert!(cert.fell_back);
let cert = evaluate_render_certificate(&inputs(1, 10), vec![10]);
assert_eq!(cert.level, RenderCertificateLevel::FullRequired);
assert_eq!(cert.causes, vec!["dirty-row-witness-inconsistent"]);
}
#[test]
fn evidence_json_is_stable_and_named() {
let cert = evaluate_render_certificate(&inputs(2, 10), vec![3, 7]);
let json = cert.to_evidence_json();
assert_eq!(
json,
r#"{"level":"narrow-to-dirty","causes":["dirty-rows-witnessed"],"narrowed_rows":2,"fell_back":false}"#
);
}
#[test]
fn certified_changes_equal_dirty_changes_across_generated_frames() {
let (w, h) = (24u16, 8u16);
let mut seed = 0x9E37_79B9_7F4A_7C15u64;
let mut next = move || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
seed
};
for round in 0..50 {
let old = Buffer::new(w, h);
let mut new = Buffer::new(w, h);
new.clear_dirty();
let mutations = (next() % 20) as usize;
for _ in 0..mutations {
let x = (next() % u64::from(w)) as u16;
let y = (next() % u64::from(h)) as u16;
let ch = char::from(b'A' + (next() % 26) as u8);
new.set(x, y, Cell::from_char(ch));
}
let dirty_count = new.dirty_row_count();
let dirty_rows = new.dirty_row_indices();
let cert = evaluate_render_certificate(&inputs(dirty_count, h), dirty_rows);
assert!(!cert.fell_back, "round {round}: unexpected fallback");
let mut certified = BufferDiff::new();
certified.compute_certified_into(&old, &new, cert.to_hint());
let mut truth = BufferDiff::new();
truth.compute_dirty_into(&old, &new);
assert_eq!(
certified.changes(),
truth.changes(),
"round {round}: certified path diverged ({} mutations)",
mutations
);
}
}
}