Skip to main content

rlx_core/render/scenes/lines/
renderer.rs

1//! The shared line primitive: a GPU helper that draws thick, glowing lines as
2//! instanced camera-facing quads. Each [`SegmentInstance`] (two endpoints, a
3//! colour, a half-width) is expanded in the vertex shader into a quad whose
4//! width is uniform *on screen* — the swarm scene's instanced-quad pipeline
5//! (ADR-0007) with segments in place of points. Additive blend, so overlapping
6//! and dense strokes bloom.
7//!
8//! Native wgpu line primitives are deliberately not used: their width is locked
9//! near 1px and varies by backend (ADR-0007). The buffer is fixed-capacity and
10//! reused every frame, so a full curve upload never allocates on the hot path.
11
12// Hot-path panic-denial pragma (Plan 0002 Phase 2, extended to scenes by Plan
13// 0003 Phase 0). `draw` runs every displayed frame.
14#![deny(
15    clippy::unwrap_used,
16    clippy::expect_used,
17    clippy::indexing_slicing,
18    clippy::panic,
19    clippy::unreachable
20)]
21
22use crate::render::gpu;
23use crate::render::metrics::{self, DrawExtent};
24
25/// The sharpest corner a miter is drawn for, as a multiple of the half-width
26/// the miter would reach (ADR-0158).
27///
28/// A corner of interior angle `theta` needs `width / sin(theta / 2)`, which
29/// grows without bound as the corner sharpens: a spike that doubles back on
30/// itself would reach to infinity and paint a spear across the frame. Past the
31/// limit the joint **reverts to the flat half-width** — a bevel, which is what
32/// `stroke-miterlimit` selects in SVG and what an unmitred joint always drew.
33/// It is a fallback and not a truncation: clamping to `MITER_LIMIT * width`
34/// instead leaves the stroke reaching four half-widths past the vertex along its
35/// own direction, which reads as a burr rather than as a corner.
36///
37/// **`4.0` is adopted, not measured.** It is SVG's `stroke-miterlimit` default,
38/// and it draws exactly every corner at or above
39/// `2 * asin(1 / 4) = 28.96 degrees` — that is the angle at which
40/// `1 / sin(theta / 2)` reaches 4, so the limit is a statement about which
41/// corners are mitred and which are bevelled, not a tuned number (ADR-0071).
42/// `diamond`'s 61.9-degree vertex needs 1.9437 and is well inside it; a Maurer
43/// chord web's near-reversals are the population outside it.
44pub const MITER_LIMIT: f32 = 4.0;
45
46/// The extension a joined end needs to reach its corner's point: the miter
47/// length at `vertex`, where the chain arrives from `prev` and leaves for
48/// `next`, clamped to [`MITER_LIMIT`] half-widths (ADR-0158).
49///
50/// # The expression, and why it carries no trigonometry
51///
52/// For an interior angle `theta` the miter is `width / sin(theta / 2)`. Writing
53/// `d1`, `d2` for the two unit directions, the turn between them is
54/// `theta_turn = pi - theta`, so
55///
56/// ```text
57/// sin(theta / 2) = cos(theta_turn / 2) = sqrt((1 + d1 · d2) / 2)
58/// ```
59///
60/// — a dot product and a square root, with no `acos` to lose precision near the
61/// straight case and no branch on the turn's sign. A straight joint has
62/// `d1 · d2 = 1` and yields exactly `width`, which is the flat half-width, so a
63/// collinear chain is byte-identical to one that extends by `width`.
64///
65/// # Homogeneous of degree 1 in `width`
66///
67/// Both `width / sin(theta / 2)` and the clamp `MITER_LIMIT * width` scale
68/// linearly, so `miter(c * w) == c * miter(w)` and the clamp cannot be engaged
69/// at one width and not another. That is what lets the cached producers compute
70/// this against `PLACEHOLDER_WIDTH` at `configure` and have
71/// `LineInstance::styled` carry it to this frame's width by the ratio. Both are
72/// crate-private, so this names them rather than linking them. `theta` survives
73/// that too: every transform between a producer and the shader — uniform scale,
74/// rotation, reflection, the mirror replication, `normalize_fit` — is a
75/// similarity, and a similarity preserves angles.
76///
77/// # Degenerate input
78///
79/// A zero-length arm has no direction, and a chain that doubles back exactly
80/// (`d1 · d2 = -1`) has no reachable point. Both fall back to `width`, the flat
81/// half-width — which is the same value a corner past [`MITER_LIMIT`] takes, so
82/// the degenerate case is the limit case rather than a separate rule.
83pub fn miter_extension(width: f32, prev: [f32; 2], vertex: [f32; 2], next: [f32; 2]) -> f32 {
84    let unit = |from: [f32; 2], to: [f32; 2]| -> Option<[f32; 2]> {
85        let (dx, dy) = (to[0] - from[0], to[1] - from[1]);
86        let len = dx.hypot(dy);
87        (len > 1e-9).then(|| [dx / len, dy / len])
88    };
89    let (Some(d1), Some(d2)) = (unit(prev, vertex), unit(vertex, next)) else {
90        return width;
91    };
92    miter_extension_between(width, d1, d2)
93}
94
95/// [`miter_extension`] for a chain that carries its own tangents rather than a
96/// third point — a fitted arc/line chain, where the direction a neighbour
97/// arrives or leaves at is a property of the piece and not of any vertex.
98///
99/// `incoming` and `outgoing` are **unit** directions of travel through the
100/// joint. A G1 joint has them equal, so this returns exactly `width` there and a
101/// tangent-continuous run is byte-identical to one extended by the flat
102/// half-width; only the chain's genuine corners move.
103pub fn miter_extension_between(width: f32, incoming: [f32; 2], outgoing: [f32; 2]) -> f32 {
104    let half = ((1.0 + incoming[0] * outgoing[0] + incoming[1] * outgoing[1]) * 0.5)
105        .max(0.0)
106        .sqrt();
107    if half <= 1.0 / MITER_LIMIT {
108        // Bevel, not a truncated miter — see `MITER_LIMIT`.
109        return width;
110    }
111    width / half
112}
113
114/// One line segment: endpoints `a`/`b` in world space (x is divided by aspect
115/// in the shader, matching the swarm's convention), an RGB colour, a
116/// half-width in world units, and the per-endpoint extension length the join
117/// needs.
118#[repr(C)]
119#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
120pub struct SegmentInstance {
121    /// First endpoint (world space).
122    pub a: [f32; 2],
123    /// Second endpoint (world space).
124    pub b: [f32; 2],
125    /// RGB colour (pre-brightness; additive blend sums overlaps).
126    pub color: [f32; 3],
127    /// Half-width in **world units** — the isotropic space, where the stroke is
128    /// the same thickness on screen at every orientation (ADR-0160). It is
129    /// numerically an NDC-y half-width: world y and NDC y are the same axis, so
130    /// the horizontal case is the same number in either space and every other
131    /// orientation agrees with it.
132    ///
133    /// **Unless the draw selected [`StrokeMetric::Clip`]**, where this is an NDC
134    /// half-width and the on-screen thickness varies with the segment's
135    /// orientation by up to the aspect. That metric is per draw call rather than
136    /// per instance, and `warp_mesh` is the one surface on it.
137    pub width: f32,
138    /// How far the quad extends **backward** past `a`, along the segment's own
139    /// direction, in the same world half-width units as [`width`](Self::width).
140    /// `0.0` is a free end (ADR-0158).
141    ///
142    /// The length is the **producer's** to compute: only it knows whether an end
143    /// is shared with a neighbour and at what interior angle, which is the same
144    /// argument that put connectivity on the producer side. `0.0` renders exactly
145    /// the geometry an unflagged end rendered, because `dir * 0.0` is exactly
146    /// zero — that is what keeps the isolated producers, `spectrum`'s `Bars` and
147    /// `RadialRing`, byte-identical.
148    ///
149    /// **A length, not a factor.** It is resolved against `width` at
150    /// the moment the producer fills the instance. Every producer in this crate
151    /// rebuilds its instance buffer per frame, so the two cannot drift; a
152    /// producer that ever caches instances across frames while animating
153    /// `thickness` would have to recompute this alongside it.
154    pub ext_a: f32,
155    /// **How much of its own footprint this stroke occupies**, on top of the
156    /// across-the-stroke falloff — the fragment's alpha is `falloff * alpha`.
157    ///
158    /// `1.0` for every producer that draws through the additive seam, and that is
159    /// the value to pass unless you know otherwise: ADR-0056's rule is that a
160    /// dimmed stroke still covers its own footprint, so brightness belongs in
161    /// [`color`](Self::color) and never here. Every line scene in this crate
162    /// passes `1.0`, which makes the fragment exactly what it was before this
163    /// field existed.
164    ///
165    /// What it is for is [`LineRenderer::draw_split`]'s second range, where the
166    /// stroke is composited **over** rather than added and the blend needs the
167    /// producer's real coverage: a MilkDrop waveform at `wave_a = 0.1` must
168    /// replace a tenth of what is under it, not all of it (Plan 0100 Phase 4).
169    ///
170    /// **Its position in this struct is load-bearing.** `vertex_attr_array!`
171    /// derives each attribute's byte offset from the order of the *shader
172    /// locations*, so a field inserted ahead of this one shifts location 5 onto
173    /// the bytes location 4 is reading and every attribute after it by one slot.
174    /// The result compiles, renders, and quietly reinterprets one field as
175    /// another's mantissa — which is what it did, moving five composite golden
176    /// baselines. **A new field goes last**, which is where
177    /// [`ext_b`](Self::ext_b) is.
178    pub alpha: f32,
179    /// How far the quad extends **forward** past `b`. The `b`-end counterpart of
180    /// [`ext_a`](Self::ext_a), in the same units and with the same `0.0`-is-free
181    /// convention.
182    ///
183    /// **Declared last**, for the reason [`alpha`](Self::alpha) records: it was
184    /// appended when the endpoint stopped carrying a flag and started carrying a
185    /// length, and appending is the only placement that re-points nothing.
186    pub ext_b: f32,
187}
188
189/// One **circular arc**: a centre and radius in world space, a signed angular
190/// span in radians, an RGB colour and a half-width in NDC-y units — the same
191/// conventions [`SegmentInstance`] uses, so the two kinds place geometry in one
192/// coordinate system and stroke it to one profile.
193///
194/// Expanded in the vertex shader to a single bounding quad and shaded by the
195/// **per-pixel distance to the arc** (ADR-0098) rather than by an
196/// interpolated across-the-stroke coordinate. So a `circle` is one instance
197/// with no vertices at any resolution, where the segment path needs one
198/// instance and one additive joint per sample.
199///
200/// **No extension fields: an arc has no interior joints**, which is the whole
201/// point of the primitive. Where two arcs in a chain meet they overlap by a
202/// half-width as any two strokes do, and the additive composite
203/// sums that overlap exactly as it does for segments — the bead is reduced by
204/// there being fewer joints, not by a joint doing anything different.
205///
206/// **No `alpha` field either.** Every arc producer draws through ADR-0056's
207/// additive seam, where the coverage a premultiplied fragment carries is the
208/// stroke's own falloff; the OVER range [`LineRenderer::draw_split`] serves is
209/// a MilkDrop waveform, which is segments. A future over-blended arc adds the
210/// field **last**, for the reason [`SegmentInstance::alpha`] records.
211///
212/// **Field order is shader-location order**, and that is load-bearing for the
213/// same reason it is on [`SegmentInstance`]: `vertex_attr_array!` derives each
214/// attribute's byte offset from the order of the locations, so a field inserted
215/// anywhere but the end silently re-points every attribute after it.
216#[repr(C)]
217#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
218pub struct ArcInstance {
219    /// Centre of curvature (world space).
220    pub centre: [f32; 2],
221    /// Radius (world space). The arc's centreline, not either stroke edge.
222    pub radius: f32,
223    /// Where the span starts, in radians, measured the usual way from `+x`.
224    pub angle_start: f32,
225    /// How far it sweeps, **signed**. `|sweep|` may exceed `PI`, and a full
226    /// circle is one instance at `sweep = TAU`.
227    pub angle_sweep: f32,
228    /// RGB colour (pre-brightness; additive blend sums overlaps).
229    pub color: [f32; 3],
230    /// Half-width in world units — the same quantity, in the same space, as
231    /// [`SegmentInstance::width`]. Named `width` to match its sibling; it is a
232    /// half-width in both.
233    ///
234    /// There is no [`StrokeMetric`] choice here: no arc producer wants the clip
235    /// metric, because `warp_mesh` — the one surface still on it — emits no
236    /// [`ArcInstance`] at all (ADR-0160).
237    pub width: f32,
238}
239
240/// One segment in **3D**, drawn through the shared camera (ADR-0257): two world
241/// endpoints, an RGB colour, a width in pixels at the camera's reference depth
242/// and an alpha.
243///
244/// Drawn by the `seg3d` pipeline, which [`LineRenderer::new_3d`] builds and
245/// `LineRenderer::draw_3d` selects — a separate pipeline and instance buffer
246/// rather than a branch in the 2D one, so every 2D line scene keeps its bytes.
247///
248/// **Field order is shader-location order**, for the reason
249/// [`SegmentInstance::alpha`] records: a field inserted anywhere but the end
250/// re-points every attribute after it.
251#[repr(C)]
252#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
253pub struct Segment3dInstance {
254    /// First endpoint, world space, in front of the near plane.
255    pub a: [f32; 3],
256    /// Second endpoint, world space, in front of the near plane.
257    pub b: [f32; 3],
258    /// RGB light (pre-glow; additive blend sums overlaps).
259    pub color: [f32; 3],
260    /// Full stroke width in **pixels of the render target** at the camera's
261    /// reference depth; each end is scaled by perspective from there.
262    pub width: f32,
263    /// How much of the stroke is present: its light and its coverage are both
264    /// multiplied by this, so a fading edge fades out of the frame rather than
265    /// into a dark line.
266    pub alpha: f32,
267    /// The polyline's point before [`a`](Self::a), or `a` itself where `a` is
268    /// a free end. A joined end is mitred in screen space against the
269    /// direction it arrives from, so two neighbours meet on one line instead
270    /// of overlapping (ADR-0263); a free end draws flat, as an unjoined stroke
271    /// always did.
272    ///
273    /// **Bit-equal to the neighbour's own `a`**, or the two quads compute the
274    /// shared corner from different numbers and leave a seam:
275    /// `joined_chord` is what makes it so, near-plane clip included.
276    pub prev: [f32; 3],
277    /// The polyline's point after [`b`](Self::b), or `b` itself where `b` is a
278    /// free end. The `b`-end counterpart of [`prev`](Self::prev).
279    pub next: [f32; 3],
280    /// `1.0` on a **skirt**: not a stroke but a filled band from `a -> b` down
281    /// to the ground plane the draw names
282    /// (`LineRenderer::draw_3d_terrain`), at full coverage times
283    /// [`alpha`](Self::alpha) in [`color`](Self::color). A solid waterfall
284    /// row lays one under each of its segments, so a near row hides what lies
285    /// behind it (ADR-0263). `0.0` on every stroke.
286    pub skirt: f32,
287}
288
289/// The chord `pa -> pb` of a polyline, clipped against `view`'s near plane, as
290/// `[prev, a, b, next]` for a [`Segment3dInstance`] — or `None` when the whole
291/// chord lies behind the plane.
292///
293/// `before` is the point ahead of `pa` and `after` the point past `pb`, `None`
294/// at an open polyline's ends. A neighbour that reaches behind the plane is
295/// taken where its own chord meets the plane, which is **exactly the point**
296/// that chord's own [`CameraView::clip_near`] cuts to — the same arguments in
297/// the same order — so the two quads still share their corner bit for bit. An
298/// end that was itself cut has no neighbour on screen and is left free.
299///
300/// [`CameraView::clip_near`]: crate::render::camera::CameraView::clip_near
301pub(crate) fn joined_chord(
302    view: &crate::render::camera::CameraView,
303    before: Option<[f32; 3]>,
304    pa: [f32; 3],
305    pb: [f32; 3],
306    after: Option<[f32; 3]>,
307) -> Option<[[f32; 3]; 4]> {
308    let (a, b) = view.clip_near(pa, pb)?;
309    let prev = match before {
310        Some(p) if a == pa => view.clip_near(p, pa).map_or(a, |(cut, _)| cut),
311        _ => a,
312    };
313    let next = match after {
314        Some(n) if b == pb => view.clip_near(pb, n).map_or(b, |(_, cut)| cut),
315        _ => b,
316    };
317    Some([prev, a, b, next])
318}
319
320/// **Which space the stroke is measured in** (ADR-0160) — the half-width, the
321/// join extensions and the direction all three are taken along.
322///
323/// A per-draw-call parameter rather than a per-instance field or a second
324/// pipeline: it rides the segment uniform's `v.w`, a lane that was unused, so it
325/// costs no bytes, no bind-group entry and no new resource (ADR-0058).
326///
327/// Explicit at every call site rather than inferred from which entry point the
328/// caller used, so a producer that picks the wrong one is visible in its own
329/// source rather than implicit in a pipeline.
330#[derive(Clone, Copy, Debug, PartialEq, Eq)]
331pub enum StrokeMetric {
332    /// **World space, the isotropic one**: a world displacement `(dx, dy)` lands
333    /// on `(dx * H/2, dy * H/2)` pixels whichever axis it is on, because
334    /// dividing x by the aspect is exactly what squares the space up. So the
335    /// half-width is a thickness on screen and the extension is a length on
336    /// screen, at every orientation.
337    ///
338    /// What the four line families pass, and what makes a producer's
339    /// world-space interior angle the angle the shader extends along (ADR-0158).
340    World,
341    /// **Clip space, the anisotropic one**: one clip x unit is `aspect` times as
342    /// many pixels as one clip y unit, so a vertical stroke is `aspect` times
343    /// thicker on screen than a horizontal one at the same `width`.
344    ///
345    /// `warp_mesh` is the one surface on it, and it is a **dated deferral, not
346    /// an endorsement** — see the comment at its call site.
347    Clip,
348}
349
350impl StrokeMetric {
351    /// The `v.w` lane's value. `World` is `0.0`, so the uniform a line family
352    /// writes is byte-identical to the one written before this existed.
353    fn lane(self) -> f32 {
354        match self {
355            Self::World => 0.0,
356            Self::Clip => 1.0,
357        }
358    }
359}
360
361#[repr(C)]
362#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
363struct Uniforms {
364    // x: aspect, y: glow multiplier, z: softness (ADR-0124), w: the stroke
365    // metric (ADR-0160) — 0 world, 1 clip
366    v: [f32; 4],
367    // x: zoom, yz: pan, w: unused — the shared ViewTransform (ADR-0018)
368    view: [f32; 4],
369}
370
371/// The across-the-stroke profile, **one definition prepended to both fragment
372/// modules** (ADR-0124).
373///
374/// `u` runs 0 at the stroke edge to 1 at the centreline; `du` is that
375/// coordinate's change per pixel of the render target, from `fwidth` — so the
376/// ramp derived from it is a width in **pixels of the render target** rather
377/// than a fraction of the stroke, and no uniform has to carry a resolution.
378/// `softness` is the ramp's width as a fraction of the half-width:
379///
380/// - **`1.0` reduces the whole expression to `g = u²` term for term** — the
381///   profile every line scene drew before this parameter existed. That equality
382///   is what the golden corpus rests on, and it holds **only because `edge` is
383///   capped at 1.0**: a sub-pixel stroke drives `fwidth` above 1, where an
384///   uncapped `max(softness, edge)` would divide `u` down and *dim* the stroke
385///   instead of sharpening it. `warp_mesh`'s pin
386///   ([`MILKDROP_SOFTNESS`](crate::render::scenes::warp_mesh::MILKDROP_SOFTNESS))
387///   is byte-identical for the same reason — its `THIN` stroke is 1.0–1.35 px of
388///   half-width, exactly that regime.
389/// - `0.5` makes the inner half of the stroke solid and ramps across the outer.
390/// - `0` is a solid stroke whose coverage falls to zero across **one pixel**,
391///   whatever the stroke width, the resolution or the aspect.
392///
393/// Shared rather than written twice: two copies of a profile is a divergence
394/// that compiles, and here it would mean a mandala whose circles and interlace
395/// stop matching.
396///
397/// **`fwidth` exists only in a fragment shader**, so each caller evaluates it at
398/// the fragment's top level and passes the result in — which also keeps the call
399/// out of the arc fragment's non-uniform endpoint branch.
400const PROFILE_WGSL: &str = r#"
401fn stroke_coverage(u: f32, du: f32, softness: f32) -> f32 {
402    let edge = clamp(du, 1e-6, 1.0);
403    let ramp = max(clamp(softness, 0.0, 1.0), edge);
404    let core = clamp(u / ramp, 0.0, 1.0);
405    return core * core;
406}
407"#;
408
409/// The WGSL body, minus the shared profile — [`shader_source`] prepends that.
410const SHADER_BODY: &str = r#"
411struct Uniforms {
412    v: vec4<f32>,
413    view: vec4<f32>,
414}
415
416@group(0) @binding(0) var<uniform> u: Uniforms;
417
418struct VsOut {
419    @builtin(position) pos: vec4<f32>,
420    @location(0) side: f32,
421    @location(1) color: vec3<f32>,
422    @location(2) alpha: f32,
423}
424
425@vertex
426fn vs_main(
427    @builtin(vertex_index) vi: u32,
428    @location(0) a: vec2<f32>,
429    @location(1) b: vec2<f32>,
430    @location(2) color: vec3<f32>,
431    @location(3) width: f32,
432    @location(4) ext_a: f32,
433    @location(5) alpha: f32,
434    @location(6) ext_b: f32,
435) -> VsOut {
436    // (along, side): along runs a->b, side spans -1..1 across the width.
437    var corners = array<vec2<f32>, 6>(
438        vec2<f32>(0.0, -1.0), vec2<f32>(1.0, -1.0), vec2<f32>(0.0, 1.0),
439        vec2<f32>(0.0, 1.0), vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0),
440    );
441    let c = corners[vi];
442    let aspect = max(u.v.x, 0.1);
443    let inv_aspect = 1.0 / aspect;
444
445    // Shared ViewTransform (ADR-0018): zoom about the frame centre, then pan, in
446    // world space before the aspect divide. Endpoints move; stroke width does not.
447    let zoom = u.view.x;
448    let pan = u.view.yz;
449    let a_v = a * zoom + pan;
450    let b_v = b * zoom + pan;
451
452    // The stroke is measured in WORLD space, which is the isotropic one: a
453    // world displacement lands on the same number of pixels whichever axis it
454    // is on, because dividing x by the aspect is exactly what squares the space
455    // up. So the perpendicular offset below is a uniform on-screen thickness
456    // whatever the segment's orientation, and the extension is a length on
457    // screen (ADR-0160). The divide happens once, on the way out.
458    //
459    // `v.w` selects the space, per draw call: `warp_mesh` passes the CLIP
460    // metric, which scales x in on the way IN and leaves `out.pos` alone, so
461    // the offset lands in the anisotropic space. Multiplying by exactly 1.0 is
462    // exact in f32, so each arm carries only its own scale - and at aspect 1.0
463    // `inv_aspect` is exactly 1.0 and the two arms are the same arithmetic.
464    let clip_metric = u.v.w > 0.5;
465    let into_stroke = select(1.0, inv_aspect, clip_metric);
466    let out_of_stroke = select(inv_aspect, 1.0, clip_metric);
467    let a_s = vec2<f32>(a_v.x * into_stroke, a_v.y);
468    let b_s = vec2<f32>(b_v.x * into_stroke, b_v.y);
469    var dir = b_s - a_s;
470    let len = length(dir);
471    if (len > 1e-6) {
472        dir = dir / len;
473    } else {
474        dir = vec2<f32>(1.0, 0.0);
475    }
476    let nrm = vec2<f32>(-dir.y, dir.x);
477
478    // Join (ADR-0158): an end that continues into a neighbouring segment is
479    // pushed past that endpoint along its **own** direction by the length the
480    // producer computed for it. Adjacent quads then overlap across the shared
481    // vertex and the additive falloff fills the wedge the two divergent
482    // perpendiculars would otherwise leave. The producer is the only party that
483    // can compute it, because a segment cannot see its neighbour's direction.
484    // Each end is independent, and a free end is exactly `0.0` — `dir * 0.0` is
485    // exactly zero, so a producer that extends nothing is byte-identical.
486    let a_j = a_s - dir * ext_a;
487    let b_j = b_s + dir * ext_b;
488
489    let base = mix(a_j, b_j, c.x);
490    let pos = base + nrm * c.y * width;
491
492    var out: VsOut;
493    out.pos = vec4<f32>(pos.x * out_of_stroke, pos.y, 0.0, 1.0);
494    out.side = c.y;
495    out.color = color;
496    out.alpha = alpha;
497    return out;
498}
499
500@fragment
501fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
502    // The shared profile (ADR-0124): a solid core of width `softness`, then a
503    // quadratic ramp to the quad edge, floored at one pixel of the render
504    // target. `side` spans -1..1 across the half-width, so `1 - |side|` is the
505    // coordinate the profile takes.
506    //
507    // The derivative is read off `side` and NOT off `|side|`, whose kink at the
508    // centreline would make `fwidth` meaningless on the 2x2 quad that straddles
509    // it. Away from that quad the two are equal in magnitude.
510    let inward = max(0.0, 1.0 - abs(in.side));
511    let g = stroke_coverage(inward, fwidth(in.side), u.v.z);
512    // Premultiplied: colour AND alpha carry the same coverage `g * alpha`, so
513    // the two long edges of the quad - where the across-the-stroke falloff
514    // reaches zero - write nothing at all rather than opaque black (ADR-0056).
515    // Note the glow multiplier scales the LIGHT, not the coverage: a dimmed
516    // stroke still covers its own footprint. See
517    // `gpu::ADDITIVE_LIGHT_SATURATING_COVERAGE`.
518    //
519    // `alpha` is 1.0 for every additive producer, so this is byte-identical to
520    // the pre-Plan-0100 fragment for all of them; it is the OVER pipeline's
521    // second range that passes anything else. The colour is NOT divided by it -
522    // an over-blended producer arrives premultiplied already.
523    return vec4<f32>(in.color * g * u.v.y, g * in.alpha);
524}
525"#;
526
527/// The full WGSL: the shared profile prepended to the body.
528///
529/// **The prelude carries no constants.** An endpoint's extension is an `f32`
530/// length the shader multiplies by a direction (ADR-0158), and a float has no
531/// bit assignment that could disagree with a Rust-side numbering — so there is
532/// nothing here to generate and keep in step.
533///
534/// Prepending rather than `format!`-ing the whole body is deliberate: the body
535/// is full of braces and every one would need escaping.
536///
537/// Runs once per [`LineRenderer::new`] (pipeline build, not the hot path).
538fn shader_source() -> String {
539    format!("{PROFILE_WGSL}\n{SHADER_BODY}")
540}
541
542/// The arc pipeline's full WGSL: [`PROFILE_WGSL`] prepended to [`ARC_SHADER`].
543///
544/// The two fragments are separate modules, so **the profile is shared by
545/// construction rather than by convention** - the same reason [`shader_source`]
546/// prepends it instead of the body restating it. Two hand-kept copies of the
547/// expression would compile, render, and give a mandala whose circles and
548/// interlace stop matching.
549///
550/// Runs once per [`LineRenderer::new_with_arcs`] (pipeline build, not the hot
551/// path).
552fn arc_shader_source() -> String {
553    format!(
554        "{PROFILE_WGSL}
555{ARC_SHADER}"
556    )
557}
558
559/// The arc pipeline's WGSL, **a separate shader module** from [`SHADER_BODY`]
560/// and built only when a scene asked for arcs.
561///
562/// Separate rather than two more entry points in the one module, so a
563/// `LineRenderer` without arcs creates exactly the resources it created before
564/// this existed. Appending to the shared module would have changed what every
565/// line scene compiles, and on the software adapter the golden suite captures
566/// on, a changed resource is a changed picture (ADR-0058; measured on WARP,
567/// unverified on lavapipe as of 2026-10-06).
568///
569/// # What the fragment computes, and in which space
570///
571/// The arc is authored in **world space**, which is isotropic on screen: the
572/// vertex shader divides x by the aspect on the way out, so a world circle is a
573/// circle in pixels. The **stroke** is measured there too (ADR-0160), which is
574/// where the segment path measures it — so the two primitives place geometry
575/// and stroke it in one space, and this arc draws the same picture as a densely
576/// sampled polyline of it.
577///
578/// So the signed distance is `length(p - c) - r`, the exact world radial
579/// distance, used as it stands. Outside the angular span it is the distance to
580/// the nearer endpoint, which is a point, so that arm is exact too.
581///
582/// **The aspect is the render target's** (ADR-0037): it arrives in the uniform
583/// `draw` was handed, and there is no internal grid, texture or second size
584/// anywhere in this shader for another one to come from. This family has
585/// shipped that bug three times, which is why the control renders at a
586/// non-16:9 target where a grid-derived aspect and the target's disagree.
587const ARC_SHADER: &str = r#"
588struct Uniforms {
589    v: vec4<f32>,
590    view: vec4<f32>,
591}
592
593@group(0) @binding(0) var<uniform> u: Uniforms;
594
595const TAU: f32 = 6.2831853071795864;
596const QUARTER_TURN: f32 = 1.5707963267948966;
597
598struct VsOut {
599    @builtin(position) pos: vec4<f32>,
600    @location(0) ndc: vec2<f32>,
601    @location(1) @interpolate(flat) centre: vec2<f32>,
602    @location(2) @interpolate(flat) radius: f32,
603    // (lo, hi): the span's two ends in increasing order, so the sweep's sign
604    // stops mattering past this point and the two endpoints are the same two
605    // points either way.
606    @location(3) @interpolate(flat) span: vec2<f32>,
607    @location(4) @interpolate(flat) color: vec3<f32>,
608    @location(5) @interpolate(flat) width: f32,
609}
610
611@vertex
612fn vs_main(
613    @builtin(vertex_index) vi: u32,
614    @location(0) centre: vec2<f32>,
615    @location(1) radius: f32,
616    @location(2) angle_start: f32,
617    @location(3) angle_sweep: f32,
618    @location(4) color: vec3<f32>,
619    @location(5) width: f32,
620) -> VsOut {
621    // The unit square, expanded to the arc's own bounding box below.
622    var corners = array<vec2<f32>, 6>(
623        vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 0.0), vec2<f32>(0.0, 1.0),
624        vec2<f32>(0.0, 1.0), vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 1.0),
625    );
626    let c = corners[vi];
627    let aspect = max(u.v.x, 0.1);
628
629    // Shared ViewTransform (ADR-0018), applied exactly as the segment shader
630    // applies it: in world space, before the aspect divide. A uniform zoom
631    // scales the radius with the centre; stroke width does not move.
632    let centre_v = centre * u.view.x + u.view.yz;
633    let radius_v = radius * u.view.x;
634
635    let a0 = angle_start;
636    let a1 = angle_start + angle_sweep;
637    let lo = min(a0, a1);
638    let hi = max(a0, a1);
639
640    // The centreline's bounding box: the two ends always, plus each axis
641    // extreme the span actually reaches. A full turn reaches all four and the
642    // box is the whole circle; a short span gets a box barely bigger than its
643    // own chord, which is what keeps the shaded area near the stroke's.
644    let end0 = vec2<f32>(cos(a0), sin(a0));
645    let end1 = vec2<f32>(cos(a1), sin(a1));
646    var lo_dir = min(end0, end1);
647    var hi_dir = max(end0, end1);
648    for (var k = 0u; k < 4u; k = k + 1u) {
649        let ang = f32(k) * QUARTER_TURN;
650        // The smallest representative of `ang` modulo TAU at or above `lo`.
651        let t = ang + TAU * ceil((lo - ang) / TAU);
652        if (t <= hi) {
653            let d = vec2<f32>(cos(ang), sin(ang));
654            lo_dir = min(lo_dir, d);
655            hi_dir = max(hi_dir, d);
656        }
657    }
658
659    // The stroke reaches `width` in WORLD units on every side (ADR-0160), so
660    // the pad is isotropic in the space this box is built in. The 2 % is slack
661    // for the distance being a first-order expression of a curved level set -
662    // see the fragment.
663    //
664    // The pad has to be at least what the stroke reaches on each axis or the
665    // box clips its own edge, and in world units that is `width` on both -
666    // scaling either axis by the aspect under-pads it on one side of 1.0.
667    let pad = vec2<f32>(width, width) * 1.02;
668    let lo_w = centre_v + radius_v * lo_dir - pad;
669    let hi_w = centre_v + radius_v * hi_dir + pad;
670    let p = mix(lo_w, hi_w, c);
671    let ndc = vec2<f32>(p.x / aspect, p.y);
672
673    var out: VsOut;
674    out.pos = vec4<f32>(ndc, 0.0, 1.0);
675    out.ndc = ndc;
676    out.centre = centre_v;
677    out.radius = radius_v;
678    out.span = vec2<f32>(lo, hi);
679    out.color = color;
680    out.width = width;
681    return out;
682}
683
684@fragment
685fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
686    let aspect = max(u.v.x, 0.1);
687    // Back into the isotropic world space the arc is authored in.
688    let p = vec2<f32>(in.ndc.x * aspect, in.ndc.y);
689    let q = p - in.centre;
690    let len = length(q);
691
692    // Inside the span, or past one of its ends? The same modulo reduction the
693    // vertex shader uses, so an arc that wraps the branch cut of atan2 is not
694    // a special case.
695    let theta = atan2(q.y, q.x);
696    let t = theta + TAU * ceil((in.span.x - theta) / TAU);
697
698    // SIGNED across the stroke, and that is load-bearing: the profile below
699    // takes `fwidth` of this, and `fwidth` of an ABSOLUTE distance is garbage
700    // on the 2x2 quad that straddles the centreline - the kink makes the finite
701    // difference near zero exactly where the stroke is brightest. The segment
702    // fragment reads `side` and not `|side|` for the same reason.
703    var sd: f32;
704    if (t <= in.span.y) {
705        // The exact world radial distance, and the stroke is measured in world
706        // units too (ADR-0160) - so it is used as it stands, with nothing
707        // converted and nothing to keep in step with the segment path.
708        sd = len - in.radius;
709    } else {
710        // Past an end: the world distance to the nearer endpoint. A point has
711        // an exact distance, so this arm approximates nothing.
712        let e0 = in.centre + in.radius * vec2<f32>(cos(in.span.x), sin(in.span.x));
713        let e1 = in.centre + in.radius * vec2<f32>(cos(in.span.y), sin(in.span.y));
714        sd = min(length(p - e0), length(p - e1));
715    }
716
717    // The segment path's profile on the arc's own distance - literally the
718    // same `stroke_coverage`, prepended to both modules (ADR-0124), so the two
719    // fragments cannot draw different strokes on the same figure.
720    //
721    // `sd` is a world distance and `width` is flat-interpolated, so the
722    // normalized coordinate is `sd / width` and its screen derivative is
723    // `fwidth(sd) / width`. One pixel of the render target is `2 / H` world
724    // units on BOTH axes - `2 * aspect / W` is that same number - so the ramp
725    // is one pixel of the target at every orientation and on any frame. Taken
726    // on the signed distance, per its declaration.
727    //
728    // Premultiplied, so colour and alpha carry the same coverage and the quad
729    // outside the stroke writes nothing at all rather than opaque black
730    // (ADR-0056). The glow multiplier scales the LIGHT, not the coverage,
731    // exactly as it does for a segment.
732    //
733    // DIVIDED, not multiplied by a reciprocal: `x / w` and `x * (1 / w)` differ
734    // in the last ulp, and byte-identity at the default is what the golden
735    // corpus rests on.
736    let width = max(in.width, 1e-8);
737    let g = stroke_coverage(max(0.0, 1.0 - abs(sd) / width), fwidth(sd) / width, u.v.z);
738    return vec4<f32>(in.color * g * u.v.y, g);
739}
740"#;
741
742/// The `seg3d` pipeline's full WGSL: [`PROFILE_WGSL`], the shared camera and
743/// [`SEG3D_SHADER`], in that order.
744///
745/// The profile is prepended here for the reason [`arc_shader_source`] gives: a
746/// 3D stroke and a 2D one fall off across their width by one definition.
747///
748/// [`MITER_LIMIT`] is written into it from the one Rust constant, so a 3D
749/// joint and a 2D one fall back to the bevel at the same corner, and
750/// [`NEAR`](crate::render::camera::NEAR) likewise, so a skirt's foot is held to
751/// the plane the CPU clips at.
752///
753/// Runs once per [`LineRenderer::new_3d`] (pipeline build, not the hot path).
754pub(crate) fn seg3d_shader_source() -> String {
755    format!(
756        "{PROFILE_WGSL}
757const MITER_LIMIT: f32 = {MITER_LIMIT:?};
758const NEAR_DEPTH: f32 = {:?};
759{}
760{SEG3D_SHADER}",
761        crate::render::camera::NEAR,
762        crate::render::camera::CAMERA_WGSL
763    )
764}
765
766/// The `seg3d` pipeline's WGSL: each [`Segment3dInstance`] projected through the
767/// shared camera and expanded into a quad **in pixels of the render target**.
768///
769/// The quad is built in pixel space after the divide, where the space is
770/// isotropic, so the stroke has the same thickness on screen at every
771/// orientation; the aspect comes from the camera's viewport, which is the
772/// render target's (ADR-0037). Each end's half-width is the stroke width carried
773/// to that end's depth by perspective, and the quad is a trapezoid between the
774/// two, so a line receding from the camera thins along its length.
775///
776/// **A half-width is floored at half a pixel, and the light is scaled down by
777/// the same ratio**, so a stroke too thin to rasterize fades rather than
778/// breaking into a dotted line, and keeps the light it would have had.
779///
780/// # Depth of field, per endpoint (ADR-0257)
781///
782/// Each end takes the camera's circle of confusion at its own depth, `coc`, and
783/// its half-width grows from `w` to `w + coc`. Its edge softness widens with
784/// it, from the stroke's own `s0` toward `1` by `coc / (w + coc)`, so a blurred
785/// end has no hard edge. Both are interpolated along the quad, so one line is
786/// sharp where it crosses the focal plane and soft at either end.
787///
788/// **The light is divided down so the stroke's integrated cross-section keeps
789/// its energy.** The profile integrates across a half-width `h` to
790/// `h * (1 - 2s/3)` — a solid core of `1 - s` plus a quadratic ramp worth
791/// `s / 3` — so each fragment's light is scaled by
792///
793/// ```text
794/// w * (1 - 2 s0 / 3)  /  ((w + coc) * (1 - 2 s / 3))
795/// ```
796///
797/// from the interpolated sharp half-width `w`, blurred half-width `w + coc` and
798/// softness `s`. It is evaluated per fragment rather than at the two ends and
799/// interpolated: the energy is a product of interpolated quantities, and a
800/// linear blend of the end factors overshoots it everywhere between them.
801///
802/// **A blurred stroke reads its across-the-stroke coordinate exactly.** A
803/// trapezoid is drawn as two triangles, and the corner coordinate `side`
804/// interpolated affinely over each of them bends wherever the two ends'
805/// widths differ — which a blurred end makes them do by a factor of several.
806/// The perpendicular pixel offset is affine over the whole quad, and so is the
807/// half-width along it, so their ratio is the true coordinate at every
808/// fragment.
809///
810/// A sharp stroke skips both the factor and the exact coordinate on a flat
811/// flag, so an aperture of `0` draws the bytes a pinhole camera drew.
812///
813/// # Joins (ADR-0263)
814///
815/// An end whose [`Segment3dInstance::prev`] or [`next`](Segment3dInstance::next)
816/// names a neighbour is mitred in pixel space: its two corners move onto the
817/// line bisecting the turn, where the neighbour's corners also lie, so two
818/// blurred neighbours abut instead of summing their overlap into a ridge. A
819/// corner sharper than [`MITER_LIMIT`] stays flat, as in 2D.
820///
821/// # Fog (ADR-0263)
822///
823/// Each end's light is scaled by `fog_light()` at its own depth, `1 - fog * d`
824/// with `d` its place across the volume, and the factor is interpolated along
825/// the quad. Coverage is untouched, so a fogged stroke darkens rather than
826/// thins.
827///
828/// The position leaves the vertex shader already divided (`w = 1`). The quad is
829/// a screen-space shape, and interpolating its varyings perspective-correctly
830/// against the endpoints' depths would bend the across-the-stroke coordinate.
831const SEG3D_SHADER: &str = r#"
832struct Stroke {
833    // x: glow multiplier, y: softness (ADR-0124), z: fog (ADR-0263), w: the
834    // world y of the ground plane a skirt reaches down to. The vertex stage
835    // reads the softness too, to widen it toward a blurred end.
836    v: vec4<f32>,
837}
838
839@group(0) @binding(0) var<uniform> cam: Camera;
840@group(0) @binding(1) var<uniform> stroke: Stroke;
841
842struct Seg3dOut {
843    @builtin(position) pos: vec4<f32>,
844    @location(0) side: f32,
845    @location(1) color: vec3<f32>,
846    @location(2) alpha: f32,
847    // The edge softness, widened by the circle of confusion toward each end.
848    @location(3) soft: f32,
849    // The half-width the stroke would have with no blur, and the blurred one,
850    // in pixels - the two sides of the energy ratio.
851    @location(5) sharp_hw: f32,
852    @location(6) wide_hw: f32,
853    // The fragment's perpendicular offset from the centreline, in pixels.
854    @location(7) offset: f32,
855    // 1 when either end is blurred, 0 otherwise. Flat, so a sharp stroke reads
856    // the uniform softness itself rather than an interpolation of copies of it.
857    @location(4) @interpolate(flat) blurred: f32,
858    // 1 on a skirt, which fills at full coverage rather than stroking.
859    @location(8) @interpolate(flat) skirt: f32,
860}
861
862// The profile's integral across a unit half-width at softness `s`.
863fn profile_mass(s: f32) -> f32 {
864    return 1.0 - 2.0 * clamp(s, 0.0, 1.0) / 3.0;
865}
866
867// Half a pixel: the narrowest half-width a stroke is rasterized at.
868const MIN_HALF_PX: f32 = 0.5;
869
870// The mitred corner offset at `v`, where the polyline arrives from `p0` and
871// leaves for `p1`, for a half-width `hw` - all in pixels. `xy` is the offset of
872// the corner on the left side (`side = +1`), and `z` is 1 when the end is
873// joined, 0 when it is free: a neighbour that coincides with `v` on screen,
874// or a corner sharper than MITER_LIMIT, which draws flat as the bevel
875// fallback does in 2D (ADR-0158).
876//
877// Both quads that share `v` call this with the same three points, in the same
878// roles, so they compute the same corner bit for bit and abut on the mitre
879// line with no pixel covered twice and none missed.
880//
881// The left normal of each arm, summed, is the mitre direction; its corner
882// lies `hw / cos(turn / 2)` along it, and `cos(turn / 2)` is
883// `sqrt((1 + d_in . d_out) / 2)` - the expression `miter_extension_between`
884// takes, with no trigonometry.
885fn mitre_offset(p0: vec2<f32>, v: vec2<f32>, p1: vec2<f32>, hw: f32) -> vec3<f32> {
886    let arm_in = v - p0;
887    let arm_out = p1 - v;
888    let len_in = length(arm_in);
889    let len_out = length(arm_out);
890    if (len_in <= 1e-6 || len_out <= 1e-6) {
891        return vec3<f32>(0.0, 0.0, 0.0);
892    }
893    let d_in = arm_in / len_in;
894    let d_out = arm_out / len_out;
895    let cos_half = sqrt(max((1.0 + dot(d_in, d_out)) * 0.5, 0.0));
896    if (cos_half <= 1.0 / MITER_LIMIT) {
897        return vec3<f32>(0.0, 0.0, 0.0);
898    }
899    let bisector = vec2<f32>(-d_in.y, d_in.x) + vec2<f32>(-d_out.y, d_out.x);
900    let m = bisector / length(bisector);
901    return vec3<f32>(m * (hw / cos_half), 1.0);
902}
903
904@vertex
905fn vs_main(
906    @builtin(vertex_index) vi: u32,
907    @location(0) a: vec3<f32>,
908    @location(1) b: vec3<f32>,
909    @location(2) color: vec3<f32>,
910    @location(3) width: f32,
911    @location(4) alpha: f32,
912    @location(5) prev: vec3<f32>,
913    @location(6) next: vec3<f32>,
914    @location(7) skirt: f32,
915) -> Seg3dOut {
916    // (along, side): along runs a->b, side spans -1..1 across the width.
917    var corners = array<vec2<f32>, 6>(
918        vec2<f32>(0.0, -1.0), vec2<f32>(1.0, -1.0), vec2<f32>(0.0, 1.0),
919        vec2<f32>(0.0, 1.0), vec2<f32>(1.0, -1.0), vec2<f32>(1.0, 1.0),
920    );
921    let c = corners[vi];
922
923    let ca = project(cam, a);
924    let cb = project(cam, b);
925    let sa = clip_to_px(cam, ca);
926    let sb = clip_to_px(cam, cb);
927
928    let hw_a = at_depth(cam, 0.5 * width, ca.w);
929    let hw_b = at_depth(cam, 0.5 * width, cb.w);
930    let coc_a = coc(cam, ca.w);
931    let coc_b = coc(cam, cb.w);
932    let s0 = stroke.v.y;
933
934    // The blurred half-width at each end, and `c.x` is exactly 0 or 1 at every
935    // corner, so `mix` picks one end exactly.
936    let hw_true = mix(hw_a + coc_a, hw_b + coc_b, c.x);
937    let hw = max(hw_true, MIN_HALF_PX);
938
939    // Each end's softness, widened toward 1 by its share of blur. Guarded
940    // against a zero width, where there is no stroke to soften.
941    let wide_a = hw_a + coc_a;
942    let wide_b = hw_b + coc_b;
943    let soft_a = mix(s0, 1.0, select(0.0, coc_a / wide_a, wide_a > 0.0));
944    let soft_b = mix(s0, 1.0, select(0.0, coc_b / wide_b, wide_b > 0.0));
945
946    var dir = sb - sa;
947    let len = length(dir);
948    if (len > 1e-6) {
949        dir = dir / len;
950    } else {
951        dir = vec2<f32>(1.0, 0.0);
952    }
953    let nrm = vec2<f32>(-dir.y, dir.x);
954    var p = mix(sa, sb, c.x) + nrm * c.y * hw;
955
956    // The join (ADR-0263). This corner's end, its neighbour on the far side
957    // and the half-width there are picked with `select` rather than `mix`:
958    // `mix(x, y, 1.0)` need not be `y` to the bit, and the neighbouring quad
959    // reads the same end as its `a`, where it is. A free end keeps the flat
960    // corner above untouched, so a stroke with no neighbours draws the bytes
961    // it drew before joins existed.
962    let at_b = c.x > 0.5;
963    let joint = select(sa, sb, at_b);
964    let before = select(clip_to_px(cam, project(cam, prev)), sa, at_b);
965    let after = select(sb, clip_to_px(cam, project(cam, next)), at_b);
966    let hw_joint = max(select(wide_a, wide_b, at_b), MIN_HALF_PX);
967    let mitre = mitre_offset(before, joint, after, hw_joint);
968    let joined = mitre.z > 0.5;
969    if (joined) {
970        p = joint + mitre.xy * c.y;
971    }
972
973    // Fog (ADR-0263), per end and interpolated along the quad. It scales the
974    // light and never the coverage, and at fog 0 it is exactly 1.
975    let fogged = fog_light(cam, stroke.v.z, select(ca.w, cb.w, at_b));
976
977    // A skirt (ADR-0263) is the band under this end: its `side = +1` corners
978    // on the segment, its `side = -1` corners straight below on the ground
979    // plane, each projected on its own. No width, no join and no blur reach
980    // it; the fragment fills it.
981    let is_skirt = skirt > 0.5;
982    if (is_skirt) {
983        let top = select(a, b, at_b);
984        let foot = vec3<f32>(top.x, stroke.v.w, top.z);
985        let corner = project(cam, select(foot, top, c.y > 0.0));
986        p = clip_to_px(cam, vec4<f32>(corner.xyz, max(corner.w, NEAR_DEPTH)));
987    }
988
989    var out: Seg3dOut;
990    out.pos = vec4<f32>(px_to_ndc(cam, p), 0.0, 1.0);
991    out.skirt = select(0.0, 1.0, is_skirt);
992    out.side = c.y;
993    out.color = color * (hw_true / hw) * fogged;
994    out.alpha = alpha;
995    out.soft = mix(soft_a, soft_b, c.x);
996    out.sharp_hw = mix(hw_a, hw_b, c.x);
997    out.wide_hw = hw_true;
998    // A mitred corner lies `hw_joint` from the centreline across the stroke,
999    // wherever the mitre has slid it along, so the offset stays the true
1000    // across-the-stroke distance.
1001    out.offset = c.y * select(hw, hw_joint, joined);
1002    out.blurred = select(0.0, 1.0, max(coc_a, coc_b) > 0.0);
1003    return out;
1004}
1005
1006@fragment
1007fn fs_main(in: Seg3dOut) -> @location(0) vec4<f32> {
1008    // The shared profile, read off `side` rather than `|side|` for the reason
1009    // the 2D fragment gives.
1010    let blurred = in.blurred > 0.5;
1011    let side = select(in.side, in.offset / max(in.wide_hw, 1e-6), blurred);
1012    let inward = max(0.0, 1.0 - abs(side));
1013    let soft = select(stroke.v.y, in.soft, blurred);
1014    let g = stroke_coverage(inward, fwidth(side), soft) * in.alpha;
1015    // The energy ratio, exactly 1 for a sharp stroke.
1016    let spread = in.wide_hw * profile_mass(soft);
1017    let keep = select(
1018        1.0,
1019        (in.sharp_hw * profile_mass(stroke.v.y)) / spread,
1020        blurred && spread > 0.0,
1021    );
1022    // Premultiplied (ADR-0056): the glow scales the light, not the coverage.
1023    // A skirt covers its whole band at its alpha; it is chosen after the
1024    // stroke is computed rather than branched to, because `fwidth` above must
1025    // stay in uniform control flow.
1026    let lit = vec4<f32>(in.color * g * stroke.v.x * keep, g);
1027    let fill = vec4<f32>(in.color * stroke.v.x * in.alpha, in.alpha);
1028    return select(lit, fill, in.skirt > 0.5);
1029}
1030"#;
1031
1032/// The `seg3d` stroke uniform: `[glow, softness, fog, ground y]`.
1033#[repr(C)]
1034#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
1035struct Stroke3dUniform {
1036    v: [f32; 4],
1037}
1038
1039/// The `seg3d` pipeline and everything it binds — built only by
1040/// [`LineRenderer::new_3d`].
1041struct Seg3d {
1042    pipeline: wgpu::RenderPipeline,
1043    /// The same pipeline composited **over** rather than added, for a solid
1044    /// frame (ADR-0263). Built with the additive one rather than on the first
1045    /// solid frame: a resource created mid-run moves what later passes
1046    /// resolve to on the software adapter the goldens capture on (measured on
1047    /// WARP, unverified on lavapipe as of 2026-10-06).
1048    over_pipeline: wgpu::RenderPipeline,
1049    instances: wgpu::Buffer,
1050    camera: wgpu::Buffer,
1051    stroke: wgpu::Buffer,
1052    bind_group: wgpu::BindGroup,
1053    capacity: usize,
1054    /// A solid frame's sort keys, `(depth, index)`, preallocated to
1055    /// `capacity` so the sort allocates nothing.
1056    keys: Vec<(f32, u32)>,
1057    /// A solid frame's segments in draw order, far to near, preallocated to
1058    /// `capacity`.
1059    sorted: Vec<Segment3dInstance>,
1060}
1061
1062/// `segments` in **far-to-near** order into `out`, by the camera depth of each
1063/// segment's midpoint under `view` — the painter's order a solid frame draws
1064/// in (ADR-0263), so a nearer stroke composites over a farther one.
1065///
1066/// **The order is a function of the set, not of the order it arrived in.**
1067/// Two segments at one depth are ordered by their own bytes, which is a total
1068/// order on the instance, so a system that emits the same segments in another
1069/// order draws the same frame.
1070///
1071/// **Allocation-free** while `segments` fits the capacity `keys` and `out`
1072/// were reserved to: both are cleared and refilled, and `sort_unstable_by`
1073/// sorts in place.
1074pub(crate) fn sort_far_to_near(
1075    view: &crate::render::camera::CameraView,
1076    segments: &[Segment3dInstance],
1077    keys: &mut Vec<(f32, u32)>,
1078    out: &mut Vec<Segment3dInstance>,
1079) {
1080    sort_painter(view, segments, None, keys, out);
1081}
1082
1083/// [`sort_far_to_near`], with the depth each segment sorts by taken at its
1084/// midpoint's **foot** on the plane `y = ground` when one is named.
1085///
1086/// A heightfield's rows are what occlude one another, not its peaks: keyed at
1087/// the midpoint itself, a tall peak several rows back sorts nearer than a low
1088/// row in front of it, because raising a point toward a camera that looks down
1089/// shortens its depth. Keyed at the foot, every segment of one row sorts by
1090/// where the row stands.
1091///
1092/// At one key, a skirt sorts **before** the stroke it lies under, so the
1093/// stroke is drawn over its own band; past that the instance bytes break the
1094/// tie.
1095pub(crate) fn sort_painter(
1096    view: &crate::render::camera::CameraView,
1097    segments: &[Segment3dInstance],
1098    ground: Option<f32>,
1099    keys: &mut Vec<(f32, u32)>,
1100    out: &mut Vec<Segment3dInstance>,
1101) {
1102    keys.clear();
1103    out.clear();
1104    for (i, s) in segments.iter().enumerate() {
1105        let mid = [
1106            0.5 * (s.a[0] + s.b[0]),
1107            ground.unwrap_or(0.5 * (s.a[1] + s.b[1])),
1108            0.5 * (s.a[2] + s.b[2]),
1109        ];
1110        keys.push((view.depth(mid), i as u32));
1111    }
1112    let skirt = |i: u32| segments.get(i as usize).map_or(0.0, |s| s.skirt);
1113    let content = |i: u32| segments.get(i as usize).map(bytemuck::bytes_of);
1114    keys.sort_unstable_by(|x, y| {
1115        y.0.total_cmp(&x.0)
1116            .then_with(|| skirt(y.1).total_cmp(&skirt(x.1)))
1117            .then_with(|| content(x.1).cmp(&content(y.1)))
1118    });
1119    out.extend(
1120        keys.iter()
1121            .filter_map(|&(_, i)| segments.get(i as usize).copied()),
1122    );
1123}
1124
1125impl Seg3d {
1126    fn new(
1127        device: &wgpu::Device,
1128        surface_format: wgpu::TextureFormat,
1129        capacity: usize,
1130        label: &str,
1131    ) -> Self {
1132        use crate::render::camera::CameraUniform;
1133
1134        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1135            label: Some(&format!("{label}-seg3d-shader")),
1136            source: wgpu::ShaderSource::Wgsl(seg3d_shader_source().into()),
1137        });
1138        let instances = device.create_buffer(&wgpu::BufferDescriptor {
1139            label: Some(&format!("{label}-seg3d-instances")),
1140            size: (capacity * std::mem::size_of::<Segment3dInstance>()) as u64,
1141            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1142            mapped_at_creation: false,
1143        });
1144        let camera = gpu::uniform_buffer(
1145            device,
1146            &format!("{label}-seg3d-camera"),
1147            std::mem::size_of::<CameraUniform>(),
1148        );
1149        let stroke = gpu::uniform_buffer(
1150            device,
1151            &format!("{label}-seg3d-stroke"),
1152            std::mem::size_of::<Stroke3dUniform>(),
1153        );
1154        // A shape of its own — two sized uniforms, the camera for the vertex
1155        // stage and the stroke for both — so no other live layout matches it
1156        // (ADR-0058).
1157        let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1158            label: Some(&format!("{label}-seg3d-bind-layout")),
1159            entries: &[
1160                wgpu::BindGroupLayoutEntry {
1161                    binding: 0,
1162                    visibility: wgpu::ShaderStages::VERTEX,
1163                    ty: wgpu::BindingType::Buffer {
1164                        ty: wgpu::BufferBindingType::Uniform,
1165                        has_dynamic_offset: false,
1166                        min_binding_size: wgpu::BufferSize::new(
1167                            std::mem::size_of::<CameraUniform>() as u64,
1168                        ),
1169                    },
1170                    count: None,
1171                },
1172                wgpu::BindGroupLayoutEntry {
1173                    binding: 1,
1174                    visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
1175                    ty: wgpu::BindingType::Buffer {
1176                        ty: wgpu::BufferBindingType::Uniform,
1177                        has_dynamic_offset: false,
1178                        min_binding_size: wgpu::BufferSize::new(
1179                            std::mem::size_of::<Stroke3dUniform>() as u64,
1180                        ),
1181                    },
1182                    count: None,
1183                },
1184            ],
1185        });
1186        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1187            label: Some(&format!("{label}-seg3d-bind-group")),
1188            layout: &bind_layout,
1189            entries: &[
1190                wgpu::BindGroupEntry {
1191                    binding: 0,
1192                    resource: camera.as_entire_binding(),
1193                },
1194                wgpu::BindGroupEntry {
1195                    binding: 1,
1196                    resource: stroke.as_entire_binding(),
1197                },
1198            ],
1199        });
1200        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1201            label: Some(&format!("{label}-seg3d-pipeline-layout")),
1202            bind_group_layouts: &[Some(&bind_layout)],
1203            immediate_size: 0,
1204        });
1205        // The two pipelines differ in the blend state and nothing else, so they
1206        // are built from one closure, as the 2D pair is.
1207        let make = |blend: wgpu::BlendState, suffix: &str| {
1208            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1209                label: Some(&format!("{label}-seg3d-pipeline{suffix}")),
1210                layout: Some(&pipeline_layout),
1211                vertex: wgpu::VertexState {
1212                    module: &shader,
1213                    entry_point: Some("vs_main"),
1214                    compilation_options: Default::default(),
1215                    buffers: &[Some(wgpu::VertexBufferLayout {
1216                        array_stride: std::mem::size_of::<Segment3dInstance>() as u64,
1217                        step_mode: wgpu::VertexStepMode::Instance,
1218                        attributes: &wgpu::vertex_attr_array![
1219                            0 => Float32x3,
1220                            1 => Float32x3,
1221                            2 => Float32x3,
1222                            3 => Float32,
1223                            4 => Float32,
1224                            5 => Float32x3,
1225                            6 => Float32x3,
1226                            7 => Float32,
1227                        ],
1228                    })],
1229                },
1230                fragment: Some(wgpu::FragmentState {
1231                    module: &shader,
1232                    entry_point: Some("fs_main"),
1233                    compilation_options: Default::default(),
1234                    targets: &[Some(wgpu::ColorTargetState {
1235                        format: surface_format,
1236                        blend: Some(blend),
1237                        write_mask: wgpu::ColorWrites::ALL,
1238                    })],
1239                }),
1240                primitive: wgpu::PrimitiveState::default(),
1241                depth_stencil: None,
1242                multisample: wgpu::MultisampleState::default(),
1243                multiview_mask: None,
1244                cache: None,
1245            })
1246        };
1247        // The seam every additive line draws through (ADR-0056), then the
1248        // premultiplied OVER a solid frame takes. The fragment is premultiplied
1249        // either way, which is why one shader serves both.
1250        let pipeline = make(gpu::ADDITIVE_LIGHT_SATURATING_COVERAGE, "");
1251        let over_pipeline = make(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING, "-over");
1252        Self {
1253            pipeline,
1254            over_pipeline,
1255            instances,
1256            camera,
1257            stroke,
1258            bind_group,
1259            capacity,
1260            keys: Vec::with_capacity(capacity),
1261            sorted: Vec::with_capacity(capacity),
1262        }
1263    }
1264}
1265
1266/// The share of the segment `a -> b` lying inside `[-aspect, aspect] x [-1, 1]`,
1267/// as a fraction of its own length: Liang-Barsky against the four edges.
1268///
1269/// **Exactly `1.0`** when the segment is untouched by any edge — the two
1270/// parameters start at `0.0` and `1.0` and no edge moves them — which is what
1271/// lets an unclipped figure sum to exactly its own total. **Exactly `0.0`** when
1272/// the segment is wholly outside.
1273///
1274/// A parametric clip rather than an endpoint test on purpose: the case a naive
1275/// "are both ends outside" check gets wrong is a segment whose ends are both out
1276/// but which crosses the frame between them, and that is precisely what a badly
1277/// over-scaled figure is made of.
1278fn in_frame_fraction(a: [f32; 2], b: [f32; 2], aspect: f32) -> f32 {
1279    let [ax, ay] = a;
1280    let [bx, by] = b;
1281    let (dx, dy) = (bx - ax, by - ay);
1282    let (mut t0, mut t1) = (0.0f32, 1.0f32);
1283    // (direction, distance) per edge: left, right, bottom, top.
1284    for (p, q) in [
1285        (-dx, ax + aspect),
1286        (dx, aspect - ax),
1287        (-dy, ay + 1.0),
1288        (dy, 1.0 - ay),
1289    ] {
1290        if p == 0.0 {
1291            // Parallel to this edge: wholly out if it starts outside it.
1292            if q < 0.0 {
1293                return 0.0;
1294            }
1295            continue;
1296        }
1297        let r = q / p;
1298        if p < 0.0 {
1299            if r > t1 {
1300                return 0.0;
1301            }
1302            if r > t0 {
1303                t0 = r;
1304            }
1305        } else {
1306            if r < t0 {
1307                return 0.0;
1308            }
1309            if r < t1 {
1310                t1 = r;
1311            }
1312        }
1313    }
1314    t1 - t0
1315}
1316
1317/// Sub-arcs one [`ArcInstance`] is measured in — a **power of two**, which is
1318/// load-bearing.
1319///
1320/// Each sub-arc is clipped by its own chord and weighted `1 / ARC_STEPS`, so an
1321/// arc wholly inside the frame accumulates that weight exactly `ARC_STEPS`
1322/// times. At a power of two the weight is exact in binary and the sum is
1323/// **exactly 1.0**, which is what keeps an unclipped figure measuring exactly
1324/// its own length — the property `in_frame_fraction` is written to preserve for
1325/// segments.
1326///
1327/// Sixty-four puts 5.6 degrees in a sub-arc of a full circle, whose chord
1328/// departs from it by `r * (1 - cos(2.8 deg))`, about `0.0012 * r`. The error is
1329/// a function of the angle per step alone, so it does not grow with radius.
1330const ARC_STEPS: usize = 64;
1331
1332/// The world-space length of `arc` and the share of it inside
1333/// `[-aspect, aspect] x [-1, 1]`, under the same view transform the vertex
1334/// shader applies.
1335///
1336/// Measured as [`ARC_STEPS`] sub-arcs clipped by their own chords rather than by
1337/// solving the circle against the four edges: the closed form needs the
1338/// intersection of four half-planes with a circle, which is up to four disjoint
1339/// angular components and considerably more code than the property is worth.
1340/// Both sums are taken from the arc's **own** length (`|sweep| * radius`), never
1341/// from the chords', so the sub-chord sampling changes only *where* the arc is
1342/// judged to be, never how long it is.
1343fn measure_arc(arc: &ArcInstance, aspect: f32, xform: super::ViewTransform) -> (f32, f32) {
1344    let [pan_x, pan_y] = xform.pan;
1345    let centre = [
1346        arc.centre[0] * xform.zoom + pan_x,
1347        arc.centre[1] * xform.zoom + pan_y,
1348    ];
1349    let radius = arc.radius * xform.zoom;
1350    let len = (radius * arc.angle_sweep).abs();
1351    if len <= 0.0 || !len.is_finite() {
1352        return (0.0, 0.0); // a degenerate (or non-finite) arc measures nothing
1353    }
1354    let step = 1.0 / ARC_STEPS as f32;
1355    let at = |k: usize| {
1356        let t = arc.angle_start + arc.angle_sweep * k as f32 * step;
1357        [centre[0] + radius * t.cos(), centre[1] + radius * t.sin()]
1358    };
1359    let mut inside = 0.0;
1360    for k in 0..ARC_STEPS {
1361        inside += in_frame_fraction(at(k), at(k + 1), aspect) * step;
1362    }
1363    (len, len * inside)
1364}
1365
1366/// Measure `segments` against the frame — the diagnostic's whole computation.
1367///
1368/// **The aspect is a parameter, and it is the only source of one in here**
1369/// (ADR-0037): this is a free function over the endpoints, so there is no
1370/// internal grid, no texture and no `self` for a second aspect to come from.
1371/// Its caller hands it the value `draw` was handed, which is the **render
1372/// target's**.
1373///
1374/// The view transform is applied first, exactly as the vertex shader applies it
1375/// (`a * zoom + pan`, before the aspect divide), because a figure pushed off the
1376/// frame by `zoom` or `pan_y` has overshot just as surely as one scaled off it.
1377fn measure_extent(
1378    segments: &[SegmentInstance],
1379    arcs: &[ArcInstance],
1380    aspect: f32,
1381    xform: super::ViewTransform,
1382) -> DrawExtent {
1383    let mut extent = DrawExtent::default();
1384    let [pan_x, pan_y] = xform.pan;
1385    for segment in segments {
1386        let [ax, ay] = segment.a;
1387        let [bx, by] = segment.b;
1388        let a = [ax * xform.zoom + pan_x, ay * xform.zoom + pan_y];
1389        let b = [bx * xform.zoom + pan_x, by * xform.zoom + pan_y];
1390        let ([ax, ay], [bx, by]) = (a, b);
1391        let (dx, dy) = (bx - ax, by - ay);
1392        let len = (dx * dx + dy * dy).sqrt();
1393        if len <= 0.0 || !len.is_finite() {
1394            continue; // a degenerate (or non-finite) segment measures nothing
1395        }
1396        extent.total_len += len;
1397        // `len * 1.0` is `len` exactly, so an unclipped figure adds the same
1398        // value to both sums and the fraction is exactly 1.0.
1399        extent.in_frame_len += len * in_frame_fraction(a, b, aspect);
1400    }
1401    // Arcs, into the same two sums: the fraction is over everything drawn, and
1402    // a batch of both kinds has one denominator.
1403    for arc in arcs {
1404        let (len, in_frame) = measure_arc(arc, aspect, xform);
1405        extent.total_len += len;
1406        extent.in_frame_len += in_frame;
1407    }
1408    extent
1409}
1410
1411/// Draws segment buffers as thick glowing quads. Owns its pipeline, a
1412/// fixed-capacity instance buffer, and the aspect/glow uniform.
1413pub struct LineRenderer {
1414    pipeline: wgpu::RenderPipeline,
1415    /// The same pipeline with the light composited **over** rather than added —
1416    /// see [`LineRenderer::draw_split`]. It shares the shader, the layout and the
1417    /// bind group with [`pipeline`](Self::pipeline), so ADR-0058 has nothing to
1418    /// separate: there is one layout, not two that happen to match.
1419    ///
1420    /// **`None` unless the scene asked for it** ([`LineRenderer::new_split`]),
1421    /// and that is not a micro-optimization. Building a pipeline the scene never
1422    /// binds still allocates on the device, and on the WARP software adapter a
1423    /// changed allocation order changes what a later pass resolves to — the
1424    /// hazard `core/tests/suite/composite.rs`'s header records, on a software
1425    /// adapter like the one the golden suite captures on. Building this for the
1426    /// nine line scenes that do not use it moved five composite baselines while
1427    /// changing nothing a driver would render differently. (WARP readings,
1428    /// unverified on lavapipe as of 2026-10-06.)
1429    over_pipeline: Option<wgpu::RenderPipeline>,
1430    /// The [`ArcInstance`] pipeline, drawn in the same additive pass from its
1431    /// own buffer — [`ARC_SHADER`] and ADR-0098.
1432    ///
1433    /// **`None` unless the scene asked for it**
1434    /// ([`LineRenderer::new_with_arcs`]), for the reason
1435    /// [`over_pipeline`](Self::over_pipeline) records and with the same
1436    /// evidence behind it: building a pipeline nobody binds still allocates on
1437    /// the device, and on WARP a changed allocation order changes what a later
1438    /// pass resolves to (unverified on lavapipe as of 2026-10-06). It **shares
1439    /// the bind-group layout, the bind group and the pipeline layout** with the
1440    /// segment pipelines — one uniform, one layout, so ADR-0058 has nothing new
1441    /// to separate. Only the vertex layout and the shader module differ.
1442    arc_pipeline: Option<wgpu::RenderPipeline>,
1443    /// [`arc_pipeline`](Self::arc_pipeline) with the light composited **over**
1444    /// rather than added — the arc half of the opacity-preserving seam
1445    /// (ADR-0138), built exactly when [`over_pipeline`](Self::over_pipeline) and
1446    /// the arc pipeline both are.
1447    ///
1448    /// Without it, [`draw_opaque`](Self::draw_opaque) on a scene whose motifs are
1449    /// arcs would lay opaque strokes and additive circles into one picture, and
1450    /// the limited-ink guarantee would hold for half of what the scene drew.
1451    arc_over_pipeline: Option<wgpu::RenderPipeline>,
1452    instances: wgpu::Buffer,
1453    /// The arc instance buffer, `Some` exactly when
1454    /// [`arc_pipeline`](Self::arc_pipeline) is.
1455    arcs: Option<wgpu::Buffer>,
1456    uniforms: wgpu::Buffer,
1457    bind_group: wgpu::BindGroup,
1458    /// Maximum segments the instance buffer holds; extra are dropped by `draw`.
1459    capacity: usize,
1460    /// Maximum arcs the arc buffer holds; `0` when there is no arc pipeline.
1461    arc_capacity: usize,
1462    /// The 3D segment pipeline (ADR-0257), **`None` unless the scene asked for
1463    /// it** ([`LineRenderer::new_3d`]) — for the reason
1464    /// [`over_pipeline`](Self::over_pipeline) records: a pipeline nobody binds
1465    /// still allocates on the device, and on WARP a changed allocation order
1466    /// changes what a later pass resolves to (unverified on lavapipe as of
1467    /// 2026-10-06).
1468    seg3d: Option<Seg3d>,
1469}
1470
1471impl LineRenderer {
1472    /// Build the pipeline and a `capacity`-segment instance buffer on `device`.
1473    /// `label` names this instance's GPU resources; it must be **unique per
1474    /// LineRenderer** — two line scenes coexist (parametric + generator), and
1475    /// distinct labels keep their pipelines/buffers unambiguous in tooling and
1476    /// captures.
1477    pub fn new(
1478        device: &wgpu::Device,
1479        surface_format: wgpu::TextureFormat,
1480        capacity: usize,
1481        label: &str,
1482    ) -> Self {
1483        Self::build(device, surface_format, capacity, label, false, 0)
1484    }
1485
1486    /// [`new`](Self::new), plus the second pipeline
1487    /// [`draw_split`](Self::draw_split) needs. Only a scene that actually splits
1488    /// its batch by blend mode should call this — see
1489    /// `over_pipeline` for why building it unconditionally
1490    /// is not free.
1491    pub fn new_split(
1492        device: &wgpu::Device,
1493        surface_format: wgpu::TextureFormat,
1494        capacity: usize,
1495        label: &str,
1496    ) -> Self {
1497        Self::build(device, surface_format, capacity, label, true, 0)
1498    }
1499
1500    /// [`new_with_arcs`](Self::new_with_arcs), plus the OVER pipelines
1501    /// [`draw_opaque`](Self::draw_opaque) needs — the constructor the shared line
1502    /// renderer takes, because any of the four line systems may ask for the
1503    /// opacity-preserving seam (ADR-0138).
1504    ///
1505    /// The pipelines are built here rather than on the first preset that asks,
1506    /// deliberately: building a GPU resource mid-run changes what a later pass
1507    /// resolves to on the DX12 software adapter, which would make the seam's
1508    /// arrival visible in scenes that never selected it. (A WARP reading,
1509    /// unverified on lavapipe as of 2026-10-06.)
1510    pub fn new_split_with_arcs(
1511        device: &wgpu::Device,
1512        surface_format: wgpu::TextureFormat,
1513        capacity: usize,
1514        arc_capacity: usize,
1515        label: &str,
1516    ) -> Self {
1517        Self::build(device, surface_format, capacity, label, true, arc_capacity)
1518    }
1519
1520    /// [`new`](Self::new), plus the arc pipeline and an `arc_capacity`-instance
1521    /// arc buffer ([`ArcInstance`], ADR-0098).
1522    ///
1523    /// Only a scene that actually draws arcs should call this — see
1524    /// `arc_pipeline` for why building it unconditionally
1525    /// is not free. `arc_capacity` is its own budget rather than a share of
1526    /// `capacity`: an arc replaces many segments, so the two counts are not the
1527    /// same order and sizing one from the other would waste most of it.
1528    pub fn new_with_arcs(
1529        device: &wgpu::Device,
1530        surface_format: wgpu::TextureFormat,
1531        capacity: usize,
1532        arc_capacity: usize,
1533        label: &str,
1534    ) -> Self {
1535        Self::build(device, surface_format, capacity, label, false, arc_capacity)
1536    }
1537
1538    /// A renderer for **3D segments only** (ADR-0257): the `seg3d` pipeline and
1539    /// a `capacity`-instance buffer for it, drawn with `draw_3d`.
1540    ///
1541    /// Its 2D half is built empty — no segment capacity, no arcs, no OVER
1542    /// pipelines — so a 3D scene owns its renderer outright and the roster's
1543    /// shared 2D renderer creates exactly the resources it did before 3D existed.
1544    pub fn new_3d(
1545        device: &wgpu::Device,
1546        surface_format: wgpu::TextureFormat,
1547        capacity: usize,
1548        label: &str,
1549    ) -> Self {
1550        let mut renderer = Self::build(device, surface_format, 0, label, false, 0);
1551        renderer.seg3d = Some(Seg3d::new(device, surface_format, capacity, label));
1552        renderer
1553    }
1554
1555    fn build(
1556        device: &wgpu::Device,
1557        surface_format: wgpu::TextureFormat,
1558        capacity: usize,
1559        label: &str,
1560        split: bool,
1561        arc_capacity: usize,
1562    ) -> Self {
1563        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1564            label: Some(&format!("{label}-shader")),
1565            source: wgpu::ShaderSource::Wgsl(shader_source().into()),
1566        });
1567        let instances = device.create_buffer(&wgpu::BufferDescriptor {
1568            label: Some(&format!("{label}-instances")),
1569            size: (capacity * std::mem::size_of::<SegmentInstance>()) as u64,
1570            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1571            mapped_at_creation: false,
1572        });
1573        let uniforms = gpu::uniform_buffer(
1574            device,
1575            &format!("{label}-uniforms"),
1576            std::mem::size_of::<Uniforms>(),
1577        );
1578        let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1579            label: Some(&format!("{label}-bind-layout")),
1580            entries: &[wgpu::BindGroupLayoutEntry {
1581                binding: 0,
1582                visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
1583                ty: wgpu::BindingType::Buffer {
1584                    ty: wgpu::BufferBindingType::Uniform,
1585                    has_dynamic_offset: false,
1586                    min_binding_size: None,
1587                },
1588                count: None,
1589            }],
1590        });
1591        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
1592            label: Some(&format!("{label}-bind-group")),
1593            layout: &bind_layout,
1594            entries: &[wgpu::BindGroupEntry {
1595                binding: 0,
1596                resource: uniforms.as_entire_binding(),
1597            }],
1598        });
1599        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1600            label: Some(&format!("{label}-pipeline-layout")),
1601            bind_group_layouts: &[Some(&bind_layout)],
1602            immediate_size: 0,
1603        });
1604        // The two pipelines differ in exactly one field — the blend state — so
1605        // they are built from one closure. Anything else that diverges between
1606        // them would be a bug that renders as a difference between two ranges of
1607        // the same batch, which is close to unreadable in a capture.
1608        let make = |blend: wgpu::BlendState, suffix: &str| {
1609            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1610                label: Some(&format!("{label}-pipeline{suffix}")),
1611                layout: Some(&pipeline_layout),
1612                vertex: wgpu::VertexState {
1613                    module: &shader,
1614                    entry_point: Some("vs_main"),
1615                    compilation_options: Default::default(),
1616                    buffers: &[Some(wgpu::VertexBufferLayout {
1617                        array_stride: std::mem::size_of::<SegmentInstance>() as u64,
1618                        step_mode: wgpu::VertexStepMode::Instance,
1619                        attributes: &wgpu::vertex_attr_array![
1620                            0 => Float32x2,
1621                            1 => Float32x2,
1622                            2 => Float32x3,
1623                            3 => Float32,
1624                            4 => Float32,
1625                            5 => Float32,
1626                            6 => Float32,
1627                        ],
1628                    })],
1629                },
1630                fragment: Some(wgpu::FragmentState {
1631                    module: &shader,
1632                    entry_point: Some("fs_main"),
1633                    compilation_options: Default::default(),
1634                    targets: &[Some(wgpu::ColorTargetState {
1635                        format: surface_format,
1636                        blend: Some(blend),
1637                        write_mask: wgpu::ColorWrites::ALL,
1638                    })],
1639                }),
1640                primitive: wgpu::PrimitiveState::default(),
1641                depth_stencil: None,
1642                multisample: wgpu::MultisampleState::default(),
1643                multiview_mask: None,
1644                cache: None,
1645            })
1646        };
1647        // Additive light, saturating coverage (ADR-0056) — the same constant the
1648        // swarm's sprite pipeline takes, so the two draw seams cannot drift
1649        // apart. This is what every line scene draws through.
1650        let pipeline = make(crate::render::gpu::ADDITIVE_LIGHT_SATURATING_COVERAGE, "");
1651        // Premultiplied OVER, for a producer whose source blend *replaces* rather
1652        // than accumulates — see `draw_split`. The fragment is premultiplied
1653        // either way, which is why one shader serves both.
1654        let over_pipeline =
1655            split.then(|| make(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING, "-over"));
1656
1657        // The arc pipeline (ADR-0098). Its own shader module and vertex layout,
1658        // the *same* bind layout, bind group and pipeline layout as above, and
1659        // the same additive blend — so an arc and a segment emit into one pass
1660        // through one uniform and cannot drift apart on aspect, glow or the
1661        // view transform.
1662        let arcs = (arc_capacity > 0).then(|| {
1663            device.create_buffer(&wgpu::BufferDescriptor {
1664                label: Some(&format!("{label}-arc-instances")),
1665                size: (arc_capacity * std::mem::size_of::<ArcInstance>()) as u64,
1666                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1667                mapped_at_creation: false,
1668            })
1669        });
1670        let arc_shader = arcs.is_some().then(|| {
1671            device.create_shader_module(wgpu::ShaderModuleDescriptor {
1672                label: Some(&format!("{label}-arc-shader")),
1673                source: wgpu::ShaderSource::Wgsl(arc_shader_source().into()),
1674            })
1675        });
1676        // The arc pair is built from one closure for the reason the segment pair
1677        // is: they differ in the blend state and in nothing else, and any other
1678        // divergence would render as a difference between two batches of the same
1679        // figure.
1680        let make_arc = |blend: wgpu::BlendState, suffix: &str| {
1681            let arc_shader = arc_shader.as_ref()?;
1682            Some(
1683                device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1684                    label: Some(&format!("{label}-arc-pipeline{suffix}")),
1685                    layout: Some(&pipeline_layout),
1686                    vertex: wgpu::VertexState {
1687                        module: arc_shader,
1688                        entry_point: Some("vs_main"),
1689                        compilation_options: Default::default(),
1690                        buffers: &[Some(wgpu::VertexBufferLayout {
1691                            array_stride: std::mem::size_of::<ArcInstance>() as u64,
1692                            step_mode: wgpu::VertexStepMode::Instance,
1693                            attributes: &wgpu::vertex_attr_array![
1694                                0 => Float32x2,
1695                                1 => Float32,
1696                                2 => Float32,
1697                                3 => Float32,
1698                                4 => Float32x3,
1699                                5 => Float32,
1700                            ],
1701                        })],
1702                    },
1703                    fragment: Some(wgpu::FragmentState {
1704                        module: arc_shader,
1705                        entry_point: Some("fs_main"),
1706                        compilation_options: Default::default(),
1707                        targets: &[Some(wgpu::ColorTargetState {
1708                            format: surface_format,
1709                            blend: Some(blend),
1710                            write_mask: wgpu::ColorWrites::ALL,
1711                        })],
1712                    }),
1713                    primitive: wgpu::PrimitiveState::default(),
1714                    depth_stencil: None,
1715                    multisample: wgpu::MultisampleState::default(),
1716                    multiview_mask: None,
1717                    cache: None,
1718                }),
1719            )
1720        };
1721        let arc_pipeline = make_arc(crate::render::gpu::ADDITIVE_LIGHT_SATURATING_COVERAGE, "");
1722        let arc_over_pipeline = split
1723            .then(|| make_arc(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING, "-over"))
1724            .flatten();
1725
1726        // Zero unless the pipeline exists, so `draw_all` needs no second test:
1727        // a renderer without arcs clamps every arc batch to nothing.
1728        let arc_capacity = if arc_pipeline.is_some() {
1729            arc_capacity
1730        } else {
1731            0
1732        };
1733
1734        Self {
1735            pipeline,
1736            over_pipeline,
1737            arc_pipeline,
1738            arc_over_pipeline,
1739            instances,
1740            arcs,
1741            uniforms,
1742            bind_group,
1743            capacity,
1744            arc_capacity,
1745            seg3d: None,
1746        }
1747    }
1748
1749    /// Segments the instance buffer can hold — the scene clamps its geometry to
1750    /// this and surfaces any drop at load (ADR-0007 cap must never be silent).
1751    pub fn capacity(&self) -> usize {
1752        self.capacity
1753    }
1754
1755    /// Arcs the arc buffer can hold — `0` when this renderer was not built with
1756    /// [`new_with_arcs`](Self::new_with_arcs), in which case
1757    /// [`draw_arcs`](Self::draw_arcs) draws none.
1758    pub fn arc_capacity(&self) -> usize {
1759        self.arc_capacity
1760    }
1761
1762    /// Draw `segments` as thick glowing quads at the given `aspect` and `glow`
1763    /// multiplier, under the shared `xform` camera transform (zoom/pan, ADR-0018),
1764    /// **loading** over the engine backdrop rather than clearing (Plan 0018 Phase
1765    /// 3 — the background pass owns the clear). Segments beyond `capacity` are
1766    /// dropped defensively (the scene is responsible for capping at load).
1767    ///
1768    /// `softness` is the across-the-stroke profile (`PROFILE_WGSL`, ADR-0124):
1769    /// `1.0` is the pre-Plan-0114 quadratic falloff, `0` a solid stroke with a
1770    /// one-pixel edge. **There is no default here** — one uniform serves every
1771    /// entry point, so each caller names the constant it answers to:
1772    /// [`lines::DEFAULT_SOFTNESS`](super::DEFAULT_SOFTNESS) for the four line
1773    /// families, [`warp_mesh::MILKDROP_SOFTNESS`](crate::render::scenes::warp_mesh::MILKDROP_SOFTNESS)
1774    /// for the MilkDrop surface.
1775    ///
1776    /// `metric` is the space the stroke is measured in (ADR-0160) and has no
1777    /// default either, for the same reason: [`StrokeMetric::World`] for the four
1778    /// line families, [`StrokeMetric::Clip`] for `warp_mesh`.
1779    #[allow(
1780        clippy::too_many_arguments,
1781        reason = "distinct GPU handles plus the per-frame draw parameters (aspect, glow, \
1782                  softness, stroke metric, view transform); bundling them would only shuffle \
1783                  the same values behind a one-use struct"
1784    )]
1785    pub fn draw(
1786        &mut self,
1787        queue: &wgpu::Queue,
1788        encoder: &mut wgpu::CommandEncoder,
1789        view: &wgpu::TextureView,
1790        aspect: f32,
1791        glow: f32,
1792        softness: f32,
1793        metric: StrokeMetric,
1794        xform: super::ViewTransform,
1795        segments: &[SegmentInstance],
1796    ) {
1797        self.draw_split(
1798            queue,
1799            encoder,
1800            view,
1801            aspect,
1802            glow,
1803            softness,
1804            metric,
1805            xform,
1806            segments,
1807            segments.len(),
1808        );
1809    }
1810
1811    /// [`draw`](Self::draw), with the batch split by blend mode: the first
1812    /// `n_additive` segments are added (ADR-0056's seam, what every line scene
1813    /// uses), and the rest are composited **over** using each segment's own
1814    /// [`alpha`](SegmentInstance::alpha).
1815    ///
1816    /// # Why a split range rather than two calls
1817    ///
1818    /// One instance buffer, one upload, one render pass, two `draw` calls that
1819    /// differ only in the pipeline bound. Two calls would mean two passes over
1820    /// the same attachment and a second buffer, and the *order* would stop being
1821    /// expressible: an over-blended stroke has to land on top of the additive
1822    /// light it covers, which a single ordered batch gives for free.
1823    ///
1824    /// The caller partitions — it is the only thing that knows which producer
1825    /// each segment came from. Passing `n_additive >= segments.len()` is exactly
1826    /// [`draw`](Self::draw).
1827    #[allow(
1828        clippy::too_many_arguments,
1829        reason = "see `draw` — this is that signature plus the partition index"
1830    )]
1831    pub fn draw_split(
1832        &mut self,
1833        queue: &wgpu::Queue,
1834        encoder: &mut wgpu::CommandEncoder,
1835        view: &wgpu::TextureView,
1836        aspect: f32,
1837        glow: f32,
1838        softness: f32,
1839        metric: StrokeMetric,
1840        xform: super::ViewTransform,
1841        segments: &[SegmentInstance],
1842        n_additive: usize,
1843    ) {
1844        self.draw_all(
1845            queue,
1846            encoder,
1847            view,
1848            aspect,
1849            glow,
1850            softness,
1851            metric,
1852            xform,
1853            segments,
1854            n_additive,
1855            &[],
1856            false,
1857        );
1858    }
1859
1860    /// [`draw`](Self::draw), plus `arcs` — [`ArcInstance`]s stroked by the
1861    /// per-pixel distance field (ADR-0098) **in the same additive pass**, from
1862    /// the same uniform, after the segments.
1863    ///
1864    /// One pass rather than two for the reason [`draw_split`](Self::draw_split)
1865    /// gives: a second pass would mean a second load of the attachment and a
1866    /// second set of uniforms to keep in step. Additive blending is
1867    /// order-independent, so "after the segments" is a statement about the
1868    /// command stream and not about the picture.
1869    ///
1870    /// Arcs beyond [`arc_capacity`](Self::arc_capacity) are dropped defensively,
1871    /// exactly as segments beyond `capacity` are; a renderer built without the
1872    /// arc pipeline has a capacity of zero and draws none.
1873    #[allow(
1874        clippy::too_many_arguments,
1875        reason = "see `draw` — this is that signature plus the arc batch"
1876    )]
1877    pub fn draw_arcs(
1878        &mut self,
1879        queue: &wgpu::Queue,
1880        encoder: &mut wgpu::CommandEncoder,
1881        view: &wgpu::TextureView,
1882        aspect: f32,
1883        glow: f32,
1884        softness: f32,
1885        metric: StrokeMetric,
1886        xform: super::ViewTransform,
1887        segments: &[SegmentInstance],
1888        arcs: &[ArcInstance],
1889    ) {
1890        self.draw_all(
1891            queue,
1892            encoder,
1893            view,
1894            aspect,
1895            glow,
1896            softness,
1897            metric,
1898            xform,
1899            segments,
1900            segments.len(),
1901            arcs,
1902            false,
1903        );
1904    }
1905
1906    /// [`draw_arcs`](Self::draw_arcs), with the **whole batch composited over**
1907    /// rather than added — the opacity-preserving seam of ADR-0138's limited-ink
1908    /// class, reached by the four line systems through `stroke_blend`.
1909    ///
1910    /// Segments and arcs both take the OVER pipeline, so a scene whose figure is
1911    /// part strokes and part circles draws one substance rather than two. Order
1912    /// inside the batch becomes the order on screen: a later stroke replaces the
1913    /// interior of what it covers instead of summing with it, which is the whole
1914    /// property. Pass an empty `arcs` from a scene that draws none.
1915    ///
1916    /// A renderer built without the OVER pipelines falls back to the additive
1917    /// ones, exactly as [`draw_split`](Self::draw_split) does — the wrong blend
1918    /// rather than a panic, and unreachable in the shipped path, where the shared
1919    /// line renderer is built with them.
1920    #[allow(
1921        clippy::too_many_arguments,
1922        reason = "see `draw` — this is that signature plus the arc batch"
1923    )]
1924    pub fn draw_opaque(
1925        &mut self,
1926        queue: &wgpu::Queue,
1927        encoder: &mut wgpu::CommandEncoder,
1928        view: &wgpu::TextureView,
1929        aspect: f32,
1930        glow: f32,
1931        softness: f32,
1932        metric: StrokeMetric,
1933        xform: super::ViewTransform,
1934        segments: &[SegmentInstance],
1935        arcs: &[ArcInstance],
1936    ) {
1937        self.draw_all(
1938            queue, encoder, view, aspect, glow, softness, metric, xform, segments, 0, arcs, true,
1939        );
1940    }
1941
1942    /// 3D segments the `seg3d` buffer can hold — `0` when this renderer was not
1943    /// built with [`new_3d`](Self::new_3d).
1944    pub fn capacity_3d(&self) -> usize {
1945        self.seg3d.as_ref().map_or(0, |seg3d| seg3d.capacity)
1946    }
1947
1948    /// Draw `segments` through the shared camera (ADR-0257), **loading** over
1949    /// the backdrop, in one pass of their own. Segments beyond
1950    /// [`capacity_3d`](Self::capacity_3d) are dropped defensively; a renderer
1951    /// without the pipeline draws none.
1952    ///
1953    /// Every endpoint must already be in front of the near plane: the scene
1954    /// clips on the CPU against the same view the uniform carries
1955    /// ([`CameraView::clip_near`](crate::render::camera::CameraView::clip_near)).
1956    ///
1957    /// `frame` is the camera block's whole frame, not only its uniform: the
1958    /// depth cues it carries (`fog`, `solid`) are the block's, so every system
1959    /// that splices the block draws them without naming them.
1960    ///
1961    /// **A solid frame** (ADR-0263) is drawn far to near and composited over:
1962    /// the segments are sorted by [`sort_far_to_near`] into scratch reserved
1963    /// at the buffer's capacity, so the sort allocates nothing, and the over
1964    /// pipeline paints each nearer stroke over what it covers. A glow frame
1965    /// uploads `segments` as they came, through the additive pipeline.
1966    #[allow(
1967        clippy::too_many_arguments,
1968        reason = "distinct GPU handles plus the per-frame camera and stroke values; bundling \
1969                  them would only shuffle the same values behind a one-use struct"
1970    )]
1971    pub(crate) fn draw_3d(
1972        &mut self,
1973        queue: &wgpu::Queue,
1974        encoder: &mut wgpu::CommandEncoder,
1975        view: &wgpu::TextureView,
1976        frame: &crate::render::camera::CameraFrame,
1977        glow: f32,
1978        softness: f32,
1979        segments: &[Segment3dInstance],
1980    ) {
1981        self.draw_3d_on(queue, encoder, view, frame, glow, softness, None, segments);
1982    }
1983
1984    /// [`draw_3d`](Self::draw_3d) for a **heightfield** standing on the plane
1985    /// `y = ground`: a [`skirt`](Segment3dInstance::skirt) instance fills down
1986    /// to that plane, and a solid frame orders its segments by their feet on
1987    /// it ([`sort_painter`]), so a near row hides a far one whatever either's
1988    /// height (ADR-0263).
1989    #[allow(
1990        clippy::too_many_arguments,
1991        reason = "see `draw_3d` — this is that signature plus the ground plane"
1992    )]
1993    pub(crate) fn draw_3d_terrain(
1994        &mut self,
1995        queue: &wgpu::Queue,
1996        encoder: &mut wgpu::CommandEncoder,
1997        view: &wgpu::TextureView,
1998        frame: &crate::render::camera::CameraFrame,
1999        glow: f32,
2000        softness: f32,
2001        ground: f32,
2002        segments: &[Segment3dInstance],
2003    ) {
2004        self.draw_3d_on(
2005            queue,
2006            encoder,
2007            view,
2008            frame,
2009            glow,
2010            softness,
2011            Some(ground),
2012            segments,
2013        );
2014    }
2015
2016    /// The one body behind [`draw_3d`](Self::draw_3d) and
2017    /// [`draw_3d_terrain`](Self::draw_3d_terrain). `ground: None` keys the
2018    /// solid sort at each midpoint and puts a stray skirt's foot at `y = 0`.
2019    #[allow(clippy::too_many_arguments, reason = "see `draw_3d_terrain`")]
2020    fn draw_3d_on(
2021        &mut self,
2022        queue: &wgpu::Queue,
2023        encoder: &mut wgpu::CommandEncoder,
2024        view: &wgpu::TextureView,
2025        frame: &crate::render::camera::CameraFrame,
2026        glow: f32,
2027        softness: f32,
2028        ground: Option<f32>,
2029        segments: &[Segment3dInstance],
2030    ) {
2031        let mut pass = gpu::color_pass(encoder, "seg3d-pass", view, wgpu::LoadOp::Load);
2032        let Some(seg3d) = self.seg3d.as_mut() else {
2033            return;
2034        };
2035        let count = segments.len().min(seg3d.capacity);
2036        let drawn = segments.get(..count).unwrap_or(&[]);
2037        if drawn.is_empty() {
2038            return; // nothing to stroke; the backdrop shows through
2039        }
2040        if frame.solid {
2041            let (keys, sorted) = (&mut seg3d.keys, &mut seg3d.sorted);
2042            match ground {
2043                None => sort_far_to_near(&frame.view, drawn, keys, sorted),
2044                Some(_) => sort_painter(&frame.view, drawn, ground, keys, sorted),
2045            }
2046            queue.write_buffer(&seg3d.instances, 0, bytemuck::cast_slice(&seg3d.sorted));
2047        } else {
2048            queue.write_buffer(&seg3d.instances, 0, bytemuck::cast_slice(drawn));
2049        }
2050        queue.write_buffer(&seg3d.camera, 0, bytemuck::bytes_of(&frame.uniform));
2051        queue.write_buffer(
2052            &seg3d.stroke,
2053            0,
2054            bytemuck::bytes_of(&Stroke3dUniform {
2055                v: [glow, softness, frame.fog, ground.unwrap_or(0.0)],
2056            }),
2057        );
2058        pass.set_pipeline(if frame.solid {
2059            &seg3d.over_pipeline
2060        } else {
2061            &seg3d.pipeline
2062        });
2063        pass.set_bind_group(0, &seg3d.bind_group, &[]);
2064        pass.set_vertex_buffer(0, seg3d.instances.slice(..));
2065        pass.draw(0..6, 0..count as u32);
2066    }
2067
2068    /// The one body behind [`draw`](Self::draw),
2069    /// [`draw_split`](Self::draw_split), [`draw_arcs`](Self::draw_arcs) and
2070    /// [`draw_opaque`](Self::draw_opaque): one buffer upload per kind, one
2071    /// uniform write, one render pass.
2072    #[allow(
2073        clippy::too_many_arguments,
2074        reason = "see `draw` — this is that signature plus the partition index, \
2075                  the arc batch and the arc batch's own seam"
2076    )]
2077    fn draw_all(
2078        &mut self,
2079        queue: &wgpu::Queue,
2080        encoder: &mut wgpu::CommandEncoder,
2081        view: &wgpu::TextureView,
2082        aspect: f32,
2083        glow: f32,
2084        softness: f32,
2085        metric: StrokeMetric,
2086        xform: super::ViewTransform,
2087        segments: &[SegmentInstance],
2088        n_additive: usize,
2089        arcs: &[ArcInstance],
2090        arcs_over: bool,
2091    ) {
2092        let count = segments.len().min(self.capacity);
2093        let drawn = segments.get(..count).unwrap_or(&[]);
2094        let arc_count = arcs.len().min(self.arc_capacity);
2095        let arcs_drawn = arcs.get(..arc_count).unwrap_or(&[]);
2096
2097        // The in-frame geometry diagnostic (Plan 0069, ADR-0083). Off in the
2098        // shipped path, and it reads `drawn` — the segments that actually reach
2099        // the instance buffer — without touching a GPU resource, so "off" is a
2100        // `Cell::get` and "on" changes nothing about the picture. The aspect it
2101        // measures against is the one this call was handed: the render target's
2102        // (ADR-0037), under the same `max(0.1)` clamp the uniform and the shader
2103        // apply, so the rectangle is the one the frame actually shows.
2104        if metrics::extent_diagnostic_on() {
2105            let extent = measure_extent(drawn, arcs_drawn, aspect.max(0.1), xform);
2106            metrics::record_draw_extent(extent);
2107        }
2108
2109        if !drawn.is_empty() {
2110            queue.write_buffer(&self.instances, 0, bytemuck::cast_slice(drawn));
2111        }
2112        if let (Some(buffer), false) = (&self.arcs, arcs_drawn.is_empty()) {
2113            queue.write_buffer(buffer, 0, bytemuck::cast_slice(arcs_drawn));
2114        }
2115        queue.write_buffer(
2116            &self.uniforms,
2117            0,
2118            bytemuck::bytes_of(&Uniforms {
2119                v: [aspect.max(0.1), glow, softness, metric.lane()],
2120                view: [xform.zoom, xform.pan[0], xform.pan[1], 0.0],
2121            }),
2122        );
2123
2124        // Load over the engine backdrop (ADR-0018); additive strokes bloom over
2125        // it and the empty space reveals it.
2126        let mut pass = gpu::color_pass(encoder, "line-pass", view, wgpu::LoadOp::Load);
2127        if drawn.is_empty() && arcs_drawn.is_empty() {
2128            return; // nothing to stroke; the backdrop shows through
2129        }
2130        // Clamped to what actually reached the buffer: `n_additive` counts into
2131        // `segments`, which may be longer than `drawn`.
2132        let split = n_additive.min(drawn.len()) as u32;
2133        pass.set_bind_group(0, &self.bind_group, &[]);
2134        if split > 0 {
2135            pass.set_pipeline(&self.pipeline);
2136            pass.set_vertex_buffer(0, self.instances.slice(..));
2137            pass.draw(0..6, 0..split);
2138        }
2139        // Between the two segment ranges rather than after both: an arc is
2140        // additive light, and the OVER range has to land on top of the light it
2141        // covers. No scene draws both today; the ordering is here so that the
2142        // day one does, it is already right.
2143        let arc_pipeline = if arcs_over {
2144            self.arc_over_pipeline
2145                .as_ref()
2146                .or(self.arc_pipeline.as_ref())
2147        } else {
2148            self.arc_pipeline.as_ref()
2149        };
2150        if let (Some(pipeline), Some(buffer), false) =
2151            (arc_pipeline, &self.arcs, arcs_drawn.is_empty())
2152        {
2153            pass.set_pipeline(pipeline);
2154            pass.set_vertex_buffer(0, buffer.slice(..));
2155            pass.draw(0..6, 0..arcs_drawn.len() as u32);
2156        }
2157        if (drawn.len() as u32) > split {
2158            // Falls back to the additive pipeline when the scene did not ask for
2159            // the second one. That is the wrong blend rather than a panic, and it
2160            // is unreachable in practice: the only caller that passes a partition
2161            // is the one that built with `new_split`.
2162            pass.set_pipeline(self.over_pipeline.as_ref().unwrap_or(&self.pipeline));
2163            pass.set_vertex_buffer(0, self.instances.slice(..));
2164            pass.draw(0..6, split..drawn.len() as u32);
2165        }
2166    }
2167}
2168
2169#[cfg(test)]
2170mod tests;