Orbit C++ → Rust · Post 16 · Renderer internals

How Orbit Draws a Million Scopes

A line-by-line reading of the three WGSL shaders behind the live timeline — written for someone who has never opened a shader before.

src/OrbitLiveViewer/crates/orbit-live-render/src/shaders/ wgpu 25 · WGSL
01 — THE PROBLEM

Why any of this exists

A profiler timeline is a pathological drawing problem.

Orbit's live viewer shows a flame chart: thousands of nested coloured rectangles, each one a function call, scrolling and zooming at 60 frames per second. The naive approach — ask the UI toolkit to draw a rectangle, a million times — falls over immediately. egui would need to build a million shape objects, tessellate each into triangles, and hand the lot to the GPU every single frame.

So the viewer does something different. The entire timeline is one draw call. The rectangles never exist as objects; they exist as 48 bytes each in a flat buffer, and the GPU invents the geometry on the fly. And when you zoom out far enough that rectangles are thinner than a pixel, the viewer stops drawing rectangles altogether and draws a photograph of them instead.

Three small shader programs make that work. This post reads all three, line by line. If you know what a function is but have never heard of a fragment shader, you are the intended reader — the vocabulary section below is not optional filler, it's the part that makes the rest legible.

Event ring 32 B / event widest ≥ 4px? yes no Instanced SDF instance.wgsl CPU raster → blit blit_rect.wgsl Frame alpha blended one path per frame, never both
The level-of-detail fork. choose_lod samples the densest lanes; if any event in view is at least INSTANCE_MIN_PX (4 pixels) wide, the frame takes the instanced path. Otherwise rectangles are smaller than pixels and the CPU rasterises columns instead.

That fork is the single most important structural fact about the renderer, and it's four lines of Rust:

crates/orbit-live-render/src/lod.rs · choose_lod_from_keysRust

let ns_per_px = (t1 - t0) as f64 / width as f64;
for key in keys {
    if let Some(e) = lane.overlapping(t0, t1) {
        if (e.duration_ns as f64 / ns_per_px) >= min_wide_px as f64 {
            return TimelineLod::Instanced;
        }
    }
}
TimelineLod::PixelColumns

02 — VOCABULARY

What a shader actually is

Skip this only if you already know what a fragment shader does.

A GPU draws triangles. That is the entire primitive vocabulary — no circles, no rectangles, no rounded corners. Everything you see on a screen is triangles with a colour computed per pixel.

To draw, you give the GPU a pipeline made of two small programs:

The vertex shader runs once per corner of each triangle. Its job is to decide where that corner lands on screen. It runs in parallel across all corners at once.

The fragment shader runs once per pixel covered by the triangle. Its job is to return a colour. Also massively parallel — a modern GPU runs thousands of these simultaneously.

Between them sits interpolation. Whatever extra values the vertex shader returns get smoothly blended across the triangle before reaching the fragment shader. If one corner says 0.0 and another says 10.0, a pixel halfway between receives 5.0. This is the mechanism by which the vertex shader talks to the fragment shader, and Orbit leans on it heavily.

Three kinds of input feed these programs:

Input kindChanges how oftenIn Orbit
UniformOnce per draw call — same value for every vertex and pixelViewport size, scroll origin, animation clock
Vertex attributePer vertex, or (here) per instanceThe 48 bytes describing one scope rectangle
Texture + samplerRandom access, read per pixelThe CPU-rasterised timeline image

Normalised device coordinates

The one piece of coordinate maths you need. A vertex shader must output position in normalised device coordinates (NDC): a box from −1 to +1 on both axes, with Y pointing up. But every UI framework, Orbit included, thinks in pixels with Y pointing down from the top-left. So every vertex shader here ends with the same conversion, and the Y line has that 1.0 - in front precisely because the axis flips.

Screen pixels (0, 0) (w, h) y+ x+ x/w*2-1 1-y/h*2 NDC (0, 0) y+ (-1, +1) (+1, -1)
Why every vertex shader ends the same way. The X conversion is a plain rescale. The Y conversion subtracts from 1 instead of adding, because screen Y grows downward and NDC Y grows upward — forget the flip and your timeline renders upside down.

03 — THE SIMPLE PATH

blit_rect.wgsl, in full

31 lines. Copy a CPU-drawn image onto a rectangle of the screen.

When you zoom out past the point where individual calls are visible, Orbit's CPU walks the event ring and paints an RGBA image — one texel per pixel column per lane. The GPU's only job is to put that image on screen at the right place. This shader does nothing else, which makes it a good place to learn the shape of a WGSL program.

Bindings
What the shader can read from outside
struct Uniforms { viewport: vec2<f32>, dest: vec4<f32>, };

A plain data record. vec2<f32> is two 32-bit floats; vec4 is four. viewport holds the framebuffer size in pixels. dest packs the destination rectangle as (x, y, width, height).

Packing four numbers into one vec4 isn't cosmetic — GPU uniform buffers have alignment rules that make four-float groups the natural unit.

@group(0) @binding(0) var<uniform> uni: Uniforms;

Declares that record as an actual input. @group and @binding are the address the Rust side uses to attach a buffer; you can see the matching numbers in create_bind_group_layout in timeline.rs.

var<uniform> means read-only, same value for every invocation this draw call.

@group(0) @binding(1) var tex: texture_2d<f32>; @group(0) @binding(2) var samp: sampler;

The image, and the rule for reading it. A sampler decides what happens between texels — blend neighbours, or snap to the nearest one.

Orbit configures this one as FilterMode::Nearest. That matters: each texel is a measured pixel column of profiling data, not a photograph. Blurring adjacent columns would invent activity that never happened.

Vertex stage
Runs 6 times, once per triangle corner
struct VsOut { @builtin(position) pos: vec4<f32>, @location(0) uv: vec2<f32> }

What the vertex stage hands onward. @builtin(position) is mandatory and special — the GPU reads it to place the triangle.

@location(0) is a free slot for anything else you want interpolated across the surface. Here it carries texture coordinates.

@vertex fn vs_main(@builtin(vertex_index) vid: u32) -> VsOut {

@vertex marks the entry point. vertex_index is a counter the GPU supplies: 0, 1, 2, 3, 4, 5 for the six corners.

Notice there is no vertex buffer. The geometry is generated from that counter alone.

var corners = array<vec2<f32>, 6>( vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(0.0, 1.0), vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(1.0, 1.0) ); let uv = corners[vid];

A rectangle is two triangles. These six entries are those triangles' corners in unit space, where (0,0) is top-left of the rectangle and (1,1) is bottom-right.

Read them as two groups of three: the first triangle covers the top-left half, the second the bottom-right. The middle two corners appear in both.

let x = uni.dest.x + uv.x * uni.dest.z; let y = uni.dest.y + uv.y * uni.dest.w;

Stretch the unit square onto the real destination rectangle. .x .y .z .w are the four components; here they mean left, top, width, height.

A corner at uv = (1, 0) becomes (left + width, top) — the top-right corner, in screen pixels.

o.pos = vec4((x / uni.viewport.x) * 2.0 - 1.0, 1.0 - (y / uni.viewport.y) * 2.0, 0.0, 1.0);

Pixels to NDC, exactly as in the diagram above. The 0.0 is depth — unused, since Orbit runs without a depth buffer and relies on draw order for layering. The 1.0 is the perspective divisor, and 1 means "no perspective".

o.uv = uv; return o;

Pass the unit-square coordinate through untouched. Because it gets interpolated, a pixel in the middle of the rectangle receives (0.5, 0.5) — which is exactly the middle of the texture. The mapping falls out for free.

Fragment stage
Runs once per covered pixel
@fragment fn fs_main(v: VsOut) -> @location(0) vec4<f32> { return textureSampleLevel(tex, samp, v.uv, 0.0); }

Read the texture at this pixel's interpolated coordinate and return it as the colour. @location(0) on the return type means "write to the first colour attachment" — the screen.

The trailing 0.0 picks mip level zero. textureSampleLevel rather than plain textureSample because the explicit level makes the call legal from any control flow, and there are no mips to choose from anyway.

Aside — the fourth shader that isn't

There is a third file, blit.wgsl, that looks almost identical but skips the destination rectangle and covers the whole viewport using a three-vertex trick (a single oversized triangle instead of two). It is exported but never compiled into a pipeline — only BLIT_RECT_WGSL and INSTANCE_WGSL reach create_shader_module. Its only remaining reference is a string assertion in a test. Worth knowing before you go looking for where it runs.


04 — THE DATA

48 bytes per rectangle

Before the interesting shader, the buffer it reads.

The instanced path issues exactly one draw call: pass.draw(0..6, 0..instance_count). Six vertices, N instances. The GPU runs the vertex shader 6 × N times, and for each run it hands over one slice of the instance buffer — because the vertex layout declares step_mode: VertexStepMode::Instance rather than the usual per-vertex stepping.

That is the whole trick. One quad definition, reused N times, each time with different numbers attached.

One instance — 48 bytes, written by pack_instances() x y w h bytes 0–15 r g b a bytes 16–31 radius flags 32–39 0.0 0.0 40–47 unused @location(0) rect @location(1) color @loc(2) extra Offsets declared once, in the Rust vertex layout offset 0 → Float32x4 offset 16 → Float32x4 offset 32 → Float32x2 array_stride = 48 step_mode = Instance
The contract between Rust and WGSL. Nothing checks that pack_instances and the shader's VsIn agree — the offsets in the vertex layout are the only thing tying them together. The last eight bytes are written as zeroes and never bound to an attribute; they exist to round the stride to 48.

Two of those fields deserve a note. extra.x is the corner radius in pixels. extra.y is a mark — a small integer smuggled through a float, encoding the rectangle's interaction state:

ValueConstantWhat it means on screen
0.0FLAG_NONEOrdinary scope, palette colour
1.0FLAG_HOVERUnder the cursor — 5% brighter, stronger rim
2.0FLAG_SELECTEDClicked — recoloured blue, lifted, pulsing
3.0FLAG_SIBLINGSame function elsewhere — recoloured light blue
4.0FLAG_DIMMEDFiltered out by search — desaturated and darkened
5.0FLAG_INACTIVEOutside the thread focus — recoloured flat grey
6.0FLAG_SAME_PIDAnother thread of the focused process — a lighter grey

The last two are the thread-focus greys, and they mirror how C++ Orbit paints its scheduler track: select a thread and every scheduler slice outside that selection drops to a flat grey, with a lighter grey kept for the selected thread's own process so its other threads still read on the cores. The CPU applies them in apply_highlight_flags, and only to scheduler slices — thread tracks keep their colours whatever is selected, because greying a thread's scopes would hide the very comparison a selection is for.

Hence the odd-looking comparisons in the shader: mark > 1.5 && mark < 2.5 rather than mark == 2.0. Floats that have been through interpolation are not reliably exact, so the shader tests a window around each integer instead of equality.


05 — UNIFORMS

32 bytes, and why the number matters

The instanced shader's uniform block is small — viewport, scroll origin, a clock — but its size was recently a bug that stopped the viewer from starting at all.

shaders/instance.wgsl · lines 7–14WGSL

struct Uniforms {
  viewport: vec2<f32>,   // framebuffer size in physical pixels
  origin: vec2<f32>,     // where the timeline body starts on screen
  time: f32,             // seconds, drives the selection pulse
  _pad0: f32,
  _pad1: f32,
  _pad2: f32,
};

Those three _pad fields are never read. They exist because WebGPU's downlevel targets — WebGL2 among them — require a uniform struct's total size to be a multiple of 16 bytes. Without them the struct is 8 + 8 + 4 = 20, rounded to 24 by alignment. Pipeline creation fails outright:

The error this produced

In Device::create_render_pipeline, label = 'orbit-live-sdf': the type given for group 0 binding 0 has a size of 24. As the device does not support DownlevelFlags::BUFFER_BINDINGS_NOT_16_BYTE_ALIGNED, the type must have a size that is a multiple of 16 bytes.

Not a subtle rendering artefact — the viewer panicked before drawing a single frame.

The fix is three scalar pads rather than one vec3<f32>, and that distinction is worth understanding because it's the kind of thing that looks arbitrary and isn't. A vec3 in WGSL has 16-byte alignment. Placing one after time at offset 20 would round its start up to 32, making the struct 48 bytes — still legal, but no longer matching the 32 bytes the Rust side writes. Three separate f32s have 4-byte alignment, so they pack at offsets 20, 24, 28 and land the struct at exactly 32.

BROKEN — 24 bytes, not a multiple of 16 viewport 8 origin 8 time _pad 24 ✗ CORRECT — 32 bytes via three scalar pads viewport 8 origin 8 time _pad0 _pad1 _pad2 32 ✓ matches Rust IF _pad WERE A vec3 — align 16 pushes it to offset 32 viewport 8 origin 8 time skipped to reach align 16 vec3 pad 48 ✗ mismatch
Padding is not free-form. All three layouts are valid WGSL; only the middle one is both 16-byte-legal and byte-compatible with the 32 bytes pack_inst_uniforms() writes on the Rust side.

06 — THE VERTEX STAGE

Inventing a quad that's bigger than the rectangle

instance.wgsl lines 37–78.

The vertex shader builds the same unit-square quad as the blit, but with two twists. First, it must offset the quad by the instance's own position. Second — and this is the non-obvious part — it deliberately makes the quad slightly larger than the rectangle being drawn, because the antialiased edge needs somewhere to live.

A fragment shader can only colour pixels inside its triangles, but the soft edge of a rounded rectangle falls a pixel or so outside the geometry. So the quad is inflated by a couple of pixels on every side to leave room for that edge, and a selected scope — which lifts and grows as it pulses — is given a touch more.

An earlier version of this shader inflated the quad by six pixels a side to make room for an analytic drop shadow: a blurred, offset copy of the rectangle evaluated per pixel with no second pass. That shadow was cut. The padding it forced was pure overdraw on a dense track, and the faint offset copy read as a rendering artifact more than a shadow. What survives is the inflation trick itself, shrunk to its honest size — a roughly two-pixel margin for the edge.

the scope rect rect.zw quad actually drawn pad pad = AA_PAD (2.0) + 1.0 if selected + pulse * PULSE_RADIUS Grows with the pulse so the breathing highlight never gets clipped by its own quad.
Padding the geometry. Every instance draws a quad inflated by a couple of pixels on all four sides — just enough for the antialiased edge, with a little extra for a selected scope's lift and pulse. Inside the rectangle the fill is opaque; out in the thin margin the coverage falls smoothly to zero and the pixel ends up transparent.
const AA_PAD: f32 = 2.0; const SIBLING_RGB: vec3<f32> = vec3(0.392, 0.710, 0.965); const SELECTED_RGB: vec3<f32> = vec3(0.0, 0.502, 1.0); const PULSE_PERIOD: f32 = 1.2; const PULSE_RADIUS: f32 = 0.45; const PULSE_BRIGHT: f32 = 0.05;

Compile-time constants. AA_PAD is the margin, in pixels, added around every rectangle so the antialiased edge is not clipped by the quad.

The three PULSE_ values tune the selected-scope animation: a 1.2-second cycle that grows the quad by up to 0.45px and brightens the fill by 5% at its peak. Small numbers on purpose — a breathing highlight, not a flashing one.

fn selected_pulse(time: f32) -> f32 { return 0.5 + 0.5 * sin(time * 6.28318530718 / PULSE_PERIOD); }

A sine wave remapped from its natural −1…+1 range into 0…1, so it can be used directly as a blend factor.

6.28318… is 2π. Dividing by PULSE_PERIOD makes one full cycle take 1.2 seconds regardless of frame rate, because the input is wall-clock time rather than a frame counter.

vs_main
6 × N invocations
@vertex fn vs_main(inst: VsIn, @builtin(vertex_index) vid: u32) -> VsOut {

Two inputs now. vid still counts 0…5 around the quad, but inst arrives from the instance buffer and holds the same values for all six corners of one rectangle.

let uv = corners[vid]; let mark = inst.extra.y; let selected = mark > 1.5 && mark < 2.5;

Unpack. mark is the interaction state from the table earlier; selected tests the window around 2.0.

The vertex stage cares about selection because selected rectangles change shape, not just colour — they lift and grow.

let pulse = select(0.0, selected_pulse(uni.time), selected); let lift = select(0.0, -0.8, selected);

select(a, b, cond) returns b when the condition holds, a otherwise — note the reversed argument order compared to a ternary in most languages.

GPUs run many invocations in lockstep, so a branch where different pixels disagree costs both sides. select computes both and picks, which is cheaper for something this small.

lift is negative because screen Y grows downward: −0.8 moves the selected scope up by eight-tenths of a pixel.

let pad = AA_PAD + select(0.0, 1.0, selected) + pulse * PULSE_RADIUS;

The inflation from the diagram. Base 2px for the edge, one extra pixel when selected, plus up to 0.45px more at the peak of the pulse — so the quad breathes in step with the highlight it has to contain.

let x = uni.origin.x + inst.rect.x - pad + uv.x * (inst.rect.z + 2.0 * pad); let y = uni.origin.y + inst.rect.y + lift - pad + uv.y * (inst.rect.w + 2.0 * pad);

The corner in screen pixels, assembled in four parts: the timeline's scroll origin, the rectangle's own position, minus one pad to start outside the rectangle, plus the unit coordinate scaled across the padded width.

2.0 * pad because the inflation applies to both edges.

o.pos = vec4((x / uni.viewport.x) * 2.0 - 1.0, 1.0 - (y / uni.viewport.y) * 2.0, 0.0, 1.0);

The same pixels-to-NDC conversion as before. Every vertex shader in the codebase ends with this identical pair of expressions.

let cx = inst.rect.x + inst.rect.z * 0.5; let cy = inst.rect.y + inst.rect.w * 0.5 + lift; o.local = vec2(x - (uni.origin.x + cx), y - (uni.origin.y + cy));

The key line of the whole shader. o.local is this corner's offset from the rectangle's centre.

Because it gets interpolated, every pixel in the fragment stage receives its own distance-from-centre vector for free — no per-pixel maths required. That vector is exactly what the distance function needs.

o.half_size = vec2(inst.rect.z, inst.rect.w) * 0.5; o.color = inst.color; o.radius = inst.extra.x; o.mark = mark;

Constants forwarded to the fragment stage. These are identical at all six corners, so interpolating them is a no-op — it's simply the only channel available for passing values down the pipeline.

half_size is halved here rather than in the fragment shader: computed 6 times per rectangle instead of once per pixel.

o.pix = vec2(x - (uni.origin.x + inst.rect.x), y - (uni.origin.y + inst.rect.y + lift));

A second coordinate frame, this time relative to the rectangle's top-left rather than its centre. Used for exactly one effect: a 1-pixel lighter band along the top edge that gives each bar a subtle bevel.


07 — SIGNED DISTANCE FIELDS

How you draw a rounded rectangle with no rounded rectangle

The idea that makes the whole approach work.

The quad is a plain rectangle with hard corners. The thing on screen has soft, rounded, antialiased corners. The gap between those two facts is closed by a signed distance function.

The idea: instead of describing the shape's outline, write a function that answers a simpler question — how far is this point from the shape's edge? Negative inside, zero exactly on the boundary, positive outside. Every pixel evaluates it independently, and the shape emerges from where the sign flips.

d < 0 d = 0 d > 0 contours of equal distance The whole shape, in two lines: q = abs(p) - b + r d = min(max(q.x,q.y),0) + length(max(q,0)) - r p — pixel offset from centre b — half width and height r — corner radius
A shape defined by distance, not by outline. Each contour ring is a set of points at equal distance from the edge. The visible rectangle is simply the contour where d crosses zero — and because d is continuous, the crossing can be found to sub-pixel precision.
fn sd_rounded_box(p: vec2<f32>, b: vec2<f32>, r: f32) -> f32 { let q = abs(p) - b + vec2(r); return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - r; }

Line 1. abs(p) folds all four quadrants onto one — the shape is symmetric, so only one corner needs solving. Subtracting b puts the origin at that corner; adding r shrinks the box to the radius-inset rectangle whose corners the rounding arcs are centred on.

Line 2, first term. max(q.x, q.y) is the distance for a point inside; min(…, 0.0) discards it when the point is outside.

Line 2, second term. length(max(q, 0)) is straight-line distance to the corner for points outside, and zero for points inside. The two terms are mutually exclusive — exactly one is non-zero.

Finally subtracting r pushes the whole field outward by the radius, which is what rounds the corners. This is Inigo Quilez's formulation, near-universal in SDF code.

From distance to a crisp edge

A distance alone doesn't draw anything. Two lines in the fragment shader turn it into antialiased coverage:

let d = sd_rounded_box(v.local, v.half_size, v.radius); let aa = fwidth(d) * 0.75; let fill = 1.0 - smoothstep(-aa, aa, d);

fwidth(d) is the trick. It reports how much d changes between this pixel and its neighbours — the GPU computes fragments in 2×2 groups specifically so derivatives like this are available. In effect it answers "how many units of distance is one pixel worth, here?"

smoothstep(-aa, aa, d) then ramps smoothly from 0 to 1 across a band roughly one pixel wide, and 1.0 - flips it so inside is 1. Pixels straddling the edge get a fractional value.

That is the entire antialiasing scheme. No multisampling, no supersampling — the coverage is computed analytically from the distance, and it stays correct at any zoom because fwidth rescales with the geometry.

let border = 1.0 - smoothstep(-aa, aa, abs(d) - 0.5);

The same trick applied to abs(d) instead of d. Because absolute distance is small near the boundary on both sides, this peaks in a thin band straddling the edge — a 1-pixel ring, used later for the rim highlight.


08 — THE FRAGMENT STAGE

Assembling one pixel

instance.wgsl lines 89–131. Everything above, combined.

This runs for every pixel of every rectangle — the hottest code in the viewer by a wide margin. It reads the interpolated values, computes coverage from the distance function, applies whatever state the mark encodes, and returns a single colour.

let hover = v.mark > 0.5 && v.mark < 1.5; let selected = v.mark > 1.5 && v.mark < 2.5; let sibling = v.mark > 2.5 && v.mark < 3.5; let dimmed = v.mark > 3.5 && v.mark < 4.5; let inactive = v.mark > 4.5 && v.mark < 5.5; let same_pid = v.mark > 5.5 && v.mark < 6.5;

Decode the state, one window per value. Every instance carries exactly one mark, so at most one of these is true. The last two are the thread-focus greys from section 4.

var rgb = v.color.rgb; if dimmed { let luma = dot(rgb, vec3(0.2126, 0.7152, 0.0722)); rgb = mix(rgb, vec3(luma), 0.84) * 0.36; }

Search filtering. Those three coefficients are the Rec. 709 luminance weights — green dominates because human vision is most sensitive to it, so this preserves perceived brightness where a naive average wouldn't.

mix pulls 84% of the way toward grey, then * 0.36 darkens the result. Non-matching scopes recede without disappearing.

if inactive { rgb = vec3(0.392, 0.392, 0.392); } else if same_pid { rgb = vec3(0.549, 0.549, 0.549); }

The thread-focus greys, straight replacements of the palette colour. 0.392 and 0.549 are 100 and 140 on the 0–255 scale — the exact flat greys C++ Orbit paints scheduler slices with: the darker one for everything outside the selection, the lighter one for the focused process's other threads, so they still read on the cores.

These run before selection so a selected or sibling slice, decided next, still wins its colour back.

if sibling { rgb = SIBLING_RGB; } else if selected { rgb = SELECTED_RGB * (1.0 + pulse * PULSE_BRIGHT); }

Selection recolouring replaces the palette colour entirely rather than tinting it, so a selected scope is unmistakable regardless of its original hue.

The pulse contributes at most 5% brightness — visible as motion, never as flashing.

let top = v.pix.y < 1.15 && fill > 0.5; rgb = rgb * select(1.0, 1.08, top); rgb = rgb * select(1.0, 1.05, hover);

This is where o.pix from the vertex stage earns its slot. Within 1.15 pixels of the rectangle's top edge — and only where the shape is actually solid — brighten by 8%. A one-pixel bevel that gives a flat rectangle a hint of dimensionality.

Hover adds a further flat 5%.

var rim = vec3(0.92, 0.93, 0.95); var rim_w = 0.18; if selected { rim_w = 0.40; } else if hover { rim_w = 0.36; } rgb = mix(rgb, rim, border * rim_w * fill);

The rim highlight, using the border band computed earlier. Multiplying by fill confines it to the inside of the shape, so the outline reads as a lit edge rather than a drawn stroke.

The rim colour is very slightly blue — 0.92, 0.93, 0.95 rather than flat white — which keeps it from looking like a hard graphic outline.

return vec4(rgb, fill * v.color.a);

The line that makes the padded quad work. The alpha is coverage times the instance's own opacity, and nothing else. Inside the shape fill is 1, so the pixel takes the colour at full strength; out in the two-pixel margin fill is 0, so the pixel is transparent and the padding costs nothing on screen. Across the edge fill is fractional, which is the antialiasing.

This is where the old drop shadow used to add a second, softer term under a max. With it gone the margin exists only to keep that fractional edge from being clipped — the whole point of section 6's inflation.

Why there is no depth buffer

The pipeline sets depth_stencil: None, so nothing sorts by depth — later draws simply paint over earlier ones. That's why split_drag_instances exists on the Rust side: to make a dragged thread's scopes paint last, it moves them to the end of the buffer. Draw order is the z-order.


09 — ORIENTATION

Where to look

All paths below are relative to src/OrbitLiveViewer/.

FileWhat lives there
crates/orbit-live-render/src/shaders/instance.wgslThe SDF path — everything in sections 6 through 8
crates/orbit-live-render/src/shaders/blit_rect.wgslThe pixel-column path — section 3
crates/orbit-live-render/src/shaders/blit.wgslCompiled into the binary; never made into a pipeline
crates/orbit-live-render/src/shaders.rsThree include_str! lines and nothing else
crates/orbit-live-viewer/src/timeline.rsPipelines, bind groups, vertex layout, pack_instances, the draw call
crates/orbit-live-render/src/lod.rschoose_lod, lane heights, the FLAG_* constants

The shaders live in .wgsl files rather than Rust string literals specifically so they can be checked on their own:

naga crates/orbit-live-render/src/shaders/instance.wgsl --validate 15
# Validation successful
One limit worth knowing

naga --validate checks that the WGSL is well-formed. It does not catch the 16-byte uniform rule from section 5 — that's a downlevel-adapter capability, not a language violation, and the broken 24-byte struct validated cleanly right up until create_render_pipeline rejected it at runtime. Standalone validation is a real check, but it is not the same as building the pipeline.