1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// =============================================================================
// INVALIDATION MODEL -- Platform-wide dirty flag system (crosscrate.md Finding #3)
// =============================================================================
use crate::identity::KvasirId;
use serde::{Deserialize, Serialize};
/// Bitmask encoding which pipeline layers are dirty for a given object.
///
/// # Why this exists
/// The crosscrate audit (Finding #3) identified that each crate had its own
/// `is_dirty: bool` field with no shared semantic. Without a unified model,
/// updates propagate as full-tree redraws instead of targeted passes, leading
/// to performance collapse at scale.
///
/// # Layers (in pipeline order)
/// - `STATE` — application-level data changed (triggers LAYOUT + PAINT + COMPOSITE)
/// - `LAYOUT` — size or position changed (triggers PAINT + COMPOSITE)
/// - `PAINT` — visual appearance changed (triggers COMPOSITE only)
/// - `COMPOSITE` — compositing properties changed (e.g. opacity, transform, blur)
///
/// # Contract
/// A crate that dirtifies a layer MUST also dirtify all downstream layers.
/// Use the helper constants [`DirtyFlags::from_state_change`] etc. rather
/// than setting bits manually to ensure the invariant is maintained.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub struct DirtyFlags(pub u8);
impl DirtyFlags {
/// No layers are dirty.
pub const CLEAN: DirtyFlags = DirtyFlags(0b0000_0000);
/// Application state changed — propagates to all downstream layers.
pub const STATE: DirtyFlags = DirtyFlags(0b0000_1111);
/// Layout (size/position) changed — propagates to paint + composite.
pub const LAYOUT: DirtyFlags = DirtyFlags(0b0000_0111);
/// Paint (visual) changed — propagates to composite.
pub const PAINT: DirtyFlags = DirtyFlags(0b0000_0011);
/// Compositing properties changed (opacity, clip, backdrop).
pub const COMPOSITE: DirtyFlags = DirtyFlags(0b0000_0001);
/// All layers dirty (equivalent to STATE).
pub const ALL: DirtyFlags = DirtyFlags(0b0000_1111);
/// Returns `true` if any dirty bits are set.
#[inline]
pub fn is_dirty(self) -> bool {
self.0 != 0
}
/// Returns `true` if the composite layer needs reprocessing.
#[inline]
pub fn needs_composite(self) -> bool {
self.0 & 0b0000_0001 != 0
}
/// Returns `true` if the paint layer needs reprocessing.
#[inline]
pub fn needs_paint(self) -> bool {
self.0 & 0b0000_0010 != 0
}
/// Returns `true` if layout needs reprocessing.
#[inline]
pub fn needs_layout(self) -> bool {
self.0 & 0b0000_0100 != 0
}
/// Returns `true` if application state has changed.
#[inline]
pub fn needs_state(self) -> bool {
self.0 & 0b0000_1000 != 0
}
/// Merge another set of flags into this one (bitwise OR).
#[inline]
pub fn merge(self, other: DirtyFlags) -> DirtyFlags {
DirtyFlags(self.0 | other.0)
}
/// Clear all dirty flags, marking this object as clean.
#[inline]
pub fn clear(self) -> DirtyFlags {
DirtyFlags::CLEAN
}
}
impl std::ops::BitOr for DirtyFlags {
type Output = DirtyFlags;
fn bitor(self, rhs: DirtyFlags) -> DirtyFlags {
DirtyFlags(self.0 | rhs.0)
}
}
impl std::ops::BitOrAssign for DirtyFlags {
fn bitor_assign(&mut self, rhs: DirtyFlags) {
self.0 |= rhs.0;
}
}
impl std::ops::BitAnd for DirtyFlags {
type Output = DirtyFlags;
fn bitand(self, rhs: DirtyFlags) -> DirtyFlags {
DirtyFlags(self.0 & rhs.0)
}
}
/// A single invalidation record associating a `KvasirId` with its dirty layers.
///
/// # Contract
/// Invalidation records are produced by any system that mutates state and
/// consumed by the scheduler to determine what work must be done next frame.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct InvalidationRecord {
/// The object that was mutated.
pub id: KvasirId,
/// Which pipeline layers need reprocessing.
pub flags: DirtyFlags,
}
impl InvalidationRecord {
/// Create a new invalidation record.
pub fn new(id: KvasirId, flags: DirtyFlags) -> Self {
Self { id, flags }
}
/// Create a record indicating the object's full pipeline needs rebuilding.
pub fn full(id: KvasirId) -> Self {
Self {
id,
flags: DirtyFlags::ALL,
}
}
}