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
//! Intrinsic per-dab record header — the fields every terminal packs
//! at the front of every dab record, before any node-contributed fields.
//!
//! The per-dab record overall is *dynamic* (its layout is the union of
//! every node's contributed fields, laid out in alignment-descending
//! order), so there is no single Rust `#[repr(C)]` struct mirroring it.
//! The intrinsic header (`pos`, `bbox_target_px`,
//! `inv_radius_target_px`) is the fixed prefix, packed by hand here so
//! all four terminals — paint, watercolor, smudge, liquify — share one
//! source of truth.
use Arc;
use crate;
/// Numerical-stability floor for the target-px radius division. Not a
/// physical limit — the post-scale preview radius can legitimately drop
/// below 1 target px on extreme brush sizes (a sub-pixel-radius preview
/// is useless anyway). The clamp prevents 1/0 / huge inv values from
/// poisoning the fragment.
pub const EPS_RADIUS_TARGET_PX: f32 = 0.125;
/// Compile-time number of intrinsic fields the terminal packs itself
/// before per-node fields begin. Used by the terminal's packer to skip
/// over the header.
pub const INTRINSIC_DAB_HEADER_FIELDS: usize = 3;
/// Fields every per-dab record carries, regardless of upstream nodes:
/// dab centre (`pos`), bbox half-extent (`bbox_target_px`), and the
/// reciprocal of the dab's nominal radius (`inv_radius_target_px`).
///
/// **Invariant: the dab record describes the dab in the *target
/// texture's pixel space*.** Whichever texture the brush is rasterizing
/// into, `pos` is a coordinate in that texture's pixel grid,
/// `bbox_target_px` is a half-extent in those same pixels, and
/// `inv_radius_target_px` is `1.0 / radius_in_target_pixels`. Stroke
/// renders into the layer scratch where target px ≡ canvas px;
/// preview renders into the preview mask where target px ≢ canvas px.
/// Both paths pack a well-typed record for their target via
/// [`pack_intrinsic_dab_header`], and the WGSL is target-agnostic.
///
/// Why this matters: an earlier shape of this header carried `radius`
/// and `bbox_radius` in canvas pixels in both modes, which silently
/// broke the preview path's discard test when target ≠ canvas (the dab
/// filled the texture to a square edge on large brushes). Renaming
/// these fields to declare their frame makes the bug structurally
/// inexpressible.
///
/// `bbox_target_px` is the single source of truth for the dab's write
/// footprint — the vertex stage sizes the rasterized quad against it,
/// the fragment stage discards past it, and (in stroke mode) the CPU
/// layer-clip bbox is derived from the same value so save-points
/// cannot truncate the GPU writes.
/// Pack the intrinsic dab header. Single source of truth — every
/// terminal's `evaluate_gpu` (stroke path) and
/// [`crate::brush::wgsl::render_compiled_cursor_preview`] (preview path) call
/// this. The fields are interpreted in the *target texture's pixel
/// space* (see the docblock on [`intrinsic_dab_header`]). Internally
/// inverts radius once so the fragment hot path is a multiply, not a
/// divide.
///
/// Stroke-only consumers — notably watercolor's pickup shader in
/// `watercolor.rs` — treat `1 / inv_radius_target_px` as
/// canvas-px radius. That's valid only because stroke's target ≡
/// canvas. Any new sampler that derives canvas-px sizes from the dab
/// record must restrict itself to stroke-mode dispatch.