Ollin

Ollin — the shipped-capability inventory

This is the per-feature inventory of everything Ollin ships today, split out of CLAUDE.md so the always-loaded guidance stays small. It is the authoritative answer to "does Ollin already do X, and what must not be broken about it?" — one bullet per capability, each a terse statement of the load-bearing invariants plus pointers to its example, snapshot, tests, and Docs/ page.

Where each kind of fact lives:

  • Here — what exists, and the regression-preventing invariants of each shipped capability (terse; a rule whose violation is a bug).
  • CLAUDE.md — vision, scope, and the cross-cutting policy that spans features (the architecture rule, conventions, sourcing & attribution, the shader/Metal and rendering-performance rules, the ship checklist).
  • ARCHITECTURE.md — engineering internals: the mechanism, the history, and the measurements behind a shipped system.
  • Docs/ — the user-facing API reference.
  • ROADMAP.md / DESIGN-NOTES.md — what is ahead, and why it is shaped that way. Never a changelog.
  • Guide/ — the narrative book that teaches the capability.

When work ships it lands here, as step (1) of the ship checklist in CLAUDE.md § Roadmap & design notes. Keep bullets terse and route depth to its home: explanation and history to ARCHITECTURE.md, user-facing API to Docs/, forward plans to ROADMAP.md / DESIGN-NOTES.md.

A few load-bearing rules recur across the satellites; they are stated once in CLAUDE.md § Current state rather than repeated per bullet (non-isolated render-thread closures, no defaulted resource:in: bundle, never replacing a GPU texture in place, satellites not depending on each other, and the @unchecked Sendable producer + OSAllocatedUnfairLock handoff shape).

Today (the shipped surface)

  • Lifecycle & input. Sketch base class; setup() may throw (open func setup() throws; an override may leave throws off, so every sketch compiles unchanged): a loader's FileError goes out with a bare try, the runner's performSetup turns it into one sentence on stderr and SketchRunner.setupError (readable afterwards, cleared by the next setup() that runs through, forwarded once through onSetupError), the window is held with nothing drawn (draw(in:) returns while the sentence stands, as a compile error leaves the stage) until a reload or an asset rerun sets up clean, every headless drive goes through runSetup() throws so an export fails with the sketch's own error, and the in-memory recorders (svg(of:), pdf(of:), gcode(of:), dxf(of:), drafting(of:), stitching(of:), embroidery(of:), spatialScene(of:)) throw with it; SketchSession.setupError sits between the Swift compile error and the shader error in errorMessage, and both hosts' chips turn red on it (SetupThrowsTests); temporal state (frameCount / time / deltaTime / frameRate); A canvas rebuilds its frame timer when the screen under it can have changed (OllinMTKView.restartFrameTimer: pause then resume on NSWorkspace.screensDidWakeNotification, NSApplication.didChangeScreenParametersNotification, and the window's NSWindow.didChangeScreenNotification / NSWindow.didChangeOcclusionStateNotification, never for a canvas the runner paused): a window opened while no display was awake got an MTKView timer that never fired and stayed blank for good (proven by hand on the lid-closed Studio Display: no frame before or after the wake, about 55 fps from the wake with the fix; a sketch already running when the display slept always recovered); ScreenWakeTests pins the four triggers, the paused case, and the teardown; mouse (mouseX / mouseY, the one-point mouse: Vector2 + previousMouse — snapshotted in advance because the window writes the pointer only when it moves, so mouse - previousMouse is a true per-frame delta; first frame's delta is zero, and in-draw remaps (view box, viewControl) never land in it — mouseIsPressed, mousePressed() / mouseReleased()) and keyboard (key / keyCode / keyIsPressed, keyPressed() / keyReleased(), isKeyDown(_:), typed KeyCode; moveAxis reads WASD + arrows as one -1...1 Vector2 in canvas orientation, up (0, -1), opposite keys canceling). Canvas edges read as shortSide / longSide beside the per-mille scale. The held-flag + release-hook pair makes drag-to-paint a draw() poll. Files dropped on the window arrive as String paths through the draining droppedFiles() (the house poll-and-clear read) with filesDropped() fired once per drop after the pointer has moved to the drop point, so a sketch responds at the drop or polls in draw(); the canvas view (OllinMTKView) registers for file URLs and answers draggingEntered/performDragOperation, mapping the drop through the same canvasPoint the mouse uses (so a projected run reads the drop in its own canvas), and a drop is live input outside a take (neither recorded nor replayed, since a path is machine-bound). Every host takes it wherever the sketch runs; on the performance stage the editor takes a drop as text while the code shows. DropTests; Examples/Images/Dropped. Examples Input/Drag (grab, per-frame mouse - previousMouse, release, and a fling thrown at the hand's averaged speed) + Input/Keys (moveAxis polled in draw(), keyReleased() stamping a ring). What a sketch asks of its window goes through one internal seam, Sketch.host (SketchHost, answered by SketchRunner, nil in every export, so each ask is a no-op there by construction). redraw() wakes a noLoop() sketch for exactly one frame: SketchRunner.drawsOneFrame unpauses the timer and the frame that draws hands the pause back (unless camera work owns the pause through pauseHoldover); a refresh that draws nothing (the ring full, a held frame shown) leaves it set, so the frame is late rather than lost; asked inside setup()/draw() it is spent by that frame, so a redraw() in draw() cannot loop; it passes the timeline's held clock with a zero step. isExporting forwards to OllinApp.isExporting (isRenderingHeadless && !isBenchmarking, the same predicate Drawer.runsOnFixedClock now reads), true through setup, warmup, and written frames of every headless drive (still, frames, vector, spatial, web, EXR, contact sheet, widget, each pinned) and false live and under --bench. The pointer over the canvas (pointerShape(_:), hidePointer()/showPointer(), PointerShape over the system cursors) is a cursor rect on OllinMTKView alone, never NSCursor.hide(), so a host's sidebar keeps the arrow and installation mode's app-wide hide count is never touched; shape and hiding are separate state (a shape set while hidden is the one shown again), a call that changes nothing is one comparison, and the default arrow adds no rect at all. chooseFiles(withExtensions:allowsMultiple:) shows NSOpenPanel as a sheet over the sketch's window (a free panel without one), so the run loop and the frames keep going; one panel at a time; the pick goes to the instance drawing when the panel closes through the draining chosenFiles() and the filesChosen() hook, live input outside a take like a drop; extensions are normalized (dot, case) to UTTypes and an unknown one still matches by name. A swap re-points the canvas at the new instance (wireLoopControl sets OllinMTKView.sketch and its pointer): before, every click, key, pointer seed, and description after a live reload reached the first-mounted instance, since mouse input landed (the reload test verified red by sabotage). SketchConvenienceTests (redraw counted one frame per wake, sabotage red; isExporting through every drive and the bench exclusion, sabotage red). Docs/Core/Sketch.md, Docs/Helpers/Input.md.
  • Rendering. Drawer + Metal renderer over several pipelines recorded into call-ordered batches (so SDF / triangle / fringe / image / glyph / 3D geometry composite front-to-back as drawn — never collapse into unordered passes): a tessellated-triangle path (libtess2 fills + 8×/4× MSAA), an instanced-SDF path, a fringe-stroke path (the high-quality stroke renderer), a textured-quad image path, and a glyph-atlas path. Gamma-correct linear-light compositing in a rgba16Float intermediate that a final present pass tone-maps + dithers to the sRGB drawable. The dither (ditherEncoded, one helper for the opaque present, the alpha-keeping one, and the web page's tail) reaches at most one level, and within a level of black or white only distance + 0.3 levels, so exact 0 and exact 255 never move (a two-level picture exports two-level, a black ground feeds no noise to a video encoder) and nothing near an end leans on the clamp; 0.3 sits under the 0.41 level at which this GPU's 8-bit sRGB encode already rounds black up (measured: full reach moved 17.6% of black and 12.2% of white). Black reads up to a quarter level high from about 0.5 to 1.5 levels with or without the limit, which is that encode's straight segment, not the dither. PresentDitherTests (3: exact ends, the middle still dithered, the mean within 0.4 of a level of either end). blendMode(_:) (.normal / .add / .subtract / .multiply / .screen / .lightest / .darkest) is stack state carried as a Pipeline cache-key parameter, not a renderer-per-mode. HDR / tone-map — toneMap(_:exposure:) (.clamp default / .reinhard / .aces / .neutral), a frame-wide mode over the float pipeline (Docs/Drawing/HDR.md). .neutral keeps a bright color's hue (the Khronos PBR Neutral curve, toneMapNeutral in the present library block, so the web page's present pass carries it through the translator): the per-channel curves turn a bright saturated color toward a primary (linear (4, 1, 0.2) is an orange at 12.6°; .aces shows it at 45°, .reinhard at 32°), where the neutral curve scales the three channels by one factor and holds it within a degree; below its knee (0.76) a color keeps its hue and loses only the curve's toe (0.04 at most). ToneMapTests (the hue, the toe), FrameInvariantTests carries its scalar form; the Guide figure ToneNeutral beside the clamp and ACES pair; Rendering/ToneMapping cycles four. Accumulation surface — noClear() / clearEachFrame(), a persistent float canvas drawing piles onto; background(_:) is the in-mode wipe (Docs/Drawing/Accumulation.md). noClear(precision: .float32) makes the MSAA target and resolve rgba32Float (Drawer.accumulationPrecision, MetalRenderer.accumFormat; a change reallocates and clears like a size change) and the canvas encode keys every pipeline on it through its canvasColorFormat argument; the two readers that assumed half float follow it: the frame grab's mip chain takes the source's format, and the linear capture copies 16-byte rows and narrows them to half on the CPU (LinearCapture.colorIsSingle). LightAccumulationTests.singlePrecisionPileKeepsAdding / aLongFaintExposureReachesItsSum (0.25 + 300 x 8 x 0.00005: float32 reads 0.37, half float holds 0.25), EXRExportTests.aSinglePrecisionPileIsWrittenAtItsSum. The EXR writer clamps alpha to 1: additive marks sum their alpha with their light, so two added coats on an opaque canvas held A = 3. The SDF path covers a broad analytic-shape catalog (drawPoint / drawCircle / drawEllipse / drawRect / drawOrientedBox / circular drawArc / drawTriangle / drawNgon / drawStar / drawRing / drawHeart / drawMoon / … + hollow(_:) band mode, strokeAlign(_:), area-conserving disk + outline coverage; a rectangle's corners round one radius each through cornerRadii: CornerRadii (.top(r) / .bottom / .left / .right / the four named), the box reading param0 = (top-left, top-right) and param1 = (bottom-right, bottom-left) whenever any of them is set and extra otherwise, so four equal radii collapse on the CPU to the cornerRadius: instance byte for byte (RoundedCornersTests) and every box drawn with one radius keeps its old distance; the per-corner SDF picks the quadrant's radius then runs sdRoundBox's own expression; radii are fitted before they draw, all four scaled by one factor where two on a side add up past it, the rule stylesheets use; the dashed outline, the hollow band, the combinator leaf (SDF.rect(width:height:cornerRadii:)), the SVG (<path> with an A per rounded corner, <rect rx> kept for one radius) and the PDF (tangent arcs) all read the same four); the fringe path covers every stroke (drawLine / drawBezier / drawPolyline + polygon/shape/elliptical-arc outlines, honoring strokeJoin / strokeCap / strokeProfile, translucent + gradient paint; drawArrow(from:to:headLength:headWidth:) composes a shaft on this path with a solid SDF-triangle head, the whole mark painted with the stroke so one color states the arrow — head defaults scale with the weight, shaft stops at the head's base so translucent arrows lay one coat, and composition over the existing primitives is what makes SVG export inherit it — catalog figure Docs/Figures/DrawArrow.swift, example Shapes/Arrows (a field aimed at the cursor, plus a translucent ring sweeping the head geometry — the one-coat property drawn), Docs/Drawing/Drawing.md#arrow); the triangle path covers fills (drawShape for concave/holed, curved via the Path builder + drawCurve; drawPolygon is a fan and fills convex only — hand it a concave outline and every dip is bridged straight across, so an area chart or any dipping profile goes through drawShape) and elliptical/full-arc fills. Collection sugar: drawCircles / drawPoints / drawRects. Kaleidoscope symmetry — symmetry(_:mirrored:) / noSymmetry(), stack state: every 2D draw call replicates through canvas-space fold matrices (each local rotation/reflection conjugated by the CTM at the symmetry call — fold 0 stays the exact identity, so the primary copy is byte-identical). Load-bearing: replication is per emission funnel — SDF instances and combinator groups left-multiply their per-instance transform (groups share one node program), the triangle/fringe/image/glyph paths range-copy canvas-space vertices via Drawer.replicated whose body must stay within one batch kind (wrap a fill section and its stroke separately; isReplicating passes a nested funnel through, and folds are rigid so screen-space AA/fringe widths stay exact), and svgRecord replicates commands so --export-svg folds too; 3D geometry, GPU particles, and depth scenes are untouched by design. Example Patterns/Kaleidoscope (declares loopDuration); snapshot symmetry; SymmetryTests. Clipping — withClip(shape/rect/circle) { }, the standard stencil clip (increment/decrement): the region triangulates through the fill path into a stencil-only .clipPush batch (CTM baked at the push), content batches carry a clipLevel and stencil-test equal to it, and the block's end appends a fullscreen-decrement .clipPop; nesting intersects. Load-bearing: a stencil8 MSAA attachment (memoryless unless the pass is heavy, below) joins a pass only when that surface clips (Drawer.usesClipStencil / RenderTarget.needsStencil — an unclipped frame is byte-identical), and in a stencil-carrying pass every pipeline key must declare stencilFormat (skybox and raymarch-upsample included) or Metal validation rejects the draw; never pass setDepthStencilState(nil) (validation rejects nil — use noDepthState); clipping is per surface (a layer opened inside a clip block starts unclipped); background(_:) inside a clip block re-emits the open pushes after the wipe (reemitClipPushes); clip edges AA at MSAA resolution (the clip shape rasterizes binary per sample); --export-svg emits native <clipPath> defs + nested <g> groups. Example Shapes/Clipping; snapshot clip; ClipTests. A refresh the GPU has no room for is dropped, not waited on — MetalRenderer.canStartFrame (a frame count kept in lockstep with the frameBoundary semaphore, since a semaphore can only be asked by waiting on it) is read at the top of SketchRunner.draw(in:), before advance/performDraw, so a heavy sketch gives a low frame rate and a window that still answers the mouse instead of both stopping (the drop is early, so nothing is drawn and discarded and an accumulating canvas keeps its ink; the clock is real time, read on the next refresh that lands). A take is the exception, playing or recording: a replay consumes one recorded frame per refresh. FrameRingProbes (Tests/OllinTests/FrameRingTests.swift) pins the three halves (the ring reports full and empties, a full ring drops the refresh, a take keeps every one), the drop test verified red by sabotage. The load-bearing renderer rules (the vertex-buffer ring, the SDF tagged union, coverage models, the present/float pipeline) live in Shaders & the Metal back end + Rendering performance above. Docs/Drawing/Drawing.md. A heavy pass takes backed attachments (Renderer/PassStorage.swift): a pass's MSAA color, depth, and stencil are memoryless unless its counted geometry (Drawer.primitiveCount(on:), a quad per SDF shape, particle, or splat, a third of every vertex list, the copies of an instanced mesh) passes MetalRenderer.backedAttachmentPrimitives (1M triangles), then .private. A pass the tiler cannot bin whole needs a partial render, which stores the attachments and loads them back; a memoryless attachment cannot be stored, so the GPU refuses the whole command buffer ("Too much geometry to support memoryless render pass attachments") and the frame came back empty with nothing said (a LineSpray past ~9.5M points a frame on a 256-pixel canvas). Measured edges on the M2 (4x, half float): one-pixel marks 7.7M (4096²) to 10.6M (64²), 40-px 3.6M, 200-px 0.56M; private drew 40M. Private costs the memory behind the samples and nothing else below the edge (GPU time equal to within 2% from 0.2M to 5M marks), and the pixels are the same. Every finished frame's command buffer is read (GPUFailures, from both live completion handlers and commitHeadless): a failed one writes one stderr line per distinct sentence, and an overflow turns every later pass private (GPU-grown geometry, mesh fields, strands, the ocean, is not counted and is caught that way). Pools and caches (targetTexPool/targetDepthPool, mainMSAA/mainDepth, the clip stencils, the glass pass) are keyed by storage too. HeavyPassTests (ten million marks on the canvas and in a layer, red with the budget at Int.max). What a long run keeps is counted and held flat: MetalRenderer.census and Drawer.census count every cache, pool, and slot table either keeps across frames, and SoakTests drives 20,000 live frames that churn all of them and fails when a count in the last half passes the most it reached by frame 4,000 (a margin for caches that fill and clear on a cycle; a planted texture a frame verified red). A new cross-frame cache goes into its census. leaks, run at exit over headless exports, is the other half (Tests/README, What a long run leaves behind). The blobby cross's distance is floored by the diamond through its four tips (sdBlobbyCross, the shader and the CPU copy alike): past a tip the formula clamps its closest point to the parabola's end and reported a fraction of the distance there, so the coverage ramp drew a faint spike from every tip to the quad's edge; and below a blobbiness of about 0.35 the tip sits at the parabola's end, so the mapping caps tipUnit at 1 and the shape reaches its radius.
  • Wide gamut & HDR output (Sources/Ollin/Color/ColorOutput.swift + the present twin in Renderer/ShaderEffects.metal). Sketch.colorOutput (declared, like canvasSize, since the drawable is built from it once) opens the last step of the pipeline: .standard (8-bit sRGB, the default), .wide (Display P3 through an rgba16Float drawable), .extended (+ values above 1 shown as highlights, and an HDR10 video export). Color(displayP3:green:blue:) / displayP3Components / isOutsideSRGB name a color outside sRGB, stored as the extended sRGB components that decode back to it. Load-bearing: the wide/HDR present is a twin fragment (ollin_present_wide_fragment) beside the byte-for-byte-untouched ollin_present_fragment (the fast-math re-contraction rule), and the new OllinPresentUniforms fields are appended so the shipped fragment's reads keep their offsets — proven by a clean-HEAD worktree double record (185/185 identical); no out-of-sRGB authoring needed a shader change because the shipped srgbToLinear already decodes a negative component through the curve's straight segment, so the CPU encode (linearToSrgb, c*12.92 below the knee) is its exact inverse and a P3 color survives the ordinary vertex path; PresentEncoding distinguishes .linearDisplayP3 (stops at white) from .linearDisplayP3Extended (starts unbounded, so a headless render keeps the highlights an exported HDR video is for, with a live host pulling the ceiling down to the screen's reported headroom every frame — the system grants and withdraws it); tone-map curves and .extended pull against each other by construction (.reinhard/.aces squeeze into 0…1, which is what an HDR display does not want), so the default .clamp is the right one there; HDR video is PQ-encoded on the GPU into the same float texture and handed over as kCVPixelFormatType_64RGBAHalf (VideoToolbox writes 10-bit HEVC Main10 with kVTHDRMetadataInsertionMode_Auto supplying the mastering-display + content-light metadata), 1.0 = 203 cd/m² (BT.2408 reference white), peak 1000; a still keeps the range too, in HEIC (--export frame.heic; the path's extension picks the format; PNG clips at white). The files keep their depth (Export/FrameBytes.swift): a float frame is quantized at the file, never by a Core Graphics conversion (which rounds a slow gradient into bands and may bend a frame past white rather than clip it): a PNG at 16 bits and the HEIC's picture at 16, which the writer stores as 10 (ruled 2026-10-17: within 0.4% of the frame, no banding, and smaller than a dithered 8-bit picture, which sat two levels off on a dark pixel), both under the Display P3 curve and clipped at white; the 8-bit sinks (a .wide clip's frames, a wide window's recording) take the present's TPDF dither on the CPU (a curve table indexed by each half's bits, the same end rule so 0 and 255 stay exact, a position hash so an export repeats). aWidePNGKeepsSixteenBits, aWideHEICKeepsTenBits, anEightBitFileOfAWideFrameIsDithered (the wide ProRes clip's gradient: 3 runs to 1,006), SessionRecorderWriterTests.aWideTakeIsDithered, each red under its plant. Export/GainMapExport.swift writes an ISO 21496-1 gain map beside a base picture that is the frame clamped at white (what the PNG carries), returning OllinApp.StillExport (peak, keepsHighlights). Load-bearing: the map is built here, never derived by Core Image, whose ceiling comes from frame statistics and so deletes a small highlight (measured at 4x white: 0.1% of the frame → 0.99x back, 1% → 1.75x, 5% → 3.63x, 25%+ → exact) and which ignores a declared contentHeadroom despite its header saying it drives the calculation (the value is live: the tone-map filter reads it); a ready-made map passed as .hdrGainMapImage writes the legacy Apple aux type, not the ISO one, so the file is assembled with ImageIO directly; the map carries a gain per channel because clamping is per channel (an amber core of (4.0, 2.2, 0.72) clamps to (1, 1, 0.72): one channel for all three read green +81% / blue +300%, three read within 0.6%); the base is written at quality 1.0 (within a level of its bytes, measured) so a still stays as faithful as the PNG; a frame at or below white carries no map and says so. Reconstruction is (base + offset) * 2^(e * GainMapMax) - offset; the file keeps the map's color at half resolution (4:2:0), each sample at the left of its block, and a per-channel gain is that color, so the map is written at four times the frame's size (GainMap.mapScale; twice once the map would pass 24 Mpx): a highlight's antialiased rim rebuilt 40% off at 1x, 4% at 2x (a quarter of the neighbor's color), 1.7% at 4x (2.1% on the test's drawn rim; aGainMapKeepsAHighlightsEdge, red at 1x and at 2x). GainMapExportTests (12; the per-channel map and the true-peak ceiling both verified red by sabotage). Spatial video stays .standard. Sketch.displayHeadroom reads what the display actually granted (1 headless). The float drawable is cheaper, not dearer (M2 release, paired runs: 1080² present 0.44 → 0.23 ms) because the 8-bit path's dither + transfer curve outcost the extra bytes, so the case against .wide as a default is consumer compatibility and re-record churn, never speed. Example Rendering/ColorOutput; ColorOutputTests (10, the gamut-matrix sabotage verified red); no pixel snapshot beyond the byte-identity gate (a wide frame's whole point is invisible in an 8-bit reference). Mechanism + the measurement: ARCHITECTURE.md § Wide gamut and HDR output. Docs/Drawing/ColorOutput.md; Guide Ch 41 § Brighter than white: HDR output.
  • Spectral color (Sources/Ollin/Color/Spectrum.swift + Renderer/SpectralTaps.swift; data vendored in External/CSpectralData, MIT — see THIRD-PARTY-NOTICES.md). Spectrum — 81 samples, 380–780 nm at 5 nm, read as reflectance in daylight: a color's curve is its linear r/g/b times three basis spectra (a partition of unity, so white is flat 1), and the return conversion goes through the matrix inverted from those same tables, which is what makes color→spectrum→color exact to float rounding rather than to the data's fitting error. .paint joins ColorSpace (Kubelka-Munk over the curves, so Ramp(in: .paint) and every mix surface get it too); mixedAsPaint(with:_:) is the typed form; */+ filter and add spectra; Color(wavelength:) (peak-normalized locus) and Spectrum.blackbody(_:) are the physical doors. GPU trio: Filter.thinFilm (true two-beam interference per tap, thickness in real nanometers; .iridescence stays the styled sibling), Filter.diffraction (grating orders, offset ∝ λ so red reaches farther; premultiplied accumulation, so streaks pass a silhouette; tap colors sum to white by construction, so a flat field cannot tint), Combine.paintMix (the GPU form of .paint; the aux's coverage gates the mix per pixel, so an uncovered pixel passes the base bit-exact — note a never-rendered aux target is pooled garbage, a pre-existing combine-wide fact, which is why the guarantee is per-pixel coverage, not per-target). All three read a cooked tap block (SpectralTaps.block, tap count via resolveDispersionTaps) whose XYZ→RGB inverse is computed from the taps themselves, keeping zero-amount and equal-input identities at every quality tier; both spectral filters also early-out at amount 0 (the premul/unpremul round trip alone costs a level). KM clamps reflectance to [1e-4, 1−1e-4]; a near-black saturated paint legitimately drags a mix toward dark, as on a real palette. Examples Color/Mixing (six bands) + Effects/SoapFilm + Effects/PigmentMix (.paintMix beside .mix over one pair of layers, both combine spellings); snapshot spectral-color; tests SpectrumTests (11, pure) + SpectralEffectsTests (7, Metal); Docs/Drawing/Spectrum.md; Guide Ch 2 § Blue and yellow make green: mixing in .paint.
  • Variable-width strokes (Sources/Ollin/Drawing/StrokeProfile.swift + the expander in Core/Drawer+Tessellation.swift). strokeProfile(_:) / noStrokeProfile() is stack state shaping width along a path: the StrokeProfile value type (.uniform / .taper(start:end:) / .ramp(from:to:) / .nib(angle:thinness:) / .values(_:) + the closure forms) returns a multiplier on strokeWeight, read at path fraction 0…1 plus the local direction (what .nib uses). The fringe expander carries a half-width per path vertex (Drawer.strokeHalfWidths), so a segment quad becomes a trapezoid, exact for a linear ramp along a straight run. Load-bearing: width is sampled per path vertex, not per segment, which is why joins stay simple (both segments at a corner agree on the width there, so a corner is the constant-width corner problem at the local width) and why joins/caps just read their own vertex's width; a non-uniform profile subdivides like a gradient does (maxLength: 12) since the profile can only follow the points it's given; .uniform is the untouched path (isUniform gates the expander, the outline builder, and the vector recorder) and the coverage ramp is the shipped one either way, so a profiled and an unprofiled stroke of the same width lay down the same ink and every existing snapshot is unchanged; the analytic SDF shapes carry one width by construction and ignore a profile with a one-time noteOnce. Vector export stays true: svgRecord routes a profiled stroke to variableStrokeOutline (trapezoids + join wedges + caps, each wound the same way into one nonzero-wound Shape, so the union needs no boolean work), recorded as a filled path rather than a stroke-width; PDF replays the same commands. Example Shapes/StrokeProfiles; snapshot stroke-profiles; StrokeProfileTests (the family, arc-length sampling, outline winding + area, rendered taper falloff). Docs/Drawing/Drawing.md#strokeProfile.
  • The stylus beyond its press (Sources/Ollin/Core/Stylus.swift; Sketch.stylus, the tablet reads in SketchView.swift, the .stylus case in Take.Event). What a tablet says about a stylus other than where it is and how hard it is pressed, which was already pressure and reaches a trackpad too: stylus.tilt is the lean, -1…1 per axis and .zero upright, whose direction is what a broad-edged nib is laid across and whose length is how far from upright the pen is; stylus.twist is an art pen's barrel turn in radians, folded into one lap; stylus.isEraser is which end is down; stylus.isNearby covers hovering as well as drawing; stylus.tiltIsAvailable answers from the events the way pressureIsAvailable does. Load-bearing: the tablet fields on NSEvent raise when read off any other event, so the guard checks the type first and reads subtype only on the four mouse kinds that have one (|| and && short-circuit, and that is the whole protection); a tablet's motion arrives as an ordinary mouse event with the tabletPoint subtype while proximity arrives on its own, and pointingDeviceType is readable only on the proximity event, which is why the end being used is carried forward rather than re-read; tiltIsAvailable latches on (a stylus lifted out of range would otherwise take the pen path away mid-stroke), while a lift zeroes the lean and the turn so nothing stale is read as current; the pen and the pressure never write each other. Recorded into a take and replayed with the rest of the input (Take.Event.stylus, the file version unmoved since an old take still decodes). Six tests in StylusTests: the lean is held to its range and the turn folded to one lap (a non-number reads upright); a sketch with no tablet reads as one; the pen path stays offered once a pen has been seen and a lift says so without forgetting; the erasing end is its own thing; the pressure and the pen are separate; a take carries the pen through a replay exactly. The latch and the replay were both verified red by sabotage. The AppKit mapping itself is desk work: no test can synthesize a tablet event, so the tilt, the turn, the eraser end and the proximity edges owe a hand with a stylus. Examples/Input/Pen (a chisel nib across the lean, the press for width, the eraser end taking ink away, and a panel that says what the tablet is sending); Docs/Helpers/Input.md § pen; Guide Ch 17 § Painting as it happens: stroke dynamics + the NibAndLean figure.
  • Pinch and turn (Sketch.pinchScale / turnAngle, the hooks fingersPinched() / fingersTurned(); reportGesture in Core/SketchView.swift, the phone's pair in PhonePointer). Two fingers on the trackpad as this frame's change: pinchScale is a factor, the product of 1 + magnification over every magnify event since the last frame, 1 when still, and turnAngle is radians clockwise on the canvas, the sum of each rotate event's counterclockwise degrees negated and converted, so angle += turnAngle; rotate(angle) follows the fingers. Both are gathered between frames and surfaced in advance() the way the scroll is, and a hook sees its own event's value. An event reaches the sketch once: the SwiftUI hosts' gesture plumbing can take gesture events before responder dispatch (it takes pressure), so a local monitor over .magnify/.rotate, gated on the window and the view's bounds, reports beside the magnify(with:)/rotate(with:) overrides, and the last reported type and timestamp is skipped; an event that moves nothing (a gesture's start or end) is not reported. A factor at or below zero is held at 0.01 and a non-finite value is dropped. Takes record .pinch(scale:) / .turn(angle:) (the format version is unchanged: the cases are additive). viewControl() zooms by the pinch about the pointer (one zoom * pow(base, scroll) * pinch, so a scroll alone is byte-identical) and the orbit rigs dolly by goalRadius / pinchScale; both count a pinch as the viewer taking over. The phone showing the sketch (PhoneScreen) reads its first two fingers as a pair: a reading with the same two ids as the last, moved, is .pair(from:to:), a changed pair starts over so nothing jumps, and the change is measured on the canvas (Pair.change(to:in:), the turn wrapped the shorter way) and applied only in Sketch mode. The sketch installed on the phone (OllinMTKView, multi-touch off) reads one finger only. PinchAndTurnTests (real NSEvents from a gesture CGEvent: type 29, field 110 the HID type, 8 magnify with the value in field 113, 5 rotate in 114), PhonePointerTests, PhoneTouchTests; Examples/Input/PinchAndTurn; Docs/Helpers/Input.md#pinch; Guide Ch 38 § Two fingers on the trackpad: pinch and turn. The live window's route through a SwiftUI host is a desk check: the screen was locked when this shipped.
  • Typed text (Sketch.typedText() / textTyped() / composingText / caretPosition, String.append(typing:); Core/TypedText.swift). The keyboard read as a text field reads it: OllinMTKView adopts NSTextInputClient (an isolated @MainActor conformance) and hands every keyDown to interpretKeyEvents(_:) after the key hook, so the system's input methods compose a dead key's accent, an input method's confirmed word, and the emoji viewer's pick into insertText, and a Return that confirms a composition still finds composingText holding it. key/keyPressed() are byte-identical (a repeat still fires no hook; it types again). A Command chord types nothing (a shortcut no menu claimed); inserted text keeps \n (a carriage return reads as one) and \t and drops other control characters; the editing commands a text field answers by editing are typed as characters (insertNewline: and its kin \n, insertTab: \t, deleteBackward: the backspace \u{8} that append(typing:) reads as taking a character away), and every other command is swallowed without the beep. The client's only document is the composition still under way (markedText/markedSelection on the view): selectedRange is {0,0} with nothing marked, markedRange {NSNotFound,0}, unmarkText commits what was shown, and resignFirstResponder drops a half-written composition with nothing typed (abandonComposition, which also discards the context's). firstRect places the candidate list below caretPosition (canvas coordinates through viewRect, so a fitted projection is honored) or below the pointer. Takes record .typed(text:) / .composing(text:) (additive; the format version is unchanged) and a replay gates live text off. A headless export types nothing; on iOS nothing types yet. TypedTextTests (the client driven by insertText/setMarkedText/doCommand, a real repeat through keyDown, the take); Examples/Input/TypedText; Docs/Helpers/Input.md#typed-text; Guide Ch 38 § Text the way a text field takes it: typed text. The input system composes only for the active app's key window, so a test process cannot drive the layout's own dead keys (the context accepts the event and composes nothing: probed with a real Option-E); the real route (a dead key, a Japanese input method, the emoji viewer) is a desk check.
  • Stroke dynamics (Sources/Ollin/Drawing/StrokeDynamics.swift + StrokeMark.swift). The width-profile sibling driven by how a mark is made rather than where you are along it: a StrokeMark records the pointer (record(_:deltaTime:pressure:), sugar Sketch.record(into:)) measuring speed and press force, runs both through a StrokeDynamics (one StrokeResponse per axis over a StrokeInput of speed/pressure/direction/distance), and keeps the resulting width + opacity at each point; drawMark(_:) strokes it. Load-bearing: a profile can't express this because a mark in progress has no length to take a fraction of, and a recorded hand's points are not evenly spaced (dense where it slowed), so .values(_:) genuinely doesn't fit and StrokeProfile.values(_:at:) (values at given fractions) is the general form drawMark builds; speed is measured over real elapsed time with skipped sub-minSpacing frames banking their dt (frame-rate independence, pinned by recordingIsFrameRateIndependent), smoothed by a OneEuroFilter per axis fed a scaled signal (a px/s speed runs in the hundreds, and the filter's beta is in the signal's units); the two axes are separate and each defaults to .unchanged (a shared preset with per-axis defaults silently drove both, the bug the first probe caught); the first point is held until a second gives it a heading; the ambient strokeProfile multiplies with the mark's widths; the mark's opacity is carried in the existing per-vertex color alpha via an internal strokeOpacityShape (.uniform default → every other stroke byte-identical, and a .uniform mark is byte-identical to drawPolyline). Pressure is plumbed through the one platform-neutral input surface (Sketch.pressure / pressureIsAvailable, capability latching on the first press), so it reaches standalone + gallery + OllinLive — but not via pressureChange: the SwiftUI hosting layer consumes .pressure events before responder dispatch and its recognizers override the view's .primaryGeneric with deep-click (reads ~0 across the whole normal force range), so OllinMTKView feeds a view-owned local .pressure event monitor and mouseDown re-claims the stream with pressureConfiguration?.set(); never read associatedEventsMask on a .pressure event (the access raises and AppKit swallows the raise, silently dropping the report — a pressure event is itself the capability proof), and mouse/drag events from a pressure-capable device seed zero, then go quiet once the stream is live (their pressure field is the legacy constant 1). Mechanism: ARCHITECTURE.md § Trackpad pressure delivery. Vector export keeps width exactly (the profiled filled-outline path) and flattens opacity to its length-weighted average with a noteOnce (one fill, one alpha per path; free for the one-pen plotter case). Example Shapes/Brushwork (class renamed off StrokeDynamics to dodge the SlitScan shadowing trap); StrokeDynamicsTests (mapping, measurement, arc-fraction placement, three rendered probes, no pixel snapshot since dynamics is interactive). Docs/Drawing/Marks.md.
  • Brushes (Sources/Ollin/Drawing/Brush.swift + Core/Drawer+Brush.swift). The third mark axis beside the width profile and the recorded mark: strokeBrush(_:) / noStrokeBrush() is stack state that replaces the ribbon with a row of stamps. The Brush value type carries a tip (.circle / .square / .shape(Shape) / .image(Image)), spacing, sizeJitter, angle (.followPath / .fixed / .random), angleJitter, scatter, count, opacityJitter, seed, plus the .round / .chisel() / .spray() / .scatter() presets; size comes from strokeWeight and color from stroke, so a brush is texture only. Load-bearing: every distance is a fraction of the stamp's own size, which is what keeps a brush's texture constant across weights (pinned by spacingScalesWithTheStampNotTheCanvas); stamps are placed by arc length, not per vertex (a recorded mark's points bunch where the hand slowed, and a brush must not bunch with them); randomness is a pure function of stamp index + seed via a private SplitMix, never the sketch's rng (the halton/sobol rule), so adding a brush cannot shift a sketch's other rolls; the ambient strokeProfile / strokeOpacityShape are read per stamp and multiply, and the step is taken at the local size so a tapering mark crowds rather than going gappy; a stamp draws through the ordinary draw methods (drawCircle / drawOrientedBox / drawShape / drawImage) with fill/stroke/brush swapped and restored, so vector export, symmetry, clipping, and retained batches all work with no knowledge of brushes (and recursion is impossible); strokedAsBrush(_:closed:) is called ahead of every stroke entry point's svgRecorder short-circuit (drawLine / drawBezier / drawPolyline / drawMark / drawArc, and drawShape / drawPolygon which re-enter fill-only then stamp) or the vector path would silently export the bare path instead of the stamps — the appendFringeStroke hook is the catch-all for internal callers; a brushed drawMark keeps its varying opacity in vector export (each stamp is its own shape) where the ribbon must flatten it; opacity over a gradient stroke is the one thing a brush cannot vary (it would mean rewriting the ramp) and notes once, as do the analytic SDF outlines (the strokeProfile precedent). Brush is not Sendable/Equatable (an Image tip is neither). Example Shapes/Brushes; snapshot brushes; BrushTests. Docs/Drawing/Marks.md#brushes.
  • Dashed strokes (Sources/Ollin/Drawing/StrokeDash.swift + the cut in Core/Drawer+Tessellation.swift). strokeDash(_:) / noStrokeDash() is stack state cutting every stroked path into dashes: the StrokeDash value type (lengths as dash/gap/dash/gap in the path's own units, an odd count repeated the way SVG reads one, phase sliding the pattern along, .dashes(_:gap:) / .dots(spacing:), period, isSolid) and Contour.dashed(_:) / Shape.dashed(_:) as the same cut on geometry. Load-bearing: the cut happens once, at the two stroke funnels (strokeRuns inside appendFringeStroke and appendBrushStamps, never per entry point), so every path stroke, stroked text, and a brush dash through one rule; each run carries fractions (where its points sit on the whole path), which strokeHalfWidths, the along-path color, and a brush's width/opacity (Drawer.fractionMap) read, so a profile or gradient runs on across the gaps rather than restarting per dash (aProfileReadsTheWholePathAcrossTheGaps); a positive phase carries the dashes forward (the pattern position at arc length s is (s - phase) mod period, the opposite sign from SVG's dashoffset, so phase: time * k marches); a closed path's seam dash is merged (last piece + first) so the join at the first point stays a join, and one dash covering the whole loop leaves it closed; a zero-length dash is nudged 1e-3 along the path so its caps have a direction (a dot under .round, nothing under .butt), and a zero-length dash exactly on the first or last point counts; the analytic shapes with a polygonal outline hand it over through withDashedOutline (circle/ellipse via SDFOutline.ellipse sampled at circleSegments(for:), rect via SDFOutline.roundedRect, the three triangles, ngon, star, trapezoid, parallelogram): the fill stays the instance with the stroke cleared and the outline expands as a closed path, so a dashed ring also takes a profile and a brush for as long as the dash is on, while the traced shapes (heart, moon, ...) keep their band and noteOnce; svgRecord makes the same cut (dashCut, the same SDFOutline samplers for the two analytic elements) and writes the dashes as one <path> of open subpaths, the fill recorded first as the element it was (a plotter lifts the pen at each M; PDF and G-code replay the same commands, strokePaths reading the open contours), and dash + profile goes through variableStrokeOutline(fractions:); each dash records its own WebSource, so the page expands them as the Mac does; a pattern past maxDashesPerPath (20,000) on one path draws whole and notes once. No dash is the byte-identical path (strokeRuns hands the input back as one run with fractions == nil). Example Shapes/DashedStrokes; snapshot dashed-strokes; StrokeDashTests (the pattern rules, the cut, the seam, dots, the profile across gaps, the shape handover, the SVG subpaths, web sources, and three rendered probes). Docs/Drawing/Drawing.md#strokeDash.
  • Instanced meshes (drawMesh(_:instances:) + MeshInstance in Sources/Ollin/3D/MeshInstance.swift; the .meshInstanced recorder in Drawer.swift, the encode arm in MetalRenderer+Effects.swift, the shadow legs in MetalRenderer+Targets.swift, ollin_mesh_instanced_* in Shader3D.metal). One mesh, many placements, ONE draw: the base mesh expands once per call into its own frame array/ring (LOCAL space, model NOT baked — the per-mesh path's expensive per-copy re-bake is the thing this removes) and each copy is an OllinMeshInstance (OllinShaderTypes.h: model float4x4 + tint, stride 80) applied per vertex on the GPU; the [MeshInstance] form composes the CTM per copy at record time (T·Rx·Ry·Rz·S, pinned bit-exact against the equivalent translate/rotateX/Y/Z/scale sequence), the GPU-resident form (ComputeBuffer<OllinMeshInstance>, carried in the batch's particleBuffer like the point-cloud path) is absolute world space by design (the kernel owns placement). Load-bearing: the instanced vertex emits the solid path's own MeshOut and reuses ollin_mesh_fragment, with the whole lit binding set shared through the bindLitMeshFragment local function in the encode loop (extracted from the .mesh3D arm — the two arms must never drift), so copies take fill/finish (PBR included), lights, shadows received, IBL, GI, and fog identically (pinned within tolerance by the A/B render test); the normal transform is the adjugate of the model's linear part, computed in-shader (exact under non-uniform scale after normalization) — deliberately no normal matrix in the struct, so a compute kernel fills only model + color; batches carry explicit vertex/instance start + count (never next-batch run-length arithmetic — no other batch creator records these starts); copies cast into the 2D shadow map (.meshInstancedShadow) and the point cube (instanceCount = copies × 6, face = iid % 6, copy = iid / 6; the layered-topology gate in makeShadowPipeline keys on pointShadowOp != 0 because both point-shadow vertices write render_target_array_index), and a frame whose only casters are instanced still renders its maps (the shadow-pass guard takes instanced casters as geometry); the [MeshInstance] form is added to the traced scene as one instance per copy (MTLInstanceAccelerationStructureDescriptor over the plain meshes' structure plus one per instanced batch's base mesh — so a copy costs a matrix there, never a triangle list), which is what puts a copy in a ray-traced reflection, under a traced point/area shadow, and in the GI bounce. Load-bearing, four parts: the base vertices are appended to the mesh buffer after the plain ones so ONE pointer still serves the hit fetch (tracedMeshVertexCount reserves the room wherever the buffer is asked for; no room = copies left out, never a write past the end); the hit-lookup table replaced the flat per-geometry offsets with count, then one five-uint record per instance (base vertex, the copy's tint as three float bit patterns, and the run's material slot), then the per-geometry offsets (ollin_rt_hit_lookup, OLLIN_RT_PLAIN_MESH marking the record that reads the geometry table instead) — every direct geoOffsets[...] read goes through it; a copy's normal takes the transpose of the query's world-to-object matrix (the inverse transpose, exact under non-uniform scale); and every traced signature is now instance_acceleration_structure + intersection_query<triangle_data, instancing> (28 + 18 sites, the stand-in structure wrapped to match, and every encoder that binds the scene calls useTracedScene because an instance structure reaches the structures under it indirectly and they are not resident on their own), so there is one structure type and no per-scene shader variant. The extra structure level is not free, and one line pays for it: every query now declares the scene through ollin_rt_params (assume_geometry_type(triangle) + force_opacity(opaque), so a hit commits inside traversal and each caller's candidate loop never runs). Measured (M2), and the sign differs by ray kind: the reflections example at 1080² read GPU 24.75 → 21.23 ms (14% faster) than before this work with no copies in it at all, while the soft-shadow stress benchmark at 2160² reads 22.08 → 23.46 ms (+6.3%) at 1 ray and +9.7% at 64 — closest-hit rays gain more from the opacity hint than the instance level costs them, accept_any shadow rays gain less. assume_identity_transforms was measured too and recovers only ~2%, so the level itself is the cost, not the transform. Read the first of those two figures narrowly: --bench --gpu then encoded no deferred reflection pre-pass, so the mesh fragments took the inline trace and the number describes that path, not the live frame (which reads the pre-traced layer). The harness was fixed with the resolution tier below; the live frame reads 20.23 ms on the same scene. All three placement forms reach the traced scene, the two GPU-held ones (the ComputeBuffer form, MeshField) through ollin_rt_instance_write (MetalRenderer+RTInstances.swift): only the COUNT is CPU-known, so the build reserves the instance slots and one thread per copy fills both the 64-byte MTLAccelerationStructureInstanceDescriptor (its layout mirrored in MSL, options passed in rather than hardcoded) and that slot's five-uint hit record, from the same placement buffer the draw reads — which is why the hit table is now written CPU-side as head + tail around the GPU gap rather than as one array, and why a run whose pipeline will not build is dropped whole (stale descriptors would be traced). A field's base meshes append after the instanced ones (one expansion per distinct mesh, never per copy) and each entry takes its own structure; every copy joins, culled or not (the raster cull is against the camera; a reflection sees what the camera cannot). A traced copy costs ~2 µs of GPU per frame on an M2 whatever placed it (measured 1→8,000 copies: GPU 4.19→19.56 ms, the shipped list form reading the same curve), so MeshField.tracedCopyBudget (default 20,000) keeps the 240,000-copy case from spending the whole frame there: over budget the field leaves the traced passes whole and says so once, naming both numbers. Copies reach the path-traced export too, which is what the fifth uint of the hit record is for: that export resolves a finish per geometry, and every copy of one base mesh shares a single geometry, so each run takes its own slot past the plain geometries in the same OllinMaterial/OllinPTTexEntry tables (the map gates lowered, since a copy's raster draw binds no maps) and a hit reads it through OllinRTHit.mat. A batch the traced scene turned away (an over-budget field, a matcap prop) keeps rastering over the traced layer rather than vanishing: pathTracedCopyBatches is the set the composite arm consults, and the export's own traceable-scene guard now takes a frame made only of copies. One envelope, stated rather than silent: a glowing copy is not sampled as a mesh light (the CDF weighs each emissive triangle by its world area, and a copy's triangles are unplaced — for the two GPU-held forms the CPU never sees a placement at all), so its hit emission takes full MIS weight through copied and its light arrives by the paths that find it, unbiased but grainier. The remaining exclusions are documented, never silent: textures/wireframe/matcap fall back to the solid look with a one-time note, not in the screen-space pre-passes (contact shadows / SSAO normals / scatter mask) or the mover velocity pass, refused inside makeBatch (the retained-mesh shadow rule), spatial export records the array form per copy and refuses the GPU form with a note, SVG skips meshes as ever. Measured (M2, 1080², release, lit + shadowed 12,398-copy field): CPU record 11.99 ms/frame per-copy loop vs 0.23 ms instanced (53×); GPU identical (~56.6 ms in that stress scene) — the win is CPU elimination, the roadmap's own framing. Example Rendering/InstancedMesh (A/B parameter vs the per-copy loop); snapshot instanced-mesh; InstancedRayTracingTests (the traced probes, live and path-traced, each verified red by sabotage); MeshInstanceTests + MeshInstanceRenderTests (the A/B equivalence + the pipeline-build test that keeps the RT-dev-machine-unreachable cube leg compiling) + MeshInstanceBenchmarkTests (Scripts/benchmark.sh instancing). Per-copy motion (InstancedMoverRange, Drawer.recordInstancedMover, ollin_mesh_velocity_instanced_vertex): a list draw inside withMotion keeps its composed copy matrices under the block's MoverKey (sharing the block's draw counter with mesh movers) in instancedMoverHistory (pruned like moverHistory, counted by the census); a frame whose count equals last frame's appends last frame's matrices to moverPreviousInstances and records a range, copy i to copy i; a changed count, a first sighting, or a skipped frame records nothing and restarts. The velocity pass uploads the frame's instanced base vertices, placements, and previous matrices into its own ring slot (one per encode, 256-byte-aligned regions), so no caller passes instanced buffers; every main-canvas instanced draw now occludes in phase 1 (the list form from the upload, the compute form from its own buffer; before, copies neither moved nor hid a mover), the movers draw in phase 2 with previousOfCurrent the identity. Pinned: each copy's own delta, the instanced field equal texel for texel to one withMotion block a copy, an instanced wall hiding a mover, an export's streaks against separate movers and repeating byte for byte (InstancedMotionTests, two planted defects red). The compute-buffer and MeshField forms note once and keep the camera's motion (DESIGN-NOTES, 3D mode). Mechanism: ARCHITECTURE.md § Instanced meshes and mesh fields. Docs/3D/Instancing.md; Guide Ch 29 § Ten thousand of the same thing: instancing.
  • Mesh fields (GPU-culled worlds) (Sources/Ollin/3D/MeshField.swift; encodeMeshFieldCulling + the .meshField arm in MetalRenderer+Effects.swift; the field shadow legs in +Targets.swift; ollin_mesh_field_* in Shader3D.metal). A retained world of many distinct meshes + copies drawn by ONE drawMeshField(_:) per frame: place(_:at:) expands each mesh once into a concatenated base-vertex buffer and appends its copies (OllinMeshInstance) plus an OllinFieldEntry (local bounding sphere, vertex run, copy run, compact region — all in OllinShaderTypes.h); per frame a GPU cull kernel (one thread per copy, sphere-vs-frustum through the copy's matrix × the draw-time field matrix, Gribb-Hartmann planes from the unjittered view-projection) atomically compacts survivors per entry (under a fixed clock the camera set compacts in copy order instead: a SIMD-group count, one serial scan that also gives each entry its base and count, a ranked write, since copies meeting at equal depth draw whichever lands first and the example came out three ways in four renders; culled then equals unculled to the pixel; the shadow set stays atomic, since a depth map keeps the nearest depth in any order), and an encode kernel writes one MTLDrawPrimitivesIndirectArguments per entry with baseInstance = the entry's compact offset, so [[instance_id]] lands directly on that entry's slice of the compacted-index buffer and the vertex fetches instances[compacted[iid]] with zero per-draw bindings; the render arm issues one indirect draw per entry (CPU cost per KIND, never per copy). Load-bearing: the draws are GPU-authored but deliberately NOT an MTLIndirectCommandBuffer — Metal rejects the RT-compiled lit fragment inside an ICB pipeline ("Fragment shader cannot be used with indirect command buffers"), and indirect draws carry the identical payload with no fragment restriction, so copies keep the FULL solid shading (the shared bindLitMeshFragment set) on every device; culling must never change the picture (a culled copy is off-screen by definition; cullingChangesNothingInThePicture pins it, and field.isCullingEnabled = false is the public A/B switch); the cull runs at drive level (encodeMeshFieldCulling, after encodeCompute in every depth-carrying drive) and the draw arm requires depthFormat != nil, which doubles as the stale-arguments gate (the 2D-only accumulating drives never cull, so they never draw a field); surface color bakes at place (mesh base color × per-vertex colors × per-copy tint — the draw-time fill deliberately does not tint a field); shadows cast UNCULLED from the field's own retained buffers (ollin_mesh_field_shadow_vertex + the cube sibling with iid % 6 face routing; a caster behind the camera still shadows the view), one draw per field per frame (a second is noteOnce'd — the cull results live on the field); spatial export walks the CPU-kept placements per copy; place after realization bumps generation and re-uploads. A field also stands in the traced scene (its base meshes appended to the frame mesh buffer, one structure per entry, one GPU-written instance per copy — see Instanced meshes), the path-traced export included, bounded by tracedCopyBudget (default 20,000; over it the field leaves the traced passes whole with a one-time note naming both numbers, since ~2 µs of GPU per traced copy per frame would make the 240k case unusable — in the export it then rasterizes over the traced layer, which is the difference between out of the mirrors and out of the picture). The old RT-device point-caster exclusion is therefore gone: a field now casts a traced point shadow like a plain mesh. The 2D shadow pass culls against the light's own frustum (exact: anything culled would have been clipped by the map's rasterizer; the cube pass stays uncculled, omnidirectional) — and the shadow-cull dispatch must rebind the instance buffer at index 0 (the encode dispatch just repointed it at the draw-arguments buffer; missing that rebind culled against garbage matrices and silently dropped real shadows, found by the culled-vs-unculled A/B). Measured (M2, 1080², release, the example's 240k-copy plain, lit + shadows + fog): GPU 18.5 ms culled vs 50.8 ms unculled (2.7×), CPU record ~0 ms (Scripts/benchmark.sh field). Example Rendering/MeshField (240k solids, fly-through, culling parameter); snapshot mesh-field; MeshFieldTests + MeshFieldRenderTests (field == equivalent instanced draws; culling == no picture change; the RT-unreachable cube shadow leg compiled explicitly) + MeshFieldBenchmarkTests. Mechanism: ARCHITECTURE.md § Instanced meshes and mesh fields. Docs/3D/Instancing.md § A field that culls itself; Guide Ch 29 § A world the camera trims: MeshField.
  • Strand fields (in-draw grass) (Sources/Ollin/3D/StrandField.swift; ShaderStrands.metal, the first mesh-pipeline segment; makeMeshPipeline + the .strands arm in the renderer). drawStrands(_ field: StrandField) grows grass-like blades ENTIRELY inside the draw call through a Metal 3 mesh pipeline (MTLMeshRenderPipelineDescriptor, built from the same runtime-compiled library — the [[object]]/[[mesh]] stages compile through makeLibrary(source:), <metal_mesh> included by ShaderCore): no vertex, index, or instance buffer exists anywhere; every blade's root, height, lean, bend, sway phase, and tint derive from hashes of its index (the shader library's hash22/hash33), so a StrandField is a plain value with zero GPU state to invalidate. Load-bearing: the object stage runs one threadgroup per tile (frustum-culls the tile's AABB — isCullingEnabled is the pinned A/B: skipping tiles must not change a pixel — and grades the tile's per-blade segment count 4→1 between detailNear/detailFar; the detail parameter is that dial's A/B), and the mesh stage emits bundles of OLLIN_STRAND_BUNDLE (24) blades, one thread per blade, into the solid path's own MeshOut, so blades shade through ollin_mesh_fragment (full lit set via bindLitMeshFragment: lights, shadows RECEIVED, IBL, GI, fog, the current finish) — a mesh pipeline pairs fine with the RT-compiled lit fragment (probed; unlike ICBs, which reject it); MTLGPUFamily.metal3 is the only honest capability signal, and everything nearer the failure lies — a paravirtualized GPU (a VM's display, a hosted CI runner) compiles the [[object]]/[[mesh]] stages, builds the descriptor, and answers respondsToSelector: true for setObjectBytes:length:atIndex: and its siblings, then raises unrecognized selector on the first one called, which is an uncaught Objective-C exception, so what should have been a missing feature was a killed process taking every other test in the run with it (it crashed the CI test job twice on 2026-09-01, the second time through a selector probe that the stubs defeated). Measured on the runner: Apple Paravirtual device, apple5/mac2 true, apple6-apple9 and metal3 false, pipeline built, all four selectors true, setObjectBytes exit 134. So MetalRenderer.meshShadersAvailable(on:) is supportsFamily(.metal3) (with OLLIN_NO_MESH_SHADERS=1 forcing false), the arm leaves the field undrawn with a noteOnce where it answers no, and Snapshot.hasMeshShaders gates the two tests that draw one (strandPipelineBuilds keeps the plain Metal gate, since building is all it asks, and it does build there); mesh capacities are sized in OllinShaderTypes.h from the bundle and OLLIN_STRAND_MAX_SEGMENTS (240 ≤ 256 verts, 192 ≤ 512 prims — change either, re-check both); a dead mesh thread still writes degenerate zeros so the fixed layout stays initialized, and thread 0 alone sets set_primitive_count; sway is driven by the sketch clock (computeUniforms.time), so exports reproduce and the same frame renders byte-identically (pinned); the blades exist only in-draw, so they cast no shadows (they receive), the spatial exporter refuses with a note, SVG skips, makeBatch refuses. The honest pitch is architectural, per the platform's own guidance (mesh shaders earn their place only where draws cannot express the geometry — a blade's per-length bend is exactly that): measured (M2, 1080², release, the 511,584-blade meadow, lit + shadows + fog): ~22.5 ms GPU, zero geometry memory, zero per-frame CPU; distance grading saves a further ~15% (1.15×) over full detail (Scripts/benchmark.sh strands). Example Rendering/Grassland (detail parameter); snapshot strands; StrandFieldTests + StrandFieldRenderTests (pipeline build, byte-exact determinism, tile-cull invariance) + StrandBenchmarkTests. Docs/3D/Strands.md; Guide Ch 29 § Grass that was never built: strand fields.
  • The ocean (Ocean / WaterSurface / OceanField in Sources/Ollin/3D/Ocean.swift; makeOceanField(_:at:resolution:) + drawOcean(_:segments:tiles:water:); Renderer/ShaderOcean.metal, the .ocean origin and batch in the renderer). A wind sea written as a spectrum (Phillips, from Tessendorf's published account, credited in ATTRIBUTION.md) and turned into a surface by one inverse Fourier transform of it, then drawn as water by a grid with no geometry buffers anywhere: each vertex works out its corner from its own index and reads the field for where the water has carried it. The field is an ordinary layer (rgba32Float, one texel per grid point: sideways x, height, sideways z, fold) so it filters, combines, and draws like any other. Load-bearing: waveHeight is a measurement, not a dial (significant wave height in world units) because the variance of the surface is the sum of the spectrum over the whole grid, a closed form the CPU evaluates once per sea state (cached on the drawer, 65k terms at the default resolution) and scales the spectrum by, which is what holds the height steady across resolutions and wind speeds (theSurfaceStandsTheHeightItWasAsked / theHeightHoldsWhenTheGridChanges measure it off the GPU's own output); one transform carries the whole surface, the height in rg and the two sideways shifts packed as one complex number in ba, since both are real fields; the fold (the Jacobian of the sideways shift) is read in the resolve pass by neighbor differences, never by a second transform; h0 comes from a hash of the grid index through Box-Muller rather than from stored state, so a field is a pure function of (sea state, time) with nothing to invalidate and seed reproduces exactly; loopSeconds rounds every frequency down to a multiple of one step, which makes the whole surface periodic (pinned: t = 0 and t = period match inside 2 levels while the halfway frame differs); the normal is read per pixel from the field (four taps, central differences of the moved surface) rather than carried down from the vertex, so the light runs over the water at the field's resolution rather than the grid's; the reflected ray is folded up at the horizon (r.y = abs(r.y)) or an environment's flat ground reads as pale plates lying on the water with the map's own horizon for an edge (a real fixed defect); the sun glitter uses the same Fresnel term the sky does, or it spreads into flat white plates wherever the sea is smooth (the second fixed defect); foam is a smoothstep on the fold broken by noise at two scales, since a ramp from nothing whitens half the sea and a flat patch of white reads as paper. Like a strand field the geometry exists only in-draw, so it casts no shadow, is not in the ray-traced structures, and the vector and spatial recorders skip it with a note; material(_:) does not apply (its own water fragment). Measured (M2, 1080², sky environment + sun, Scripts/benchmark.sh ocean): 1.4 ms GPU at 160 segments over a 256 field, 1.8 ms at 320/256, 2.8 ms at 320/512, 3.7 ms at 512/512 (doubling either the grid or the field costs about a millisecond at that size). OceanTests (12: the height, its proportion, its steadiness across grids, wind moving the energy rather than the height, the fold, determinism, the loop, the drawn surface moving, plus the closed form with no GPU); snapshot ocean; example 3D/Geometry/Ocean; Docs/3D/Ocean.md; Guide Ch 29 § The sea, from what a sea is made of: an ocean spectrum. The amplitude cache is bounded (256 sea states, then cleared): a sea state that follows the clock added an entry a frame (SoakTests found it).
  • Retained batches (Sources/Ollin/Drawing/Batch.swift + encodeRetained in MetalRenderer+Effects.swift). makeBatch { } records a drawing once into a Batch (persistent, made in setup() and held — the Feedback shape) and drawBatch(_:) replays it from the handle's own once-written per-device MTLBuffers — the escape from per-frame re-recording for static heavy content (measured M2 1080² release: 150k circles ~13.5 ms/frame per-frame vs ~0.003 ms replayed; example parameter flips the two live). Load-bearing: recording is an array swap on the Drawer (per-frame arrays + batch list + gradient-row table), so recorded runs index the handle's arrays and baked gradient rows are handle-relative — the replay binds the batch's own strip texture, never the frame's; the body runs style-scoped (pushState/popState) from an identity CTM with the target/clip stacks swapped out, and the .retained reference batch supplies the draw-time context (CTM + target + clip level + depth(at:)); the draw-time CTM is carried in flag-gated Uniforms fields every 2D vertex shader applies to its output position only (flag 0 outside a replay → ordinary paths byte-identical, snapshot suite unchanged; identity replay byte-identical to the per-frame drawing, pinned by BatchRenderTests; SDF analytic AA stays exact under any replay transform because shape-local interpolants never change); unsupported content is gated at its funnel with a one-time note (meshes + 3D fields would silently lose shadows/reflections; particles; layer blocks; clipping; background; nesting) — never dropped after recording, which would corrupt the next-run count derivation; point clouds record camera-less and replay through the frame's camera (the 2D replay CTM doesn't touch them); draw-time symmetry does not fold a replay (record folds inside the block); vector export flips OllinApp.isVectorExporting for the whole recordVectorFrame drive (setup + warmup included) so makeBatch captures SVGCommands instead and drawBatch splices them under the composed CTM — SVG/PDF see every stamp. Example Rendering/RetainedBatch; snapshot retained-batch; BatchTests/BatchRenderTests. Mechanism: ARCHITECTURE.md § Retained batches. Docs/Drawing/Batches.md.
  • Kept shapes (Sources/Ollin/Geometry/ShapeMemory.swift + drawShape in Core/Drawer+Text.swift, the plain-stroke path in Core/Drawer+Tessellation.swift). A Shape drawn again unchanged copies its fill triangles and stroke instead of working them out again. Load-bearing: every Shape carries a ShapeMemory (a let box allocated at init, shared by copies, outside ==, which is written by hand over contours and winding); it recognizes its shape by holding the contours array and comparing buffer identity plus winding, so an edit to any copy must copy the contours first and can never match stale geometry, and no observer allocates a box per edit; nothing is kept until the second draw (a fill on its second draw; a contour's stroke on its second draw under the same StrokeKey: half-width, join, cap, and ctmScale, the last because the fringe is about 1px on screen), handed back from the third, so a shape made and drawn once a frame pays a lock and nothing more; a stroke is kept only when strokeIsPlain (solid paint, no brush, no dash, uniform width and opacity, no web sources recording); kept stroke vertices are pre-transform (position + coverage) and take the frame's color and transform as they are appended; the kept triangles are the Vector2s triangulatedFill returns, so a gradient fill still reads its paint at full precision. Every plain solid stroke, kept or not, expands into a list of its own and is appended in one pass (appendExpandedStroke), its batch checked once a stroke: a per-vertex ensureBatch was three quarters of a stroke's CPU. Byte-identical: 150 of 150 frames of a peer's 195-outline sketch match HEAD, and the snapshot and stroke suites pass unrecorded. Measured on an M2, release: 124 ms to 5.9 ms kept; a stand-in of 196 outlines, 107 ms to 28 ms as new shapes and 5 ms kept. A stored field changes Shape's layout, so a sketch dylib built against the old module must be recompiled (a live host that reloads one segfaulted; filed). KeptShapeTests (6; seven plants red). Docs/Drawing/Drawing.md § drawShape; Guide Ch 19 beside batches.
  • SDF combinators (Sources/Ollin/Drawing/SDF.swift + Renderer/ShaderCombinator.metal; 3D in Sources/Ollin/Drawing/SDF3D.swift + Renderer/ShaderRaymarch.metal). Compose signed-distance fields so shapes merge instead of stack — the field-combining axis the per-instance SDF path can't express. The typed SDF value type (leaf region shapes + .union/.smoothUnion(_:k:)/.subtract/.smoothSubtract/.intersect/.smoothIntersect/.morph, the hg_sdf joint + detailing ops (the chamfer/stairs/columns trios + engrave/groove/tongue/pipe — crisp nearer-side/body color; the detailing ops are value-type-only (the morph precedent), columns gets block forms; they assume frankly-crossing surfaces — near-parallel faces within the radius echo the pattern, the documented envelope), and .rounded/.onion/.at/.rotated/.scaled/.stretched/.colored/.mirrored/.repeated/.repeatedRadially) draws via drawSDF(_:) honoring the current fill/stroke/CTM, the stroke tracing the merged outline; the scoped block forms (smoothUnion(k:){ … } + hard/mirror/repeat variants, and the stateful sculpt { } block whose add()/carve()/blend(_:) verbs mutate per-child mode/melt mid-block — built for the live-coding loop) capture bare region draws at Drawer.appendSDF as leaves, reaching every fillable region shape. Rendering is an "SDF VM": the tree flattens (CPU) to a flat SDFNode program (own SDFGroupInstance + .sdfGroup pipeline beside the per-instance SDFInstance path, which is full at 144) walked per pixel with two fixed-depth stacks (cap 16); smooth ops also lerp the operands' colors (the melt look). Load-bearing invariants: leaves carry no transform (all positioning is XFORM nodes → method-chain order exact: a.at(p).repeated() tiles the moved shape, a.repeated().at(p) shifts the tiling); the leaf distance ollin_sdf_distance is kept in sync by hand with ollin_sdf_fragment's switch (a new region shape goes in both; the per-shape switch stays untouched so single-shape snapshots are byte-identical); polar/radial repeat is the shared sel-5 XFORM (2D fold around origin, 3D around an axis), the single-evaluation kind (wedge-symmetric content exact, else a seam); region shapes only (lines/open-arcs/Béziers skipped with a one-time log); a field past the caps (depth 16, ~256 nodes) is logged + skipped, never mis-drawn; the buffers thread all 9 encode sites. 2D gradient paint samples in field space (in.field; along-path falls back to solid; a solid-leaf field is byte-identical). Per-axis sizing: stretched(x:y:[z:]) (iq opElongate, an exact SDF, the preferred tool) vs scaled(x:y:[z:]) (true non-uniform, only a conservative bound — rescales by the min scale component for a safe Lipschitz march, so blends distort under strong anisotropy). The raymarched 3D sibling — SDF3D + drawSDF3D(_:): the same ops over the 3D leaves (sphere/box/roundBox/torus/capsule/cylinder/cone/octahedron/ellipsoid/plane + line-between-points/hexPrism/pyramid/cappedTorus/link) plus the sculpting distortions .twisted/.bent and .displaced/.roughened (every distortion/displacement is a distance bound, so flatten attaches a conservative Lipschitz rescale that keeps the march hole-free), sphere-traced through the active camera and depth-composited with the rasterized meshes (a parallel path: own SDFNode3D + .raymarch pipeline). Load-bearing: the march runs in world space (the 4-tap gradient normal + written [[depth(any)]] come out world-space with no normal matrix); one fullscreen triangle per field bounded by its world AABB (a miss bails O(1)); shading reuses meshLitColor unchanged, honors material(_:) per batch (ensureSDF3DBatch records the finish — the generic ensureBatch records none, the bug that left field materials silently ignored) and adds the same IBL ambient the mesh fragments add (incl. the traced-reflection swap under rtReflections; gated on iblEnabled → byte-identical with no environment; resolveFieldLighting carries the same setup so the half-res tiers light identically). A field is absent from the deferred reflection layer (that pre-pass traces the mesh carriers only), so every encode that binds ollin_raymarch_fragment hands it lighting with rtReflectionDeferred cleared and the field traces inline; with the flag set, the ambient's deferred branch reads the fragment's zero stand-in sample and traced reflections silently no-op, leaving the raw environment in place of the traced scene — at the silhouette, where grazing Fresnel mirrors the environment at full strength, that read as a 3-4px blown-white rim band plus one dark pixel, invisible to every patch mean and found by eye (the rasterized mesh beside it swapped in the traced scene and stayed smooth; without ray tracing the two rims match, band and all, so the band itself is the honest picture of a hot environment under the clamp tone map, not a field defect). Pinned by GlassRenderProbes.aFieldRimShadesLikeAMeshRim, an edge-aligned rim scanline probe (verified red against the deferred-flag leak). Residual envelope: the rim compresses the mirror image, so the field's single inline reflection ray can read one bright pixel where a mesh's supersampled deferred layer averages (Docs/3D/Combining.md § the field rules). The infinite plane is the one unbounded leaf (flattener marks the field unbounded, marches the near..far span; value-type-only). The block form reaches 3D too — SDF-able mesh builders capture leaves via Drawer.drawMeshPrimitive, positioned by the model matrix decomposed to translate+rotate+uniform-scale (the only similarity an SDF respects); a non-primitive Mesh is ignored (one-time log). Screen-space gradient paint (3D, by each hit's projected screen position); analytic silhouette AA (the ray's closest approach vs the pixel cone). Shadows (gated on castShadows(), byte-identical off): self-shadows via iq's standard min(k·h/t) penumbra — not the previous-step refinement, which divides by zero on a receding ray and false-shadows lit faces — plus the full field↔mesh cast/receive matrix across every light type; the per-receiver-pixel point-light cast is trimmed by a conservative AABB early-out + a half-res field-shadow pass. Quality dial (raymarchQuality/raymarchResolution): a field's cost is bound to pixel count, not step count, so the dial scales internal resolution via a reduced-res pre-pass (point-sampled depth keeps the silhouette crisp) whose fraction is a coverage-adaptive budget, not a fixed downscale (min(1, fraction/√coverage) — a dollied-out field traces at full res, the zoom-out-blur fix); the coverage estimate must stay honest or the blur returns: a combine's AABB grows by each op's provable outward reach (smooth/chamfer/stairs k/2, columns/pipe/tongue k, subtract/intersect/morph zero — morph's k is a blend fraction, not a length), never a blanket k, which compounds per nested op until a dollied-out field reads near-screen-filling (a real re-blur bug), and per box the estimate takes the smaller of the corners' clipped NDC rect and their convex-hull area; the automatic tier is live-preview only (export resolves .detail, full-res → default-tier snapshots byte-identical; an explicit fraction/tier is honored on export, adaptively); uses the shared RenderQuality model with the other three quality parameters. ollin_sdf3d_eval's switch stays in sync with SDF3DShape. Examples Shapes/Combinators* (2D) + the 3D/Raymarched* set; snapshots sdf-combinators + its -gradient/-stretch/-joinery/-detailing/-sculpt and -3d* variants. Full mechanism, rationale, and the field-shadow matrix: ARCHITECTURE.md § SDF combinators. Docs/Drawing/Combinators.md. Fractal leaves (SDF3D.mandelbulb(power:iterations:radius:) / .mengerSponge(iterations:size:) / .mandelbox(scale:iterations:size:), shape tags 15-17, value-type-only): distance estimates read off each fractal's own iterated map, drawn by the same tracer. Load-bearing: the Mandelbox uses the (|z| - |scale-1|) / |dr| form, never the plain |z| / |dr| — the plain one is unsigned (≈0 through the whole solid) and over-reaches near the surface (29% of samples 0.01 above a face would step through it), so a grazing ray crossed the face plane and stopped on interior pocket walls whose four-tap normal read as noise (a dark, dusty box; lit correctly only when viewed head-on), while the subtracted form goes negative inside and reaches less far outside, which fixed it with no safety factor. Fractal fields ask for a wider normal step through SDF3DGroupInstance.normalEpsilon (the old _pad0; 0 = the 0.0008 default, so every non-fractal field is byte-identical): bulb 0.008·radius, box 0.004·half-side, sponge 0 (a clean signed bound), propagated by the flattener as the max over leaves and scaled through .scaled; below it the estimate's sub-step noise, not the surface, sets the normal (the bulb read beaded at the default step). Bounds: the bulb maps its escape ball 2^(1/(n-1)) onto radius (AABB padded 5%), the sponge is exact, and the Mandelbox scales the cube its canonical set sits in (2(s+1)/(s-1) per axis for s > 1, 2 for s < 0, measured) onto size and clips to it, so it stays bounded for every scale (|scale| held ≥ 1.1). half is a reserved word in MSL (a float half parameter fails the whole library compile, which the headless export used to swallow as "no Metal device"; OllinApp.headlessRenderer now prints the error). The sponge's carpet test also caught a pre-existing ortho bug in the silhouette AA: the pixel cone was t·kPixel for every projection, but under an orthographic camera the footprint is flat, so the cone inflated by the eye distance, drew a soft halo at every ortho silhouette, and swallowed gaps narrower than it (the level-2 shafts rendered as pinholes); the cone is now kPixel when projection[2][3] == 0, perspective byte-identical. FractalFieldTests (7 tests on flat top-down renders: the carpet's (8/9)^k coverage, the see-through center, deeper-only-removes, the bulb's 8-fold turn with a power-5 bulb failing it, the radius bound, the box's octahedral symmetry, size as an exact scale); snapshot sdf-combinators-3d-fractals; example 3D/Raymarching/RaymarchedFractals; Docs/Drawing/Combinators.md § Fractal leaves; Guide Ch 32 § Infinite detail: the fractal leaves.
  • User-supplied shaders (Sources/Ollin/Effects/Shader.swift + the wrapper/compile/error path in MetalRenderer). A sketch writes its own fragment shader and runs it through the effect graph. The contract is Ollin-native, not raw Metal: define float4 shade(float2 uv, ShaderInfo info) (uv 0…1 top-left, return straight sRGB — input layers are read back as straight sRGB too) and Ollin generates the surrounding fragment + vertex. The Shader value type (Sendable) carries inline-String or resource:in: source, params (up to 32 floats, read via param(info, i); the readers param/sample/sampleAux/sampleRaw/sampleAuxRaw are free functions taking thread const ShaderInfo &, never macros — sample is a member function on every Metal texture type, so a function-like #define rewrote t.sample(s, uv) inside anything the shader included, refused a three-argument call outright, and blocked a helper of the author's own by that name; as overloads a member call is never a candidate, and the whole snapshot table stayed byte-identical through the change), and a Modules opt-in set (.color/.hash/.noise/.sdf/.domain/.visual/.complex; the last is the complex arithmetic on a float2 (cmul/cdiv/cinv/conj/cabs/carg/cpolar/cexp/clog/cpow/csqrt/the trigonometric and hyperbolic set, C's names), the complexPlane framing (imaginary axis up, the domain-coloring generator's own frame at span = 3 / zoom) and domainColor (an OKLCH wheel at one lightness plus the modulus and conformal rulings), depending on .color; every helper mirrors the CPU Complex value, ComplexShaderTests paints each one over a grid against it, and the page cuts the section like any other, Argued in the page-versus-Mac table); routing reuses the effect-graph enums — a .shader(Shader) case on Generator/Filter/Combine.Kind, so input count is the variant (generate(.shader(s)) 0, layer.filtered(.shader(s)) 1, a.combined(with: b, .shader(s)) 2) — with no new render-graph plumbing. Each shader compiles once per composed-source hash (its own makeLibrary(source:); failed compiles are cached too, so a broken shader doesn't retry every frame; all caches clear on a framework-shader reload). Friendly errors are the headline: Shader.init captures its call site and the wrapper emits a #line directive, so Metal diagnostics land at the real, IDE-clickable sketch line (ShaderErrorTests pins the off-by-one semantics for no-newline literals); errors surface via stderr (once per hash) and the pull-model onUserShaderError channel into the OllinLive overlay — a shader error and a Swift compile error never clear each other. The OllinShaderLib helper segment (color/hash/noise/sdf/domain + palette/OKLab + the once-compute-only helpers) is one source of truth used three ways: the framework's own segments build on it, it splices into every user shader, and composeComputeSource splices it into every compute kernel (one helper set, hashNM names, everywhere); the 2D sd* distance catalog lives in its sdf section, shared with ShaderShapes; using: [.noise, .sdf] keeps only marked sections — a compile-time lever, default .all, and now also the escape hatch for a name clash: every public helper sits in a section that can be turned off (luma moved to color and rotate2D to domain on 2026-08-27, out of the always-spliced part), so an included file defining one at the same signature can narrow it away; only what the generated fragment itself reads (srgbToLinear/linearToSrgb/ollin_unpremul) is unconditional, and a clash needs a matching signature, not merely the name. Hot-reload covers both forms in OllinLive: an inline shader reloads with the sketch; a .metal resource shader reloads on its own with no swiftc pass (Shader(resource:in:) stores the bundle-resolved path; a watched-.metal change routes to SketchRunner.invalidateUserShaders()); a framework-segment .metal still routes to the full library reload (dispatch by path). Examples Shaders/HelloShader (a Toby-Howard-homage plasma generator, inline beside the hot-reloading .metal-file form) / ShaderFilter / ShaderBlend; snapshot user-shader. Mechanism: ARCHITECTURE.md § User-supplied shaders. Docs/Shaders/Shaders.md + the per-function Docs/Shaders/ShaderLibrary.md.
  • A shader as paint (Paint.shader, Sketch.fill(_ shader:) / stroke(_ shader:); the .paint variant of the user-shader wrapper in MetalRenderer+Pipelines.swift, paintPipeline(for:kind:key:)). A Shader fills or strokes any 2D shape, its shade(uv, info) run per pixel in the shape's own coordinates on all three 2D paths. One shader per run, and the run carries it: GeometryBatch.paintShader is one more thing ensureBatch breaks on (beside kind, blend, depth, clip, light set), set around each emission by Drawer.currentPaintShader (appendSDF, emitTriangle, the fringe expander, appendStrokedPath), so consecutive shapes under one shader are one draw and an SDF shape whose fill and stroke are two different shaders is recorded twice (fill, then stroke over it; composites as one instance would). The pipeline is the shader's own library, composed like a layer shader's (the helper library with .sdf forced in, then ShaderCore.metal + ShaderShapes.metal with their quoted includes dropped, the user's source under its #line, and three generated fragments), compiled once per composed hash through the shared userShaderLibrary(for:variant:), with a pipeline per PaintPipelineKey (hash + the plain PipelineKey's blend/depth/stencil/color format) built by the same makePipeline(vertex:fragment:using:) the framework pipelines use, the vertex stages the framework's own (ollin_vertex / ollin_fringe_vertex / ollin_sdf_vertex, compiled into the paint library from the spliced segments, so a paint batch is placed exactly as a plain one); a shader that does not compile skips its batches and reports once (reportUserShaderError). The data rides where a color would: the SDF tag's three-bit paint slots take kind 5 (EncodedPaint, no slot data, isPositional false, the framework fragment painting middle gray if it ever meets one), SDFOut/VertexOut/FringeVertexOut gained a canvas varying (info.position; FringeVertexOut.opacity from aa.y too), and under a shader a triangle or fringe vertex's color is (u, v, along, distance) with aa.y the opacity profile (OllinVertex stride unchanged at 32; the plain paths write aa.y = 0 and read nothing, byte-identical). uv is the shape's box: p / (2 size) + 0.5 on the SDF path, Drawer.PaintBox on the others (the outline's bounds for a fill and a closed stroke, inflated by the half-width for an open stroke, the whole Shape's box for every contour, the text run's outlines for outline text; atlas text under a shader fill takes the outline path). distance is ollin_sdf_signed_edge on the SDF path (the region leaf distance onioned by a hollow band; the capsule, Bezier, and arcs as their painted edge), the expander's new signed StrokeVertex.offset on the fringe path (sign flipped by Drawer.signedArea so inside a closed outline is negative whichever way it was drawn; the WASM expander rebuilt from the same source, WebExpander.wasm), and Drawer.farInside (-1e6) on a tessellated fill, which has no measured edge; along is the SDF pathT / the arc-length fraction on strokes and 0 on fills. Coverage after the shader, exactly as the plain fragments apply it after their paint, so a constant shader equals a flat color byte for byte (pinned over 13 forms, fill, stroke, both, and mixed). A mesh under fill(shader) reads the shader as its base texture, baked once per frame per shader into a paintBakeSize (1024) square generator layer (paintBake, generators fill before any geometry pass, so a mesh in any layer samples this frame's picture), which is why it reaches every pass a texture reaches; no uvs → white and a note; instanced meshes draw untextured (a note). Fallbacks: SVG/PDF/embroidery write Paint.shaderStandIn (middle gray); the web recorder refuses with the call named (fill(Shader) or stroke(Shader)), inside a drawBatch too; 3D lines and feature edges draw white under a shader stroke with a note; a combine leaf under a shader paint takes no color (white). ShaderPaintTests (13: the constant-shader identity over 13 forms, uv at the corners on both paths within a level and under a quarter turn, the signed distance at pixel centers, a stroke's along and distance and the inside sign on both stroke paths and both windings, position, params, the mesh against the rectangle (mean under 1.5 levels, corners oriented), one run per shader, a broken shader drawing nothing, the web refusal, the SVG gray, Paint as a value); snapshot shader-paint; example Shaders/ShaderPaint; Docs/Drawing/Color.md#shader-paint, Docs/Shaders/Shaders.md#a-shader-as-paint; Guide Ch 18 § Paint, not a layer: fill and stroke with a shader.
  • Including another shader file (Sources/Ollin/Renderer/ShaderIncludes.swift). Metal's runtime makeLibrary(source:) has no include search path, so every quoted #include in every shader Ollin compiles at runtime is resolved by hand before the compiler sees it, in the built-in segments, in a user's own Shader (a .metal resource or an inline string), and in a ComputeKernel. The rules are the load-bearing part: a file is spliced once per compile (so a diamond defines nothing twice), the root is seeded into that set (rootKey) so a file naming itself is the cycle it is rather than a second copy, a cycle and a nest past depthLimit (32) are reported rather than followed, a #line on each side of a splice means a mistake inside an included file is reported against that file at its own line (the property that makes the include worth having), and #include <angle> forms pass through untouched so a helper can still compile on its own. A spelling that names a place of its own (leading / or ~) is taken as it is, which is how a sketch reads a shader library cloned elsewhere on the machine, and that library's own files then resolve from where they sit, so one line brings the whole chain (verified against a real LYGIA clone: generative/snoise.msl pulls six more files and compiles; most of its Metal port does, and what fails fails in its own file at its own line, which is the resolver working). Any other spelling resolves against the folder of the file that named it: its own .metal, or the .swift an inline string was written in (ComputeKernel gained a sourcePath, captured from #filePath or the file it was read from, for the same reason). An include problem is raised as the shader's own ShaderCompileError before the compiler is asked, since what follows would only be undeclared identifiers pointing away from the real mistake. The built-in segments now declare what they need with an #include at the top, so MetalRenderer.shaderSourceNames is an unordered (alphabetical) set and the concatenation order falls out of the declarations: adding a segment is a name in that list plus its dependencies in the file, not a slot in a hand-kept sequence. theBuiltInSegmentsCompileInAnyOrder compiles five orders (listed, reversed, three seeded shuffles) on the real device and is verified red by sabotage; writing it found two dependencies nobody had ever declared, because the fixed order happened to satisfy them (Shader3D reads ShaderShapes' diskCoverage, ShaderPathTrace reads ShaderIBL's equirect helpers). The composed user-shader library text is cached per Modules set (userShaderLibraryText, dropped on invalidation), because the compose runs once per shader per frame. ShaderIncludeTests (16). Docs/Shaders/Shaders.md § Pulling in another file; Guide Ch 18 § One helper, two shaders: the include line + the SharedHelper figure.
  • Checking a shader (Sources/Ollin/Effects/ShaderCheck.swift + the OllinCheck target, behind ollin check <file.metal>). A shader is compiled by the sketch that uses it, so the only way to learn whether it compiles was to launch something that draws it, and the first sign of failure was a blank layer. ollin check compiles the file on the real device through the same composeUserShaderSource path a sketch uses, so a shader that passes here cannot fail in a window, and it builds the pipeline too, not only the library (a shader can compile and still be refused there). It reports the compiler's complaint at the author's own line through cleanShaderDiagnostics, the shape inferred from the layer readers the shader calls (0 generator / 1 filter / 2 combine, the effect graph's own rule; --as names it instead, and a wrong shape failing is itself the useful answer), the param(info, i) indices it reads with a gap called out (an index nothing writes reads as zero and is usually a slip), and the files it included. The readers are matched as calls, not words, so discSample( and sampleCount are not layer reads. --using narrows the spliced library to the same section list a Shader's using: takes (all/none/a comma run), so a shader written against a subset is checked the way it runs. Several files per run, nonzero exit if any failed. ShaderCheckTests (10); no pixel snapshot (the output is text). Docs/Tools/ShaderCheck.md; Guide Ch 18 § Trying a shader on its own: ollin check.
  • Visual chains (Sources/Ollin/Effects/Visual.swift + the visual section of OllinShaderLib.metal). The fluent composition tier over the effect substrate: Visual is a value type (struct over an indirect enum Node, the SDF shape) recording a chain of per-pixel ops in five families — sources (.oscillator/.noise/.voronoi/.shape/.gradient/.solid/.layer), coordinate warps (.rotated/.scaled/.pixelated/.repeated/.kaleidoscope/.scrolled), color adjustments (.brightness/.contrast/.saturation/.inverted/.posterized/.thresholded/.luma/.hueShifted/.tinted/.colorCycled/.channel), combines (.blended(with:_:amount:) over the typed BlendMode, .mixed/.differenced/.masked), and modulations (driver overloads of the warps: .displaced(by:), .rotated(by:), .scaled(by:), .pixelated(by:), .kaleidoscope(by:) — one chain's color perturbs another's sampling coordinate). compile() walks the tree once and emits a shade(uv, info) body over ollin_vis_* library functions; the whole chain (drivers included) is one fragment shader, one pass, run through the user-shader path (Shader + the compile-once source-hash cache). Load-bearing: the generated source depends on the chain's structure only — every numeric argument becomes a params-buffer slot in traversal order (VisualTests pins this), so a chain rebuilt each draw() hits the cached pipeline and animated values/@Param parameters never recompile (structural selectors — blend mode, channel — are spelled as literals by design); the params buffer is 16 rows / 64 floats (OLLIN_SHADER_PARAM_ROWS, raised from 8 — paddedParams and the wrapper both read the macro), overflow bakes literals with a one-time note; the codegen's fresh-variable counter starts at 1 (st0 is the root the body declares — 0 collides); .layer leaves dedupe by target identity and cap at the wrapper's 2 textures (routing by count: 0 → generate, 1 → .filtered, 2 → .combined; extras sample black + one-time note); .layer(_ feedback:) reads the previous frame (video feedback); BlendMode.visualIndex is kept in step with ollin_vis_blend's switch; ops aspect-correct inside the function (coordinates stay 0…1 so fract wraps compose), colors are straight sRGB per the user-shader contract, and .noise is signed so displacement wobbles about zero; Modules.visual splices the ops into every user shader (dependency visual → hash+noise in filterLibModules). Examples Shaders/VisualSynth (one deep chain) + Shaders/VisualCatalog (all five families as a switchable contact sheet); snapshot visual-chain (all five families + a layer read); Docs/Shaders/Visuals.md + the module's function table in Docs/Shaders/ShaderLibrary.md.
  • Layered effects (the substrate; Sources/Ollin/Effects/). Draw into off-screen layers, filter them on the GPU, composite back with blend modes (the OPENRNDR RenderTarget/Filter/compose model): makeRenderTarget(scale:) → an off-screen rgba16Float RenderTarget; withTarget(_:) {} redirects drawing into it (scoped like withState; background(_:) inside clears that layer, and the canvas's background(_:) keeps every layer run recorded before it: with any layer run in the frame, keepOnlyLayerRuns rebuilds the shared lists from the layer runs alone, every implicit start, explicit range, SDF group node range, and web source rebased, so the frame is byte for byte the one whose canvas drew nothing before the wipe; a frame with no layer run takes the old full wipe untouched; BackgroundKeepsLayersTests, nine plants red); target.image composites it back; target.filtered(_:) → a new layer; postProcess(_:) filters the whole frame. A blur or glow radius (.gaussianBlur/.bloom/.halation) is a pixel count, blurred at sigma max(0.1, radius), so a share of the canvas written there spreads nothing; Drawer.noteSubPixelSpread notes a radius in (0, 1) once on both roads (postProcess, recordFilter), zero stays silent as the way to ask for no spread (FilterSpreadNoteTests, 3, sabotage-verified). Four examples shipped a fraction there and never glowed. Filter is a catalog in six families — blur/glow, color/tone, stylize/optical, retro, uv-warp distortion, and the design filters (.flutedGlass/.water/.paperTexture/.melt transform the layer; .liquidMetal/.heatmap/.gemSmoke read its alpha shape — draw a shape, filter it); .channelMixer(_:), .lensDistortion(amount:quartic:center:fillsFrame:), and .cornerPin(topLeft:topRight:bottomRight:bottomLeft:) are three single-pass entries sharing one rule, the identity changes no byte (ChannelMixerTests, LensDistortionTests, CornerPinTests, each with a sabotage gate): the mixer is a ColorMatrix (four rows of [r, g, b, a, offset], .gray/.gray(red:green:blue:)/.swapping(_:_:)/.identity) applied with its color weights on the premultiplied color and scaled by the new-over-old alpha ratio, written as exactly one when the alphas are equal, because dividing the alpha out and back in is fast-math and moved two rim bytes; the lens is the Brown-Conrady radial read r(1 + k₁r² + k₂r⁴) with r a fraction of the far corner's distance in aspect-corrected space, the read written as the pixel's own uv plus a displacement that is exactly zero at zero coefficients, fillsFrame a CPU scale of one over the largest factor on the boundary (a quadratic in r² over near²…1, so an endpoint or its vertex), and a CPU inversion of the model lands a dot grid within 1 px; the pin packs the internal Homography.shaderInverse (the same map Installation.Projection runs on the present pass) as three rows, a nil map (no area) as zeros so w ≤ 0 draws nothing; both warps read through ollin_fx_read_within, transparent past the layer's edge with a 1 px fade placed a quarter pixel outside it so an identity read at half a pixel from the edge keeps full coverage; a black pixel reads 0 or 1 after the present pass's dither, so a test for an empty pixel says <= 1; snapshot lens-pin-mixer; the web page probes Mixed/Distorted/Pinned in WebEffectsTests; Guide Ch 20 § Through a lens, onto a wall, and through a mixer: lensDistortion, cornerPin, and channelMixer; .brushwork(radius:stretch:sharpness:) is the anisotropic Kuwahara filter (Kyprianidis, Kang, and Döllner 2009 with the polynomial sector weights of Kyprianidis, Semmo, Kang, and Döllner 2010, both written from the paper, credited in ATTRIBUTION.md): three passes in MetalRenderer+Effects.swift (the Sobel structure tensor of the display color over white paper summed across RGB, its Gaussian blur at a fixed sigma of 2, and the sector filter, whose brush is an ellipse of radius drawn out to r·(α+A)/α along the tensor's minor eigenvector and squeezed to r·α/(α+A) across it, cut into eight overlapping sectors by [(x+ζ) − η·y²]₊² under a Gaussian of sigma 0.4 on the unit disc with ζ = 2/r and η at 3π/2N, the sector means blended by 1/(1 + s^q) with the spread s on the 8-bit scale the paper assumes). Four rules, each pinned by BrushworkTests: stretch is the brush's aspect ratio at full anisotropy, so the paper's α is 1/(√stretch − 1) and a stretch of 1 is a round brush whatever the tensor says (the stripe invariant: three-texel stripes keep their full contrast under the stretched brush and lose three quarters of it under the round one, which holds only if the stretch follows the tensor's own direction); the means are of the premultiplied linear color, so the result composites and keeps the layer's alpha, while the variance is of the display color over white paper (a transparent region reads as paper, and the sharpness exponent is on the scale the paper quotes); the tangent is ollin_xdog_tangent's two-spelling eigenvector, since the textbook (λ1 − E, −F) is zero on a vertical edge; and a hard edge stays within two columns and blurs across four only when sharpness is 0, the counterfactual for the sector weighting. Multi-pass, so the page export refuses it by name. Snapshot effects-brushwork; Examples/Effects/Brushwork + the FilterCatalog tile; Docs/Drawing/Effects.md; Guide Ch 20 § Paint that follows the picture: brushwork. .xdog(radius:sharpening:threshold:softness:flow:foreground:background:) is the flow-based extended difference of Gaussians (Winnemöller 2012 over the structure-tensor flow of Kyprianidis and Döllner 2008, both written from the paper, credited in ATTRIBUTION.md): four passes in MetalRenderer+Effects.swift (the Sobel structure tensor of the brightness, its Gaussian blur at a fixed sigma of 2, the one-dimensional (1+p)·G_σ − p·G_1.6σ taken across the tangent, and the response gathered along the tangent under a Gaussian of flow texels, then cut), so it stays with the renderer and the page export refuses it by name. Four rules, each pinned by XDoGTests: the brightness is the sRGB-encoded luma of the layer composited over the paper (the background's luma, a transparent background counting as white), so the threshold sits on the display scale and empty space is paper, never ink; the tangent is the perpendicular of whichever spelling of the major eigenvector does not vanish on an axis-aligned edge (E ≥ G ? (λ₁−G, F) : (F, λ₁−E)), since the textbook (λ₁−E, −F) is exactly zero on a vertical edge and only worked through the flat-texel fallback; the walk along the tangent keeps its heading through the stored sign's flips (the line-integral rule) and stops at the layer's edge, so a straight edge gathers nothing and the flow-0 and flow-6 renders of one are identical to the level; and the cut is the published 1 + tanh((u−ε)/s) averaged over the pixel's footprint through its running integral (s·ln(1+e^{2(u−ε)/s}) below the threshold), so a softness of 0 is still anti-aliased and a ramp wider than a pixel is exactly as written. Snapshot effects-xdog; Examples/Effects/InkDrawing; Docs/Drawing/Effects.md; Guide Ch 20 § A line where there is an edge: xdog; .shock(iterations:flow:radius:smoothing:threshold:) is coherence-enhancing filtering (Kyprianidis and Kang 2011, written from the paper, credited in ATTRIBUTION.md): a round of six passes in MetalRenderer+Effects.swift, repeated iterations times (the color structure tensor as brushwork takes it, kept from the previous estimate where the gradient's magnitude is under the paper's relaxation threshold of 0.002 so a flattened region still knows its flow; its Gaussian blur at a fixed sigma of 2; the line integral convolution along the tensor's minor eigenvector in unit second-order Runge-Kutta steps with the tensor sampled bilinearly, under a Gaussian whose sigma adapts to the pixel's anisotropy as ¼·flow·(1+A)²; the tensor and its blur again over the smoothed layer; and the gradient-directed shock across the major eigenvector, whose sign is a scale-normalized one-dimensional Laplacian of Gaussian at 0.75·radius made zero-sum so a straight ramp reads as no curvature, negative past threshold taking the brightest texel center within radius along that direction and positive the darkest, from the layer or, when smoothing is set, its blur), then one convolution along the flow at a fixed sigma of 1.25 over the last tensor to anti-alias the steps. Seven tests, each pinned by ShockFilterTests: a flat tone is left alone; a smoothstep edge narrows and more rounds narrow it further (a straight ramp only clips at its ends, since its interior has no curvature, which is why the probe bends); a hard straight edge stays put in every row; noise along a line averages away while the line keeps its contrast against the gap beside it (the walk follows the line); a noisy edge straightens and its sides calm; a red-to-blue blend loses its middle and never makes green (no color is invented, since every pixel is a blend along its contour or a texel the layer holds); and empty space reads as paper and stays transparent. The paper's line-thickness rule holds: a feature thinner than the Laplacian's scale is fattened to it, so a shaded rim becomes a bold contour, and dense noise turns into the maze its Figure 7 shows unless smoothing blurs the sign's source. Multi-pass, so the page export refuses it by name. Snapshot effects-shock; Examples/Effects/Coherence + the FilterCatalog tile; Docs/Drawing/Effects.md; Guide Ch 20 § Regions with crisp edges: shock; .hatching(spacing:length:directions:foreground:background:) draws the layer as pen work along its own flow (the line integral convolution of Cabral and Leedom 1993 over the structure tensor, cut into strokes by the tone-as-density rule of pen-and-ink illustration, credited in ATTRIBUTION.md): three passes in MetalRenderer+Effects.swift (the color structure tensor brushwork takes, reused as-is; its Gaussian blur at a fixed sigma of 2; the hatching, which walks that flow once per direction). The rank is the load-bearing part: a box average of value noise of cell size spacing along the walk is bell-shaped, not uniform, so the average is standardized by the noise's own measured spread (0.046) over the count of independent draws the walk took (its samples divided by spacing, since that is how far it travels before it reaches a new cell) and pushed through the normal distribution as its tanh form, which Metal carries and erf is not; the answer is near enough uniform on 0…1 that inking every mark under the pixel's darkness covers that share of the paper, which is what makes the drawing hold its tone. Layer 0 runs along the tensor's minor eigenvector, 1 across it, 2 between them; every layer but the last is capped at half the paper and hands on (D − c)/(1 − c), so a second direction crosses the first instead of thickening it and the tone still comes out right. The cut is anti-aliased over the rank's own screen-space derivative, so every rank is computed before any is used (a walk skipped where the paper is light would take a pixel out of the quad the derivative is read from). Seven tests, each pinned by HatchingFilterTests: the ink covers as much of the paper as the picture is dark at a quarter, half and three quarters (within 0.05, and within 0.03 on a canvas big enough to hold the sampling error); white stays bare and black fills; the marks run along the flow and turn when the picture turns (verified red by walking across it instead); half the spacing lays more than twice the strokes; a second direction crosses the first and both reach the same tone (verified red by returning the raw average, which is the same sabotage the tone test catches); nothing but the two colors is on the page; and empty space counts as paper, so background: .clear leaves it empty. Multi-pass, so the page export refuses it by name. Snapshot effects-hatching; Examples/Effects/Hatching; Docs/Drawing/Effects.md; Guide Ch 20 § Strokes that follow the picture: hatching; .antialias(amount:threshold:quality:) is the post-process pass for a layer a fragment shader wrote per pixel (a Generator, a raymarched field, an imported shader, a finished chain), which none of the four anti-aliasing paths reach: a brightness range test that returns early, a second-difference test for whether the edge runs across or down, a walk along it to both ends with a growing stride, and the low-pass term for a lone pixel with no span (FXAA, written from the paper, credited in ATTRIBUTION.md). Four rules, each measured. Every tap names level(0) (the early return is exactly the case that leaves a derived mip level undefined). The orientation test is tie-broken by |lN-lS| against |lW-lE|, because a shader writes hard two-value edges that tie the published sums constantly, and >= alone then answers across for every edge including the ones running down (the steep probe is red without it). The brightness the walk compares against is read from the layer at the half-pixel tap, never averaged from the two neighbors already in hand: this layer is linear-light where the technique assumes display-encoded input, and across black and white an averaged brightness and the brightness of the average differ by 0.235 against an end test of 0.25, which ends nearly every walk at its first step. The signal reads the layer over a mid-gray backdrop (a black silhouette on a clear layer carries premultiplied color 0 on both sides of itself; sabotage-verified) and sRGB-encoded on past 1 rather than clamped there (a layer is linear and the tone map is at the present pass). amount: 0 and a layer under the threshold are both byte-identical. Measured on a 1-in-8 slope: edge wander 0.286 px to 0.140 px, half rather than none being what a pass reading the finished image can buy; it cannot tell a stair-step from genuine one-pixel detail, which is why it is placed rather than automatic. Example Effects/Antialias, snapshot effects-antialias, AntialiasFilterTests (8); Docs/Drawing/Effects.md. .melt is the luminance melt (one displacement vector both warps the field's domain and shifts the layer sample — keep them shared, that coupling is the recipe; sampled brightness mixes into the field before the 4-stop sRGB ramp; the sway sines are non-commensurate, so it never perfect-loops — example Images/LuminanceMelt, snapshot luminance-melt); Chromatic aberration is a mode axis, not one look (ollin_fx_chromatic, one fragment for the whole family): .magnify (default; a per-channel scale about the center, byte-identical to the pre-mode filter) / .lens(radius:falloff:) (a shaped radial slide; radius: 0, falloff: 1 is .magnify, which is why the default is the named scale rather than a degenerate lens) / .offset(angle:) (flat, the only mode that splits the frame's center) / .edges (along the luminance gradient, scaled by its strength; a negative amount gives the cool purple-fringing halo) / .axial (a 12-tap golden-angle disc per wavelength: focus, not position; the sign picks which end stays sharp), plus spectral: (quality-sized wavelength taps, 7/15/31 via resolveDispersionTaps) and the aux-driven Combine.disperse / .dispersed(by:). Load-bearing: the spectral weights are three cosine lobes normalized per channel (over 15 taps green collects ~5.0 and red ~3.0, so an un-normalized loop tints every flat area green), three taps reduce to the exact R/G/B basis so the shipped picture is unchanged, every tap is unpremultiplied before its channel is read and repremultiplied by the pixel's own coverage (else a soft-edged layer grows a dark rim; a fully transparent texel still carries no color to borrow), the split is computed in a space scaled by (aspect, 1) and written back to uv (.magnify needs no correction because a scale about the center is already aspect-free, so the design note's claim that the shipped filter was anisotropic was wrong; .lens / .offset / .edges do need it), and every mode is an exact identity at amount: 0. Not done and deliberately so: dispersion is not read off a camera the way .defocus takes its blade count from the depth layer's, because a Filter runs on a layer and there is no camera attached to one. Example Effects/Dispersion; snapshot effects-dispersion; DispersionTests (9, three sabotages verified red); Guide Ch 20 § One filter, five pictures: chromatic aberration + the Dispersion figure. full per-filter list in Docs/Drawing/Effects.md; new built-ins are written from the published technique, credited in ATTRIBUTION.md, never in .swift. .droste is the picture inside itself (ollin_fx_droste; the Escher construction Lenstra and de Smit completed): the ring between inner and the layer's edge is straightened by a complex log, repeated along the strip, and mapped back by exp. The twist is a complex multiply in the log plane, not a shear ((1 - ik)·w, k = twist·period/2π): one turn around the middle then advances twist periods along the strip and exactly one turn of the source, which is what keeps the map single-valued and the spiral closed (dropping the second component leaves the picture no longer self-similar, sabotage-verified). The wrap is x -= period·ceil(x/period), so the sampled radius stays in [inner, 1) and the hole is never read (a marker disc inside inner must not appear in the output). zoom counts periods, so zoom + 1 returns the same picture, which is what makes an endless fall loop exactly. The claim the tests measure is the theorem, not the look: the picture is unchanged by one scale-and-turn, log γ = period/(1 - ik). Only |z| ≤ 1 in half-height units is ever sampled, so the layer's corners never appear, and the join between copies shows unless the content clears both edges of the ring (the example and the Guide figure both end the ring on flat ground). Example Effects/Droste; snapshot droste; DrosteTests (6, two sabotages verified red); Guide Ch 20 § A picture inside itself: droste + the PictureInsideItself figure. .diffuse is the diffusion-curve solver (Orzan 2008), and the only filter that makes a picture rather than changing one: the layer's own drawn pixels are Dirichlet sources and everything else relaxes to the average of its four neighbors (drawDiffusionCurve(_:left:right:) lays the two-sided form). Four load-bearing pieces: the constraints are carried down a premultiplied pyramid (color × w, w) instead of being resampled per level, and each level sums its 2×2 block with the weight clamped at one, so a block with any held texel in it is a hard wall at every coarser level (the sum is the operators' scale — a texel a level up stands for four times the curvature — and a weight that fell off with the size left the coarsest levels nearly free to drift, so the correction they sent up grew with each cycle and a 1080 canvas went white in three); the ladder runs down to 4 texels, not 32 — Jacobi error falls with the square of the size, so the bottom is where the long waves are solved and it is nearly free (starting at 32 left a closed ring's interior stuck at three quarters of its color, aRingFillsWithTheColorItIsDrawnIn pins it); the coarse-to-fine cascade is only a first guess and multigrid V-cycles do the solve — the finest level's residual is restricted down (×4 a level, zero at a wall), the correction is relaxed at each level with every held texel a wall of zero correction, and prolonged back up where a texel is free — because a cascade alone hands a region held by one side of a thin curve the mean of both sides (a hairline's two sides land in one coarse texel), and sharpness buys cycles (3 at 0, 6 at 1; each takes about half of the far field's remaining error, since the residual against a wall lands in the wall's own block half the time and is dropped there; aCurveHoldsItsFarSideOffTheAlignment pins the far side at a curve that is not on a power-of-two texel boundary, the alignment that hid the leak); and every neighbor sample names level(0) since a fragment may sample past a branch its quad did not take. The tests are the theorems: away from the marks a pixel equals its neighbors' average (worst deviation ≤ 6/255), no color appears that was not put there, and a closed mark fills its interior. Docs/Drawing/Effects.md, example Effects/DiffusionCurves, snapshot diffusion-curves, Guide Ch 21 § A picture made of a few marks: diffusion. Generator + generate(_:) fills a procedural pattern into a target: the four basics (.noise takes a warp: domain-warp parameter — 0 skips the warp path so it stays byte-identical), the .cellular Worley source (.cells/.borders/.mosaic styles; feature points wander sinusoidally on hashed orbits, so phase is periodic over 2π and a whole lap loops — example Effects/Cellular, snapshot noise-toolkit), the .gaborNoise source (sparse Gabor convolution from Lagae et al. 2009: a Poisson field of Gaussian-enveloped cosine kernels, so the spectrum is designed by wavelength in pixels, bandwidth as a fraction of the frequency, angle, and spread (0 one direction, π isotropic); impulses per kernel is the quality, phase periodic over 2π, and a field at angle + π is the same field. Load-bearing: the output is normalized by the paper's closed-form variance, so the mean is 0.5 and one standard deviation is a sixth of the range at any impulse count; every kernel is filtered against a σ = 0.5 px Gaussian footprint in the frequency domain (the product of two Gaussians is a Gaussian, so the filtered kernel is a Gabor kernel with a lower frequency, narrower band, and less gain), which is why a wavelength driven toward two pixels fades to gray instead of aliasing and why the contrast at 2.5 px matches the closed form to 1%; the per-cell impulse stream is a Borosh-Niederreiter congruential generator seeded by an integer hash of the cell and a 24-bit seed, stepped in single precision on both sides, so the CPU GaborNoise (the typed value; gaborNoise(x, y, ...) the sugar) computes the field the GPU paints to within the dither (pinned) and a sketch can place marks by a field it never reads back; the seed is a parameter, not noiseSeed's, for the same reason; the pass carries the layer's pixel size and render scale so a 2x export keeps the picture; the shader-library gaborNoise(p, ...) is the same code, and the fragment translates for the web page; example Effects/GaborNoise, snapshot gabor-noise, GaborNoiseTests (11 laws: mean and sigma, the spectral peak at 1/wavelength, the band narrowing, one direction is one direction, spread π isotropic, phase a period and half of it a negation, seeds, the closed-form fade, GPU = CPU)), the design-pattern set (cross-read against Paper Shaders, written from the canonical techniques, per-technique credits in ATTRIBUTION.md), the pattern fields, and the escape-time fractals plus .orbitTrap (the same iteration colored by the orbit's closest pass to a point/cross/circle/square trap through an exp glow ramp, the trap turned by angle; escaped and trapped pixels color alike, the escape test only ends the orbit; example Effects/EscapeTime, snapshots escape-time and orbit-trap; all five complex-plane generators frame the plane one way, center in the middle, 3 / zoom units across the shorter side, the imaginary axis up with the uv's y negated once on the way in, which the escape-time pair and the orbit trap did not until 2026-09-27 (they read the axis down and nothing said so, so a Julia with c off the real axis drew mirrored and the Guide's marked-c figure marked the conjugate point; measured per generator before the change, then flipped at @eaviles's ruling); ComplexPlane (Math/ComplexPlane.swift: center and span, init(center:zoom:) with span = 3 / zoom, canvasPoint(of:in:) and planePoint(at:in:) against the rectangle the picture was drawn in undistorted) is that framing as a value, and Generator.plane hands a generator's own back after the factory's clamps (nil for a generator with no plane), so a mark lands on the picture without the sketch redoing the arithmetic; ComplexPlaneFramingTests (11) pin the round trip, the shorter-side rule, and per generator that a feature at a known number (the unit disk's edge, a Julia's CPU twin, a Mandelbrot window off the axis, an orbit-trap knot, a zero's winding, a Newton root) sits within a pixel of where the plane says, with the mirrored mapping red on each), .newton (Newton's method from every pixel over a polynomial given by up to eight placed roots, the step the reciprocal of the sum of 1/(z - rᵢ) so no coefficient is ever formed; each pixel colored by the root it lands on off the palette wheel at i/n plus phase, shaded by a continuous step count (the quadratic-convergence renormalization, clamped to one step for the linear cases) with a contour per step; a pixel within a thousandth of the framed span of a root has landed, one that never lands or hits a critical point paints trapped, relaxation scales the step; example Effects/NewtonBasins, snapshot newton-basins, NewtonBasinsTests (7 tests: root inside its basin, the third-of-a-turn symmetry, the Wada property at 95% of boundary pixels within 12 px, the z³ - 2z + 2 pools, shading, phase, eight roots)), and .domainColoring (a complex function painted over its own plane: the value's direction walks a wrapping palette wheel ollin_pat_wheel — the last stop blends back into the first, so a turn has no seam; the plane is written with the imaginary axis up, so the uv's y is negated once on the way in, and every placed zero/pole then reads as written — ComplexFunction is .rational(zeros:poles:) with up to four of each packed two per params row (so the colors always start at row 7) plus .power/.exponential/.sine/.tangent/.logarithm; DomainShading .modulus/.conformal rule size and direction off |f| and arg f directly, never the palette position, so phase only ever recolors; the argument principle is the test, not the look: a simple zero must lap the wheel +1 and a pole -1, a double zero +2, z^3 +3 — DomainColoringTests pins it, verified red by removing the y flip; example Effects/DomainColoring, snapshot domain-coloring); generate(_:width:height:) fills an explicit-size layer so patterns compose per-layer instead of squashed full-canvas fills (the Shader and Visual generate forms have the same overloads). Feedback — makeFeedback(scale:) → a persistent Feedback layer (made once in setup() and held — its identity carries the ping-pong state across frames) + withFeedback(_:) { prev in … }. Simulation fields — makeSimField(_:scale:) → a persistent SimField running a Sim (.reactionDiffusion(feed:kill:) Gray-Scott / .predatorPrey(halfSaturation:predatorGrowth:predatorDeath:steps:) the Rosenzweig-MacArthur predator-prey kinetics on the same 9-point diffusion, prey in red and predators in green over a prey-at-capacity rest state (so a drawn green mark is a release), the dimensionless form where the prey's growth rate and the capacity are 1 and Holling's saturating response u/(u+h), one explicit Euler step of the fixed Sim.predatorPreyTimeStep (0.05) per substep and steps substeps a frame, the state clamped to 0...4; PredatorPreyTests pins the period of a uniform field's prey mean against the model's own Jacobian stepped as the Euler map (within 3%), the invasion front's steady outward pace and the oscillating wake behind it, and the full-prey rest state / .gameOfLife Conway / .lenia(...) continuous CA / .smoothLife(radius:birth:survival:intervalSoftness:cellSoftness:) Rafler's SmoothLife (the disc of radius ra/3 and the ring out to ra, each an area-normalized filling with the paper's one-texel anti-aliasing ramp at every rim, the sigmoid steps 1/(1+exp(-(x-a)·4/α)), the transition σ2(n, σm(b1,d1,m), σm(b2,d2,m)) and the discrete time-stepping where the new state is the transition itself; the defaults are the paper's glider parameters, ra 21, birth 0.278...0.365, survival 0.267...0.445, αn 0.028, αm 0.147; SmoothLifeTests pins the rule texel for texel against a double-precision CPU reference over two generations, the two fixed points (empty stays empty, full dies in one generation), and a notched-disc seed that keeps its mass within a band and moves the same vector between checkpoints, the glider) / .ripples(speed:damping:) wave-equation pool / .sandpile(pour:topplings:) Bak-Tang-Wiesenfeld / .fallingSand(passes:friction:) Toffoli-Margolus block-CA sand, water, and walls / .fluid(...) real-time fluid / .multiScaleTuring(scales:seed:) McCabe / .cyclic(...) Griffeath's cyclic CA / .excitable(...) Greenberg-Hastings / .briansBrain() Silverman / .forestFire(growth:lightning:neighborhood:) Drossel-Schwabl (empty/tree/burning; starts bare and grows itself in — no seed fill, and the per-cell coin is hash12(cell, field age) so a run replays exactly; with both rates at 0 the rule is deterministic, which is what StateAutomataTests checks cell for cell against a CPU reference, since a GPU hash has no bit-exact CPU twin; rates at 1 pin the other branch, and the corner test separates the two neighborhoods — both sabotage-verified) / .hodgepodge(...) Gerhardt-Schuster's oscillating-reaction machine / .wireworld() Silverman's computing automaton (empty/conductor/tail/head; a conductor fires on exactly one or two heads, which is the clause a diode and every gate are built from; the field starts empty and a circuit is typed as text and stamped, WireworldCell naming the four grays; the eight-cell count cuts a ring's corners, so a nine-by-five ring is a clock of period twenty) / .schelling(preference:vacancy:mobility:passes:seed:) Schelling's segregation on a bounded board (empty/first/second, the seeded fill holding vacancy of the cells empty through a third stateSeedFill field, negative for the even split cyclic/hodgepodge keep, so those fills stay byte-identical; an agent is content when its like share reaches preference, an unhappy one hops to an empty cell within an 8x8 block that walks half a block per pass, the sand's Margolus scheme scaled up because 2x2 blocks jam at any vacancy a board can show, content cells taken first in raster order with the mover's own cell counted vacated; the per-agent coin is hash12(cell, pass, seed) under mobility, so at 1 the rule is deterministic and StateAutomataTests matches it cell for cell against a CPU reference, and at 0 nothing moves) / .ising(temperature:field:sweeps:seed:) the Ising model by checkerboard Metropolis (down/up as 0/1, the seeded fill an even split; a pass updates one parity of x + y, matching the pass index's parity, from the other parity's four wrapped neighbors, two passes to a sweep and sweeps per frame; the flip energy is 2 s (neighbors + field) in -1/+1 spins, taken when it is <= 0 and otherwise when hash12(cell, pass, seed) < exp(-change / temperature), the pass count running on across frames from the field age so a run replays; at a temperature of 0 no coin is thrown, which is the deterministic limit StateAutomataTests matches cell for cell against a CPU reference, tie flips included, and what the Quenched page-versus-Mac case runs; Sim.isingCriticalTemperature is Onsager's 2 / ln(1 + sqrt 2) and the default; the magnetization and coarsening invariants were pinned from a numpy prototype first, 0.999 cold and under 0.03 hot after thirty sweeps from an ordered stamp, the seeded mix within 0.4...0.6 after one deterministic sweep, and field at 4 magnetizing every spin in two sweeps; IsingSpin names the two grays) / .selfWarp(amount:refresh:decay:smoothing:) motion feedback — the picture's own measured motion drags the field's history) that you seed/force by drawing into it (withField(_:_:), force:); SimField.image is the raw state, .filtered(_:) recolors it through the Filter catalog, and reading either registers the field so it steps (Drawer.ensureFieldSteps) — a sim only evolves while its layer is one of the frame's render targets, which used to mean a withField block was mandatory, and a self-organizing sim that needs no seeding would otherwise sit frozen and read black with no hint why (an empty-block wart the Turing work surfaced; existing sketches register via withField first, so target order is unchanged). A simulation of your own is Sim.shader(_:inject:substeps:rest:): a user Shader compiled as the .simStep variant of the user-shader wrapper (readers cell(info) / cell(info, dx, dy) / sampleRaw(info, p) / input(info, i) over the state bound at texture 0 and up to SimField.maxInputs extra layers from SimField.inputs (a drawn or generated layer as this frame left it; a filtered/combined output one frame late, since filters resolve after the sims: keepDelayedInputs blits each one, after the filter ops, into a DelayedInputSlot per field and slot that the next step reads, empty on the first frame or after a frame it did not resolve, pruned with the field; UserSimTests.aFilteredInputArrivesAsItResolvedTheFrameBefore, red under a skipped copy and under the old blank), a raw tail that stores the returned float4 as data with no color conversion, and two rows the renderer appends after the user's 16 parameter rows (ollin_params[16] = texel/pass/age, [17] = substeps/input count) so OllinShaderUniforms never widens), with an optional inject kernel (.simInject, mark(info) = this frame's marks un-premultiplied in linear light, coverage in .a) that runs where the block drew, which is how gpu-io's region steps map onto Ollin's drawing model: the region is what withField drew, and the kernel is the inject; without one, ollin_sim_inject_data lays marks on by alpha and keeps the state's alpha (a kernel may store data there). SimField.edge (FieldEdge, .wrapping default / .clamped / per axis) is the sampler, not a shader branch: the twelve wrapping sims (Sim.honorsEdge) tap uv + d * t with no fract, and the renderer binds simSampler(for:) (repeat or clamp-to-edge per axis, the image sampler's filtering) for their passes, which is byte-identical for the torus (a tap at a texel center lands on the same texel under either sampler; StateAutomataTests' cell-for-cell toroidal references held unrecorded) and lets a clamped border cell read itself past the edge (an insulated boundary for a diffusion, and what UserSimTests pins against a CPU reference under every axis pair); ripples, the sandpile, the falling sand, Schelling, and the multi-pass pipelines keep the image sampler. The web page mirrors it by setting REPEAT/CLAMP_TO_EDGE on the two scratch textures the steps read while they run (WebSimNode.wrapsX/wrapsY, JSON wx/wy), then clamping them again for the pool. precision: .float32 on makeSimField threads the field's format through the slot, the scratch pool (acquireFilterTexture(format:)), every pass's pipeline (encodeEffectFragment(format:), the user pipeline's hash carries the format) and the seed pair: the marks draw through pipelines built for the target's format, so a seed MSAA/resolve pair left at half float read back as noise (alpha 1.875 on a full mark; the first run of the precision test). SimField.snapshot() returns a FieldSnapshot (width/height/values row-major, a clamped subscript) through a reader closure the renderer sets each frame it serves the slot (weak self): a blit of the slot's front into a shared buffer, committed and waited for, so a call from draw() reads the state the last frame left; UserSimTests pins readback == image on two instances of one deterministic run. Filter.arrows(spacing:scale:color:width:) (ollin_fx_arrows, a singlePass, carried to the page) reads any two-channel layer's rg at each cell center and draws shaft plus barbs as segment distances over the 3x3 neighborhood, the shown length held to the spacing so that neighborhood is every arrow that can reach a pixel; ArrowsFilterTests pins direction, count, and the empty field. A user sim is refused by the web export by name (its stepFragment is empty). Snapshots effects-simfield-shader (clamped heat plate with an inject and an input layer) and effects-arrows; example Simulation/Wind; ShaderCheck infers the two kernel shapes (simulation from cell, inject from mark). Modulated regimes — .reactionDiffusion(feed:kill:toFeed:toKill:) + SimField.modulation: a layer's brightness lerps feed/kill per texel (black → the first pair, white → the to pair), so one continuous field runs different regimes in different places and the pattern crosses the boundary instead of seaming (a camera matte as the map is the Vision/TuringMirror example; snapshot effects-simfield-modulated). Load-bearing: the modulated step is a separate fragment selected only when the map layer was drawn this frame (membership in the frame's render targets, which resolve before the sim passes) — the plain path stays byte-identical, an undrawn map falls back to the uniform black-end field rather than sampling stale, and a filtered(_:) output can't be a map (filters resolve after the sims). Params row z/w carry the to pair; x/y stay the plain pair the unmodulated kernel reads. Self-warp — .selfWarp is a multi-field sim like the fluid (own SelfWarpSlot: previous clean source + quarter-res flow pair + history pair, evolved by runSelfWarp; example Simulation/SelfWarp, snapshot effects-selfwarp): draw the scene into the field and a GPU two-level windowed least-squares motion fit (Lucas-Kanade, on luminance, coarse sixteenth-res for reach then quarter-res refinement, temporally steadied by smoothing, flow stored in uv units so passes are resolution-independent) measures how the picture moved since last frame, then a semi-Lagrangian pass carries the history along it and mixes refresh of the fresh drawing back in. Load-bearing: strength picks the regime and 1 is nearly invisible — below 1 the picture outruns its history (ribbons trailing the motion, the default 0.6), at 1 an accurate fit lands the carried ghost exactly back under the mover, above 1 ghosts are thrown ahead, negative reverses (SelfWarpTests pins both directions on an overshooting dot plus the exact still-scene null — a static picture solves to zero displacement, so the field converges to the drawing); the first frame primes source and history from the first seed (zero measured motion, not a jump from black); and the motion is measured on the GPU by design, never routed through Vision per frame (a per-frame canvas→Vision→texture trip breaks the substrate's no-CPU-round-trip rule, cadence, and determinism — FlowTracker stays the measurement tool). A main-canvas background() before or after withField leaves the seed (see the layer rule above). Depth-capturing targets — a target a 3D scene is drawn into resolves a sampleable depth buffer; target.depth is a gray layer (0 near … 1 far) that feeds .defocus (set the camera near/far to bracket the scene); two-tier opt-in so 2D never pays. Compose DSL (Effects/Compose.swift) — compose { layer { … }.post(_:).blend(_:).scale(_:) … }, composited bottom-to-top in declared order; pure sugar over the substrate. Multi-input combine (Effects/Combine.swift) — base.combined(with: aux, op) with nine ops: .mask / .displace / .disperse / .mix / .seamlessClone / .lineIntegralConvolution / .defocus (near/far-separated bokeh-gather depth of field; artifact history in [[ollin-depth-of-field-gather-artifacts]]) / .ambientOcclusion (SSAO over depth + the mesh-normal G-buffer) / .screenSpaceReflections (SSR: trace → resolve → temporal → composite); aside { } + .masked(by:) / .displaced(by:amount:) / .mixed(with:amount:) / .cloned(from:) / .streaked(along:length:field:) / .defocused(by:) is the combine's compose sugar. .lineIntegralConvolution(length:field:) (ollin_fx_lic in ShaderCombine.metal) brushes the base along a direction field read from the aux layer: each pixel is the box average of the base along the streamline through it, walked both ways in Euler steps (length is a fraction of the layer's long side; the step count is capped at 128, the step stretched to keep the length). Three readings of the aux (Combine.FieldEncoding): .vector (rg recentered at linear 0.5, a normal map or a drawn flow), .angle(turns:) (luminance times turns as a heading, noise into swirls), .contour (the perpendicular of the central-difference luminance gradient, so streaks run along a gradient's level lines). Every field is unoriented: a step whose direction points back against the last heading is flipped, which is what carries a walk through an .angle field that turns past a half turn (aFieldThatTurnsBackIsWalkedThrough goes red without it); the walk stops at a zero field or the layer edge, so a still field returns the base byte-for-byte and the length is honest at the border. All taps name level(0). One gotcha with a hard tone edge in an .angle field: the half-covered AA texel reads as the mid angle, a wall along the edge that traps walks, so harden a seam with .threshold(0.5) first (the probe does). LineIntegralConvolutionTests (6) pins the smear across a stripe, the whole stripe along it, the still field, the radial contours, the turn-back, and the length clamp. Example Examples/Effects/FlowStreaks; snapshot line-integral-convolution; Docs/Drawing/Effects.md. .seamlessClone(amount:threshold:) is gradient-domain compositing (Perez 2003) in the membrane form: the aux layer's own alpha is the region, its rim carries base - patch as boundary values, the harmonic field between them is added back under the patch's coverage, so the rim lands exactly on the base (S + (T - S) = T) and the inside is the patch plus the smoothest correction reaching it; amount 0 is a plain paste, the honest before picture. The solver is a convolution pyramid (Farbman 2011), not the .diffuse Jacobi ladder, and the split is load-bearing in both directions. A pyramid interpolates scattered values (a source votes, weighted by coverage) and settles in ~23 passes against Jacobi's ~210; a relaxation's source is held, so it walls off what is on either side of it. The clone has one closed rim with nothing to separate, so it takes the pyramid; diffusion curves are named for that wall, and under a pyramid a curve one texel wide stops dividing its sides (a red/blue probe returned blue 172 on the warm side against a limit of 110 - aCurveCarriesADifferentColorOnEachSide pins it; no weighting fixes it, since at the coarse levels both sides land in one texel and get averaged). Three non-obvious pieces, each a real defect first, each pinned: the pyramid's intermediates are rgba32Float (PipelineKey.effectFormat, defaulting to the shared linear format so every other effect pass is byte-identical) because the fitted kernels are unnormalized (h1 sums to ~2.7, so magnitudes climb ~7x per level down and fall ~20x per level up) and a layer's worth of levels leaves half float at both ends - the tell is underflow, the weight channel reaching zero deep inside a wide patch so the normalize has nothing to divide by and the raw patch returns as a speckled blob ringed by concentric ripples, exactly where the answer should be smoothest (worst error at radius 240 on a 512 layer: 179/255 -> 1/255); rescaling h1 to sum to one is an exact identity and no help (scale h1 by s, take h2 to h2/s^4, level zero is unchanged) since it only trades the overflow end for the underflow end; and the normalize's floor sits at 1e-30, not a comfortable small number, since a legitimate deep-interior weight lands around a millionth. The tests are theorems, not tastes: with a flat backdrop the rim values are all equal so the field between them is that same value, and the clone must hand the backdrop back (<= 2/255 incl. the present pass's dither); the patch's own flat color must leave no trace whatever it was; and the middle of a wide patch must be as right as its edge. Envelope, both from what it does rather than the implementation: only the low, slow part of the color is replaced, so the patch's range of tone is kept and a contrasty patch on a much darker backdrop clips its shadows to black; and the rim is the whole answer, so a rim across a hard edge in the base smears that edge inward. Docs/Drawing/Effects.md, example Effects/SeamlessClone, snapshot seamless-clone, SeamlessCloneTests. Load-bearing invariants (terse — mechanism and why in ARCHITECTURE.md § Layered-effects substrate + § Screen-space combine effects): the Drawer stays a pure recorder and the renderer resolves the deferred graph at render()/image(of:) time, entirely GPU-resident (layers are attachments, filter inputs sampler binds, never a CPU round-trip), premultiplied linear, the no-targets path byte-identical; every GeometryBatch begin snapshots all the *Start offsets (omitting one zeroes a preceding batch's count — the SceneDefocus bug); withTarget/withFeedback/withField push/pop the drawing state around the block (a blendMode set inside a layer block must never leak to the canvas — was a real bug); design-pattern fragments animate by an explicit phase param (deterministic export, no hidden clock) and blend palettes internally in sRGB (mixed in linear they read washed-out pastel — a real bug); Feedback.filtered(_:) mirrors SimField.filtered(_:); Sim.params is an array of SIMD4 rows the renderer binds after the texel row (a new sim never widens the runSimulation call site, it returns more rows), SimField.sim is a public var re-read every frame (live growth parameters; the evolved state carries), and Lenia is the paper-faithful continuous CA (exponential kernel core in up to 3 β-rings, normalized in-loop by the summed weight, gaussian growth, clipped dt integration; Orbium defaults R=13/T=10/μ=0.15/σ=0.015) whose dying / labyrinth / rings / maze looks are all real regimes — seed it densely (sparse mass starves, the documented envelope, not a bug); .ripples is the height/velocity interactive-water kernel (state h in r, v in g, both signed; 6 substeps; clamped taps, soft absorbing border rim) whose per-sim inject is load-bearing: Sim.injectFragment routes it to ollin_sim_inject_height, which adds the drawn mark's premultiplied luminance to height only — the generic replace-by-alpha inject pins the surface and writes color into the velocity channel (the exact bug that parked the 2026-07-07 attempt, see [[ollin-ripples-sim-parked]]); the coupling gain speed must stay under 2 (clamped to 1, default 0.5): 2 is the CFL boundary where the Nyquist checkerboard sits on a repeated λ=−1 root and grows linearly, settling ~22× the ring amplitude under any damping and shading into glitter — substeps went 3→6 so the ring pace is unchanged (wave speed ∝ √gain); soft radial-gradient dabs are the drop contract (hard discs legitimately ring at every frequency), and the raw image is a debug view (shade with .relight); .sandpile is the Bak-Tang-Wiesenfeld toppling CA on the generic single-texture step path (no dedicated pipeline; Dhar's abelian property licenses any parallel schedule, and SandpileTests pins the settled pile cell-for-cell against a sequential CPU reference, the edge variant verified red against its counterfactual) with five terse invariants: the step is the k-fold parallel form q' = fract(q) + Σ floor(q_n)/4 — topple as many times as a cell can per pass, identical to single toppling in the critical regime but load-bearing at a hot source, where single toppling pools for thousands of passes (a saturated blob's interior is net zero: lose four, receive four back); grains stored in quarters (stable cells read 0/¼/½/¾, gradientMap-ready; exact in half float to 2048 grains), state read from .r only (a luminance dot is off by an ulp and the toppling floor/fract must be exact), boundary open — reject the position before sampling, never wrap or clamp (the clamping sampler reads the edge texel back as its own neighbor, a reflecting wall; on a torus sand accumulates until every cell topples forever), and the inject adds rint(luma·pour) whole grains (an AA fringe pours whole grains or none; nothing erases); topplings is substeps, the pacing dial (an avalanche front moves one texel per pass), the classic relaxed figure is drop-and-relax (one heavy mark on one frame, then settle — a steady heavy pour keeps a molten super-critical core by design), and runSimulation binds sim.params to the inject pass too (pre-existing injects read none — byte-identical); .fallingSand (ollin_sim_falling_sand + ollin_sim_inject_grains in ShaderSim.metal; SandMaterial names the four levels) is the Toffoli-Margolus block CA on the generic step path: every cell settles its whole 2x2 block from the same four reads and writes back only its own corner, so the update is conflict-free with no ordering. The origin walks the block's four corners over four passes (x flips every pass, y every second; passes is rounded up to a multiple of four so a frame runs every tiling equally), which sets the pacing (a grain falls one cell per two passes, water spreads one cell per pass, each diagonal roll comes once per four) and is load-bearing: with only the two diagonal tilings a cell owns a single diagonal and half the grains on a heap's edge can never roll off it, so a pour stacked a solid tower over a proper cone (seen in the first render, fixed by the four-corner walk). The block rule is falls first (the heavier top cell sinks: sand through empty and through water, lifting the water), then in a block where nothing fell one diagonal roll taken with probability 1 - friction by one hash per block seeded by the field's frame age (FeedbackSlot.age, not the renderer's frame index, which cycles mod 3), then water swaps with an empty cell beside it in either row (a surface cell drifts one cell per pass in a parity-decided direction, so a pool spreads as a wedge and levels over time); the field's edge is a closed box (outside reads as wall). The inject snaps in sRGB terms (linearToSrgb before rounding to thirds) so SandMaterial.color (Color(white: raw/3)) lands as the named material through the linear-light layer, alpha-gated so an AA fringe writes nothing. FallingSandTests pins the rule cell-for-cell against a CPU walk of the same block rule (falls, the sink through water, the roll and its order after the falls, the spread, the closed edge, the four-corner walk: reverting to the two-tiling alternation, swapping the roll and spread order, or rolling in a block where another cell fell all turn it red), plus the tower at friction 1 and a cell-identical replay; headless image(of:frame:) has run passes * (frame + 1) passes. Example: the sand rule in Examples/Simulation/Automata; snapshot falling-sand; Docs/Drawing/Effects.md. the state automata (.cyclic / .excitable / .briansBrain / .hodgepodge / .forestFire / .wireworld / .schelling) share one encoding — integer state s of N levels stored as s/(N−1) in .r, decoded with rint (exact in half float well past N=200) — and three load-bearing pieces: .cyclic/.hodgepodge start as seeded random states via the first-allocation fill (Sim.stateSeedFill → ollin_sim_state_seed; a uniform field is a fixed point of both rules, so the Turing fill: hook generalized to initialNoiseFill), a probability roll draws ollin_uniform24 (PCG3D taken to 24 bits: the forest fire's growth and lightning, Ising's flips, Schelling's moves), never hash12, whose last float product lands on exactly 0 about once in 1,800 rolls and floors any chance below about 5e-4 (aSmallLightningStrikesAtItsOwnRate, aColdMagnetKeepsItsSpins), the draw-to-spark pair carries snapping, alpha-gated injects (ollin_sim_inject_excite/_brain, and the sRGB-snapping _wire/_kinds/_fire over ollin_sim_inject_levels for the rules whose grays the reference names (the two …Cell enums, and the forest fire's mid-gray tree, which the plain blending inject had landed in linear light as bare ground): a solid mark writes the nearest meaningful state, a soft fringe writes nothing, though two one-texel rims beside one gap reach the three-quarter gate, so a dense stand is planted by its outline (aMidGrayRectPlantsATreeAndLeavesItsNeighborsBare) — a blended AA rim decodes as refractory/resting cells that block every birth, the bug that killed the first Brian's Brain seed), and .hodgepodge is Dewdney's formulation of Gerhardt-Schuster (healthy ⌊A/k1⌋+⌊B/k2⌋; infected ⌊S/(A+1)⌋+g, S summing the cell and all its neighbors, the +1 counting the cell itself among the infected — the divide-by-zero guard; ill → 0; everything caps at n). A solid blob legitimately dies under Brian's Brain (the interior rests together and a flat edge shows three neighbors where a birth needs exactly two) — sprinkle loose soup instead, a rule fact verified on a CPU model before suspecting the renderer. StateAutomataTests pins each rule cell-for-cell against a sequential CPU reference (both neighborhood shapes, the toroidal wrap, the injects; sabotage-verified red), with two probe gotchas worth keeping: per-texel stamps must be drawPolygon unit quads, not drawRect (an SDF rect's ~1-render-px analytic halo leaves edge-adjacent texels hovering at the injects' 0.5 alpha gate), and headless image(of:frame:) runs frame+1 generations (it renders draws 0...frame inclusive); .relight measures slope over a span, not a texel (taps on the half-texel grid for a free 2×2 box, gain divided by the span, span growing with resolution past 1080) — slope weights a wavelength by 1/L, so a per-texel Sobel lights few-texel content as steeply as the real relief; smooth relief is preserved to ~1% (effects-relight did not need re-recording). Don't soften a finish's specular exponent to chase speckle — a broad lobe lifts the highlight on flat areas too (a pow(sh,60) .liquid washed the Guide's Organism background several shades lighter), so fix the normal, not the lobe; headless image(of:frame:) renders every warmup frame when drawer.usesFeedback (feedback / sim fields / SSR — else the ping-pong never evolves); fluid keeps its own fluidSlots beside FeedbackSlot so the single-field sims stay byte-identical; .multiScaleTuring is McCabe's rule (activator/inhibitor disc averages at several scales, least-disagreement scale fires, renormalize each step) and runs its own multi-pass pipeline (runMultiScaleTuring) like the fluid while sharing the one-pair FeedbackSlot (so flip/prune/repeat are the shipped ones), with four load-bearing pieces, each of which was a real defect first: the field starts as seeded noise, not a constant (a flat field is a fixed point of the rule, so feedbackSlot grew a fill: override beside restState); the blur pyramid is Gaussian (Burt-Adelson binomial), not a 2x2 box (a square kernel has square preferred directions, so the labyrinth came out rectilinear); a radius gathers a 9-tap disc two rungs finer than the radius match, never a single tap at the matching rung (that rung holds one sample per feature, so the pattern locks to the lattice, unfixable by any kernel since the information isn't there); and variation is averaged over a per-scale variationRadius (read at a point, a fine scale's disagreement crosses zero along every contour of its own structure and, since least disagreement wins, it claims a dense web of pixels and buries the coarse scales). Two parameter facts are equally load-bearing because renormalization couples the scales: equal amounts across rungs (the hardest-pushing rung sets the range and squeezes the rest toward mid gray) and a ladder starting at radius 2 (radius 1 works on single texels and reads as speckle). TuringScale.maxScales (6) must equal OLLIN_TURING_MAX_SCALES, and MetalRenderer.turingPyramidLevels (12) must equal OLLIN_TURING_LEVELS (it sizes the texture-array binding; a shallower pyramid repeats its top rung to fill it). Deliberate v1 cut: the winning-scale index is not carried in the state (color-by-scale would muddy a channel the display and the extent reduce both read as gray); layer(_:)'s draw closure is @escaping @_implicitSelfCapture (without it, bare draws inside layer { } need self. and the p5 feel breaks); SSAO's and SSR's stabilizer sets each fixed a worse artifact — don't undo any of them ([[ollin-ssao-normal-gbuffer-followup]], [[research-before-implementing]], [[verify-perceptual-temporal-fixes]]); DoF/SSAO/SSR quality is a RenderQuality tier resolving to a per-GPU budget (DoF: the bokeh tap budget via resolveDofTaps — M2 1080²: .default 192 ≈ 14 ms; benchmark back-to-back on a cool machine, single runs drift 50% under thermal load, enough to invert an A/B). .defocus is the canvas lens's gather over a layer (ollin_fx_lens_dof_layer_prepass + the ollin_fx_lens_* passes in Renderer/ShaderCombine.metal, the .defocus arm of applyCombine): the same five passes applyDepthOfField runs, fed by the layer prepass and the ramp law (zero inside focus ± range, growing with the distance from the band to maxBlur at the map's ends; growing to maxBlur one range past the band, the earlier law, gave every depth past a narrow band the same blur, the masked-blur look a sketch complained of; the gather's DofLaw names which law sizes a signed blur, affine in inverse distance for the lens and in depth on the ramp, so the continuity test holds for both, and sameness of depth is a hundredth of the range rather than of the distance). The layer prepass resolves a drawn map's anti-aliased rim to its nearer side: a pixel whose depth lies strictly between the nearest and the farthest within two pixels of it (past one gray level, so a smooth ramp is untouched) takes the nearest, which is what the canvas's nearest-sample depth resolve does on its own; the earlier two-size prepass (a neighborhood max the pixel received by, to consume the in-focus seam a rim leaves where it crosses the focus, and a radius-2 min it scattered by, to stop the rim flinging the color under it across its in-between blur) consumed a thin line in focus along with the seam (a 1.2 px bar kept 4% of its light). Measured on the study scenes against the earlier running-average gather (bars at eight depths, four small highlights, a near bar over a subject in focus, 512 px, .detail): light kept by a near bar 0.48 to 0.99 and by a thin bar in focus 0.04 to 1.00, a near highlight's light 0.27 to 0.98 with its peak over its mean 6.8 to 1.3 (an even disc rather than a core in a halo), the rim at an in-focus silhouette 1.099 to 1.000, and the near bar over the subject 0.0365 to 0.0229 RMS against the analytic layered reference at each law's own radius (the canvas reads 0.0222 at the lens's). What the layer filter still shares with the canvas's far field: a far object much sharper than the backdrop behind it keeps a little less of its light than the canvas does (a bar at a tenth of the backdrop's blur 94.9%, a highlight 91.1%; 84% under the averaged model). maxBlur under half a pixel hands the layer back untouched. The opening is a parameter of the gather, not a second path (blades / irisAngle / catsEye, now carried in the gather's fourth parameter row): the reach test divides the tap distance by how far the opening reaches that way (ollin_dof_aperture), so a round opening with no cat's eye takes the arithmetic the canvas takes (the canvas asks for neither, byte for byte). A polygon is taken at the area of the round opening it replaces (R = sqrt(pi / (n sin(pi/n) cos(pi/n)))), so a blade count changes shape, not size, and the tile size and the gather's rim move out to maxBlur * R or the corners are cut off. catsEye intersects two discs pushed apart by pinch along the line to the frame's middle (reach sqrt(pinch^2 c^2 + 1 - pinch^2) - pinch |c|, c the cosine to that line), capped at 0.9 so an opening never closes; it changes shape only, never brightness (.vignette darkens), which holds only because every density in the gather and the sprite draw divides by the opening's own area (ollin_dof_opening_share: (2 acos p - 2 p sqrt(1 - p^2)) / pi of the round opening for a round one, the rim walked at 64 angles for a bladed one; pi with no pinch, so the canvas pass and a round layer are byte-identical), never by pi r^2: divided by the round disc's area, a pinched disc carried only its clipped share of the light, whole inside its own silhouette (normalized by its own weights) and that share outside, a step at every near silhouette toward the frame's edges (a fifth of the brightness across four pixels on the Defocus example's largest disc, found by profiling one disc against the lens's convolution; theBarrelKeepsTheLight, theBarrelLeavesNoStepAtASilhouette). A flat disc follows the lens's convolution within 0.015 at every radius in both paths (aDiscFollowsTheLensAtEveryRadius in DefocusTests and DepthOfFieldTests, against lensDiscCover in LensDiscSupport.swift, the overlap of two circles): the first thing to run when a blurred edge looks wrong, before any theory about the composite. The depth gray is read perceptually, and that conversion is the filter's (ollin_dof_depth is linearToSrgb of the luminance): Color(white: 0.47) in a linear-light layer is stored as 0.19 and read back as 0.47, so the gray a sketch draws is the depth it means, and ollin_fx_depth_normalize sRGB-encodes a target's .depth so it reads back the same; a sketch never converts. blades defaults to the camera's own Camera3D.apertureBlades, stamped on the depth layer through DepthReconstruction, so the live blur, the flare ghosts, and the path-traced export share one lens; a hand-drawn depth map carries no camera and stays round. Envelope: the shape is only as clean as the tap budget, so a source smaller than the tap spacing (sqrt(pi/budget)·rim, ~13% of the rim at .default) is found by some taps and missed by others, which the moved spiral and the median mostly even out, and a hexagon's corners (13.4% of the rim) sit right at that limit. ApertureBokehTests pins it (reach ratios 0.952 flat / 1.0996 corner, the tangential-vs-radial cat's-eye split, the camera stamp), all verified red by sabotage. The snapshots' mean-diff tolerance averages every one of these away (they all live in a band around a silhouette), so DefocusTests pins them behaviorally — reach for a probe here, not a whole-frame diff. A layer that carries additive light wants an opaque background. .add sums alpha as well as color (sourceAlpha: .one, destinationAlpha: .one), so an additive draw over already-covered pixels leaves the layer's alpha above 1, and compositing it back (src·a + dst·(1 - a)) then subtracts what is behind: a white sphere under an additive glow came back green over magenta. Clear the layer with background(_:) inside the withTarget block so it composites as a picture, or add the glow at the canvas instead of inside the layer. Credits: AsyncGraphics + ofxFX + orx-fx/OPENRNDR + Apple MPS (ATTRIBUTION.md, never in .swift). Docs/Drawing/Effects.md; examples Effects/* (Layers / FilterCatalog / GeneratorCatalog / GaborNoise / Glitter / MeshGradient / Feedback / Aside / FlowStreaks / Defocus), Simulation/GrayScott / Automata / Ripples / Fluid, 3D/Effects/SceneDefocus / AmbientOcclusion / ScreenSpaceReflections; snapshots effects-* + line-integral-convolution + lenia + ripples + sandpile + falling-sand + cyclic-automaton + excitable-medium + brians-brain + hodgepodge + wireworld + schelling + mesh-gradient / design-patterns / design-filters / scene-defocus-3d / ssao-3d / ssr-3d. RipplesTests pins the wave sim behaviorally (rest stays rest, rings expand, channels decay). The film look is two filters: .halation(threshold:radius:tint:amount:) reuses bloom's bright pass and MPS blur and adds the tinted halo back through ollin_fx_halation_combine weighted per channel by the room left below white (1 - saturate(b.rgb), the alpha by the max-channel key), which is what makes it a fringe around a white core rather than a glow over it; an empty bright pass adds exactly zero, so a frame with nothing above the threshold and an amount of 0 are byte-identical (pinned, and the room rule is pinned over a red field where the ring's green must rise four times its red). .filmGrain(amount:size:seed:) (ollin_fx_film_grain, a single pass) is the Newson-Delon-Galerne Boolean grain model in its Gaussian limit applied per channel on the encoded picture with a residual apply into linear (a value above white keeps its excess): the spread is 2·amount·sqrt(e·(1-e)) with a 1e-6 dead zone on the tone weight (the encode of white lands a float's rounding under 1 and flips eight-bit ties, the ColorLUT lesson), so black and white are byte-identical and the mean of a flat region holds to the level; the draw is a size-pixel lattice of triangular values under a quintic fade, divided by the spread its own weights predict so every pixel carries the same spread at any size (pinned: 25.5 levels at amount 0.1 whatever the size) and set half a pixel back so size 1 is white noise; the seed reaches the shader as Filter.grainPattern(of:), a hash of the Double's bits under 4093, so a fractional seed is its own pattern and a frame count never rounds in a float. On the web page halation is the third page-owned pass (ollin_web_halation, its add-back binding two rows) and the grain crosses as data. Example Effects/FilmLook; snapshot film-look; FilmLookTests (14, three sabotages verified red); the Filmed fixture in WebEffectsTests. A layer's write target names its Feedback / Accumulator / SimField weakly (RenderTarget.Owner), and the drawer holds each owner it records until the frame renders: the strong back-reference was a cycle that kept every such layer and its GPU slot for the life of the process (leaks over the feedback example found it), and the slot tables prune on a dead owner. Layer precision: makeRenderTarget(precision:) / makeFeedback(precision:) take LayerPrecision.float32 (rgba32Float) for a layer whose values are sums; the format is carried in PipelineKey.colorFormat (nil for the canvas and every half-float layer, so those keys are untouched), the target pool and the feedback slot key on it. Precision follows the source through a chain: recordFilter / recordCombine give the output the input's (the base's) precision, the filter loop sets MetalRenderer.filterChainFormat from each op's output around that op only (so a frame filter or a sim step never sees it), acquireFilterTexture's default format reads it, and encodeEffectFragment / encodeUserShader key the pipeline on the output texture's own format when none is named, so no pass in a float32 chain meets a half-float pipeline (only fragment, MPS, and explicit-format passes live under applyFilter / applyCombine; a blit or a geometry pipeline added there would have to follow the same rule). No example or Guide figure renders through a float32 layer, so nothing published moved. DistanceFieldTests.aSinglePrecisionLayerMeasuresFarDistancesToAFractionOfAPixel reads a dot's field 1,400 to 1,408 px away on 1080x1920: 0.29 px of scatter in half float (the rounding), about 0.1 in float32, through a blur too, and red when the chain format is planted nil; precisionFollowsTheLayerThroughAChain pins the recorded precision. combined(with:_:) on Feedback, Accumulator, and SimField forwards beside filtered(_:) (the sim field's keeps it stepping); PersistentLayerCombineTests draws each against the .exposure(stops: 0) hop it replaces, pixel for pixel, plus a blank-base twin that must differ. Accumulator (makeAccumulator / withAccumulator(_:passes:), Origin.accumulate): the block renders like a geometry target into a transient layer and one compute dispatch (MetalRenderer.accumulateKernel) adds it into a persistent rgba32Float sum and writes the rgba16Float mean beside it, dividing by the passes the drawer counted at record time (passesAtRecord, so a repeat encode of one frame divides by the same count and adds nothing); reset() flags a replace consumed at the next encode; usesFeedback includes it so a headless drive renders every frame; the web export refuses it by name. Filter.develop(exposure:ground:) is the sandpainting print: exposure, Reinhard, the ground added after the curve as display components, the result written as the display value (srgbToLinear of it into the intermediate). Accumulator.developed / LineSpray.developed run it over the mean. LightAccumulationTests pins the mean (six passes read as one, passes: divides, a reset restarts), the float32 feedback climbing where float16 stalls, and the print recipe.
  • Depth of field from light (Sources/Ollin/Effects/LineSpray.swift; the sandpainting lens over the substrate above). LineSpray (makeLineSpray(_:sampling:passesPerFrame:bokeh:) / drawLineSpray(_:)) renders [SprayLine] (world-space segments carrying linear RGB light / endLight per pass as SIMD3<Double>, the stored value, with the color:intensity: initializer converting a tone on the way in through Color.linearRGB, and a weight) as depth of field: each frame one dispatch of pointsPerPass × passesPerFrame threads (LineSpray.kernel, over the buffers: form: the OllinSprayLine records, a point-to-line table, the particle buffer of a two-deep PingPong) samples a spot on each line, takes it into camera space, pushes it by ballSample × max(strength × defocus^power, minSize) with the defocus measured along the view axis, fades it by exp(-attenuation × defocus), and projects it through ollin_project_eye; the particles go into the spray's Accumulator under .add in the .light style with passes: passesPerFrame. Load-bearing: every point deposits its line's share (start.w = 1 / points on the line), so a line's light per pass is its color × intensity under either Sampling (.perLine(n), or .byLength(pointsPerPass:) sharing by length × weight with at least one point per line, LineSpray.pointCounts); a point's alpha is 4 / (π × pointSize²), so its whole light lands in its disc and the exposure is independent of pointSize; a Signature of camera, Bokeh, point size, canvas size, and line count resets the accumulator on any change (Blurry's reset-on-control-change); setLines is a per-frame call: the same lines again return before anything happens (SprayLine is Equatable), so a sketch rebuilding its scene every draw converges under --settle, changed lines mark the spray and reset the average, and the GPU side is brought up to date in record at most once per frame (refresh(frame:), keyed on the sketch's frameCount): while the per-line point counts are unchanged the point table is kept and the records are rewritten in place (ComputeBuffer.replaceContents, internal) into a ring of MetalRenderer.maxFramesInFlight record buffers, one slot per frame, so a slot is only rewritten after every frame that read it has completed (the same count the vertex ring rests on; a second draw of one frame rewrites the same slot rather than advancing), and changed counts rebuild the records and the point table, never the particles: the PingPong every point lands in is grow-only with a sixteenth of headroom in 4,096-point blocks (particleCapacity(for:)) and is handed back only under a quarter full, since both dispatches and the draw take count outright and nothing reads past it. Sized to the count exactly (!=), a .byLength scene whose lengths change (rings that breathe, where one that only turns holds its totals) drifted by a few points a frame and remade both buffers every frame, 576 MB of fresh zeroed shared memory at 6M points, which wedged a live desktop (found by a sketch in use; 80 exported frames: 11.9 s to 10.2, system time 2.1 s to 0.7, peak 959 MB and climbing to 642 MB flat). The tables stay allocate-on-change on purpose: they are CPU-written, an in-flight frame may be reading the old one, and a breathing scene measured no dearer than a still one (setLinesEveryFrameKeepsOrRestartsTheAverageAsTheLinesAsk pins the kept average, the reset, the ring's depth, the kept table, the rebuilt table and the kept particles under .byLength, and that a rewrite reaches the GPU by the light's centroid following the lines); Bokeh.power is a continuous exponent (1 and 1.5 are the reference's two settings); no camera means no draw. developed(exposure:ground:) prints the mean; with the reference's deposit (a one-pixel point, its radiance into its pixel) the exposure that matches its print is 1 / 0.0019 ≈ 526. Surfaces beside the wires (SprayQuad, setQuads(_:)): a quad is the parallelogram two edges span from a corner, sampled over its area through its own kernel (LineSpray.quadKernel, OllinSprayQuad, stride 80) and its own point-to-quad table and record ring, so a scene can be made of faces rather than a wireframe of one. Load-bearing: lines and quads are one picture (both dispatches write into one particle buffer, the quads at an offset the params carry, and one withAccumulator block records the lot, so what the two draw together is what they draw apart, added, inside the print's slack); a quad's light is the whole quad's, not per unit area (corner.w is one over its point count, the line's rule one dimension up, so a one-unit and a hundred-unit quad at the same light deposit the same light per pass); under .perLine(n) every primitive gets n points while .byLength gives the quads a pool of their own shared by area times weight, since a length and an area are not the same units; the quads keep their own lastQuadWriteFrame so one refresh cannot consume the frame the other tests against. The aperture has a shape (Bokeh.aperture, Aperture): .round (the ball, unchanged), .blades(count:rotation:) for an iris of straight blades (.blades(6) is the hexagonal sparkle; under three reads round), or .picture(_:) for a mask read across the hole, which covers an iris a sketch drew into a render target and one it loaded alike, since both are an Image (nothing is bundled). Load-bearing: the blades are sampled exactly and without rejection (bladeSample: one draw picks the wedge, two place the sample in that wedge's triangle with a square root, since every wedge of a regular polygon has equal area), so nothing is thrown away and there is no per-sample texture fetch; an aperture moves where a sample lands and never how much light it carries (a hexagon, a triangle and the ball print the same total light, pinned below the print's ceiling, since a shape that crowds its middle clips and reads short); a mask is read at the spot the sample took, so the mark's own center moves toward the side the hole is open on (the invariant is the centroid, not the skew: a half disc's skew about its own mean is small, which cost a wrong instrument before the right one); .round is byte-identical to what it drew before the aperture existed, which is what let this ship without moving a committed image, and the probes fix randomSeed because the spray takes its per-frame seed from the sketch's generator. Blades scatter in the aperture's plane with no depth jitter, and a flat hole spreads a little wider than the ball, whose projection crowds the middle. Four sabotages verified red (an ignored rotation, blades falling back to the ball, a mask read away from its sample, plus the quads'). Measured on a real 1080x1920 portrait frame rather than only derived: three defocused marks read 1.035, 1.059 and 1.053 wide against tall at -19.8, +61.1 and -9.7 degrees, and it is the scatter of the angles that says round, since a projection that disagreed with the viewport would elongate every mark along one axis; the residual few percent is neighbors in a busy frame. .blades(5) reads as a pentagon by the same frame's radius-against-angle profile, a cosine at 5 cycles a turn standing ten times its own neighboring harmonics where .round raises none. A probe that reads aperture shape off a rendered spray must pick the dots, never the brightest marks: a traveling line blurs to an open smear with no closed edge, which loses half a half-max walk's rays, and brightness selects for exactly those. A picture the quads read (LineSpray.picture, SprayQuad.pictureBounds): the two random numbers that place a sample on the surface also read the texel at the matching spot, so a photograph or a glyph sheet becomes light the lens defocuses; bound through ImageComputeTexture over the image's own linearTexture(for:), so a picture a sketch already drew is uploaded once and every Image backing answers; a pictureBounds of no size is what tells the kernel to read nothing. Read from the reference implementation's surface, written here (the sampling, the budgets and the texel read follow its shape; credited in ATTRIBUTION.md). LightAccumulationTests pins the point counts, the defaults, convergence (the mean after twelve frames within a tenth of two), the pass count, the reset on a lens change, the settled export (a still and a sequence counting N passes per written frame), the quads' area shares (a parallel-edged quad has no area and falls to one point), their light invariant, the patch a quad carries, and the additivity of the two primitives in one picture. A turning camera in an export wants --settle N, since every frame restarts the average. Docs/Drawing/DepthOfField.md; examples Rendering/LineSpray (the reference's own scene and viewport, built from its description) and Rendering/DepthOfField (the same pipeline from the primitives).
  • The frequency domain (Filter.fourier(of:) / .inverseFourier() / .spectrum(gain:); Renderer/ShaderFourier.metal, the ladder in MetalRenderer+Effects.fourierLadder). The 2D discrete Fourier transform over a layer, both ways, as a Stockham autosort radix-2 gather: each rung reads two texels and writes one, so a fragment shader runs it with no scatter and no bit-reversal pass, rows then columns, 2 . log2(n) passes. Written from the published algorithm, credited in ATTRIBUTION.md, never in .swift. Load-bearing: the ladder ping-pongs through rgba32Float (the pooled acquireSATTexture pair) because the sum a transform builds is the whole picture added up and a 512 square's low frequencies run to hundreds of thousands, far past half float's useful range; a texel carries two complex fields (rg and ba) and the butterfly transforms both at the price of one, which is what lets the ocean carry its height in one pair and its two sideways shifts in the other; the forward pass ends with a quadrant shift so the lowest frequency sits in the middle (a mask drawn over the spectrum then covers what it looks like it covers) and the inverse begins by undoing it; the 1/n normalization is applied in the last rung of each axis, so it costs no pass of its own, and the ocean asks for the raw sum instead (normalize: false), since its spectrum is written as wave amplitudes rather than as a transformed picture; a layer that is not square with a power-of-two side comes back untouched, with the note printed at Drawer.recordFilter (where the sketch's own call is) rather than silently as a black rectangle; .fourier(of:) names the channel because a transform works on one signal and a color layer is three, so the round trip is gray. Frequency-domain filtering needs no new op: spectrum.combined(with: mask, .mask()) multiplies both parts by a real mask, which is exactly a filter by scale (with the ringing a hard cut brings, documented). Round trip measured inside one 8-bit level on a 256 square. FourierTests (5: the round trip, a grating landing at its own frequency on each axis, a flat field as one spot in the middle, the refusal path); snapshot fourier-spectrum; example Effects/Fourier; Docs/Drawing/Fourier.md; Guide Ch 21 § A picture read as waves: the Fourier transform.
  • Measured distance fields (Filter.distanceField / Filter.fieldMap; the passes in ShaderEffects.metal, the ladder in MetalRenderer+Effects.measuredDistanceField). The counterpart of the authored SDF: a field measured back out of a drawn layer. Two Filter cases and no new plumbing — .distanceField(from:threshold:maxDistance:) writes r = signed distance in pixels (negative inside), gb = the unit direction to the nearest edge, a = 1, so pixel + dir * abs(d) is the edge point (which is what an outline, a push-apart, and a picture-keyed Voronoi actually read; a distance-only field would be redone the first time one was wanted), and .fieldMap(_:from:to:repeating:) reads it back through a Ramp/Colormap over a pixel window (repeating wraps → contour bands; a ramp turning over at one distance is dilate/erode; a narrow band is an outline). Jump flooding (Rong & Tan 2006) with the leading step-1 pass of 1+JFA (2007), credited in ATTRIBUTION.md, never in .swift. Load-bearing: the seed is the sub-pixel crossing found between two pixels (linear interpolation to the axial neighbor across the threshold), never the pixel that holds it — snapping to the center grows every measurement by a whole extra pixel and steps the field along the grid (theEdgeIsFoundBetweenPixelsRatherThanAtTheirCenters + cuttingTheFieldGrowsTheShapeByThatMuch both go red on that sabotage); the ladder ping-pongs through rg32Float (a half float spaces integers a whole unit apart past 1024, which would quantize the sub-pixel seed away), and the resolve writes a layer of the measured layer's precision, so a .float32 source keeps far distances to their fraction (see Layer precision) pooled per ring slot like filterTexPool; the first rung is the smallest power of two whose ladder reaches far (a rung k plus every halving carries 2k - 1), so maxDistance genuinely shortens the work (12 passes over a 1080 square, 8 at 64 px); every tap is a clamped read through ollin_field_tap rather than bounds-branching (same answer — a clamped tap is still a real seed position and the pass keeps the nearest — for ~15% less GPU, since the ladder is read-bound; it differs from the branching form only at the canvas border, 44 px at ≤2/255); it replaced a clamped pixel-space filter::nearest sampler so the page can run the same fragments, measured 1.00x that sampler in one process with the GPU saturated (2.28 to 2.30 ms against 2.26 to 2.34 a whole-canvas field), and the three passes return float4 (the rg32Float targets keep two components) for the same reason; a pixel the flood never reached reads the far value with a zero direction ("nothing within reach"), never a direction pointing nowhere. The user-shader seam had to grow a raw read: sample/sampleAux return linearToSrgb(unpremul(c)), which mangles a distance and cannot carry a negative, so sampleRaw/sampleAuxRaw (ollin_layer_read) hand back the stored values untouched — without them the direction channel is unreachable from any user code. Cost on an M2 at 1080 square: ~4.9 ms GPU whole-canvas, ~2.9 ms capped at 64 px. Two test-writing traps, both paid for: a drawn circle's half-covered contour is not at its nominal radius (thin-mark coverage is remapped to perceptual alpha, putting it ~0.26 px out), so accuracy is checked against the shape itself rather than against the radius it was asked for; and a hard pixel count of a disc wobbles ~0.08 px of radius with where the grid falls, so areas are summed from an antialiased cut in linear light instead. The ladder is data (MeasuredField: source, threshold, reach, the row, the steps), read by the renderer and by the web recorder, so the page runs the identical ladder: ollin_web_field runs ollin_field_seed/ollin_field_flood/ollin_field_resolve over page-owned RG32F targets read by texelFetch (NEAREST filters, or the texture is incomplete and reads 0), falling back to the page's half-float layers where EXT_color_buffer_float is missing. DistanceFieldTests (7, two sabotages verified red); snapshot measured-field; example Effects/DistanceField; Docs/Drawing/DistanceFields.md; Guide Ch 21 § A field you measure: the distance field.
  • Layer styles (Filter.outline / .dropShadow / .innerShadow / .outerGlow / .innerGlow / .bevel; ollin_fx_outline/_glow/_bevel/_shadow in ShaderEffects.metal, described as data by Filter.styledPass). A layer's edge given a style read from its alpha: a preparation (the measured field of the alpha cut at 0.5, held to the style's reach plus 2 px, or the hardware Gaussian of the layer, sigma = radius as gaussianBlur) and one composite pass reading the layer at texture 0 and the preparation at 1. Filters on the chain, ruled 2026-10-08 (no style value that groups them): chained order is stacking order. Every style is one of two premultiplied composites, written so weight 0 is the layer's own bytes (c + 0): under (c + paint·k·(1 - c.a): outside outline, outer glow, drop shadow) and inside (c + (paint·c.a - c)·k: inside outline, inner glow, inner shadow, bevel); never an unpremultiply-and-lerp (not exact at 0, aClearColorOrAZeroReachHandsBackTheLayersOwnBytes goes red). The layer's AA edge is composited once by its own coverage, so no seam. Outline: a 1 px ramp at |d| = width off the field, centered = two half-width bands; below 1 px the band's ink is held to its width by 6w / (1 + 3w + 3w² - w³) (a box-filtered edge carries w²/2 + w/2 + 1/6 - w³/6 unscaled, a sixth of a pixel at width 0). Glow: (1 - d/r)², faded in over the first pixel of radius. Shadow: the blurred alpha read at position - offset from the four nearest texels with zero past the border, so a whole-pixel offset lands on one texel and the shadow slides off the canvas; inner = 1 - that, past the canvas counts as empty. Bevel: steepness is the profile's at the measured distance (rounded depth·π/2·cos(πt/2), chiseled depth with its crease AA'd over one pixel), direction is the field's gradient by a Sobel stencil at 2 px spacing, never the direction channel: the field is the distance to discrete edge points, so its direction (and a 1 px difference of it) turns by up to atan(0.5/d), 7° RMS on a disc in a numpy model, which lit draws radial streaks; the 2 px Sobel holds 0.8° RMS (2.5° max), and a ridge's shorter gradient levels the slope (outward = g / max(|g|, 1)); light normalized so facing = full highlight and taking none = full shadow, flat top exact (rise == 0 returns c). Measured precision (linear light, area-summed, against a 360-gon's symmetric edge; the analytic disc's perceptual edge leans 0.07 px for an inside band): one-sided outline within 0.06 px of its width, centered within 0.125 (near-edge field overestimate below 2 px, filed as the Gustavson-Strand AA distance transform); drop shadow bit-equal to the blur moved by drawImage (blurring the moved shape instead differs by the hardware blur's own position dependence, up to 8 half-float steps); glow band means within 0.004 of the curve, single pixels by the edge's coverage steps (~0.26 px of distance on 8x MSAA). Web: each crosses as ollin_web_field:<fragment> or ollin_web_blur:<fragment>, the first row the preparation's and the rest the fragment's; probes within 0.06 mean of the Mac. LayerStyleTests (13 tests, 25 cases; seven plants each red, plus the direction-channel plant at 0.080 against 0.0072 on the ring); WebEffectsTests (aDistanceFieldAndTheLayerStylesAreThePagesOwn, six probes); snapshot layer-styles; example Effects/LayerStyles; Docs/Drawing/LayerStyles.md; Guide Ch 21 § A field you measure: the distance field (shown; the family section waits on a structure review).
  • Light in a flat sketch (Combine.light / ComposeLayer.lit(by:); ShaderRadiance.metal, the ladder in MetalRenderer+Effects.radianceCascades). Radiance cascades over the effect substrate (Sannikov, credited in ATTRIBUTION.md): base = the scene (its alpha stops a ray), aux = the lamps (its rgb gives light off), out comes the light arriving at every pixel, an ordinary layer. Soft-with-distance shadows, 1/r falloff, beams through a gap, and color bleed all fall out of one measurement, and the cost tracks the layer and the reach, never how much was drawn. Rung i puts a probe every s0·2^i px with 4^(i+1) directions over a span of s0·4^i starting at s0·(4^i - 1)/3, stored as one 2^(i+1)-texel block per probe, so every rung is one texture of the same size and the ladder costs one rung times the rung count. Load-bearing: the probe grid is padded to a multiple of 2^(rungs-1) (a grid that cannot be halved that many times puts one rung's probes out of step with the next one's, which draws as a grid of cells rather than as light); each of the four merge neighbors is reached by its own ray, ending where that probe's own ray begins (the published "bilinear fix") or rings of light appear at every hand-over distance (measured against the exact asin(R/r)/π answer for a disc lamp: 0.900…1.108 of it with one shared ray, 0.902…0.952 with four, 0.926…0.972 once each ray is a cone — over 1 is light nobody emitted, and theLightMatchesWhatTheGeometryAllows goes red on that one-line sabotage); each ray is a thin cone, not a line (ollin_rc_trace, half the rung's angular spacing wide, halfTan): a surface it passes counts by the straight-edge share of the cone's width it covers, measured across the ray (the field's distance over the sine of the angle to its nearest edge), and a surface its axis enters is walked in one to three jumps to tell a wall crossed obliquely (opaque, however thin: the far face looks straight back) from a lamp's limb grazed (its share, then the march goes on), with a ray within 45° of head-on blocked outright. A line either meets a lamp or misses it, so a lamp nine rays wide read as eight, nine, or ten from one probe to the next and every penumbra moved in steps of a ninth, cells the size of the rung's probe spacing at every quality (halving the spacing moves the same lamp one rung up, so .detail did not cure it); two probes pin it, a one-ray lamp's shadow edge climbing without a dip (theShadowEdgeIsOneSmoothRamp, 0.054 → 0.018 of the peak) and reading evenly around a ring (0.113 → 0.072 RMS), and a lamp narrower than about three cones is the one case it errs on, bright; the field's alpha marks whether the nearest surface emits (ollin_light_mark, one pass per ladder run, since a bounce turns walls into emitters), which lets a ray skip reading a dark wall, and that skip pays for the cone: measured old against new in one process through live shader reload (the only GPU A/B that holds here: a separate process drifts 30% with the GPU's state, and frames submitted back to back inflate one buffer's span with its neighbors'), 0.94…1.02× at .performance/.default and 1.03…1.12× at .detail; the march is sphere-marched against the shipped measuredDistanceField, built once from the combined coverage (a lamp occludes too) and shared by every rung and every bounce, stepping max(d - 0.5, 0.5) because the field is half-float and a step longer than the truth walks a ray through a thin wall, and reading the emission 1.5 px inside the surface it stopped on; every tap names level(0) (the loop returns from inside a conditional, so there is no footprint to derive); the top rung is cut at reach (rungs come in whole steps of four, so a reach of 90 px would otherwise light 340 and the parameter would read as inert); and a bounce steps *outside* — a probe inside a shape meets it at once, so the field inside a wall is dark by construction, and the field's direction channels are what let a solid pixel read the light standing just off its own face (spread over 5 taps growing with depth, cosmetic only: no ray ever reads deeper than a shape's shell, so this decides only how a solid shows its own lit face, and a wide round shape still shows a soft seam where its two sides meet). Quality is probe spacing 4 / 2 / 1 px and 16 / 24 / 48 march steps; measured M2 release 1080² one bounce 11.3 / 23.4 / 79.6 ms (7.8 / 13.1 / 42.0 with none). LightTests (8, the merge-ray sabotage verified red); snapshot light-2d; example Effects/Light; Docs/Drawing/Light.md; Guide Ch 21 § Light that works itself out: .light. Mechanism and the measurements behind it: ARCHITECTURE.md § Light in a flat sketch (radiance cascades).
  • Local averages (Filter.boxBlur / Filter.adaptiveThreshold; ollin_sat_* + ollin_fx_box_blur + ollin_fx_adaptive_threshold in ShaderEffects.metal, the build in MetalRenderer+Effects.summedAreaTable). A summed-area table (Crow 1984) built by recursive doubling (Hensley et al. 2005), so the average over any rectangle is four reads whatever its size, and two Filter cases built on it with no new plumbing: .boxBlur(radius:), whose cost does not grow with the radius, and .adaptiveThreshold(window:bias:inverted:), the Bradley & Roth (2007) local cut (all three credited in ATTRIBUTION.md, never in .swift). Load-bearing: the table is rgba32Float and nothing narrower (a canvas of ones adds past a million; a half float spaces integers a whole unit apart long before that), pooled per ring slot like filterTexPool; elements go in biased by -0.5 (Hensley's own recommendation, worth one bit of mantissa) and the consumers add it back after dividing, since the mean of v - 0.5 is the mean of v less 0.5 whatever the window; a box that hangs over the border is clamped and divided by the area it really covered, or every edge darkens by the fraction that fell outside; the adaptive cut is a fraction of the local mean, not a distance below it, and is taken in linear light (illumination multiplies what a page returns, so only a proportional test in a linear space is invariant under it — the published form works on gamma-encoded bytes, where it is not), and its mean can come straight from the table only because luminance is linear (unpremultiplying first would make the mean a different quantity than the sample). The precision is the structure's one weakness and it points the useful way: a running total spends about log2(w * h) bits of the mantissa on itself, leaving an absolute box-sum error that the reader divides by the area, so it falls as the window grows — on a 1080 square, ~7/255 in the worst corner of a 3-px window and under 1.5/255 from 37 px up, which is why the docs send a narrow radius to gaussianBlur. Cost on an M2 at 1080 square: ~6.2 ms GPU to build (about twenty bandwidth-bound passes), so one a frame is comfortable and a dozen are not. Test-writing trap, paid for: the -0.5 bias makes a mid-gray probe nearly blind to an area error (its biased value is near zero, so the sums are tiny and dividing by the wrong area barely moves the answer — the sabotage read 3/255 at gray 0.72 where 0.25 and 1.0 read 7); flat-field probes sit at the ends of the range, never the middle. SummedAreaTests (5: a CPU Double table as the reference, flat-field drift at both ends, the illumination-invariance theorem with .threshold as its counterfactual, and the small-window precision bound; two sabotages verified red); snapshot summed-area; example Effects/SummedArea; Docs/Drawing/LocalAverages.md; Guide Ch 21 § Averages of a neighborhood, at a flat price: the summed-area table.
  • Fitting (RadialBasis + Fit.minimize, Sources/Ollin/Geometry/RadialBasis.swift + Fit.swift). Two plain-Swift substrate pieces the ROADMAP's technique tier named. RadialBasis<Point, Value> puts a smooth field through values known at scattered places: generic over the place (Vector2/Vector3 via RadialBasisPoint) and over what it carries (Double/Vector2/Vector3/Color via RadialBasisValue), six kernels (.thinPlate default, .linear, .cubic, .multiquadric, .inverseMultiquadric, .gaussian), smoothing for noisy data. Fit.minimize walks a handful of parameters downhill against a caller-written cost (adaptive per-parameter steps, Kingma & Ba 2015; central finite-difference slopes; optional per-parameter bounds). Techniques credited in ATTRIBUTION.md, never in .swift. Load-bearing: the system is the kernel block bordered by a degree-1 polynomial tail with a zero corner block, which is what makes a conditionally-positive-definite kernel solvable and what reproduces a flat plane exactly (aFlatPlaneComesBackExactly fails away from the points without it while still passing at them); the solve pivots, because that bordered matrix has zeros down part of its diagonal by construction and the first unpivoted division is a division by zero; the fit runs on points shifted to their centroid and divided by their own mean spacing (and any kernel scale divided to match), which leaves the answer alone — the scale-free kernels are homogeneous — but is what makes smoothing one parameter that means one thing: raw smoothing is not scale-free, and at 1 it did nothing over hundreds of pixels while flattening the same layout measured in fractions (smoothingMeansTheSameThingAtAnyScale pins it; a mean-of-|K| normalization is not enough for thin plate, whose r² log r picks up an extra log s under scaling); channels share one factorization, so a Color field costs a Double field; collinear points return nil rather than a guess (the tail is undetermined). Cost: the fit is cubic in the point count and each read is linear in it, so fit in setup(), read in draw(); minimize calls the cost about twice per parameter per step. RadialBasisTests (9) + FitTests (5), every one an invariant (interpolation, affine reproduction across all six kernels, smoothing monotonicity, scale invariance, channel independence, a known bowl, parameters on wildly different scales, bounds, a recovered circle); snapshot scattered-fit; example Shapes/Scattered; Docs/Drawing/Fitting.md; Guide Ch 14 § A field you pin down yourself: radial basis functions. Slopes: gradient(at:) (a field of numbers, returning the point type through its RadialBasisValue conformance) and derivative(at:along:) (any value, linear in the direction) come from each kernel's closed-form φ'(r)/r (Kernel.slopeOverDistance), zero at a data point (thin plate's limit, the linear cone's choice), and cost about one value read. Read against central differences of the value at 1e-6 relative over all six kernels, smoothed and not, a fitted plane's gradient exact everywhere, a warp's Jacobian columns, and a color field's per-channel rates; thin plate's + 1 is an equivalent mutant (the side conditions, Σw = 0 and Σw·x = 0, cancel its whole contribution), so no test can see it. FieldSlopeTests (6), three plants red; example Motion/ContourWalkers; Docs/Drawing/Fitting.md § The slope of a field.
  • The Hopf fibration (hopfFibers / hopfFiber / hopfBases / HopfFiber, Sources/Ollin/Geometry/HopfFibration.swift). The picture half of the ROADMAP's technique-substrate trio, and a geometry emitter rather than a shader: every point of a two-sphere lifts to a great circle of the three-sphere, stereographic projection carries it into space as an ordinary circle, and each fiber comes back as a plain [Vector3] for drawTube or any 3D path. Hopf 1931, the explicit circle parameterization credited in ATTRIBUTION.md, never in .swift. Load-bearing: the emitter works in the construction's own convention (special axis third) and hands back in the world's y-up one, and the two are a quarter turn about x, not a swap of two coordinates — a swap mirrors the figure, and a mirrored fibration links the other way round, which is a different object with the same shape (theBottomPolesFiberComesBackStraight catches a sideways emit; the whole 3D set would otherwise have shipped lying down). The base set must be structured (hopfBases(latitudes:perCircle:spanning:) rings, each turned a half-step against the last so the circles interleave instead of forming spokes, or hopfBases(spiralCount:)), because the linking only reads when neighboring circles are neighbors and a scattered set draws as wool. Color is taken from the base point (hue around, lightness up), which is what shows the tangle is an image of a sphere; a draw-order rainbow says nothing. The base at the bottom pole lifts through the projection's own pole and comes back as a straight line — the axis every other ring is threaded onto — handed over as a two-point path with isStraight set and a reach length (a line has no size of its own), never dropped; spanning defaults to keeping clear of it, so a rings sketch adds Vector3(0, -1, 0) by hand, while hopfBases(spiralCount:) already reaches it. Circles grow without limit as the base nears that pole. HopfFibrationTests (10, every one a theorem: lifted points on the three-sphere, the map sending a fiber back to its base, each fiber flat-and-round about a solved center — the parameter's equal steps are not equal steps around the projected circle, so a centroid-based check fails on a good circle — the top pole's fiber being the unit circle, the straight one standing up, every fiber linking the top pole's circle exactly once counted by disc crossings, and no two fibers meeting; one sign flip in the lift turns five of them red). Snapshot hopf-fibration; example 3D/Geometry/HopfFibration; Docs/3D/HopfFibration.md; Guide Ch 27 § A shape from four dimensions: the Hopf fibration (a pointer; the section and its figure are on the reference page).
  • Watercolor wash (Sources/Ollin/Effects/WatercolorSim.swift + runWatercolor in MetalRenderer+Effects.swift + the ollin_wash_* section of ShaderSim.metal). The three-layer wet-paint simulation (shallow water / pigment deposition / capillary; Kubelka-Munk optical rendering; credit in ATTRIBUTION.md): Sim.watercolor(pigments:...) runs on a WatercolorField (a SimField subclass, made with watercolor(...); SimField is no longer final), painted through withField with seed rgb = up to 3 palette pigments (premultiplied = dilution) and alpha = water; WatercolorPigment carries K/S triples + density/staining/granulation (12 published measured presets; overWhite:/overBlack: derives K/S via the paper's inversion). Load-bearing: the velocity update is A + μB (the continuous equations' +μ∇²u; the printed pseudocode's sign reads inverted and anti-diffusion detonates) and relaxation runs divergence-reducing in gather form; the staggered grid packs (u right face, v top face, p, M) per texel, off-canvas taps read dry via bounds-rejection (the clamp sampler would reflect the edge), and dry-bordering faces re-pin to zero in every velocity-writing pass; pigment advects in 4 substeps of dt = ¼ with |u| ≤ 1 so the four-face outflow can never exceed the cell (pure gather, no rescale); transfer_dep/transfer_pig encode from one snapshot (exact conservation); the inject's dry-brush height gate applies to both halves (flow and pigment) or gaps hold flat unsimulated stamps; dry() bakes the wash into the dried R/T glaze stack (fresh textures swapped into the slot, never in-place) and caps saturation below the mask-expansion threshold or the sheet re-wets itself and dry() never sticks; blot() lifts water keeping pigment parked (the paper's authored damp state), and a held water touch blooms the backrun where a single tap only nudges (one frame's pressure relaxes away); K-M math runs in display-space sRGB (linearized only at output; CPU KubelkaMunk reference and ollin_wash_km_layer kept in step); a stroked mark rings stamps with pigment-free water, so painting blocks call noStroke(). Deterministic end to end → pixel snapshot watercolor-sim; WatercolorSimTests (K-M inversion round-trip + edge-darkening / dry-brush / dry-freeze / backrun-counterfactual / byte-replay probes); example Simulation/Watercolor; Guide Ch 23 § Paint that behaves: watercolor. Mechanism: ARCHITECTURE.md § The watercolor wash. Docs/Simulation/Watercolor.md.
  • Compute & GPU particles (a core capability, GPU/Metal in Ollin). Runtime-compiled MSL kernels over persistent buffers/textures feeding the instanced render path — 10⁶ particles update + draw GPU-side, the sandpainting depth-of-field at scale. Types in Sources/Ollin/Compute/: ComputeKernel / ComputeBuffer / PingPong / ComputeParams / Particles (buffer half) and ComputeTexture / PingPongTexture / Simulation (texture half — reaction-diffusion / cellular automata / image kernels drawn as an Image). Facade: compute(_:over:) / compute(_:reading:writing:) / compute(_:writing:) / stepParticles / stepSimulation / drawParticles. Kernels inline or in a .metal file (ComputeKernel(resource:in:)); shared types + the full OllinShaderLib (the same hash/noise/curl/disc + palette/OKLab/sd* helpers fragment shaders get, hash12 not the old hash21) auto-splice. Gotchas: ping-pong (not in-place) for any buffer the render path also reads; a file using OllinParticle must import COllinShaders; the COllinShaders clang module is stale-cache-prone (swift package clean after adding a shared struct); kernel compile errors are swallowed → missing particles, not a crash; headless export runs intermediate sim steps via stepCompute; seed once via a flag (frameCount is 1 on the first draw()). A ComputeTexture also bakes a map a mesh then uses (texture.image into textured(_:) / normalMapped(_:) / surfaceMapped(...), dispatched once and kept, which is what the planet does): written values are read linear with no sRGB decode (author through srgbToLinear), the alpha of a base-color map is the surface's opacity so a field parked there draws a half-transparent solid, and a GPU-written texture carries no mip chain, so a surface seen nearly edge-on reads a whole triangle's worth of texels at once and its silhouette breaks into the mesh's own teeth: measured on the planet's cloud shell, halving the map does not help and finer triangles only make finer teeth, while keeping the shell's overhang under a pixel removes it. Up to ten buffers of any element types bind through compute(_:buffers:count:params:) at indices 0…9 (the thread count defaults to the first buffer's), and compute(_:reading:writing:) takes two element types, so a kernel reads a static geometry buffer and writes an OllinParticle buffer with nothing packed into the other; ComputeBindable is public with the Metal realization behind an internal ComputeRealizable (a conformance of your own binds nothing). A kernel projects through the sketch's camera: cameraParams() packs the active Camera3D's public viewMatrix/projectionMatrix(aspect:) as an OllinCameraMatrices (128 bytes, the head of a ComputeParams; append(camera:aspect:) for any camera) and the library's ollin_project / ollin_project_eye return canvas points, view depth, and clip w (≤ 0 behind the camera); ballSample(float3) is the uniform ball scatter beside discSample. Two particle styles: .marks (perceptual coverage, sRGB color, the default) and .light (drawParticles(_:style:): plain area coverage, linear color, color × alpha × area wherever a particle lands; a particle at or under one texel takes the one-texel path, its quad sized from Uniforms.pixelScale, the deposit π/4 d² continuous across the boundary, so a million one-pixel points cost a million fragments), selecting the ollin_particle_light_* pipeline. LightAccumulationTests pins the deposit's linearity in size (a half-pixel disc a quarter of a one-pixel one, marks reading brighter), the one-texel landing, the kernel agreeing with project(_:), and the ball's statistics. Docs/Shaders/Compute.md; examples Compute/CurlField, Compute/ReactionDiffusion, Rendering/DepthOfField, 3D/Geometry/Planet. An inline kernel that includes no file is found by ComputeKernel.quickHash, worked out once when the kernel is made, so only its first dispatch composes the shader library around it; the check the key replaced (an empty source path) could never match, since an inline kernel records the Swift file it was written in, and every dispatch of every frame recomposed and rehashed the whole library (anInlineKernelIsFoundWithoutComposingTheLibrary).
  • GPU artificial life (Sources/Ollin/Compute/SpatialHash.swift + ParticleLife.swift / PPS.swift / Physarum.swift). The compute path's first cooperative primitive + three emergent-behavior sims on it. SpatialHash (public) is a GPU uniform-grid fixed-radius neighbor search (Hoetzlein counting sort: clear → count → serial exclusive-prefix-sum → scatter, the four kernels sharing one source); it's the enabler ParticleLife/PPS query and a sketch builds its own sim on via the neighborStep(_:over:reading:writing:) facade + the OLLIN_FOR_NEIGHBORS macro. Load-bearing: the shared struct is OllinSpatialGrid in OllinShaderTypes.h (stride 32; distinct from the pre-existing OllinGridParams, the 3D ground-grid uniform, whose name it must not reuse); the grid/neighbor helpers (ollin_grid_cell, ollin_torus_delta, the OLLIN_FOR_NEIGHBORS/OLLIN_END_NEIGHBORS macro pair) live unmarked at the tail of OllinShaderLib.metal so filterLibModules always keeps them (compute-only; never bloats a user fragment shader's using: subset); cell edge = query radius over a toroidal domain so all in-radius neighbors sit in the wrapped 3×3 block, which needs gridW/gridH ≥ 3 (init clamps, so worldSize may round up past bounds) or the wrap double-counts a cell; atomics power the count/scatter (atomic_fetch_add) and never appeared in this codebase before, and the serial compute encoder (default makeComputeCommandEncoder()) orders the build passes with no explicit barriers (same as Simulation's substeps); buffer-index contract 0/1 particles read/write, 2 sortedIndices, 3 cellStart, 4 cellCount, 5 grid, 6+ sim-specific (ParticleLife's matrix at 6); nil gaps in a RecordedDispatch.buffers array bind nothing (index = array position). All three sims reuse OllinParticle (no new struct): type/heading are stored in the free seedA scratch; each is a @MainActor final class recording its step by appending RecordedDispatches directly (the Particles.recordStep model, not the 1-2-buffer compute() facade). Physarum uses no hash (trail path): agent buffer + r32Float PingPongTexture trail + an atomic UInt32 deposit grid (fixed-point, avoids Metal's undefined concurrent-texture.write race), stepped clear→agents(sense/steer/move/deposit)→diffuse+decay→colorize; draw via .image. Load-bearing anti-lattice-artifact trio: bilinear trail sensing + a per-step motor noise (heading jitter) + a 1-2-1 Gaussian diffuse (not a box mean); without them the faithful-Jones steering (which keeps heading exactly when all three sensors read equal, i.e. in the empty region an agent forages into) sends escaping agents ballistically straight, and those coherent parallel escapes self-reinforce on the square lattice into cardinal-axis spurs (the motor noise is the decisive one, turning the ballistic escape into an even-spreading wander). Reproducibility (load-bearing): the counting-sort scatter lists a cell in GPU-atomic-race order, and the sims are chaotic, so a sum over neighbors taken in that order grows into a different picture within seconds (31 to 78% of the pixels by frame 480 in ParticleLife, Swarm, ParticleFluid). Under a fixed clock (Drawer.runsOnFixedClock: an export, or a take playing back; never --bench) a rank pass puts every cell in index order, so the same seed exports the same frames on one machine: one thread per member counts the smaller indices in its cell, the work cut into 32-member chunks the serial scan counts (chunkStart, binary-searched per chunk), because one thread per particle diverged on the densest cell of each SIMD group and one SIMD group per cell put a packed cell on a single core (a ParticleLife frame took seconds); the rank costs a clumping sim about a fifth of its step (ParticleLife 21.3 → 26.3 ms, Swarm 38.4 → 45.6, ParticleFluid 8.6 → 8.7), which is why a live frame skips it (a wall clock never repeats a run). Still no pixel snapshot (another GPU's arithmetic can take a chaotic run elsewhere); SpatialHashTests pins counts, the exact prefix sum, the permutation, per-cell bucketing and the neighbor set on both paths, and index order within every cell on the export path over a packed layout (one cell of 300, others at 31/32/33/64/65). Facade factories makeSpatialHash(in:radius:count:) / makeParticleLife(count:kinds:radius:…) / makePrimordialParticles(count:radius:…) / makePhysarum(agents:resolution:…) default bounds to the canvas and seed to variation. Docs/Simulation/ArtificialLife.md + Docs/Shaders/Compute.md#spatialhash; examples Simulation/ParticleLife / PrimordialParticles / Physarum + Compute/NeighborSearch (the raw primitive, the one example that import COllinShaders for OllinParticle, wired via the extended example(…, dependencies:)).
  • Emergent matter and heritable rules (Sources/Ollin/Compute/ParticleLenia.swift + SwarmChemistry.swift; shared header structs OllinLeniaParams, stride 112, and OllinSwarmChemistryParams, stride 48, both bound at buffer 11). Two more per-particle-rule sims over SpatialHash, deliberately two systems rather than one: a rule that makes matter organize and a rule that is itself heritable are different questions, and fusing them (genome + bonds + energy + construction) is the step from a recipe to an environment, which the framework lane refuses. Both reuse OllinParticle and follow the artificial-life reproducibility rule (no pixel snapshot: a neighbor sum's order is atomic-race-ordered, so a run is not bit-reproducible even at one seed, and the tests measure what each term does against the run with that term made inert). ParticleLenia is the energy-based formulation (Mordvintsev/Niklasson/Randazzo): one neighbor pass accumulates the ring-kernel field U, its gradient, and the repulsion gradient, then ∇E = ∇R − G'(U)∇U and the particle steps down it. Load-bearing: wK is derived, never tabulated (the closed form πσ(μ√π(1+erf(μ/σ)) + σe^{−(μ/σ)²}) reciprocal, which reproduces the paper's 0.022 at μ=4/σ=1) so muG keeps meaning the same crowding when the ring moves; growth is the only attractive term and repulsion the only one with an opinion at short range (the kernel is a ring, so coincident particles add nothing to each other's field and growth alone would let them pile up) — the two oppose each other on nearest-neighbor distance, which is what both counterfactual tests read, and neither is measurable as population spread (the opening disc already sits near the settled size); the seeded disc holds ~1 particle per unit² of area (a canvas-wide scatter opens outside everyone's kernel and shows a minute of nothing); a step is never larger than the published 0.1, so more pace means more substeps, each with its own hash rebuild. SwarmChemistry (Sayama 2009 kinetics + the 2011 evolutionary layer) gives every particle its own eight-value Recipe in a ping-ponged float buffer (stride 12: 8 values + lineage + pad, buffers 6/7), transmitted on contact with mutation, direction set by a Competition (.faster / .slower / .majority). Load-bearing: the published ranges were chosen for a world whose particles sit ~50 units apart and they carry length (separation is length²/step²), so recipes are stored in published units and the kernel converts by a lengthScale derived from the world's own density (lengths and speeds by it, separation by its square) — dropped in unconverted on a 1080 canvas, separation runs ~8.6× too strong and the swarm is an even gas; perceptionLimit and contactRadius derive from the same spacing, so a particle sees about as many others as one in the published world did and a sketch names only count and kinds. Randomness is listed beside cohesion/alignment/separation, not instead of them (replacing them means a recipe with c4 = 0.33, which real published recipes have, spends a third of its steps with no cohesion at all and nothing coheres), and "steer randomly" is the same desired − velocity toward a random heading at the recipe's own normal speed in both the straying and the randomness branch. Transmission is a pull: a thread takes the best of the contacts that beat it, ties broken on the lower index so the neighbor sort's order cannot decide it; .majority compares same-line neighbor counts stored one step stale (a thread can count its own neighborhood, not a neighbor's). Mutation is per contact, not per generation (default 0.01 against Evolution's 0.04) because a crowded particle makes contact several times a second; a mutant keeps its parent's line, so lines only ever die out and the tally is a contest. Opening state is a design decision both times: a handful of recipes each held in quantity, since one random recipe per particle leaves nothing for a structure to be made of. Facades makeParticleLenia(count:spacing:) / makeSwarmChemistry(count:kinds:) + stepParticleLenia / stepSwarmChemistry + drawParticles. Readbacks snapshotRecipes() / snapshotLineages() / snapshotLineageCounts() stall on the GPU (call them a few times a second). ComputeBuffer.snapshot() now returns the pending seed before first bind, so a sketch can read a sim's state in setup(). Examples Simulation/ParticleLenia + SwarmChemistry; ParticleLeniaTests (8) + SwarmChemistryTests (9), each verified red by sabotage; Docs/Simulation/ArtificialLife.md; Guide Ch 25 §§ Matter that decides what shape to be: Particle Lenia + A rule that spreads by winning arguments: swarm chemistry with a figure each.
  • Steering at scale (Sources/Ollin/Compute/Swarm.swift; shared header struct OllinSwarmParams, stride 96, bound at buffer 11). The Reynolds steering vocabulary as a GPU sim over SpatialHash, reusing OllinParticle (seedA = last heading, seedB = the wander walk's angle). Swarm carries eight behaviors as weights (separation/alignment/cohesion need neighbors; seek/flee/arrive/wander/flow do not), plus maxSpeed/minSpeed/maxForce/separationRadius/viewAngle/slowingRadius/the wander + flow parameters, all live; facade swarm(count:perceptionRadius:colors:size:bounds:seed:) + stepSwarm + drawParticles, bounds defaulting to the canvas and seed to variation. Load-bearing: every behavior returns desired − velocity so all eight forces share one unit and their weights are comparable, and the weighted sum takes one truncation at maxForce (the published combination method) before the paper's truncate(velocity + acceleration, maxSpeed) integration; arrive is the one behavior that must not be normalized (its ramp lives in the magnitude); flee is not seek negated (negating the force leaves desired + velocity, not desired − velocity); the three neighborhood behaviors share one pass over the 3×3 block and honor a field-of-view cut (viewCosine); the world is a torus like its SpatialHash siblings (so no containment behavior, deliberately); speeds/forces are per second where the CPU Vehicle's are per step; the hash build is skipped when all three flocking weights are zero, which is what lets a million wanderers/seekers run (measured M2 release 1080²: 1M stepping at 60 fps, drawing them is the limit; 30k flocking drawn at 60; 120k at 34). minSpeed (default 0 = the published model) is an addition, and the fix for a dense swarm setting solid: a stalled agent is a wall the crowd jams against, so a floor keeps it flowing. Three numbers are coupled and a wrong-looking swarm is usually one of them: neighbors seen = count·π·r²/area (~20 works, far more washes structure into the global mean), separationRadius ≈ 1–2× mean spacing (much larger saturates into an even gas), and the turning circle maxSpeed²/maxForce should be a few times perceptionRadius (tighter and agents orbit inside their own neighborhood). Follows the artificial-life reproducibility rule (no pixel snapshot; the neighbor sort is atomic-race-ordered and flocking is chaotic) — SwarmTests pins each behavior against its counterfactual instead, and alignment is tested by local order (agreement with one's own neighbors, 0.98 by 4 s) because global consensus measures mixing time, not the rule (it does get there: 0.10 at 1 s → 0.97 at 20 s). Gotcha the tuning cost a session: a still frame shows density, not movement (agents move a few points per frame), so the look needs a translucent rect over an accumulating canvas — while accumulating, background(_:) wipes whatever alpha its color carries, so it leaves no trail at all (the rect form accumulates on the headless --export --frame N path too, via drawer.accumulates, so a Guide figure gets real trails). A dense space-filling swarm organizing into lanes, cells, and a labyrinth is the rules working, not a fault. Example Simulation/Swarm; Docs/Simulation/Swarm.md.
  • Chaotic systems on the GPU (Sources/Ollin/Compute/AttractorFlow.swift + the Chaotic systems tail section of OllinShaderLib.metal; shared header struct OllinAttractorParams, stride 208, bound at buffer 11). The million-particle leg of the strange attractors: AttractorFlow runs one of the eight published continuous systems (AttractorSystem, the Sim struct-plus-internal-Kind shape, factories mirroring StrangeAttractor's defaults and .attractor handing back that CPU twin) under a million particles, each advanced by fourth-order Runge-Kutta on the GPU, drawn as camera-facing splats. It reuses OllinPoint, not OllinParticle, because a flow is 3D, which needed the one new draw seam: a GPU-resident point cloud (Drawer.recordPointCloud(buffer:count:) opens a .points3D batch carrying particleBuffer, and the encode binds that instead of the frame's uploaded point list). Load-bearing: that batch leaves currentKind = nil so a following drawPointCloud opens its own batch (a merged CPU cloud's points would be silently passed over, since a batch carrying a GPU buffer draws only that buffer); it is gated out of makeBatch like drawParticles. The MSL side is unmarked compute-only at the tail of OllinShaderLib.metal (the spatial-hash precedent, always spliced): the eight derivatives + the three iterated maps + OLLIN_RK4_STEP(state, h, derivative), a macro because Metal has no function pointers here (the OLLIN_FOR_NEIGHBORS precedent) whose derivative is an expression in the sample point _p; ollin_attractor_field's switch is kept in step with AttractorSystem.shaderIndex by hand (the ollin_sdf_distance rule). One CPU orbit at build does everything else and is why a sketch names no per-system numbers: it measures center/extent, the pace, and the speed range, and it is where the particles start. Four invariants, each a fixed defect: the reach is a percentile, not the extremes (rare excursions make a maximum grow with sample size, the four-wing measuring 2.0 over 120k points and 3.5 over 400k, which moved the framing, the dot size, and the stray test with it); particles seed on the attractor, not in a box around it, because the transient is the system's own contraction rate (a fraction of a second for Lorenz, a minute of watching a filled ball for Aizawa) and the CPU tier's settle throws it away for the same reason; the seeding jitter is load-bearing (exactly on the orbit every particle follows one trajectory forever and the picture is that curve with dots sliding along it; a hair off, chaos separates them into a million within a few laps); and the survey filters non-finite points, since constants nobody checked have no attractor and the diverging orbit's NaN otherwise spreads to every measured figure and traps the seeding range (a real crash the tests caught). The step is speed worth of the system's own time split into substeps never larger than the published step (past it the integrator is solving a different system), so the maxSubsteps ceiling slows the flow rather than coarsening it; a particle past extent * 12 is dropped back in the middle rather than left to streak. Color is speed, brightness is density, so the default ramp shifts hue and holds brightness roughly level (a dark-to-light ramp puts two facts on one channel and a slow crowded region reads like a fast empty one), and opacity is low by design so the pile-up carries tone. Follows the artificial-life reproducibility rule (no pixel snapshot) — AttractorFlowTests (12) pins the region the cloud occupies against its own CPU orbit, each with a counterfactual (a different system lands elsewhere; the sabotage check: a wrong constant in the kernel moves the cloud's middle 13 units and fails it). Measured M2 release 1080²: 1M particles stepping and drawing at 55 fps, 0.2 ms CPU. Facade makeAttractorFlow(count:_:seed:) + stepAttractorFlow + drawParticles, seed defaulting to variation. Example Simulation/Attractor; Docs/Drawing/Attractors.md#flow + the function table in Docs/Shaders/ShaderLibrary.md; Guide Ch 25 § A million following the same field: attractor flow + AttractorFlow figure.
  • Evolution (Sources/Ollin/Compute/Evolution.swift + Sources/Ollin/Geometry/Population.swift; shared header struct OllinEvolutionParams, stride 272, bound at buffer 11). Selection as a recipe, in the two sizes the technique actually comes in. Evolution is the GPU tier: tens of thousands of individuals each fly a genome of steering impulses over a fixed trial, are scored on closest approach to a target past up to eight obstacles, and are replaced in one breeding pass. Population + Genome are the CPU tier, Karl Sims' normalized parameter-set genotype judged by a person (breed(from:) for interaction, breed(scoredBy:) for a measure). Load-bearing: the population lives in a compute buffer, not a SimField because selection needs each child to read an arbitrary rival's score (a texel-local step cannot reach one), and it is tournament selection because that is the one method needing no total, no sort, and no normalizing (it only ever compares), so a generation breeds in one dispatch with no atomics and no ordering between threads, and a score's scale never matters, only its order — where the CPU tier can afford the wheel of fortune, whose sum is exactly what a GPU pass would have to stop and agree on; no explicit elitism because every child draws 2 * tournament rivals, so the best is passed over about e^(-2·tournament) of the time (three generations in ten thousand at the default). Genomes are kept in their own buffer ping-ponged beside the particles (both swap together in the breed pass, buffers 6/7); the sim reuses OllinParticle with life = flying, seedA = closest approach, seedB = the score. The opening generation is a design decision, not a detail: genes drawn independently are a random walk whose steps cancel, so a whole population would mill about the start with nothing to select between — genomes open as smooth arcs (a heading with a per-gene turn), measured keeping ~0.48 of their length as displacement against a walk's ~0.18. Everything about the pace is derived from the start-to-target distance (top speed crosses it in ~2 s, trial is 1.7 crossings, thrust reaches top speed in a quarter trial), re-derived every frame so dragging the target re-paces the run, and spanToTarget is floored because a target on the start would otherwise divide every derived figure by zero. Scoring: closest approach squared (pressure where gains are small), a wall keeps a tenth, an arrival is its own band above every near miss and higher the sooner (the two bands must not overlap or a population that found the target can be out-bred by one that has not). Mutation nudges and clamps rather than replacing, which is why with it off every gene is bit-identical to one generation 0 held (the exact statement of what mutation is for, and the test; "stops improving" is not true at short range with a large population — initial variety carries it). Randomness is a pure function of index, generation, and the population's own seed, never the sketch's rng. measuresGenerations opts into a per-generation readback (lastGeneration: best/mean/arrived/closest) taken without stalling the GPU, so it can be a frame stale. The default ramp shifts hue and holds brightness level (the AttractorFlow rule: a whole population leaves at one point, so a dark-to-light ramp reports the launch crowd as the leaders). Population owns a seeded SplitMix64 (SplitMix64 gained Sendable), carries the chosen genomes through untouched (keepsParents — one breeding is a big step when a person is judging), does nothing at all when nothing is chosen, counts a negative score as zero and picks evenly when every score is zero; its mutationRate default is Sims' 0.2 against the GPU tier's 0.04, because a few hundred looks have to cover ground a scored run covers in millions. Follows the artificial-life reproducibility rule (no pixel snapshot; one flight's ulp changes a tournament) — EvolutionTests (13) + PopulationTests (15) pin each piece against its counterfactual, every one verified red by sabotage. Facade evolution(count:genes:from:to:seed:) + stepEvolution + drawParticles, and population(count:genes:seed:). Examples Simulation/Evolution + Simulation/Breeding; Docs/Simulation/Evolution.md; Guide Ch 25 § Letting the sketch find it: evolution + Evolution figure.
  • GPU fluids & soft bodies (Sources/Ollin/Compute/ParticleFluid.swift + SoftBodies.swift). Particle dynamics in a walled box over SpatialHash, reusing OllinParticle; shared header structs OllinSPHParams (stride 112) / OllinSoftBody (32) / OllinSoftBodyParams (64). ParticleFluid is 2D SPH (Müller 2003 symmetrized EOS pressure + Clavet 2005 near-density/near-pressure for anti-clumping and emergent surface tension; quadratic/cubic spike kernels normalized for 2D by integration): per substep, predict (gravity + one-frame pull/push) → rebuild hash over predicted positions (fixed 1/120 s look-ahead) → density pass → force/integrate pass, all pure gathers off one snapshot so pair forces cancel equal-and-opposite with no atomics. Load-bearing: densities are normalized to the seeded lattice (restDensity 1 = as packed, parameters survive a radius change); the box is walls, not a torus — kernels use plain deltas and the r < h test rejects the wrapped 3×3's cross-wall candidates (safe because the hash world rounds down by less than one cell); the velocity clamp (0.45·h per substep) makes tunneling impossible, so a hitch can't detonate the sim; wall under-density compression is countered by near-pressure (raising nearStiffness, not clamping pressure, is the intended lever). SoftBodies is meshless shape matching (Müller 2005): a serial one-thread-per-body reduce (the prefix-sum trade) fits centroid + closed-form 2D rotation, then particles steer to rotated rest offsets at squish (converted per substep, feel is frame-rate-independent); contacts are cross-body only and inelastic (overlap repulsion + each side sheds half the approach speed). Load-bearing: rest offsets are taken about the rest centroid (the lattice mean, not the disk center) — anything else leaves Σ(goal − x) ≠ 0 and the body silently self-thrusts (a real bug: blobs hovered and pinned into corners; pinned by FluidDynamicsTests.isolatedBodyConservesMomentum); spawns separate via relaxCircles before particle fill (an overlapped spawn explodes). Both run fixed substeps against a clamped frame clock (≤ 1/30 s), pack params at buffer 11 per substep, and follow the artificial-life reproducibility rule (no pixel snapshots; behavioral FluidDynamicsTests). Facades makeParticleFluid(count:radius:) / makeSoftBodies(bodies:radius:) + stepParticleFluid / stepSoftBodies / drawParticles overloads default bounds to the canvas and seed to variation. Examples Simulation/ParticleFluid (dam break, drag to grab) + Simulation/SoftBodies (jelly pile, drag to knead). Docs/Simulation/Fluids.md.
  • Text. Three font kinds over one drawText / textToShapes / metrics surface (textFont / textSize / textAlign / textWidth): BitmapFont (default Cozette, BDF + Playdate .fnt), OutlineFont (Core Text .ttf/.otf, each glyph a Shape taking fill and — only once the sketch has set one (Drawer.strokeSet) — stroke, so the initial default black stroke never outlines glyphs (it eats thin light text on a dark ground; TextDefaultStrokeTests); variable-font axes; default .systemMedium), StrokeFont (Hershey .jhf, plotter polylines). textMode(.atlas) flips outline text to an SDF glyph atlas (GlyphAtlas, exact EDT, fourth glyph pipeline). Consecutive atlas drawText calls with the same atlas, blend, depth, clip and target are ONE run (beginGlyphBatch extends the last batch when it is that run; quads carry their own color and size, and a run ends where the next batch starts), so text set a character per cell costs one draw call, live and in a Batch replay (Cordeiro's Gente, four sheets of 16,000 cells: 125 to 11 ms a frame, every frame byte-identical); GlyphAtlasTests.characterAtATimeTextIsOneRun pins the merge and its splits, mergedRunsRenderAsTheyDidOneByOne the pixels. The styled one-call label: every drawText anchor form (scalar, at:, in: box) takes optional size: / color: / align: that apply to that call alone (Drawer.withTextStyle saves and restores exactly the touched fields) — color: paints the glyphs whatever the font kind (fill, or stroke for a stroke font) and suppresses the outline decoration a standing stroke would add; byte-identical to the four-call preamble it replaces (ConvenienceHelperTests). Per-glyph / on-path / box-wrap / metrics helpers; standard notices drawStatus / drawCaption. Examples Text/TextBox (the wrapped box ruled one textLeading() apart) + Text/Scripts (textMissingCharacters naming what the font cannot draw, before it draws). Gotchas: glyph Shapes must be .nonZero (even-odd cuts e/a/g terminals); San-Francisco overlapping sub-contours need a Clipper2 union clean at reference scale 1024 then RDP-simplify (OutlineFont.simplifyClosed) per render size, or a per-frame fps regression; warp via bounded signedNoise, not raw curlNoise; textToShapes returns RAW uneven Core Text vertices (resampled(spacing:) respaces them evenly for dot/jitter effects); bare textFont(.builtin) is ambiguous — use the typed form. Glyphs extrude into 3D solids through drawText3D / Mesh.text (see the 3D bullet). Docs/Drawing/Text.md. A full atlas page is never cleared: the atlas moves on to a fresh GlyphAtlas.Page (its own bytes, its own texture) and leaves the full one as it was, a Slot names the page it was placed on, and a .glyphAtlas batch holds that page, so text placed earlier in the frame that fills the page, a retained Batch, and a recording all keep sampling the bytes their glyphs were drawn with (clearing in place drew the wrong glyphs for everything placed before, once per fill). drawAtlasText emits one batch and one replicated { } per page run, since a symmetry copy is appended after its body and must land in its own run's batch. A page that gained cells uploads into a fresh texture, never into the one a frame in flight reads. GlyphAtlasTests: aFullPageKeepsTheGlyphsPlacedOnIt, aCallThatFillsThePageSplitsItsRunByPage (per-batch counts under three folds exactly three times the plain ones), theFrameThatFillsThePageDrawsItsEarlierTextRight (byte-identical to the frame that fills nothing), each red on the in-place clear or the one-replication form. The glyph atlas finds a face again after making a slot, since making one can fill the page and move on, which forgets every face (a held index crashed the first time a face already on the page filled it; aPageThatFillsMovesOnUnderTheFaceThatFilledIt, and SoakTests fills page after page). The per-glyph route (drawText(_:along:), perGlyph:, the SVG run) takes the same cached glyph geometry the plain run fills (OutlineFont.geometry(for:): cleaned of self-overlaps at a reference scale, simplified to 0.3 px), so a glyph along a path is the plain run's glyph to the pixel (TextPlacementTests.alongAStraightPathIsThePlainRun); flattening afresh there kept every curve point and carried 4.5% more ink on a 20 px O.
  • Text in every script (Sources/Ollin/Text/TextClusters.swift + ColorGlyphs.swift + LineBreaks.swift, plus the layout core in OutlineFont). The four places the one-glyph-per-letter assumption breaks, each fixed at the layout tier so every text path inherits it: TextDirection (textDirection(_:), stack state, ParamOption) forces the base writing direction where the text can't say it (.automatic default = today's behavior, byte-identical); cluster-aware runs replace per-glyph runs in glyphRun (per-glyph drawText, text-on-path), so GlyphRunItem/TextGlyph carry text: String (+ character as the first char, isPicture) rather than one Character; color glyphs (emoji) rasterize through ColorGlyphCache and draw on the image path in the outline and atlas modes; LineBreaks replaces the space split in layoutBlock. Readbacks: OutlineFont.fontsUsed(for:) / missingCharacters(in:) + the font-kind-agnostic Sketch.textMissingCharacters(_:). Load-bearing: the cluster unit is the grapheme, not the string index (a Devanagari syllable's glyphs report different indices inside one grapheme — bucketing by index splits it into pieces that overlap on the canvas), a grapheme with no glyph of its own joins the piece before it (a ligature is reported at its first character in either direction; one at the very start waits for the first piece instead), and clusters sort by originX with a build-order tie-break so a right-to-left line comes out left-to-right on the canvas (TextGlyph.index 0 is the letter read last, which is what a sweep across the drawing wants); a cluster's advance is the sum of the shaper's own advances, never a difference of neighboring positions (an Arabic mark is stored after its base at a larger x, so the difference goes negative, clamps to 0, and the total is wrong); glyphRun applies both pen offsets (x and y) or Arabic harakat and combining marks drop onto the baseline (measured up to 0.063 em, invisible in a whole-line bounding box, so the test compares glyphs one at a time); a color glyph is told from an ordinary one only by the absence of a path (there is no flag), rasterizes once at 160 px = the largest sbix strike Apple Color Emoji carries (read from the font's own table, so a bigger raster only upsamples), takes neither fill nor stroke, is left out of textToShapes with a noteOnce, and counts as a skipped image in a vector export; the SDF atlas is one channel so a picture can never be stored in it (its own image batch, in call order); LineBreaks creates its tokenizer with no locale (measured identical to en_US/th_TH/current across English, Japanese and Thai, so an export can't depend on the machine) and a wrapped line is trimmed of its trailing space; the paragraph-style setting holds a pointer, so the two forced-direction styles are built with nested withUnsafePointer (letting it escape reads freed stack memory and the direction silently does nothing). Bitmap and stroke fonts have no shaping engine and stay left-to-right by design. No pixel snapshot (the layout and the emoji bitmaps come from the machine's own OS and installed faces, which a reference would pin instead of Ollin's code, matching the repo's system-font-free snapshot convention) — ComplexTextTests (21, all eight mechanisms verified red by sabotage) + two Metal-gated render probes pin it instead. Example Text/Scripts; Guide Ch 8 § Shaping and direction: text in any script + EveryScript figure; Docs/Drawing/Text.md#scripts.
  • Writing in columns, and justification (TextDirection.topToBottom + textJustify(_:), over lineStarts / layoutBlock in OutlineFont.swift). Tategaki: text down the page in columns that fill right to left, plus flush-both-edges justification in either axis. Load-bearing: the axis is its own TextDirection, not a second parameter, and asking Core Text for kCTVerticalFormsAttributeName does both halves at once — it substitutes the font's own sideways glyphs and returns positions already running down the column with the font's vertical origin applied, so Ollin rotates nothing and translates nothing (probe-verified: ( goes 27.6×92.2 → 92.1×27.6, the comma moves corner to corner); whether Latin turns is the face's decision (Hiragino carries a turned A, San Francisco does not), which is why the doc promises the font's shapes rather than a rotation. One lineStarts returns the point each line measures from — a line's left edge on its baseline, a column's center axis at its top edge — so one glyph formula (start + (x, -y)·size) serves both axes and placedGlyphs/placedAtlasGlyphs cannot drift; a column is one em across by definition (fixed block width whatever the text) and column pitch is the same textLeading. The two textAlign axes swap roles (vertical says where a column starts, horizontal places the block), textWidth measures along the writing axis, and drawText(_:in:) wraps against the box height. In glyphRun the piece's distance is accumulated from advances, not read off a glyph (Core Text's y = 0 is the column top, one ascent above the first baseline — measuring from the baseline slides every column, the defect perPieceDrawingPutsInkWherePlainDrawingDoes pins) and only the along-axis coordinate is made relative (ShapedCluster.anchor, sorted descending for a column since pen y falls as you read). Text on a path stays horizontal (the curve already carries the direction) and bitmap/stroke fonts note once and stay horizontal. Kinsoku needed no code: the system break set LineBreaks already asks is UAX #14, which encodes the Japanese rules, so a comma arrives joined to the character it follows and greedy whole-piece filling is the push-out behavior — the old "the finer rules are not applied" claim was stale, and japaneseKeepsForbiddenCharactersOffTheEdgesOfALine pins it with a naive per-character wrap as the counterfactual. Justification is CTLineCreateJustifiedLine (the engine knows each script's opportunities: spaces in English, between characters in Japanese), needs a box (only a box states the target), never squeezes, and the last line of each paragraph is left natural — which is why wrappedLines reports paragraphEnds while it still knows them, since joined lines lose the distinction. TextJustification lives on the drawer for the length of one box call. No pixel snapshot (system fonts + system layout, the complex-scripts rule); VerticalTextTests (18) + one ComplexTextTests addition, all eight mechanisms verified red by sabotage — including a reading-order test the first sabotage pass proved was missing (a reversed column measures perfectly and lands its ink correctly; only the character each piece stands for gives it away). Example Text/Columns; Docs/Drawing/Text.md#vertical + #justify.
  • Hanging punctuation (textHangingPunctuation(_:) / noTextHangingPunctuation(), over HangingPunctuation in LineBreaks.swift, Drawer.hangingWidth(of:), and layoutBlock's per-line hang). A stop or comma at a line end may sit past that end (ぶら下げ) rather than taking the character it follows to the next line. Load-bearing: the hangable set is stops and commas only (。 、 plus their full-width and half-width forms, and the Latin . ,) — a closing bracket is line-start-forbidden too, and hanging one would put it outside the thing it closes; the hang width is measured as the difference the last character makes to the line (textWidth(line) - textWidth(line.dropLast())), not read off the character alone, so it carries whatever the shaping did about the pair, and only a line actually ending in a stop pays the second measurement. Two places consume it and both matter: the wrap fit test subtracts it (that is the whole visible effect, and it is why the piece the stop is attached to can stay), and layoutBlock reports a per-line hang that lineStarts subtracts from the extent, so a right-aligned or centered line lands where the line without its stop would land and a justified line stretches to extent + hang (the part that counts reaches the box, carrying the stop past it). Like justification it is scoped to the box call (textHangsInBox, set and cleared by drawText(_:in:); only a box has an edge to hang past) while the setting itself is ordinary drawing state that withState restores. The extent half is outline-font work; bitmap and stroke fonts still wrap this way and still align on the whole line. Note the effect only appears where a stop would not otherwise fit, so a demo has to be tuned to a box width that produces one (Text/HangingStops is, and a width sweep is how it was found). No pixel snapshot (system fonts + system layout); HangingPunctuationTests (10). Example Text/HangingStops; Docs/Drawing/Text.md#hanging.
  • Mongolian columns (TextDirection.topToBottomLeftToRight, over the same lineStarts / penOrigin / geometry cache in OutlineFont.swift). Vertical text whose columns fill left to right, with the letters joined. Load-bearing, and the opposite of the tategaki path: this script is UAX #50 vo = R, so its glyphs are stored in horizontal orientation and a line is shaped horizontally and then turned a quarter turn clockwise; asking for kCTVerticalFormsAttributeName instead gives every letter a full em advance and breaks the cursive joins (probe-verified against CTLineDraw's own vertical output — the joined spine is the thing a still frame shows). The turn is (lx, ly) → (-ly, lx) in local canvas space and matches Core Text's own rendering glyph for glyph; CTRunGetTextMatrix reports the opposite sense for these runs (its vertical text space is itself flipped), so never apply the run matrix here — a wrong turn still stacks and still measures, and only the joins failing to meet gives it away, which is what aColumnIsTheLineTurnedAQuarterTurnClockwise pins against the horizontal twin. A column is as wide as the face's ascent and descent rather than one em, and its axis sits one descent in from the block's left edge, because the turn puts the ascender side to the right; the pen formula becomes start + (y, x)·size (both offsets added, no y-flip), and the turned glyph geometry is cached under its own key, built by turning the upright one, so a turned column stays a translate of cached vertices and the flatten happens once for the pair. The atlas quad takes the same turn corner by corner (a quarter turn keeps a quad a quad and carries each corner's UV with it). The face must have the script: the column width comes from the base face's own metrics, and the system font does not have it. No pixel snapshot (system fonts + system layout, the complex-scripts rule); MongolianTextTests (12), the turn verified red by sabotage. Example Text/MongolianColumns; Docs/Drawing/Text.md#mongolian.
  • Images. Image value type (ImageIO decode — PNG / JPEG / HEIC / TIFF / GIF — from path / url / data / resource / CGImage; loadImage sugar), drawn by drawImage on the textured-quad pipeline in call-ordered batches; lazy device texture cached on the Image, premultiplied, uploaded by hand into an sRGB format (see the gotcha at the end of this bullet). tint(_:) / noTint() stack state. imageSampling(_:) (ImageSampling: .linear default / .nearest) is stack state beside it: a picture drawn larger than its pixels keeps each one a hard square under .nearest, with no enlarged copy made (the batch carries imageSamplesNearest, the renderer binds pixelSampler, magnification nearest and minification still linear through the levels, anisotropy 1 and clamped like imageSampler; a picture drawn smaller is blended under either, byte-identical); the web page sets the picture's magnification filter per draw from a flag after the item's bounds; the SVG recorder skips pictures as it always did. ImageSamplingTests (16 flat cells to the byte at 2×, the seams blended under .linear, the reduction identical, the state restored); example Images/PixelArt. fit: (ImageFit: .stretch default / .contain / .cover) says what happens when the picture and the box are different shapes: .contain shrinks the destination through the shipped Rectangle(fitting:in:), .cover keeps the destination and reads a centered source crop instead (Drawer.drawImage's source: rect, normalized, measured from the picture's top-left; the flipsVertically swap is applied after the crop, so a caller never works in texture coordinates) — so a covered picture needs no clip pass and stays one quad, byte-identical to before at the default crop. Rectangle(covering:in:) is the public other end of fitting (max-scale, centered, runs past two edges). withViewBox(_:fit:_:) runs a block inside a rectangle as if that rectangle were the canvas (clip + translate + scale, fit: reusing ImageFit over the same fitting/covering pair). Two remaps are what make an unedited piece run in a box: background(_:) inside one fills that box's virtual canvas instead of setting the frame's clear color (Drawer.viewBoxCanvases, a stack — the clear color belongs to every box at once, so one wipe would take the others), and the mouse arrives in box coordinates. The mouse is written straight into mouseX/mouseY, never through setMouse (that is the runner's input path: it logs a pointer event into a running take and is a no-op during a replay). width/height deliberately do not change — the code inside believes it has the canvas — so the virtual canvas takes the sketch's shape and a box of that shape is the un-letterboxed case. Boxes nest (clips intersect, mappings compose). ViewBoxTests (8 tests incl. two GPU probes: two boxes each keeping their own background, and the frame outside every box untouched); example Rendering/ViewBoxes. viewControl(center:zoom:in:) is the 2D pan-and-zoom counterpart of cameraControl() (Core/View2D.swift + the Sketch face; viewCenter/viewZoom/resetView()): drag pans, scroll zooms, the arguments are the view's home after the first call: a constant framing changes nothing, a changed one (a @Param turned in the inspector, an edit on the stage, carryRun carries the whole View2D across it) glides the view there at the camera rig's rate (homeGlideRate 8/s, an exponential approach, landing exactly within 1e-9) while restsAtHome holds, and the first drag or scroll ends that for good; resetView() opens on the current home (ViewControlTests: the glide lands and a constant holds, a taken view is left alone, the reset goes to the new home). Undamped on purpose (an orbit gains from inertia, a plane under a finger does not), so both gestures are exact equations rather than a feel: a drag moves content by exactly the pointer delta (center -= d / zoom) and a zoom is anchored on the pointer (center = anchored - (pointer - middle) / zoom'). It leaves a plain transform, so what is drawn before it stays put and a HUD goes after a withState { } around it. The mouse is remapped into content coordinates for the rest of the frame; performDraw restores the pointer around draw() (the runner only writes the mouse when it moves, so a remap left standing would reapply to itself and the view would crawl on still frames — pinned by aStillPointerDoesNotDriftTheView). ViewControlTests (10 tests incl. a GPU probe against screenPoint; the probe measures a centroid, since the first-found brightest pixel of a filled disc is its top edge, a whole radius off); example Input/PanAndZoom. ImageFitTests (10 tests: the crop is centered, one axis survives whole, what it leaves has the box's shape measured in the picture's own pixels, plus three GPU probes over a four-band 4:1 picture in a square, where .cover must show only the middle two bands); example Images/Fit. Part of a picture: drawImage(_:_:_:_:_:_:_:_:_:) (four source numbers after the box) and drawImage(_:in:source:), the source in the image's own pixels from its top-left, through Drawer.drawImage(_:in:sourcePixels:), which cuts a part that runs off the picture and carries the same cut to the box (the off part draws nothing). Load-bearing: the batch carries imageBounds (the part in texture coordinates) and ollin_image_fragment holds the sample half a texel inside it, choosing the level from the quad's own uv (gradient2d(dfdx(in.uv), dfdy(in.uv))), since the held uv stops moving at the edge; a whole-picture draw binds MetalRenderer.wholeImage (min past max) and samples exactly as before, and .cover keeps reading across its crop (a photograph continues there). Drawn at its own size or larger a cell equals the same cell cropped first pixel for pixel at 1x, 3x, 4.5x, whole and quarter-pixel positions (ImageSourceTests, red with the clamp planted off: 113 px at 1x, 1,000 at 3x); drawn smaller, the mip chain averages across cells (padding is the answer, documented). The web page carries the bounds after the item's count and its image fragment holds the read the same way (textureGrad on the quad's uv); the parity case Sprites reads mean 0.029, and 2.44% far with the page's clamp planted off. Example Images/SpriteSheet. Pixel get/set via the Image[x, y] subscript (premultiplied, top-left origin; a GPU-backed image, a layer's, a compute texture's, a live feed's, answers .clear and drops the write on hasCPUPixels, before any buffer is set aside, ImageTests.layerImageHasNoPixelsToReadOrWrite); blank Image(width:height:color:) + bulk Image(width:height:premultipliedRGBA:). Gotchas: every texture is built by hand, never through the synchronous MTKTextureLoader — that loader decodes on a dispatch worker and parks the caller on its own semaphore, so with every worker already blocked (a test run with several blocking calls in flight) the main thread waited forever, the intermittent CI wedge the watchdog sampled on 2026-09-06; Image.texture(for:) draws the CGImage into a premultiplied sRGB bitmap on the calling thread and fillSmallerLevels blits the mip chain on a command buffer, which also settles two things the loader got wrong for some sources (a context-made CGImage came back in a linear format holding sRGB bytes and washed out lighter; a straight-alpha PNG stayed straight while the pipeline blends premultiplied), and linearTexture(for:) draws into the image's own color space so a value map's bytes are never converted; don't add a y-flip to the pixel buffer (it's already top-down). Docs/Drawing/Images.md.
  • Sample photographs. OllinSamplePhotos (a product of its own, never part of Ollin, so an app that skips the import ships none of it), twenty pictures and one short film: four faces SamplePhoto.portrait / .scarf / .profile / .marigolds, four whole figures .reaching / .wrestler / .dancer / .handstand, two tables from above .breakfast / .desk, a pair of open .hands, four streets .alley / .street / .textiles / .city, two dusk landscapes .headland / .boats, a .page under uneven light, and two surface textures .talavera / .stone (+ .all, in that order), each load()ing a JPEG at 1600 on its long side through the library's own Bundle.module and carrying its credit (photographer / place / source page / license / license URL; credit.line is the one-line form). The JPEGs are .processed so they land flat in the bundle root; total 6.4 MB, each under 512 KB at quality 72 to 95 by request (a smooth dusk sky is the one case that wants the top of that range, since banding lives in exactly such a gradient). The two surfaces are 1024 squares, not 1600 on the long side (theSurfacesAreSquarePowersOfTwo pins it): a texture is magnified and repeated, so it wants clean pixels at a size a GPU mips evenly, and neither fits the size budget at 1600 without artifacts a magnified surface shows. Every face is square (theFacesAreSquare pins it) so it crops to the canvas without choosing what to lose; a figure keeps whatever frame holds its whole body, which is upright for reaching / dancer / handstand, where a square would cut a raised arm or a foot off, and square for the wrestler, whose arena frame had the room. A sketch fits an upright one rather than covering it. Every figure was read by the joint model before it was bundled: reaching / dancer / handstand give all 19 joints, wrestler 17 (his ankles are behind the ring rope). The two tables were chosen by measurement, not by eye: breakfast yields 5 detections (2 cups, 2 bowls, a sandwich) where the wrestler it replaced in ObjectDetection yielded 3, one of them a misread; desk yields 7 rectangles and 5 text lines, and is cropped in as far as it is because the recognizer read nothing at the photograph's full width (the print is small, and what fits the frame decides whether it is legible at all). Reading it needs TextRecognizer's .accurate, not the .fast the live tracker defaults to, and the first accurate pass in a process can cost ~47 s while the system prepares its reader (measured; a warm pass on the same 1600-square picture is ~140 ms, and a following process is usually warm), which is why TextScan's caption says so rather than reading as broken. Who reads which is by what the technique wants: the frontal portrait (bare paper to solid ink in one face) for the trackers, the mosaics, shock, the filter catalog, and the halftone example; the scarf (full tonal range, but a dark ground and no bold mass) for XDoG, the dithers, and the reference halftone; the profile (bold masses on a plain ground) for string art, the single line, the spanning tree, and the stipple (a mid-toned face stipples nearly EVEN, and an even stipple grows an even maze, which is how the scarf was tried and dropped for both); the marigolds (one hue, fine texture) for palettes and brushwork; and among the streets, alley (half of it plain sky, hard vertical edges, the most directional spectrum of the four) for the pixel sorter and the seam carver, city (a round dome in the middle distance) for both fit figures, since a round shape is the fastest tell for a stretch, and textiles (a repeating weave in stripes) for the frequency domain, whose spectrum carries bands rather than the plain blob the marigolds it replaced gave. Crops are decided once and committed (centered on the face; the profile keeps the frame's full width, so the head sits left of center by design), so a face lands in the same place in every figure. A street is cropped to the largest square its original held, by request, which leaves two of the photographer's own edges untouched rather than trimming all four. The two landscapes are the only wide pictures (everything else is a square or an upright, so a sketch wanting a landscape had to crop one) and they share one shape, theLandscapesShareOneWideShape pinning it, so either drops into the same box the way any face drops into a square; headland cost eleven percent of its height to get there, all of it unlit foreground. boats is the one picture chosen by a filter's stated requirement rather than by eye: .seamlessClone measures its correction around the patch rim and spreads it inward, so a rim over a hard edge drags that edge in, and this is the picture whose brightness stays in a narrow band (0.6% near black) while its color runs pink to turquoise (hue spread 0.86); the example takes its left square, since the sun in the right third is the one hard thing in it, and keeps both slabs above the horizon. headland displaced nothing: it was tried against scarf in Dithering (it is near monochrome, so the four extracted colors come back gray and half the lesson dies) and against portrait in Halftone (its lower half is solid ink, so the dot ramp never shows), and it ships as the set's only silhouette-plus-gradient rather than as any example's source. hands was chosen by drawing the tracker's own overlay over the candidates, not by eye: thirteen were run through HandTracker.detect at bundle scale and four gave two hands at a full 21 joints, but rendering the example's bones and tips over them is what separated open palms (ten chains straight out to ten separated tips) from a beautiful pair of pointing hands whose curled fingers bunch into a knot at the knuckles. The handstand it replaces reads two hands whose fingers are planted and half closed, so the tracker finds them and the drawing shows nothing. page was chosen by running the filter, not by the light meter: of twelve candidates the two with the strongest measured light gradient read zero lines (a book small in a dark frame), while this one reads 60 lines at 0.91 and, under .threshold against .adaptiveThreshold, loses whole bands of text and gets all of them back, which is the demonstration. The SummedArea example keeps its painted page and traveling lamp, since a still cannot show a global cut failing as the light moves; the still figure 19-LayersAndEffects/LocalAverages takes the photograph instead, as a detail crop, because at tile size the whole page's type is unreadable and the two cuts then read as abstract shapes. talavera is the only bundled picture that tiles: its frame was found by searching crop size and origin for the smallest wrap error (769 px scored 0.155, and the 1544 two-period crop used here 0.207, both against 0.304 for a naive cut), which matters because a triplanar projection repeats whatever it is given regardless of wrap. Its normal map is built on the CPU in setup from the picture's own luminance slopes; doing it on the GPU at the top of draw (a render target plus .normalMap) filled the canvas with the normal map and is the trap to avoid. stone carries real relief and no repeat, so it went to Parallax, where one picture serves as both color and height map and the sphere's own uv covers it once. textiles is also the one where red sits beside green oftenest (5.0% of its pixels, against 4.1% for street and 1.2% for portrait, measured over 537 candidates), so it is what a color-vision simulation collapses hardest. Image.resized(width:height:) (CoreGraphics resample into an sRGB context) is the working-copy call every CPU technique now makes first, with cropped(x:y:width:height:) (clamped to the picture, so a crop off an edge comes back smaller rather than empty) and cropped(toAspect:) (the largest centered piece of a given width-over-height shape, nothing scaled) beside it for changing what is in a picture rather than how big it is; a texture-backed image has no CPU pixels and comes back blank. Terms: Unsplash License / Pexels License (free incl. commercial, no attribution required, no unaltered sales, no competing stock compilation, a person never shown in a bad light), the full record in THIRD-PARTY-NOTICES.md. The Examples package links it as the .samplePhotos satellite, the hosts through the root Satellite enum. SampleClip.dance is the one film: 30 s at 960 square, 25 fps, 1.5 MB, silent, cut from a wider minute. The window and the crop were chosen together, since the dancer leaves a square frame between 0:31 and 0:47 and never once inside the first thirty seconds (measured over 300 sampled frames, no edge contact on any side). It is locked off, which is the property that matters: 63% of the square never changes, so a flow vector belongs to the dancer rather than to a moving lens, and the body model finds 19/19 joints in every frame sampled across the run. It is vended as a URL, not a player (SampleClip.url), because VideoPlayer lives in OllinVideo and no satellite depends on another; a VideoPlayer is both a FrameSource and a VideoFeed, so it drops in wherever a camera goes. Vision/OpticalFlow and Vision/ContourTrace read it when there is no camera (both opened a bare Camera() before and showed nothing headless), --photo forcing it as it does the stills. A fixed pixel scale in a flow overlay is a bug waiting for a different source: the arrows were length- and color-scaled against a constant 18 px, which suits a hand waved at a webcam and drew 4-pixel near-transparent lines for a dancer moving a few pixels a frame; they now read against that frame's own fastest sample, which adapts to either. Thirteen more Guide figures read the set rather than authoring their subject, taking the figures that read it to twenty-five, and three swaps were tried and put back. Chapter 34's four: MotionBrush and FlowArrows step SampleClip.dance one of its own frames at a time (the two-disc StagePerformer they each carried a copy of is gone), SlitScanDelay pushes 48 consecutive frames into the history and the swung arm comes back as a comb, ReadingTheDesk runs both detectors over desk, AttentionAndLabels runs saliency and the classifier over marigolds (it lands on her face, and marigold itself comes back at 7%, under the classifier's own default floor, which is now the lesson), and Contours traces hands with detectsDarkOnLight: false, since the subject is the light half there, the palm creases coming back as holes. Then Melt takes headland, TriplanarSkin talavera at tile 2.2, SurfaceGrain stone as a deliberately 96-pixel base with a mirrored patch of the same wall as its detail pair (grayed and centered on its own average, since a detail color map multiplies and 128 is its neutral), LiveRoom and LiveSurface hold alley where the webcam would be, DesignFilters puts city behind the three picture filters, and Separations separates marigolds. Tried, rendered, and reverted, so do not re-try them: 19-LayersAndEffects/FrequencyDomain (a photograph's spectrum is a noisy blob, where the test plate's stripes and hard edges are exactly what make the harmonic line and the cross readable), 02-Color/Dithering (a smooth ramp is the hardest thing to quantize and its banding is the whole lesson; a face posterizes into flat regions that read as a style rather than a failure, and the dithered panels lose the source's tone), and 19-LayersAndEffects/SeamlessClone (a photographic patch carries its own light and shade, so the cloned disc swings far off the destination's tone, where a flat-color patch shows color replaced and texture kept; a first attempt with the rim over a mortar joint blew out exactly as the filter's own rule predicts, and the Effects/SeamlessClone example already carries the photographic case). A figure that measures the film does it with seek(to:) then snapshot(), which is deterministic under the headless driver the figure runner uses. OllinSamplePhotosTests (7, theTablesAndStreetsAreSquare / theLandscapesShareOneWideShape / theSurfacesAreSquarePowersOfTwo pinning the size and crop rules) + SampleClipTests (2) + ImageCropTests (3). Docs/Drawing/SamplePhotos.md; Guide Ch 9 § A picture on the canvas: loadImage and drawImage.
  • Image as input (glyph mosaic Sources/Ollin/Text/GlyphMosaic.swift, pixel sorting Image/PixelSort.swift, single line Geometry/SingleLine.swift, slit scan Image/SlitScan.swift, halftone Image/Halftone.swift, string art Geometry/StringArt.swift, seam carving Image/SeamCarve.swift, photo mosaic Image/PhotoMosaic.swift, autostereogram Image/Autostereogram.swift). Nine CPU readings that turn a picture into something else or into another shape of itself, all following the Dither bulk-pixel precedent (premultipliedPixels() in, Image(width:height:premultipliedRGBA:) out) and all deterministic. Glyph mosaic — drawGlyphMosaic(_:columns:characters:in:inverted:colored:glyphScale:) + the data form glyphMosaic(of:) ([GlyphMosaicCell]): brightness → glyph by measured ink coverage in the active font (bitmap lit-pixel count / outline signed shoelace area / stroke pen travel; ramp cached per (font, characters), normalized to its densest member); empty floor below half the sparsest glyph (shadows stay voids); transparency carries no ink in either mapping; GlyphSet.technical/classic/blocks curated sets (Cozette covers the whole technical set); glyphScale default 0.85 keeps gutters so dense glyphs read as discrete marks (1 tiles blocks seamlessly) — for the reference "marks" look, use a set that tops out below the solid blocks (the example does). Pixel sorting — Image.pixelSorted(_:by:threshold:reversed:) (Asendorf's interval model): threshold-bounded runs along rows/columns sorted by brightness/hue/saturation, keys from straight color, alpha moves with its pixel, position tie-break → byte-deterministic; the iconic jagged look needs texture in the source (run boundaries must vary per line — a clean gradient sorts invisibly, and ascending on an already-dark-to-bright gradient is a near no-op; reversed: true is the dramatic direction there). Single line — singleLine(through:closed:) (pure, rng-free: grid-accelerated NN construction + neighbor-list 2-opt, ring searches use the (r−1)·bucketSide stopping bound; open form cuts the longest edge) + the singleLine(of:points:in:iterations:cutoff:closed:) sugar over the seeded stipple; cutoff rounds bright grays to paper (the Kaplan contrast rule — without it light regions collect a wandering thread). Slit scan — SlitScan class (push + image(delay:) closure/map forms): rolling CPU frame ring, delay 0 = newest / 1 = oldest held, first push fixes the size (mismatch/GPU-backed frames skipped with a one-time note), memory is w×h×4×frames so push modest sizes. Halftone — drawHalftone(_:pitch:angle:in:inverted:colored:) + the data form halftone(of:) ([HalftoneDot]): classic AM dot screening as real circles (45° default screen); coverage is the per-cell mean of (1 − linear luminance)·alpha over exact rotated-cell pixel binning, and the radius inverts the shared halftoneCoveredFraction spot function — the same function the separations' raster screen thresholds (one source of truth in Image/Halftone.swift; PrintSeparation.halftonePlane calls it), so tone is area-exact and shadow dots merge into the checkered diamonds; the separations' 2% printable-dot clamp applies (below → no dot, above 98% → the corner-flooding pitch/√2 cap); pitch floors at 2 (dot-count guard); dots emit through drawCircle/drawCircles, so SVG/PDF export writes true circles (the plotter path). String art — StringArt(of:center:radius:pins:chords:ink:minSpan:inverted:resolution:) (Geometry/StringArt.swift), a stateful stepper with no rng at all (the same picture always winds the same thread): one continuous thread over a ring of pins (pin 0 top, clockwise), each chord the greedy pick by mean remaining darkness along it (mean, not sum, so a short dark chord beats a long gray one), the ink then paid down along the very samples the score walked; a pin pair never rewinds, minSpan (default pins/10) keeps chords off the rim, and the winding stops at the chords cap or when the best mean drops to ink/2 (the over-inking floor). The working grid reads the picture's largest centered square at resolution cells a side, linear-light darkness (1 - luminance)·alpha, zero outside the inscribed circle; inverted winds luminance instead (bright thread, dark ground). thread is one open Contour (the single plotter line), sequence the pin order (the hand-winding recipe). Bold tonal masses read best; whole-frame soft gradients wind into fuzz. Pair ink with the drawn stroke alpha so the solver's pass matches what a chord actually darkens; a caption over a noClear() accumulation must keep word-for-word stable text or it smears. The direction of the image-to-grid mapping is pinned by a sabotage-verified seek-the-dark test. Seam carving — Image.seamCarved(toWidth:toHeight:energy:protecting:discarding:) + seams(_:along:…) ([Contour]) + seamMap(along:…) (SeamMap: image(_:) / seam(_:) / sizes) + seamEnergy(_:along:) (Avidan-Shamir content-aware resizing; the default .forward cost is Rubinstein's forward energy, .gradient the original measure): one seam is one DP pass, and the picture is really carved between seams (gray/bias/origin shift left in place; the order map records the seam number at the pixel's original column, which is what lets one map serve every size and both directions of resize, and what a sequential-equals-batched test pins). The forward cost is the cost of the join, and each way in has its own price (C_L/C_R add |up−left|/|up−right| to the crossing |left−right|; trade the two and the seam leans the wrong way, pinned by a hand-worked two-row test, the one thing no other test sees). Ties break leftmost, so a carve is byte-deterministic. Masks read brightness × opacity and add ±1000 (far above any real energy, which tops out near 3); a wrong-size or texture-backed mask is left out with a note. Growth duplicates the first k seams averaged with the right neighbor, capped at 2·size−1 (a seam is duplicated once). Height carves by transposing, so toWidth + toHeight is width first then height and the mask travels through the first resize with the pixels. The honest form of the claim is texture survives and flat gives way: a plain object carves away like any other flat region (a straight seam through it creates no new edge), so hold it with protecting: — which is why the snapshot scene and the example both grain what must survive. Autostereogram (Image/Autostereogram.swift) — Image.autostereogram(_:pattern:seed:) + the Autostereogram settings (repeatWidth / relief / whiteIsNear) + drawAutostereogram. A depth map hidden in a repeating pattern: the picture is built column by column, each taking its color from the column one separation to its left, so the leftmost repeat is the only free choice and a pattern one repeat wide is enough; the separation shortens where the depth is near, which is the entire technique. Columns are joined through a union-find, so a chain of pairings stays consistent. Depth is read in linear light, and a transparent pixel reads as far. The invariants are exact: every paired pair of columns holds the same color, a flat map is a plain repeat, a white map repeats at repeatWidth·(1−relief), and the shape reads back out (the smallest matching shift recovers the separation for >95% of pixels; the rest are the shape's own edges, where the depth changes inside one pair's span — comparing against the depth at the pixel rather than at the pair's midpoint reads as a band of disagreement along every edge, which is what the first version of that test measured). Four practical limits, all in the docs: draw at native pixel size (scaling resamples the very repeats the eyes pair), keep relief under about a third (past that the eyes cannot fuse it), hard edges read best, and an exported frame is dithered so its repeats match to within a level or two rather than exactly (measured on the export: 46% of probed pixels pair byte-exactly, 89% within two levels). Example Images/Autostereogram; AutostereogramTests (8, sabotage-verified); no snapshot (a dithered, scaled reference would pin nothing the invariants do not). Docs/Drawing/Autostereogram.md; Guide Ch 9 § A picture that hides a shape + the DepthInARepeat figure. Photo mosaic — Image.mosaic(of:columns:rows:maxUses:) → PhotoMosaic (tiles in reading order, each carrying its cell's target color; frame(of:in:), uses(of:)) + Image.averageColor(in:) + drawMosaic(_:of:in:tint:). Each cell takes the library picture whose own average is nearest. The averaging is in linear light and that is the whole correctness claim: half black and half white is 0.5 linear, ~0.735 sRGB, and averaging the sRGB numbers answers 0.5, a quarter too dark (the pinned invariant, and the only one a wrong-space sabotage trips). Transparent pixels count for nothing rather than for black, and the returned alpha says how much of the cell was really there. maxUses is spent in reading order and a starved cell falls back to the nearest picture rather than to nothing. The result is placements, not a new picture, so a per-cell rotation or gutter is the caller's to draw. The thing that decides whether a mosaic works is not the code: the target needs range and the library needs range in the same places (a flat dark target takes one picture everywhere; uses(of:) is the number to watch, and the first example wasted 98 of 100 pictures that way). Example Images/PhotoMosaic; PhotoMosaicTests (8, sabotage-verified); Docs/Drawing/PhotoMosaic.md; Guide Ch 9 § A picture made of pictures + the PicturesFromPictures figure. All eight of the others need CPU pixels and so does this one (texture-backed images go through snapshot() first; pixelSorted and seamCarved return self, the others note once and return empty/nil). The example sketch classes SlitScan and StringArt shadow their types → Ollin.SlitScan / Ollin.StringArt (the @SwiftUI.Environment precedent). Examples Images/GlyphMosaic / PixelSort / SingleLine / SlitScan / Halftone / StringArt / SeamCarve; snapshots glyph-mosaic / pixel-sort / single-line / slit-scan / halftone / string-art / seam-carve; GlyphMosaicTests / PixelSortTests / SingleLineTests / SlitScanTests / HalftoneTests / StringArtTests / SeamCarveTests (19). Docs/Drawing/GlyphMosaic.md, Docs/Drawing/PixelSorting.md, Docs/Generators/SingleLine.md, Docs/Video/SlitScan.md, Docs/Drawing/Halftone.md, Docs/Generators/StringArt.md, Docs/Drawing/SeamCarving.md.
  • Data as input (Sources/Ollin/Data/Table.swift + JSON.swift). The load-once material for data-driven drawing: loadTable reads CSV/TSV into a Table (a RandomAccessCollection of Rows; row["col"] / row[2] plus number/int/bool/color and their at position: twins, column(_:) / numbers(_:)) and loadJSON reads a document into JSON (a @dynamicMemberLookup enum; reach by key/index then ask .text/.number/.int/.bool/.color/.array/.object/.keys/.count/.isNull). Both follow the palette-importer posture: total decode, nil rather than a throw or a trap, and resource:in: with no default bundle (the framework-wide rule). Load-bearing: the CSV parse is the published format as a byte scanner (every byte it compares is ASCII and no byte of a multi-byte UTF-8 character can be mistaken for one, so cell text passes through untouched), and its case order is too: separator and line endings are tested before the after-closing-quote whitespace case, since a tab is both a separator and whitespace and the separator reading must win (a TSV silently lost a column otherwise); the BOM is stripped or it joins the first column's name invisibly and every lookup of that name fails; a quoted cell is verbatim while an unquoted one is trimmed (quoting is how a file says it means the spaces, and the whitespace on either side of the quotes is not part of the cell); the blank-line rule keys on whether the record held quotes, because a line holding "" is a row where a truly blank line and the newline closing the last real row are not (dropping it left a 0-row table that crashed the suite's first table[0]). Two things are guessed and an explicit answer overrides each (the palettePerLine: Bool? precedent): the separator (most frequent of comma/tab/semicolon/pipe on the first line, outside quotes) and the header (a first row holding no numbers). numbers(_:) drops non-numbers (a series to scale by) while column(_:) pads to the row count (stays row-aligned) — the one place the two reads deliberately differ. Rows share one name-to-position map by class reference rather than each copying a dictionary, and a repeated column name resolves to the first. JSON: a missing key reads as .null so a path chains without a check at every joint, .array/.object are non-optional so a loop over an absent key runs zero times, and true is told from 1 by CFBooleanGetTypeID (the system reader hands both back as an NSNumber holding 1). Deliberately small: a document with a shape worth naming is Codable's job, and the docs say so. Examples Data/Readings (a CSV as a range chart, notes holding commas and doubled quotes) + Data/Places (a JSON survey, a station with no color landing on the fallback); TableTests / JSONTests (no pixel snapshot: both are CPU value types). Guide Ch 9 § Numbers you didn't type: CSV and JSON + DataAsMaterial figure. Docs/Helpers/Data.md.
  • Live data (Sources/Ollin/Data/DataFeed.swift). The live sibling of the load-once readers: DataFeed reads one address over and over on a background queue, so a piece draws what is true now and draw() never waits for the network. Made with an address (or a URL) plus every: seconds, as: a Content (.auto/.json/.table/.text) and headers: (where an API key or a contact address goes); driven by start() / stop() / refresh(); read as json / table / text / bytes plus updates / timeSinceUpdate / isRunning / isWaiting / failures / problem. The bytes are parsed on the network queue, never in the frame that first asks, which is why the shape is decided at arrival rather than lazily. Load-bearing: an empty feed and an unreachable one are one state, not two (null/nil throughout, with problem saying why), and a failure never clears what the feed already holds so a sketch keeps drawing the last good answer under the notice; updates counts only answers whose bytes differed, which is what a sketch keys an entrance on (a poll that brought back the same document is not news); the next request is scheduled only from the previous one's completion, so a slow server can never stack requests, and refresh() bumps the generation so the request already on the schedule is orphaned rather than running beside the new one (two chains ask twice as often as the feed promised); the stored ETag/Last-Modified are sent back as If-None-Match/If-Modified-Since and a 304 is a successful non-update, which needs an ephemeral session with .reloadIgnoringLocalCacheData or the shared cache answers from disk and hides the very header that says so; failures back off by doubling up to 8x the interval and reset on the next answer; every: clamps to >= 1s; URL(string:) accepts a bare phrase as a relative reference, so DataFeed.parse insists on a scheme rather than failing much later as an unloadable request; the export path reads once during start(), synchronously, and timeSinceUpdate reads 0 throughout, because a feed fetching per frame would make an export render differently every run (the headless flag is read once in start() and stored, so the rest of the type consults it from any thread). start() is nonisolated, and deliberately: the flag it reads is the only thing that ever wanted the main actor, so OllinApp.isRenderingHeadless is nonisolated(unsafe) and a piece may start a feed from any context. The flag is only believed on the main thread (Thread.isMainThread && ...), because a headless drive owns that thread from setup() through the last frame: a start() anywhere else is not part of it, and taking a process-wide flag at face value off-thread is how a test process hands its own renders to every feed under test (which parked the pool and wedged a whole run before the gate went in). A hop there would be a hop in the tests too, and a hop from a suite that is not on the main actor waits behind every drawing test already queued (see Pushed live data for the measured cost). Deliberate v1 cuts: no response-size cap, no @Param binding (the push transport is its own type — see Pushed live data). A test-only answeredBy: initializer takes URLProtocol classes, so the suite drives every branch with no socket, and the suite is deliberately not @MainActor and touches the main actor nowhere at all. Example Data/Quakes (a day of USGS earthquakes, rebuilt only on a change, the last good list kept through a failure); DataFeedTests (21) + DataFeedExportTests (2; the export read and the off-main start, both sabotage-verified red), with the unchanged-bytes guard, the conditional request, and the refresh generation bump each sabotage-verified red too, including one case through a real URLSession load; no pixel snapshot (the output is values). Guide Ch 38 § Numbers that keep arriving: DataFeed + NumbersThatKeepArriving figure. Docs/Helpers/LiveData.md.
  • Pushed live data (Sources/Ollin/Data/PushFeed.swift + PushTransport.swift). The held-open sibling of the polled feed: PushFeed keeps one connection alive and reads messages the moment the other end sends them. The address decides the transport — ws/wss a web socket (URLSessionWebSocketTask), anything else a stream of server-sent events (the internal ServerSentEventParser, written from the published wire shape: multi-line data:, sticky id:, retry: in ms, comments as keepalives, a CR/LF pair split across chunks read as one ending) — and the surface is DataFeed's (json/text/bytes + event for the latest, updates, timeSinceUpdate, isRunning/isConnected/failures/problem, headers:) plus the draining messages() (oldest first; an undrained feed keeps the newest 256, dropped is still counted) and send(_:) (socket-only). Load-bearing: reconnection is the feed's own job — a drop redials with the retry base (retryEvery: clamped >= 1s; an SSE retry: hint wins) doubling per consecutive failure up to 8x, failures resetting on anything arriving (message, keepalive comment, pong — an accept-then-drop server must not reset the backoff on open alone); an SSE redial carries Last-Event-ID so a blink loses nothing the server still holds (sabotage-verified); greeting: is said at every open, never once (a subscribe message must survive the redial, which is why it is feed configuration rather than a send in setup()); every decision point bumps the generation (start/stop/reconnect()/each redial), so a dead socket's second close report (close frame + task completion) can neither double-count a failure nor fork a second dial chain (sabotage-verified); updates counts every message, byte-identical repeats included — a push is sent because the server had something to say, so the poll's changed-bytes rule deliberately does not carry over; a web socket is pinged every 20s (a failed ping is how a silent death is noticed) while the events side leans on the 60s idle request timeout; an SSE 204 ends the feed for good (no redial — the polite-client rule); the feed's lock only ever returns instructions (greeting send, semaphore signal, redial scheduling are carried in a Followup acted on outside it) and transports deliver outside their own lock, or .opened → greeting send deadlocks; each connection owns its own URLSession, invalidated when done (a session retains its delegate until invalidated); headless export waits for the first message during start() (20s cap), closes, and holds it, timeSinceUpdate reading 0. The suite drives SSE end to end through a streaming URLProtocol stub (a terminal step on the heels of a body chunk must wait ~0.2s or the session drops the undelivered bytes) and the transport-agnostic redial logic through a scripted transport factory (carriedBy:) — no loopback socket anywhere, per the DataFeed environment rule. The suite is not @MainActor and touches the main actor nowhere, which is what keeps it honest: the drawing suites run one at a time on the main actor, so a hop from here waits behind every one of them already queued, and the wait is queue position rather than cost. Measured 2026-08-29 in one 3,107-test run: no hop 74.8 s, one hop 142.6 s, and the test that called start() twice 617.0 s, the run's only failure, against 2.5 s for the whole suite alone. PushFeed.start() is nonisolated so that no hop is needed (believing the headless flag only on the main thread, the drive's own, so an unrelated render is never mistaken for this feed's export), and every wait helper here probes before reading its clock, since a task starved past its deadline otherwise gives up having never looked; one real-URLSession case reads a file:// event stream. Example Data/Edits (the encyclopedia's edits as rain over the Wikimedia stream, verified live by headless export); PushFeedTests (35) + PushFeedExportTests (2; the export read and the off-main start, both sabotage-verified red); no pixel snapshot (the output is values). Guide Ch 38 § Messages that arrive on their own: PushFeed + MessagesThatPushThemselves figure. Docs/Helpers/LiveData.md.
  • The weather outside (Sources/Ollin/Data/Weather.swift + Place.swift). A DataFeed that knows its address and the shape of its answer: Weather(at:) / Weather(latitude:longitude:) / Weather(in: "Oaxaca"), every: clamped to >= 60 s (default 900), the feed's own start() / stop() / refresh() and isRunning / isWaiting / failureCount / problem, read as reading (a Weather.Reading, public memberwise with plain defaults, so a figure or a test draws a sky without asking for one) or field by field (temperature, apparentTemperature, humidity, cloudCover, precipitation, snowfall, windSpeed / windDirection / windGusts, pressure, isDay, condition, sunrise / sunset), all SI and fractions of one. Load-bearing: the provider is Open-Meteo (keyless, CC BY 4.0, non-commercial limits; named in the Docs page and ATTRIBUTION.md, and the request asks wind_speed_unit=ms and timezone=auto, so the answer's local times cross to instants through its own utc_offset_seconds); updateCount counts readings, never bytes (Reading is Hashable, and every answer carries a fresh generationtime_ms, so the feed's own changed-bytes rule would call every poll news); the internal DataFeed.onChange hook is how the reader learns an answer landed, called outside the feed's lock on the network queue and, headless, before start() returns, with both captures weak (a feed holding its own hook never deallocated); a name is looked up once through the provider's geocoding, retried from 60 s rather than the weather's interval, and a lookup that answered with no match sets problem to "no place named …" and stops; the headless chain runs on the main thread by hand (the lookup read, then the forecast started from the same thread), because a forecast started from the geocoder's network-queue hook would not believe the headless flag and the export would render empty; Condition is the open RawRepresentable set over the WMO codes (the raw value is the human word, Condition(code:) and .code round-trip, an unknown code arrives as "weather code N"); Place.sun(at:) is the NOAA low-precision solar model (geometric elevation, refraction left to isUp's -0.833°), checked against a forecast's own sunrise minute and against solar noon on both sides of the equator. The Mac's own location is not read: a bare executable gets no Location Services prompt on macOS 14+ without an embedded Info.plist (the -sectcreate __TEXT __info_plist the reference command-line tool had to add), and embedding one in the hosts may re-attribute their TCC grants, so that half waits for the desk (DESIGN-NOTES.md § Body data, and where the Mac is). Example Data/Outside (the sky over Mexico City, the sun from the clock, clouds and rain from the reading); WeatherTests (13) + SunPositionTests (4); no pixel snapshot (the output is values). Guide Ch 38 § The weather outside: Weather + TheWeatherOutside figure. Docs/Helpers/Weather.md.
  • Audio (OllinAudio satellite). AudioAnalyzer (a rolling last-fftSize sample window — Hann + vDSP FFT re-run per chunk, so waveform is a true rolling window and chunk size never costs resolution) → amplitude / spectrum / waveform / band queries (bass / mid / treble / magnitude(in:)), plus the draw-shaped bands(_:) (log-spaced, adaptively normalized, attack/release) and beat detection (beatCount / timeSinceBeat / beat / beatSensitivity): spectral flux over log-compressed magnitudes (gain cancels in the log difference, so the threshold is loudness-invariant), peak-picked with the online conditions — trailing local max + an additive margin over a trailing mean (multiplicative-only false-fires on steady material — don't regress it) + a sample-counted refractory — and the whole beat surface on the sample clock (timeSinceBeat = samples, not wall clock), so detection is deterministic + testable. The note heard (pitch / nearestPitch / chroma, forwarded by every source): pitch is a DetectedPitch? (frequency, confidence, nearestPitch, cents, fractional midi) from YIN steps 1 to 5 as published (difference function over every lag, cumulative mean normalization, the absolute threshold at 0.1 taking the first dip so an octave low is never reported, parabolic interpolation; step 6 left out), nil under a 0.5 confidence floor (noise reads under 0.2, a chord reads at its common period or nil); chroma is twelve classes C first, max-normalized, from the peak form of the pitch class profile (each spectral peak's power lands on the class of its parabola-interpolated frequency; a bin-sum profile leaked half a note's power into its semitone neighbor at this window length, so never regress to it), peaks floored at 1/300 of the strongest so a Hann sidelobe never counts. Load-bearing: the pitch window is set by the lowest pitch (40 Hz → 2×lagMax = 2,204 samples at 44.1 kHz), not by fftSize, so the sample ring is now max(fftSize, pitchWindow) rounded up and each window unrolls from its own distance behind the head (RMS stays over the last fftSize samples, so amplitude/spectrum/waveform/beats are byte-identical); the audio thread only publishes the window and the reader works it out on first read, once per window (stamped by samplesSeen, cached under pitchLock, the window copied out of stateLock first so the audio thread never waits on the million multiply-adds), so a source nobody asks costs nothing; neither read is smoothed. PitchTests (11 tests: sines and band-limited sawtooths within 3 cents, a missing fundamental, the tuner numbers, a five-octave sweep with zero gross errors, silence and noise nil, the rolling window, three chroma tests, the long-chunk path); verified red by sabotaging the normalization. Examples Audio/Tuner (the bundled violin, --mic, or a path) and Audio/GuitarTuner (the nearest of six strings by midi, cents against that string's pitch, a pretend guitar through a real analyzer until the mic hears a note, since a running mic hearing a silent room must not blank the face); the FollowingANote figure. Sources: AudioInput (mic), AudioPlayer (file), Tone (oscillator), Soundtrack (any core AudioTapSource, i.e. a playing video's audio). Headless export is deterministic for AudioPlayer (the HeadlessVideoReader sibling): under isRenderingHeadless it skips the engine and follows the export clock — a FrameAdvancing sample playhead feeds each frame's slice of the decoded buffer to the analyzer (analyzer runs at the file's rate there, not the mixer's — hardware-independent), so frame k reads the file at k/fps and audio-reactive exports reproduce byte-identically; create the player by the end of setup(). Mic/Tone/Soundtrack stay live-only. Example Audio/Spectrum; Docs/Helpers/Audio.md.
  • Synthesis (Sources/OllinAudio/Synthesis/). The playing side: Synth (@MainActor, AudioSource) is a polyphonic instrument a sketch asks for notes (play(_:velocity:for:after:) / noteOn / noteOff / allNotesOff / play(chord:)), over the Voice value type (waveform + Envelope + optional Voice.Filter + detune + gain, presets .pluck/.bass/.pad/.bell/.stab/.breath/.sine), pitched by Pitch (ExpressibleBy{Integer,Float,String}Literal → 60 / 60.5 / "C4"; MIDI as a Double so bends and non-equal tunings have somewhere to land), with Delay / Reverb slots on the output. Load-bearing: the renderer is pure and engine-free (SynthRenderer: events in, buffer out, no clock, no AVFoundation, noise from a per-voice SplitMix), which is what makes the sound testable at all and what a sound-carrying export would be built on — a feature that reaches for the engine from inside it is going the wrong way; the sketch reaches the render thread through a lock-free SPSC EventRing of trivially-copyable events (preallocated slots, two atomic counters, one written by each end, drop rather than wait when full) and continuous values are held in atomics (gain as a Double bit pattern, activeVoiceCount published once per block) — never a lock and never a plain property, the render-thread rule's audio cousin; the render block is built in a free function (the @MainActor-closure SIGILL rule); the effect chain runs in the output's channel layout, not mono (the units reject a format the hardware end does not use, and the failure is a thrown exception, not an error to catch — the bug that killed the example on first run), and the units are wired once and mixed dry rather than attached on demand (rebuilding a running graph gaps the sound); voice stealing takes a fading note before a held one (so a melody over a chord eats its own earlier notes, pinned by measuring the chord's pitch against the held-melody counterfactual) and a stolen voice gets a ~5 ms fade while the new note's attack runs from the level already reached, which is what keeps both ends of a steal click-free; oscillators are PolyBLEP-corrected (triangle by integrating the corrected square at gain 4·dt, measured peak 0.80…1.10 across 55…3520 Hz) and the filter is the exact Cytomic trapezoidal SVF (g = tan(π·fc/sr), a1 = 1/(1+g(g+k)), bounded v1/v2/v3 tick), chosen because a cutoff that moves every sample must stay where it was asked for and stay stable; Envelope.level(at:heldFor:) is the closed form of what is actually played (pinned against the runner to 0.02, one-sample time base) so a sketch can draw the sound it designed. Deliberate v1 cuts: one voice per Synth, no sequencer, no sampler, no 3D placement, no audio in an export. Tone folded onto the shared Waveform + anti-aliased oscillator (its naive saw used to alias). No pixel snapshot (sound); SynthTests (22) pins it behaviorally, and SynthAudition renders a listenable WAV under OLLIN_AUDITION (a sound cannot be snapshotted, so the audition render is the figure). Example Audio/Synth; Guide Ch 39 § A sketch that plays + Voices figure; Docs/Helpers/Synthesis.md. Per-note expression — noteOn / play hand back a PlayingNote (@discardableResult, so a sketch that ignores it is untouched) and noteOff(_ note:) / bend(_:semitones:) / press(_:_:) / slide(_:_:) reach that voice alone, through three SynthEvent kinds carrying noteID and amount; pitchBend is the whole instrument's, an event too so an export replays it where it fell. Load-bearing: a voice keeps its base pitch and a separate sounding pitch (base + its bend + the instrument's), so noteOff(_ pitch:)'s nearest-note rule still finds a bent note by the pitch it started on; expression glides per block (an 8 ms one-pole stepped once per render in settle), and a value arriving in the same drain as its note-on lands whole (a controller says where the finger is before it says the finger is down); every model cut to length at note-on gained a retune that keeps the line's contents (WaveguideLine.setLength, the string's delay/eta, the sampler's baseStep, the wavetable's increment and level, the patch's increments), called only when the offset moves (tunedTo), while the oscillator reads sounding per sample and the struck body holds (its tones are the strike's); press is the note's own drive on a driven voice from the first press (hasOwnPressure, else the instrument's pressure) and on any other voice raises the level to struck + (1 - struck) * pressureAmount * pressure (Voice.pressureAmount, 1 by default, applied only to a pressed note, so an unpressed note is byte-identical); slide scales the cutoff by 2^(slideAmount * (slide - 0.5) * 2) (Voice.Filter.slideAmount, one octave by default, skipped at exactly 0.5); the synth dedupes each note's last sent values so a per-frame hand-over costs the render thread nothing. SynthExpressionTests (10): one voice bends alone; the handle and the pitch both release a bent note; the whole instrument bends; every model but the struck one follows an octave bend; a press lifts one bow and lands byte-identical at the instrument's own drive; a press raises a struck level 2.5x at velocity 0.4; a slide opens and closes the filter and rests byte-identical; an unexpressed chord is byte-identical with and without identities; an expression sent with its note lands whole. The voice filter's band-pass is unity at its cutoff (k * v1, the normalized band of the trapezoidal state-variable filter) whatever the resonance, so resonance narrows the band and never makes it louder; the bare band output peaked at Q, five times at resonance 0.9 (SynthTests.theBandPassIsUnityAtItsCutoffAtEveryResonance).
  • Algorithmic composition (Sources/OllinAudio/Composition/). The note-deciding half beside synthesis, which shapes the sound: Rhythm (Bjorklund's even spread as Rhythm(k, in: n), written-out patterns via ExpressibleByStringLiteral, onsets/intervals/rotated/inverted, 11 named traditional rhythms), Scale (root + intervals, Mode catalog, degrees running both ways past the ends, snap(_:) for a pitch decided by something that is not music, chord(on:noteCount:spacing:) stacking out of the key), Chord (root + Quality + inversion + spread(over:)), Arpeggio (a pattern over a chord's notes), MarkovChain<Element> (order-n with backoff, continuations reading the learned counts back; in the core, Sources/Ollin/Math/MarkovChain.swift, since it picks colors and lengths as readily as notes, its generator SplitMix64 started one step past the seed so every walk a seed gave before the move comes out the same, pinned in MarkovChainTests; learn(_:after:times:) declares a table of counts with no sequence behind it, each call counted as that many occurrences would be, so the counts land under every shorter tail of the context and an unseen context backs off to them, and a table declared in a sequence's own order walks exactly as the learned sequence does; the Kawano recreations are its picture-side customers), StepCounter (beats in, step numbers out), Note (pitch + velocity + a NoteLength, played via Synth.play(_:tempo:)), and the two values that know a second, Tempo + NoteLength (in the core, Sources/Ollin/Math/Tempo.swift: beats(at:) / bars(at:) / seconds(of:) / seconds(beats:) / seconds(bars:) / secondsPerBeat over beatsPerMinute + beatsPerBar; the stave names .whole….thirtySecond with .dotted / .triplet / * / +, a beat being a quarter; both literal-expressible, so tempo: 120 and length: 0.5 still read). Load-bearing: every type answers a step number and owns no clock, which is exactly what keeps the tier free of OllinMIDI (satellites can't depend on each other) and what lets time, a detected beat, and a TempoClock drive the same pattern unchanged; randomness is seeded per object, never the sketch's rng (Arpeggio.random is a pure function of step + seed, the halton/brush rule; MarkovChain keeps its own generator, rewound by reset()); the Markov table is a Dictionary but every decision walks an array (successors in first-seen order, continuations tie-broken by that order) so a seeded walk can never depend on hash order — the determinism rule; .upDown/.downUp drop both ends of the way back (repeating the turnaround reads as a stutter, pinned across counts 2…6) and .asPlayed is the only pattern that does not sort (a hand-made voicing is otherwise silently re-laddered); StepCounter returns a Range because a frame is longer than a step at any real tempo, fires step 0 on the first call (a pattern starts on the downbeat), re-fires the landing step when time goes backwards (a loop's downbeat), and jumps rather than emptying the pattern into one frame past maxCatchUp (the maxTimestep precedent); Chord.inversion must not wrap (a full turn is the chord an octave up, a negative one drops notes from the top — the mod-only form silently returned the original); a Tempo parameter is never overloaded against a Double under one label (both take a literal, so tempo: 120 is ambiguous, verified with swiftc; the tempo-taking calls take Tempo only, and a Double variable is Tempo(x)); the conversions keep the examples' own operand order (seconds * bpm / 60, beats * 60 / bpm), so a converted sketch lands on the same step in the same frame, pinned bit-for-bit in TempoTests; a @Param tempo stores .number(bpm) so every payload consumer (the web controls, the room, the remote inspector, the source write-back) reads it as a number, and ParamValue.restored(_:keeping:) (a defaulted requirement) is what keeps beatsPerBar across a reload; the MIDI and OSC binders hold a write closure rather than a Param<Double>, which is what lets bind take a Param<Tempo>; NoteLength is a ParamChoices menu of the stave names, and a length off the menu reads as the first entry, the protocol's rule. Bjorklund is written from the paper's construction, and where its two notations for E(7,16) disagree the gap vector is the one the construction gives and the one Ollin follows (Rhythm's maximal evenness — gaps taking at most two lengths differing by one — is property-tested over every k,n up to 32). Scale.Mode / Chord.Quality / Arpeggio.Pattern are ParamOptions, so a key or a figure is a live parameter. Deliberate cut: no arrangement (nothing holds a whole piece with sections; the sequencer and the arpeggiator below hold a bar and a figure and read the beat handed to them, so a sequencer arrived without the tier gaining a clock). No pixel snapshot (pure CPU values); CompositionTests (35, twin-pair based) + MarkovChainTests (12, in OllinTests: the pinned walk, the declared table against the sequence it was counted from, times, backoff, a long context read by its tail) + TempoTests (12, in OllinTests, the bit-for-bit sweep and the parameter round-trips) + a tempo binding case in each loopback suite + CompositionAudition (a 40 s piece rendered through the engine-free renderer). Example Audio/Generative; Guide Ch 40 § Hits spread evenly: Euclidean rhythms with Rhythm + Euclidean figure; Docs/Helpers/Composition.md.
  • Listening: speech and sound events (Sources/OllinAudio/Listening/). The audio sibling of the vision trackers, and the tier's input half beside synthesis: SpeechListener turns talking into words and SoundClassifier names everyday sounds, both binding to any AudioTapSource the way a tracker binds to any FrameSource. No new core seam (the shipped AudioTapSource is exactly right); what was added is conformance on AudioInput / AudioPlayer / Tone (an AudioTapRelay uses the existing analyzer tap, forwarding the whole buffer as mono, since an analyzer keeps one windowful but a listener transcribing words needs every sample) plus an internal AudioTapHub, one per source (the SourceAnalyzers mirror), which owns the single tap slot and fans out weakly, so a Soundtrack + a SpeechListener + a SoundClassifier over one microphone coexist where the old direct-slot grab silently unseated the previous consumer (Soundtrack now routes through it and releases the slot when the last listener goes). Speech is the modern on-device SpeechAnalyzer/SpeechTranscriber, not SFSpeechRecognizer, which settles the permission question the design note guessed at: probed, a bare swift run binary transcribes with authorizationStatus == .notDetermined and no prompt (fully on-device, no TCC consent for recognition; the microphone keeps its own, attributed to the launching terminal as always). The result surfaces are picked per question, not inherited: speech gives caption (the running guess, tail included, trimmed to maxCaptionWords) for drawing, transcript + the draining phrases() for acting (the whole point: an uncommitted result replaces the tail because that is what the recognizer is doing to it, so a trigger on caption acts on a word that can be taken back); classification gives a level (confidence(of:) / top / classifications) beside a trigger (draining events() + non-draining timeSinceHearing(_:), both on the sample clock), where an event is a crossing from below so a continuing sound fires once and not once per window. SoundClassification and SoundEvent live in the core (Core/SoundClassification.swift), not in this satellite, because a tethered iPhone's ears (PhoneDevice.sounds, under iPhone capture app) report the same values and the satellites cannot depend on each other. Load-bearing: the recognizer takes a moment to start and audio arriving first is held, not dropped (a bounded 5 s pre-roll flushed in order, since the first words into a microphone are the ones worth hearing) with isListening the read for it, distinct from isAvailable (which is true until something is wrong, model install included); both listeners are live only and say so under OllinApp.isRenderingHeadless rather than sitting silently empty, with the deterministic one-shots (SpeechListener.transcribe / SoundClassifier.classify over samples / URL / resource:in:, no default bundle) the way into an export, reached from setup() through a new no-argument waitFor { } (the OllinVision sibling; parks the caller, so never from an async context and never touching the sketch inside the closure); the classifier runs off the audio thread on its own serial queue (drop past 4 queued, a classifier being far slower than the audio it judges) while speech converts off it and drops nothing (a dropped block is a missing word); a sample rate of 0 must be rejected before SNAudioStreamAnalyzer, which throws an uncatchable ObjC exception on a zero-rate format (a real crash the tests caught). windowDuration defaults to 1.5 s on measurement, not by feel: swept 0.5...3 s against synthesized tones and taps, 1.0 s is the one setting where a click train reads as a synthesizer (0.60) while 1.5 s keeps taps at click 0.74 and silence under 0.21. A user's own Core ML classifier is in scope (model: / modelAt: an .mlmodelc, the ModelTracker precedent + the stable-compiled-path gotcha). Deliberate cuts: no per-word confidence, no speaker separation, no wake word, no @Param binding (a confidence is already a plain draw() read; @Smoothed if it jitters). No pixel snapshot (a sound cannot be snapshotted): ListeningTests (18) run over audio the tests synthesize, each claim against a counterfactual, and the two end-to-end speech checks speak to themselves through AVSpeechSynthesizer.write so nothing recorded is committed. Gotcha that made both speech tests pass vacuously at first: the synthesizer delivers on the main queue, so the caller must let the main thread go (await in an async context, pump the run loop in a plain synchronous one like a figure's setup()); blocking on a semaphore collects zero frames and the test soft-skips silently. Example Audio/Listening; Guide Ch 37 § Words, and what that noise was: speech and sound events + the Listening figure (figure: unstable, both models being the system's: four real transcriptions of growing prefixes of one spoken sentence, the third of which is confidently wrong, beside three arithmetic sounds and what the classifier called them); Docs/Helpers/Listening.md.
  • MIDI files (Sources/OllinAudio/Composition/MIDIFile.swift + MIDIFileReader.swift + MIDIRecording.swift). Standard MIDI Files read and written, from the MIDI Association's specification (no vendored reader): MIDIFile carries format (.oneTrack / .parallelTracks / .separatePatterns), division (.perQuarter(n) or .perSecond(framesPerSecond:ticksPerFrame:)), name, tracks, tempoChanges, timeSignatures, markers; MIDIFile.Track is name / channel / program / notes / controls / bends with isEmpty / lastBeat. In through init(contentsOf:) / init(resource:withExtension:in:) / init(data:), out through data() / write(to:) (a String path, the house rule), built from notes by MIDIFile(_:tempo:name:channel:). Reading across the piece: notes, notes(from:to:), lastBeat, duration, tempo, tempo(at:), timeSignature(at:), and the tempo map from either side, seconds(at:) / beats(at:). A Synth writes down what it is asked to play (startRecording(tempo:name:) / stopRecording() / recordedSoFar() / isRecording), tapped at the single emit funnel so a take catches the same notes live or under an export. Load-bearing: it lives in OllinAudio, never OllinMIDI, because a file arrives as music (its notes are ScheduledNotes, so Synth.play(_:tempo:from:) takes them with nothing in between) and satellites cannot depend on each other; a track plays on one channel (a chunk using several splits into one Track per channel, in first-appearance order, which is what makes the common format 0 file useful and what makes the write side exact); the first chunk of a parallel-track file is the tempo track (absorbed into the file's own map on read when it holds no channel event, so a round trip keeps its track count); note pairing is FIFO per channel and pitch (a pitch struck again before its first release pairs oldest-first; oneNoteStruckTwiceIsLetGoInTheOrderItWasStruck pins it) and anything still sounding is let go at the track's end; running status is the reader's one place a byte is pushed back (its rule releases at velocity 64, never 0, or a run read as the wrong kind still comes out right by being a note-off in the other spelling, which is how the first version of that test passed under sabotage); a meta payload is taken as its own array, since a slice keeps the file's indices and reading it from 0 traps; a note-on of no loudness is a note-off both ways, so the quietest note written is 1 of 127; a tempo is whole microseconds in a quarter note (120 and 96 exact, 112 a ten-thousandth off, so tempos compare with a tolerance) and the default 960 ticks divides by three, so triplets and swing survive; beats(at:) and seconds(at:) walk the map rather than dividing by one tempo, and are exact inverses; a film-timed file converts through seconds with the map integrated over it. Read and written: notes, control changes, bends, programs, track names, tempo, time signatures, markers. Read and stepped over: aftertouch, system exclusive, key signatures, the rest of the meta set, and any chunk whose kind is unknown (skipped by its own length, as the format says). MIDIFileTests (21) + MIDIRecordingTests (5); four sabotages verified red (the pairing order, the continuation bit, the borrowed status, and a missing end-of-track event). The outside reader is the test our own reader cannot be: somebodyElsesReaderAcceptsWhatWeWrote loads a written file through AVMIDIPlayer, Apple's own parser, and holds it to the same duration (25 ms apart on the example's piece, tempo change and all) — drop the end-of-track event and every other test here still passes while that one alone goes red, which is exactly the class of defect a reader that wrote the bytes forgives. No pixel snapshot (the output is bytes). Example Audio/MIDIFiles (eight bars composed, written, forgotten, read back, and everything drawn and heard after that comes off disk); Guide Ch 40 § Music that travels between programs: MIDI files + the MIDIFileFigure; Docs/Helpers/MIDIFiles.md.
  • Sonification (Sources/OllinAudio/Composition/Sonification.swift). Numbers read out as notes, the reading half beside the deciding half: Sonification is a value type spreading a series over a pitch range, snapped through a Scale, read by [step] / note(at:) / notes() / pitch(for:) — it answers a step number and owns no clock like the rest of the tier, so StepCounter drives it. Four sources, one call each: [Double], Table column, Heightfield row:/column:/from:to:count:, Image row:/column: (luminance). Parameters: in scale:, pitches: (default "C3"..."C6"), bounds: (.extremes / .robust(ignoring:) / .fixed), polarity:, noteLength: (a NoteLength, .sixteenth by default), plus amplified(by:) / inverted() / reference(at:). Written from the published technique (Grond & Berger, The Sonification Handbook ch. 15; credited in ATTRIBUTION.md, never in .swift). Load-bearing: pitch is spread linearly in MIDI note number, never in hertz (hearing is logarithmic, so an even-in-hertz spread crushes the bottom half of the data into the top of the range — the figure draws both, and pitchIsSpreadEvenlyInSemitonesNotInHertz pins the two mappings far apart so the test can't pass by accident), and loudness linearly in decibels (velocity = 10^(dB/20), so the floor is a real fraction rather than silence) — both are the perceptually linear units, which is what makes a linear transfer function correct; loudness does nothing unless asked (loudness == nil → every note at full level) because how loud something sounds depends on how high it is, so it is for a second series, not for saying the same thing twice; .robust uses interpolated quantiles (one outlier at 1000× otherwise flattens a whole series into one note — pinned against its .extremes twin at span <1 vs >30 semitones); values outside the domain are held at the ends, a flat series reads at the middle (no divide by zero), and an empty one plays nothing; the reference tone is the published grid-line idea (without it a listener needs absolute pitch for a note to mean anything) and lands exactly where that value would in the reading. No rng anywhere, so a reading is a pure function and an export repeats. SonificationTests (20, exact values + twins); SonificationAudition (four takes of one landscape row: chromatic / snapped / inverted / with a reference). Example Audio/Sonification (a terrain profile drawn and read out at once, playhead on the sounding note); Guide Ch 40 § Numbers you can hear: sonification + Sonification figure; Docs/Helpers/Sonification.md.
  • Composition polish: progressions, tunings, following a beat (Sources/OllinAudio/Composition/Progression.swift + ChordSymbol.swift + Tuning.swift + BeatFollower.swift). Four of the five gaps the Composition page listed as not here yet, all pure values answering a step number and owning no clock like the rest of the tier. Progression is a cycle of chords written as scale degrees, not names (roman numerals I…VII, case ignored, any separator), so the qualities fall out of the key and the same four numbers are major or minor with nothing changed; pitches(at:) / degree(at:) / root(at:) / chord(at:) / transposed / rotated, named set (.pop / .fifties / .twoFiveOne / .blues / .andalusian / .circleOfFifths), and wandering(_:order:seed:) over a MarkovChain learned round the loop (loops: true, so the last chord returning to the first is a move) and seeded per call, never the sketch's rng. Chord(symbol:) + ExpressibleByStringLiteral parses chord symbols (root / accidentals / quality / /bass); load-bearing: the quality is tried on the whole suffix before a trailing number is read as an octave, or Cmaj7 strips the 7 and becomes major in the wrong register (the bug the tests caught), and a bad symbol is nil from init(symbol:) while the literal notes once and lands on C major (the Pitch precedent). Progression(symbols:) / Progression([Chord]) set the parallel written store, so a progression is either degrees-in-a-key or named chords, and count / pitches / root / rotated / transposed honor both. Tuning is the Scale sibling for pitches described by ratios and a period rather than semitones ([degree] / snap / cents / frequency / rooted(at:), .equal(_:period:) + .just / .pythagorean / .quarterTones / .nineteen / .thirtyOne / .bohlenPierce): ratios fold into one period and dedupe within 0.01 cent, degrees run past both ends, and because Pitch.midi is a Double a non-twelve degree simply lands between the keys. Deliberate: Scale is untouched and stays twelve-tone (a tuning is a separate type, not a setting, so nothing existing changed) and chords are not built out of one. BeatFollower + the pure BeatEngine (the TempoClock/TempoEngine split: onset times in, musical time out, testable by handing it a beat) turn detected onsets into beats a StepCounter runs on, plus tempo / steadiness / rhythm(steps:). Load-bearing: a tempo outside the believable range is halved or doubled until it lands inside (a detector firing on every eighth reports twice the tempo, which is the same music), the tempo is the middle of the recent gaps, never the average (one missed beat doubles a gap, which moves an average and not a middle), and the position re-anchors to the nearest whole beat on each onset so a missed one costs nothing. ProgressionTests (31, exact values + twins) + ProgressionAudition. Examples Audio/Changes (degrees) + Audio/ChordSymbols (the symbol path, typed into a text parameter) + Audio/Tunings (all seven, a triad held while the cents ladder shows what moved) + Audio/PlayAlong (a generated pulse standing in until the microphone is allowed); Guide Ch 40 §§ Chords that come out of a key: Progression / Other divisions of the octave: Tuning / Playing along with the room: tempo sync; Docs/Helpers/Composition.md.
  • Step sequencer and arpeggiator (Sources/OllinAudio/Composition/StepSequencer.swift + Arpeggiator.swift + ScheduledNote.swift, over a wait in SynthRenderer). StepSequencer is a bar of Steps (pitch or rest, velocity, probability, ratchet) with rate / swing / gate / seed / maxCatchUp on the bar, written out as a string literal ("36 . . 36"; init?(pattern:) is nil on a bad token and the literal notes once and lands empty, the Rhythm precedent), from a Rhythm + one pitch (a drum lane), or empty to fill in; Arpeggiator is the live Arpeggio (the figure recomputed from playing, which is notes or the latched set), hold / release / notes set per frame, figure / next / nextStep for drawing; both answer beats and own no clock (a StepCounter inside, so the tier's rule holds) and hand back ScheduledNote (a Note + beat + step), which Synth.play(_:tempo:from:) plays: from: is where the music is now, so a note whose beat is ahead waits to the sample, and nil plays everything with the frame (where every other call lands). Load-bearing: the wait is Synth.play(_:velocity:for:after:), a delaySamples / delaySeconds pair on SynthEvent: live, the renderer parks a delayed event in a fixed 256-slot pending store (no allocation on the render thread; past capacity an event lands at once, never late) and starts it inside the block on its own sample (one compare a sample while nothing waits), settling the fresh voice itself since the block's settle has already run; allNotesOff drops pending notes (a waiting note is more held than a tail); in an export the wait is folded into RecordedNote.at and the fields zeroed so the renderer cannot count it twice, and record inserts by place, not on the end, since a waited note can be asked for before an earlier-landing one; the look-ahead is the sketch's: events(upTo:) with the beat at the end of the frame and from: with the beat now, the pair the Docs teach, because deltaTime is exactly 1/fps under export and the measured frame live, and only that pair lets a swung offbeat land off the frame grid (at 120 bpm a frame is 17 ms and the shuffle's move is 42). Swing moves only the odd steps, by (2·swing − 1)·rate, so downbeats hold the grid; a ratchet subdivides the nominal step (not the swung interval) and the gate applies per strike; probability is a pure hash of (absolute step, seed) (StepTiming.plays, the Arpeggio.random mixer, top 53 bits), never the sketch's rng, so a bar replays; the arpeggiator's cursor is an index into the figure that restarts only on a chord after silence (notes empty → non-empty) and otherwise keeps its place modulo the new count (a note added joins the ladder on the next pass, no stutter, no restart, pinned), .random reads figure[step] instead; a rate change re-anchors the counter at the last position through reset(to:), so no burst and no skipped stretch. Verified on a rendered sample (SequencerLaws.aSwungBarIsHeardWithItsOnsetsWhereTheNumbersSay): a bar through the export path at 120 bpm has onsets alternating 4/3 and 2/3 of a sixteenth at swing 2/3 within 2 samples, downbeats unmoved, and a ratchet of three a third of a step apart; the renderer tests pin waits of 0 / 100 / 511 / 512 / 1000 / 5000 samples landing exactly, and a test that reads a whole bar in one call must raise maxCatchUp (the counter's stall rule fires at sixteen). SequencerLaws (25: twelve sequencer, nine arpeggiator, three renderer, two export). Example Audio/Sequencer (click a cell to toggle it); Guide Ch 40 § Steps with a feel: the sequencer and the arpeggiator + the Sequencer figure (taught); Docs/Helpers/Composition.md#stepsequencer + #arpeggiator; Docs/Helpers/Synthesis.md for the wait.
  • Physical models: the plucked string (Sources/OllinAudio/Synthesis/PluckedString.swift + StringVoice.swift). A voice worked out rather than drawn: VoiceSource (.wave(Waveform) / .plucked(PluckedString)) is now what a Voice is built from, with waveform kept as a computed view over it so every existing call site, preset, doc, and example is unchanged; PluckedString carries the four controls a player has (pick / hardness / decay / damping) with presets .nylon / .steel / .harp / .muted (and Voice presets of the same names), and Envelope.plucked is the stay-out-of-the-way gate the string's own fade runs under. The render side is the extended Karplus-Strong loop: delay line → one-zero loss filter → first-order tuning allpass → loop gain. Load-bearing: the loop must come out exactly one period long, so the loop filter's own delay (shade, which is its coefficient) is subtracted from the budget before the integer split and the allpass supplies the rest — which is why damping cannot move the pitch (pinned by a twin pair differing only in damping; the analytic soundingFrequency is within 0.01 cents across MIDI 21…105 where rounding the loop would be >40 cents out at the top); the integer part is floor(remaining − 0.2) so the fraction lands in [0.2, 1.2), the range where a first-order allpass is well behaved (a fraction near zero puts its pole on the unit circle); the excitation is built in a scratch half of the buffer, because the pick-position comb reads the burst it is overwriting, and the comb wraps circularly so its notches land on the harmonics rather than near them; the burst must be written where the first read will reach it (write = count, not 0 — leaving the write head at the start points it at the empty tail and the string is silent, the bug that shipped nothing at all until a probe caught it); velocity multiplies into the excitation's bandwidth as well as its height (playing harder is brighter, the dynamic-level filter's actual musical job); every string's delay line is one allocation taken in SynthRenderer.init (raw memory, the EventRing precedent) sized by lowestStringFrequency (16 Hz), since the render thread may not allocate, and a note below it plays sharp rather than outrunning the buffer; detune gives a second string, symmetric with the oscillator path. SynthEvent stays trivially copyable (PluckedString is four Doubles), which is what lets a voice change be sent through the event ring. No pixel snapshot (sound); PluckedStringTests (11, twin-pair based: pick position vs the even harmonics, hardness and velocity vs the spectral centroid measured at the note's own harmonics rather than on a fixed grid, decay vs level at one second, damping measured above 4 kHz because a one-zero loss removes almost nothing near the fundamental) + PhysicalModelAudition. decay is the fundamental's T60 at every pitch (loopLoss, shared with the bow): the loop gain is the per-trip target divided by the one-zero's response at the note, and where the target exceeds what the filter alone allows under the 0.99999 cap, the coefficient is eased to the value whose response is exactly the target (Jaffe and Smith's decay stretching), so the top harmonics there fade closer to together; within 10% from MIDI 33 to 93 on steel, nylon, and harp (SoundInvariantTests.aPluckedStringRingsForItsDecayAtEveryPitch), where the uncompensated loop rang a sixth of the steel's 3.6 s at MIDI 93; pitch unmoved (the eased coefficient stays the filter's delay in the budget, within the tuning test's cent). Example Audio/Strings (click a string where you want to pluck it; the drawn shape is the modal sum that pluck excites, which is the same fact as the comb); Guide Ch 39 § A string worked out sample by sample: the plucked string + PluckedString figure; Docs/Helpers/Synthesis.md#physical-models.
  • Physical models: the struck shape (Sources/OllinAudio/Synthesis/ModalBody.swift + ModalShape.swift + ModalVoice.swift). Modal synthesis, and the reason it earns its place: StruckShape(_ shape: Shape, modes:resolution:) measures a drawn outline and body(struckAt:decay:damping:hardness:) hands back a ModalBody (.drum / .plate / .bar / .bell / .wood / .glass; Voice presets .drum / .bar / .chime / .glass), so a shape a sketch drew rings at the frequencies its own geometry allows. The frequencies are the Dirichlet eigenvalues of the Laplacian on the shape (a membrane held at its rim), solved by measuring the outline onto a grid then banded Cholesky + subspace iteration with Rayleigh-Ritz + cyclic Jacobi on the small projected problem, all written here (no LAPACK interface). Load-bearing: the model is verified against physics, not itself — a circle returns the Bessel-zero ratios (1, 1.593, 2.136, 2.295…) and a square √((m²+n²)/2) to under 0.5%, which is what ModalBodyTests pins; a symmetric shape returns its tones twice (a pattern that fits at one rotation fits at another) and that is correct, not an artifact, so a gain readback must sum over a degenerate pair (which member takes the strike is arbitrary; the sum is the invariant, and the first test draft failed exactly here); ratios come out scale-free (the grid spacing cancels), so size never changes the instrument, only the note does; ModalBody stores its modes in SIMD16<Double> pairs, not an array, because Voice is carried in SynthEvent through the lock-free ring and a heap reference would refcount on the render thread (hence maxModes = 16, and modes is a computed view); a tone landing above Nyquist is dropped, never folded (it would arrive as something that was never struck); each resonator's input is scaled by sin(its own angle) so every tone comes out at the gain asked for rather than at whatever its frequency gives, and the strike is divided by the summed gains so sixteen tones are not sixteen times as loud; the excitation is a raised-cosine push whose width is hardness (a wide one reaches only the low tones), divided by its own area so hardness is the color of a strike and never its level: the push is 12 ms of contact at 0 and two samples at 1, and undivided it was its length in samples times too loud, so every preset but .wood sat on the renderer's soft clip at its attack (the bell above 0.95 for 56% of its first second, glass 92%, the drum 6%) and the clip's intermodulation put lines at 1.4 and 3.4 times the note 7 dB under the bell's prime; divided, the bell peaks at 0.58 with those lines over 50 dB down, and the loudness is off the sample rate (within 3% between 44.1 and 48 kHz). The division also showed the presets had been tuned under the clip: at the bell's old hardness 0.6 its octave sat 30 dB under its declared gain and the glass lost its fundamental an octave up, so the presets strike hard (bell and bar 0.9, drum, plate, and the default 0.85, glass 0.8, wood 1), each ringing its declared partials within about 3 dB at MIDI 60 to 72 (the bar's and wood's top partials read lower only because the window outlasts their decay), and the four struck Voice presets carry gain 1, peaking at 0.46 to 0.57 at velocity 0.8 beside .pluck's 0.67 (aStruckBodyIsASumOfTones over six presets, aSofterStrikeIsDarkerAndNeverLouder); resolution defaults to 40 because accuracy plateaus there (measured: 28 → 1.11% error, 36 → 0.34%, 64 → 0.22%, at 0.9 s / 1.7 s / 8.8 s in a debug build and 0.01…0.07 s in release), and the solver's hot loops run over raw pointers on flat storage for exactly the unoptimized-build reason the point-cloud work documents. StruckShape exists because striking must be free — it keeps the eigenvectors so a new strike point is a lookup, where ModalBody(shape:) re-measures every call (pinned equal to each other). No pixel snapshot (sound); ModalBodyTests (19) + PhysicalModelAudition. Example Audio/StruckShapes; Guide Ch 39 § A shape you can hit: modal synthesis + StruckShapes figure; Docs/Helpers/Synthesis.md#physical-models.
  • Sampled instruments (Sources/OllinAudio/Synthesis/SampledInstrument.swift + SFZ.swift + SamplerVoice.swift). The one source that is recorded rather than worked out: Synth.instrument holds a SampledInstrument and Voice(sampled: Sampler) says how to play it, read from SFZ (the text format free libraries ship in) or from Recordings in hand. Load-bearing: the recordings live on the Synth, never inside the Voice — a Voice is carried in SynthEvent through the lock-free ring and must be copyable a word at a time (the constraint that caps ModalBody at 16 and Patch at 8), and megabytes of heap cannot be carried along, so Sampler carries only numbers (_isPOD pinned) and the instrument is set separately; the read head is linearly interpolated (nearest-sample stepping is an audible grainy edge on every note not at its recorded pitch, which is most of them) and moves at 2^(semitones/12) × recordedRate/playRate, so pitch and length move together like a tape; the nearest recording wins where several cover a note (moving a recording far is the one way a sampler audibly goes wrong and the one thing choosing well avoids), and a note outside every range is stretched from the nearest rather than silent; an unknown SFZ opcode is skipped, not refused (the format has hundreds this does not model, and a library using them should still load), a missing audio file costs that part of the range not the instrument, <group>/<global> opcodes are inherited, note names read either way round (60 or f#3), and Windows backslash paths are turned round; the renderer stops applying velocity to a sampled voice because velocitySensitivity is the sampler's own setting and would otherwise be scaled twice (0 = as loud as recorded, for an instrument whose recordings are already its dynamics) — scoped to .sampled, every other source unchanged. The bundled instrument is generated, not sourced (Scripts/make-sample-instrument.swift, a free-free bar's inharmonic ratios at five pitches, 384 KB, .copy resource): Ollin's own, no third-party license, no THIRD-PARTY-NOTICES entry. Docs list where to get real libraries with their license tiers — CC0 (VCSL, VSCO 2 CE, Iowa) bundlable, Freesound per-clip, and Philharmonia free to make music with but explicitly not redistributable as a sampler instrument. Every loader throws (SampledInstrument(sfz:in:) / (contentsOf:) and GrainSource(named:withExtension:in:) / (contentsOf:): AudioError.resourceNotFound / .couldNotDecode / .empty, one vocabulary with AudioPlayer's), where they returned nil beside a printed note; the bundled builtIn stays Optional with a developer note, since a framework resource missing is a broken build. SamplerTests (17). Examples Audio/Sampler (five recordings against one stretched, which is not a subtle difference) + Audio/OwnSampler (SampledInstrument(sfz:in:) over a three-region map and recordings generated beside the sketch, the loop region drawn on each); Guide Ch 39 § An instrument somebody recorded: sampled instruments; Docs/Helpers/Synthesis.md#sampled-instruments.
  • Granular synthesis (Sources/OllinAudio/Synthesis/GrainCloud.swift + GrainVoice.swift). Grains cut out of a sound and piled back up: Synth.grainSource holds a GrainSource (samples, a SampledInstrument.Recording, an audio file, or a drawn Waveform; .builtIn is the bundled struck bar) and Voice(granular: GrainCloud) says how to cut it, over size / density / position / positionJitter / speed / pitchSpread / panSpread / scatter / shape / seed, with presets .cloud / .smear / .rain. The whole claim is two clocks where every other source has one: the grains are read at the note's pitch (by 2^(semitones/12) times the rates' ratio, from the source's rootKey) while the read position travels at speed alone, so pitch and time are separate (theNotesPitchMovesTheGrainsAndNotTheTravel, verified red by making the travel follow the pitch as a sampler's does) and speed: 0 holds a moment for as long as the note lasts, dragged live by Synth.grainScrub (an atomic read every sample, the pressure shape, in source lengths added to where each note has reached). Load-bearing: the sound lives on the Synth, never inside the Voice (the _isPOD constraint, as for the sampler and the wavetable), and a cloud with none set reads .builtIn, the rule .basic keeps for a wavetable; every grain's state is raw memory the renderer owns (GrainSlot, one block per voice taken in init) with sounding grains packed at the front so the sample loop walks only what sounds, and the envelope is a table built at GrainVoice.warm() from the renderer's own setup, since a static let touched first by the render thread is an allocation there; a note may have 48 grains sounding and drops past that rather than waiting (the event ring's bargain); the sound wraps rather than running out (position 1 is position 0, and a grain reaching the end reads the start, since stopping would cut its envelope and click); onset gaps blend regular to exponential by scatter with the mean preserved either way, so scatter changes the texture and not the loudness, and at 0 a frozen cloud is periodic at the density, a pitch of its own; .tick/.swell have their exponential floor taken out so a grain ends at nothing (a hundredth at the end of a grain is still a click, and there are a great many grains); detune shifts alternate grains rather than running a second cloud. panSpread is why the renderer gained a second channel (render(into:right:frameCount:)): a block with nothing panning takes the one-channel path and writes it to both, and the scaling is spelled out per channel rather than through a shared factor because the two multiplication orders do not give the same bits — pinned by nothingPanningRendersTwoChannelsAsOne over a cloud, a wave, a string and a struck body (verified red), and by the whole 425-test audio suite passing unrecorded; pan is equal power scaled so the middle is unity, a panned cloud needs its own right-channel filter (one filter carries one stream's memory), and a placed Synth is one stream by definition so the spread folds back to the middle. A scrub made while an export's frames go by is written down and replayed (RecordedScrub, the RecordedPose lane) or an exported drag comes out frozen (aDragMadeWhileTheFramesWentByIsInTheFile, verified red). GrainShape.level(at:) and Synth.grainCount are the read-backs a sketch draws with (Envelope.level(at:heldFor:) and activeVoiceCount precedents). Written from Roads's Microsound and Gabor's acoustic quanta, credited in ATTRIBUTION.md. GrainCloudTests (21: one grain the source sample for sample through a plateau's flat middle, RMS as the square root of density, a frozen spectrum against a traveling one, a scrub moving a sounding note, the loop, backward, the two clocks, pitch spread widening the peak, the clocked line, the two-channel rules, the seed) + 2 in SoundExportTests; GrainCloudAudition writes a two-channel WAV under OLLIN_AUDITION. Example Audio/Grains; Guide Ch 39 § A sound in pieces: grains; Docs/Helpers/Synthesis.md#grains.
  • Effects as a chain (Sources/OllinAudio/Synthesis/Effect.swift + the wiring in Synth.swift/SynthExport.swift). The output side of routing-as-a-value, closing the symmetry the patch tier opened: Synth.effects: [Effect] is an ordered chain (.delay / .reverb / .equalizer(Equalizer) / .distortion(Distortion)) replacing the two fixed slots. Load-bearing: order is the feature (a distorted echo and an echo of a distorted sound are different, pinned by exporting both and diffing); delay/reverb survive as computed views over the chain (read the first of that kind, write replaces in place or appends, nil removes — replacing must not walk an effect down the chain, pinned) so every existing sketch is untouched; the graph rebuilds only when the sequence of kinds changes (wiredKinds), so a setting change never disturbs the wiring; the rebuild happens on the running engine without stopping it, which is a measured decision, not an assumption — probe: reconnecting while running throws nothing and costs 1 sample, stopping first costs the same 1, and a pure parameter change also shows 1, so the "gap" is the test tone's zero crossing and there is no gap at all (this reverses the old "wire once and mix dry, rebuilding a running graph gaps the sound" comment, which was never measured); all four unit kinds chain at the output format without throwing (probed, since AVFoundation refuses formats by throwing at connect time); the live and placed paths now share one rebuildEffectChain() hanging off chainHead (the listener once placed, the source otherwise), and OfflineRender builds the same chain from the same list or an export would run different effects from the ones the sketch was heard through (the stage-1 lesson). Delay/Reverb gained Codable and Synth.configure became nonisolated so an Effect can be a value. Deliberate cut: the chain is per instrument, not per note. EffectChainTests (13). A built-in room loads as the unit's currentPreset, never loadFactoryPreset, which on macOS 27 names the room and leaves every parameter at the unit's default (the four rooms were one medium hall, live and in an export; theBuiltInRoomsAreFourRooms diffs all six pairs). Delay.time is held to 0...Delay.maxTime (2 s, the unit's own limit) at init, with feedback and mix to their ranges, so what it reads is what echoes (aDelayWaitsAsLongAsItsTimeSays). A distortion at mix 0 bypasses its unit, as a flat equalizer does (the preset colored a bell 7% at mix 0). Example Audio/Patching (the finish parameter, whose last two options differ only in order); Guide Ch 39 § After the note: the effects chain; Docs/Helpers/Synthesis.md#effects.
  • An effect of your own (Sources/OllinAudio/Synthesis/CustomEffect.swift + the .custom case in Effect.swift). The chain's seam for an effect the sketch wrote itself: Effect.custom(name) { AudioBlock in } (plus the state: form, whose memory comes back inout on every block) goes in Synth.effects beside the built-in kinds, anywhere in the order, and reaches the live chain and OfflineRender alike. Load-bearing: the unit is an in-process registered audio-unit subclass (ClosureAudioUnit, instantiated inline like the built-in units) and the closure is the setting, not the wiring — apply(to:) swaps the runner on the standing unit, so replacing one custom effect with another never rebuilds the graph (a new effect does start with fresh state:); a connection touching the custom unit must name the chain's format outright — both chain builders pass the explicit format there — because left to format: nil the engine keeps the unit's declared bus format and silently resamples around it (measured: sample-exact at 1.5e-8 with the format named, worst-sample 0.82 off without); the render block takes the pull buffer's mutableAudioBufferList once and sizes that same pointer (each property access re-derives sizes from the zero frame length, and the engine refuses a zero-sized pull with -50); the user closure must be @Sendable (so it can never be main-actor-isolated, per the render-thread rule) and runs inside the runner's unfair lock (no reference counting on the render thread; a runner placed twice in one chain serializes on its own state), which is also why state: is the sanctioned memory — a @Sendable closure cannot write into a capture; equality is by runner identity, and Codable decodes to a passthrough that keeps its name (a closure cannot be written down). AudioBlock hands over left/right (the same channel on mono), frameCount, sampleRate, and time (rendered frames since allocation, so an export's clock starts at zero). CustomEffectTests (8: per-channel asserted as a relationship, order, block-to-block memory, the 48 kHz rate gate, the passthrough decode, the in-place swap) plus the widened EffectChainTests. Example Audio/Shaping (a wavefolder, a state: crush, a time wobble behind one parameter); Guide Ch 39 § An effect nobody wrote for you: custom effects; Docs/Helpers/Synthesis.md#an-effect-of-your-own.
  • Convolution reverb (Sources/OllinAudio/Synthesis/Convolution.swift + PartitionedConvolver.swift; the .convolution kind in Effect.swift). Reverb(impulse, mix:, preDelay:) plays the instrument through an ImpulseResponse: a recording (.load(path) / .resource(_:withExtension:in:) / init(contentsOf:), each throwing AudioError or the system's own error for a file it cannot open and AudioError.empty for one with no sound, where they returned nil beside a printed note until 2026-09-22; the first two channels kept, cut at maxSeconds = 20), fading noise (.decay(seconds:damping:sampleRate:seed:): sixty decibels over the length, a one-pole whose cutoff falls with time for the damping, the filter's own RMS loss undone so the fade is the fade asked for), or a rule (init(seconds:sampleRate:seed:) { time, noise in }, two seeded SplitMix64 noise streams so a drawn room has width), plus reversed(). Load-bearing: the reverb uses the existing .reverb(Reverb) case (so synth.reverb still reaches it, and a Reverb written down before rooms existed still decodes) but reads as its own Effect.Kind.convolution, since it runs on the ClosureAudioUnit rather than AVAudioUnitReverb and the chain is rebuilt by kind; the delay/reverb views therefore match by case, not kind (a built-in room set over a drawn one must replace it in place, pinned). Effect.apply(to:sampleRate:) carries the chain's rate (Synth.chainSampleRate, read off the head's format at rebuild; the export passes its own), because a room is prepared for a rate: resampled through AVAudioConverter when the file's differs (a click lands within two samples of where it should), pre-delayed, capped, and normalized to unit energy across the sides so mix means one thing for every room. The engine (ConvolutionReverb) is kept on the unit across setting changes (ClosureAudioUnit.room; serves(_:at:) compares impulse, pre-delay, rate) so a mix turn never cuts a sounding tail, the mix crossing to the audio thread as a float under a lock. PartitionedConvolver is uniformly partitioned overlap-save at 256 samples with a direct time-domain head (vDSP_conv run backward over the response) and the tail's partitions in a frequency-delay line; the tail's one-partition buffering delay is exactly the offset its taps carry, so the whole is latency-free and exact at any block size (pinned against the double-precision sum to 1e-5 in blocks of 1 to 4096; the packed DC/Nyquist pair multiplied as reals, the rest through vDSP_zvma ping-ponged between two accumulators since it is not in-place; the inverse of a product of two forward transforms scales by 1/(4N)). A ten-second room measured 2.7% of real time per side in release. The export is pinned to be the dry export convolved with the prepared room, sample for sample. ConvolutionTests (18) + ConvolutionAudition (OLLIN_AUDITION); Examples/Audio/Rooms; Docs/Helpers/Synthesis.md § A room of your own; Guide Ch 39 § A room you can draw: convolution reverb.
  • The four that move (Sources/OllinAudio/Synthesis/Modulation.swift; the .chorus(Chorus) / .flanger(Flanger) / .phaser(Phaser) / .tremolo(Tremolo) cases in Effect.swift, each its own Kind). One slow wave and the thing it moves, written from the textbook definitions: a chorus reads a copy 20 ms behind sliding ±6 ms·depth (the two sides a quarter turn apart, so it is wide by itself); a flanger reads a copy 1 ms to 7 ms·depth behind with feedback written into the line, so held still it is the comb 0.5 + 0.5·e^{-iωD}; a phaser runs stages first-order all-passes (y = a·x + x₁ − a·y₁, a = (tan(πf/fs) − 1)/(tan(πf/fs) + 1)) whose corner sweeps from 200 Hz up by 20^depth on a log path, with feedback from the row's output back to its input, so a pair of stages is one notch; a tremolo scales by 1 − depth·(0.5 − 0.5·sin), spread offsetting the right side by up to half a turn (1 is a swing from side to side). Load-bearing: all four use the ClosureAudioUnit the custom effect and the convolution reverb use, one ModulationEffect per unit kept across setting changes (setMotion, serves(_:at:)), the settings crossing under a lock and read once per block while the wave's phase, the delay lines, and the stages' memories live on, so a turn of any dial changes the motion in flight and never restarts it; the delay lines are read with linear interpolation, so a click through a chorus lands as two samples and a measurement takes the centroid; a sliding copy arrives when the read reaches it, so its delay is the wave at the arrival, not at the click (0.8 ms off the naive formula at full depth, which is the detune a chorus is for, and the first test run's whole failure); the export path builds the same units through apply(to:sampleRate:), so the motion reaches a file. ModulationTests (12 tests: the tremolo's floor, ceiling, and dip spacing; spread as opposite turns; the still chorus as a 20 ms copy and the sliding one following the wave at the arrival; the still flanger's notch at 1/(2D) and peak at 1/D, feedback standing the peak at 0.5 + 0.5/(1 − fb); two stages notching at their corner and four adding notches at f·tan(π/8) and f·tan(3π/8); mix 0 as the plain sound to the bit; a depth turned in flight keeping the wave's phase; the values round-tripping and clamping; the export carrying a tremolo's breathing) + ModulationAudition; Examples/Audio/Movement; Docs/Helpers/Synthesis.md § The four that move; Guide Ch 39 § Something that moves: chorus, flanger, phaser, tremolo + the Movement figure.
  • The three that hold a level (Sources/OllinAudio/Synthesis/Dynamics.swift; the .compressor(Compressor) / .limiter(Limiter) / .gate(Gate) cases in Effect.swift, each its own Kind). What watches how loud the sound is and acts on it, written from the feed-forward design worked out in decibels (Giannoulis, Massberg, and Reiss 2012, credited in ATTRIBUTION.md): a compressor measures the level, reads the static curve (a corner at threshold with slope 1/ratio − 1, and a quadratic knee (1/R − 1)(x − T + W/2)² / 2W across a band knee wide), and smooths the reduction rather than the level, attacking while it grows and releasing while it eases, then applies 10^((makeup − reduction)/20); a limiter listens Limiter.lookahead (5 ms) ahead and rises back toward full over release; a gate follows the level, holds the gate open hold seconds past the last crossing, and moves the gain between 1 and 1 − depth. Load-bearing: the level is read from both sides at once and one gain is applied to both, or a loud note on one side walks the image (pinned with the loud side on each side in turn, and a left-only detector fails the second); the compressor follows the level from crest to crest (a 10 ms decay under the smoothing) rather than reading each sample, since a per-sample peak eases every time the wave passes through nothing and a tone then lands a decibel short of where its ratio says; the limiter's gain is the mean, over the last ahead + 1 samples, of min(the window's smallest need, the release rising): the sound leaves ahead samples late, every gain in the mean saw the leaving sample in its window, so none is above its need and the ceiling is a promise instead of a tendency (the release alone can rise past what the sample needs, and did, at 0.5018 against an allowed 0.5012), while the gain comes down along a line over the lookahead, so a full-scale sine through a -6 dB ceiling keeps its THD under 0.3% where taking each sample's need at once flattened every crest (1.2%, 10% of samples on the ceiling); the window's minimum is a monotonic queue and the buffers are memory the object owns, allocated at init; aLimitersCeilingHoldsAndItsDelayIsItsLookahead (bursts 24 dB over, a spike, a step; under the ceiling the sound passes untouched but for the delay) went red under a one-sample delay too long; smoothing in decibels is what makes the attack and release times mean the same thing at every input level; all three use the ClosureAudioUnit the motions use, one DynamicsEffect per unit kept across setting changes (setDynamics, serves(_:at:)) so a turn continues from the standing gain, and the export path builds the same units. Deliberate cut: no key from another sound (each Synth owns its engine, so a detector on one has nothing to listen to on another), said in the docs rather than left to be discovered. DynamicsTests (13 tests: a tone under the threshold untouched to the bit; a tone landing at −12/−15/−16.5 for ratios 2/4/8; the attack and release reaching their fractions in the times asked; the knee working a decibel in at the threshold where a corner has nothing to do; makeup lifting by exactly what it says; both sides dropping alike whichever is loud; the ceiling unbroken at three heights over a signal twice full scale; the limiter letting go over its release; the gate open above and silent below; the hold keeping a decaying tail; a threshold lowered in flight walking to the new depth rather than jumping; the values round-tripping and clamping; the export carrying a gate) + DynamicsAudition; the ceiling and the linked detector both verified red by sabotage; Examples/Audio/Levels; Docs/Helpers/Synthesis.md § The three that hold a level; Guide Ch 39 § Something that holds a level: compressor, limiter, gate + the Levels figure.
  • The two that work in the spectrum, and the stretch (Sources/OllinAudio/Synthesis/PhaseVocoder.swift + Spectral.swift; the .pitchShift(PitchShift) / .freeze(Freeze) cases in Effect.swift, each its own Kind; stretched(by:) on SampledInstrument.Recording and ImpulseResponse). A phase vocoder (Flanagan and Golden 1966, Dolson 1986, and the peak-region form of Laroche and Dolson 1999, credited in ATTRIBUTION.md) over the vDSP real FFT: frames of 2048 (4096 above 50 kHz) at a hop of an eighth, the periodic Hann both ways, each bin's true frequency read off its phase advance, the frames written back by overlap-add and divided by the summed window energy. PitchShift(semitones:mix:) moves the pitch and keeps the length; Freeze(amount:) catches the spectrum of the first frame after amount rises above 0 and keeps synthesizing it (magnitudes and true frequencies held, phases advancing) until the amount returns to 0; stretched(by:) (0.25...16) is the offline time-scale change on a recording or a room (a stretched room is a bigger room, still cut at maxSeconds). Load-bearing: peaks own regions (a bin louder than its two neighbors on each side and above a ten-thousandth of the loudest; regions split at the valley between peaks) and a region's bins take the peak's synthesis phase plus their own analysis phase relation (identity phase locking), which is what keeps a partial's main lobe whole; a pitch shift translates each region by a whole number of bins and lets the peak's own advance at ratio times its frequency absorb the fraction, so there is no resampler, the hop is fixed on both sides, and the latency is exactly one frame (SpectralEffect.latency, 2048 samples), at a worked-out cost under a decibel of level and no ripple (eight windows overlap every sample, and the frames the residual puts out of phase carry almost no weight); the accumulator is reset one advance short (resetAccumulated(to:frequency:hop:), the phase less the frequency times the hop) because propagate adds the hop before it writes, and resetting to the phase itself shifted every bin by its own frequency times the hop, which read as the output arriving a hop early (worst error 0.66 on the identity invariant); the live path frames from the first hop over a zero-filled ring and discards negative positions, so the first sample out sits under a full stack of windows (framing from the first full frame left the first 29 samples zeroed and the first 256 under one window edge); a shift of nothing is therefore the input itself to 2e-3 from the first sample, and a stretch of one the recording to 1e-4; the dry half of a blend is delayed by the same frame, so a harmonizer's voices land together and a freeze at 0 is bit-exact passthrough; a capture resets the accumulator to the held phases so the first held frame equals the live one, and a release resets the live phases so the passthrough is exact again; the two channels run their own engines; everything is allocated in init (raw pointers, vvsincosf over the bins) and the per-sample loop touches no heap; both kinds use the ClosureAudioUnit (setSpectral, serves(_:at:) by kind and rate), so a turn of the semitones slides rather than restarts, and the export builds the same units. Deliberate cuts: no formant preservation (a voice an octave up is a small voice; said in the docs) and no transient detection (a struck attack softens a little). SpectralTests (15 tests: the values round-tripping and clamping; a shift of nothing the sound itself, sample for sample, over blocks of 512, 100, 3, and 1024; 440 landing at the ratio for 12, -12, 7, 3.5, and -5 semitones within 0.5%; the level within 1.5 dB; a harmonizer keeping both voices and nothing else; the two sides on their own; an octave turned in flight reaching its pitch with no dropout; a freeze holding the tone a second after the input went silent, at its level, and passing the sound bit-exact before; a freeze at 0 bit-exact; a let-go freeze following the sound again; a stretch of one the recording itself; stretches of 2, 0.5, and 3 at the exact length, the pitch within 2 Hz, and the level within 1 dB; a stretched room longer at the same pitch and cut at the limit; both kinds wiring; the export carrying an octave) + SpectralAudition (OLLIN_AUDITION); Examples/Audio/Spectral; Docs/Helpers/Synthesis.md § The two that work in the spectrum + § Stretching a recording; Guide Ch 39 § Something that takes the sound apart: pitch shift, freeze, stretch + the Spectral figure.
  • The patchable graph (Sources/OllinAudio/Synthesis/Patch.swift + PatchVoice.swift). Routing as a value: Patch is an operator graph (.tone(_:ratio:level:) / .modulated(by:amount:) / .mixed(with:) / .fedBack(_:) / .level(_:) / .ratio(_:), presets .bell / .brass / .glass / .buzz / .struck), reached as VoiceSource.patch + Voice(patch:) + Voice.fmBell / .fmBrass / .fmBuzz. The bare form is untouched — the presets sit on this the way the bare draw calls sit on Drawer. Load-bearing: the shipped constraint (a Voice is carried in SynthEvent through the lock-free ring, which is why ModalBody caps at 16) is met the same way — a patch is fixed lanes, capped at 8 operators, SIMD8 throughout with an output bitmask, so it stays POD (_isPOD(Patch/VoiceSource/Voice/SynthEvent) pinned); past the cap it returns the patch unchanged and notes, never silently drops an operator (a patch with a piece missing is a different instrument); modulated(by:) puts the modulator's lanes FIRST so every modulator sits in an earlier lane than what it pushes and one forward pass evaluates the graph (pinned: every modulatedBy index < its own lane, at any build depth); an operator's level is the mix level where it is heard and the modulation index where it is not, which is the FM convention and why no extra lane is needed; the sum is divided by the number of output operators so mixing does not double the level. PatchVoice keeps phases in SIMD8 lanes, never an array of Oscillator — an array is a reference the voice struct carries and a voice is copied at note start, which would refcount on the render thread. waveformSample was extracted from Oscillator.next so both read the shapes through one function (no drift), and Oscillator.next gained a phaseOffset defaulting to 0 that moves where the shape is read without moving the phase — byte-identity pinned by aSingleOperatorPatchIsExactlyAnOscillator across four waveforms, plus every non-patch preset still rendering. Deliberate v1 cut: a patch is oscillators only (no per-operator filter/envelope/effect; those stay the Voice around it). Waveform conforms to ParamOption alongside. PatchTests (16). Example Audio/Patching; Guide Ch 39 § Building an instrument instead of choosing one: patches and FM; Docs/Helpers/Synthesis.md#patch.
  • Wavetables (Sources/OllinAudio/Synthesis/Wavetable.swift + WavetableVoice.swift). A row of single cycles read by position: Wavetable (a final class holding every frame at eleven band-limited levels, built once from a harmonic spectrum, from drawn cycles resampled and analyzed, or from a (phase, frame) rule; .basic / .pulse / .vowels built in; frame(_:), frame(_:harmonicsUpTo:), cycle(at:) for drawing) set on Synth.wavetable, and WavetableScan (position, sweep, envelope; .at(_:)) carried in the voice as VoiceSource.wavetable / Voice(wavetable:) / Voice.morph. Load-bearing: the table lives on the synth for the sampled instrument's reason (a Voice is carried in the POD SynthEvent; _isPOD pinned for the scan, the source, the voice), and the offline export renderer now copies both instrument and wavetable from the synth (the sampled one never was copied, so a sampled note in an export rendered silent); a synth handed a wavetable voice with no table reads .basic (set in init and the voice setter, on the main thread, never on the render thread); the level a note reads is chosen once at note start from its increment (level(forIncrement:), the strongest whose top harmonic stays under half the rate), so a 5 kHz sawtooth keeps four harmonics and folds nothing (pinned against the full-band counterfactual read at the same rate, which folds harmonics 7 and 6 back at 1/7 and 1/6 of the fundamental, and red by sabotage of the level choice); every level of a frame is summed from the same spectrum, each harmonic added exactly once from the weakest level down, and the whole frame scaled by the full level's peak, so the levels agree and moving between them is silent; the spectrum stops one bin short of half the length (the Nyquist bin is not a harmonic; the first build walked one past the array); the read is bilinear over phase and frame; the scan's position is read per note and only its sweep envelope moves it while the note sounds (a live position, pressure's cousin, is the deliberate v1 cut). WavetableTests (12) + WavetableAudition; OllinAudioTests 296 green. Example Audio/Wavetable; Guide Ch 39 § A wave you can draw: wavetables with the WavetableFrames figure; Docs/Helpers/Synthesis.md#wavetables.
  • Driven physical models: the bowed string and the blown tube (Sources/OllinAudio/Synthesis/DrivenModels.swift + DrivenVoice.swift). The other half of the model tier: a pluck and a strike are set going once, a bow and a breath are kept going, so a note has a middle. VoiceSource gains .bowed(BowedString) / .blown(BlownTube) (+ isDriven), Voice gains bowed: / blown: inits + presets (.violin / .cello / .bowed / .clarinet / .reed / .hollow), Envelope.sustained is the stay-out-of-the-way gate, and Synth.drive (0…1) is the continuous control: an atomic Double bit pattern like gain, read once per block, shared by every note (one bow, one breath). Both models are digital waveguides over the shipped delay-line machinery; credited in ATTRIBUTION.md (Smith's PASP, McIntyre/Schumacher/Woodhouse 1983, Smith 1986), never in .swift. Load-bearing: the bow is the published scattering junction (vΔ = vb − (fromNut + fromBridge), toBridge = fromNut + ρ(vΔ)·vΔ, toNut = fromBridge + ρ(vΔ)·vΔ) over two lines whose round trips sum to one period, split at the bow position; the friction curve is flat while stuck then falls as a cube, and bow speed against the slip threshold is the whole regime — below ~1 the string is caught and released once a cycle and the spectrum is the 1/n Helmholtz sawtooth (measured cello [1.00 0.51 0.34 0.25 0.19 0.10]), above it the string tears loose twice a cycle and jumps to the octave, which is real over-bowing, not a bug (force widens the threshold, so a light bow can be over-driven and a heavy one stays solid); stickingGrip is 0.9, deliberately short of a perfect hold (at exactly 1 the stuck bow is a lossless reflector, so the note settles against the threshold and bowing faster changes nothing — even at 0.9 the drive range only spans ~8 dB, so force is the loudness parameter and drive the expression one, pinned at >1.5× rather than at the speed ratio); the tube's round trip is half a period, not a whole one (stopped at the reed, open at the bell), which is the single line that produces odd-harmonics-only (measured [1.00 0.01 0.28 0.01 0.12 0.00]) and puts it an octave and a fifth below an open pipe; the reed reflection is derived from its two physical limits (shut = rigid termination, open = mouthpiece held at breath pressure) which expands exactly to the published form, so nothing was copied; both loops carry a tuning allpass and only one side may round (the bow's nut side rounds and the bridge side takes the remainder, the tube's bore takes the fraction) or the loop is up to half a sample out, which is nothing low down and most of a semitone at the top — pinned <1 cent across MIDI 33…93 at four bow positions and three dampings; pressure is ignored by every non-driven source (byte-identical at drive 0 vs 1, pinned), and both structs stay plain Doubles so they are still carried in SynthEvent through the lock-free ring. Deliberate cut: no jet-driven tube. A flute jet was built and declined on measurement — it sat at a DC operating point where the cubic's slope is negative, so the loop was far too lossy to oscillate and the output was identical (rms 0.0363) across every jet delay; two attempts, then stopped per the anti-thrash rule rather than keep patching. A lifted bow lets the string ring: the junction's grip is scaled by a contact of pressure / BowVoice.liftedBelow (0.1, below where any preset speaks, so a sounding note is untouched), so at 0 both waves pass straight through, and the loop's whole loss is taken once a period at the bridge (the nut reflects without loss; taking it on both sides rang a lifted string half its decay), with loopLoss's compensation; a lifted violin or cello falls under 6 dB in its first 60 ms, rings at its note, and rings for its decay within 10% at MIDI 48, 69, and 81 (aLiftedBowLetsTheStringRingDown), where the parked bow muted it 25 dB. A glided lift (a note's own 8 ms pressure glide) slows the bow on the string first and loses 7 to 12 dB in that time, filed as measured. Every tube speaks from TubeVoice.speaksFrom (0.3): the breath is reckoned in closing pressures, each note's onset worked out in closed form from the reed's small-signal gain (the reed returns 2x - 1 of a change, x = (m - 2r)/c; the note grows once that times the loop's gain G at the fundamental passes 1; the bore's DC operating point under the loop's DC gain g gives the breath, x² + 2(k - 1)x = 2km with k = (1 + g)/2g), which lands within 0.005 of a breath sweep at five pitches on each preset; the dial rises to that onset over the first 0.3 and on to the old full breath, 1.1 / closingPressure (so a full-pressure tone is what it was), and every preset is silent at 0.25 and speaking at 0.5 from MIDI 45 to 69 (aBlownTubeSpeaksFromTheSamePressureAtEveryPitch). What a tube radiates is DC-blocked (one pole at 20 Hz on the returned wave, outside the loop, so the oscillation is untouched): no standing offset over 0.005 at pressure 0.2 to 1.0 (aBlownTubeHoldsNoOffset), where it held -0.05 to -0.17. Left open, pinned: a tube's register edge closes on its onset near the top (MIDI 87 to 93: within a tenth of breath), so the clarinet and reedy tubes at MIDI 93 blown at 0.8 sound a weak fundamental under the twelfth (filed in DESIGN-NOTES.md). DrivenModelTests (16, twin pairs) + DrivenModelAudition (9 takes, drive shaped through each). Example Audio/Bowing; Guide Ch 39 § A note you keep playing: bowed and blown; Docs/Helpers/Synthesis.md#physical-models.
  • Spatial audio and sound in an export (Sources/OllinAudio/Synthesis/Spatial.swift + SynthExport.swift + the core seam Sources/Ollin/Core/ExportAudioSource.swift). synth.place(at:heardFrom:) puts an instrument in the 3D scene with a Camera3D as the listener (position / hearingDistance / unplace()), and --export-video / --export-loop carry the sketch's own music into the file with nothing to switch on. Load-bearing (spatial): placing is a different graph, not a setting, so the first place rewires (a source node's channel count is fixed at construction, so the stereo node is replaced by a mono one); the environment node must come first, before the effects (a mono chain through AVAudioUnitDelay/Reverb throws -10868 at connect time, probe-verified, the stage-1 format lesson again), and the 3D position is set on the source node (AVAudioSourceNode conforms to AVAudioMixing; the effect units do not). Load-bearing (export): the core cannot depend on a satellite, so ExportAudioSource is a package seam in Ollin that Sketch finds through heldObjects(of:) (Sources/Ollin/Core/HeldObjects.swift) the way it finds FrameAdvancing values and TransportMutables: a stored property, and inside a struct, an enum, a tuple, an optional, an array, a set, or a dictionary, or an object of a class the sketch's own module declares, so the Spatial example's three chimes inside structs in an array export and are advanced. Load-bearing (the finder): the direct properties come first in declaration order (a sketch with two instruments mixes exactly as before); each object is found once and never looked into twice; the framework's own half of the sketch (Sketch's stored properties) is read one level deep as it always was and a framework object is never looked into; a collection is read through its first element, and skipped whole when that element holds no object anywhere inside it (a million Vector2s cost under 2 s, a [Post] whose first post holds a synth is read entire; a list plain first and an instrument later is the one shape it misses, and the reference says so); the pending values sit on a list, never the call stack, and the walk stops after 200,000 values (HeldObjectsTests: an Eased inside a struct, an array, a dictionary, and the sketch's own object moves, one held twice moves once, and anInstrumentInsideAnArrayReachesTheExport finds eight of eight). Under isRenderingHeadless a Synth's notes are written down rather than played (emit routes to record, start() no-ops) against a clock the sketch sets to the frame's own time through FrameAdvancing.advance(to:) right after each advance(by:) step (stepped alone the clock stood one frame past the picture while the frame was drawn, since the sketch advances before it draws, and every note landed a frame late: 736 samples at 60 fps; aNoteAskedForOnAFrameStartsOnThatFrame at frames 0 and 10, within 32 samples), hearingDistance changes are written down against that clock like a move (RecordedRange; the first change also writes the range it replaced at time 0, so the machine opens with the range the export began with) and applied at their block (aHearingDistanceChangedMidExportIsHeard: the later note at the new range, the earlier note untouched), unplace() switches the source to the mixer's pass-through (.stereoPassThrough, measured at exactly the never-placed level in both ears from any position and switchable while the engine renders; place sets .auto back; sat at the listener's own position instead the pan played it at half level in each ear, 6 dB down for the rest of the piece; anUnplacedSoundIsHeardAtFullLevel, and placed again to the left it pans left), and a note's length is stored in seconds not samples so the file renders at its own rate; the soundtrack replays through the same clock-free SynthRenderer inside an AVAudioEngine in offline manual-rendering mode (which is what lets the delay and reverb render offline too), chunked so a note starts on the exact sample it was asked for. The interleaving trap, which cost a session: AVAssetWriter paces file-mode inputs in ~1 s chunks and holds every other track until the lagging one delivers its chunk — and this soundtrack cannot lead by a second, because the notes for those frames have not been asked for yet, so the writer held video forever at ~frame 67 and the export looked like a hang (it also hung with zero notes played, since declaring the track is enough). The fix is three parts, all load-bearing: the audio input is expectsMediaDataInRealTime = true (exempt from chunk gating), audio is pumped before each video frame so it never lags, and the video wait is bounded and fails loudly rather than spinning. Verified: 1200-frame --export-loop completes in 19.8 s and two runs decode bit-identically (max sample difference 0.0). Envelope: GIF carries none, and an instrument must be a sketch property to be found. SoundExportTests (8). Examples Audio/Spatial + Audio/SoundInAnExport; Guide Ch 40 §§ Sound from a place in the scene: spatial audio / Putting it together: the music box; Docs/Helpers/Synthesis.md#placing-a-sound + #sound-in-an-export + Docs/Output/Export.md#sound. Placing carries into the export (RecordedPose/ListenerPose in Spatial.swift, the environment node in OfflineRender): place(at:heardFrom:) under isRenderingHeadless writes the pose down against the export clock instead of rewiring, and the soundtrack renders through an AVAudioEnvironmentNode in manual rendering mode (probe-verified: it spatializes offline, distance attenuates on the inverse law, and the source→env→delay→reverb chain connects without throwing). Load-bearing: the environment node is built only for an instrument that recorded a pose, so an unplaced export keeps its own chain, and because a chain's shape is fixed when it is built, a pose arriving after that notes loudly rather than silently centering (the one degrade that stays); poses collapse to changes only (a sketch calls place every frame, so a still instrument costs one pose, not one per frame, pinned at 1 vs 600); a note breaks the render block but a pose is applied at the next block boundary (an onset is a transient and must land on its own sample, a position is not, and a block is a fifth of a frame); .auto is the algorithm both paths ask for, and with no output device it resolves to a plain left/right rather than the head model (HRTF works offline but needs ~0.5 s to settle, measured, so it would wreck a short export); the hearing range lives on the Synth (placementRange), not the placement, so an export can read it without a live environment node existing. Fixed alongside, a real shipped defect: the offline chain built its source at mono and fed a stereo mixer, so every exported soundtrack was entirely in the left channel with the right at exact digital silence (measured L rms 0.0657 / R rms 0.0 on the shipped code); an unplaced instrument is now built at the output's channel count the way the live path builds it, and the render block writes its one stream into both channels. SpatialExportTests (12, counterfactual twins: left-vs-right, near-vs-far, listener-moved, unplace-recenters, a walk crossing the ears measured as balance rather than level since a voice's own envelope otherwise reads as the crossing) + SpatialAudition.
  • OSC (OllinOSC satellite). OSC 1.0 wire format written from spec (OSCArgument / OSCMessage / OSCBundle / OSCTimeTag / OSCPacket, total init?(data:) decode — malformed datagrams return nil, never trap) over UDP (OSCSender / OSCReceiver). Three read surfaces: latest-value cache (float / int / string / bool), event drain (messages()), and @Param binding (bind("/addr", to: $param, from:)). Example Integration/OSCLoopback; Docs/Integration/OSC.md.
  • OSCQuery (OllinOSC satellite; OSCQuery.swift). The sketch's @Params published as the namespace a control app browses: extend(OSCQueryServer(port:name:)) serves the tree as JSON over HTTP on TCP and takes values over OSC on UDP at the same port number (9000 by default, 0 system-picked), both advertised through NWListener.Service as _oscjson._tcp and _osc._udp under the sketch's type name (advertises off for tests and privacy; OSCReceiver.serviceName is the receiver's half). A parameter is /name or /Group/name (the group made address-safe by containerName, the real name kept in the container's DESCRIPTION); every leaf carries FULL_PATH, TYPE, ACCESS 3, VALUE, RANGE, DESCRIPTION, the type per kind in the Docs table, and a swatch strip is a container of r children, one per color, so a client that knows nothing about palettes gets a color well per stop. ?HOST_INFO names the server and the OSC port; a query naming one attribute (?VALUE, ?RANGE, and the rest) answers it alone (204 when the node lacks it, 400 when the server does not speak it, 404 for no node); no LISTEN and no WebSocket, so clients poll. Three layers like the remote surface, and the split is load-bearing: the model (OSCQueryNode / OSCQueryValue / OSCQueryRange / OSCQueryHostInfo, public Codable under the protocol's own keys, sorted, slashes unescaped), the wire (OSCQueryWire: the namespace from ParamHandles, a payload from OSC arguments per Leaf, apply through the control's own write closure so smoothing and clamping match an inspector drag, one response per request target; all pure), and the engine (nonisolated OSCQueryEngine owning both sockets; the @MainActor extension discovers, rebuilds the tree every 250 ms, and applies the drained queue in beforeDraw). An incoming message is read on the network queue against a Route (name + leaf) and never touches the parameter there. Reads are generous (any numeric tag for a number, label or index for a menu, r or a hex string for a color); a short or wrong-kind message is dropped. OSCArgument.color (tag r, four bytes R G B A) joined the codec for this. The HTTP head parser is the core's package-access HTTPRequestHead, which RemoteHTTP.parseHead forwards to (satellites cannot depend on each other). Tests are socket-free but for one UDP loopback datagram (the house no-loopback-TCP rule): OSCQueryTests (15). Example Integration/OSCQuery (draws the tree it serves); Docs/Integration/OSCQuery.md; Guide Ch 38 § The sketch that says what it takes: OSCQuery. Failure shape (2026-09-22): isRunning (the listener is up), unavailableReason (a port another program holds, a bind the system refused, the OSC port that would not open; follows the listener's own state and clears on .ready), and start() (serves again after stop(), or tries the port again after a failed start, which drops the failed listener so it can); the failure is also said once on stderr for the terminal, never print. OSCQueryTests.aPortAlreadyTakenIsSaidAndTriedAgain holds a plain listening socket on a port the system picked and asks the server for it.
  • TUIO (Sources/OllinOSC/TUIO.swift, inside the OSC satellite — a satellite may not depend on another, and TUIO is sent over OSC). A tangible surface (marker table, touch wall, phone app) read as three polled lists: cursors (TUIOCursor), objects (TUIOObject, carrying the marker's symbol and angle), blobs (TUIOBlob, carrying size/area/bounds(in:)), each item holding the tracker's session id, a point in the unit square, velocity in surface widths per second, and position(in: Rectangle). TUIO 1.1's three 2D profiles only (2Dcur/2Dobj/2Dblb); the 2.5D/3D profiles, TUIO 2.0 addresses, and non-TUIO messages are ignored rather than half read, and two trackers on one port are not separated (sourceName reports the last one that named itself). Written from the spec. Load-bearing: the per-profile state machine commits on fseq — sets and the alive list are held aside, so a poll never reads half a frame (one finger moved, another not); a frame numbered below the last is dropped as a datagram that overtook another unless it is more than 1000 below, which is a tracker that restarted its count; frames arrive in the specification's order, alive list first, then the sets, then fseq, so a set never closes a frame (closing there commits the alive list before fseq can turn a late frame away, and counts every frame twice), while a sets-first frame is still read; a tracker that omits fseq commits when the next frame's alive list arrives (one frame late, rather than never); the alive list is the only removal (the protocol has no ended message) and a set for an id absent from it is not invented. Coordinates are 0...1 from the top-left, the canvas's own direction, so position(in:) flips nothing. TUIOReceiver owns an OSCReceiver and uses its new internal observe(_:) seam (kept internal so the OSC public listing is unchanged, and non-destructive, so a sketch's own messages() still sees everything); a shared socket is served instead by feeding receive(_:), which also takes a whole OSCPacket. Example Integration/TUIOSurface (a stand-in tracker sends real frames to 127.0.0.1, simulate off for a real one); TUIOTests (18 tests, the late-frame drop and the alive pruning both sabotage-verified, the wire order's three tests red on the receiver that closed a frame on a set, plus one real UDP round trip); Docs/Integration/TUIO.md; Guide Ch 38 § A table you put things on: TUIO with the SurfaceFrame figure.
  • DMX lighting (OllinDMX satellite). Art-Net 4 + sACN (ANSI E1.31-2018) written from spec over Network.framework UDP, the OSC recipe applied to lighting: total-decode ArtDmxPacket / SACNDataPacket (golden-byte-pinned against both specs' published tables), DMXUniverse (512 channels, 1-based like every console; out-of-range reads 0 / writes ignored, never a trap) + DMXFixture named layouts (.rgb / .rgbw / .drgb / .dimmer presets + role lists, nextAddress chaining; RGBW takes the min-subtract white split, and a dimmerless fixture scales its color so dimmer: means brightness either way), DMXSender (DMXSender() = sACN multicast to the per-universe 239.255.hi.lo group — the zero-config default; sACN: / artNet: unicast forms — Art-Net 4 forbids ArtDmx broadcast) and DMXReceiver (latest-value reads + @Param binding 0…255 → range; no event drain by design — DMX is levels, not events; start(universes:) joins sACN multicast groups via NWConnectionGroup). Load-bearing: the wire cadence lives in the pure DMXPacer (send-on-change capped at 44 Hz = a DMX512 gateway's max refresh, the E1.31 three-repeat tail after a change, then ~0.9 s keep-alives — so a sketch just calls send every frame and the pacer is unit-tested clock-in-hand); close() must wait for its sACN goodbyes (3 stream-terminated packets per universe; canceling the NWConnection immediately drops the still-queued sends — a real test-caught bug, fixed with a completion-counted wait); receiver ingest applies the standard's rules (highest priority wins under the 2.5 s source timeout, the signed-8-bit sequence window ≤0 ∧ >−20 dropped, preview data + alternate START codes ignored, a terminate clears the universe) and handle(_:) is internal so tests feed crafted datagrams with no sockets; ArtDmx sequence wraps 1…255 skipping 0 (0 = disabled) and its port-address packs Net(7):SubNet(4):Universe(4), valid 1…32767. Envelope (stated in the docs, not faked): streaming tier only — no ArtPoll/ArtPollReply, no sACN discovery/sync packets, no RDM; multicast receive is best-effort until hand-verified against a real console (loopback tests are unicast). The spec-required "Art-Net™ Designed by and Copyright Artistic Licence" credit lives in Docs/Integration/DMX.md + ATTRIBUTION.md — keep it. LED mapping (LEDMap in OllinDMX over the core CanvasSampler seam, Sources/Ollin/Renderer/CanvasSampler.swift): strips (polyline/ring) / matrices / loose points laid over the canvas, sampled each frame and sent as universes. Load-bearing: sampling is a small GPU compute pass on the rendered-texture extension hook (wantsRenderedTexture; never the full-frame CPU readback) — CanvasSampler is package-access (satellite-seam tier, same package), compiles its kernel from an inline source string outside the segment concatenation (plain float4/uchar4 buffers, no shared struct to drift), averages each point's box patch in linear light and re-encodes to display (sRGB) bytes (radius 0 round-trips the stored byte exactly; the sRGB-space average is the pinned counterfactual), clamps off-canvas points to the edge texel, and is synchronous (own queue + wait — the Syphon cross-queue hazard rule); universes pack whole LEDs (a straddling LED jumps whole to the next universe's channel 1 — 170 RGB / 128 RGBW per universe, the pixel-controller convention) and a run spans consecutive universes from its start; strip LEDs space by walked length (endpoints included; closed: spaces i/N around the loop, no doubled seam); a matrix defaults to straight rows (pixel controllers are configured with the panel's wiring and own the zigzag — serpentine: true only for direct-wired panels); default sampleRadius = the patch the LED stands for (half spacing / half of the smaller cell side, banded 1…32); colors land through the fixture path (DMXUniverse.set(_:color:dimmer:)), so RGBW white-split and master brightness follow the shipped rules; Fixture.address(ofLED:) is the wire readback a drawn preview uses; live-only (headless exports never fire the texture hook, the frame-sharing precedent). Example Integration/LEDMapping (loopback: strip in wire order + panel lit from received universes); CanvasSamplerTests (Metal-gated) + LEDMapTests; mechanism in ARCHITECTURE.md § The canvas sample pass. Example Integration/DMXLoopback (both ends on loopback, the drawn stage lit from received channels); DMXWireTests / DMXReceiverTests / DMXLoopbackTests. Docs/Integration/DMX.md.
  • Laser projection (OllinLaser satellite). Line work from draw() scanned by a show laser: LaserFrame (paths in canvas coordinates, one color per path or per point; Contour/Shape outlines only — a fill hatches first, the plotter rule), LaserOptimizer → LaserStream (the point list a projector plays, plus what it cost), LaserSafety, LaserProjector (the sketch surface + the arm gate), EtherDreamDAC/EtherDreamFinder (the wire), ILDAFile (format-5 files), and drawLaserPreview (the beam's own path drawn on screen). Load-bearing: the optimizer works in projector space (ProjectorSpace: -1…1, x right, y up, the canvas fitted so nothing lands outside the field, every point clamped), never canvas pixels, because each distance it sets is a distance the galvos move; segments are subdivided, never resampled end to end (an arc-length resample drops the authored vertices, which is exactly what rounds a corner off — pinned by everyVertexTheSketchGaveLandsExactlyWhereItPutIt); dwell is held in two places for two reasons (a turn past cornerAngle, and both ends of every blanked jump — light while the mirrors still move and the beam drags a tail between shapes); the frame's last travel leg returns to the first path's entry, so replaying one frame has no seam (which is what the DAC does between sends); path order reuses the plotter's greedy tour through the new package orderedPathSteps seam (GCodeExport.swift) — the decision is shared, the application is not, since a laser path carries a color per point that must reverse and rotate with it; and nothing is dropped over budget (pointsPerSecond / refreshRate): the frame plays whole and repeats slower, with isOverBudget/refreshRate saying so. Safety is API, not documentation: a projector emits only after an explicit arm() (disarmed and stalled both play a dark hold, connection kept), maxBrightness halves the drive by default, the stopped-beam guard blanks a lit run that stays inside stationaryRadius (its limit must sit above cornerDwell, which repeats a position on purpose — both directions pinned), and stallTimeout blanks a frame the sketch stopped feeding. The DAC pump self-clocks on replies, which is a trap: the buffer figure arrives only on a reply, so a host with no room must ping rather than re-read the number it already holds — a stale re-read deadlocked the stream permanently, caught by the loopback and fixed by askAgain(after:). Two more non-obvious facts: the rate-change command byte is 0x74 (the published table names it after a letter that is not that byte; an independent implementation agrees on the byte), and an NWListener asked for any port reports 0 until ready, which is not somewhere to connect (the test stand-in's own bug). Both formats are golden-byte pinned (ILDA big-endian, 32-byte header, 8-byte records ending B,G,R, blanking bit 6 / last-point bit 7, a null header ending the file; the DAC wire little-endian, 18-byte points, 20-byte status, 22-byte reply, all packed) and a file is read back by a second parser written from the same spec. Envelope (stated in the docs, not faked): no hardware has ever been on the other end — the wire is verified end to end against a stand-in DAC speaking the protocol on loopback, and broadcast discovery is best-effort until it hears a real DAC (only the datagram decode and handle(_:) are tested, the DMX multicast-receive precedent); one DAC family, no IDN; true-color files only (no indexed palettes); no geometric correction; no point-rate feedback. LaserOptimizer(pointsPerSecond:refreshRate:) takes the two numbers a projector is set up by, and the rating stays a number (30K is the vocabulary, the lens-preset ruling), with the ILDA rating table on the Docs page rather than presets in code. Example Integration/LaserPreview (the optimizer made visible: parameters, the beam's footsteps, the budget readout); LaserOptimizerTests / LaserSafetyTests / LaserWireTests / EtherDreamLoopbackTests (63); no pixel snapshot (the output is a point list). Guide Ch 45 § Drawing with light: a show laser; Docs/Integration/Laser.md.
  • MIDI (OllinMIDI satellite). MIDI 1.0 written from spec (MIDIMessage incl. .songPosition, total init?(status:data1:data2:) / init?(umpWord:)) over the modern Core MIDI UMP API (MIDIInput / MIDIOutput, virtual source via openVirtual). Read surfaces: control cache, held-note state, messages() drain, @Param binding. The pitch wheel reads and binds whole-channel beside MPE's per-note bend: pitchBend(channel:) is the wheel as a fraction of its travel (-1/0/1; 0 until a bend arrives; nil channel = the channel that bent last; controller 121 rests it), and bindPitchBend(to:channel:travel:) (Param<Double> and Param<Tempo>, unbindPitchBend) maps travel (default -1...1) into the range, so rest is exactly the midpoint. Load-bearing: the fraction divides each side by its own count (8,192 below rest, 8,191 above), so both ends reach 1 exactly, and travel: is on that scale, never raw 14-bit (the label was ruled 2026-10-01 so a reader never writes 0...16383 there); HeldNote.pitchBend keeps the specification's /8192 for semitones. MIDIExpressionTests (the wheel, each channel) + loopback pitchBendDrivesAParamAboutItsMidpoint / pitchBendBindsByChannelAndRange. @Param smoothing (.eased / .smoothed) lives on the param, so a softened parameter applies to MIDI, OSC, and inspector drag alike (runs in the per-frame advance pass with @Eased / @Smoothed). Tempo sync — TempoClock(from: input) follows MIDI clock into musical time (tempo / beats / beatCount / phase / beat decaying pulse / bar / barPhase over declared beatsPerBar / progress(over:), the loopProgress sibling). Load-bearing: position counts ticks (each F8 is exactly 1/24 beat — the grid can't drift); the smoothed interval window (~2 beats, flushed on a >±40% jump or a >0.25 s break) only glides phase between ticks, clamped so position never runs backward; start/continue arm and begin on the next tick (the MIDI convention; song position = sixteenths × 6 ticks, and an SPP jump clears the pulse anchor so the next tick lands on it exactly); a transport-less master (DJ gear) free-runs from its first tick; the math is the pure TempoEngine fed host-time-stamped events through MIDIInput's internal listener seam (packet timeStamp → seconds; MIDIOutput stamps sends with real host time so in-process loopback timing is exact), so TempoClockTests pin it deterministically. Examples Integration/MIDILoopback, MIDIMonitor, Tempo (internal-timer loopback: an in-sketch master sends clock through a virtual source; turn it off and real gear drives the same sketch); Docs/Integration/MIDI.md. Timecode — TimecodeClock(from: input) follows MIDI Time Code into a position (timecode / seconds gliding between frames / frameRate / isPlaying / isReceiving / isReversed) over the Timecode value (hours, minutes, seconds, frames at .fps24 / .fps25 / .fps30Drop / .fps30; frameNumber and totalSeconds do the drop-frame arithmetic, 1,798 frames a minute and 17,982 per ten; quarterFrameValue(piece:) / init(quarterFrameValues:) and fullFrameSysEx / init(fullFrameSysEx:) are the two wire forms). Timecode is Comparable, so a cue test is < and a cue window is a range: the order runs on totalSeconds rather than the frame number, so two rates answer honestly about which moment came first (frame 30 at 30 fps is earlier than frame 30 at 24 fps), and two naming the same moment at different rates are broken apart by rate so < and == never disagree, which is the total order the protocol requires. Four tests in TimecodeTests. Load-bearing: a completed set of eight quarter frames anchors the position at the time it spells plus 1.75 frames (the set took two frames to arrive; backward, minus), every further quarter frame moves the anchor a quarter frame in the direction the pieces run, and the glide between messages is clamped under a quarter frame so a late one never runs the position back; a full-frame exclusive locates and holds; half a second without a quarter frame is stopped. The input walks Universal MIDI Packets by their word count (a 7-bit exclusive is two words, and reading it a word at a time took its data for a message) and assembles exclusives across packets for a module-internal addSysExListener; MIDIOutput.send(sysEx:) packs them six bytes a packet and send(timecode:) sends a full frame. TimecodeTests (13, pure: the drop-frame round trip over two hours at every rate, both wire forms, and the engine's landing, glide, stop, locate, reverse, and restart) + the loopback case timecodeCrossesTheLink. Example Integration/Timecode (an internal deck through a virtual source; real gear drives the same sketch); Guide Ch 43 § Following another timeline: timecode. Per-note expression (MPE) — heldNotes reads every held note with its bend, pressure, and slide resolved (HeldNote: note / channel / velocity / pitchBend in semitones / pitch / pressure / slide; id is the channel and the note together) over mpeZones (MPEZone.lower(members:) / .upper(members:), a master channel beside its member channels, the members' bend range 48 semitones and the master's 2 by the specification): a member channel's bend, pressure, and controller 74 are one note's, the master channel's are applied on top (a bend added, a pressure added and clamped, controller 74 as an offset from 64), and a channel outside any zone reads plainly (2 semitones, a key's own pressure per key). Load-bearing: the zone is laid out from the controller's configuration message (registered parameter 6 on channel 1 or 16, assembled per channel from controllers 101/100/6/38; a non-registered selection cancels it; the other zone gives up the channels the new one takes; 0 members takes a zone away), the bend range from registered parameter 0 (semitones plus cents; a member sets the zone's members, the master its own), controllers 120/123 clear a channel's notes and 121 its expression; the math is the pure ExpressionEngine under the input's lock (the TempoEngine shape), slide is nil until told so a note at rest reads exactly 0.5 rather than 64/127, and controlValue(74) with no channel is still whichever member moved last. MIDIOutput sends pitchBend / channelPressure / polyPressure / registeredParameter(_:value:fine:channel:) / send(mpeZone:) (the configuration message plus both bend ranges). MIDIExpressionTests (8, pure) + the loopback case expressionReachesOneNote. Example Audio/Expression; Docs/Integration/MIDI.md § Per-note expression; Guide Ch 39 § A note under the finger: expression.
  • Link tempo session (OllinLink satellite). The network tier of tempo sync: LinkClock joins the local network's shared tempo-and-phase session (the Link protocol) — peer discovery over UDP multicast 224.76.78.75:20808 (_asdp_ ALIVE/RESPONSE/BYEBYE + TLV payload), ping/pong clock-offset measurement (_link_, chained until >100 points, median offset, ghost = host + offset, re-measured every 30 s unless founder), and session consensus — implemented independently from published protocol documentation, nothing vendored (ATTRIBUTION.md § Satellite libraries; the wire format is pinned against a real captured announcement in LinkWireTests). Read surface mirrors TempoClock on purpose (tempo / beats / beatCount / phase / beat pulse / timeSinceBeat / bar / barPhase / progress(over:)), plus writable tempo and isPlaying, peerCount, start()/stop(); beatsPerBar doubles as the session quantum. Load-bearing: the state machine is the pure LinkEngine (host-microseconds in, send-actions out — the TempoEngine discipline), so priority, arbitration, and the measurement chain are pinned with synthetic clocks; a same-session timeline is adopted only on a strictly larger anchor beat (a local edit outranks by exactly +1 µbeat and arms a 1 s guard against in-flight traffic); arbitration joins the older session — larger ghost time, ties within ±0.5 s to the smaller session id; losing the last peer is a continuity reset (fresh node+session identity, ghost re-anchored at 0, tempo/beat/playing kept, transport stamp 0 so any real change outranks it); the beat grid never stops (transport is advisory, last-writer-wins on a strictly newer ghost stamp) and beats is per-participant — the mod-quantum residue (phase encoding) is what aligns, so barPhase is the cross-peer read; gateways are per-interface BSD sockets (SO_REUSEADDR+SO_REUSEPORT on 0.0.0.0:20808, per-interface membership, IP_MULTICAST_IF + loop on, loopback interface included so one machine syncs with itself; rescan every 5 s). LinkLoopbackTests is .serialized — every clock shares the one multicast group, and the fixed port means two overlapping test runs on one machine can disturb each other's peer counts (same class as the Core MIDI loopback names). Example Integration/Tempo (its Link mode; run two copies and they pulse together); LinkWireTests + LinkTimeTests + LinkEngineTests + LinkLoopbackTests (30); Docs/Integration/Link.md; Guide Ch 38 § One beat for the whole room: Link. Failure shape (2026-09-22): interfaceCount (gateways open, loopback included) and unavailableReason (a sentence only while running with no interface, naming the last socket stage that failed; every interface is retried about every five seconds, so it clears itself), since a running clock that opened nothing read exactly like one alone on the network. LinkLoopbackTests.aRunningClockSaysWhetherItIsOnAnyNetwork pins the loopback case; a socket that refuses cannot be forced from a test, so the negative is by reading.
  • Serial (OllinSerial satellite; SerialPort.swift + SerialDevice.swift + LineAssembler.swift). The classic physical-computing loop: IOKit discovery (SerialPort.availableDevices(), callout /dev/cu.* paths, USB product names searched up the registry, USB devices sorted first) + POSIX termios I/O, nothing vendored. Three read surfaces, the OSC/MIDI recipe: latest-line cache with numeric coercion (latestLine / float / int / bool, the whole trimmed line as one number — the framing bind understands too), event drain (lines(), arrival order), and @Param binding (bind(to:from:), default input 0…1023, the 10-bit analog read); a fourth, bytes(), drains the raw stream for binary protocols and is independent of lines() (two buffers, both capped — 4096 lines / 64 KB, oldest dropped). Load-bearing: open() never throws and never gives up — reconnection is the design center (absent/busy/unplugged retried every 1 s, constant, no backoff: the device is local and coming back fast from a cable bump or a firmware flash is the point), and SerialPort(matching:) re-resolves its match on every attempt (a board plugged in later, or re-enumerating under a new number after a replug, is still found — the fixed-path init can't be, so the docs steer at matching); a generation counter bumped by open()/close() invalidates stale retries and stale reads, and attempt re-checks it after storing the descriptor (the close-during-open window). The descriptor opens O_RDWR|O_NOCTTY|O_NONBLOCK + TIOCEXCL (two readers each get half the bytes; consequence: close() before a firmware upload, the reconnect loop handles the reopen) with raw-8N1 termios applied best-effort (pty pairs and some drivers refuse parts; a partly configured port still reads — this is what lets a pty stand in for the device in tests). Reads go through a DispatchSourceRead whose cancel handler owns the close() of the fd (no event can fire on a closed descriptor); EOF (read → 0) = device gone → drop + retry. LineAssembler tolerates LF / CRLF / bare CR (the CR flag survives a chunk boundary), decodes UTF-8 per complete line (a multi-byte char split across reads stays whole), caps a never-terminating line at 4096 bytes, and is replaced on every (re)connection so a partial line never leaks across sessions. Writes go out on the port's queue (bounded EAGAIN grace, ~50 ms, then the rest drops) and bytes sent while the device is away are dropped, not queued — a reconnected board wants current values, not a replay. Whole loop testable headless over a pty pair (the replica opens as a real SerialPort; manager side is the fake device — the same trick the SerialLoopback example plays live). Examples Integration/SerialLoopback (pty fake device, click writes back and the wave flips) + Integration/SerialMonitor (live device list, auto-opens matching: "usb", scrolling lines + value bar); SerialUnitTests + SerialLoopbackTests; Docs/Integration/Serial.md. Firmata over the port (FirmataBoard, Firmata.swift): StandardFirmata's protocol spoken over a SerialPort (its own at 57600, or one passed in), so a board needs no firmware of the sketch's own. analog(_:) (channel-numbered, scaled 0...1 over the 10-bit reading), digital(_:pullUp:), write(_:_:) for Bool (digital out) and Double (PWM 0...1) and write(_:angle:) (servo), setMode, sampleEvery, send(text:), messages() draining every decoded message, bind(analog:to:) onto a @Param, firmware once the board answers. Load-bearing: asking a pin is what turns it on (the first read sets the mode and the report, the first write the mode, nothing sent twice), and since an Arduino resets when its port opens the board is queried on every connection and everything asked so far is restated whenever it announces itself (a replug picks up where the sketch is). The parser is a byte state machine that survives any chunk boundary and resynchronizes at the next command byte; a digital port word becomes per-pin changes (all eight the first time). The port grew two internal sinks (byteSink on the port queue per chunk, connectionSink per fresh connection) so a protocol layers on without touching the line framing. FirmataTests (12: the codec against the spec's byte tables, the parser, chunk boundaries, resynchronization, and a pty loopback with a fake board for the query, the asks, the writes, the binding, and the restatement); example Integration/Firmata (a real board owes a desk check). Failure shape (2026-09-22): lastError on the port and the board, the sentence open() is waiting on (no device at <path> / no serial device matches "<needle>", the system's own errno text for a port another program holds, the device went away, a read that failed), cleared the moment the port opens; open() retries as it always did. A going-away sentence stands while the retries only confirm the absence (ENOENT at the path, no device matching the needle), and any other retry failure replaces it: the runner's pty path is gone by the first retry where the M2's lingers, so the errno text overwrote the device went away there and nowhere else (2026-09-23). SerialLoopbackTests.aPathWithNoDeviceSaysSo and the device-going-away test pin it.
  • Bluetooth (OllinBluetooth satellite; BluetoothDevice.swift + BluetoothScan.swift + BluetoothCatalog.swift + BluetoothUUID.swift + BluetoothBackend.swift + CoreBluetoothBackend.swift). The physical-computing loop with no wire, over CoreBluetooth: BluetoothScan is the room (every device in range, strongest signal first, forgetAfter dropping one that has gone quiet, an optional service filter applied at the radio), BluetoothDevice is one device found three ways (named: substring / service: / id: / a scanned BluetoothPeripheral) and read the OSC/MIDI/serial way: latest-value cache (number/int/text/bool/bytes/data/latest, each with a default: form), event drain (readings(), arrival order, capped at 4096 oldest-first), and @Param binding (bind(_:to:from:), clamped into the parameter's range), plus read/poll(_:every:)/subscribe/unsubscribe and write(_:to:) for text and bytes. The permission is the headline, not a footnote: macOS asks once per app before a program may use Bluetooth, and until that is answered the radio reports .waiting, not off and not refused — measured on this Mac, a swift run binary sits in CBManagerState.unknown with CBManager.authorization == .notDetermined for ever while the screen is locked, because the question cannot be shown there (an embedded __info_plist section + ad-hoc signature gives the process a bundle identifier and changes nothing about this). So isAvailable / unavailableReason / radioState are the surface every object carries, the reason is a finished sentence a sketch draws (naming the locked screen, since that state is what reads as a broken sketch), it is repeated once on stderr past a 3 s grace (the haptics precedent), and a headless export reports the radio absent rather than asking for a permission a file cannot use (connect()/start() are @MainActor only to read isRenderingHeadless there and stash it, so the reason stays readable from anywhere). Values carry their own format, because Bluetooth says nothing about what its bytes mean: BluetoothFormat (.raw / .unsigned(bytes:scale:) / .signed(bytes:scale:) + the .uint8….int32 shorthands / .float32 / .text / .heartRate), least-significant byte first, written from the standard's published layouts; .heartRate is its own case because the first byte's lowest bit decides whether the rate is one byte or two, and more fields may follow (reading two where it said one turns a resting 114 into 626 — the sabotage-verified test, and the only one that separates a right reading from a lucky one is a narrow rate with fields after it). A BluetoothCharacteristic is equal by identifier alone, so a value stored under a discovered characteristic is found by a catalog constant and can be re-read another way at the point of reading (.read(as:)); BluetoothUUID expands the standard's 16-bit numbers into the 128-bit base so both spellings compare equal, and keeps unparseable text as itself with isWellFormed false (a mistyped UUID is a device that never answers, never the wrong value). Catalog: .heartRate / .battery / .environmentalSensing / .deviceInformation / .uart services with their values, .uart being the de facto serial line most maker boards speak. Load-bearing: connect() never fails and never gives up (a 1 s constant retry, the serial precedent), connectingTo is tracked beside connectedTo because a connection that fails arrives before there is anything connected to compare it against and a check against the connected device alone waits for ever (its own test); a connected device is asked about everything it offers (notify on what announces itself, one read of what it holds) so the first frame that asks usually has a value; CoreBluetoothBackend retains every CBPeripheral it sees (CoreBluetooth does not, and a released one drops its connection silently) and reads CBUUID.uuidString through BluetoothUUID (it prints short for standard values and long for everything else); the scan asks for repeats and the device hunt does not (without repeats a signal never moves; with them a hunt pays for nothing). The radio sits behind an internal BluetoothBackend seam — the controller satellite's synthetic-pad precedent — which is what makes the whole path testable with nothing switched on: OllinBluetoothTests (46) drive a stand-in radio through the shipped matching, connecting, subscribing, caching, draining, clamped binding, polling, letting go, and coming back. What that does not cover, and only real gear can: CoreBluetooth itself, and this Mac hearing another device. No pixel snapshot (a live radio has nothing deterministic to draw). Examples Integration/BluetoothRoom (the room by signal strength; needs no gear of your own) + Integration/BluetoothSensor (a name typed into a parameter, everything the device offers as it arrives). Docs/Integration/Bluetooth.md; Guide Ch 38 § The same loop, without the wire: Bluetooth + the BytesIntoValues figure.
  • MQTT (OllinMQTT satellite; MQTTClient.swift + MQTTMessage.swift + MQTTWire.swift). The message bus a building speaks, so a sketch reads a house's sensors and drives its lamps: MQTT 3.1.1 written from the published specification over Network.framework TCP, nothing vendored. MQTTClient(host:port:clientID:username:password:will:keepAlive:reconnects:) + connect()/disconnect()/isConnected/lastError, publish(_:_:qos:retains:) for text / a number / a Bool / payload: bytes, subscribe(to:qos:)/unsubscribe(from:)/subscriptions, and the OSC/MIDI/serial read triple: latest cache (message/number/int/text/bool, each with a default: form, plus topics and topics(matching:)), event drain (messages() and messages(matching:), which takes only its filter's own and leaves the rest so two readers do not steal from each other), and @Param binding (bind(_:to:from:), mapped and clamped). MQTTMessage reads the three things devices actually write: a decimal number/int (whitespace-tolerant), a bool over the words a switch publishes (ON/true/1/yes/open/online/active and their opposites, any casing), and a JSON object's field through number(named:)/int(named:)/text(named:)/bool(named:) (a field written as a string of digits still reads as a number). Load-bearing: MQTTQoS has no exactly-once case on purpose (the four-packet handshake buys a sketch nothing over the newest reading), so a broker never opens it; .atMostOnce published while the line is down is gone, not queued (that is what at most once means) while .atLeastOnce is held and resent with the duplicate bit after a reconnection and dropped on its PUBACK, both sabotage-verified; a refusal is an answer, not a dropped line (wasRefused stops the reconnect and keeps the reason, since the broker closes the socket right behind the CONNACK and a later drop would otherwise overwrite it — the race the stub reproduces); a reconnection resubscribes from the stored filters (clean session is always asked for, so the broker remembers nothing) after a backoff from 0.25 s doubling to 8 s, and connectionCount is what a sketch watches rather than catching isConnected between frames; the heartbeat goes out at three quarters of keepAlive and silence past one and a half of it is a dead line; MQTTTopic.matches carries the two rules that are easy to get wrong, a trailing # matching its own parent level and no leading wildcard reaching a $ topic; a publish to a topic holding a wildcard does nothing (a published topic is exact); the remaining-length field is four bytes at most and a fifth continuation byte is refused; and a partial packet reads as incomplete rather than wrong, since a packet arrives split across reads as often as not. Tests: MQTTWireTests (11: every packet kind round-trips, the CONNECT layout byte for byte against the specification, the reserved 0b0010 on SUBSCRIBE/UNSUBSCRIBE, the length field at all eight boundaries, every prefix of a packet incomplete, 2,000 random datagrams refused without trapping, the specification's own matching table, and the payload readings) + MQTTSessionTests (14 over the loopback against MQTTStubBroker, a broker in the test process because mosquitto is not installed on the machine and because a real broker will not cut a socket on request, sit on an acknowledgment, or refuse with a chosen code: connecting, the refusal, the round trip, a retained value handed to a later subscriber, both directions of .atLeastOnce, the two drains, unsubscribing, the parameter binding, the heartbeat, the will firing on a cut socket and not on a goodbye, the reconnection with its resubscribe, and the resend). A real broker at the desk is still owed. No pixel snapshot (the output is values). Example Integration/MQTTRoom (every topic heard as a dial, publishing a wave of its own so a bare broker already shows the round trip; media held back for a desk with a broker on it). Docs/Integration/MQTT.md; Guide Ch 45 § What the building already says: MQTT + the TopicsAndFilters figure.
  • Remote surface (OllinRemote satellite; RemoteInspector.swift + RemoteWire.swift + Resources/surface.html). The @Param inspector served to a phone: extend(RemoteInspector()) starts one NWListener TCP port (default 9330, port: 0 for system-picked; url/boundPort report it, one line printed on ready) whose connections speak hand-rolled HTTP (the page, a .copy resource, served on GET /) and a hand-rolled WebSocket upgrade + frame codec — the whole wire layer (RemoteWire/RemoteHTTP/RemoteWire.Frame) is pure functions over bytes/values, and its tests are socket-free on purpose (the house no-loopback-TCP rule; the accept key is pinned to the RFC 6455 worked example). Values are sent as the Codable ParamStored payloads the hosts persist, with one deliberate exception: a menu travels by option *index* (.number), because the page knows display labels and their order, never the enum's case names; every other kind applies through restore, a menu through its control setter with a bounds check. Phone edits queue and land in beforeDraw on the main thread — the same frame boundary the host inspector writes at — never on the network queue; the wire snapshot the queue reads (hello descriptors + values + shown) is rebuilt on the main thread each ~250 ms push tick and handed across under a lock. Pushes are diffed (changed values + flipped show(when:) rows) plus a stats message (fps/clock/frame); the page renders every control family touch-first (whole-row bar sliders, pad, segmented, native color picker), reconnects on close, and ignores echo while a row is mid-touch. Reload safety: a fresh sketch brings a fresh extension; deinit calls stop() and allowLocalEndpointReuse covers the swap window. Open-network caveat (possession of the address is the key) documented, not gated. Example Integration/RemoteSurface (aurora serving all control families, draws its own pairing URL); OllinRemoteTests/RemoteWireTests; Docs/Integration/Remote.md.
  • The room (OllinRoom satellite; Room.swift + RoomSession.swift + RoomWire.swift + RoomClock.swift + RoomTransport.swift + LocalNetworkTransport.swift). Several machines drawing one piece, with no server and no address: Room(named: "wall") + extend(room) and two Macs on one network find each other over MultipeerConnectivity by that name alone (as: overrides the display name, seat: fixes the slice, passcode: gates the room). Three things travel: values under a key (send(_:_:reliable:) for Double/Int/String/Bool/Vector2/Color/Data, read the OSC/MIDI three ways — latest cache number/int/text/bool/point/color/bytes + defaults, drain messages(), and bind(_:to:from:) onto a Param<Double>); @Param values (share(_:) / shareAll(), sent as the ParamStored payloads the hosts persist); and one clock (room.time, ownsClock, clockError). Plus membership (name/peers/everyone/isAlone/sketchName(of:)/arrivals()/departures()/problem) and the wall split (seat/seatCount). Load-bearing: the transport is a seam (RoomTransport, three calls over bytes), which is what lets the whole session be tested in memory and the loopback example run two rooms in one process — no socket is opened in any test, the house no-loopback rule; connected membership is kept apart from what greetings say (a hello can arrive before the transport reports the peer that sent it, and reading membership off greetings then makes a machine think it already greeted that peer, so its own hello never goes out and the other end never learns its seat or sketch name — the defect the first run had, caught by twoRoomsFindEachOtherAndSayWhatTheyAreRunning); only the lower-sorting name invites (both sides see each other, and if both invite, two connections race and one drops, which reads as a peer joining and leaving forever), and the room name is checked twice (in the advertised discoveryInfo and in the invitation) because the service name is capped at 15 characters, with RoomWire.serviceType(for:) prefixing ollin- and, when it must truncate, hashing the cleaned name rather than the typed one (or "Sala Grande" and "sala grande" would be different rooms); the clock owner is the lowest name (elected locally, no protocol) and RoomClock.offset survives a reset, so an owner leaving re-bases the estimate without room time jumping back to this machine's own clock (measured across a real handover: a 0.1 ms step); the estimate is the published round-trip one with the quickest recent answer believed rather than an average (a slow answer is a delayed one); a parameter carries the room time it was set at, and the later change wins everywhere with the machine name breaking a tie — without it two people setting one parameter at once take each other's value and stay mismatched for good (sabotage-verified red); an arrival re-sends every shared parameter (a machine switched on later would otherwise show its own defaults until somebody touched one, so the join tally is counted, never drained, or the sketch's own arrivals() would come back empty); incoming parameters apply at beforeDraw on the main thread (the inspector's own frame boundary) while a bound key applies on arrival (OSC parity); the transport handler closures are built inside the non-isolated session, never in the @MainActor extension, per the render-thread-closure rule; unframe copies the Data into an array because a network callback hands over a slice whose first index is not zero; the inbox caps at 4096. A headless export never opens the local network (nobody to meet while a file is written, and no permission asked), though a room over a supplied transport still opens. Measured (two sketches on one Mac, real transport): they find each other in about a second, the first clock answer lands within 0.25 s of joining, and the clocks then agree to 0.1 ms (the measurement floor). Local-network permission is the terminal's, as with screen capture. Examples Integration/RoomCanvas (a wall as wide as the room has seats, every parameter shared, a readout of seat, company, and clock agreement) + Integration/RoomLoopback (two rooms in one sketch over an in-process transport, space cuts the wire); OllinRoomTests (53: the codec and its refusals, the service-name rules, the clock arithmetic, and two whole rooms over a memory bus); no pixel snapshot (the output is values). Guide Ch 45 § Several machines, one sketch: Room + the OneRoom figure. Docs/Integration/Room.md.
  • Syphon (OllinSyphon satellite). Live GPU frame-sharing with other Mac apps over the vendored Syphon Framework (BSD-2, External/CSyphon, Metal portion only — wrapped, off the public surface). SyphonServer / publishSyphon(name:) publishes the rendered texture; SyphonClient.frame draws an incoming feed as Image(texture:). Brought two core seams (shared with future layered effects): the GPU-texture frame hook (wantsRenderedTexture + frameRendered(_:texture:), the on-screen frame tone-mapped once more into a canvas-sized texture, no CPU readback) and texture-backed Image(texture:). Gotchas: publish flipped: true + client flips back (orientation); hand the server a non-sRGB .bgra8Unorm view and read back through a .bgra8Unorm_srgb view (color — both need .pixelFormatView usage); single-GPU Macs only; and the vendored engine carries no unsafe build flag, because a target holding one cannot be reached through a package depended on by version, which made import OllinSyphon unresolvable for every consumer outside this checkout (error: the target 'CSyphon' in product 'OllinSyphon' contains unsafe build flags, and note a branch: dependency does not show it, only a version one does). Upstream's forced prefix header is replaced by one import of External/CSyphon/OllinSyphonPrefix.h at the top of each .m, recorded in THIRD-PARTY-NOTICES.md. Examples Integration/SyphonLoopback, SyphonViewer; Docs/Integration/Syphon.md. Failure shape (2026-09-22): unavailableReason (no source of that name was available when the client looked, no Metal device, a source that refused the connection, or one that has since gone away, read off isValid), nil while the connection stands; SyphonLoopbackTests.aMissingSourceSaysSo.
  • Haptics (OllinHaptics satellite; HapticEvent.swift + HapticPattern.swift + TrackpadPlan.swift + HapticHub.swift + DeviceHaptics.swift + Sketch+Haptics.swift). Touch as an output beside the pixels and the sound: HapticPattern (a value holding .tap/.hum HapticEvents on its own timeline, built by .tap/.hum/.pulses/.silence and composed by then/over/delayed/repeated/scaled/speed/reversed), played through playHaptic(_:) / stopHaptics() / hapticStrength(_:) with hapticsAreAvailable / hapticHardware / hapticsUnavailableReason reporting what is on the other end. The platform fact that shapes the whole design, and it runs the other way from the design note: Core Haptics does not drive a Mac. CHHapticEngine.capabilitiesForHardware().supportsHaptics is false on a MacBook Pro with an actuating trackpad, and CHHapticEngine() init fails -4809 (notSupported) — probed on this machine; the Force Touch trackpad is reached through AppKit's NSHapticFeedbackManager instead, which offers three fixed feelings and one strength, and it actuates only while the button is held (felt at the desk 2026-09-01: every knock asked for during a plain pointer move was planned and delivered to nobody, while the same knocks under a held button landed), so a sketch asks for touch inside a drag, never on a hover. So the tier is two back ends behind one seam, and the trackpad one plans before it plays (TrackpadPlan.knocks(for:strength:), a pure function, which is what makes the whole translation testable with no hardware): sharpness picks the feeling (thirds), strength becomes density (rate coding, 6…30 knocks a second, and a fade thins the run rather than lowering it, since the hardware has one strength), anything under the 0.05 floor is dropped (so a pattern that fades to nothing ends in silence, not one stray knock), and knocks closer than 20 ms are dropped twice over — inside a pattern by the plan, and across patterns by the hub against the clock, since the plan only ever sees one pattern and a sketch calling every frame is the case it cannot see. Load-bearing details: availability is read from the IOKit registry (AppleMultitouchDevice / ActuationSupported), never from the performer, which is handed back on every Mac and silently does nothing where there is no actuator; stop() resets the spacing clock (nothing is playing, so the next knock waits for nothing) and bumps a generation so scheduled knocks that have not landed are canceled; DeviceHaptics is not @MainActor (the engine's stopped/reset handlers arrive off-thread, the render-thread-closure rule); a HapticEvent clamps every number at init and turns a NaN or an infinity into 0 (one infinity in a time is a knock that never arrives, one NaN makes every comparison in the plan false); HapticEvent.strength(at:) is public so a sketch can draw the envelope the hand is being given, and both the example and the Guide figure do. The .engine path (CHHapticEvent transient/continuous, fades expressed in the system's normalized attack/release adjusters rather than seconds, sustained 1) is written and checked as far as it can be without hardware — the system builds a pattern out of events with no device involved, so the tests read the translation straight back off it — and it waits on the phone and pad rather than on this library; makePattern throws nothingToPlay on an all-silence pattern (the system itself would accept an empty one and start an engine for it). Felt at the desk 2026-09-01: the three feelings tell apart, and the hum's rate coding reads as a stronger hum toward the right. Rumble on a game controller is the named next leg (the Controller docs already point here for it). Example Integration/HapticRidges (three strips of ridges knocked under a held-button drag, a hum under the space bar, and the plan drawn along the bottom edge); HapticPatternTests / TrackpadPlanTests / HapticOutputTests (51, the rate coding, the spacing rule, the cross-play guard, and the fade each verified red by sabotage; the hub suite .serialized on the one process-wide hub, with a stand-in performer and a hand-wound clock the stand-in scheduler advances, since firing a whole timeline at one instant would let the spacing rule eat everything after the first knock); no pixel snapshot (the output is touch). Docs/Integration/Haptics.md; Guide Ch 38 § Touch as an output: haptics + the FeltPattern figure.
  • Game controllers (OllinController satellite; Controller.swift + ControllerButton.swift + ControllerHub.swift + Sketch+Controller.swift). A modern controller read in draw() over GameController: controller (player one) / controller(_ player:) / connectedControllers / controllerCount hand back a Controller value snapshot (sticks / dpad / triggers / isDown/wasPressed/wasReleased over the ControllerButton enum / isConnected+didConnect+didDisconnect / motion / touchpad / battery / unavailableReason), with the bare settings controllersReportMotion(_:) / controllerDeadzone(_:) / controllersRunInBackground(_:) / discoverControllers(). A satellite, not core input, and the reason is the seam, not convenience: mouse / keyboard / pressure are core because they arrive as NSEvents through OllinMTKView and need the AppKit/UIKit seam, where a controller is handed to the process by a system service, touches no view, and is character-identical on macOS and iOS, so core membership would buy the iOS leg nothing and cost every 2D sketch a framework link. Load-bearing: the tier is polled once per frame (sampleIfNeeded(frame:) keyed on frameCount, so every read within a frame agrees and the hardware is hit once), and that is why it is testable at all — a synthetic GCController (GCController.withExtendedGamepad()) accepts written values and reads back through the shipped path, but does not fire the system's change handlers and never joins GCController.controllers(), so a handler-driven design would be unverifiable with nothing plugged in and the hub takes its list through an injectable source seam; the classic extendedGamepad profile is the one to build on, not GCControllerLiveInput, whose buttons/axes/dpads come back empty on a synthetic controller (probed). Nothing arrives off-thread, so there is no lock and no @unchecked Sendable producer here, unlike every other satellite. Presses/releases are edges differenced between two samples (ControllerButtonMask, one bit per button, wasDown/down on the slot) with no event drain by design — a press lasts several frames, so unlike MIDI a hand cannot press and release between two of them. Sticks/dpad/touch are y-flipped once into canvas terms (up reads negative, so p += stick * speed moves up the screen); the deadzone (default 0.1) is radial and rescaled, never a per-axis cut (which jumps at the edge and bends a diagonal hold). Player slots are held, not recomputed (a controller keeps its number while connected, a newcomer takes the lowest free one, so unplugging player 2 cannot renumber player 3), assigned by walking the slot array in order rather than a dictionary (the determinism rule). Motion is opt-in (sensorsActive costs battery; hasMotion is false both with no sensors and with none asked for) and Xbox hardware has none at all; the touchpad comes from physicalInputProfile.touchpads[GCInputDualShockTouchpadOne], never GCDualShockGamepad.touchpadPrimary, which is typed as a plain direction pad and so cannot tell a finger resting dead center from an untouched surface (only the named element carries touchState). Permission: none — probed, a bare swift run binary reads a controller with no TCC prompt and no entitlement (unlike the screen); what is real is shouldMonitorBackgroundEvents, NO by default since macOS 11.3, so a sketch behind another window reads centered until controllersRunInBackground(true). Headless reads neutral with unavailableReason set and a one-time stderr note. Deliberate cuts, each named in the docs rather than dropped: rumble / adaptive triggers / the light bar (outputs, so they belong to the haptics work), Apple Pencil (waits on the iOS leg; its force joins the shipped pressure rather than becoming a second thing), attitude (no public quaternion type yet), racing wheels and spatial accessories. Example Integration/ControllerInput (a pad as a drawing instrument, map drawing every input as it is read); ControllerTests (27, .serialized on the one process-wide hub, each invariant verified red by sabotage; the single hardware test is .enabled(if:) so a machine with no pad reports it skipped and says so rather than passing vacuously); no pixel snapshot (live input). Docs/Integration/Controller.md; Guide Ch 38 § Something to hold: game controllers + ReadingAPad figure.
  • Screen and window capture (OllinScreen satellite; ScreenCapture.swift + ScreenSource.swift). Any display / app / window as a live FrameSource + VideoFeed over ScreenCaptureKit: the non-cooperative counterpart to Syphon (which needs the other app to publish). ScreenCapture(_ source:) + start()/stop(), drawn with drawFrame/frame, GPU-resident (IOSurface → CVMetalTextureCache → Image(texture:), the VideoPlayer path, .bgra8Unorm_srgb against config.colorSpaceName = .sRGB); parameters source / excludesOwnWindows / showsCursor / frameRate / scale are all live (a filter-only change pushes through updateConfiguration, a filter-changing one rebinds). ScreenSource is the nameable value (.mainDisplay / .display(id) / .app(name-or-bundleID) / .window(matching:app:) / .windowID), with availableDisplays()/availableWindows()/apps() for discovery — named-in-code is primary and the system picker is a deliberate cut (SCContentSharingPicker is a Control-Center application singleton with an observer protocol, host-app shaped, and a picked filter is a click nobody recorded; enumeration gives the same discovery). Load-bearing: the stream must be retained or it is deallocated and silently stops delivering with no error and no callback (cost a probe to find), and it lives in a StreamHolder so the nonisolated deinit can end it; self-exclusion keys on pid, never bundle identifier (a swift run sketch is listed in SCShareableContent with an empty bundle id, so ScreenMatch.app must reject an empty needle or every such process matches ""); frames arrive on a background queue so the SCStreamOutput is a file-scope non-@MainActor class (the render-thread rule) handing the buffer over an NSLock box (a CVPixelBuffer is explicitly not Sendable, so OSAllocatedUnfairLock's generic form can't hold one); only SCFrameStatus.complete samples carry pixels (idle/blank ones are also delivered); the captured size is filter.contentRect * filter.pointPixelScale (points → backing pixels) times scale; the CPU CGImage conversion for the tap runs only while a tap is installed; a source naming something absent is a wait, not an error (a 1 s retry loop, isRunning = wanted vs isReceiving = arriving), and a stream the system ends re-binds. The matching rules are extracted as pure functions (ScreenMatch) because SCShareableContent has no public init, so a fake screen cannot be staged; app names match in full (else "Mail" takes MailMate) while window titles match as a substring, ties broken by lowest window id so a run repeats. Permission is the design problem and it is a terminal's, not the sketch's: a swift run / ollin / OllinLive / gallery binary has no bundle identity, so TCC attributes screen recording to the launching terminal (verified: CGPreflightScreenCaptureAccess true and a full stream from a bare binary, with no entry of its own in System Settings), one approval covering every sketch from that terminal; surfaced as isAvailable / unavailableReason / requestAccess() (all nonisolated), a missing permission drawing its reason rather than throwing, and granting it never reaching a running process. Deliberate v1 cuts: system audio (belongs with the audio analyzer surface, not bolted to a video frame source) and the picker. Example Integration/ScreenCapture (the display filtered, L listing what is capturable as the code line naming it, and the feedback tunnel on a parameter); OllinScreenTests (26; the rules always-on, the end-to-end capture soft-gated on the permission — a consent a machine cannot grant itself); no pixel snapshot (a capture is whatever is on screen). Docs/Integration/ScreenCapture.md; Guide Ch 34 § Drawing with the screen: ScreenCapture + ScreenAsMaterial figure.
  • Virtual camera (Apps/OllinCameraApp host + CMIO system extension, plus the OllinCamera satellite). Registers a system-wide "Ollin Camera" every webcam app reads; publishVirtualCamera() letterboxes the canvas texture into a 1280×720 IOSurface-backed CVPixelBuffer enqueued on the device's sink stream, with a built-in "no signal" test card while idle. Gotchas: the sink is the direction-0 stream; CMIOStreamCopyBufferQueue returns a nil queue unless the queue-altered proc is non-nil (pass an empty closure); the extension is an installed, signed, notarized .app (xcodegen, not SwiftPM) with strict activation rules (bundle-id filename, CFBundlePackageType = SYSX, run from /Applications, install with ditto --noextattr --norsrc) — see the app README. Example Integration/VirtualCamera; Docs/Integration/VirtualCamera.md. Failure shape (2026-09-22): VirtualCameraError is internal, since no public call ever produced it; unavailableReason carries its sentences and clears when the retry connects.
  • Video playback (OllinVideo satellite). VideoPlayer (@MainActor) plays a file as a live texture-backed Image via AVPlayerItemVideoOutput → CVMetalTextureCache → drawImage (zero CPU copies); play / pause / seek(to:) / loops / rate / volume, duration / size (async), snapshot() for CPU/vision. All three initializers throw VideoError.fileNotFound / .resourceNotFound for a file that is not there (init(url:) used to open in silence, so the same missing clip was an error or a frameless player by door), and a file that opens but is not a movie says so in unavailableReason while frame stays nil, written by the metadata load or the headless reader, whichever finds it (VideoPlayerTests, 2026-09-22). Conforms FrameSource + VideoFeed, so a tracker attaches and analyzes the footage as it plays (via a second video output). Clearing frameTap means no more calls, and that takes two things beyond canceling the pump's timer, since canceling stops the next tick and says nothing about the one already holding a pixel buffer: the tick re-reads a cancel flag immediately before it delivers, and the clear drains the pump's serial queue (queue.sync {}) so no delivery survives it, skipping the drain when it is called from that queue, which is a tap clearing itself inside its own callback and would otherwise deadlock. Before that, a tap removed under load could still be handed one late frame, which is what made frameTapDeliversCPUFrames fail in a batch and pass alone. Headless export is deterministic: under OllinApp.isRenderingHeadless (a package flag every offline driver sets) the player swaps to a virtual playhead advanced by the sketch's per-frame pass (FrameAdvancing, now package so satellites conform; isolated @MainActor conformance) and decodes by timestamp through a sequential AVAssetReader (HeadlessVideoReader), so exported frame k always shows the clip at k/fps after play() — create the player by the end of setup() or the advance pass never finds it. A play() made before the export began counts: an export builds the sketch before it raises the headless flag, so a player started in a property's initializer went to the live player, the virtual playhead never moved, every exported frame was the clip's first, and a tracker reading it was handed nothing (OpticalFlow's published media showed no flow at all, found 2026-10-17). play / pause / stop now set the virtual playhead's intent in either mode (ExportClockTests.aVideoStartedBeforeTheExportPlaysOnItsClock). snapshot() follows that virtual playhead too, so seek(to:) then snapshot() is the deterministic way to read one frame's pixels offline, and a still-image detect(in:) over a clip works under an export where a live tracker reads nothing (three Chapter 34 figures measure the bundled film that way). Soundtrack tap: audioTap (the core AudioTapSource seam) delivers mono PCM off an MTAudioProcessingTap; the tap's C callbacks touch only a storage object the tap itself retains (released in finalize — never reference the player from the callbacks; the tap can outlive it), and OllinAudio's Soundtrack(of:) runs the analyzer surface over it (rebuilds once at the stream's real sample rate). Gotchas: .bgra8Unorm_srgb texture; no y-flip (top-down); keep a short CVMetalTexture ring alive; isMuted = true starves the audio tap (a hard mute stops audio processing) — use volume = 0; a Soundtrack/vision tracker reads nothing during a headless export (live-clock paths). Examples Video/VideoPlayback, Video/SoundReactive (spectrum bars + beat ring over its own clip's music); Docs/Video/Video.md. A tap's lock holds the closure in Held, never as its bare state: the bare shape re-wrapped the closure two thunk frames a read and overflowed the tap thread after about twenty minutes of play (2026-10-06); VideoAudioTapTests and AudioTapRelayTests pin the consumer one call deep on the three-hundredth block, and the rule is in CLAUDE.md beside the handoff rule.
  • Physics (OllinPhysics satellite). One World stepped each frame, hybrid by design: a soft Verlet side (Particle — a final class, no stored velocity, previous-based; Spring, a positional distance constraint; World with gravity / drag / bounds / bounce / opt-in disk collisions broad-phased through a uniform spatial hash) and a rigid side backed by vendored Box2D (MIT, External/CBox2D) — addBody / Body / Collider (.circle / .box / .capsule / .polygon) / connect / Joint (.revolute / .distance / .weld / .prismatic) / grab (mouse joint), sharing the world's gravity / bounds / bounce / drag, bridged by unitsPerMeter. Gotchas: drag is the share of speed lost per sixtieth of a second, scaled by the step for a particle ((1 - drag)^(60 dt), exactly 1 - drag at a 1/60 step so a 60 Hz run is byte-identical) and handed to the rigid engine as the matching exponential rate (-60 ln(1 - drag), on both linear and angular damping), so one number slows a particle and a body alike at any frame rate (a weightless drift field needs drag = 0; a .kinematic body gets 0 whatever the world's, and takes it again when switched back); the walls carry restitution 0, so a body's own wins against them under the engine's max-combine, and a body that named none follows world.restitution through its didSet (Body.namesRestitution, flipped by addBody's argument or the restitution setter); gravity.didSet wakes every rigid body (the engine never does); allowFastRotation lifts the engine's quarter-turn-a-step spin cap; substeps cuts the soft step into pieces, the frame's applied forces re-added each piece and the default 1 byte-identical (the rod's projection loss is first order in the step: a bob on a rod gives back 70% of its height on its first swing at 1, 98% at 16, and holds within 5% for ten seconds at 64; four pieces at 60 Hz and one at 240 Hz agree to the third digit); Box2D's world pool is a non-thread-safe global → every Box2D-touching test in one @Suite(.serialized); spawn a Body inside bounds, not above; disk-collision resolution walks particles in index order with a fixed cell scan — never iterate the spatial-hash Dictionary directly (per-process hash order reordered the position-mutating pair resolution, so a seeded pile settled differently on every run; a real Guide-figure flake). Soft bodies from outlines (SoftBody.swift): World.addSoftBody(from: Shape, mass:spacing:stiffness:bend:pressure:shapeMemory:) throws -> SoftBody rings each closed contour of the fill (resolved by a union with nothing, islands and holes by separated(), outer rings wound positive and holes negative) with ordinary particles about spacing apart, every corner over 30 degrees kept as a particle of its own; four XPBD families inside the world's passes (λ cleared each step, α̃ = α / h²): edges, links two and three along the ring (never an angle constraint: measured to pump energy when not converged), each ring's own area (one constraint an island let a counter collapse), and shape matching toward a rigid fit; compliances are relative gives against the body's own weight (mass · pull, pull = gravity at build or 980), shape memory's against a drop (a further factor of 100, measured: read against a load the useful dial was 0.9 to 0.99); the walls hold a soft particle by position only (a per-pass reflection re-reflects, a post-solve velocity pass was measured to change nothing) so restitution does not reach a soft body; contacts per island, the same body's included, walked in insertion order, plus keepRingsApart and loose particles with a radius when particlesCollide; rigid bodies pass through, no friction, no self-collision (filed); a world with no soft body steps byte-identically; move(by:) keeps the motion. SoftBodyTests 15 (area within 2% through a drop, rigid at shapeMemory 1 against the default, no sinking past a pixel, one height within 2% at 30/60/120/240 Hz, a counter kept inside at shapeMemory 0), each red under a plant of its own (zero compliance, no area, no step in the compliance, no ring keeping, no contacts). Example Physics/Blobs (rewritten over it, 4.3 ms a frame for 556 particles in release); Docs/Simulation/Physics.md § Soft bodies; Guide Ch 11 § A squishy thing from any outline with the SoftShapes figure; ARCHITECTURE § The 2D soft body. Examples Physics/Packing + Blobs (soft), RigidBodies / Chain (rigid), Joints (all four JointKind cases off static anchors, each rig grabbable) and Forces (applyForce / applyImpulse / applyTorque, a .kinematic sweeper, a pin()ned cloth tinted by Spring.strain, unitsPerMeter on a parameter); Docs/Simulation/Physics.md. The 3D rigid side (World3D.swift / Body3D.swift / Joint3D.swift / Sketch+Physics3D.swift, backed by vendored Jolt Physics — MIT, External/CJolt, C++ behind Ollin's own extern "C" bridge, the only code that includes Jolt headers, so the JPH_* define-consistency rule holds by construction); the target defines JPH_NO_DEBUG (the library's debug checks are on unless it or NDEBUG is defined, and its asserts compile to a trap, so a full contact buffer, which the library reports and recovers from, stopped a release build; a Chain poster renders pixel-identically without them), the solver reserves sixteen touching pairs a body for both its body pairs and its contact constraints (a packed box touches up to 26 neighbors; one a body overflowed any dense pile; reserved, not touched, so a default world's peak footprint held at 120.8 MB against 120.9), the step's scratch is TempAllocatorImplWithMallocFallback (the plain allocator aborts past its 16 MiB block), and a step's error flags reach World3D.lastError as one sentence, cleared by the next step that keeps every contact (aPileDenserThanOneContactABodySteps, which trapped before, and aPileThatOverflowsSaysSoAndRecovers); maxBodies reads back what the world was made with: World3D (eager handle, gravity y-up (its didSet wakes every body through cjolt_world_wake_all), ground slab at restitution 0 so a body's own wins against it, bounce (a body that named none follows it, Body3D.namesRestitution), drag (the 2D reading, default 1 - exp(-0.05 / 60) so the engine's own 5% a second holds and nothing moves; cjolt_body_set_damping on every rigid body when it changes; soft bodies, ropes, ragdolls, and vehicles keep their own), maxTimestep clamp, substeps (multiplies the one-pass-per-sixtieth count handed to the solver's Update; the hinge's own loss is first order in the step, a horizontal bar's first apex 7.9% short at 1, 2.1% at 4, 0.6% at 16, so a pendulum scene opts in), unitsPerMeter default 1 — scene units ≈ meters, unlike 2D's 100; cjolt_body_create lifts the engine's quarter-turn-a-step spin cap) / addBody with Collider3D (.sphere / .box / .capsule / .cylinder / .taperedCapsule / .taperedCylinder / .cone / .hull / .mesh / .heightfield(Heightfield, width:depth:height:) / .compound([Part]) — mesh and heightfield colliders are static-only, a dynamic one is pinned with a one-time note) / connect with JointKind3D (2D's kinds + .ball) / grab, and the sketch sugar withBody(_:) (pose via withState + translate + quat→angle/axis rotate, so replay/export/materials all apply untouched) + body(under:in:) / grabBody(at:in:) / dragGrab(_:to:) — with the polled one-call dragBodies(in:) over them (press grabs, hold drags in the view-parallel plane, release lets go; a press that lands on nothing grabs nothing for that press — the miss is remembered on the world (pointerGrab/pointerGrabAttempted), so a mid-hold sweep over a body never picks it up, and several worlds drag independently) — and the draw side drawCollider(_:) / drawBody(_:): every Collider3D case renders (primitives as themselves, .mesh/.heightfield as their meshes — the heightfield built per call, cache when large — .compound recursing, .hull as corner markers, the tapered pair through the package Mesh.frustum), closing the hand-written switch's default: break hole where an exotic body was silently invisible; drawBody = withBody + drawCollider, byte-identical to the spelled-out loop (PhysicsConvenienceTests, sabotage-verified). (camera ray-pick; drags in the screen-parallel plane at the picked view depth). Load-bearing: never pass &tuple.0 across the C boundary — the compiler may materialize a temporary for that one element and silently drop every value the C side writes past it (a real bug: positions read back with y/z stuck at 0 while the sim ran perfectly); all marshaling goes through CJoltInterop.swift's withUnsafe(Mutable)Bytes(of:) helpers. The grab is the reference drag model and must stay it: a static anchor body created but never added to the world, teleported per move (velocity stays zero), holding the body on a zero-length distance spring (frequency 2, damping 1) — a kinematic anchor driven by MoveKinematic feeds its arrival velocity into the constraint and pumps the body into orbit (a real bug). Jolt keeps no global world pool (only an init-once type registry behind call_once), so the 3D tests run parallel — no .serialized needed, unlike Box2D's. No pixel snapshots for 3D physics (the artificial-life precedent): determinism is per-binary — the same build replays byte-identically (test-pinned, incl. across thread counts) but a toolchain rebuild may shift ulps and chaotic scenes amplify them; behavioral RigidBody3DTests pin the solver instead, and the Guide's CrateFall figure freezes one collapse. Joint motors, limits & springs (stage 2): .revolute/.prismatic take limits: (a ClosedRange relative to the connect pose — that pose is 0, so the range must straddle it; hinge clamped to min ∈ [-π, 0] / max ∈ [0, π], the solver's window) and the returned Joint3D carries drive(at:strength:) (velocity motor) / drive(to:frequency:damping:strength:) (spring-servo position motor, upstream defaults 2 Hz / damping 1) / stopMotor() / friction / softenLimits(frequency:damping:) / the angle/offset readbacks. Load-bearing: cjolt_constraint_set_motor wakes both bodies (a sleeping pair never feels a motor change — a late-engaging door closer would hang open forever); joint friction applies only while unpowered (the solver's contract — one parameter is both stiff-hinge drag and the post-stopMotor coast-down brake); slider targets/offsets convert by unitsPerMeter while angles and the N·m / N effort caps pass through; the motor calls no-op with a one-time note on undrivable kinds (bridge switches on GetSubType, so no Swift downcast can mis-drive). JointMotor3DTests pins every parameter against its counterfactual twin. Compound & world-geometry colliders (stage 3): .compound([Part]) fuses shapes into ONE body via .part(_:at:rotated:axis:density:) (part density multiplies the body's; parts nest; the Windmill's blade cross is the proof — five shapes, one body, drawn by a recursive part walk in withBody); .heightfield takes a Heightfield sized exactly like its mesh(width:depth:height:) so collider and drawn mesh trace one surface (resampled onto the smallest power-of-two grid covering the source cells, 4…1024 per side, 16-bit samples; beyond the edges is void); addStaticBodies(from: Scene) walks Scene.visitWorlds (now package) baking each mesh node's composed world transform into the triangles (scale/shear included) as one static mesh body at identity. Load-bearing: mAllowDynamicOrKinematic must be false for any MustBeStatic() shape — allowing the switch makes body creation compute mass properties the shape cannot provide and the library traps (latent stage-1 bug: nothing had ever created a .mesh-collider body until the stage-3 tests); the static pin asks the created shape, never the descriptor type. Shape marshaling goes through ShapeDescArena (arena-owned allocations held with withExtendedLifetime across the create) because compound children make the pinned-buffer count data-dependent — fixed withUnsafeBufferPointer nesting can't express it. Collider3DTests pins the tier (compound mass/balance vs counterfactual twins, part poses, cone = ⅓ cylinder mass, heightfield mappings, scene-collider composition, byte-identical replay). Contact events & sensors (stage 4): every touch that started or stopped during a step lands in world.contacts (Contact3D: phase / a / b / point / normal / speed + involves/other(than:)), polled in draw() like mouse state, with the per-body views contacts / arrivals / departures / touching / isTouching(_:); addBody(…, isSensor: true) makes a detector region that reports overlaps and pushes nothing. Load-bearing: the listener fires on solver worker threads mid-Update, so the bridge's mutex-guarded ContactRecorder buffers flat-POD events the main thread drains after cjolt_world_step returns (never a callback into Swift, the render-thread rule's C cousin); the buffer collapses sub-shape pairs into body pairs by reference count (a crate on terrain reports one landing, not one per triangle) and sorts before draining (worker-thread record order is nondeterministic; the Swift touching lists are sorted arrays, never Sets, for the same reason); speed is the pre-solve closing speed (v1 − v2)·n (the manifold normal runs a→b), and an ended event carries only the pair (OnContactRemoved may not touch the bodies at all, one may be destroyed) so its point/normal/speed are zero and an event naming a removed body is dropped after its bookkeeping applies; a sensor is kinematic and never sleeps whatever kind was asked for (a static sensor loses the contact the moment a body settles, which would empty a pressure plate) and lives in its own object layer pairing only with moving bodies, so sensor.touching answers standing occupancy where an ordinary settled pile sleeps and stops reporting (Jolt's model, pinned deliberately against its sensor twin); sensors are invisible to ray picking (NonSensorBodyFilter) and Body3D.kind refuses to change one (noteOnce); the ground slab is now a registered-but-unlisted world.groundBody (in the id map so a landing names a body, out of bodies so no drawing loop skips a 1000-unit box). Contact3DTests pins it against counterfactual twins (sensor pass-through vs solid deflection, one event per pair vs two loose bodies, √(2gh) impact speed, read-twice-not-drained, no stale touches after a removal, identical replays). Characters (stage 5, Character3D.swift): world.addCharacter(radius:height:at:) is a walking figure driven from draw() — move(_:) sets the horizontal velocity it's trying to walk at (held until changed), jump(_:) is granted only if it's on the ground at the next step, and world.step sweeps every character along with the bodies (no second update call); readbacks are position (the feet) / isOnGround / groundState / groundNormal / groundBody / groundVelocity, the parameters stepHeight / maxSlope / pushStrength / mass / facing, and the sketch sugar withCharacter(_:) (feet + facing, withBody's twin). Load-bearing: a CharacterVirtual is a shape swept by hand, not a body in the world, so it needs its own bridge object (CJoltCharacter) and its own update call — run before cjolt_world_step (the reference's order) via ExtendedUpdate, the one that does stair-stepping and stick-to-floor (both distances are its arguments, so the wrapper holds them, not the character); the capsule is built bottom-at-origin through a RotatedTranslatedShape so position is the feet, and mSupportingVolume is Plane(up, -radius) so only the lower cap can hold it up (a hand brushing a wall is not ground); isOnGround reads GetGroundState, never a normal test. The velocity composition is the library's own recipe and lives in Swift (advance): on the ground it inherits groundVelocity (so a moving platform carries it, pinned) and only then may a jump be added, in the air it keeps just its vertical, gravity is added, and the walking velocity is added last and always (air control on, no inertia smoothing — the sketch owns input feel). velocity is intent, not outcome — the library never writes back the achieved velocity, so a character pinned against a wall still reads a full walking pace; actualVelocity is derived from the step's position delta (the reference's "effective velocity") and is what a walk cycle must read. Every character always carries an inner kinematic body (character.body, 0.9× scale, Layers::MOVING): it is what gives the character presence among the ordinary bodies, so the stage-4 contact/sensor surface, ray picking, and tunneling protection all work on it — registered in bodyByID but kept out of world.bodies (the groundBody precedent, so no drawing loop renders a capsule over the sketch's own figure); characters register in a CharacterVsCharacterCollisionSimple so they collide with each other. stepHeight is a probe distance, not a hard ceiling (the rounded foot slides up an edge first, so a ledge ~25% taller may still be climbed — test with a margin, don't fake precision); pushStrength (N) vs ground friction decides a shove (4 kg crate moves under the default 100 N, 43 kg does not); a slope just past maxSlope reads .onSteepSlope but a near-vertical face reads .notSupported. Character3DTests (19) pins each parameter against its counterfactual twin (same ramp, one maxSlope; same ledge, one stepHeight; same crate, one pushStrength), plus platform carry, sensor entry, teleport-rereads-ground, and identical replays. (Fixed alongside: World3D.removeAll() emptied bodyByID while groundBody still pointed at the slab, so floor contacts were dropped and a character's groundBody read nil after a rebuild.) Vehicles (stage 6, Vehicle3D.swift): world.addVehicle(_:at:wheels:) builds a driveable machine from a chassis collider plus Wheel3Ds (position = the chassis-local mounting point, radius/width, steers/driven/handBrake, suspensionLength/suspensionTravel/suspensionFrequency/suspensionDamping, brakeTorque/handBrakeTorque, grip, casterAngle), driven from draw() by throttle / steering / brake / handBrake (all held until changed, world.step hands them over) with the parameters engineTorque / topSpeed / wheelContact / maxTilt / antiRollStiffness and the readbacks speed / forward / up / rpm / gear / isOnGround plus per-wheel center / spin / steerAngle / isOnGround / groundBody / suspensionCompression / slip / slipAngle; the sketch sugar withWheel(_:) poses a +y-aligned cylinder onto a wheel (withBody's twin), and balances: true makes the leaning two-wheeler. Load-bearing: a vehicle is a constraint on a body, not a body — the chassis is an ordinary Body3D in world.bodies (so it collides, reports contacts, and is ray-pickable) and creating one must AddConstraint and AddStepListener (the wheels are collided and driven inside the step listener; without it the vehicle never moves, upstream's own warning), with the constraint destroyed before its chassis; the chassis takes an explicit mass and a lowered center of mass (defaulting to the mean wheel-mount height) through two new CJoltBodyDesc fields, and the OffsetCenterOfMassShape wrap leaves GetPosition at the shape origin so drawing is untouched; axles are derived from geometry, not list order (Vehicle3D.axles(of:) sorts by (z, x, index) and clusters within a quarter of the wheelbase, so a two-wheeler gets two single-wheel axles where list-order pairing would fuse front and back), an axle with any driven wheel takes a differential and every full pair an anti-roll bar, and at least one axle must be driven (zero differentials trips the controller's own torque-split assert, so with nothing marked the rear axle takes it); gearing is solved from topSpeed in the bridge (top gear at max RPM turns a driven wheel that fast) rather than exposing ratios, and the sample's 10× SetTireMaxImpulseCallback hack is deliberately not adopted (defaults measured by a torque sweep instead); every wheel setting except driven is live via cjolt_vehicle_set_wheel_settings (one applyWheelDesc serves build and retune, const_casting settings the bridge alone owns and the solver re-reads each step; friction curves rebuild from a default tire before grip scales them) while driven is the gearbox and is refused after construction with a note; the wheels' collision tester filters out the chassis and sensors (overriding the body filter replaces the default self-filter); and a two-wheeler needs a real casterAngle (~30°) or the balance controller cannot hold a line — note that running dead straight it stays up even unbalanced, so the counterfactual starts it leaned. Vehicle3DTests (19) pins each parameter against its twin (the sharpest: two cars with slick fronts differing only in which axle is driven travel 2× apart), plus suspension sag, braking vs coasting, the hand brake locking only its own wheels, reverse braking through a stop, the gearing ceiling, wheels in the air, axle grouping, a leaned two-wheeler righting itself, and identical replays. (Fixed alongside: a Character3D/Vehicle3D a sketch still holds when its world goes now tears down safely — the world marks them destroyed on its way out.) Ragdolls (stage 7, Ragdoll3D.swift + RagdollFit.swift): world.addRagdoll(from: scene, at:joints:swing:twist:mass:friction:) turns a skinned Scene into a tree of rigid bodies (one per skeleton joint, hung on the new .swingTwist(at:axis:swing:twist:) joint kind) and scene.apply(ragdoll) writes the simulated pose back onto the skin, the inverse of apply(_:at:); drive(toward: scene, frequency:damping:strength:) powers every joint toward the pose a scene is holding (the animation becomes a request), goLimp() cuts it, pose(from:) stands the figure back up, limit(_:swing:twist:) retunes one joint live, applyImpulse(_:) shoves the whole figure, kind switches it dynamic/kinematic/static; readbacks are limbs (name/body/parent/collider/the shape's pose inside the body) + bodies + position + isAwake, and the sketch sugar withLimb(_:) poses onto a limb's fitted shape (withBody's twin). Built on the library's own RagdollSettings/Ragdoll for Stabilize() + constraint priorities + DisableParentChildCollisions(). Load-bearing: a limb's body stands at its joint, not mid-bone (the shape is pushed out along the bone via RotatedTranslatedShape), which is why the write-back is exact (body transform ≡ joint transform, pinned to 1e-5) and why a motor target is the skeleton's own local rotation; the skeleton seam lives in the core (satellites can't depend on each other) as package Scene.skeleton() / skinnedVertices() / setJointWorlds(_:) / nodeWorldTransforms() in 3D/SceneSkeleton.swift, and setJointWorlds walks top-down, keeps each node's scale (a rigid pose has none; dividing it out shrinks the mesh) and re-syncs a TRS animation base to the posed transform (else a rotation-only track next frame drags the node back to its authored translation); shapes are fitted to the mesh, not the bones (vertices assigned to the limb weighting them most, in the joint's frame; axis = covariance's dominant eigenvector by power iteration seeded from the largest-variance axis, radius = 85th percentile of perpendicular distances, ends = 2nd/98th percentiles, so one stray vertex can't decide an arm's thickness) — the bundled figure fits a 0.157 torso against a 0.051 forearm from similar-length bones; mass splits by fitted volume then rebalances through Stabilize() (chain totals preserved); the root has no constraint, so a powered figure holds its shape, not its place (pin limbs[0].body.kind = .kinematic for the puppet-on-a-hook; ragdoll.kind = .kinematic routes drive(toward:) to MoveKinematic so it shoves what it walks through); the target scene and the drawn scene must be separate copies (a figure driven toward the scene it was just posed from has nowhere to pull); limbs are ordinary bodies registered in bodyByID but kept out of world.bodies (the groundBody/character.body precedent — the sketch draws the mesh), which needed body(under:in:) to look up bodyByID instead of bodies (also makes a character's stand-in and the ground slab pickable); each figure gets its own collision group, so a limb never fights its parent while two figures collide normally; joints not named in joints: move with the nearest limb above them with their pose and their share of the flesh. Ragdoll3DTests (22) pins it against counterfactual twins — the headline being one drop run twice, shape error 0.48 limp vs 0.034 powered. Asset authored by Scripts/make-ragdoll-scene.swift (a 16-joint humanoid + a looping wave animation). Soft bodies (stage 8, SoftBody3D.swift + the package Mesh.welded() seam in Sources/Ollin/3D/MeshWeld.swift): world.addSoftBody(from: mesh, at:rotation:axis:mass:stiffness:bend:pressure:damping:friction:bounce:iterations:vertexRadius:twoSided:pinned:) turns any Mesh into simulated particles (position-based dynamics; cloth that drapes, closed surfaces that hold air), read back as softBody.mesh (the source mesh with the simulation's positions + derived normals, cached per step) and drawn by the sketch sugar drawSoftBody(_:); per-particle control is pin / unpin / isPinned / move(_:to:) / nearestVertex(to:), the live parameters are pressure / iterations / vertexRadius, and the mouse pair is grabSoftBody(at:in:) / dragSoftGrip(_:to:) / releaseSoftGrip(_:) over a Sketch.SoftGrip (a pinned particle, since a constraint can't act on a soft body). Bodies live in world.softBodies, out of world.bodies (whose one-pose surface doesn't describe them; the groundBody/character.body/limb precedent). Load-bearing: welding is a precondition, not a nicety — Ollin's generators emit flat-shaded meshes whose triangles share no vertex index, so an unwelded soft body has no connectivity at all and falls apart into loose triangles; Mesh.welded() merges by position and the remap republishes results in the source mesh's order (the MeshReactionDiffusion precedent), which is what keeps uvs/colors/material intact. Read-back normals follow the source mesh's own normals, not the winding (a one-time area-weighted vote at build time against the rest shape; Ollin's catalog doesn't agree with itself here — Mesh.plane came back lit from underneath, the subdivision-surface lesson again). All three parameters are normalized so one number means the same at any size: stiffness maps onto compliance through scale = meanEdge · √particleCount / (mass · gravity) (the compliance at which one loaded edge stretches by its own length; a fixed compliance softens a heavy cloth and does nothing to a light one — pinned by stiffnessMeansTheSameAtAnyWeight), bend divides that same scale by meanEdge² (a fold constraint measures an angle where a stretch constraint measures a length, so it carries two fewer powers of length; without the correction the whole 0.2…1 range was already rigid), and pressure is the solver's n R T expressed as outward acceleration in gravities (a = P·A/(m·V) worked back through ApplyPressure, so 1 just holds the body's own weight up; refused with a note on an open surface, the closed test being every edge in exactly two faces). Settling takes ~600 steps — a 200-step probe read non-monotone and nearly flat, which is a measurement artifact, not the mapping. The envelope is the library's (not a shortcut): soft bodies collide with rigid bodies but not each other or themselves (a folded sheet passes through its own layers), they are outside world.contacts and body(under:in:), impulses/joints/grabs don't reach one (hence applyForce for wind), and there is no tearing (a tear must split a shared vertex and rebuild the constraint set, impossible mid-simulation). SoftBody3DTests (19) pins it against counterfactual twins (pinned vs free, stiff vs slack stretch, bend-held vs limp droop, firm vs limp ball height/volume/shove, pressure inert on a sheet, drape over a sphere, wind, identical replays). Water & buoyancy (stage 9, Water.swift): world.water = Water(level:density:linearDrag:angularDrag:flow:waves:) floats every dynamic body below the surface with no per-body opt-in (the ground DX), plus Body3D.buoyancy (a multiplier on what density alone would do), world.waterHeight(at:) / waterMesh(extent:resolution:around:) / waterPhase, and Water.Waves(amplitude:wavelength:speed:heading:). Load-bearing: buoyancy is an impulse the caller applies per body per step, not something the solver works out, so the loop lives in World3D.step before cjolt_world_step (the samples' PrePhysicsUpdate slot). The library takes a dimensionless factor (fluid density ÷ body density) rather than a density; the bridge forms it as scale · density · totalVolume / mass using the totalVolume GetSubmergedVolume just returned, never the shape's own reported volume, so the waterline lands where the displaced volume says (a body of density d settles with fraction d under, pinned to 0.08; the margin is what sleeping costs, freezing the body wherever its last oscillation reached). Ollin's relative density: 1 already maps to 1000 kg/m³, so Water.density uses the same scale and a cork/stone falls out of the collider density a sketch already sets. Waves are a per-body tangent plane (the impulse takes a surface point + normal per call, the boat sample's own approach), the surface function lives in Swift with an analytic gradient, so waterMesh/waterHeight and the surface bodies float on are one source of truth (test-pinned per vertex); the envelope is that a body much larger than its wavelength is approximated. The query (cjolt_world_bodies_in_box, broad-phase CollideAABox sorted by handle) filters to dynamic rigid non-sensor bodies: a kinematic body (sensor, character stand-in) would be nudged off its driven path and the library's buoyancy asserts on a soft body (its documented gap, stated not faked). Waking has three modes and needs them (CJoltBuoyancyWake): a settled floater sleeps and holds its waterline exactly (correct, and free); a swell wakes only what its surface passes through (so a sunk stone sleeps on); a changed setting must wake everything afloat wherever it is, because the equilibrium moved (the first draft had only the swell rule and a probe caught it: raising the level 0→3 left a sleeping crate frozen at the old waterline, outside its own bounds, so it never woke). Water is an ocean, not a pool (everything below level, out to the horizon; a harbor is static walls). Buoyancy3DTests (20) pins it against counterfactual twins (cork vs stone vs no water, waterline vs density, brine, drag settles vs bobs forever, sleeper holds its level, tide reaches it, swell carries the top and lets the bottom sleep, current drifts, buoyancy both ways, sensors/soft bodies untouched, drawn surface == the surface bodies float on, identical replays). World queries (stage 10, Query3D.swift): raycast(from:to:) / raycastAll / sweep(_:from:to:rotated:axis:) / sweepAll / bodiesOverlapping(_:at:rotated:axis:) / bodiesContaining(_:), all taking ignoring: + includingSensors: and all answering the shared Hit3D (body / point / outward normal / distance along the query) between steps, changing nothing. Load-bearing: the C filter is a struct (CJoltQueryFilter: ignore list + includeSensors + includeSoftBodies) passed by pointer, so collision filtering lands as a field rather than a parameter on every call; what a query is blind to is a decision, not an inheritance — a sensor is transparent (a region to be inside, not a surface to hit; the mouse-pick rule) and a soft body is too (no rigid pose to hand back, matching contacts / body(under:in:)), while the ground slab / character stand-ins / ragdoll limbs are visible because hits resolve through bodyByID, which is what makes a ground probe and a sight check on a walking figure work; the mouse pick is the same call (World3D.raycast(from:to:), the internal ray with includeSoftBodies on) so there is one ray implementation, not two that can drift; a ray's normal needs a BodyLockRead (only the body knows which face the sub-shape id names), a sweep's is the negated penetration axis; the bridge returns how many hits it found (not wrote), so Swift sizes a 16-slot buffer and grows exactly once; mesh/heightfield probes are refused with a one-time note (MustBeStatic() asked of the created shape, the stage-3 rule). Example 3D/Physics/Sightlines; Query3DTests (24). Collision groups (stage 11, CollisionGroup.swift): a named CollisionGroup (ExpressibleByStringLiteral) on every thing that can be added (addBody / addCharacter / addVehicle / addRagdoll / addSoftBody / addStaticBodies, each also settable live as .group) plus a symmetric world table — ignoreCollisions(between:and:) / allowCollisions(between:and:) / collides(between:and:) / collisionGroups / World3D.maxCollisionGroups (64) — and as: on all six queries (ask as a body of that group would). Load-bearing: the group is stored in the object layer (layer = (group << 2) | kind, kinds NON_MOVING/MOVING/GHOST/SENSOR), not Jolt's per-body CollisionGroup/GroupFilterTable: the layer is the one thing consulted at pair-finding (so the contact listener never hears a filtered touch), in every query's DefaultObjectLayerFilter, in ExtendedUpdate's filters, in a VehicleCollisionTester's constructor, and in SoftBodyCreationSettings, where the narrow-phase group filter reaches none of those, hardcodes "same sub-group never collides", and is already the ragdoll tier's slot (so the two stay orthogonal — a figure keeps its own limb filter and two figures in one Ollin group still collide). Group 0 is numerically today's layer set, so a world that names no group is byte-identical (whole suite unchanged, no re-records). Four hand-honored spots, each a would-be half-applied filter: character-vs-character is outside the broad phase, so GroupedCharacterCollision refills a scratch list per call and delegates to the library's own loop (the group travels as the CharacterVirtual's user data); wheel testers take their layer at construction, so a group change rebuilds all three and re-selects the active one, routed through World3D.bodyChangedGroup so setting the chassis body's group works too; buoyancy's SpecifiedObjectLayerFilter(MOVING) was an exact-layer match and had to become kind-based or a grouped body silently stops floating (a real trap); and motion changes re-derive the layer from the current group (a body let go from static keeps its group). Queries narrow via a group field on CJoltQueryFilter (never a new C parameter — the stage-10 rule); past 64 groups the extras stay .default with a note rather than aliasing an in-use group, and collisionGroups is what makes a typo (a new group, silently doing nothing) findable. Example 3D/Physics/Sieve; CollisionFilter3DTests (22). Per-body motion parameters (stage 12, Freedom3D.swift): freedom (a Freedom3D OptionSet over the six DOFs — .all / .plane(normal:) / .upright / .noTurning / .noMoving / raw [.moveX, .turnZ]), gravityScale, and checksPath (continuous collision detection) on every Body3D, each also an addBody parameter. Load-bearing: a restricted DOF lives inside the body's mass properties, so cjolt_body_set_freedom re-derives the whole set from the shape and takes the mass as an argument (a travel-locked body reports inverse mass 0 and cannot say what it weighed; Swift passes the explicit mass: a body was created with, else 0 = the shape's own) — which also made cjolt_body_get_mass fall back to the shape's mass properties, since a pinned body reporting 0 reads as weightless when it is the opposite; Freedom3D's raw bits are EAllowedDOFs' (the bridge casts), a zeroed desc field means unrestricted (so every existing CJoltBodyDesc(), the ground slab included, is byte-identical) and an empty mask reads as .all (the library's one invalid case, which would divide by a zero mass); .plane(normal:) rounds to the nearest world axis (only whole axes can be taken away, so there is no tilted plane); CCD stays an explicit opt-in because the solver's own threshold makes it free while slow (two runs byte-identical, pinned) so guessing would only change results for existing fast bodies, and speculative contacts catch far more than the naive estimate (a 0.05 pellet vs a 0.04 wall tunnels at 40 units/s, bounces at 20 — a demo needs genuinely thin plates); a Character3D shape-casts its own path and a Vehicle3D's wheels ray-cast the road, so only the chassis would want it. Fixed alongside: the floor slab kept its collision group across a ground/unitsPerMeter change (remembered on the world, since rebuildGround builds a fresh descriptor). Example 3D/Physics/Bagatelle; Motion3DTests (16); Guide Ch 30 § Taking a direction away: degrees of freedom + Flattened figure. Tracks, ropes, freedoms, and joint-to-joint links (stage 13, JointPath.swift): three more JointKind3D cases between two bodies plus a second enum JointLink3D between two joints, through a connect(_ a: Joint3D, _ b: Joint3D, _:) overload. .path(through:looping:alignment:) threads a body onto the smooth curve through a ring of points (PathAlignment .free/.rolls/.followsPath/.fixed; drive(at:) = units/s, drive(to:) = a 0…1 fraction, Joint3D.progress reads it back; a Contour form lays a plan-view curve on the ground). .pulley(from:over:and:to:ratio:taut:) is one rope over two hooks (ratio = rope falls on the second side). .allowing(_:at:travel:rotation:) is the general joint written as the Freedom3D set it keeps ([] is a weld, the three turns a .ball, one bounded turn a hinge). .gear(ratio:)/.gear(teeth:and:) ties two hinges and .rackAndPinion(travelPerTurn:) a hinge to a slider. Plus the standing gap closed: a standalone .swingTwist now takes drive(at:)/drive(to:) (the twist only, swing left free) and the new drive(toward:twist:) pointing its bone along a world direction, with Joint3D.twist the readback. Load-bearing: a link names joints because what it constrains is the motion they allow (bodies, axes, and the drift-correction constraints all fall out of them), and its moving end is the non-static body, or the second one named, matching every connect call's order; the path spline's normals are parallel-transported (a per-point up-minus-along flips on a vertical track) and its points go into body-1 space so a track follows a moving carrier, with progress normalized in the bridge (only it knows the segment count); a gear ratio is forced positive with a note (a negative one leaves the position correction pulling against the velocity rule until the pair detonates); a kinematic pulley end is refused with a note (the library's pulley part guards on IsStatic where every other guards on IsDynamic, so it asserts in a checked build and shoves the body in a release one; the vendored source stays pristine); meshed wheels want ignoreCollisions(between:and:) or plain cylinders jam; a link records all four bodies and both source joints so removing any of them cuts it. Example 3D/Physics/Contraption; JointKind3DTests (24); Docs §§ Tracks, ropes, and freedoms + Gears and racks; Guide Ch 30 § Machines out of joints: motors, gears, and racks + Machines figure, Ch 30 § More joints: a track, a pulley, a list of freedoms, and cables + TrackAndPulley figure. The soft-body seams (stage 14, Colliding3D.swift): a cloth becomes a member of the world in three ways. Contacts — world.contacts carries soft touches (softBody.touching/contacts/arrivals/departures mirror Body3D's), which widened the pair type: Contact3D.a/b, Body3D.touching/entered/exited, and Hit3D.body are now any Colliding3D (a tiny AnyObject protocol over group/isAwake/wake()/userData, conformed by Body3D + SoftBody3D) — every public call site was an identity comparison, so the churn was one cast. Buoyancy — SoftBody3D.density (relative, 1 = water; derived from mass/enclosed volume when closed, else 1) floats a sheet particle by particle. Queries — the deliberate reversal: every query now sees soft bodies (a curtain blocks a sightline), so World3D.pick folded into raycast and body(under:in:)/grabSoftBody cast the hit. Load-bearing: the solver's SoftBodyContactListener hands over a whole contact set per pass and never reports a parting, so began/ended are diffed by finishSoftStep after Update (the same ContactRecorder, registered as both listeners), the event's point/normal come from the first reporting particle, speed reads GetLinearVelocity() during the callback (the arrival velocity — the update averages the new one in afterwards, and the other body's would race), and a sleeping soft body keeps its touches (the solver stops asking, unlike the rigid removal report) — which is also what tells the buoyancy pass a sunk sheet is lying on something; buoyancy takes a ratio (ρ_fluid/ρ_body, since a sheet has no volume to displace) and per-particle surface heights computed in Swift, never one tangent plane (one plane over a 4-unit raft on a 9-unit swell curls it into a bowl — a real fixed defect), ramped over a band of one particle spacing (a flat sheet's particles all cross at once, so a hard test chatters); and a body heavier than the fluid has no equilibrium, so if it is asleep, submerged, and touching nothing it has stalled, not settled, and is woken (cloth's drag holds a sinker below Jolt's sleep threshold; comparing measured lift against 1 fails, since a floating raft balances at ~0.92). Example 3D/Physics/Raft; Contact3DTests/Buoyancy3DTests/Query3DTests grown; Docs §§ Soft bodies + Water + Queries; Guide Ch 31 § A raft made of cloth: soft bodies that float + Raft figure. Tracked machines (stage 15): addVehicle(…, tracked: true) builds the third controller on the shipped Wheel3D surface: the wheels become road wheels split into a left and a right band by which side of the hull they sit on (driver's right is -x; a one-sided machine is refused, never half-built), steering runs the inside band at 1 - 2·|steering| (half lock stops it, full lock reverses it into a pivot on the spot, and it needs throttle since the bands are turned by the engine), and the readbacks are isTracked / trackSpeed(_:) / wheels(on:) over a nested TrackSide. Load-bearing: the controller's ConstructWheel asserts on the wheel settings type, so a tracked build news WheelSettingsTV (flat 4.0/2.0 friction coefficients, which is why a band keeps pulling while it slides where a tire's slip curve falls away) and applyWheelDesc split into a shared WheelSettings base plus two overloads, with cjolt_vehicle_get_wheel guarding its WheelWV cast (a WheelTV has no slip, so slip/slipAngle/steerAngle read 0); every wheel must belong to exactly one band (the controller fills each wheel's track index from the two lists and an unclaimed wheel keeps its -1); neither steering ratio may be exactly zero (the controller divides by them and asserts), so a stopped band is spelled as a very slow one; the axle list is still computed and passed but read only for the anti-roll bars (they live on the constraint, not the controller); track inertia is derived, not exposed (0.08 · mass · radius² at the sprocket, measured across sizes: 0.02…0.15 lands within a few percent, so it stays a constant) and a band's brake is the sum of its wheels' brakeTorque (so the shipped parameter keeps meaning something and handBrake pulls the same brake). Closed alongside: a wheel's driven is now live (cjolt_vehicle_set_drive rebuilds the differentials or the two sprockets and re-solves the gearing against the new driven radius, so topSpeed is kept), and the rebuild waits for the step rather than the assignment because which wheels drive is set a list at a time and an eager report-back undoes the assignments still to come. Example 3D/Physics/Crawler; TrackedVehicle3DTests (20); Docs § Tracks; Guide Ch 31 § Turning without steering: tracks + Crawler figure. Saving a world (stage 16, PhysicsSnapshot.swift): world.snapshot() captures the whole world as a PhysicsSnapshot (a Sendable/Equatable value wrapping its own bytes, with bodyCount/jointCount, init(data:)/init(contentsOf:)/init(resource:in:)/write(to:)) and restore(_:) builds it back, over the file pair save(to:) / load(contentsOf:); it holds the rigid tier (every Body3D with its collider, pose, motion, and every addBody parameter, every Joint3D incl. gears and racks, the group table and its rules, and gravity/ground/bounce/maxTimestep/unitsPerMeter/water) plus the tiers above it by value — characters (capsule + parameters + pose + velocity + facing), vehicles (chassis + every Wheel3D parameter + gearing + the live drivetrain: engine RPM, gear, clutch, per-wheel spin rate and rolled angle), and ragdolls (the RagdollPlan fitting, per-joint limits, and every limb's pose) — and soft bodies once their mesh is named. Naming bulk geometry (PhysicsAsset.swift) is the one referencing seam: Body3D.assetName / SoftBody3D.assetName (a serializable sibling of userData) + restore(_:resolving:) / load(contentsOf:resolving:) over a PhysicsAssetResolver ((String) -> PhysicsAsset?, cases .mesh / .heightfield) + snapshot.assetNames (what a file will ask for, read without restoring), so the sketch stays the source of truth about where its assets live (the resource:in: rule). Opt-in by design (a self-contained file is what makes an arrangement committable); measured 105 KB held → 1.1 KB named on a 65² terrain + 5k-vertex mesh + 20 crates. Load-bearing: a name is per body, top level only (a mesh nested in a compound is written whole); an unresolved name costs that one body, not the restore (skipped with a note, its joints dropped, and the restored-body list keeps nils in the gaps so saved indices still line up); the fingerprint hashes the geometry (FNV-1a), because counts+bounds is blind to the case that matters most (Heightfield.diamondSquare normalizes to 0…1, so two seeds give identical counts and bounds), and a mismatch notes and restores anyway (the saved poses are the best answer there is); a soft body needs a name to be saved at all (it is nothing but its mesh) and carries its build parameters + every particle position/velocity + the pinned set via new cjolt_soft_body_get_velocities / _set_state (write goes through the COM transform mirroring the read, and sets mPreviousPosition too; 289 particles back at 1.19e-07). Gotcha: the reader's count() bounds a length against the bytes left, which is right for an array in the stream and wrong for a fingerprint's piece count (geometry deliberately not there) — reading a 4,225-sample terrain that way threw and refused the whole restore. Load-bearing for the tiers: the taxonomy is the design (sort by what is actually heavy in each: a character and a vehicle hold nothing heavy, a ragdoll splits into a fitting that fits by value and a skinned Scene that is the sketch's own asset, still loaded and still what scene.apply(ragdoll) writes onto, so a figure restores in a run that never read the file); Ragdoll3D's init split into a convenience scene form over a designated plan form, so a restored figure is built by the same code as a fitted one; a vehicle must carry its drivetrain (without it a car at full throttle falls 6.4 units behind over 2 s, vs 1.5 with it — and the fair comparison is a rigid stack captured mid-collapse, which diverges 2.13, so this is the already-documented in-flight-contact envelope, not a vehicle gap; a parked machine restores at 0); the chassis is written in the vehicle section but still takes a body index after the loose ones (so a joint naming it survives); never write a pose as a position then a rotation — the solver holds a body by its center of mass, so the position is computed against the old orientation and a shape offset inside its body (a ragdoll limb's RotatedTranslatedShape) lands ~5e-7 out (new cjolt_body_set_pose over SetPositionAndRotation; invisible for a box/sphere); and the bytes settle after one restore, not on the first capture (integration drifts a quaternion off unit length and the bridge normalizes anything handed back), which is the property aWorldOfEveryTierRoundTripsToTheSameBytes states — reproducing the normalization in Swift was tried and reverted as a half-measure (15 limbs of 16). Load-bearing: the format is Ollin's own, not Jolt's PhysicsScene, because a snapshot must hand back the Ollin description (a restored Jolt shape cannot become a Collider3D again, and the collider is what a sketch draws from), which makes restore a replay of the ordinary addBody/connect calls so a restored world is one a sketch could have built; a joint's zero is the pose its bodies held at connect (Joint3D.connectPoseA/B, recorded at creation), so restore stands them back there, makes the joint, then moves them on (exact for every kind at once: a door saved half open is still half open and still stops where it did, where the naive re-connect reads 0); the desc's three new fields (linearVelocity/angularVelocity/startAsleep) follow the zero-is-the-old-behavior rule, velocity passed in BodyCreationSettings because SetLinearVelocity wakes what it moves, and startAsleep is what makes a settled pile come back settled (0/12 awake vs 12/12 for the same poses built by hand, zero drift over 60 further steps); a vehicle's chassis is in world.bodies so capturing filters it out rather than saving a crate that restores wheel-less; the Swift group table is rebuilt in the saved (= solver) order and every pair in the union of old and new counts is reset. PhysicsSnapshot wraps Data (a Collider3D is neither Sendable nor Equatable), so a snapshot through a file and one in memory are the same value. The payload is LZFSE-packed (system codec, deterministic bytes; conditional on being smaller, which the codec reports by refusing an equal-size destination) behind a header carrying a flag, the unpacked length, and an FNV-1a checksum — load-bearing because LZFSE's decoder returns the full requested length from a truncated stream rather than reporting the truncation, so without it a cut-short packed payload unpacks to rubbish that parses into some world (the checksum is what makes "refused rather than half-read" true by construction, and it catches a corrupted-but-complete file too). Measured 1912 KB → 966 KB on a scenery-heavy world and 10.7 KB → 1.2 KB on a settled heap of primitives; the same measurement rejected narrowing mesh positions to Float32 (saves less, lossy for the Mesh a sketch reads back from body.collider, and dominated where it would help most). Round trip is exact (worst pose error 0, a ballistic body stepped on from a restore lands where the uninterrupted one does); the envelope is the solver's in-flight contact bookkeeping (a vehicle's wheel-contact cache included), plus grabs and motors, which a sketch says again. Two bridge getters came with it, surfaced as live Body3D.friction / .restitution. Example 3D/Physics/Yard; PhysicsSnapshotTests (23) + SnapshotTierTests (13) + SnapshotAssetTests (10); Docs § Saving and loading; Guide Ch 30 § Keeping what settled: snapshots + Kept figure. Reading UsdPhysics (3D/SceneLoaderUSDPhysics.swift in the core + OllinPhysics/UsdPhysics.swift): world.addBodies(from: scene) picks up rigid-body / collider / joint annotations authored elsewhere, over the shipped OllinUSD parser — the interchange leg, where lossy-on-the-way-in is the point (writing the format would not be, which is why the snapshot stays Ollin's own). Core reads into package ScenePhysics* value types on the Scene (the lights/cameras spec precedent), satellite turns them into ordinary addBody/connect calls. Every attribute name was verified against pxr/usd/usdPhysics/schema.usda, and the schema drove the design: a collider's shape is the prim's own geometry (no shape attribute; Cube.size default 2, Capsule height = span between cap centers = Ollin's meaning); USD stands Capsule/Cylinder/Cone on z (axis, default "Z") so the turn onto y is baked into the shape's pose; a rigid body owns its whole subtree so several collider prims fuse into one .compound; a collider with no body over it is scenery; and a joint naming one body holds it to the world, which the bridge already supported (resolveBody → Body::sFixedToWorld for CJOLT_BODY_INVALID, documented in the header all along), surfaced as the new public connect(_:toWorld:). Load-bearing bug: a collider prim that is its body has an identity local transform, so shape scale must come from the collider's world transform (the body-relative one supplies position/rotation only, measured against the body's rotation+place, not its scale) — taking it from the local transform silently drops every scale, the common case. Out of scope (said, not faked): articulations, drives + limit API, collision groups/filtered pairs; upAxis/metersPerUnit stay unapplied (the scene-import contract). Example 3D/Physics/Imported (hand-authored yard.usda); UsdPhysicsTests (14); Docs § Reading physics from a file; Guide Ch 30 § Scenery and bodies from a file: scenes and USD physics. Cloth a skeleton carries (addSoftBody(…, skinnedTo:carriedBy:sway:backStop:maxStretch:) + SoftBody3D.follow(_:) / snap(to:) / live swayScale / followsSkin): the solver's skinned constraints turn a hanging sheet into a cape, carriedBy: naming the joint that carries each vertex (mesh-local, the pinned: space) and follow(_:) handing over this frame's pose. Load-bearing: the bind pose is whatever the figure is standing in at build, so no weights are authored anywhere (invBind = jointWorld_bind⁻¹ · placement) — and since placement · invBind⁻¹ is that bind transform back exactly, a restored surface with no scene in sight opens standing in the pose it was hung in rather than collapsed on its origin (only the translation is divided by unitsPerMeter, which is the conjugation S⁻¹[R|t]S, so bind and pose compose in meters as they did in units); pinned: means held by whatever holds it (a joint carries it → held to the figure, none does → held to the world, the shipped meaning); a particle held exactly on the skin (sway 0) is made kinematic in the bridge, which is what lets maxStretch: (the LRA long-range attachments, driven at GeodesicDistance) work from anchors the skin holds — measured: a 6 kg cape hangs 8 cm long on its springs and exactly at rest length capped; sway/backStop are raw world-unit lengths, a deliberate departure from the stage-8 rule (that rule is about compliance; a distance needs no normalization — probed 0.02→0.0202, 0.05→0.0504, 0.1→0.1009, backstop 0.02/0.1/0.4→0.0200/0.1000/0.4005), so watch the sign of a computed sway (negative clamps to 0 and hard-skins that part into a board); the back stop sits behind the normal the solver reads from the winding, so the faces it is handed cross reversed whenever the winding faces the carrying joints (windingFacesCarriers, decided from the rest shape and the inverse binds alone so a restored surface decides the same way with no skeleton present): "behind" is toward the figure whichever way the mesh is wound, and a cape whose sheet faced the figure used to pass straight through it (the plane winding fix of 2026-09-14 turned every written cape that way; theBackStopHoldsWhicheverWayTheSheetIsWound pins both windings); follow records and World3D.step applies (once per step, the interpolation the solver eases across), snap(to:) applies at once (particlePositions/mesh are right immediately; the body's own center re-centers on the next step); a changed pose is what wakes the body — the skin constraints are solved during the update, so a settled sleeping cape is left behind by a figure that walks away (a real probe-caught defect), and comparing the flat pose against the last applied one is what still lets a still cape sleep; build order is load-bearing (skinned constraints + their inverse-mass zeroing before CreateConstraints, which derives the LRAs; CalculateSkinnedConstraintNormals before Optimize; both no-ops when nothing is carried, so every existing soft body is untouched). The snapshot writes the skin down rather than naming a scene (the closures are a sketch's, the pinned list precedent; ~4.7 KB for a 169-particle cape, no new PhysicsAsset case; format version 3). Envelope: still no self-collision, so a cape passes through its own folds. Ropes (Rope3D.swift): world.addRope(through: [Vector3], thickness:sides:mass:stiffness:bend:damping:friction:bounce:iterations:pinned:maxStretch:group:) builds the solver's fifth constraint family, Cosserat rods (Kugelstadt/Schoemer 2016), as a Rope3D: SoftBody3D whose particles are the polyline's points one for one (identity welding, so pin/move/positions/nearestVertex speak in point indices) and whose mesh override sweeps a Mesh.tube, so drawSoftBody works unchanged; the readback is segments (RopeSegment3D: index/start/end/center/direction/length + a rotation the rod itself carries) posed by the sketch sugar withSegment(_:) (withBody's twin, local +y along the rope matching Ollin's y-standing primitives, +x the twist), plus points/length/restLength/segmentCount and live thickness/sides. Load-bearing: a rope is the first body with no faces, so cjolt_soft_body_create's mFaces.empty() guard now asks for faces only when there are no rods, and the rods must go in before CreateConstraints (which derives the LRAs by walking vertices, not faces, and CalculateClosestKinematic walks rods beside edges → maxStretch: works faceless, measured 1.80 → 1.00); the bridge undoes both things the solver does to the rod list (Optimize() reorders, CalculateRodProperties() reverses a rod pointing against its neighbor) via a per-rod remap + flip recorded pre-Optimize, so segments is in the caller's order and direction (a reversed rod's rotation times a half turn about its own x); bend needed its own measured scale (a bend constraint holds a rotation, so it carries no length to compare against): compliance ∝ meanRod²/(length³·mass·gravity) with a measured constant puts bend: 0.5 at drop/span 0.26…0.33 across 12× length, 2× particle count, and 100× mass, where the naive scale ran from 0.13 to 0.92 over the same lengths — and both sweeps are non-monotone until ~3000 steps (a still-swinging cantilever measures its swing); iterations is the lever on a long finely-divided rope (40 particles over 6 units: drop/span 0.40 at 5 passes, 0.027 at 20); restoring needs the rod frames or the rope springs (0.361 units of drift one step after an exact restore, vs 0.0014 carrying them), and the way in is that RodStretchShear::mBishop is read exactly once at runtime (Initialize, to seed each rod's state) with mLength/mOmega0 already computed, so the bridge writes the saved orientations over it after CalculateRodProperties and before Optimize; a rope therefore needs no assetName to be saved (its rest shape is a handful of points, format version 4). Envelope: no self-collision (a coil passes through its own turns), and SoftBodyShape is face-based so a rope is invisible to raycast/sweep/bodiesOverlapping (pinned deliberately) while still colliding with the rigid world through its particles' vertexRadius — grabSoftBody falls back to the nearest particle along the line of sight; one strand only, no branching. Example 3D/Physics/Rigging; Rope3DTests (20); Guide Ch 31 § A line that knows how it is turned: ropes + Rope figure. Example 3D/Physics/Cape; SkinnedCloth3DTests (15). Examples 3D/Physics/Stack / Tumble / Chain / Yard (a truck, a pacing figure, a fallen one, and a named heightfield floor + cloth banner, all kept in one file) / Crawler (a machine on tracks, turning without steering) / Windmill (motors + the compound cross) / Rockslide (heightfield terrain + eroded slope) / Trigger (a scoring hoop = solid bead rim + sensor disc, a sensor tray lit by its load, knock rings scaled by contact speed) / Stroll (a walkable eroded island: stairs to a sensor-lit lookout, shoveable crates, stepHeight as a live @Param) / Joyride (a car over an eroded island: chase camera, live gearing/springs/grip parameters, wheels colored by slip) / Ragdoll (a skinned figure that stands and waves powered, collapses limp, and drags by an arm either way; B shows the fitted capsules) / Drape (a banner pegged to a washing line flapping in a gusting wind, a sheet thrown over a crate, a deflatable beach ball, all draggable) / Cape (a figure striding with a cape clasped at the neck, carried by the skeleton under the skin; it collapses with the figure and the leash can be cut live) / Rigging (a limp rope, a chain whose links interlock because every other one is rolled a quarter turn about the rope's own axis, and a leafy vine stiff enough to hold a curve, all hanging in the same gusting wind) / Flotsam (crates from cork to nearly waterlogged floating on a swell at their own depths, a stone anchor on the bottom, a current carrying the lot past, all draggable) / Sightlines (a lamp lighting only the crates its rays reach, a drone holding clearance by sweep, a pulse shoving what a sphere overlaps) / Sieve (three colors of bead sorted down one ramp by three ignoreCollisions calls, the rules withdrawable live) / Bagatelle (a pin table held flat by .plane(), live parameters for freedom / weight / path-checking, and a plunger shot that leaves through a thin rail the moment sweeping is off) / Contraption (a workshop of the machines a hinge cannot make: a gear pair driving a rack off one shaft, a rope trading a tray for a counterweight, a platter allowed only to rise and spin, a cart on a track) / Raft (a cloth raft floating on a swell with cargo aboard, a sounding line that stops at her deck, a gate that reports her sailing through); Docs/Simulation/Physics3D.md; Guide Ch 30 §§ Things with weight: World3D and bodies (CrateFall figure) + Machines out of joints: motors, gears, and racks (Windmill figure) + Things that pass through each other: collision groups (Sorted figure) + What hit what: contacts + A region that counts what is inside: sensors (Trigger figure) + What is in the way: rays, sweeps, and overlaps (Sightlines figure) + Taking a direction away: degrees of freedom (Flattened figure) + Water, and what it holds up: buoyancy (Floating figure) + Ground from a landscape: the heightfield collider (Rockslide figure) + Keeping what settled: snapshots (Kept figure); Guide Ch 31 §§ Someone to be in there: the character controller (Walker figure) + Something to drive: the vehicle (Joyride figure) + Letting a figure fall: the ragdoll (Ragdolls figure) + Cloth that finds its own shape: soft bodies (Cloth figure) + A cape on someone's back: cloth a skeleton carries (Cape figure) + A line that knows how it is turned: ropes (Rope figure) + A raft made of cloth: soft bodies that float (Raft figure) + Turning without steering: tracks (Crawler figure). Mechanism: ARCHITECTURE.md § The 3D physics bridge (+ §§ Cloth a skeleton carries, Saving a world). What stays out is the solver's own gap: soft-body self-collision and cloth-vs-cloth, and tearing it cannot do at all (DESIGN-NOTES.md § 3D mode). Volume constraints (tetrahedral jelly) are declined on measurement, not absence — built and probed against the shipped pressure, they tie on holding a weight and on dent recovery and lose on recovery from a hard squash, because ApplyVolumeConstraints forms its residual from an unsigned volume so an inverted tetrahedron reads as satisfied, where a gas law's push grows without bound as the space shrinks; the win they do have (holding an authored shape with corners) belongs to the interior springs, not the tetrahedra, and bend closes most of it anyway. Numbers + the two reopeners in ARCHITECTURE.md § Why there is no tetrahedral solid. The Jolt/Physics/Hair/ module is likewise declined on measurement (2026-08-06 audit, apparatus reverted): it takes no force of any kind (so it cannot be blown, where every Ollin cloth/rope example gusts via applyForce), collides only with ConvexHullShape (silently skipping the meshes and capsules Ollin's figures are), and needs an authored groom + skinned scalp where a sketch generates its own geometry. Decisive number: the already-exposed rod family carries 20,000 simulated strands at 12.45 ms/frame in ONE soft body (nothing requires a body's rods to form one chain) against Hair's ~2,500 on its shipped CPU backend, so the Metal path it would need (49 shaders cross-compiled to a Jolt.metallib build step, Objective-C++ in the C++-only CJolt target, a new draw path for a Jolt-owned buffer) buys a strand count Rope3D already reaches. If hair is ever wanted, the way in is a multi-strand Rope3D, not this module. Reopeners + numbers in ARCHITECTURE.md § Why there is no hair tier. A number the solver cannot hold is refused at the bridge (SolverNumber, one definition for a snapshot's numbers and a sketch's own: finite, within a billion units, a rotation long enough to be made a unit one, an axis of some length): every setter, force, step, and creation argument of both worlds checks there, a setter keeping what it had and a creation argument falling back to its default with a one-time note naming the call (a collider carrying one becomes a unit sphere; a joint with one is handed back inert and is not the world's), the throwing creators (addVehicle, addSoftBody, addRope, addRagdoll, addTensegrity) throwing PhysicsError.unbuildable naming it, and .infinity passing only where a limit means none (a motor's strength). A Rotation3D never carries one (its initializer makes a unit rotation of anything else), so that check is a backstop. Before the check, 28 of 37 probed cases stopped a debug build (the 3D solver asserts one step later, in its integrator, since JPH_ENABLE_ASSERTS follows JPH_DEBUG; Box2D asserts at the call) and poisoned a release build's broad phase; SolverNumberTests and the two cases in RigidBodyTests pin every family. 2D applyTorque takes point units, a force in points times an arm in points, scaled to the engine's newton-meters by the square of unitsPerMeter, so the numbers that push a body also turn it (RigidBodyTests.aTorqueSpinsInPointUnits against the box's own inertia).
  • Tensegrity (Sources/Ollin/Geometry/Tensegrity.swift + Sources/OllinPhysics/Tensegrity3D.swift). Tensegrity is the geometry (nodes + Member struts + Member cables) with three balanced forms: prism(struts:radius:height:twist:) (twist defaults to prismTwist(struts:) = π/2 − π/n, Connelly & Terrell's result, independent of height and radius; struts run bottom i → top i+1 with the top turned by +twist, the handedness the node force balance requires), icosahedron(strutLength:) (Jessen's icosahedron: the cyclic permutations of (±2, ±1, 0) at strut length 4; struts are the 6 edges of length 4, cables the 24 of length √6; parallel pairs sit L/2 apart, the a = 2b ratio at which equal cable tensions balance), tower(levels:struts:radius:levelHeight:) (class-2: prisms on shared polygons, alternating twist). imbalance is a least-squares equilibrium fit (every strut at force density −1, cable densities solved, ridge 1e-9): ~1e-9 for the forms, 0.47 for prism(twist: 0.2). No CPU form-finder ships, on purpose: with equal cable force densities a prism has no finite equilibrium (the radial balance needs q_polygon/q_riser ≈ 0.577 at n = 3, so it shrinks to a needle), and with rest-length springs a wrong-twist prism is an isostatic framework that never moves; a settle() was built, measured, and removed. Physics: World3D.addTensegrity(_:at:strutRadius:prestress:stiffness:density:friction:group:) → Tensegrity3D: a capsule body per strut (height = L − 2r, r ≤ L/4, local +y from node a to b), a .cable joint per cable at length = L × (1 − prestress) (default 0.02), a .ball per shared node; source is the geometry as built (zero-length struts, cables to a node no strut reaches, and that node are dropped and the indices renumbered, so source.cables aligns with cables one for one); nodes are read live from the strut poses. .cable(from:to:length:stiffness:) is the new JointKind3D: the same solver distance constraint as .distance with limitMin = 0 (slack below length), snapshot tag 9. Measured (2026-09-06): prism, icosahedron, and tower all stand from a 0.3 drop (the top keeps 95%+ past the lift; cables at 1.00…1.03 of rest, none slack; asleep in 0.7 s / 6 s), the same struts without cables collapse to 12%; a prism(twist: 0.2) turns to 0.58 rad (balanced 0.524) as it settles under prestress, so the wrong twist is a demonstration, not a fold; a tall mast dropped on a 0.12 rad lean falls over (set masts down straight, which the example and figure do). Snapshots carry the tier: format 5 appends a tensegrity section (source, strut body indices, cable and shared joint indices, rest lengths, anchors) after the soft bodies; the reader accepts format 4 (oldestReadableVersion) and reads an end-of-payload as "no section"; madeJoints is index-aligned with the saved joints (nil for a dropped body) so a tier can name joints. Tensegrity3DTests (12) + TensegrityTests (7, pure). Example 3D/Physics/Tensegrity; Docs/Generators/Tensegrity.md; Guide Ch 30 § Standing on cables: tensegrity + the Tensegrity figure.
  • Computer vision (OllinVision satellite). A Camera (or any FrameSource) feeds VisionAnalyzer (drops frames it can't keep up with; the display frame is never dropped) and composable trackers over Apple's modern Swift Vision API. An export reads its detectors on its own clock (ExportClockTests; before it, seven examples exported a different picture each run and most of the rest only matched because neither run had an answer yet: DepthCloud never got past loading, BodyPose showed nobody): a frame offered on the main thread under OllinApp.isExporting (--bench keeps the live path) is analyzed before submit returns, every tracker, each one's prepare(for:) awaited first (the model trackers' loads, ConceptTracker's pending phrase encodings, BodyTracker's one warm-up per process, since the cold model answers with nobody), after any analysis the live path started (the inFlight group is entered under the lock that claims an analysis, so the export's first frame either waits for it or comes after it); the main thread parks on a semaphore there, which is sound because it is no cooperative worker and nothing those analyses or loads await runs on the main actor (read per tracker; a DepthTracker-style load through ModelLoader proved it under the release product); the wait includes a model the system compiles on first use (text recognition's Neural Engine compile held a cold release product about 40 s, and once under media.sh's load past its 300 s limit, so re-run a vision example that fails there warm before suspecting it); a tracker registering during an export reads the source's newest offered frame at once, so a lazy tracker has its answer on its first draw; StillFrames is FrameAdvancing, publishing rate times a second of sketch time from the first frame (reached within 1e-9, since fifteen sixtieths sum to just under a quarter) and nothing on a held clock, its thread silenced under the lock it calls the tap in (set by start(), a tap installed, or the first advance under an export; a window's advance hands it back); VideoPlayer hands its tap each new frame under the virtual playhead, the pump having nothing while the player does not play; a frame source of the sketch's own steps itself from draw() under isExporting (TrajectoryTracking); TrajectoryTracker stamps an export's frames a thirtieth apart (the rate detect(across:) assumes), starting its request over when the clock changes, and is still not exact, since Vision's trajectory fit varies a few millionths run to run on identical input (held back as NOT_EXACT in check-determinism.sh); SourceAnalyzers matches the source itself beside its address, and the still feed's thread holds the feed weakly and ends with it, because that thread kept a dead feed's analyzer alive through its tap and a new feed at the same address was handed it and never tapped. Nineteen trackers: FaceTracker / Face (76-pt landmarks + roll/yaw/pitch), ContourDetector (camera → vector Shape), HandTracker / Hand (21 joints), BodyTracker / Body (2D pose; detect(in:) asks again on an empty answer until this process has seen one pose, at most three times, because Vision loads the 2D body-pose model on the first request that needs it and answers everything arriving meanwhile with no observations at all, so a sketch reading a still was told the picture held nobody. Measured in fresh processes, first call nothing and second all 19 joints, every time, and reproduced later than the first call under a parallel test run; once any picture yields a body an empty answer is believed, at no cost. No other request here does this, BodyTracker3D included, so the retry stays put), RectangleDetector, BarcodeScanner (each DetectedBarcode carries a typed Symbology — a RawRepresentable struct of static constants, so a kind the system learns later still arrives under its own raw value), TextRecognizer (OCR), ObjectTracker (lock + follow; ObjectTrackerTests pin a textured 64-px square moving five pixels a frame within 3 px at the worst of sixty frames and 1.5 on average (measured 2.7 and 1.1), the confidence above 0.85 while it is whole and under 0.1 once it is gone, the box frozen where it was last seen, so the confidence is the sign of a lost patch, and forty re-seeds through the live path each followed; Examples/Vision/ObjectTracking takes its first lock from a FaceTracker box on the bundled film), TrajectoryTracker (parabolic arcs, predicted), FlowTracker / MotionField (dense optical flow; the field value lives in the core, Core/MotionField.swift, built from a read closure, so the phone's flow stream produces the same type and a source of your own can too), PersonSegmenter / SubjectSegmenter (matte + cutout Images), PointSegmenter (whatever you point at, lifted — point-prompted segmentation over a fetched three-part model: pick(at:in:) freezes the frame and encodes once (~0.2 s, .cpuAndGPU by measurement — the Neural Engine spends ~9 s specializing these graphs at load against under a second on the GPU pair, warm speed equal), include/exclude refine against the cached encoding in ~10 ms, three candidate readings with the best score winning; the mask's logits map to a soft rim — byte = 128 + logit·64 — so the upsampled edge lands anti-aliased with alpha 50% on the model's own boundary; prompts and frame are both mapped onto the model's 1024² square squashed, never letterboxed, so a prompt is fraction × 1024 on each axis), ImageClassifier, SaliencyTracker (heat-map + regions + point query), BodyTracker3D / Body3D (17 joints in meters; canvas / model / camera space), ModelTracker (any Core ML model — classifier labels / detector boxes / image map / semantic-segmentation ClassMask, decoded by observation kind), and ConceptTracker (open-vocabulary scoring — typed phrases as parameters: concepts/labels/confidence(of:) softmax shares + similarity(of:) raw cosine + the imageEmbedding/embedding(of:) unit-vector surface, over a fetched contrastive image-text encoder pair), and DepthTracker (depth that holds still: temporally consistent relative depth over any FrameSource, one frame at a time, from a fetched video depth model whose 32-frame temporal-attention window is kept in Core ML state (eight MLState caches of 42 slots, the upstream streaming policy: slot 0 the session's first frame as a permanent anchor, slot 1 an aging second anchor, the last 29 the newest frames), so the tracker feeds one image a frame and reads one map; map / value(at:in:) read 0 far … 1 near through a range that follows the scene (range, the 1st and 99th percentiles, out halfway at once and back 2% a frame) so the picture never re-scales frame to frame; a session's first frame is read alone and anchors the depth scale, reset() starts a new one). DepthTracker invariants: the package is built, not downloaded (Scripts/convert-video-depth.sh, called by the fetch script, since nobody publishes a Core ML build: the converter traces the upstream streaming step with the cache as model state, a slice assignment being the one in-place form the converter turns into a state update, and refuses to write a package that disagrees with the upstream head's own cached forward pass, measured 1e-5 relative in PyTorch and 1.6% max / 0.1% mean in fp16 Core ML); the frame's state must be opened from a package loaded from disk (an in-memory converted model cannot make state); compute units are pinned .cpuAndGPU, since the Neural Engine fails to build an execution plan for the graph (error -14 at load) and the CPU alone takes twice as long (68 ms a frame on the GPU against 140 ms, M2); the reading settles over the first 31 frames even on a still scene (the first frame's cache comes from a self-only pass and the window then fills with cached-path entries), so a stability test warms up past the window; the analyzer's one-at-a-time frame drive is what keeps the state in order (never run two predictions on one session); the frame is squashed onto the fixed 518×392 input (a fixed shape is what makes the position embedding a constant, resolved once at conversion); relative inverse depth, not meters, so no RGBDFrame from it until a metric checkpoint clears its license; only the Apache-2.0 Small checkpoint is scripted (Base/Large are CC-BY-NC, and the converter takes them only by hand). DepthClip is its other half (the whole clip's depth, read ahead of time): a recording through the model's published inference, the 32-frame window forward pass (VideoDepthAnythingSmallClipF16.mlpackage, the converter's second product, 32 image inputs frame_0…frame_31, one (32, H, W) output, checked against the upstream class's own forward), scheduled the way the upstream infer_video_depth schedules it, kept on disk, and answered by clip time; bound to a VideoPlayer through the core ClipPlayback seam (url + currentTime, the one thing an analysis over a whole file needs from a player) map / value(at:in:) answer for the frame under the playhead and map(at:) / value(at:in:time:) for any second, and under a headless export the pass runs before the first frame, so exported frame k carries the map of the clip frame it shows, which the live tracker cannot promise (a tracker over a player analyzes nothing headless). DepthClip invariants: the scheduler is the published one to the letter (22 new frames a window; slots 0–9 refilled from the previous window's keyframes [0,12,24…31]; the fit a least-squares scale + shift of slots 0–1 against the clip's first frame and the previous window's keyframe 12; the 8 overlapping frames blended linearly from the older reading to the newer; every aligned value clamped at 0; the clip padded by repeating its last frame so the last window is full, the padding's maps dropped) and it is checked against the upstream pass itself: the converter writes VideoDepthClipReference.bin, the upstream infer_video_depth (fp32, input_size set to the package height so its resize is the identity) over a 60-frame clip drawn in whole numbers only (so the Swift test draws the same bytes with no shared random generator), sampled on a 32×24 grid, and thePassMatchesThePublishedInference compares the whole Swift pass against it (measured 0.39% max / 0.036% mean of the depth spread, three windows with the fit and the blend between them; the window package itself reproduces the upstream forward to 0.00000 in PyTorch and 1.0% max / 0.03% mean in fp16 Core ML, 1.23 s a window on an M2), while the always-on scheduler test drives a distorting stand-in runner (each window read through its own scale and shift) and was verified red by sabotage of the fit (max 2.03 with it removed); the pass runs on a plain Thread, never a cooperative worker (minutes of synchronous Core ML), and inline on the main thread under isRenderingHeadless, so create it by the end of setup(); its result is a mapped .depthclip file (a 64-byte header, half-float planes, the frame times at the end) under Caches/Ollin/DepthClips, keyed on the clip's and the model's path/mtime/size stamps, staged and atomically renamed, so a clip's second run opens at once (the 28 s bundled clip: a cold --export-sequence ran 1:34 with the pass, the warm one seconds, and the two runs' frames were pixel-identical, PNG encoder bytes aside); the frame at a time is the last one starting at or before it, the rule the headless player decodes by, so the map and the picture come from one frame; the range is fixed for the whole clip (1st/99th percentiles over a 1-in-7 sample), never followed; a keyframe's converted image input is reused across windows (identity-keyed), so each frame is resized once; compute units .cpuAndGPU like the streaming package; frames squashed onto the same 518×392, so the pass runs at the tracker's resolution rather than the upstream default's 518 short side (a --width/--height rebuild is the way up). ConceptTracker invariants: scores are softmax over 100×cosine, relative to the concept set — one phrase alone always reads 1, so contrasts are the intended use; text embeddings cache per lowercased phrase (one encoder run each, drained in background — both inits must queue the initial concepts into pendingPhrases, the live path published nothing until they did); compute units are pinned .cpuAndNeuralEngine — the GPU graph compiler SIGSEGVs specializing this model family's attention blocks (an MPSGraph attention-fusion pass, crash mid-MLModel.load), and ANE+CPU runs both encoders correctly; the tokenizer (PhraseTokenizer) is Ollin's own byte-pair-encoding implementation, golden-tested token-for-token against the reference (multi-byte UTF-8 + emoji included — merges file line 0 skipped, first 48,894 merges, ids: 256 stand-ins / +</w> forms / merges / start 49406 / end 49407, context 77, truncation re-pins the end marker); the fetched pair is the 2024 permissive-license release — the 2025 follow-up model is research-only and must never be pointed at by the fetch script; ModelTracker.compiledModelURL stages its cache copy and atomically renames — two trackers loading the same model concurrently read a half-copied .mlmodelc otherwise (hit live: paired encoders always double-load); and every tracker's Core ML load goes through the serializing ModelLoader, never MLModel.load directly — concurrent loads of the same compiled model directory race in the framework's GPU program preparation and some instances come back silently wrong (measured: four concurrent loads of one compiled path, three corrupt masks, nondeterministic; loads serialized with predictions concurrent, all four byte-identical). Each also runs one-shot on a still Image via detect(in:); sync contexts (setup(), deterministic figures/exports) run those inline via waitFor(image) { try await …detect(in: $0) } — the stills are passed as arguments (not captures) because Image isn't Sendable, it parks the caller (never call it from an async context), and the @Sendable closure type is what keeps the work off the caller's actor (a plain closure would inherit main-actor isolation and deadlock the parked main thread). Coordinate mapping is the crux: VisionSpace (+ each result's bounds(in:) / landmarks(_:in:)) maps Vision's normalized lower-left origin → Ollin's pixel top-left canvas (y-flip + rect-scale + a mirrored: selfie flag) — map results into the same camera.fittedRectangle(in:) you drew the frame into, and use Apple's per-result *InImage helpers (never hand-roll the conversion — it over-spreads landmarks). Feeds draw via drawFrame(_ feed:in:waiting:). A frame is an Image, so it goes wherever an image goes: globe.textured(camera.frame) maps the feed onto a mesh and environment(.feed(camera)) lights by it, with no API of their own; the material is set every frame (each capture is a fresh Image) and a fresh Image builds its texture on first draw, so a feed on a surface costs one upload per new frame and nothing while the frame holds (LiveSurfaceTests.aFeedFrameOnAMeshFollowsTheFeed pins that the fresh image reaches the surface rather than a stale texture; example 3D/Materials/LiveSurface; Guide Ch 34 § The room as the surface: textured). Hardware split: classical CV (rectangles / barcodes / contours / object-track / trajectory / optical-flow) and OCR run on any Mac; the neural models (body pose, segmentation, saliency, classification, 3D pose) need Apple silicon — surfaced via isAvailable / unavailableReason. Gotchas (pinned by probe): trackers bind to any FrameSource via the core FrameSource seam + a per-source shared analyzer kept alive by the tap closure (formed in a free function — the render-thread rule); point queries on single-channel buffers (value(at:in:)) are clampless nearest-neighbor that trap past the last pixel — clamp to (n−1)/n, and they take top-down fractions so flip y before the lookup; InstanceMaskObservation.allInstancesMask holds labels, not a soft matte (use generateMask); a Core ML model must load from a stable compiled path or it re-specializes (~10 s) every launch; DetectTrajectoriesRequest only works through timestamped CMSampleBuffers. Model weights download via Scripts/fetch-models.sh (never committed; StyleMirror's is user-trained); a new model example follows the standing delivery rule — weights fetched into the gitignored Models/, the example shows a run-the-script notice until they exist, provenance in THIRD-PARTY-NOTICES.md, real-model tests soft-skip where weights are absent — and the catalog is deliberately bounded: plain ImageNet classifiers duplicate ImageClassifier's ~1,300-label vocabulary, and the modern Ultralytics YOLOs (v8/26) are AGPL-3.0, never bundled and never scripted. Examples span Vision/WebcamFeed / FaceTracking / FaceAlign / ContourTrace / HandTracking / BodyPose / RectangleScan / BarcodeReader / TextScan / ObjectTracking / TrajectoryTracking / Lift / PointLift / OpticalFlow / SceneLabels / EyeCatcher / BodyPose3D / DepthRelief / DepthContours / FootageDepth / ObjectDetection / DigitReader / StyleMirror / PaintByClass / TugOfWords. Docs/Vision/Vision.md. Failure shape (2026-09-22): one VisionError (unavailable, the sentence unavailableReason holds, thrown from a call in hand; failed, a run that could not produce its result) replaces the five nested Error enums; PersonSegmenter.detect and SaliencyTracker.detect return their value and throw failed on a matte or heat map that will not convert, SubjectSegmenter.detect is nil only for no subject; the depth tracker's own model failure lands in unavailableReason instead of being dropped as a transient.
  • A feed with no camera behind it. StillFrames(_ picture: Image, rate: Double = 4) (core, Sources/Ollin/Core/StillFrames.swift) publishes one picture as both a VideoFeed (so drawFrame draws it and never waits) and a FrameSource (so any tracker taps it); OllinVision's Camera.orStill(_:rate:) picks a running camera where there is one and a still where there is not, and --photo takes the picture even where a camera would have worked, which is how a still is made of a live sketch; under --photo every Camera.start() throws before touching the device, so a sketch that starts a Camera itself never films the room either (four examples started one directly, so under --photo they drew the machine's own camera; they now take Camera.orStill). It republishes rather than publishing once (4/s by default, far under a camera's rate) because VisionAnalyzer drops frames it cannot keep up with, so a single published frame could be the one dropped; it also hands the picture over synchronously the moment a tap is installed, so an analyzer attached later is not blind for a quarter second. The picture is an @autoclosure, so a machine with a camera never decodes it. A headless --export shows the feed but not the overlay: the export renders frames as fast as it can rather than in real time (measured: frame 60 and frame 900 both take about four seconds, all of it launch), so nothing that needs a background analysis to land has time to. A live window has that time, and a still's one-shot analysis for a figure is waitFor + the static detect(in:) instead. 14 Examples/Vision sketches fall back this way (which photograph each takes follows what it reads); Docs/Vision/Vision.md § When there is no camera; Guide Ch 34 § When there is no camera: stills and footage.
  • Geometry & color. Vector2 / Vector3 over one shared Vector protocol (the constants, lengthSquared/dot, the operators, and with(x:)/with(y:) are the requirements; length, normalized, distance/distanceSquared, lerp, limited, projected, the scalar-on-the-left * and the four in-place operators are written once in the extension, and Collection.centroid now takes either element. The extension is @inlinable and must stay that way: a sketch calls these from its own module, where an unspecializable generic body measured +66% on a steering loop (9.19 ms concrete, 15.22 ms plain protocol, 9.37 ms inlined, four-module release A/B); marking the concrete primitives too buys nothing. The move is byte-identical, which is the point: length reads lengthSquared.squareRoot() over the same sum, and lerp is self + (other - self) * t, the same three operations in the same order as the old component-wise form. What stays per type is what does not generalize, cross above all: a Double in 2D, a Vector3 in 3D, which is also the whole of the 2D answer sitting in the 3D one's z; Vector2 keeps angle, perpendicular, the signed angle(to:), rotated(by:) and the polar initializer, Vector3 keeps unitZ, xy, and rotated(by: Rotation3D). VectorProtocolTests (8) pins the generic reach, an outside type conforming with nothing but the requirements, and the exact arithmetic over 300 awkward triples per invariant, sabotage-verified: the equivalent self * (1 - t) + other * t fails 400 of 600 expectations. The types that are a list of things say so, as RandomAccessCollections beside Palette and Table: Contour over its points, Shape over its contours, Grid / HexGrid / TriangleGrid over their cells, Polyomino over its squares. Two invariants. A conformance must not quietly redefine a call that already means something else: contour.contains(p) would have become a vertex-membership test beside Shape.contains(p)'s geometric one, so Contour gained its own geometric contains(_:) (the concrete member wins over the protocol extension, and removing it turns the test red rather than ambiguous). A grid's element is worked out as it is asked for, so iterating one allocates nothing and its subscript carries its own bounds check (nothing else would trap, and grid[-1] would otherwise answer for a column left of the grid), and cells is now Array(self) so the row-major order has a single definition; that makes Array(grid) == grid.cells tautological, and the order is pinned against a walk written out in the test instead. Contour also reaches Collection.centroid (the points' mean, which is not Shape.centroid's balance point of the filled region; both are documented against each other). CollectionConformanceTests (15), both sabotages red. What was weighed and refused is in DESIGN-NOTES under the idiom track: Path (commands, not points), Rhythm (count would repeat length), Ramp (reversed the property beside reversed() the method), and the documents with several parallel lists. Gotcha, the second time it has cost a build: Sequence conformance shadows the global two-argument min/max inside the type's own extensions, a hard error at every bare call site. Docs/Drawing/Geometry.md, Docs/Drawing/Tiling.md, Docs/Generators/Polyominoes.md), Box3 (an axis-aligned box as one value: min/max corners with center / size / longestSide / isEmpty / contains(_:), padded(by:) and union(_:), init(center:size:) and the optional init(containing:); what Mesh.bounds, Scene.bounds, Metaballs.bounds, ShadowArt.bounds, and the phone room's PhoneSceneMesh / PhonePlanes report and what isosurface(in:) / shadowArt(in:) march over, in place of the (min:max:) tuple those twelve sites each spelled; Box3Tests), Rotation3D (a turn in space as one value: a unit quaternion read as angle/axis, built from an angle about an axis, the shortest turn from:to:, or raw quaternion parts; a * b is b first, then a, matrix order; inverse, interpolated(to:_:) along the shortest arc, matrix for transform(_:), Vector3.rotated(by:), and rotate(_:) on the sketch; one form per turn, w kept non-negative so equal turns compare equal however built; it is what Body3D.rotation / Wheel3D.rotation read and set, with the rotationAngle/rotationAxis pair and setRotation(_:axis:) now sugar over it, and what withBody/withWheel pose through; Rotation3DTests (10), RigidBody3DTests for the body), Ray3 (the pointing value: an origin plus a direction kept at length 1 in the initializer, so every distance a hit reports is a real distance and point(at:) reads as "this far along", with distance(to:) / distanceAlong(_:) and hit(sphereAt:radius:) / hit(boxAt:size:) / hit(planeAt:normal:); only what is in front of the origin counts, a ray starting inside a ball answers the far side rather than handing back something behind the hand, a slab the ray runs parallel to is judged by whether it sits inside it rather than by a division, and a zero direction answers nil everywhere instead of NaN — Ray3Tests (11), sabotage-verified; the 2D Ray2 under envelope is a family of lines, not a pointer, and leaves its direction as given), Rectangle (+ the Rectangle(fitting:in:) letterbox everything shares, contains, inset(by:), the normalized point(u:v:)/uv(of:) pair — canvas sugar Sketch.uv(_:_:), example Basic/NormalizedCoordinates), Circle, Shape / Contour (arc-length walk helpers + even resampled(spacing:), on both), the Path curve builder; per-frame transform stack (translate / rotate / scale, scoped via withState {}; the withState(at:rotation:scale:) / withState(at: Vector3) forms build the placement in — fixed order translate → rotate → scale, origin at the point, byte-identical to the spelled-out block). drawSheet(_:columns:gutter:_:) lays labeled (label, item) pairs into a near-square gallery (label plates on the current textFont, state untouched, near-square column count by default) — the layout the filter-family examples each carried a private copy of. Ramp(stops: [Ramp.Stop]) is the typed twin of the tuple list and ramp.reversed mirrors every stop about ½ (built over stops.reversed() so a hard edge keeps its colors in mirrored order; PaletteTests). Heightfield.image(_ ramp:in:) paints each sample through a Ramp (pixels equal the ramp's own colors, Convenience3DTests) and coloredMesh(...) hands back the terrain mesh with it already applied (build on field change, not per frame). sketchResource(_:in:from:) finds a folder-beside-the-sketch file by walking up from the caller's #filePath (a free function so a static let can call it; nil when no folder is found — for bundled resources keep Bundle.module). Color reads back as hue / saturation / brightness (public inverse of init(hue:...); a gray reads hue 0; example Color/Mixing, which labels each band's midpoint that way), mixes as the instance mixed(with:_:in:) beside the static Color.mix, and Colormap.color(cycling:) wraps by whole laps where color(at:) clamps (a Palette already wraps; most maps still seam at the lap — CosinePalette is the seamless ring); example Motion/Mandala. Palette is a RandomAccessCollection (startIndex/endIndex over the existing wrapping subscript, which already answers correctly for every index a collection asks about), so it iterates, maps, and takes randomElement(using:); it stays read-only because a wrapping index makes a setter ambiguous. The conformance's one cost is not obvious: Sequence puts min()/max() on the type, which shadows the global two-argument min/max inside Palette's own extensions, so three call sites in Palette.swift/PaletteExtract.swift had to become Swift.min/Swift.max (a hard compile error, not a silent one). PaletteTests (2 tests, one pinning that iteration does not inherit the wrap). The generator/technique catalog below is terse invariants + pointers only — mechanism, history, and the example/snapshot/test pointers live in ARCHITECTURE.md § The geometry and generator catalog + § *Color: palettes, dithering, and print separations* (user-facing API in the named Docs/ pages), and new catalog depth routes there, not here. Three catalog-wide rules: determinism is a promise — no rng decision or position-mutating pass ever walks a Swift Set/Dictionary directly (sorted candidates, fixed iteration orders; the WFC rule, shared by mazes / space colonization / boids / the physics resolver); fit a figure by scaling its points, not scale() (the CTM scales stroke width too; the trochoid/Lévy rule); and a geometry cost dial is an artwork parameter, never a RenderQuality tier — geometry is data a sketch reads back and the tiers resolve per-GPU, so a tier would change the piece (and make it machine-dependent), not its rendering fidelity; maxVertices / levels / resolution / erosion stay explicit numbers, with @Param + --export-sweep the tuning path (RenderQuality stays GPU-sampling-only; reasoning in ARCHITECTURE.md § Performance and the quality dial). The catalog: Grid — typed columns × rows over a Rectangle; indexed points/cells elements (indexed cases are a single loop); .center/.spanning; padding/gutter over typed Insets; element-type design history settled, don't re-litigate. Shape booleans & offsets (vendored Clipper2, BSL-1.0) — offset output must be SimplifyPaths'd or cascaded insets balloon; stroke as shape stroked(width:join:cap:) — the input is not normalized first (that would erase open paths); convex hull convexHull(of:) (monotone chain). SVG import — SVG + loadSVG / drawSVG / fitted(in:); control points transform before flattening; initial fill black, initial winding .nonZero (don't "harmonize" with Ollin's .evenOdd default); unresolvable url(#…) → mid-gray, never dropped; unsupported containers skip their whole subtree. Docs/Drawing/SVG.md. Fourier epicycles — Epicycles + drawEpicycles; terms: is a largest-first prefix; deterministic sort tie-break; point(at:) periodic; exact at full terms (EpicyclesTests). Classic curves — phyllotaxis / lissajous / rose (parity-dependent closure span, no retraced doubling) / hypotrochoid/epitrochoid (integer gears, exactly wheel/gcd laps) / superellipse (Lamé identity pinned per point; uniform-angle sampling, resampled(spacing:) for plotter-even outlines at extreme n) / supershape (2D Gielis mirroring Mesh.supershape's defaults + its clamp-to-4 radius guard; integer m or the curve doesn't close in one turn — a fractional m leaves a seam, caught in the example) / guilloche + Rosette (rose-engine rings: stacked sinusoid cams over evenly spaced bases, ring k rotated by k·twist — the twist is a pure rotation of the wave and stacked rosettes superpose, both pinned exactly in GuillocheTests; snapshot guilloche, example Patterns/Guilloche) / the damped-pendulum Harmonograph / spirolateral (the one walk in the set: steps 1, 2, 3 … order with a fixed turn after each, the run repeated until it comes home) / Chaikin smoothed(iterations:) (open contours keep exact endpoints) / the count-keeping moving average smoothed(neighbors:weights:) (SmoothingWeights.gaussian, sigma half the window, or .box; one point out per point in, same order; a closed window wraps and stops where it would meet itself, (n - 1) / 2; an open window mirrors through each end point (2 * end - p), so a straight even run is a fixed point and the ends are set back exactly; a 64-point circle shrinks by the kernel's cosine sum at every window, 1.9% box / 0.95% bell at 3 neighbors, pinned per window and both weights in MovingAverageSmoothingTests (8), red under sigma = the whole half-window, a box one point short, and a clamp-to-end boundary); all rng-free; examples Shapes/Superellipse + Shapes/Supershape. Docs/Drawing/Curves.md. The spirolateral's closing is arithmetic, decided before a point is placed: one run turns the walker by netTurn, so at a turn of tau/k it closes in k / gcd(order, k) runs and never when k divides order (the run then ends facing the way it set off, so every repeat lands further away in the same direction — closes false, center nil, drawn open). The whole figure is run turned about center once per repeat, which is the invariant the tests pin (plus the centroid landing on center, and the outline measuring repeats · step · order(order+1)/2). A swept turn angle never closes anything — only an exact fraction of a full turn does — so animate the order or the reversed set instead, the gotcha that made the first example draw twelve open walks. Example Patterns/Spirolateral; snapshot spirolateral; SpirolateralTests (11, two sabotages verified red). Guide Ch 16 § A walk that comes home: spirolaterals + the Spirolaterals figure. Envelopes & caustics (Geometry/Envelope.swift) — Ray2 + envelope(of:closed:) (the curve a family of lines leans on, found as the crossings of consecutive members; parallel neighbors never cross, so the answer is a list of runs and a parallel family gives back nothing) + LightSource (.point / .parallel) + reflectedRays / refractedRays (vector mirror law; Snell in vector form, with total internal reflection dropped rather than faked, so the ray array can be shorter than the curve) + caustic(off:from:closed:) / caustic(through:from:ior:closed:) + drawEnvelope / drawCaustic. Surface normals come from each point's two neighbors, so an open curve loses its two ends and a curve wants a few hundred points. The construction arrives rather than lands: the crossing sits outside the true curve by an amount falling with the square of the step, which is the pinned invariant (halve the step, the error quarters) — a fixed tolerance was the wrong test and failed against correct code. Known answers pin the rest: the tangents of a circle envelope that circle, parallel light on a circle draws the nephroid (cusps at r/2, reaching the mirror at r — the first version of this test asserted r/2 for the reach and was simply wrong), a source at the center sends every ray back through it, and a flat mirror has no envelope. Only the lit stretch of a closed curve should be handed in (near and far families lean on different curves), and filtering a ring for it must keep the run contiguous — a wrapped filter puts the arc's two ends side by side and the segments between them are not rays at all (this bit the example and the Guide figure). Plus huygensFront(from:advancing:closed:), the same envelope with circles instead of lines: the new front is where the wavelets lean, and the folded pieces are dropped (a point of the new front is exactly distance from the old one and no nearer to any part of it), which is what turns an offset into a real wavefront and makes a front tear into a focus once it has traveled the radius the curve bends at. Its reference is the moved point's own two segments, never the distance asked for: a sampled curve is a run of chords sitting a sagitta inside it, so comparing against the radius threw away every point of a front moving into a bend (the first version returned nothing at all for a shrinking ring). The pruning is sabotage-verified by two tests, and the accuracy is one sagitta, which the front test's tolerance states. Cost grows with the square of the point count, so a wall of fronts belongs in setup(). Examples Patterns/Caustic + Patterns/Wavefront; EnvelopeTests (9) + HuygensTests (6), sabotage-verified; no snapshot (the geometry is exact). Docs/Drawing/Envelopes.md; Guide Ch 16 § The curve a family of lines leans on: envelopes and caustics + the RaysLeanOnACurve figure. Shape morphing — ShapeMorph (build once, read shape(at:)) + Shape/Contour: Tweenable + morphed(toward:_:spacing:); orientation mismatch reverses all target contours (never per-pair); spacing derives per outline, not per pair; unmatched contours lerp to their own centroid; originals verbatim at 0/1 (ShapeMorphTests). Docs/Drawing/Morphing.md. Voronoi & Delaunay (Bowyer–Watson + clip to bounds, relaxed() Lloyd; examples Patterns/Voronoi + Patterns/Delaunay, the dual pair — the triangle side also reading adjacency back through Delaunay.neighbors(of:)) + power diagrams (PowerDiagram / WeightedSite / powerDiagram(of:in:), Geometry/PowerDiagram.swift): a weight subtracted from the squared distance, each cell cut as an intersection of half-planes (the direct construction, not the lifting map — cost grows with the square of the site count, so a few hundred sites, not a few thousand). The cells stay convex and still tile the region exactly, and a site can lose everything (an empty Shape, which a plain Voronoi diagram can never produce). Only weight differences matter (lifting every weight leaves the diagram identical — a pinned invariant). Weighting a circle by r² is the case worth having: a circle touching no other lies wholly inside its own cell, where a plain Voronoi boundary cuts through the bigger one. Test-coverage gotcha: the rim test first passed a wrong-sign sabotage, because four similar circles stay inside their cells either way — it only bites once the set holds a lopsided, nearly-touching pair (a big circle beside a small one), which is the case the weighting exists for. Example Patterns/PowerDiagram; snapshot power-diagram; PowerDiagramTests (8, sabotage-verified). Docs/Drawing/Voronoi.md § Power diagrams; Guide Ch 15 § Territories and neighbors: Voronoi and Delaunay + the WeightedTerritories figure. + blue noise (Bridson poissonDisk, a public generic over any RandomNumberGenerator). Low-discrepancy sampling — halton / haltonPoints / sobolPoints: pure index functions (no rng, never touch the sketch's rng); startIndex defaults 1; Halton bases stay coprime (SamplingTests). Stippling — Secord weighted-Voronoi stipple; the ring search may stop before ring r only once the best find is ≤ (r−1)·bucketSide (the r·bucketSide bound was a real lattice-locking bug); zero ink → empty array; density = (1 − linear luminance) · alpha (StippleTests). Random walks — randomWalk / levyFlight (minStep > 0 — the power law diverges at zero) / selfAvoidingWalk (visited-forever marks + parent-link reconstruction); all seeded (WalkTests). Truchet — arcs centered on cell corners, so abutting cells join under any flip; example Patterns/Truchet (both tiles, .arcs beside .diagonals). Hitomezashi — one phase bit per grid line (shorter bit arrays cycle; seeded form draws bits at probability); two faces of one design: stitches (open 2-point Contours, same-line dashes never touch) + parities (the two-coloring, row-major zip with grid.cells — tone boundaries land exactly on stitches, HitomezashiTests pins the theorem geometrically, sabotage-verified); Docs/Drawing/Hitomezashi.md, example Patterns/Hitomezashi, snapshot hitomezashi. Ten print — tenPrint(in:columns:rows:probability:) + drawTenPrint: one coin per cell choosing between two diagonals (bits row-major, a shorter array cycling like Hitomezashi's), two faces of one design: lines (one 2-point Contour per cell, the plain reading) + runs (the same diagonals joined end to end at the corners they share). The corners are a lattice worked out from grid.bounds, not from a cell's frame, so a gutter never opens a gap two diagonals would fail to meet across. The run walk starts only at a corner that is odd right now (a corner where an odd number of diagonals meet has to end some path), which is the one thing no coverage or length test can see: deleting the rule left all nine other tests green, so aChainWithTwoLooseEndsComesBackAsOneRun (three cells leaning down-up-down must come back as one run, not two) is the test that pins it. Not claimed: minimality, since the leftovers are taken as loops rather than spliced back into the trails running through them. Example Patterns/TenPrint; snapshot ten-print; TenPrintTests (10, sabotage-verified). Docs/Drawing/TenPrint.md; Guide Ch 7 § The diagonal tile: ten print + the DiagonalMaze figure. De Bruijn sequences (Math/DeBruijn.swift) — deBruijnSequence(symbols:window:) + DeBruijnCode (the reader: position(of:) / window(at:)) + lyndonWords(symbols:maxLength:). A cyclic run of symbols^window symbols holding every window of that length exactly once, which is the shortest such run and the reason a glimpse of a few symbols names its own position (encoders, printed rulers, locally-unique tile runs). Built by the Lyndon-word concatenation (write out each necklace whose period divides the window), so the result is the dictionary-least run and therefore always opens with window zeros — a fixed answer, not any valid one. The run is a ring: reading wraps, and drawing it as a strip leaves the last windows looking broken. position(of:) is nil for a wrong-length run or an out-of-alphabet symbol (nowhere, not somewhere). The decode table holds symbols^window entries, so the window is chosen from what a reader can see, not from a wanted length. Invariants: the count, every window present exactly once (read cyclically), the known small runs symbol for symbol, the sequence equalling its own Lyndon pieces laid end to end, every Lyndon word below its own rotations, the necklace-count formula with the Möbius function computed from its own definition in the test, and the reader round-tripping at every position. Dropping the period rule (write every necklace, not only those dividing the window) is caught by four tests. Example Patterns/DeBruijn; DeBruijnTests (8, sabotage-verified); no snapshot (the geometry is exact and the picture is the example's). Docs/Generators/DeBruijn.md; Guide Ch 7 § Every window once: de Bruijn sequences + the EveryWindowOnce figure. Ford circles — fordCircles(order:in:interval:) + drawFordCircles (the Sketch facade mirrors the free function's labels exactly — a member shadows a global of the same name inside a sketch, so in:-first would break every call written for the free form; the shadowing rule the CA helpers already carry) over the new fareySequence(order:) and the exact Fraction type (Math/Fraction.swift: reduced, positive denominator, mediant / isNeighbor / determinant, comparison by cross-multiplication so no rounding can change a neighbor into a stranger; a zero denominator reads as one). Every fraction p/q gets a circle of radius 1/(2q²) on the line at p/q; the unit interval spans bounds.width and the radius is scaled by that same width, which is what keeps them touching (scale x and y differently and the whole picture is a lie). The invariant both halves rest on is exact and is what the tests measure over every pair at once: distance² − (r₁+r₂)² = ((ps−qr)² − 1)/(q²s²), so two circles touch iff |ps−qr| = 1 and are strictly apart otherwise — the never-overlapping half is the one a picture cannot show. touches(_:) answers on the fractions, never on the distance. The Farey walk is the standard next-term recurrence (cost = the length of the answer, ~3n²/π² terms), pinned against a brute-force build, against the totient-sum count (φ counted from its own definition in the test, not from a shipped helper), against ps−qr == −1 for every consecutive pair, and against the mediant being the first fraction to appear between two neighbors, at exactly the order its denominator names. Example Patterns/FordCircles; snapshot ford-circles; FordCircleTests (8). Docs/Generators/FordCircles.md; Guide Ch 15 § A circle for every fraction: Ford circles + the CircleForEveryFraction figure. Ulam spiral — ulamSpiral(in:size:start:padding:) + drawUlamSpiral over a square Grid: numbers (row-major, zips with grid.cells), order (cell per counting step, -1 for a step outside an even-sided grid), point(of:) / number(column:row:) / points(where:) / primePoints / path, plus the free primes(upTo:) (sieve of Eratosthenes) and isPrime(_:) (6k ± 1 trial division) in Math/Primes.swift. The walk is right, up, left, down with side lengths 1, 1, 2, 2, 3, 3 …, which reads counterclockwise on screen and is what puts the odd squares on one diagonal (offset (n−1)/2 both ways from the middle) and the even ones one step off the other (((n−2)/2, n/2) up and left, because their ring turns the corner just before it closes). The invariant that pins why the picture has lines in it is that a quadratic traces a straight line (4n² + 2n + 1 steps evenly along one diagonal); turning the walk the other way breaks the square invariants and nothing else. primePoints runs one sieve over the whole square, never a test per cell. Example Patterns/UlamSpiral; snapshot ulam-spiral; UlamSpiralTests (9, sabotage-verified). Docs/Generators/UlamSpiral.md, Docs/Helpers/Math.md; Guide Ch 6 § Walking a grid: numbers in a spiral + the NumbersInASpiral figure. Kolam / sona — the mirror-curve walk on a lattice twice as fine as the dots (a dot at odd-odd, the line only ever at points with exactly one even coordinate, one diagonal step per cell); the state is the outgoing direction, not the incoming one (leaving(...) == step filters the start states), or every loop comes back twice, and the walk strikes off the reverse state as it goes for the same reason; a wall asked for on the outside edge is dropped, since the edge already turns the line. Two counting invariants, both tested and neither visible in a picture: a plain field closes into gcd(rows, columns) loops, and the loops always hold 4 × rows × columns segments (four per cell, not two — measured, my first guess was wrong), while any interior wall moves the count by exactly ±1; KolamTests pins all three. loops are square-cornered Contours (honest geometry; smoothed(iterations:) is the drawn form, and drawKolam rounds by default); Docs/Drawing/Kolam.md, example Patterns/Kolam, snapshot kolam, Guide Ch 7 § No coins at all: kolam and sona. Celtic knotwork — the same MirrorCurve walk (extracted to Kolam.swift and shared, so the two can never disagree) given width and an over/under rule. The rule is one line and it is why the weave holds: the up-right pass goes over wherever the crossing's lattice x is even, which flips at every step and so alternates along every cord, while the two passes through one crossing always run in opposite diagonal senses so exactly one is over. A point the line turns at (edge or wall) is not a crossing — only one pass reaches it — so the dive indices are walk indices, never positions in the filtered crossing list (they do not line up; that mismatch was the first defect). bands(gap:) hands back the pieces already broken at each dive, so stroking them is the weave: no mask, no draw order to get right, and the gap is clamped under the crossing spacing so two dives in a row still leave cord between them. A plain field has 2·rows·columns − rows − columns crossings and each interior wall removes one. The length invariant is the test that caught the real bug (both gap offsets were signed the wrong way, so pieces ran past each crossing instead of stopping short, and the picture still looked woven because the outline pass was covering the overlap): total band length must equal total cord length minus one gap per dive; KnotworkTests pins that, the over/under consistency, and the alternation. Docs/Drawing/Knotwork.md, example Patterns/Knotwork, snapshot knotwork, Guide Ch 7 § The same line, woven: Celtic knotwork. Tiling & layout — HexGrid/TriangleGrid (true-aspect blocks, never stretch; axial math; cell(at:) via cube rounding), Subdivision (the root always splits; chance from depth 1), Maze (three carvers, all perfect; collinear walls merged; double-BFS longestPath()), apollonianGasket (Descartes/Vieta other-root — linear recursion); TilingTests. Docs/Drawing/Tiling.md. Polyominoes (Geometry/Polyomino.swift) — Polyomino (squares joined edge to edge, always normalized to the origin, so it is a shape and never a place; Polyomino(["XX.", ".XX"]) string form, rotated/mirrored/orientations(reflections:), rectangle(columns:rows:), the 12 pentominoes + pentominoNames + 5 tetrominoes) + outlines(cellSize:origin:) (the edges only one cell owns, chained; one contour per boundary loop, so a holed piece hands back the hole wound the other way, and collinear corners are dropped) + tilePolyominoes(_:covering:reuse:reflections:) returning [PolyominoPlacement]? (piece / shape / at, since the shape cannot carry its own position). Where a piece sits is swept over the region's whole box, never over its cells — a piece whose own bounding-box corner is empty (the F) sits at offsets that are not region cells at all, and anchoring on cells silently loses those fits (sabotage-verified red); both being normalized is why a negative offset is never needed. The search fills the cell with the fewest remaining ways and keeps those counts incrementally (close/open per option, the dancing-links selection rule over plain arrays): 3x20 pentominoes went 109 s → 8 s against filling cells in order, and a flood-fill island prune that helped the in-order version measured slower once the counts existed (a cell in a dead island already has zero ways), so it was removed. The undo must run the closes in reverse or the counts drift (sabotage-verified red). orientations counts rather than assumes (F/L/N/P/Y 8, T/U/V/W/Z 4, I 2, X 1, summing 63 — the published table is the test). Invariants: exact cover, each piece once unless reuse, all four classic pentomino rectangles solvable, the mutilated board refusing dominoes (nil means the whole space was searched), and the seed deciding which fit comes back. Not a per-frame call (~1 s for the 12-pentomino board): solve in setup() and animate the drawing. Example Patterns/Pentominoes; PolyominoTests (12, two sabotages verified red); no snapshot (the picture is the example's). Docs/Generators/Polyominoes.md; Guide Ch 7 § Pieces that have to fit: polyominoes + the FittingPieces figure. Aperiodic tilings (Geometry/Penrose.swift / WangTiles.swift / Girih.swift / Spectre.swift) — penroseTiling (P2/P3 by Robinson half-tile deflation from a ten-half wheel, merged into whole tiles; the P2 child orderings encode each half's axis leg — wrong assignments T-junction at depth 2, crack-free-pinned; rhomb outlines lead with the positively-wound half's apex — the intrinsic orientation the arc decoration needs, since C2-symmetric arc placement collapses to midpoint crossings (a solved dead end, not a tuning parameter); arc fractions are solved continuity constants; rng-free); wangTiling (scanline fill honoring west/north edges; WangTiling.completeSet never dead-ends, arbitrary sets retry then return nil; weighted picks off the sketch rng); girihPattern (Hankin/Kaplan inference: two rays per edge midpoint at the contact angle, paired greedily by summed length with (cost, i, j) tie-breaks; unmatched rays drop; Girih.Tile.points(onEdge:_:) is the exact patch-composition primitive); spectreTiling (the chiral-monotile paper's 9-metatile / 8-slot substitution; every generation mirrors whole, so leaves at any depth share one handedness — pinned by the cyclic turn-sequence test; gamma carries the 30°-rotated isOdd mystic; curve: bends edges into the strictly chiral spectre, alternation cyclically consistent because 14 is even; rng-free). All four crack-free-tested (AperiodicTilingTests); examples Patterns/Penrose / WangTiles / Girih / Spectre; snapshots penrose / wang-tiles / girih / spectre. Docs/Drawing/AperiodicTilings.md. Hyperbolic tiling — hyperbolicTiling(sides:meeting:) / HyperbolicTiling.tiles, any {p,q} with (p−2)(q−2) > 4 in the Poincaré disk: the central polygon placed at the exact derived vertex radius (cosh = cot(π/p)·cot(π/q)), grown breadth-first by geodesic-circle inversion with center-dedup, pruned by drawn minEdge; tiles carry depth and a parity two-coloring (checkerboards iff meeting is even), and viewpoint re-seats the tiling before pruning so a pan is a rigid motion (tile count stays ~constant — the test pins panned tiles as exact images of centered ones, the pin that caught a conjugate-sign bug the horizon checks missed). Deterministic, rng-free. Example Patterns/HyperbolicTiling; snapshot hyperbolic-tiling; HyperbolicTilingTests (7, the pan pin sabotage-verified red). Docs/Drawing/HyperbolicTiling.md. Parquet deformations — parquetDeformation(in:columns:rows:from:to:sweep:) / ParquetDeformation, a tiling whose tile changes shape as it is read across (Huff's design exercise; Kaplan's curve-interpolation reading of it): the geometry lives on the lattice's edges, never on the tiles, so both tiles beside an edge read one stored curve (reversed on the far side) and the sheet is crack-free by construction however far the shape drifts — a tile's outline is the four edges around it, each side dropping its last point, which is the next side's first. An edge is a Profile in its own frame (x along it, y sideways as a fraction of that edge's length, ends pinned to (0,0) and (1,0) so the lattice's corners never move): .straight / .tooth(depth:width:) / .zigzag(count:depth:) / .wave(count:depth:) / .bump(depth:) / .custom([Vector2]), each free to double back, with one-argument statics standing in for the common shorthands (an enum case cannot carry a default). The blend is exact rather than sampled: both profiles are reparameterized by normalized arc length and the blend's vertices sit at the union of the two vertex sets, where both inputs are straight and so is their blend — a square tooth therefore keeps square corners at its own end of the run, and there is no resolution parameter to get wrong. The amount is read at each edge's midpoint; Sweep (.horizontal / .vertical / .diagonal / .radial) is sugar over the general closure form (a canvas point in, 0…1 out, clamped rather than extrapolated), which is what lets a run follow noise, an image's tone, or a moving front. Two faces: tiles (row-major, each carrying column / row / amount / center / contour / shape, walked clockwise from its top-left lattice corner) and edges (every lattice edge exactly once, so a translucent stroke never doubles and a plotter makes one pass). The sheet's rim is fringed — the outer tiles carry their deformed outer edges — so a straight border is a boolean intersection, not a setting; the grid's gutter does not apply, since parquet tiles meet along shared edges. Deterministic, rng-free. Example Patterns/ParquetDeformation; snapshot parquet-deformation; ParquetDeformationTests (9, the shared-edge rule checked point for point with no tolerance, since both sides read one array). Docs/Drawing/ParquetDeformation.md. Circle & shape packing — grow-to-touch packCircles + closed-form packCircles(around:) + relaxCircles (fixed-axis parting); geometry-aware packShapes over the stateful ContinuousPacking (fits actual outlines); an unbounded maxRadius clamps the grid cell and reach to the region size (PackingTests); examples Patterns/CirclePacking (all three circle routes side by side) + Patterns/ShapePacking. L-systems — 13 verified presets; 2M-symbol expansion cap; one Contour per unbroken turtle run (a branch shares its branch point with the trunk). Parametric L-systems (ParametricLSystem + LSystemExpression/*Parsing/*Turtle/*Presets, ABOP §1.10 + Hanan 1992 ch. 4) — modules carrying reals, rules doing arithmetic; rules are text in the literature's notation (A(s) : s > 0.02 -> F(s)[+A(s/2)] : weight). Load-bearing: the match key is (letter, arity) and an unmatched module stands for itself (which is both why turtle symbols need no rules and how a guarded form settles); productions are a source-ordered array and first-match-wins, but first-match and weighted normalization are mutually exclusive policies, so the engine gates on whether any rule declares a weight (weights are shared over the rules that matched, so disjoint guards behave identically either way); Hanan's precedence table is not C's — ^ binds above * and groups right, unary binds above ^ (-2^2 == 4), and getting it wrong is silently wrong rather than a parse error; names resolve to positions at parse time (evaluation touches no dictionary); the word is flat SoA (letters/bounds/values, ASCII UInt8) since a published derivation runs to millions of modules and a per-module [Double] would spend the expansion in the allocator; == stays exact (the sanctioned idiom is an integer counter guarded d > 0 / d == 0) and a NaN condition reads false (NaN != 0 is true in IEEE, so a plain zero-test would fire a rule on a division that went wrong); a rule that will not parse is recorded in errors + noted once and left out, never thrown or trapped. The turtle is the plain one plus three things: per-symbol parameters (first parameter steers, extras ignored — what lets F(x,t) draw x while t counts down), a width !(w) that [/] save and restore (claimed onto a branch's opening point, so !(w)F(l) is uniform), and % cut. A roll or pitch of a whole number of half-turns is planar — it is a handedness flip (+/- reverse), which is what makes 6 of the published 9-tree rows exactly faithful rather than 2; every other roll/pitch and the polygon/color/surface symbols note once and draw nothing. + follows the shipped 2D turtle's sense (mirrors the book's figures; consistency with LSystem beats matching the page). Widths reach the canvas as StrokeMarks normalized so the widest is 1 (so strokeWeight = the trunk), via lSystemMarks / drawLSystem(…, tapered:); contours and marks come off one walk so they cannot drift. LSystem.fitTransform is shared by both kinds (extracted with identical arithmetic — 185/185 snapshots unmoved). Two turn angles are Ollin's, not the sources' (compound leaves 45°, mesotonic) and say so in the preset docs + ATTRIBUTION.md. Deliberate cut: context-sensitive productions (A < B > C), a clean published boundary — the plain LSystem has no context either. Example Patterns/ParametricLSystem; snapshot parametric-l-system; ParametricLSystemTests (49, eight mechanisms each verified red by sabotage). Differential growth — stateful class, order-stable → a fixed-frame snapshot reproduces. WFC — simple-tiled (Gumin); sorted candidates + fixed cell order or the seeded solve flakes. Overlapping WFC (OverlappingWFC + wfc(from:)/wfc(_ model:)) learns its constraints from an example Image instead of declared tiles (patternSize / WFCSymmetry / wrapsSample / tileable), synthesizing a larger texture whose every NxN square is one the sample held; six invariants are load-bearing, each a way it goes wrong rather than slow: propagation runs the four unit offsets only (sound by transitivity through the intervening cell); the support counter compatible[cell][p][d] counts the neighbor OPPOSITE d (so it inits to propagatorCount[opposite[d]][p] — using d compiles and is quietly wrong) and ban zeroes all four so nothing bans twice; entropy is Shannon over the weights (a plain survivor count can't tell a 1000:1 cell from a 50:50 one) with an .infinity sentinel; a bounded output's last N-1 columns/rows are outside the solve entirely (never observed, never propagated into — a ban spreading there reads as a false contradiction) and the read-out recovers those pixels from the last covering cell, so an output needs 2N-2 a side; patterns with no legal neighbor in a direction that exists are pre-banned (propagation can never eliminate one, so it survives to be chosen and the output goes quietly illegal, the bounded-sample case); determinism per the catalog rule. Deliberate cuts: no seeding constraints (upstream's ground names a pattern by index, an artifact of extraction order) and no backtracking (restart, attempts times). Envelope stated not faked: pixel-art samples only (exact color equality; caps at 256 colors / 1024 patterns), solves fail above ~50px a side (NP-hard), and tileable is unsatisfiable when the sample's period doesn't divide the output size. Example Patterns/TextureSynthesis; snapshot texture-synthesis; OverlappingWFCTests. 1D CA & turmites — elementaryCA / totalisticCA (pure functions; the Sketch facade mirrors the full free-function signatures — the shadowing rule) + Turmite (published turn codes, no rng, Preset catalog; CellularAutomataTests). Docs/Generators/CellularAutomata.md. Flow fields — FlowField + Jobard-Lefer streamlines (a batch commits to the occupancy grid only after tracing, so a line never blocks itself); the seeded sugar captures the sketch's noise → transient, don't store the field (it retains self). Boids & steering — Boids (order-stable force sums → seeded flocks reproduce) + Vehicle (behaviors return forces; only wander rolls — a unit roll scaled by jitter; arc-length path follow, closed: wraps; SteeringTests). Pursuit (Pursuit) — everyone-chases-somebody as a stateful stepper (trails / links / web): every runner moves off the pre-step positions (a sequential update sends a ring lopsided within a few steps, and the whole figure rests on it); the ring's four exact invariants are the tests, because a slightly wrong spiral looks exactly as good as the right one — the polygon stays regular, the path holds a constant angle to the center (90 − chasing·180/sides), a runner covers radius / sin(chasing·π/sides) (one side of the square, for a square), and one step takes the ring from r to √(r² − 2dr·sin + d²); a fixed stride never reaches the middle (hard floor d·cos(chasing·π/sides), the neighbor gap bottoming at one stride at d/(2·sin)), so catchDistance defaults to two strides and a runner's own stride floors it (it never runs past what it caught); a runner chasing nobody, itself, or an absent index holds its heading, which is how the straight-line chase is written (the pursuer covers a/(1−k²), never catches at k ≥ 1, and the gap settles at a/2); PursuitTests (16), example Patterns/Pursuit, snapshot pursuit, Docs/Generators/Pursuit.md, Guide Ch 12 § Everyone chasing somebody: pursuit curves. Billiards (Geometry/Billiard.swift, Docs/Generators/Billiards.md, example Patterns/Billiards, snapshot billiards, BilliardTests 12) — one bouncing rule over four rooms (.circle / .ellipse / .polygon / .stadium) plus circular obstacles; the path is [Vector2] of turn points, so it strokes, cuts, and plots like any other geometry. The ellipse solves in squashed coordinates but takes its normal on the ellipse itself (squashing does not carry angles, so a circle normal there is wrong everywhere but the axes). A stadium cap only counts on its own side of the flats, or the far cap catches rays that never reach it. Where a ball is let go is half the picture: a start at the middle of a circle leaves no hole (the caustic radius is the chord distance, conserved from the first run), and the middle of a posted room is inside the post, where the ball is trapped — both caught by looking at the render, and contains(_:) is the check. The suite's independent invariants are the conserved ones: chord distance from the middle in a circle (hence equal chord lengths, a constant rim advance, and a rational turn closing into a star), and the foci-distance product in an ellipse (which is why an ellipse draws two pictures, sorted by whether a path passes between the foci). Orderly-versus-disorderly is pinned by separation: a hair of angle stays a hair in a circle or a square, and grows past a thousandfold in a stadium or a posted square. Test-coverage gotcha: deleting obstacle handling entirely left every test green until two obstacle-specific tests were added (a ball aimed at a post returns along its own line; a post separates what a bare square keeps together) — a sabotage that passes means the coverage is missing, not that the code is safe.

Drainage (Geometry/Drainage.swift, Docs/Generators/Drainage.md, example Patterns/Rivers, snapshot drainage, DrainageTests 13) — rivers read off a Heightfield: D8 steepest descent (drop over distance, so a diagonal is divided by root two or every network leans diagonal), flow as the count of everything upstream, the network as reaches above a minFlow threshold, Strahler order, and basins. Hollows are filled first or the network stops dead (filled() / flooded(minDrop:), Priority-Flood: flood inward from the edges lowest-first, raising each reached cell to its flooder plus a hair). Two consequences are visible in the picture and both are load-bearing: a raised cell routes by its flood parent, never by a slope read off the filled surface (the filled surface's gradient is the flood front, so reading it drains a lake as a fan of straight parallel lines — caught by eye in the example, fixed by the parent link, which also leads to the true spill point); and a wide hollow can brim over at more than one place (the flood enters at whichever rim low point it reaches first, so a symmetric bowl has four doors — stated in the tests rather than asserted away). Flow is strictly increasing downstream, which doubles as the topological order Strahler is computed in. A reach is traced once, ends shared at meetings, with the one-cell reach kept (a threshold-crossing cell that leaves the field at once) so the network is covered rather than quietly short. The independent invariant is flowIsTheTotalOfEveryPath: a cell adds 1 to every cell on its way out, so the summed flow equals the total walked path length. Gotcha: Color(hue:) takes 0…1, not degrees (a per-basin hue in degrees renders every basin the same red).

Anamorphosis (Geometry/Anamorphosis.swift, Docs/Drawing/Anamorphosis.md, example Patterns/Anamorphosis, snapshot anamorphosis, AnamorphosisTests 16) — the catoptric plate: a picture wrapped on a mirrored cylinder at true size (a picture w wide takes w of arc, so mirrorPoint is the one place the two coordinate systems meet and both directions run through it), each point mapped by one ray run backwards (eye → glass → bounce → page). The wrap runs facingAngle - along / radius, negated — the other sign is a plate that reads mirror-reversed and looks entirely plausible (theViewersLeftStaysLeft is the only check that sees it). Marks land on the near side (a convex face returns light toward the eye) and always outside the footprint (the bounce is outward, so distance from the axis grows along it); a planted lower edge (lift == 0) does not move, which makes it a degenerate probe: the mark sits under its own bounce, so a reverse-ray test must lift the picture or divide by zero. fits gates on both limits — the tangent arc (widestPicture = 2·acos(radius/eyeDistance)·radius) and the eye height (a mark level with the eye never comes down; it runs to infinity approaching it). plate(of:) resamples before mapping (the map bends straight lines), returns [Contour] split at what the eye cannot reach, and drops a partly-surviving contour from a Shape rather than opening a region with no boundary. The map back is Alhazen's problem, so it is a search (scan the arc both ends can see, bisect the sign change) and the height then comes free: the bounce keeps the path's fall, so t = eye.z · L2 / (L1 + L2) measured flat, which is the suite's independent second derivation of the forward map.

Clothoid / Euler spiral (Geometry/Clothoid.swift) — the curve written as a turn rate (start/heading/curvature/curvatureRate/length), the one shape joining a straight to an arc with no jump in bend; curvatureRate == 0 is an arc and curvature == 0 too is a line, ordinary members not special cases, which is what lets clothoidCorners hand back straights, easements, and arcs as one seamless chain. Position is the Fresnel integral, evaluated by composite 8-point Gauss-Legendre at a panel per half-radian of phase sweep, never the Fresnel closed form — the closed form loses precision exactly as the bend rate goes to zero, the arc/line cases a chain hits most (pinned against published C(t)/S(t) to 1e-12 and against 2M-panel Simpson at a 314-radian sweep). The G1 fit Clothoid(from:heading:to:heading:) follows the Bertolazzi-Frego reduction (3 unknowns → the single zero of g(A) = Y₀(2A, δ−A, φ₀), Newton from A ≈ 3(φ₀+φ₁), both headings wrapped to ±π against the chord, which is what selects the non-looping answer out of infinitely many); the paper's g'(A) = X₁ − X₂ is sign-flipped — the derivative is X₂ − X₁ (re-derived, pinned against a finite difference, and the flipped sign turns the whole fit red). clothoidCorners scales radius and easement by one factor when a corner will not fit (at a fixed easement share every length in a corner is proportional to its radius, so one step lands exactly rather than iterating), capped at half of each leg so neighbors never collide. points(count:) accumulates span by span, so it costs O(1) per point and the ends land within rounding rather than exactly; a sampled polyline is short of its curve by the square of the step. [Clothoid] reads as one path (length, point(at:), heading(at:), curvature(at:), position(at:), contour(...)), clamped past either end, which is what makes a route drivable at a steady speed. ClothoidTests (23), example Patterns/Clothoid, snapshot clothoid, Docs/Drawing/Clothoid.md, Guide Ch 16 § A corner a car could take: clothoids. Crease patterns (Geometry/CreasePattern.swift) — CreasePattern (a list of Crease: a segment plus .mountain/.valley/.boundary/.cut), MiuraFold, RotatingSquares; the flat sheet a folded or cut thing comes from, emitted as geometry for the draw/hatch/SVG paths. Flat-foldability is measured, never assumed: kawasakiResidual (the alternating sector sum, meaningful only at an even-degree interior vertex), maekawaResidual (M−V, always ±2), isFlatFoldable (both, plus even degree). A .boundary or .cut end marks both its vertices non-interior, which is why a cut-only kirigami sheet reads flat-foldable (it asks for nothing) and why interiorVertices is exactly the (columns−1)×(rows−1) inner lattice. Vertices are matched through a tolerance lattice scaled to the pattern's own bounds (1e-9 of the longer side, looked up over the 3×3 neighborhood, never iterating the dictionary), so fitted(in:) cannot split one; fitted scales uniformly and moves points (angles kept, so the laws are kept). contours(_:) merges same-assignment creases into pen strokes, carrying on through the straightest turn at a junction, so a Miura zigzag column leaves as one stroke and a blade need not stop at every vertex. The Miura folded state is one line plus three constraints: point(i,j) = (i·L + S·[j odd], j·V, H·[i odd]), with L² + H² = major², S² + V² = minor², and S·L = major·minor·cos(angle) — that third one is the panel-congruence constraint, and without it the facets stop being the parallelograms they were cut as (planarity is automatic, since the fourth corner is e1 + e2 by construction). fold 0…1 is θ = fold·π/2 through H = major·sinθ·sinγ; poissonRatio = −V²/S² (width against length, negative, and the other direction is its reciprocal, so the two multiply to 1). The mountain/valley phase is load-bearing and only the folded geometry pins it: a straight fold (i,j) is a mountain iff (i+j) is even, a zigzag fold at column i iff i is odd — flip either and both laws stay green (Kawasaki and Maekawa cannot see it), so the test that decides is the one deriving each mark from the folded sheet's own dihedral (the facet-centroid lean, perpendicular to the crease: leaning below it means a ridge). RotatingSquares: spacing side·(cos α + sin α), turn ±α by (i+j) parity, hinged exactly at a corner point (ν = −1 exactly, measured on the lattice rather than the outline, whose corner overhang is not proportional); cuts are the interior grid edges inset by ligament at both ends, and a ligament of half a side leaves nothing to cut. CreasePatternTests (24), example Patterns/CreasePattern, snapshot crease-pattern, Docs/Drawing/CreasePattern.md, Guide Ch 15 § A pattern that folds: creases and cuts. Shape grammars (Geometry/ShapeGrammar.swift) — ShapeGrammar (start Pieces + label-keyed Rules, generational sweeps, seeded through SplitMix64), rules cut / split / inset / nested / stop / custom; a design is [Piece], so it feeds straight back in as another grammar's start (how a lattice gets its bars). The balanced cut is solved, never searched: a part's area is affine in the far end's edge parameter, so the reachable band is its two ends and the target solves in one step — |A1 − A2| ≤ balance · A holds exactly and balance: 0 halves exactly. n1 + n2 = n + 4 for every cut (the line meets two edges away from their ends), and that one fact derives the whole ice-ray rule table from sides alone: at 3...5 a triangle gives {3,4}, a quadrilateral {3,5} or {4,4}, a pentagon only {4,5}, a hexagon only {5,5}, and 7+ corners is uncuttable at any size — never write those rules out. tries (keep the shortest of N cuts) is what keeps parts compact: a balanced cut down a piece's length leaves two pieces just as long, and the first render without it was slivers. A tight balance band suppresses the corner cut — a square's corner triangle maxes at (1−s)·t·A/2, so under the default 0.2 window/0.2 band a quadrilateral only ever becomes two quadrilaterals; the published triangle+pentagon rule needs a wide band (~0.6). inset pushes every edge along its own normal (a scale about the centroid would not hold bar width), and an over-pushed convex polygon is invisible to an area test — it comes back same-winding with positive area, so the collapse guard is that each inner edge still runs the way its original did. Weight 0 is the fallback idiom (unreachable while any other rule on the label can apply, so a refusing inset falls through to a narrower one); a label no rule names is finished; no piece is left smaller than minArea · (1 − balance) / 2; maxPieces stops a sweep before the design passes it. Built-in rules expect convex and hand back convex; custom may do anything. ShapeGrammarTests (28, five sabotages verified red — and the corner test was blind until it counted its cuts, since a guard … else { continue } loop passes having checked nothing), example Patterns/ShapeGrammar, snapshot shape-grammar, Docs/Generators/ShapeGrammar.md, Guide Ch 13 § Rules over shapes, not symbols: shape grammars. Force-directed graph layout (Geometry/ForceLayout.swift) — ForceLayout + drawGraph, the FR spring embedder (repel k²/d all pairs, pull d²/k on edges, temperature-capped moves clamped to bounds, linear cooling); idealDistance is a scale dial, not a promised edge length (a ring's equilibrium spacing sits well under k — measured on-equilibrium, so raise k to open the web, never iterate longer) and a settled layout is frozen (step() early-returns; reheat(_:) is the one path back into motion — the growth/drag idiom); pinned nodes hold, gravity (default 0) centers disconnected pieces, coincident nodes part deterministically; taught in Guide Ch 11. Space colonization — single-closest-node growth; keep killRadius > stepLength; sorted Dictionary iteration + a stall counter (isFinished). DLA — off-lattice walkers; arrival order = index. Dielectric breakdown (DielectricBreakdown) — NPW lattice model: Laplace solved by incremental Gauss-Seidel (a big settle at init, 2 sweeps run with each step), candidates weighed by potential^eta (the same read the public potential(at:) exposes), 4-neighbor adjacency per the paper; branches() decomposes tip-and-fork chain ends so every edge lands in exactly one channel (walking only from tips drops the edge above each fork, a caught bug); pipe-model thicknesses; ground: .border rim vs .points electrodes (rim then insulates via mirrored relax); DielectricBreakdownTests (7) pins determinism, lattice adjacency, the eta regime, arc-to-ground, edge coverage, field read-back; snapshot lichtenberg, example Patterns/Lichtenberg, Docs/Generators/DielectricBreakdown.md. Ant colony (AntColony) — Dorigo Ant System: per-ant roulette on pheromone^alpha · (1/d)^beta, evaporate-then-deposit with deposits scaled by a greedy-tour yardstick (numbers stay O(1) at any canvas size), elitism = the paper's elitist ants; trails normalizes to the strongest edge and drops sub-0.5% edges (early frames a full haze, late frames the condensed route); best never worsens (pinned), ring convergence pinned within 2%; AntColonyTests (6), snapshot ant-colony, example Patterns/AntColony, Docs/Generators/AntColony.md. Crack growth (CrackGrowth) — angle-raster subdivision: a cell within 5° reads as the crack's own line, any other angle stops it (restart perpendicular ±2°, recruit one more, capped at maxCracks); restarts draw from the kept claimed-cell list, never a bounded random probe (goes dormant on a sparse grid, a real bug the tests caught); step() marks carry point + one-sided wash span + random-walk gain, grains lays the sin-eased wash (side pinned, verified red by sabotage); segments feeds the vector exports; example Patterns/Cracks (accumulating, noClear()), snapshot crack-growth, CrackGrowthTests. Meander (Meander) — curvature-driven channel migration (stateful stepper: centerline/oxbows/scars): per-point drift = local curvature at a negative weight + an exponentially decaying upstream average at a larger positive one (the upstream term dominating is what makes bends deepen and slide downstream); the per-step resample is an interpolating spline, never the straight-segment resampled(spacing:) (a linear resample shaves curvature every step, and curvature is the engine); pinch detection skips (cutoffDistance + 20·spacing)/spacing indices along the line so a bend's own flanks never self-cut; the seed line is a few long tapered sine waves, not white noise (point-scale jitter carries no energy at the amplified wavelengths and trips the per-step clamp — a measured stall, sinuosity 1.19 vs 1.84 at step 800); oxbows shrink toward their centroid and die under width/2; scars trim to maxScars in one pass (a one-at-a-time trim never catches up after lowering the ceiling, a caught bug). Example Patterns/Meander, snapshot meander, MeanderTests, Docs/Generators/Meander.md. Strange attractors — StrangeAttractor (RK4) + ChaoticMap (clifford/deJong/henon + gumowskiMira/ikeda/hopalong; Gumowski-Mira is the full damped form and near-conservative, so the long transient is the picture — plot with no settle, the start picks the structure, and its test pins spread-without-escape, the two degenerate failure modes; ikeda defaults u=0.9, 0.918 parks some starts on the coexisting far sink; hopalong's reach grows with count; GPU mirrors ollin_gumowski_mira/ollin_ikeda/ollin_hopalong in the shader lib; example Patterns/ChaoticMaps, snapshot gumowski-mira); RK4 shared in Math/Integration.swift; the GPU leg is AttractorFlow (its own bullet above). Chaotic 1D maps & bifurcation (Geometry/Bifurcation.swift) — IteratedMap (logistic/sine/tent/gauss families + custom rule, optional analytic derivative): orbit/graph/cobweb + bifurcation points / bifurcationImage density (ink-on-white, ONE global log factor — the flame rule) / lyapunovExponent (exact-value-pinned: ln 2 at logistic r=4, ln r for tent) + drawBifurcation sugar; rng-free throughout. Sweeps sample column centers, never range endpoints (exactly-r=2 tent collapses to 0 in floating point, one doubled bit at a time) and factory starts avoid the critical point (the logistic critical orbit dies in two steps at exactly r=4 — start is 0.3, not 0.5); gauss can hold two attractors at once, so its diagram legitimately depends on start. Example Patterns/Bifurcation (diagram + Lyapunov trace in a retained batch); snapshot bifurcation; BifurcationTests. Docs/Generators/Bifurcation.md. Percolation (Geometry/Percolation.swift): site-percolation clusters over a seeded or caller-supplied boolean grid (criticalProbability 0.592746 as API): one-pass union-find labeling (4-neighbor only, corner contact never joins), clusters largest first (ties by reading order), spanningClusterIndex top-to-bottom, cellRects(of:in:) placement, and outlines(of:in:) closed rectilinear boundary loops (holes included, collinear runs merged, interior on the walker's right, pinch vertices take the tighter turn so loops never self-cross); example Patterns/Percolation (fixed per-cell values + breathing threshold, the flood-not-reroll pattern); snapshot percolation; PercolationTests (10, north-union sabotage-verified red). Docs/Generators/Percolation.md. Fractals (Geometry/IFS.swift / InversionFractal.swift / Kleinian.swift, Image/FractalFlame.swift) — IFS (affine Maps + weighted chaos game; presets fern/triangle/carpet, the fern grows +y up so examples flip); FractalFlame (written from the flame paper ONLY — flam3/Apophysis are GPL, never read; variations use θ = atan2(x, y), the y-axis convention, pinned by FractalTests — swapping args is the classic porting bug; ONE log factor from density scales all three channels (independent logs shift hue), gamma at the downfilter; Renderer is the progressive form, deterministic per (flame, size, seed, total samples)); inversionLimitSet (the canonical walk: start OUTSIDE the discs, random circle but never the one just used, no inside/outside test — the pull-inward-only variant collapses tangent arrangements into the tangency points, a real bug); kleinianLimitSet (trace recipe on the Markov minus root; the DFS's cyclic child order + repetend fixed points are jointly what make the output one ordered closed polyline — change either and the curve disconnects); Buddhabrot (Image/Buddhabrot.swift, Melinda Green's escaping-orbit density plate: seeds from the whole upper half of the radius-2 disc (a smaller box prints its own edge into the density as a rectangle, a real defect); deposits mirrored about the real axis in the upright framing (imaginary across, real down); one cap = gray, three caps = the false-color RGB split, an orbit outliving every cap plots nothing; cardioid + period-2-bulb closed-form interior skips; develop = per-channel percentile ceiling + gamma, so hot antenna pixels can't dim the plate; Renderer progressive + deterministic like the flame's, slices compose exactly; BuddhabrotTests 7, mirror deposit sabotage-verified red); the public fitted(_:in:) point-cloud placement helper (Rectangle.swift). Examples Patterns/IteratedFunctions / FractalFlame / Buddhabrot / InversionFractal / Kleinian; snapshots ifs / fractal-flame / buddhabrot / inversion-fractal / kleinian; FractalTests. Docs/Generators/Fractals.md. Schottky circle orbits (Geometry/Schottky.swift + the shared Geometry/MobiusMap.swift, the internal MobiusMap over the public Complex (Math/Complex.swift, once an internal scalar beside it) lifted out of Kleinian.swift — the Kleinian walk is byte-identical after the move) — schottkyCircles / schottkyLimitSet over SchottkyPairing, the circle-emitting sibling of kleinianLimitSet: it renders the disconnected regimes the ordered-curve walk can't, and its [Circle] output means SVG/PDF write true circles (the plotter path, halftone precedent). Load-bearing: a circle's Möbius image is the closed-form Hermitian transform H' = (M⁻¹)†H M⁻¹ (a line image → nil, dropped as a branch, measure-zero for circles clear of the pole), and the map must be unit-determinant for inverse (the adjugate) to be exact; the walk emits w(C_k) for every non-canceling letter and prunes on that same circle's radius (the whole subtree nests inside it, so the radius is the branch's error bound — no second pass); the pairing's twist zero point folds in −u² so two touching circles paired at twist: 0 hold their tangency point fixed and the generator is parabolic, which is the entire reason a picture stays dense (measured: tangent-pair schottkyCuspedPairs 22,660 circles vs cross-ring schottkyNecklace 3,208 at its best twist — no twist rescues the necklace), so lean is the animatable dial (it swings each pair about its own tangency point in opposite senses, keeping both generators parabolic) where twist thins the lace as it grows; overlapping discs make the group non-discrete and a 2M-circle ceiling ends such a walk rather than exhausting memory. Viewing, containing, and the trace bridge: a viewpoint re-seats the whole orbit by conjugating with the involution sending that point to the horizon (walk runs in the viewed plane so minRadius prunes drawn sizes; the disc holding the viewpoint flips to the picture's outer boundary; a viewpoint ON the limit set makes circles above any floor infinite — nudge off it); a pairing disc may be its circle's exterior (fromExterior/toExterior), the containing-circle framing, and the walk's orientation is set by the sign of the Hermitian form's A' (discImage), with exterior discs never radius-pruned (their radius bounds nothing — depth stops them); schottkyCircles(ta:tb:in:) bridges to the grandma-recipe groups via isometric circles as pairing discs (letter k's disc = gens[k].inverse.isometricCircle; bases may overlap — nesting is per-map, pruning stays sound; gasket traces (2,2) = the Apollonian-gasket packing, the classic figure; grandma's minus-root makes ±imaginary trace perturbations asymmetric — the example ping-pongs the upper arc only). Example Patterns/Schottky (declares loopDuration); snapshot schottky; SchottkyTests. Spanning-tree line art (Geometry/SpanningTree.swift) — spanningTree(through:/of:), the branching sibling of singleLine: exact EMST (Kruskal over Delaunay edges, deterministic tie-breaks; collinear input → lexicographic chain, coincident duplicates → nearest-straggler joins), decomposed into the minimal open chains — a walk may start only at a currently-odd vertex (originally-odd starts strand edges into extra chains, a real bug, pinned by SpanningTreeTests). Isolines (Geometry/Isolines.swift) — isolines(at:in:resolution:field:) + levels + image forms, marching squares: oriented case table + cell-average saddle rule; null segments dropped (a contour through an exact grid corner emits them — two-point litter, a real bug); directional stitch → boundary-exiting contours; the levels form marches once: each cell computes its corner range and visits only the levels in it (binary search over the sorted levels, the cell code the final filter), so every level's segments arrive in its own march's order and stitch to the same contours (oneMarchAgreesWithAMarchPerLevel: shuffled and repeated levels, levels on exact corner values, NaN; the range's <= sabotaged red), 28 levels of a cheap 650-wide lookup 22.2 → 8.5 ms in an optimized build beside 2.4 ms of sampling; the stitch computes each endpoint key once and chains a key's segments in order instead of a dictionary of arrays (identical walk); a cell with a non-finite corner holds no contour (a crossing interpolated against NaN or infinity landed on NaN and trapped the quantized key; a hole now leaves the contour open at its edge, aHoleInTheFieldOpensTheRing) come back open; endpoint maps indexed, never iterated; levels share one sampling; image form = linear-luminance-on-white, levels in sRGB tone (the cutoff convention). Examples Images/SpanningTree + Patterns/ContourMap; snapshots spanning-tree / isolines; IsolineTests. Docs/Generators/SpanningTree.md, Docs/Generators/Isolines.md. Hulls & medial axis (Geometry/ConcaveHull.swift + MedialAxis.swift; Shape.contains(_:) added alongside, a winding-honoring ray test) — concaveHull(of:concavity:) (chi-shape: erode border triangles longest-boundary-edge-first, blocked when the opposite vertex is already on the boundary; hull-notch capping is load-bearing — the substrate's finite super-triangle drops hairline hull slivers, so missing hull edges are capped with sliver fans before erosion, making concavity 0 exactly convexHull (seed-7 regression in HullTests)); alphaShape(of:alpha:) (alpha complex, circumradius ≤ alpha; islands + holes as Shapes); medialAxis(of:spacing:prune:) (Voronoi subcomplex of a boundary resample — non-ring-neighbor + inside filters; radii are exact clearances; prune collects each round's twigs against the round-start graph and removes them together — mid-round removal turns a junction into a pass-through and the sibling's walk runs past it, a real bug; a whole-component chain never prunes). All three canonically order output by geometry, never triangle order (Bowyer–Watson's Dictionary refan makes triangle order process-varying while the set is stable — loops start at their lex-min vertex, islands and branches sort by points). Examples Shapes/Hulls + Shapes/MedialAxis; snapshots concave-hull / medial-axis; HullTests / MedialAxisTests. Docs/Generators/Hulls.md, Docs/Generators/MedialAxis.md. Straight skeleton (Geometry/StraightSkeleton.swift) — straightSkeleton(of:) (exact shrinking-wavefront skeleton: arcs + one face per boundary edge) + StraightSkeleton.inset(by:) (mitered insets cut from the faces as roof planes, the contour-ladder tool; maxInset the vanish depth; Clipper2 offsets stay the rounded/outward tool). Load-bearing: events process in levels clustered by meeting point / shared parent and dispatch as chains (the degenerate-pile-up treatment; naive one-at-a-time is famously wrong); events computed during a level that land back on it are purged as stale echoes, which is why a 2-vertex LAV closes as a bridge (its pair's mutual event always re-lands on the level) and why coincident LAV neighbors must fuse at level end (a ghost corner around a zero-length front otherwise starves its LAV — a real bug, 8% of a spiky blob's area vanished, pinned by facesPartitionSpikyBlobs); output assembles by node identity (each vertex's origin→death arc; faces stitch from their edge's endpoint-role arcs; inset chords match across faces by arc node-pair, no epsilon), and Σ face areas == shape area is the test net. Example Shapes/StraightSkeleton (island contour ladder); snapshot straight-skeleton; StraightSkeletonTests. Docs/Generators/StraightSkeleton.md. Painterly marks (Geometry/Marbling.swift + Watercolor.swift) — Marbling (Jaffer closed-form: area-preserving drop to √(d²+r²), the stylus family tine/comb/tine(around:)/swirl all displacing strength · 2^(−d/falloff) along the tool's motion; rng-free, add floats arbitrary shapes holes-intact) + drawMarbling (oldest-first fills); refinement subdivides at pre-transform midpoints while the transformed span exceeds spacing (post-hoc refinement can't recover a fold; depth-capped + source-length floor so a drop centered on an outline can't recurse away); rotational tools preserve radius exactly (MarblingTests). Watercolor (Hobbs recursive midpoint deformation: per-edge variance inherited at 0.4...0.8 with jittered 0.2...1.8 starts; immutable base + further-deformed layers) + drawWatercolor; layers must fill non-zero-wound via layerShape/drawShape (they self-cross: even-odd cuts pinholes, and drawPolygon's convex fan shatters them into radial spokes — both real first-render bugs); the detail floor (edges below it stop subdividing) bounds layer cost regardless of rounds (~40 s → seconds, invisible); painting is setup-shaped (makeBatch or noLoop). Examples Patterns/Marbling + Shapes/Watercolor; snapshots marbling / watercolor; MarblingTests / WatercolorTests. Docs/Generators/Marbling.md, Docs/Generators/Watercolor.md. Chladni figures (Math/Chladni.swift) — chladni(x,y,m:n:a:b:), the center-driven square plate's standing wave in closed form, normalized to −1…1 over plate coordinates 0…1: m == n cancels to zero at the default amplitudes (1, −1) and the diagonal is always nodal (document, don't "fix"); fractional modes morph figures, so animated tours keep m > n at every stop to never cross the degenerate diagonal; mirrored in the shader lib's noise module (chladni(float2, m, n[, a, b])) and read by the .chladni Generator (ChladniStyle .sand Gaussian gather + hash speckle re-thrown by phase / .wave signed swing by cos(phase); rawIndex kept in step with ollin_gen_chladni). Nodal lines fall out of isolines(at: 0, ...) over the field. Examples Patterns/Chladni (mode tour) + Audio/ChladniResonance (modes picked by listening, magnitude(in:) per mode pitch); snapshot chladni; ChladniTests. Docs/Generators/Chladni.md. Terrain (Geometry/Heightfield.swift) — Heightfield (closure/values inits, bilinear value(u:v:), normalized(), heights 0…1 by convention), .diamondSquare (size rounds up to 2^k+1, output normalized, seeded/generic-RNG), eroded(_:seed:) over Erosion.hydraulic (the particle droplet method: inertia-blended bilinear gradient steering, slope·speed·water·capacity carry rule, bilinear-4 deposits, distance-weighted brush erosion; the speed update is the energy form √(max(0, v² − Δh·g)) — the thesis's printed +Δh·g slows downhill drops and goes imaginary, a known erratum; heights clamp ≥ 0 so border runoff can't dig unbounded drains; mass only decreases, drops carry sediment off-map) and Erosion.thermal (talus relaxation, Jacobi-gathered against the frozen field so it's order-free, deterministic, exactly mass-conserving; transfer halved so a symmetric pair can't overshoot); mesh(width:depth:height:) emits +y-up terrain (CCW-viewed-from-above winding, central-difference normals, plan-view 0…1 UVs — a height-walked Ramp texture paints altitude bands) and image() the grayscale heightmap. Setup-shaped CPU work (erode once, keep the mesh). Example 3D/Geometry/Terrain (precomputes raw + eroded meshes for the weathered toggle); snapshot terrain-3d; HeightfieldTests. Docs/Generators/Terrain.md. Shadow art (Geometry/ShadowArt.swift) — shadowArt(fromFront:fromSide:fromAbove:resolution:threshold:inverted:in:) → ShadowArt (occupied / isSolid / count / boxes / mesh over the shipped isosurface, and shadow(from:), what the carved solid really throws). Shape-from-silhouette: a voxel survives only where every supplied silhouette says solid, so the result is the visual hull, and one silhouette alone gives a prism. Bright means solid (inverted flips it), and a picture is sampled at each cell's middle, so a small silhouette stretches rather than losing its right and bottom edges. The shadows thrown are never larger than the ones asked for and can be smaller, because two views share an axis: the front and the side are seen from either end of the same vertical, so a row empty in one empties it in the other. Two come out exact when they are solid in the same rows (the circle-and-square case); three usually fall short. That rule was learned from a failing test rather than assumed — the first version of the doc claimed a pair is always exact, and a lopsided side silhouette reaching rows the front does not have proved otherwise. Test-coverage gotcha, again: reading the side mask along the transposed axis passed every test until the silhouettes stopped being symmetric under transpose (a disc and a square are their own transposes), so the tests now use a bitten disc. Example 3D/Geometry/ShadowArt; ShadowArtTests (8, sabotage-verified); no snapshot (the marched surface is the isosurface path's, already pinned). Lighting is per-frame state, not per-shape, so noLights() after a drawMesh unlights the mesh already drawn (this cost a Guide figure two renders). Docs/Generators/ShadowArt.md; Guide Ch 27 § One solid, two shadows: shadow art + the TwoShadowsOneSolid figure. Isosurfaces & metaballs (Geometry/Isosurface.swift + Metaballs.swift) — isosurface(at:in:resolution:method:field:) -> Mesh, marching cubes by default, deliberately mirroring isolines' signature (its 2D sibling), or method: .dualContouring (Geometry/DualContouring.swift): one vertex per sheet of surface through a cell (the cell's six face contours chained into loops as the march does, each loop its own quadric, so a thin wedge whose two walls meet outside the cell gets two vertices rather than one between them; an ambiguous face is settled by the field at the bilinear saddle, since the bilinear guess reads a thin wedge as a channel) at the least-squares point of the tangent planes through its crossings (each crossing bisected six times onto the true surface, its normal by central differences over a hundredth of a cell, the quadric solved about the mass point with eigenvalues under a hundredth of the largest dropped; an answer outside the cell slides along the crease line or patch plane the data left free to where it enters the cell, and only if that misses does the rank drop), one quad per crossed lattice edge with four cells around it (split on the diagonal whose triangles face the crossing's normal), the normals clustered per sheet at 35° seeded from the crossing normal nearest the quad's, so a block's faces shade flat to the corner (three vertices at one point) while a sphere keeps one per cell; a sheet a crease cut without crossing both sides is bridged onto the chord between its crease-carrying face neighbors. Pinned by DualContouringTests (16): a block's corners land to 1e-6 of a cell where marching cubes stays a tenth of a cell away (the counterfactual), the block shades flat and closes with volume 8, a sphere stays round at resolution 10 and 32, a rim splits its normals, closed by position (a crease's vertices are split on purpose, so closure pairs edges by position), and the default path byte-identical. Known limit: two curved surfaces meeting at a knife edge step by the cell where a cube holds a sliver of one without crossing the other; Metaballs (add(at:radius:strength:) / value(at:) / bounds / mesh(resolution:)) is the headline field, and its curve is public on its own in a sketch's terms: Metaballs.falloff(distanceSquared:radius:) (1 at the center, exactly 1/2 at radius, 0 from 2 * radius out; the reach and the u stay inside it, so the polynomial is free to change) and Metaballs.Ball.value(at:) (strength times that), of which value(at:) is the plain sum, byte-identical to the loop it replaced. Load-bearing: the convention is inside = field > level (normals −∇f, triangles wound to match, pinned by a positive signedVolume), so an SDF must be negated to mesh; there is no 256-case triangulation table — each cell's surface is built from its six face contours (per face: 0/1/2 directed segments with the enclosed region on its left seen from outside, walked CCW) chained into loops and emitted reversed (the loop faces inward), and the ambiguous 4-crossing face is settled by the asymptotic decider δ = (a·c − b·d)/(a + c − b − d) over the face's own four corners, which is why it can't crack (both cells read the same four values and the reversed walk swaps starts/ends symmetrically); vertices weld on a flat lattice-edge array keyed (low corner, axis), never a Dictionary (determinism), and normals come from the sampled gradient (central differences, one-sided at walls) so a smooth field costs no extra field calls; a quad loop splits on its shorter diagonal, 5+ fans from an added centroid vertex (cell-interior, so no crack); cells stay cubic (resolution counts across the longest side) and the grid centers its overshoot; Metaballs.radius is the visible radius and reach is 2·radius, because Wyvill's polynomial is exactly 1/2 at u = 1/4 (falloff pinned at its three known values), and mesh() pads the box by 1.5 cells so the surface closes. Cost is cubic in resolution (M2 release, 6 balls at 56: ~2 ms/frame, real-time) and the per-face work is allocation-free (flat faceCorners + caller-owned scratch, worth ~2× over the obvious per-face arrays); no UVs (a blob has no natural parameterization). Deliberate v1 cut: meshing an SDF3D is out — its evaluation lives only in ollin_sdf3d_eval, so a CPU evaluator would be a third hand-synced surface across 15 leaves / 20 combines / 10 transforms. Example 3D/Geometry/Metaballs; snapshot metaballs; IsosurfaceTests (watertightness over a saddle-rich field at three resolutions, sphere volume + outward winding, determinism). Docs/Generators/Isosurface.md. Subdivision surfaces (Geometry/SubdivisionSurface.swift) — mesh.subdivided(_:levels:) refines any Mesh as a control cage: .catmullClark (default, general polygon faces) / .loop (triangle rules with Warren's β, plus a final limit push — γ = 1/(n + 3/(8β)) interior, (3v+b₁+b₂)/5 on rims). Load-bearing: the positional weld tolerance is ~1e-6 of the mesh extent — libtess2 caps come back Float-quantized (~1e-8 relative) against exact-double walls, so a tighter weld leaves every cap rim open (a real bug); quad recovery pairs the consecutive (a,b,c),(a,c,d) triangle pattern gridQuad emits (run before any winding flip — flipping first breaks the pattern); the coplanar face merge (CC only) re-derives whole polygon faces for exactly-flat patches (single simple boundary loop, no interior vertices — a deliberately tessellated flat grid is untouched) because a triangulator's arbitrary cap diagonals give identical corners different valences and an extruded star smooths visibly lopsided (0.2 arm spread → 1.7e-8, the five-fold test); orientation follows the input's normals, not its winding — Mesh.extrude winds inward while shading outward (a find worth knowing), so agreement is area-weighted-summed and a disagreeing mesh's faces flip; no normals → authored winding kept; open/non-manifold edges take the B-spline crease rules, a boundary vertex with one incident face pins (the edge-and-corner convention, so open sheets keep their extent), flat sheets stay exactly flat; deterministic (dictionaries lookup-only, first-come weld ids); 2M-face cap with a printed note; output = smooth area-weighted normals, no uvs, material carried. Example 3D/Geometry/SubdivisionSurfaces; snapshot subdivision-surfaces; SubdivisionSurfaceTests (hand-derived mask pins — cube corner 5/18 + edge 3/8, the Loop strip's odd/crease points — plus watertightness, Euler, convex-hull containment). Taught in Guide Ch 27 § Smooth from a cage: subdivision surfaces. Docs/Generators/SubdivisionSurfaces.md. Mesh growth (Geometry/MeshGrowth.swift + MeshOperators.swift + MeshReactionDiffusion.swift) — MeshGrowth, a stateful stepper that grows a Mesh surface until it folds, over four GrowthDrivers (.uniform / .curvature / .field(closure) / .chemical(SurfaceChemistry)); the public MeshReactionDiffusion runs the same Gray-Scott on a fixed mesh (Turk's surface-texture use, displaced(by:) to see it). Load-bearing: forces run along edges only, which lie in the surface, so nothing can inflate the form and every driver folds — the driver picks where and at what scale, not whether (documenting .uniform as "inflates smoothly" was wrong, caught by render, and the buckling is correct); self-avoidance excludes the 2-hop neighborhood, not just the 1-ring (a 2-ring vertex sits ~2·edgeLength off on any smooth surface, so leaving it in makes every vertex shove its own neighborhood and the whole form inflates with growth off — the defect aZeroFieldDoesNotGrow pins), radius default 2·edgeLength, past ~3· it inflates again; stiffness is the fold-size dial (the normal half of the relaxation Laplacian — a sheet with no bending resistance buckles at the triangle scale and reads as crumpled paper, which is the thin-shell term the reference has and the first cut omitted); remeshing is Botsch-Kobbelt isotropic (split >4/3·L, collapse <4/5·L guarded by the link condition + both rings' resulting lengths + no normal flip, flip toward valence 6, tangential relax at 1/5) and growth must stop at maxVertices or the springs stretch triangles the splitter can no longer subdivide; per-vertex channels (the chemicals) interpolate at a split and average at a collapse so a pattern survives the topology churning; the boundary is never collapsed or flipped and a rim vertex relaxes along the rim, so an open sheet keeps its edge. Both types weld first via the shared WeldedMesh — Ollin's generators emit flat-shaded meshes whose triangles share no vertices (an icosphere is degree-2 everywhere), so an unwelded surface cannot diffuse at all and the reaction silently dies; MeshReactionDiffusion simulates on the welded copy and republishes through remap so values still matches mesh.positions by index. The cotangent Laplace-Beltrami operator with mixed areas (Meyer et al.) is the shared substrate (MeshOperators, internal): scaled by mean-edge-length² it is dimensionless and lands at exactly 4 on a regular triangulation, matching a grid's 5-point stencil, which is why the classic Gray-Scott constants (0.16 / 0.08, feed/kill matching Sim.reactionDiffusion) transfer unchanged; a seed patch is u=0.5, v=0.25 floored at 3·edgeLength across (pure reagent has nothing to react with and decays; a patch a vertex or two wide diffuses away before igniting). settleSteps (default 0, .chemical only) runs the opening chemistry all at once at the first step so growth answers a formed pattern — and it must refine the surface to the target edge length first: chemistry settled on the coarse seed cage dies at exactly zero (the patch is a vertex or two wide there, the ignition floor above — a probe-caught bug); it's the ramp fix only, since the pattern covers part of the surface and chemical growth stays a few times slower than .uniform at equal growthRate (rate compensation stays with the sketch). Determinism: edges come from a sorted key array, never a Dictionary walk, and the repulsion hash grid dedupes its 27 slot reads (two cells can share a slot and would otherwise double-count). Cost is real CPU mesh processing (~30 ms/step at 9k vertices on M2 after a 3× pass: ring table built once, flat edge/boundary table, counting-sort grid, allocation-free EdgeFaces), so maxVertices is the interactivity dial. Example 3D/Geometry/MeshGrowth; snapshot mesh-growth; MeshGrowthTests. Docs/Generators/MeshGrowth.md. Point-cloud surfacing (Geometry/ParticleSurface.swift + SurfaceReconstruction.swift over the internal PointGrid3 + partialIsosurface) — particleSurface(of:radius:blend:resolution:) (the blended union-of-balls skin: field = radius − |p − kernel-weighted local average|, so an isolated point is an exact ball; no normals, always closes) and reconstructSurface(of:spacing:resolution:orientedToward:maxGap:neighbors:fitting:keepingLargestComponent:) (the faithful path: PCA tangent planes over k-NN, signed distance to the nearest plane, marched by the internal partialIsosurface, whose nil-mask path is byte-identical to isosurface). The fitting: choice (SurfaceFitting, an artwork parameter per the catalog rule, never a RenderQuality tier): .planes default (byte-identical to the shipped path — pinned), .robust(sharpness:iterations:) = RIMLS (Öztireli/Guennebaud/Gross 2009, written from the paper — MeshLab's implementation is GPL, never read): IMLS base (kernel-weighted average of plane heights, kernel (1−d²/h²)⁴, per-point h = reach clamped to [1.5, 4]·spacing) iteratively re-weighted by a residual Gaussian (Welsch, σ_r 0.5 in units of h) × a normal-vs-gradient Gaussian (σ_n = 1/sharpness floored at 0.5, where the surface genuinely disconnects); the first pass is unweighted on purpose (a robust start breaks the reconstruction's continuity — the paper's deliberate deviation from textbook IRLS, so iterations: 1 is plain smooth IMLS) and the gradient keeps the kernel-derivative terms ((Σαφ′f − f·Σαφ′ + Σαφn)/Σαφ — the weighted normal average alone is the classic shortcut bug); both fittings share one plane-based validity gate, so holes open identically; the evaluator reuses scratch buffers across the serial march (per-lattice allocation is the debug-build trap) with flat scalar inner loops. Measured on the staged partial-coverage ball (the phantom-fringe scene): mid-air phantom vertices 5,480 → 43, ball-annulus 1,747 → 0, the ball's observed front kept (3,169 vs 3,210 on-ball vertices) — so the fringe envelope below is now .planes-specific. Colors carry: the PointCloud overload transfers the cloud's colors (nearest sample, Mesh.colored(from:)), skipping all-white clouds so bare reconstructions stay colorless; largestComponent remaps colors with positions. Load-bearing: orientation is viewpoints-first, by a distance-weighted vote (1/d⁴) over the (nearest ≤16) viewpoints — never the nearest alone (the nearest camera sees a terminator sample edge-on, dot ≈ 0, sign = noise → flipped planes raise phantom curtains, probe-confirmed; the 1/d⁴ falloff keeps thin walls decided by proximity); the spanning-tree fallback weights edges 1−|nᵢ·nⱼ|, which reads antiparallel as agreement, so it can walk through thin double-sided structure and flip a region (documented limit, not tunable); the field is undefined, never guessed, where the projected foot lands beyond the validity cutoff (nil = per-point k-th-neighbor reach capped at 3.5·spacing — uncapped, the reach inflates exactly where data is missing and phantom bowls grow in occlusion shadows; tighter caps pinhole randomly-sampled clouds, whose local gaps routinely pass 2.5·spacing; .infinity closes everything), which is what keeps scan holes open — and cells touching an undefined sample are skipped whole; partial coverage frays by design under .planes: an open data edge grows a ~gap-wide fringe along its tangent planes just past the samples (four parameter "fixes" probed and refuted — coverage is the fix, so sweep around bodies; .robust suppresses >99% of the fringe, measured, but coverage stays the honest fix; the Docs page says both), and the marching cell must stay under ~2·spacing (a wide-bounds cloud starves the grid and junctions shred — printed note; crop to the region of interest); the near-surface occupancy band short-circuits far cells in one array read but must not run under .infinity (closing legitimately caps far from samples); PointGrid3 ring searches stop on a strict bound only (in-box ring·cell, clamped (ring−1)·cell; a tie at the bound must expand another ring — the stipple stopping rule in 3D) and its queries are flat scalar loops on purpose (per-candidate closures + Vector3 operators ran 8.7× slower in the unoptimized builds examples run under); counting-sort buckets ascend by index → byte-reproducible output. Example 3D/Geometry/SurfaceFromPoints; snapshot surface-from-points; SurfaceReconstructionTests (grid matched against brute force, watertight sphere volume + outward wind, torus Euler 0, a hole stays open, thin-wall viewpoint orientation, byte-determinism both paths). Docs/Generators/SurfaceReconstruction.md. Articulated & chaotic motion — IKChain (maxBend enforced inside both FABRIK passes then re-placed at exact segment length, never post-clamped; keep the stall exit), DoublePendulum (fixed ≤1/480 s substeps — reproducible by default), NBody (Barnes-Hut; coincident bodies chain at the depth cap; θ=0 stays the exact all-pairs sum — don't "tighten" NBodyTests' error bound). Docs/Simulation/Motion.md. Coupled oscillators (Geometry/Kuramoto.swift) — Kuramoto, the mean-field model: each phase advances at w + K r sin(psi - theta - lag) with (r, psi) the crowd's own order parameter (coherence/meanPhase, one pass per substep, so N is free), criticalCoupling = spread * sqrt(8 / pi) for the seeded normal spread (Kuramoto's 2 / (pi g(0)); a custom frequencies array keeps the made-with value), a range for nearest neighbors on a ring (locks locally and holds a twist, so global r stays low while neighbor pairs agree, the invariant KuramotoTests pins), fixed substeps of 1/240 s with phases wrapped, and a Box-Muller draw of its own over SplitMix64 so a seed replays. Local coupling beyond the ring: Kuramoto(columns:rows:layout:wraps:) lays the crowd on a Lattice (.square(.vonNeumann) / .square(.moore) / .hex, range as rings, settable live and the lists rebuilt on change, 0 falling back to the mean field), Kuramoto(neighbors:) couples over any per-site index lists, and the settable neighbors hands any crowd a graph (kept whatever range says, nil returns to what it was made with; a bad index is a precondition at set time). The pull over a list is coupling times the mean of sin(neighbor - phase - lag), the same rule the ring used, and the ring, the graph, and the lattice are one code path: a graph handed the ring's own lists steps bit-identically, and a lattice at range 0 steps bit-identically to Kuramoto(count:) under the same seed (both pinned). The hex lattice shares HexGrid's odd-r addressing (row-major, odd rows shifted right), pinned cell by cell against HexGrid.distance for three ranges, so hexGrid(columns:rows:)[i] is site i; the hex ball is walked in axial coordinates and turned back into offsets so the stagger is read once; a hex torus is clean only for an even rows (said, not hidden). localCoherence is the per-oscillator order parameter over self plus list (the layer's global r under the mean field, 1 for a loner), computed on read. layers (oscillators per site, index site * layers + layer) copies the neighborhoods per layer with a per-layer mean field, and layerCoupling pulls a site's layers together, averaged over the other layers with no lag; layer 0 of a two-layer crowd steps bit-identically to a one-layer crowd dealt its phases (pinned). rates is every oscillator's rate at the last substep (frequencies until the first advance; pinned as the phases differenced across one 1/240 s substep, unwrapped, on a three-layer hex crowd with a lag), the one number a motion blur or a trail of anything the crowd drives needs. CellNeighborhood gained Hashable for the layout. Asked for in issue #25 by a sketch that had carried its own copy of the rule for a hex field of three plates per dot. Thresholds in KuramotoTests were pinned from a numpy prototype first (r 0.987 at 4 Kc, a peak of 0.08 at Kc/4, N 400 over 20 s); a locked crowd with a lag drifts at w - K sin(lag), the exact invariant the suite reads. Docs/Simulation/Oscillators.md. Crowds (Geometry/Crowd.swift) — Crowd, optimal reciprocal collision avoidance (van den Berg, Guy, Lin & Manocha 2011) on the CPU over Crowd.Agent values (position/velocity/goal/radius/maxSpeed/group/isJammed), goals or a preferredVelocity closure, addObstacle (outline or Rectangle) / addWall (polyline or segment), advance(by:), drawCrowd. Each neighbor gives a half-plane through velocity + u/2 (the reciprocal half, pinned exactly: after one step a pair on a collision course has a relative velocity that grazes the truncated cone at the sum of the radii, eachSideTakesHalfAndThePairJustGrazes, red under a quarter share); the new velocity is the incremental 2D linear program's nearest point to the wish inside maxSpeed, and an infeasible program falls to the least-intrusive 3D program with obstacle lines kept hard. Load-bearing: the no-overlap guarantee is conditional on feasibility, as the paper states: a jam (isJammed, the fallback) overlaps by a small part of a radius (≤0.25 r pinned in a packed doorway) while walls hold to 1e-6 even jammed, and a checker built from the authors' reference library (RVO2, Apache-2.0, studied, not ported) showed the same jam overlap and the same circle numbers as the prototype, so never promise "no overlap at all"; the fallback's projection loop runs hardCount ..< max(hardCount, i), empty when the failing line is an obstacle's own (a C loop skips it, a Swift range traps; obstacleLinesThatCannotAllBeMetStillGiveAVelocity, the crash the edge-side sabotage found); an outline is normalized by signed area so either winding is the same obstacle, a wall is the out-and-back degenerate ring (each side faces its own agents, a bend convex on its outer side), and only an edge's outer side sees an agent; edges live in a lazily rebuilt grid and are sorted by (distance, index) after gathering so the dictionary's order never reaches the outcome; every step chooses all velocities from the pre-step snapshot; advance(by:) splits into equal steps ≤1/60 s (a thirtieth equals two sixtieths bitwise). Obstacles never route (the paper's own caveat): a door needs a goal that leads through it. Two defaults were measured, not guessed: timeHorizon 1 s (≈15 radii at the default speed and radius, the reference ring's scale; a dense ring of 96+ at 4 s stalls a third of the way in and never arrives, while 60 arrive at any horizon), and jitter 0.05 of top speed on moving agents (a symmetric knot is a fixed point, since a jammed agent's choice ignores its wish; at 0.005 a small ring of eight arrived in 1 of 5 seeds, at 0.05 in 5 of 5, and under a point a second of wander at 120). Head-on with jitter 0, exactly collinear, and starting near enough that the cutoff circle answers before the legs, a pair only slows and never swerves (that branch has no lateral part; from farther off the legs answer first and the pair swerves, which is the mirror law's setup). Lanes in counterflow (order > 0.9 against a shuffled < 0.65) and door flow rising with width under its geometric bound are emergent invariants. CrowdTests (17, four sabotages verified red: quarter share, no fallback, inner-side edges, inverted convexity), snapshot crowd, example Simulation/Crowd (a translucent sheet over noClear() is what makes lanes visible in a still), Docs/Simulation/Crowds.md, Guide Ch 12 § A crowd that makes room: Crowd + the MakingRoom figure. Color — CSS Color 4 named set + Color(hex:), HSB (hue wraps), the OKLab family (OKLab / OKLCH / OKHSL, gamut-mapped, written from Ottosson), Color.mix(_:_:_:in:), Color(kelvin:), withAlpha(_:) / perceptual luminance, lighter(by:) / darker(by:) / complement (OKLCH: lightness stepped, hue and chroma held, chroma lost only where the gamut runs out, a gray is its own complement, Palette.complementary(of:) agrees to the bit), Ramp / Palette (ColorBrewer + harmonies), CosinePalette (iq), Colormap. Palette import — hex / CSV / TSV / JSON / ASE; the three text formats share one parser and .auto sniffs palette-per-line from the file's shape, an explicit format overrides the sniff (palettePerLine: Bool?, nil = decide); no comment syntax is promised (#deface is a color); loaders never trap on malformed bytes; ship the loader, not the data (bundled palette data stays license-gated). Palette extraction — weighted k-means++ → Lloyd in OKLab; identical pixels merged and sorted before clustering; output most-used first; deterministic given (image, count, seed). Image dithering — Image.dithered: 8 error-diffusion kernels + centered Bayer + generated .blueNoise; CPU-only by nature; three color spaces, one per family — mixing them is the bug: tone accumulates in linear, .none picks OKLab-nearest, error diffusion picks *linear-nearest (the false-contour rule, pinned by DitherTests on a 1×1 image); threshold maps run the *Yliluoma pair search* (variance-weighted penalty, linear mix fraction, dark→light phase); a fully transparent pixel diffuses no residual. *Measuring color* (Color/ColorDistance.swift, Color/Shades.swift, Image/Histogram.swift) — Color.distance(to:measure:): .oklab straight-line (the confusion check's own scale, pinned equal) and .ciede2000 written from Sharma, Wu and Dalal (all *34 published pairs* within 1e-4, symmetric); CIELab is *D50 through Bradford* (white exactly (100, 0, 0), sRGB red 54.2905/80.8049/69.8910), a Lab color outside sRGB mapped back through the OKLab chroma reduction; Palette.nearest(to:) / nearestIndex(to:) are the brute-force minimum, *ties to the earlier entry, nil when empty. Image.histogram(of:bins:in:) returns a Histogram (counts / total / percentile(_:)): **every pixel counts once (total is the region's pixel count, the region averageColor's own pixelSpan), color straight (alpha divided out, clear at 0), bin i of n is [i/n, (i+1)/n) so 256 bins are one byte level each, a texture-backed image empty. Ramp.shades / Palette.shades: OKLCH, the lights turn toward sRGB yellow and stop there, the shadows away (yellow's own toward orange), hueDrift turns at each end and signed, the ends keep half the base's chroma (ShadeWalk.endChroma), and the palette always holds the base exactly. Two equivalent mutants, read rather than chased: CIEDE2000's zero-chroma mean hue never reaches the result (its ΔH is zero with it), and swapping the wrap's +360 and −360 moves the mean a full turn under a T periodic in 360° and a blue rotation near zero there; leaving out the wrap is the real slip, and it reads red. ColorMeasureTests (12), eleven plants red; example Color/Shades; Docs/Drawing/Color.md § CIELAB, Measuring color, Shades whose hue drifts; Guide Ch 2 § Measuring color, and shades that drift (ShadesThatDrift figure) and Ch 9 § Palettes from a photograph. Print separations — Image.separated(into:paper:) under the translucent-spot-ink overprint model (linear-light mixing, OKLab-judged coverage search; a tie goes to less ink, in the golden section and at the endpoints, since once the inks chosen print solid black every later ink scores flat and the old tie laid solid pink under a black ground, aBlackPixelSeparatesToBlackAlone in either ink order); screening drops coverage under the 2% minimum dot; the sketch declares printInks (NOT inks); --export-separations (registration band around the untouched artwork; recipes carry ExportMetadata.inks). PrintSeparation.screenAngles(for:) is public (the darkest ink at 45°, the rest over 15/75/0/30/60, then an even spread; an angle follows its ink, not its slot) and ProcessSeparation.screenAngles is the four-color rosette in plate order (15/75/0/45); both are exactly what the default halftoned uses (pinned byte for byte). Docs/Output/PrintSeparations.md. Print color management — the process-color sibling: ICCProfile (built-ins from CGColorSpace.copyICCData, installed() over the three ColorSync dirs, space/channels/prtr read straight out of the 128-byte header, an FNV fingerprint as the cache key), SoftProof (destination/source/intent/simulatesPaper), Image.softProofed(_:warning:amount:) / gamutMask / outOfGamutFraction, ProcessSeparation (Image.separated(into: profile), plates in the profile's channel order, peakTotalInk/averageTotalInk, screening shared with the spot path at the conventional CMYK rosette angles), the live Filter.softProof, and Sketch.printProfile + --export-plates / OllinApp.exportPlates (registration band identical to the spot masters; the recipe carries printingCondition). Load-bearing: every transform is ColorSync's (a 2- or 4-step profile chain through ColorSyncTransformCreate, cached per condition behind an OSAllocatedUnfairLock), and the module must be imported @preconcurrency because the SDK spells its CFString keys as mutable globals Swift 6 rejects outright; black point compensation stays off, and simulatesPaper switches the return leg's intent (relative maps white back to white while ink black still lifts; absolute keeps the stock's tint; an .absolute proof always returns absolute); the source is read as a screen (ICCProfile.colorSyncProfileAsScreen: a v2 display profile's D65 media white rewritten to the D50 connection white, the v4 convention, since ColorSync's absolute intent scales each pair by both profiles' whites and so carried the sRGB white across as a color: white proofed cyan (195, 210, 226) and the paper read (238, 220, 185) against its tag's (224, 223, 216); relative and perceptual proofs are byte-identical either way; anAbsoluteProofOfWhiteShowsTheStocksWarmth, theSimulatedPaperIsThePressProfilesMediaWhite) — measured: rel/rel white 1.005 black 0.055, rel/abs white .934/.864/.726, and BPC on the 4-step chain misbehaves (white drops to .862), which is why it is not a parameter; the gamut check is kColorSyncTransformGamutCheck, which writes one 32-bit float per pixel (1 = out of gamut) whatever depth is requested — asking for kColorSync1BitGamut with a packed buffer overruns and traps; float conversions carry device-encoded values, never linear light. A full-image proof measures ~145 ms at 1080², so the live path bakes a 33³ lattice (~4.4 ms) whose rgb is linear and whose alpha carries the gamut flag, uploaded as an rgba32Float 3D texture keyed by the ProofLUT object identity (the cache hands back the same object per condition, which is what stops a 574 KB upload per frame) and sampled at (i + 0.5)/n through the clamped imageSampler; the fragment encodes with linearToSrgb before the lookup (dropping it is a real picture change, pinned red by sabotage in theLiveProofMatchesTheImageProof). Plates come from .toDevice (1 = full ink; master byte = 255 - ink, the spot-ink convention) and preview() runs .fromDevice from the plates rather than re-proofing the source, which is what makes the CPU and GPU halves cross-check; total ink is honest only on unscreened plates (a screened set reports dot overlap, 400% at a rosette). PrintColorTests (26, incl. three Metal-gated live-filter probes) + the gamut, neutral-gray, and round-trip pins under Physical invariants, snapshot soft-proof, example Color/SoftProof, Guide figure ProofBeforePrint. Docs/Output/PrintColor.md. Color lookup tables (looks) (Color/ColorLUT.swift) — ColorLUT reads the Adobe/Resolve .cube text (init(contentsOf:) / init(resource:withExtension:in:) / init(data:) / init(text:), every one throwing a ReadError that carries the line): both forms (LUT_3D_SIZE → .cube, 2…256 on a side; LUT_1D_SIZE → .curves, 2…16,384 nodes), TITLE, DOMAIN_MIN/DOMAIN_MAX and Resolve's LUT_1D_INPUT_RANGE/LUT_3D_INPUT_RANGE, entries red-fastest; a keyword the format lacks, an entry before the size, a second size (the shaper-plus-cube file is refused rather than half read), an entry that is not three numbers, or a count that does not add up is refused by line. Filter.lut(_:amount:) applies it; ColorLUT(size:title:_:) builds a cube from a closure, identity(size:) is the control, write(to:) / cubeText write the format back, color(for:) is the CPU read, and .warmPrint is the bundled look (Sources/Ollin/Resources/Looks/WarmPrint.cube, written from the four rules its header states). Load-bearing: the table is read on the encoded picture (linearToSrgb in, srgbToLinear out), since that is what a look is authored on; a cube is read tetrahedrally (ollin_fx_lut3d: eight reads at whole coordinates of an rgba32Float 3D texture, the six-tetrahedron case table on the order of the three fractions), so a gray input meets only the cell's gray corners and a table that leaves gray alone does so between nodes, where the sampler's trilinear blend would tint a neutral (grayStaysGrayThroughWildOffDiagonalNodes pins it); the answer is applied as a difference on the encoded axis with a dead zone of 1e-6 (ollin_lut_apply), because the GPU's transfer round trip lands a few float steps off, nothing to the eye but enough to turn a pixel sitting on an eight-bit rounding tie (a channel at 0.9 is 229.5) the other way after the dither: measured, 950 of 36,864 bytes moved a level on a hue sweep, precise::pow read the same, and a thousandfold amplification barely moved the count, so the flips were ties and not error. With the dead zone an identity table of either form and amount: 0 are byte-identical (anIdentityTableChangesNoByte), and a value above white keeps its excess plus what the table does to white, so a tone map still sees the highlight. The 3D texture is cached by the table's FNV fingerprint (colorLUTTextures, bounded to 8), so a look applied every frame uploads once; the curves form is passed through EffectInput.table as a single pass (ollin_fx_lut1d), which is what crosses to the web page (Curved in WebEffectsTests, mean 0.012 against the Mac), while a cube answers nil from singlePass and the export refuses it by name, as the lut filter. GLSL gotcha: arithmetic on a uint index wants a u literal (i0.x + 1u), or the page's compile fails where Metal's passes. ColorLUTTests (17: the reader and its refusal lines, corners exact, an affine table reproduced between nodes, the gray invariant, the domain, the writer's round trip, the bundled look's four rules, and three GPU probes), snapshot color-look, example Color/Look (+ the lut tile in Effects/FilterCatalog), Guide figure Look. Docs/Drawing/Looks.md; Guide Ch 20 § A look from a colorist's suite: ColorLUT. Gradient paint — Gradient (.linear / .radial / .conic / .alongPath) / Paint on every shape via a baked 256-texel LUT strip (SDF path) + per-vertex evaluation (tessellated path), with native SVG gradient export (a conic, which neither SVG nor PDF can shade, exports as its ramp's midpoint like an along-path fill); the conic (.conic(center:startAngle:), the angle from three o'clock turning clockwise like every angle here, -.pi / 2 for twelve) is paint kind 4 in the shape tag, so each paint slot is three bits, 10-12 the fill and 13-15 the stroke (the Metal vertex, the web page's GLSL vertex, and the wire tests all mask & 7; nothing reads the tag above bit 15), its geometry (center, startAngle) relative to the shape center in the color slot like the radial's, and it reads by position, so the merged fields (2D and the raymarched 3D) paint it where they refuse an along-path; on the tessellated path a paint that sweeps around a point (a conic, or an along-path fill) has a seam, the ray from its center at the start angle, and a fill triangle whose corners fall on both sides of it would interpolate backwards through the whole ramp, so Drawer.emitTriangle cuts such a triangle along the seam's line (a corner on the seam takes the ramp end its own piece is on, the two pieces fan from the cut; RoundedCornersTests pins the cut against the exact SDF square at a mean of 3.8 levels against 6.9 uncut, over the whole frame) and every fill site (polygon fans, arc fans, drawShape, outline glyphs) emits through it; shared CPU↔GPU structs live once in the C header (see Shaders). Docs/Drawing/Geometry.md, Docs/Drawing/Color.md, Docs/Drawing/Epicycles.md, Docs/Drawing/Voronoi.md, Docs/Drawing/Truchet.md, Docs/Drawing/Attractors.md, and the per-generator Docs/Generators/* pages. Hobby's spline (Geometry/HobbySpline.swift; the Spline mode in Geometry/Path.swift) — HobbySpline(through:closed:tension:curl:startDirection:endDirection:) fits one cubic per gap with every control point solved together, reached from the sugar as spline: .hobby on drawCurve / Contour(curveThrough:) / Shape(curveThrough:) (.catmullRom stays the default and is byte-identical; Path.curve(to:) runs are untouched). Written from Hobby's 1986 system in the form Knuth programs: the curvature-matching row at each knot (A θ[k-1] + (B+C) θ[k] + D θ[k+1] = -B ψ[k] - D ψ[k+1], re-derived from the cubic's own curvature with uniform tension), the curl ratio at each open end, the velocity function held to 4 chords, and a one-chord open curve with curls at both ends is a straight line by special case (the two curl rows coincide, so the tridiagonal solve returns NaN otherwise, a real failure the first run caught). Closed runs solve the cyclic tridiagonal by a rank-one correction of the open solve (a two-point loop is a dense 2×2). tension is floored at 0.75, curl at 0; consecutive repeats and a closing repeat are dropped. HobbySplineTests (13) pins the invariants: four points on a circle give the classic quarter-circle Béziers (control distance 4/3·tan(π/8) of the radius), the symmetric three-point arch leaves straight up under curl 1 and at 67.5° under curl 0 (both solved by hand), G1 at every knot including the closed seam, mirror and reversal symmetry, tension monotone. Snapshot hobby-spline; Examples/Shapes/HobbySpline; Docs/Drawing/Geometry.md#hobby; Guide Ch 16 § A curve that reads as drawn: Hobby's spline.

  • Spatial queries. SpatialIndex, the CPU structure that answers what is near a point without reading every point: nearest / kNearest / neighbors(of:within:) / the allocation-free forEachNeighbor / anyNeighbor + hasNeighbor / indices(in:), over two backings picked at build (.grid, a uniform cell grid, the default and the only one that takes insert cheaply; .tree, a k-d tree for clumped sets, rebuilt on insert). Every answer is an index into points, so a sketch keeps its payload in its own array. The of: overloads take a point index and leave that point out, which is the flocking/relaxation form. Invariants, each pinned by a test: every array-returning query agrees exactly with a brute-force loop, both kinds, ties included (equal distances answer with the lower index, so the ring search may stop only once the best find is strictly inside the ring bound, and the tree visits a far side whose crossing distance is exactly equal); cells are cut on the lattice through the origin, never on the point set's own corner, so a rebuilt index sorts moved points into the same cells and a neighbor walk keeps its order (a drifting order moves the last digits of a force sum, and over enough steps the picture); the first candidate always wins in nearest (testing the distance alone answers nothing for a coordinate large enough to square to infinity, since inf < inf is false); the radius block is measured from the query's true cell rather than a clamped one (a clamped block reads cells that are not within reach, in the wrong order); and every cut to a cell coordinate happens in floating point before the Int conversion, which traps on a value a Double holds. Cell count is bounded (fit doubles the size rather than allocating a million cells for a set lying along a line). Four sims moved onto it and each still renders byte-identically (DifferentialGrowth, Boids, DiffusionLimitedAggregation, FlowField.streamlines, checked against the committed references at an exact zero difference rather than the mean-diff tolerance); the move also fixed a latent Boids defect, where cells were keyed from the origin but queried from bounds.origin, so a flock in a non-zero-origin box lost neighbors on one side. Packing and DielectricBreakdown keep their private grids: their query is a cell block, not a radius. No new snapshot (those four references pin it in pixels). Example Shapes/Neighbors; SpatialIndexTests (10); Docs/Drawing/SpatialIndex.md.
  • Outline questions (Sources/Ollin/Geometry/ContourQueries.swift, Docs/Drawing/Geometry.md#contour-questions, example Shapes/OverUnder, snapshot outline-questions, ContourQueryTests 22, Guide Ch 15 § Asking an outline where it goes: contour questions). Contour.tangent(at:) / normal(at:) / nearestPoint(to:) / fraction(of:) / distance(to:) / piece(from:to:) / crossings(with:) / crossings() (each a Contour.Crossing: point, fraction, otherFraction) / simplified(tolerance:) / reversed() / rounded(_:) / chamfered(_:), with Shape.simplified/rounded/chamfered editing every contour and keeping winding. Load-bearing: every answer is in point(at:)'s own measure (a fraction of the walked length), read off one internal segments() walk that drops zero-length segments the way point(at:) skips them, so point(at: fraction(of: p)) == nearestPoint(to: p); the tangent at a vertex is the segment arriving (point(at:)'s <=); the normal is tangent.perpendicular, right of travel on the canvas (inward on a clockwise outline). Each segment owns its start and not its far end (the last of an open walk owns both), so a crossing on a vertex counts once, and settled folds the rare float double report; a self-crossing skips neighbors and a closed walk's last-with-first, a parallel overlap reports nothing, a touch counts. The broad phase is sweep and prune on x, pinned against a brute-force scan of a seeded 300-step walk. piece on a closed walk always runs forward (through the seam when end < start; 0 to 1 is the whole loop, returned open with its start repeated), and backward on an open walk is the forward piece reversed. simplified is Ramer–Douglas–Peucker against the segment, not the line (the line form drops the turn of a run that doubles back past its end), iterative, a closed loop split at the point farthest from its start; the glyph path's private simplifyClosed keeps the line form and was left alone so no glyph moves. reversed() is a concrete member returning Contour and shadows the collection's view; a scratch probe showed Array(c.reversed()), for p in c.reversed(), and a variable annotated with the collection's own reversed type all still resolve. The corner edits share trimmingCorners: each edge lends at most half its length to a corner (all of it beside an open end), so an equilateral triangle rounded past its limit is exactly its incircle; arcs flatten to a 0.1-unit chord error, at most 128 steps; a cut that uses up an edge leaves one point, not two. Gotcha: a test that indexes hits[0] after #expect(hits.count == 1) traps under a sabotage, and every test scheduled after the trap never reports, which made a broken broad phase look green; try #require the count first. A closed contour's walk wraps (point(at:), tangent(at:)/normal(at:), piece(from:to:) through the one internal walkFraction; an open one and a non-finite fraction clamp), the rule Curve3D walks by; piece on a closed contour runs forward at most once round, two fractions a whole number of laps apart giving the whole loop (aClosedWalkWrapsAndAnOpenOneClamps, red under the old clamp).
  • Bending outlines (Sources/Ollin/Geometry/ContourBending.swift, the corner-keeping smoothed beside the plain one in ClassicCurves.swift, Path.cubics and CubicBezier in Path.swift; Docs/Drawing/Geometry.md#contour-bending, examples Shapes/WavyLettering and Shapes/TraceToCurves, snapshot bent-outlines, BentOutlineTests 16, Guide Ch 15 § Bending an outline: warped, pulled, split, and fitted). Contour.warped(from:along:spacing:) / pulled(toward:amount:radius:spacing:) / smoothed(neighbors:weights:keepingCornersOver:) / split(atCornersOver:span:) / fitted(tolerance:), each on Shape too (split flattens every contour's pieces into one list, fitted returns one Path per contour, the rest keep winding); Path.cubics reads any path as CubicBeziers, and HobbySpline.Segment is a typealias of that value. Load-bearing: the warp and the pull refine before they move (refined(maxSegment:) keeps every vertex and adds points so no straight run is longer than spacing; 0 keeps the points), so a straight edge bends and corners survive. The warp reads each point against the base through one Walk (distinct points, cumulative distances, a per-vertex tangent averaged from both segments and lerped along a segment) as (along, across) in the smooth frame at the nearest place, and lands it at the same fraction of the target with the same two offsets: base == target is the identity to 1e-7 for any curve, a point past an open base's end carries on past the target's end, and a target traced from a few points bends what crosses it without a step at each vertex. The pull's weight is the bump (1 - (d/r)²)², times a smoothstep of the walked distance to the nearer end over radius on an open contour, so the ends never move. A corner is a vertex turning more than angle between its two distinct neighbors (cornerIndices(over:), shared by the smoothing and the split); the corner-keeping smoothing runs the kernel over each run between corners as an open run (ends pinned, mirrored past them), so count and order hold and a square keeps four corners and straight sides; the split measures the turn over span back and ahead along the walk, merges candidates within span of each other along the walk, never by index (a square's four corners are consecutive indices and must stay four cuts), and folds a run that straddles a closed walk's seam. A jitter larger than the point spacing makes every point a corner (a test and the snapshot scene both hit it): the doc says to simplify first, and a test trace is built with its wobble well under the angle at its spacing. The fit is Schneider's on a pending list, the later half pushed first so the cubics come out in walk order; the error is the squared distance at each point's own parameter, Newton reparameterization runs at most four passes and only when the worst error is within 4× the tolerance, a collapsed or reversed handle falls back to a third of the chord, a closed contour appends its first point and reads the seam tangent as the central difference through it so the loop joins smoothly, and the comparison is <= so a tolerance of 0 still terminates (a two-point piece fits exactly). Path.cubics converts a Catmull-Rom span exactly (p1 + (p2 - p0) / 6, p2 - (p3 - p1) / 6, with the run's clamped neighbors as the sampler uses them), a quadratic by degree elevation, a line by thirds, and close() as the straight run home only when the pen is away; the test pins every sampled contour point onto a cubic to 1e-6 through a ternary search, since a 200-step sampling alone misses by a tenth of a unit. Gotcha: #expect(xs.allSatisfy(\.flag)) fails to compile inside the macro ("call can throw"); bind the result to a let first.
  • Curved patches (Sources/Ollin/Geometry/CoonsPatch.swift; Docs/Drawing/Geometry.md#coons-patch, example Shapes/WavingBanner, snapshot curved-patch, CurvedPatchTests 11, Guide Ch 15 § Bending an outline into a patch: CoonsPatch). CoonsPatch(top:right:bottom:left:) / (topLeft:topRight:bottomRight:bottomLeft:) / (_ rectangle:), point(u:v:), contour, the oriented sides and the corners; Contour.warped(from:into:spacing:) and the Shape form keeping winding. Load-bearing: the patch is Coons's bilinearly blended form, (1-v)T(u) + vB(u) + (1-u)L(v) + uR(v) less the bilinear surface of the four corners, so four straight sides give exactly the bilinear map (pinned against straight sides given as unevenly spaced polylines) and a patch made from a rectangle is that rectangle for any u, v (pinned from -0.5 to 1.5). Each side is read by the fraction walked along it (the internal Contour.Walk), never by point index, so point(u: 0.5, v: 0) is the top curve's midpoint by length; past 0 or 1 a side carries on straight along its end segment, so a point outside the source rectangle lands outside the patch. top sets the direction and every other side is turned to meet it (left and right start at the end nearer the top's start or end, the bottom at the end nearer the left's end); a corner where two ends miss is their midpoint, an empty side runs straight between its neighbors' corners, and a side of one point pinches the patch. The warp refines before it moves (the shared refined(maxSegment:), spacing measured in the source rectangle), so a straight edge bends with its side. contour samples each side at its own fractions through point(u:v:), so with matched corners it is exactly the four curves, corners once. Every plant in the suite (index fractions, no corner subtraction, no turning, no refinement, no midpoint) goes red.
  • Packing rectangles (Sources/Ollin/Geometry/RectanglePacking.swift; Docs/Generators/RectanglePacking.md, example Patterns/PackedPhotos, snapshot packed-rectangles, RectanglePackingTests 7, Guide Ch 15 § Packing rectangles: packRectangles). packRectangles(_:in:padding:allowsRotation:) (free function, and a Sketch form defaulting the bin to the canvas) returns a RectanglePacking: placed (Placement: index, rectangle, isRotated) sorted by index, unplaced indices sorted, and occupancy. Load-bearing: MaxRects (Jylänki 2010) with the best short side fit and the global choice: at every step the snuggest fit over every waiting rectangle and every maximal free rectangle, ties by the long side, then the earlier index, then the higher, further-left place, then upright, so a result is a pure function of the input whatever order the free list is in. The fits are remembered between steps: a placement only removes regions and adds a few, so a rectangle whose best region survived is weighed against the added regions only and one whose region was cut is weighed against all; the prune checks only the new pieces (an untouched region was already inside no other, and none is inside a piece since a piece is inside the region it was cut from). theRememberedFitsPlaceExactlyWhatThePlainSearchPlaces holds the shortcut to a plainly written global search over 24 seeded sets (planting a stale fit, ignored new regions, or an unpruned piece each goes red there). A rectangle with no fit is moved to unplaced at once, since the free space only shrinks. Padding pads every rectangle and the bin on the right and bottom, so neighbors sit a gap apart and the last ones still reach the bin's far edges. Measured optimized (a standalone -O build of the file): 300 rectangles 16 to 22 ms, 1,000 about 0.6 s, 3,000 12 to 18 s; about nine in ten waiting rectangles lose their region each step in a dense packing, which is the cost, and the docs state it.
  • Plane curves: segments, arcs, and ellipses as values (Sources/Ollin/Geometry/PlaneCurve.swift, LineSegment.swift, Arc.swift, Ellipse.swift, plus area/perimeter/point(at:)/nearestPoint(to:)/distance(to:)/contour on Circle, area/perimeter/edges on Rectangle, perimeter/edges on Triangle; Docs/Drawing/Geometry.md#plane-curves, example Shapes/Crossings, snapshot plane-curves, PlaneCurveTests 15, Guide Ch 15 § Shapes that know where they cross: segments, arcs, and ellipses). PlaneCurve is one requirement, var contour: Contour, and one extension method, crossings(with: some PlaneCurve) -> [Vector2]; Contour conforms by returning itself, so a [any PlaneCurve] list asks pair by pair, and the concrete Contour.crossings(with: Contour) -> [Contour.Crossing] keeps winning for two contours (overload by argument type), so nothing that read fractions moved. Load-bearing: the engine (CurveCrossings) reads every curve as one of three forms, a run of straight pieces (segment, rectangle, triangle, contour, and any outside conformer through its contour), a round (circle, arc, and an Ellipse whose radii agree within 1e-12, isCircular), or an oval, and each pair of forms has one method: straights against straights go through Contour.crossings(with:) (its sweep and vertex ownership), a straight against a round or an oval is the quadratic in the piece's own parameter (the oval carried into its unit-circle frame first, where the parameter survives the affine map and the point is taken on the original piece), a touch is a discriminant within 1e-12 · a · (|f|² + r²) of zero and gives one root, two rounds are the construction on the center line (coincident or concentric circles report nothing, a touch counts once, h² ≤ 0 clamps), and an oval against anything round is seeded by the two contours' crossings and polished by Newton's method on the two dimensionless implicit forms (|p−c|²/r² − 1 and |toUnit(p)|² − 1, their analytic gradients), a seed that will not settle (parallel gradients, a touch) kept where the outlines put it, then filtered by any arc's sweep. Every answer is settled: sorted along the asked curve in its own measure (a straight form by contour.fraction(of:), a round by the angle from its start or from the right, an oval by the parametric angle), points within 1e-9 · scale folded into one (a corner found by both edges, a touch split by rounding). An arc with |sweep| ≥ 2π is read as its circle; Arc.contains(angle:) and the sort share Round.contains/position with 1e-9 slack so an end counts. Circle.segmentCount(forRadius:) is the one flattening rule (the 0.1-unit chord error, 8 to 256) and the arc scales it by its sweep (at least 2 pieces), the ellipse takes it at the larger radius (the same maximum sagitta under parametric sampling). Ellipse.perimeter is the AGM evaluation of E(k) (4a · K · (1 − Σ 2^(n−1) c_n²), 40 steps at most, a circle exact and b = 0 giving 4a); nearestPoint(to:) is Newton on the parametric angle from the best of 64 samples; bounds is exact under the rotation. The typed draw sugar (drawLine(_:), drawArc(_:mode:), drawEllipse(_:)) forwards to the drawer, a tilted ellipse through push, translate, rotate, pop. Plants: the polish disabled turned the two ellipse tests red at 1e-9 (seven issues); the tests also pin the sort direction both ways along a reversed segment, a corner counted once, a touch counted once, the parametric sort of the ellipse's four crossings, Simpson's rule at 1e-9 and Ramanujan at 4e-4 on five ellipses down to 1000:1, and a rotated pair moving its crossings with it.
  • A turtle driven from code (Sources/Ollin/Geometry/Turtle.swift, Docs/Generators/Turtle.md, example Patterns/TurtleWalk, in snapshot plane-curves, TurtleTests 4, Guide Ch 16 § A pen you drive: the turtle). A value with mutating moves: forward/back, left/right in degrees (his pick over radians, the L-system's own unit), penUp/penDown/close, move(to:)/home(), pushState/popState (his pick; bare push/pop fail check-api-names), a settable heading from the positive x-axis clockwise on the canvas (-90 up by default, so right(90) from there faces right; left subtracts, right adds, and the spirolateral walker in the framework is the same walk, pinned point for point). Load-bearing: land(at:) is the one place the position changes, appending to the run with the pen down and resetting the run to [position] with it up; flush() always leaves run = [position]; penDown starts a run where it stands; popState flushes, restores position, heading, and pen, and starts a new run there (never draws the way back); close() drops a last point within 1e-9 of the first and keeps a run under three points open; a pop with nothing remembered is a no-op by design; contours includes the run in progress once it has two points. Plant: right subtracting turned the square and the spirolateral match red.
  • Enclosed regions (Sources/Ollin/Geometry/Regions.swift, Docs/Drawing/Geometry.md#regions, example Shapes/ColoringBook, RegionTests 15, Guide Ch 15 § The regions a drawing encloses: regions(enclosedBy:), figure EnclosedRegions). regions(enclosedBy: [Contour]) / (enclosedBy: [Shape]) returns every face of the planar arrangement of the outlines as a Shape, with Circle.contour(segments:) (the count from the radius at a tenth of a unit of chord error, 8 to 256, past which a radius over about 1,300 strays more) and Rectangle.contour (the corners, top left first, clockwise) as the ways in. Load-bearing: every segment is cut at every crossing strictly inside it (the broad phase is Contour.sweep over one flat list; two collinear segments cut each other at the other's interior ends, so an overlap is one shared edge), every endpoint snaps to a vertex through a cell grid at 1e-9 of the extent (the two reports of one crossing land on one vertex), duplicate edges fold, bridges are removed first (Tarjan with its pending work on a list, so a hanging stroke, a line joining two loops, and every tree wall nothing off and every kept edge lies on a cycle), then each face is walked as a doubly connected edge list: at the far vertex the edge just clockwise of the one arrived by, which keeps the face on the left, so a bounded face has positive shoelace area and the outside of each connected part negative. A negative cycle becomes a hole in the smallest bounded face of another part that contains its first vertex (parts never touch, so the vertex is strictly inside or outside; a part's own faces are excluded, since its vertices lie on their boundary where the ray test is undefined). Output: .evenOdd, outer and holes wound oppositely, reading order (top of bounds, then left, then area). Pinned: a 5 by 6 grid of lines gives 20 cells of 10,000; the regions tile exactly what the lines wall off, which is the union plus every hole in it (seven circles and a bar ring a 316-unit patch of bare page; the tests fill the union's holes through separated()), no two overlap, every vertex and edge midpoint within 1e-6 of an input line, nested circles give disc + ring + band with the right holes, a bowtie its two lobes, lines along each other one edge, a diagonal through two corners and a T onto an edge each once. Test lessons: Shape.area on a holed shape resolves through the boolean library and rounds at about 1e-4, so the tests measure holed regions by the shoelace and compare against the booleans at 1e-2; lines that may end anywhere inside a box ring islands that are regions and holes at once (the tiling still sums), so the no-hole claim uses chords with both ends on the frame; two in-out slices of one array in the sweep's closure trip exclusivity, so cuts go to locals first. Cost: the sweep plus one walk round every face; a few thousand segments a frame, a few hundred thousand in setup().
  • Hilbert order (Sources/Ollin/Geometry/HilbertOrder.swift, Docs/Generators/HilbertOrder.md, figure Docs/Figures/HilbertOrder.swift, example Images/HilbertLine, HilbertOrderTests 7, Guide Ch 9 § A picture as one line: stippling, singleLine, and spanningTree). hilbertIndex(of:in:level:) (a cell's distance along the curve on a 2^level grid over bounds, the quadrant walk with a reflection and a swap per level; level held to 1...30, a non-finite coordinate or an empty axis reads cell 0, outside clamps to the nearest cell), hilbertOrder(of:level:) (indices sorted by that index over the points' own bounds, stretched to the grid, ties by input index so a cell keeps its given order), hilbertSorted(_:level:). Load-bearing: the curve starts at the top left of the bounds and ends at the top right (pinned: every cell visited once with every step a grid neighbor at levels 1 to 7, and the classic 4 by 4 sequence); the order is a permutation that repeats and is unchanged by a scale or a shift of the whole set; on 3,000 random points the sorted line is under a fifteenth of the given order's and under 1.5 × side × √n; the Morton order was not taken because it keeps neighbors together less well at its quadrant seams. The figure draws the curve as hilbertSorted of the grid's own cell centers, which is the curve itself, no second routine. The example sorts 3,600 stipple dots on a level parameter and draws the curve under the line at level 6 or lower, so a coarse order's per-cell scribble is seen; where a stipple has a gap the line crosses it in one step, said on the page.
  • Breaking things (Sources/Ollin/Geometry/Fracture.swift + Fracture3D.swift, convexHullFaces in ConvexHull.swift). Shape.fractured(into:seed:) and (into:around:seed:) cut a filled region into the Voronoi cells of seeded points and trim each to the shape: the pieces' areas sum to the shape's own, no two overlap, and one seed is one break. A cell straddling a notch comes back as one piece per island (the new Shape.separated(), which decides nesting by an edge probe, not by winding, or a ring's outer boundary reads as living inside its own hole), and a hole survives. Mesh.fractured(...) is the same cut in space: the hull is cut out of a starting box once and each cell is that solid minus the other seeds' half-spaces, so volumes sum and every piece is convex, which is exactly what Collider3D.hull(...) wants. A non-convex mesh breaks as its convex hull, said in the docs rather than silently. Load-bearing in the clipper: a corner already on the cut plane joins the rim, or several planes meeting at one corner leave the cap short and the solid leaks volume (measured: an icosphere's 80 hull planes converged to 0.14 against its true 0.457, and the same run reads 0.45733902606402 with the corner in); the rim welds at 1e-6 of the extent and is sorted around the plane normal through a basis with u x v = n, which is a loop only because the cell is convex. Seeds are placed by Mitchell's best-candidate (8 tries), and the crowded form draws its radius as u^1.9 from the blow, so the chips there are small and the wedges away from it long. convexHull(of: [Vector3]) is the incremental hull underneath (the horizon is found through a directed-edge set, never a scan over every face, which is the difference between a 500-point cloud costing milliseconds and minutes); fewer than four points, or one plane, encloses nothing and gives an empty mesh. Shape.area / centroid resolve multi-contour shapes through the boolean normalizer first, so a ring authored with a same-wound hole still measures as the difference between its circles. A 2D Collider.polygon is reduced to the solver's eight-corner limit before it is handed over (World.cornersForPolygon, dropping the smallest-eared corner each round): past that limit the solver's own hull builder returns nothing, so the body had no collider, no mass, and never moved, which is what a nine-corner fracture piece hanging in mid-air was (PolygonColliderTests, verified red by sabotage). World.remove(_ body:) joins the 2D physics world to match World3D.remove(_:) (Box2D takes a body's joints down with it, so the list is pruned by b2Joint_IsValid); a removed body is spent, so read the velocity the pieces inherit before the parent goes (a use-after-destroy here trapped the process with no message). FractureTests (20 tests: the sums, overlap, convexity by each piece's own hull, the crowded break's small chips, seeded repeatability, the hull of a box and of a sphere). Examples/Physics/Burst, Examples/3D/Physics/Burst; Docs/Generators/Fracture.md; Guide Ch 11 § Breaking things.
  • Cutting solids (Sources/Ollin/3D/MeshBoolean.swift). The 3D counterpart of Shape's set operations, spelled the same: Mesh.union(_:) / intersection(_:) / subtracting(_:) / symmetricDifference(_:) (the last composed of two differences and a union), each returning an ordinary Mesh that draws, exports, fractures and collides like any other. The cut merges the two solids' binary space partitions (Naylor, Amanatides and Thibault, SIGGRAPH 1990, written from the method and credited in ATTRIBUTION.md): a tree of each solid's own face planes sorts any polygon into inside and outside, clipping keeps the half each operation wants, and invert (which flips planes, winding and vertex normals) is what turns one clip into four operations. Load-bearing, and the defect that hides: a plane-cut surface is geometrically complete and topologically torn, because a corner one face gains from a plane lands in the middle of the neighbor's edge and the neighbor, sorted down another branch, never hears about it. The volumes are then exact while the surface reads as riddled with holes (measured: a box minus a sphere came back with the right volume and 103 unshared edges, a box minus a box with 12), so volume is not a test of closure and printCheck() is. CSGSeam sews it up: weld every corner that landed in the same place (so coincident corners are bit-identical afterwards and every later merge agrees), drop the faces that collapsed, then find the edges exactly one face uses and cut them at the corners sitting in their middle, fanning from the opposite corner so no piece is a sliver; only an unshared edge can hide a T-junction, which is what keeps the search small. Tolerances are relative to the pair's own bounds and both are load-bearing: the plane thickness is scale * 1e-9 and one scale serves both trees (a face landing on opposite sides in the two trees is how a closed surface opens), while the weld is scale * 1e-6, deliberately the same rule the exporter's own bucket weld uses. Two solids whose boxes are clear of each other by more than the weld tolerance take their own branch (both, neither, or the first): planes are unbounded, so a cutter across the room otherwise runs its six planes through everything and hands back a surface carved along seams that were never a cut (2304 triangles in, 2752 out, 4.5 s of work for nothing); solids that merely touch must stay on the cut path, or a shared wall survives on both and the pair reads as a surface meeting itself. The partition plane is chosen by scoring sampled candidates against sampled polygons (8 and 32): a solid with no dividing plane in it, which is every convex one, builds a chain of one node per face, and scoring every candidate against every polygon then costs a pass over the whole surface per face (measured 4x: 5.9 s to 1.6 s on a 2304-triangle cut). Cost is CPU and superlinear in triangle count (debug: 0.19 s at 576 triangles, 1.6 s at 2304, 19.5 s at 9216), so the docs say cut in setup() and keep the mesh. What comes along: the first mesh's material and per-vertex normals always; uvs and colors only when both meshes carry a full set (a partial set is ignored by the renderer, so half of one is worse than none); tangents are dropped (generatingTangents() puts them back). Two honest limits, both documented: a mirrored cutter (a coordinate swap, a negative scale) is wound inside out and takes what it should keep, and a symmetric difference of overlapping solids touches itself along the curve where the two surfaces cross, so it closes with no boundary edge but is not manifold there. An open input gives an open result, by construction. MeshBooleanTests (18, pure CPU: the volume identities, printCheck on every operation, the stacked twelve-cut block from the example, a solid against itself where every face is coincident, scale from a millimeter to a kilometer, repeatability, and what comes along). Examples 3D/Geometry/CutAndJoin (the four operations side by side) + 3D/Geometry/Carved (a block rounded, bored three ways and bitten at every corner, re-cut only when a parameter moves); Docs/3D/3D.md § Cutting solids into each other; Guide Ch 27 § Cutting one solid with another: mesh booleans.
  • One mesh from several (Sources/Ollin/3D/MeshJoin.swift). Mesh.joined(_:) lays parts end to end as one mesh with no boolean (vertices in order, each part's indices offset to its own, a triangle pointing past its part dropped, empty parts skipped), and placed(_:) bakes a MeshInstance placement into a part first: positions through MeshInstance.matrix, normals through its inverse transpose, tangents through the linear part, a tint multiplied into the vertex colors (a full set of it for a mesh with none). Load-bearing: placed runs the same float arithmetic the per-frame mesh path runs on a mesh under the transform stack (m * float4(p, 1), normalize(nm * n)), which is what makes a joined mesh drawn at the origin render byte-identical to its parts drawn apart under translate/rotate/scale (measured 0 on the M2; the test allows one level for a normal re-normalized in double on the identity path). What comes along follows the merged-loader rules: normals wherever any part has them (a missing one is +z, the draw's own default), uvs and tangents only when every part has a full set, colors when any part has one (the rest white, exact under the multiply contract), the first material among the parts. Mesh.text is its glyphs through this join. MeshJoinTests (10) + MeshJoinRenderTests (the A/B). Docs/3D/3D.md#join; Guide Ch 27 § Cutting one solid with another: mesh booleans (closing paragraph).
  • Sweeps and strips (Sources/Ollin/3D/Curve3D.swift, MeshSweep.swift, Sources/Ollin/Math/Pace.swift; Docs/3D/Sweeps.md, figure Docs/Figures/Sweeps.swift, example 3D/Geometry/SweptKnot, SweepTests). Curve3D: points open or closed (let, since the frames are derived and stored at init), Curve3D(curveThrough:closed:segments:) = uniform Catmull-Rom with segments steps a span scaled by span length against the mean (1 to 8x), through every anchor; point(at:) / frame(at:) by walked length, clamped open but wrapped closed, the same rule Contour walks by; frame(at:) slerps the two stations' rotations and returns a station's own frame exactly at a station; resampled(spacing:) keeps an open path's last point (unlike Contour.resampled). Frames are rotation-minimizing by double reflection (Wang, Jüttler, Zheng, and Liu 2008, credited in ATTRIBUTION.md): fourth order, measured on a helix at 32/64/256 points a turn at 2.7e-5 / 1.7e-6 / 6.7e-9 rad of twist error a turn against the old rotation transport's 2.3e-2 / 5.7e-3 / 3.6e-4 (second order). Tangents are central differences (one-sided at open ends; a zero one borrows the nearest real one). The first frame stands upright (binormal toward +y, or normal toward +x when the path starts within 0.99 of vertical), so a path along +z lays a shape out exactly as Mesh.extrude does (aStraightSweepIsAnExtrusion). A closed loop's leftover turn is minus its total torsion, measured by carrying the last frame across the closing segment and spread back by walked length (pinned against Simpson of the trefoil's exact torsion to 1e-4, and the per-point unwinding to 1e-9). Mesh.tube(along:) rides the same frames with its old start normal (tangent x up) so the ring phase holds; NOT byte-identical: rings turn by at most 0.7° (helix) / 1.9° (knot), and the whole snapshot suite (Hopf included) passed unrecorded. Mesh.sweep(_:along:scale:twist:capped:): one ring per path point (sparse paths step, hence resampled), the shape's origin on the path, x along normal and y along binormal, scale/twist read at the walked fraction (twist counterclockwise as the path comes toward you); each closed contour is reversed if the right side of its longest edge is inside the shape (probed with Shape.contains, so either winding, holes, and .nonZero all face out; four windings pinned), open contours make sheets; a vertex turning more than 30° gets two columns (hard edge), else one smoothed; the seam is a duplicated column (u 0 and 1) and a closed path repeats station 0 at v = 1 with twist(1)/scale(1); normals are across × along from the built grid (falls back to the outline's own outward direction where scale is 0); caps from triangulatedFill of the closed contours only, each triangle ordered by its 2D signed area (counterclockwise faces +tangent), uvs from the fill's bounds. A shape wider than the path's bend radius folds on the inside (geometry, not a defect). Mesh.loft(_:along:at:smoothed:scale:twist:capped:) (LoftTests, snapshot loft, example 3D/Geometry/LoftedVessels, figure Docs/Figures/Loft.swift, Guide figure LoftedBottle): the sweep IS a loft of one section at station 0 through one builder (SweepBuilder), so sections of one shape are the sweep byte for byte (pinned on an open and a closed path, with scale and twist, smoothed or not). Rows are every path point plus a row at each station not within 1e-9 of a point's fraction (path.frame(at:) there), so a section is exact at its station (pinned to 1e-12, corners placed through scale/twist, between two path points included). Columns, per contour: the union of every section's vertex fractions, each closed outline read from its seam, merged within 1e-9 only across sections (a section's own vertex is exact, the others are read on their segment), so every corner of every section is a column on all of them. The seam of each closed outline after the first is the one of 256 even starts with the least summed squared distance to the outline before, read from its own seam (chained; section 0 keeps its first point, and on a closed path the last joins the first as drawn; a square and the same square a quarter turn round, listed from a different corner, make a straight prism: pinned). Sharp is turning past 30° on ANY section, and the two columns' normals coincide where the other sections round it off. The blend per column: smoothed is a cubic Hermite with the tangent half the chord across a section's neighbors and the whole chord at an open end (so two sections blend straight, and a midway section changes nothing: pinned), written as the start section plus differences so equal sections blend to themselves exactly; smoothed: false is straight; a row within 1e-9 of a station is that section exactly; before the first or past the last station of an open path the end section holds; a closed path wraps from the last section to the first. Contours are matched in order by kind, as many as the fewest any section has. Caps (SweepProfile.capTriangles): each ring's straight runs are reduced to their ends before the triangulator (libtess2 fills a run of collinear points with slivers of no area, which the sealed-solid test found), each cap edge along a run is split back at the run's points and fanned from the opposite corner (from the centroid when more than one edge splits), and the cap's corners are the ring's own doubles (the triangulator returns single precision; aStraightSweepIsAnExtrusion compares caps at 1e-6 for that reason). A capped loft or sweep has every directed edge paired by position (pinned), and isClosed is set on sweeps and lofts (closed contours only, and capped or a closed path), which the canvas depth of field reads. at is one station per section, ascending, 0...1 open and 0..<1 closed, else a precondition. Mesh.strip(between:and:colors:closed:): rung i = first[i]–second[i], quads (a0, a1, b1) (a0, b1, b0), normals along × rung per end (front = first running left to right under second), a degenerate one falling back to face normals; colors count must equal the rungs or is dropped; u = rung / (rows - 1); closed repeats rung 0; unequal counts: the longer line sets the rungs and the other is read at the same walked fractions (one point = a fan). Pace (byLengthOf: generic over Vector, byAreaBetween:and:; period: wraps and keeps counting, parameter(at: f + 1) = parameter(at: f) + period; over: clamps): the rate at resolution + 1 nodes and 5 Gauss-Legendre points a stretch, a cubic Hermite per stretch from those, safeguarded Newton (60 steps, bisection bracket) to invert; derivatives by fourth-order differences 0.05 of a stretch apart, one-sided within two steps of an over: range's ends so the curve is never read past them; non-finite rates read as 0; the area rate across a rung is ∫|p + vq| in closed form (asinh), with Gauss-Legendre when |q| is under 1% of |p| where the closed form would cancel. Pinned: the cardioid's closed-form length (1e-9), a Lissajous's steps against Simpson of its exact speed (1e-8), the limaçon's polar area (1e-9), a self-twisting strip against a brute-force surface integral (1e-5). Measured -O: a length pace builds in 0.5 ms, an area pace 1.2 ms, parameter(at:) 0.1 µs. Five planted defects (one reflection, unwinding by index, a linear stretch, Gauss-Legendre only across a rung, no outline reversal) each went red.
  • Points on a surface (Sources/Ollin/Geometry/SurfaceSampling.swift). surfacePoints(on:count:) / surfacePoints(on:spacing:) scatter a mesh's skin rather than its vertex list (a mesh puts vertices where its shape needs them, so a vertex draw crowds a rounded corner and leaves a flat wall bare), returning a SurfaceSample per point (position, normal, uv, triangle + barycentric weights). A triangle is picked in proportion to area through a running-total table, and the point inside it through the low-distortion square-to-triangle fold (press the square's two halves together across the diagonal: no square root, and an even input stays even where the sqrt map stretches one corner). .blueNoise (the default) draws 5x the count and thins the crowded ones out by weighted elimination over the occupied cells only (BlueNoiseThinning.swift; weight falls to zero at the honeycomb spacing sqrt(area / (2*sqrt(3)*count)) and rises as the 8th power, so the nearest neighbor decides who goes, with a floor under very close pairs or the pass spends its budget separating one of them); .random is the plain draw. Load-bearing: the points are renumbered cell by cell (cubes the size of the reach in (z, y, x) order, input order within a cube, three stable radix passes) and each occupied cube's nine row runs are found once through a hash and kept, so a neighbor walk is nine ranges and visits exactly what a dense grid's walk visits, in the same order: that order fixes how each weight's sum rounds, so any change to it changes picks; the queue holds one entry per live point (four-ary) and is lazy the safe way (a lighter weight is found out only at the top and goes back in at its true weight; weights only fall, so a true top is the heaviest point there is), and ties break by input index, so the survivors equal the dense-grid pass for every input (BlueNoiseThinningTests holds that pass verbatim as its reference: every scatter the sampling tests make, a terrain at four counts, ties, one repeated point, a line, a shell, a spread below the cell floor; red under ties reversed, a stale top dropped unchecked, and the rows walked in reverse z, which visits the same neighbors and still changes one terrain count's picks through the sums' rounding). Measured on the valley terrain, release: 400,000 points from 13.4 s to 4.7 s for the whole call (the thinning alone about 12.4 s to 3.7 s; drawing the 2 million candidates is the other second), same points, hash-checked; what is left is memory latency, since drops land in weight order all over the terrain (a radix heap, coordinates packed with their weights, and an indexed decrease-key heap were each measured and none is faster, ARCHITECTURE.md); zero-area triangles are dropped from the table but triangle still reports the mesh's own numbering; SurfaceSample.alignment(spin:) returns the Euler angles a MeshInstance takes (x is left at 0 and the y/z pair already reaches every direction; a spin is composed and read back as XYZ), pinned against the matrix the renderer actually builds. Mesh.surfaceArea is public and is what turns a spacing into a count. SurfaceSamplingTests (14 tests: area shares, the fold's 1 - (1 - t)^2, the honeycomb spacing, the alignment matrix; four sabotages verified red). Examples/3D/Geometry/SurfaceScatter, Docs/Generators/SurfaceSampling.md.
  • Points inside a shape (Sources/Ollin/Geometry/ShapeSampling.swift). randomPoints(in:count:) scatters a Shape's fill (holes out, every island its share) by drawing from the renderer's own fill triangulation, a triangle by area through a running-total table (ShapeAreaTable) and the point inside it through the same square-to-triangle fold surfacePoints uses, so a thin or sparse shape costs no more than a compact one; randomPoints(along:count:) scatters the outline by walked length over every contour with two points (open ones included, a closed one's closing segment counted; OutlineLengthTable); poissonDisk(in:radius:candidates:maxCount:) is Bridson inside the shape. Load-bearing: the blue-noise form seeds from the fill's triangles and restarts from a fresh triangle-drawn seed whenever the active list empties, stopping when candidates fresh seeds in a row find no room, which is what fills a bar thinner than the dart annulus end to end and reaches every island (a single Bridson seed never crosses a gap wider than 2r); each dart still pays Shape.contains (a ray test over every edge). Every form takes a RandomNumberGenerator and the Sketch sugar runs on rng. ShapeSamplingTests (10: chi-square by band over a holed frame, a concave L, two islands; the outline by side length and by ring, the closing segment, an open path; the thin bar, the islands, the cap, the seed). Docs/Drawing/Geometry.md#shape-points, Docs/Generators/BlueNoise.md#shape; Guide Ch 15 § Chance inside an outline: points in a shape.
  • 3D (camera, depth, point clouds, meshes). Opt-in: camera(_:) / perspective(...) / ortho(...) over Camera3D (per-frame state) allocates a camera-gated depth32Float buffer (a 2D-only frame allocates none and renders byte-identically). Right-handed, y-up; 3D uses the camera, not the 2D CTM. Point clouds (PointCloud / drawPointCloud, instanced camera-facing splats). 4×4 transform stack (translate(x,y,z) / rotateX/Y/Z / rotate(_:axis:) / scale / transform(_ matrix:), the last composing a whole simd_float4x4 in one call for transforms that arrive whole — a streamed body anchor, a joint pose — verified pixel-identical to the equivalent TRS calls; a separate stack from the 2D CTM, bakes into geometry CPU-side). Solid primitives — the public Mesh value type + a broad generator catalog (drawBox / drawSphere / drawIcosphere / drawCylinder / drawCone / drawCapsule (+ drawCapsule(from:to:radius:), spanning two 3D points end to end with its round tips on the points — the solid bone/strut) / drawTorus / drawTorusKnot / drawPyramid / drawHelix / the Platonic set / drawMobius / drawKlein / drawSuperellipsoid / drawSupershape / drawExtrude / drawLathe / drawTube) on a depth-tested mesh pipeline. Type as a solid — drawText3D (sugar) over Mesh.text / Mesh.textGlyphs extrudes an outline font's glyphs: size is the em in world units (not textSize's canvas points), the block centers on its own ink, align only lines up the rows of a multi-line string, and a space contributes no glyph mesh. Two invariants, both sabotage-verified in MeshTextTests: the glyphs are traced at a fixed reference em of 512 and scaled down, because a glyph's contours are RDP-simplified at a fixed 0.3 tolerance in the size they are asked for, so a one-unit em collapses (a five-letter word falls from 884 faces to 12); and canvas-to-world is a half turn about x, never a y mirror, because a mirror reverses every triangle's winding and leaves all 884 faces inside out while both orientation probes still pass. A non-outline font notes once and draws nothing (no filled outline to raise), and the result carries no UVs, as an extrusion emits none. Examples/3D/Geometry/SolidType, Docs/3D/3D.md#solid-type. Generator meshes are consistently wound outward, and their normals agree with that winding — load-bearing because nothing culls back faces and every mesh carries its own normals, so a badly wound surface still draws and the defect hides, while everything reading a mesh as a connected solid (a soft body's springs and its inside, a subdivision cage's orientation, a traced ray's facing) reads the winding: Mesh.cylinder wound both cap fans into the solid (which detonated a soft cylinder in half a second), pyramid/roundedBox/extrude/cone carried versions of the same, Mesh.plane named +y and wound its quads to face -y (an open surface, so the closed-solid probe never saw it; the ink line's hull, which culls the faces toward the eye, drew every floor as a slab of ink, 2026-09-14, and the plane now sits in the normals-agree-with-winding probe; the fix moved three Guide figures, which is the second lesson: the vendored MikkTSpace reads the bitangent sign off the UV triangle's winding, so a mesh wound against its normals draws its parallax and normal maps mirrored along v (the Bench tile), and a soft body built from it takes a different constraint order, so a chaotic scene lands elsewhere (the Raft's crates, re-dropped from lower)), and parametricSurface derived du × dv against quads wound the other way (shading superellipsoid/supershape/mobius/klein from the unlit side). MeshTests pins both invariants across the catalog; a supershape's ring beside each pinch is the one allowed exception (its central differences measure nothing there). Per-vertex mesh colors — Mesh.colors (aligned with positions, the uvs rule) multiply the resolved surface color at the drawMesh vertex bake (fill × material baseColor × vertex color, the texture contract — white fill shows a scan's colors untouched, fill stays a whole-mesh tint); an empty colors takes the constant-color path byte-identically, a wireframe ignores them (edges are the stroke), no shader/stride change (the mesh vertex already carried per-vertex color); painted via Mesh.colored(by:) (position+normal closure) / colored(from: PointCloud) (nearest sample over PointGrid3), and read from file — glTF COLOR_0 (VEC3/VEC4 × float / normalized u8 / normalized u16, re-encoded linear→sRGB like every glTF factor) + Model I/O's MDLVertexAttributeColor (PLY per-vertex color, already display values so no re-encode; the two conventions differ and mixing them is the bug); a primitive/mesh without colors fills white (the multiply identity, so a partial set is lossless) and colors survive if any did, unlike the all-or-nothing uvs rule, while a file with none yields empty colors (the constant-color path stays byte-identical). Model I/O gotcha: vertexAttributeData(forAttributeNamed:as:) returns garbage (the first vertex repeated) when the requested format changes the component count and never returns nil, so read the descriptor's declared format and match its width (low byte of MDLVertexFormat.rawValue). subdivided(_:) carries colors, refined by the same masks the positions take (mirrored beside the position math, never folded into it — the position path is byte-pinned by the snapshot + the five-fold-symmetry test; the weld keeps the first-claiming vertex's color, and Loop's limit push applies to colors too). MeshColorTests (transforms + Metal-gated multiply/variation probes) + MeshLoaderTests + SubdivisionSurfaceTests. Lights & materials — directionalLight / pointLight / spotLight / ambientLight + Light, Blinn-Phong, auto-lit by default (.auto / .custom / .off, lights() / noLights()); curated lightingPreset(_:) / LightingPreset; camera-relative lights — Light.frame (.world default / .camera), relativeTo(_:) on a light and on a preset, Light.headlight / headlight(_:intensity:specular:softness:) (a directional down the view, castsShadow: false since from the eye every shadow hides behind its caster): resolved in one place, Drawer.resolvedLights (activeLights for the packer and the spatial export, the lens flare and the ocean's sun read it too; a kind check may still read lights), through Camera3D.basis (the view matrix's own right/up/back in doubles, the world axes for a degenerate camera), position offset from the eye and direction/up turned, .camera with no camera reading as world; a frame with only world lights takes the same array, no arithmetic (byte-identical). CameraLightTests (pixel invariant: a headlit sphere's brightness centroid stays at the disk's center across an orbit while a world light's moves); the inked-solids snapshot; Docs/3D/3D.md#camera-relative, Guide Ch 26 § The cartoon look: outlines, and a light that follows the camera. The ink line — outline(width:color:) / noOutline() / Material.outlineWidth + outlineColor / outlined(_:color:), an inverted hull: OllinMaterial.outline (rgb linear, w = width in canvas pixels, the gate) drives a second draw of the same vertex run through ollin_mesh_outline_vertex/_fragment right after the lit draw (PipelineKey.meshOutline), each vertex pushed along its clip-space normal by 2·width/viewport · clip.w so the width survives the divide as pixels at any distance, setFrontFacing(.counterClockwise) + setCullMode(.front) for that draw only (the generator meshes wind CCW seen from outside; the cull goes back to .none after, since nothing else in the frame culls), depth-tested and written like the surface. Solid and textured .mesh3D batches only (never wireframe/grid/matcap, instanced, field, or the path-traced arm, which composites the traced layer and skips it); a width or color change breaks the batch through Material's equality. A hard-edged mesh opens a notch at each corner (documented, not hidden). MeshOutlineTests (the line rings the silhouette and never the face, which is also the winding check, verified red by culling .back; the run holds its pixel count at twice the distance; a nearer sphere hides a farther one's line, with the near sphere removed as the control; width 0 is the byte-identical frame); Docs/3D/3D.md#outlines; area lights — rectLight / diskLight / tubeLight (Light.rect/.disk/.tube, kinds 3-5): linearly-transformed-cosine shading (rect = the exact horizon-clipped polygon integral, disk = the ellipse eigen + cubic + tabulated clipped-sphere path, tube = the analytic line integral × radius, capless) through the bundled fitted tables (Resources/LTC/, BSD-3 + notice, float32 from the reference ltc.js) at fragment textures 8/9 on every meshLitColor carrier (solid / textured / raymarch / half-res tier), gated by OllinLighting.ltcEnabled → byte-identical with no area light; intensity × color = the surface's radiance (physical falloff — the punctual kinds keep no-attenuation); load-bearing: the packed frame is right-handed (tangent × bitangent = facing normal — the shader's corner winding and disk front-test depend on it), the cubic/eigen sqrt args clamp at 0 (float dip under exact-math-nonnegative → NaN black speckle where a surface grazes the disk's plane, a real fixed bug), non-PBR materials map specularSharpness → perceptual roughness (2/(s+2))^(1/4) with specular = norm channel × mat.specular (inert at 0, the finish rule), the RT-reflection hit shade evaluates the exact LTC diffuse per panel (ollin_ltc_diffuse, kept in step with ollin_ltc_light's dispatch; only the disk reads a table, so the one amp texture is carried into the trace — bound at texture 9 of the deferred pass, whose lighting must also resolve ltcEnabled or panels go dark only in deferred reflections; hit specular stays the traced second bounce, matching the punctual Lambert-only treatment; snapshot area-reflections (RT-gated) + AreaReflectionRenderProbes), and the SSS back term is the flipped-normal integral (carries the panel's falloff). Area (rect/disk) cast shadows — the caster search appends rect/disk after the punctual kinds (existing scenes keep their caster; tubes never cast — radial emission has no facing axis): the penumbra comes from the panel's real extent (RT device: rays to deterministic points on the actual panel surface — Vogel in a disk's plane, antithetic R2 pairs on a rect, each ray weighted by what its point sends the receiver (cos at both ends over d², visible weight over the whole weight, the ratio estimator, so the factor is the share of the panel's light that arrives: the plain count kept 0.213 under a blocker's edge where the share is 0.091, and a point behind the panel or under the receiver's horizon darkens nothing; under the export's shared passes each pass weighs its own rays against the whole set's weight, geometry alone and no ray, times the pass count, so the passes' mean is the whole set's ratio, where a pass of one ray weighed against itself is its own 0 or 1 and the passes averaged back to the plain count, which TracedShadowPassTests caught); else a spot-style map aimed at the camera target, PCSS radius = half-extent in texels capped at 40), shadowSoftness scales that extent (0 hard / 0.5 default = physical / 1 = 2×), the RT flip overwrites shadowDepthB with the softness scale (the map path needs the slot for the perspective linearization), and the shadow dims both LTC integrals inside the area branch (atten on diffI/specI → every model + SSS dim consistently; no caster multiplies by exactly 1.0, byte-identical). Example 3D/Lighting/AreaLights (its softbox breathes, the area-shadow exercise); snapshots area-lights + area-shadows (RT-gated); LightingTests packing + AreaLightRenderProbes (one-sided / two-sided / falloff) + AreaShadowRenderProbes (on/off diff isolates the shadow; path-agnostic, so they cover the map fallback too). Mechanism: ARCHITECTURE.md § Area lights + § Shadows. Light shaping (IES profiles + cookies) — IESProfile (LM-63 Type C parser: tokenize all numbers after the TILT= line, real files break the line rules; lateral symmetry keyed to the last horizontal angle 0/90/180/360 + the rare 90→270 case; candela blocks per horizontal angle with vertical fastest; normalized to peak 1; init? never traps — header Int(Double) overflow guarded) attaches to point + spot via profile: (axis: aims a point light, default straight down; roll: spins profile azimuth + cookie together), and LightCookie attaches to spots (resamples an Image once into its own fixed 512² buffer at init — it exists because Light is Equatable/Sendable and Image is neither; GPU-backed images fail to wrap). GPU: profiles bake 256×64 r16Float layers and cookies sRGB layers into two texture2d_arrays at fragment textures 10/11 on every carrier (solid / textured / raymarch / half-res + the deferred RT-reflect trace), content-hash-cached with a fresh texture on change (never replaced in place), stand-in a 1×1 array (the 2D strip is type-invalid for an array slot); iesEnabled / cookieEnabled copy the ltcEnabled discipline (the same three set-sites, deferred pass included — same pattern-lost-only-in-reflections gotcha) → byte-identical featureless frames (whole suite un-recorded); layer indices are packed into the new OllinLight.shaping float4, deduped by makeLighting into Drawer.usedIESProfiles/usedLightCookies (rebuilt every call, so repeated per-frame calls agree). Shaping applies on the light's local copy (L.color/.specular *= tint → every shading model and ollin_rt_direct pick it up); ollin_light_frame is the projector convention (right = axis × ref — the cross(ref, axis) order mirrors the projected image, a caught bug); cookie edges land at the outer cone (the [0,1]² square contains the cone circle, so the edge clamp is invisible); a profile is dark outside its measured range, so a 90°-capped file sends nothing above the fixture's horizon — a wallwasher tilts its axis at the wall (the example's lesson). Example 3D/Lighting/LightShaping (authored .ies resources, never manufacturer data); snapshot light-shaping; IESProfileTests (parse / symmetry folds / ordering / bake / packing) + LightShapingRenderProbes (ring profile / cookie orientation / roll). Atmosphere (fog + aerial perspective + volumetric light) — fog(_:density:heightFalloff:) / noFog() (exact closed-form distance/height transmittance per fragment) + aerialPerspective(density:haziness:heightFalloff:sun:) / noAerialPerspective() (the Hoffman-Preetham wavelength split: per-channel extinction over the same optical depth + closed-form sun in-scatter, Rayleigh + HG phases) + volumetricLight(_:anisotropy:) / noVolumetricLight() (the Toth-Umenhoffer single-scatter march: directional + spot beams shaped by cone/IES/cookie, shafts carved by the 2D-map caster's shadow) + volumetricQuality/volumetricSteps (the fourth RenderQuality parameter, 16/32/64 by tier, both lighting-resolution sites). Load-bearing: the constants are stored in three OllinLighting tail fields gated by fogColor.w (0 → byte-identical, suite-verified) and pack before the .off early return (unlit scenes fog); surfaces fog themselves to their own depth and the air is a fullscreen skybox-slot draw (.fogAir, premultiplied (inscatter, 1−T)) so call-ordered compositing needs no stored scene depth (splats/particles composite over the beam-painted backdrop, 2D never fogs, a batch-less frame skips the air draw); the march's ray∩cone span bounding (thin beams otherwise dissolve into a woven lattice or vanish un-jittered), light-leg extinction (the integral is otherwise unbounded along a cone; beams-only mode substitutes a 0.05 reference density for σs and that leg), and per-step re-read jitter (a single shared offset reprints the jitter pattern as a lattice) are each a fixed real defect — don't simplify any away; jitter is a pure function of (pixel, step) so exports reproduce with no temporal history; the RT hit shade applies the analytic leg inside the shared trace (inline + deferred agree; env-miss sky stays clear, documented). Aerial invariants: it is mode 2 on the fog gate (fogColor.w 0/1/2, so every existing > 0 gate stays armed and one compare picks the model) with two appended OllinLighting tail fields; ollin_apply_fog stays verbatim and aerial is a twin beside it (ollin_apply_aerial — the GI fast-math codegen rule; classic fog pinned pixel-identical by a clean-HEAD-worktree double record of all 182 refs); OLLIN_AERIAL_RAYLEIGH mirrors Drawer.aerialRayleighRatios by hand (the ollin_sdf_distance rule); the sun resolves explicit → the .sky env through its rotation (world sun = R_y(−rotation)·(0, sin e, cos e), sign confirmed against the drawn disc) → first directional light negated → a default elevation, and its radiance is airmass-reddened white (a horizon sun feeds the veil sunset color, no parameter); a nil density derives as 0.35/eye-to-target (the contact-shadow default rule); the air veil steps aside behind a skybox (renderer-set fogParams2.z; the sky already is this scattering at infinity — beams still add), and with no backdrop the air paints the implied horizon glow; long paths saturate toward the sky tone by design (the closed form's equilibrium — a milky frame means density too high for the framing, not a defect). Examples 3D/Effects/Atmosphere (fog + aerial on one colonnade) + 3D/Lighting/VolumetricLight; snapshots fog + volumetric-light + aerial-perspective; VolumetricTests (packing + the ordering/shaft/beams-only probes) + AerialPerspectiveTests (packing/sun-resolution/reset + the blue-veil/sunward/haziness/sky-byte-hold probes, sabotage-verified). Mechanism: ARCHITECTURE.md § Atmosphere. Docs/3D/Atmosphere.md. the stylized material(_:) / Material library (toon / gooch + specular / Fresnel-rim / subsurface / iridescent / sparkle-flake finishes over a per-batch uniform; the fake-subsurface bleed is the distorted-lobe translucency approximation plus an always-on ambient floor weighted thinner-at-the-silhouette, so .jade/.wax read under a front-lit rig, not only backlit (ollin_sss_translucency, shared by both mesh fragments); the library's everyday-finish roster was retuned by eye in the material explorer (clay/rubber sheen made visible, beetle's bands deepened, frostedGlass roughness 0.2 so blurred structure survives) and gained fixed .clearGlass + .gummy entries (a built-in preset keeps every field it touches within the explorer's parameter set — no color-carrying presets, so the menu's snap reproduces each one exactly and the "(tweaked)" caption stays honest); a finish writes itself back as source — Material.swiftSource (3D/MaterialSource.swift) is the expression that rebuilds it, a curated preset as its dotted roster name, otherwise a Material(...) call in the initializer's order carrying only the fields whose formatted text (four decimals, shortest Color form) differs from Material()'s, so parameter float dust never prints and a default change never drifts (the diff reads the live defaults, the reason this emitter lives in the core rather than beside the project emitters, which depend on nothing); the explorer's C key copies it (any other key still steps the roster), and MaterialSourceTests pins the roster names, the order, the dust rule, and — through a swiftc -typecheck gate over a kitchen-sink expression with every field changed, sabotage-verified red on a misspelled label — that every printed label is one the initializer takes; sparkle cells sized by OllinLighting.sceneScale so the default works at any scene scale; soap-film mode — iridescenceFlow/iridescencePhase/iridescenceFlowSize turn the iridescent rim sheen into a thickness-driven film: drainage via the normal's y (quadratic, so interference bands crowd toward the bottom) + a domain-warped 3D-fbm swirl (fbm displaced by a vector of two more fbm reads — single-octave noise only makes blobs, the warp+octaves make the sheared marble wisps; fbm(float3) added to OllinShaderLib mirroring the CPU name) in sceneScale-sized cells scaled by flowSize (the sparkle-size rule — a small bubble under a far camera needs ~0.3-0.45 or the swirl is near-constant across it), colored by per-RGB-wavelength two-beam interference (rates 1/1.2146/1.4513 — zero thickness goes dark, the black-film cap; a exp(-0.18·d) broadband rolloff washes thick film pale), phase is the sketch-driven clock (no hidden time), flow 0 keeps the plain-sheen expression verbatim (byte-identical, suite-verified); example 3D/Materials/ThinFilm composes it on thin .glass() for its floating bubbles — the merged host also calls sceneThroughGlass(), so overlapping bubbles ease back to the environment per the one-piece-of-glass rule; snapshot soap-film); the physically-based metallic-roughness finish (Material.physicallyBased + .metal(roughness:) / .dielectric(roughness:) + the brushed/polished/plastic built-ins) — Cook-Torrance GGX (glTF 2.0 / Filament forms, reimplemented from the math), surface color stays fill; it's shadingModel 3, so every non-PBR material stays byte-identical. Glass (transmission & refraction) — Material.glass(roughness:ior:thickness:attenuationColor:attenuationDistance:dispersion:) / .frostedGlass: the KHR-transmission/volume/ior model built on the PBR finish (no new shading model), the transmitted lobe replacing the diffuse one (mix by transmission·(1−metallic) in the IBL ambient; punctual + LTC diffuse scale by the same diffKeep). Dispersion (Material.dispersion, 0…1 on the caustics(dispersion:) scale: red and blue refract at ior·(1 ∓ 0.03·dispersion) around green, ollin_channel_iors in Shader3D.metal, so a body's fringe and its caustic agree): every transmission read splits per channel where it is on: ollin_env_refraction (three cube reads), ollin_scene_refraction (one march per channel; the green read's coverage decides the pixel and a refused side channel reads where green did, so a fringe never punches a hole), the traced ollin_rt_refraction (one walk per channel through its trailing ior parameter, which sits after the bodyExit pair with no #if between them), and the path tracer (a path draws one wavelength at its first dispersive solid vertex and keeps it, taking the caustics photon tint so the ensemble sums to white; dispersion-free glass draws nothing, so its random stream and its frame are untouched). Byte-identical at 0: each path tests the gate before it does anything (the 247-reference snapshot suite and the traced reference pin it). Shows only on a solid body (a thin wall leaves every color parallel). Probes: SceneThroughGlassProbes (3: the scene read, the refracted environment, the traced walk, each measuring how differently red and blue moved between the clear and the dispersive render inside the ball while the pixels nothing transmits stay byte-identical; a change that moves the channels together reads zero, and all three are red by sabotage of the split) + PathTraceTests.dispersionSplitsATracedPathByWavelength (red by sabotage of the index shift; the tint alone reads 0.2 against the 3 the bar asks) + MaterialTests.dispersionDefaultsOffClampsAndPacks. Example 3D/Materials/SeeThrough (the fifth body); Guide Ch 28 § Glass with the Prism figure. Load-bearing: F0 now comes CPU-packed as mat.f0, the exact literal 0.04 at ior 1.5 (the computed form rounds to a different float — MaterialTests pins it) so pre-glass frames stay bit-identical; transmission is inert without an environment (nothing to transmit → shades as the plain dielectric, probe-pinned byte-equal) and inert at 0; the base path is env refraction (analytic interior span, curvature-blended exit normal, blur lod fading as ior→1, Beer-Lambert pow(att.rgb, span/att.w) with channels floored 1e-4 — a zero channel NaNs pow(0,0) under fast math); rayTracedReflections() upgrades glass too (one opt-in for mirrors + glass): the traced walk reads raw hit facing — a back face is the solid's real exit (refract out, trace on, traced span feeds Beer-Lambert), a front face inside is an embedded object, a thin wall hops its own shell's back faces (≤4), TIR carries straight on — and hits shade via ollin_rt_hit_radiance, extracted from the reflection trace so mirror and glass can't drift; refraction traces inline even in deferred frames (refracted content is minified; revisit on shimmer). A raymarched field owns no triangles, so it hands the walk its own exit: the accel structure holds .mesh3D batches only, so the interior leg can never hit the body and would otherwise take whatever stands behind the field for the far interface and absorb over the run to it (a green 0.95 sphere read G137 as a field against the mesh's G189, and followed the slab behind it) — the raymarch fragment sphere-traces the inside of its own field (ollin_sdf3d_interior_exit) and passes the exit + inward normal through ollin_pbr_ibl_ambient to ollin_rt_refraction, which uses it only while it is nearer than any traced hit (an embedded mesh still shows through the entry interface); the mesh path passes zero and is byte-identical, and the env path was always in step (its span is analytic). That march starts on the surface, where the interior distance is ~0, so it needs a floor under every step (else it crawls) and must latch on having really been inside before counting the exit (else the sub-epsilon residual the outward march leaves — which bands radially — decides it, printing the body in fine concentric rings); it returns a distance, never a failure, since the caller's fallback is the defect itself. The rings were invisible to a patch mean (the center matched to 0.6/255 through them), so the probe compares the two bodies pixel by pixel over a disc that stops a few pixels short of the rim — a square's corners reach the silhouette, where the outermost pixels legitimately differ (the AA models disagree, multisampled vs analytic, and the field's single inline reflection ray meets the rim-compressed mirror image a mesh's supersampled deferred layer averages); the rim's own shading agreement has its own probe, aFieldRimShadesLikeAMeshRim, above. v1 envelope: opaque shadows, opaque seen in mirrors / through other glass; what lies behind a glass field still comes from the mesh scene, so a second field is not visible through it. Example 3D/Materials/Glass; snapshots glass-materials + rt-refraction-3d (RT-gated); GlassRenderProbes (env-inert byte-equality / absorption-vs-span / traced-wall-through-pane / field-absorbs-like-mesh) + MaterialTests packing. Mechanism: ARCHITECTURE.md § Glass: transmission and refraction. Docs/3D/3D.md#glass. Anisotropic specular (the brushed streak) — Material.anisotropy (−1…1) + anisotropyRotation (radians, packed as cos/sin CPU-side): the Kulla at/ab split (at = α(1+a), ab = α(1−a)) over the anisotropic GGX distribution + height-correlated anisotropic Smith visibility on the punctual lobe in both lit twins, and the bent-reflection-vector environment gather (RT reflections trace the bent direction for free; coat + sheen keep the plain mirror direction Rmirror; LTC area lights stay isotropic, the fitted tables having no anisotropic form). The isotropic helpers stay the strength-0 path (the gate keeps every non-anisotropic frame byte-identical, suite-verified). The tangent frame is the real per-vertex basis on the nm/surface-mapped pipelines (orthonormalized against the shading normal), else the stable world fallback (ollin_ltc_any_tangent), which reads as a lathe finish on primitives (poles pinch by design); .brushedMetal carries the finish. Example 3D/Materials/BrushedMetal; snapshot anisotropy-materials; AnisotropyPackingTests + AnisotropyRenderProbes (streak-extent probes: extents and peaks, not region means). Docs/3D/3D.md#anisotropy. Clearcoat & sheen (layered PBR lobes) — clearcoat/clearcoatRoughness + sheen/sheenColor/sheenRoughness on the PBR finish (presets .carPaint(roughness:) / .lacquer / .satin / .felt): the coat is a second GGX lobe (Kelemen visibility, fixed 0.04 Schlick) with the base dimmed by (1−Fc) and its F0 remapped for the coat interface — the remap sends the default 0.04 to exactly 0 (the interface vanishes), so a coated base legitimately loses its broad spec and a probe must compare peaks, not region means (the first probe draft failed on this); sheen is the Charlie sine NDF + cloth visibility, no Fresnel, base scaled by 1−max(tint)·E. Load-bearing: E is the sheen LUT (ollin_ibl_sheen_lut, r16Float 64², baked lazily by ensureSheenLUT beside the three resolveIBL call sites when a frame's batches carry sheen — environment-independent but needed under plain lights, so it triggers off materials, not the env bake; fragment texture 12 on every meshLitColor carrier, stand-in otherwise); both lobes branch on their packed fields → byte-identical at zero (suite-verified, no re-records); under LTC area lights the coat runs a second LTC fetch at the coat roughness and sheen takes E·diffI (the broad-lobe treatment); under RT reflections the coat reuses the traced radiance (same mirror direction, no second trace) while sheen keeps the prefiltered env; a surface seen in a mirror shades without the lobes (the glass-in-glass envelope); OllinMaterial grew 176→192 (clearcoat fills the old offset-124 hole, clearcoatRoughness the 172 tail pad, sheenColor appended, strength premultiplied into its rgb with roughness in w). Example 3D/Materials/CoatAndCloth; snapshot coat-sheen; CoatSheenRenderProbes (point / panel / env coat peaks + the sheen rim) + MaterialTests. Mechanism: ARCHITECTURE.md § Clearcoat and sheen. Docs/3D/3D.md#clearcoat-sheen. Thin film (interference color) — thinFilm (0…1) + thinFilmThickness (nanometers, the one parameter in the material that is not in world units) + thinFilmIor on the PBR finish (presets .anodized / .oilOnWater / .nacre / .soapFilm(thickness:)): light reflecting off both faces of a clear film meets itself out of step, so the color is interference rather than pigment (soap bubble, anodized metal, oil on a road, nacre). Written from the published technique and credited in ATTRIBUTION.md; it lives as thinFilm / thinFilmF0 in the shared shader library's .color module, so a user shader can call it too. Two ways in, and the split is load-bearing: a term that traces one direction takes the film's reflectance in place of its Fresnel (ollin_pbr_film_F, the punctual lobes in both lit twins and the path tracer's BSDF), while a term that integrates a whole lobe at once (an LTC area light, the IBL split-sum) takes the straight-on stand-in (ollin_pbr_film_f0, the Schlick inverse at NoV) — feeding it the raw reflectance applies the angular response twice. A thickness of 0 ships strength 0 from the CPU (so the shader gate is one compare, and the model is never asked for a color it has no basis for) → byte-identical at zero, the whole snapshot suite passed unrecorded. The base F0 is the film's second face, so the same film over a conductor and over a dielectric are different colors. The sensitive dial is the optical path difference; the second interference order is worth about 1% at these interfaces, so a probe that drops it reads inert and proves nothing. Envelope: a body seen inside a traced reflection shades from the compact RT finish record, which carries no film (the coat's limit). Example 3D/Materials/ThinFilm; snapshot thin-film-materials; ThinFilmMaterialTests (clamps, packing, the zero-thickness gate) + ThinFilmRenderProbes (inert-at-zero byte-equality, the color spread, the walk with thickness and with the angle, the base's part in it, path-traced parity, and the fidelity check against an independent Swift reading of the model across 220–820 nm, sabotage-verified: a 15% error in the path difference fails it at many thicknesses). Docs/3D/3D.md#thin-film. Real subsurface scattering (the separable diffusion) — Material.scattering (0…1) / scatteringRadius (world units, the one scene-dependent number) / scatteringColor (per-channel falloff ratios, default the warm skin set) + .skin(radius:) / .marble(radius:): a screen-space diffusion, two ollin_sss_blur passes over the linear pre-tonemap resolve inserted between the geometry resolve and the frame filters on every render path (live / headless / benchmark; accumulation + texture-handoff keep their no-deferred-pass envelope), fed by a dedicated scatter-mask mesh re-encode with its own depth (occluders suppress hidden skin; matcaps occlude but never scatter). Load-bearing: the mask pipeline (isScatterMask) runs blending off — its alpha carries the profile index, which .normal blending would multiply into the color; the kernel is CPU-built and pure (25 importance-placed taps over the published sum-of-Gaussians skin profile stretched per channel by the falloff, normalized then strength-folded into the center tap, so strength 0 is exactly the identity; cached by profile, ≤8 distinct profiles/frame, extras reuse the last + note); the depth-gap guard measures against the scattering radius (tap fully cut at four radii, un-projecting the mask's step via 2·step·wFactor/P11) — never a fixed screen-space constant, which is proportional to radius and saturates on the surface's own curvature the moment the radius is a visible fraction of the object, silently disabling the whole blur (a real first-render bug); unmarked pixels pass through bit-exact and a no-scattering frame skips the stage byte-identical (whole suite unchanged, no re-records); OllinMaterial grew 192→224 (scatter float4: raw falloff ratios floored 0.001 + radius in w, scatterStrength + explicit pads); the lit fragments read those fields only inside the transmittance branch (gated on scatterStrength > 0), so every non-scattering shading path is untouched. Transmittance (the translucency half) — under a castShadows() caster the same material transmits through thin backlit geometry: thickness from the caster's own depth (2D map via the new OllinLighting.shadowLinearize [2][2]/[3][2] constants — the PCSS ratio needs only [2][2], an absolute distance both — with the receiver shrunk 2 map texels along its normal, each tap a per-corner-linearized manual bilinear read (blending perspective depths first bends the ramp; a nearest tap terraces), gathered over the diffusion's entry footprint (transmitGather2D: a world-anchored-rotated 13-tap spiral sized by the scattering radius in world units — never map texels — per-Gaussian-normalized so a constant-thickness slab reduces exactly to the slab form; a single-tap read re-exposes the caster mesh's own tessellation at grazing light, the audit's terminator stripes, and the fix is red-first-pinned by grazingLightLeavesTheTerminatorSmooth); cube via its stored linear distance; RT point via one closest-hit ray, meshRTThicknessOne, traced by the solid/textured fragments under the same material gate); ollin_sss_transmit is the closed-form slab integral of the same Gaussian table scatterKernel uses (w·e^(−s²/2v), hand-synced — the ollin_sdf_distance rule), thickness entering in kernel units (×3/radius); irradiance is the reversed-normal 0.3-wrap. The term reads the pre-shadow attenuation (a backlit face sits in its own body's shadow — dimming by it erases exactly the transported light) while still applying the cone gate + shaped light copy, and lands before the blur (diffused with the reflectance, the published treatment); only the casting light transmits, area panels don't, and every blur gate (strength 0 / no caster / field carriers) leaves the term byte-identical. The transmitted rim on a deep body is a tight bright feature, so an overdriven radius prints the blur's tap comb around it exactly as around a tight highlight (honest radii can't show it — measured, ARCHITECTURE.md). Envelope: solid/textured meshes on the main canvas (a target's mesh and a raymarched field note once and keep plain shading; specular blurs with the diffuse, the single-buffer form). Example 3D/Materials/Subsurface (space = bare comparison; castShadows on, so the swinging key transmits when behind); snapshots subsurface-scattering + subsurface-transmittance; ScatterKernelTests + ScatteringRenderProbes (terminator carry / red-outruns-blue / radius scaling / occlusion byte-identity / off-surface bit-exactness / reproducibility — verified red by sabotage) + TransmittanceRenderProbes (thin-vs-thick per caster kind / red-through-body / no-caster + strength-0 gates / reproducibility — the spot probe isolates the linearization, all verified red by sabotage). Mechanism: ARCHITECTURE.md § Subsurface scattering. Docs/3D/3D.md#subsurface-scattering. Image-based lighting (IBL) — environment(_:) / Environment lights the PBR materials from a surrounding HDRI via the split-sum bake (Karis; equirect→cube / irradiance / GGX-prefiltered mips / BRDF LUT, baked once per source and cached). The decode keeps a sun past half range: an HDRI is drawn into a single-precision context and packed to half by packEquirect with one prescale that brings its brightest component to 65,504, which IBLMaps.normalization divides back out (the cache blob's header is version 3 and carries it; the procedural sky's readback keeps its own half path); anEnvironmentsSunReachesTheScene holds at 200,000. The specular lobe keeps its energy: every physically-based path (punctual, area-light, IBL, path-traced) carries multiple-scattering energy compensation priced from that LUT's directional albedo (ollin_pbr_ess/ollin_pbr_energy_comp; single-scatter alone kept 36% of a white furnace at roughness 1). Three invariants: the LUT bakes with the same height-correlated Smith term the lobes shade with (a mismatched bake prices the lobe low and over-fills it — the k=α/2 form read 104.5% in the path-traced furnace); the LUT bakes for any frame carrying a physically-based finish, environment or not (ensureBRDFLUT(for:), the sheen-LUT trigger shape — without it a no-environment PBR frame reads the stand-in strip as its albedo); and the IBL form returns the dropped share through the irradiance, not the mirror gather (exact in a uniform surround; Favg = F0 + (1-F0)/21). The matte body keeps one minus the lobe's directional albedo at the view direction, for every light: the image-based ambient by 1 - (FssEss + FmsEms) in both twins (ollin_pbr_ibl_ambient and _mapped), every lamp, panel, caustic receiver, trace, and bounce by ollin_pbr_lobe_albedo (the split-sum F0·A + B times its ollin_pbr_energy_comp), so a white dielectric returns a uniform field at every angle and roughness. One minus the Fresnel at the view angle (the old ambient) kept 90.3% at grazing at roughness 0.5, and one minus the Fresnel at the half vector (the old lamps) 135% at grazing at roughness 0.2, since most of what a grazing view sees sits far from the mirror direction. A panel's diffuse takes the same weight (it once took none). White-furnace pins: EnergyConservationTests (raster, three roughnesses, bounded both sides) + theMetalFurnaceKeepsItsEnergy (traced). HDRI sources: 8 CC0 HDRIs bundled at 1K (instant, offline, CI-safe) + 12 download-on-demand (Poly Haven), user .hdri(path:) / .hdri(downloadURL:) / .remote(url:fallbackResource:), highRes(.twoK/.fourK/.eightK) backdrop upgrades (the lighting never needs >1K) — a remote source downloads through EnvironmentCache, whose blocking form is for the export path's main thread only (downloadBlocking parks the caller until a worker's task lands the file; from anything async, a test body included, it parks a pool worker, the other parked thread in every 2026-09-06 CI wedge sample), so an async caller uses download(_:), which suspends, and Scripts/preflight.sh refuses the blocking call under Tests/ — and the procedural .sky(turbidity:sunElevation:groundAlbedo:) — a zero-asset Hosek-Wilkie dome (vendored External/CHosekWilkie, BSD-3) generated with no CPU read-back (the read-back was the 3-fps bug), re-baked per frame at a reduced budget when animated (the HDRI path stays byte-identical), rotated(_:) spinning the sun for free; a neutral .sky() is the default placeholder while a download lands. A live feed lights the scene — Environment.feed(_ feed: VideoFeed) (the .feed(id:) source + the weak main-actor FeedEnvironments registry, EnvironmentFeed.swift): the feed's latest frame wraps into a GPU equirect (ollin_ibl_feed_gen — front hemisphere direct about +Z, back its horizontal mirror, pinched to the picture's center column at the poles, so the wrap is seamless) and runs the reduced-budget sky bake on the frame's command buffer (no CPU read-back). Load-bearing: the bake key carries a per-feed generation (IBLKey.feedGeneration, 0 for every other source) bumped on a new frame — newness is Image identity, and FeedBakeState holds the compared frame so a freed frame's recycled address can never read as "unchanged"; a GPU-backed frame (a live texture updated in place) re-bakes every resolve, the animated sky's own cost, and stale generations evict (the animated-sky discipline); bakeReady's HDRI/sky path is textually untouched (the feed branches out first; the whole suite passed unrecorded); auto-exposure is exactly 1 (a camera exposes itself; a real average would read every frame back); the factory sets showsBackground false (the wrap is for lighting, not viewing — draw the frame yourself); no frame yet / a deallocated feed → the neutral placeholder sky (the remote-download discipline). Example 3D/Environments/LiveEnvironment; FeedEnvironmentTests (5, the color probe sabotage-verified); no pixel snapshot (the input is live). Docs/3D/3D.md#live-environment. Volumetric clouds are part of the sky bake (Environment.clouds(_:), the Clouds value: coverage/density/scale/tallness/phase, presets .fair/.scattered/.broken/.overcast) — the Schneider/Nubis recipe marched inside the sky generation so backdrop + IBL + reflections see one weather and an overcast dims the scene by construction; load-bearing: ollin_ibl_sky_gen stays verbatim with the clouds twin beside it (the GI codegen rule), the bake cache keys on IBLKey (source + clouds; clouds nil for non-sky sources so a stray value can't fragment an HDRI's slot — the cache-key sabotage is red without it), the weather field thresholds fbm into hard-cored blobs (a soft field reads as veil everywhere), the tiling noise volumes are a fixed lattice baked once per process (ollin_cloud_noise_*, deterministic on any machine), the march jitter keys on the fragment's own pixel position (a hardcoded texel scale printed stripe combs when the cloudy bake doubled to 2048), the skybox auto soft-focus turns off only when the source is really .sky with clouds (keying on the clouds value alone changed an HDRI's backdrop, a probe-caught live defect), and phase is the deterministic wind clock (a still sky bakes once and costs nothing per frame). Example 3D/Environments/Cloudscape; snapshot sky-clouds; CloudTests. Mechanism: ARCHITECTURE.md § The procedural sky. Terse invariants (mechanism, caches, and downloads in ARCHITECTURE.md § 3D lighting and environments): mip the equirect before the equirect→cube pass (else the cube reads empty → black IBL, a real bug); the bundled set is PIZ half-float (ImageIO won't decode dwab); per-environment auto-exposure (the set ranges ~800× in brightness) so any HDRI just works; non-PBR materials take the diffuse irradiance as their ambient (Gooch keeps its own ramp) so one environment lights every material — all gated on iblEnabled → byte-identical with no environment; export blocks on downloads (full-res art) while live shows a placeholder; the decoded equirect is blob-cached (<hash>.equirectf16) and read off the render thread; the in-memory bake cache is LRU-bounded (IBLCacheTests). The environment draws as a skybox backdrop by default (bicubic-reconstructed, resolution-aware backgroundBlur; .lightingOnly() opts out). The irradiance bake reads each sample at the level its footprint asks for (log2 of the step over the env cube's texel angle), never the derivative's: the tangent frame flips where |n.y| crosses 0.999, the middle of the up and down faces, and the quad there read the coarsest level, so every floor under a structured environment lost light (14% of a hazy sky, 3 to 8% of a clear one; aFloorGathersTheWholeSky holds a plain floor to the model's level irradiance within 3% clear and 8% hazy, the hazy sky's sharp glow reading 2 to 6% high through the cube's box-filtered levels). The sky's sun (Environment.sunlight(_:), .automatic / .always / .off): the model's own solar radiance function (vendored with its solar and limb-darkening tables, the disc integrated by four Gauss-Legendre nodes in the limb cosine, exact for its quintic, against a 20,000-ring integral to 1e-7) through the CIE 1931 observer into linear sRGB, the sky's own unit (the RGB dataset is the XYZ one through the sRGB matrix within a few percent, and the observer reproduces the spectral Y within 1%), joins activeLights(in:) after a set's own lamps as one more directional light, so the shadow pass, the shafts, the traced export, the GI probes, the light grid, the ocean's sparkle, the flare, and the caustics' gate see it. .automatic joins a set with no directional light of its own (a sketch's key stands in as the sun, so preset-lit sky scenes keep their look); the frame's own set decides the bake (Drawer.skySunLit → IBLKey.sunLit). One function feeds both sides: SkySun (CPU) gives the beam, the sky's coarse average, and the horizon, and the bake's exposure and the light's scale both come from SkySun.exposure, so the light lands in the backdrop's unit to the last bit. While lit, the beam joins the exposure's average as its luminance over 4π (a photographed sun's pixels count the same way in a loaded map), the clear sky's 25× stand-in disc leaves the bake (radius 0 in sky[10].w) and the skybox draws the disc at the sun's true size (half of 0.51°) at its mean radiance, area-conserving below a pixel; a cloudy sky keeps its baked disc, dimmed by the clouds, and the beam keeps 1 − coverage. An unlit sky (.off, or a sketch's own sun) bakes byte-identically to before. Facing-to-away 8.66 against the model's 8.59, a sunlit floor against its shadow 3.48 and 1.90 against 3.45 and 1.89 at turbidity 2 and 8 (a separate program over the full upstream source; aClearerSkyCastsHarderLight). The aerial air under a .sky is the sky's horizon averaged over 64 compass directions, over the phase mix averaged the same way, at the backdrop's exposure and intensity (Drawer.aerialAirLight), so the far veil averages to the backdrop's horizon and the haze's halo still leans it sunward (theFarAirIsTheSkysHorizon within 4% of the model at 60, 120, and 180 degrees from the sun, read at the frame's middle since a wider read crosses compass directions the halo leans differently); without a sky the first directional light's linear color times intensity scales the old fixed sunlight, so the frame is linear in its light. (Environment is the bare public name, so Ollin's SwiftUI files qualify @SwiftUI.Environment, as they already do for Image.) matcap(_:) / Matcap (view-normal sphere-texture shading, 26 bundled CC0 looks + a CPU generator). Meshes from file — loadMesh / Mesh(contentsOf:): Ollin's own OBJ + glTF readers beside Model I/O (.usdz / .usdc / .usda / .stl / .ply / .abc), carrying base-color material + texture + UVs; bounds / center / normalized(scale:) to recenter+fit. Textured meshes (Mesh.uvs / MeshMaterial / mesh.textured(_:)), with TextureWrap deciding what every one of a material's maps does outside the 0…1 square (.clamp the default / .tile / .mirror, on textured(_:baseColor:wrap:) and MeshMaterial.wrap). Load-bearing: a sampler is a compile-time object in MSL, so the mode is a select between three constexpr samplers in each mesh fragment (and in the tracer's hit reads, for parity) keyed on OllinMaterial.uvWrap, never sampler state carried with a batch — the branch is uniform per draw, clamp is the zero, and the bound sampler is the clamp case, so an unstated material samples through exactly the path it always did. The three modes agree strictly inside the square and differ within half a texel of the edge (the filter reaches across), which is what makes turning tiling on safe for a mesh whose uvs stay in range; triplanar and the detail pair repeat regardless (no uv tile of their own). One mode covers the whole map set, including the parallax march's height reads, since a surface that repeats its color repeats its relief. A loaded model brings the file's own answer, and each format states it differently: glTF reads the texture's sampler and defaults to repeat when none is declared (the case that decides whether an authored tiling floor tiles), .mtl tiles unless map_Kd -clamp on, USD reads inputs:wrapS, Model I/O reports its own sWrapMode, and a file that says nothing gets clamp; the USD writer now writes the material's mode rather than a hardcoded repeat, so a round trip holds. TextureWrapTests pins it by predicate rather than a snapshot (four screen quarters over a plane whose uvs run 0…2: the third quarter is white clamped, black tiled, and white-then-black mirrored), plus a traced-export parity probe, byte-equality inside the square, and the four loader readings. Example 3D/Geometry/TexturedMesh (its floor tiles four across). Texture filtering — every decoded picture carries a mip chain (.generateMipmaps on both the color and the value/data loads; the hand-built sRGB fallback fills its own levels by blit), and the samplers are trilinear plus 16× anisotropic on the surface path only, with all three constexpr samplers written into the mesh fragment carrying both settings too — the tile/mirror wrap pair, the triplanar projection, and the detail pair (miss one and that read alone keeps crawling: a projected floor read 150.8/sd 103.2 and a decoded picture there was byte-identical to a one-level one, the reading that says the chain was never reached; a detail pair read 59.3/sd 46.9 the same way). The two-sampler split is load-bearing: imageSampler (the chain, the image quads, the glyph atlas, the gradient strip) stays at 1 tap because a screen-space pass reads one texel to one pixel — there is no footprint to resolve, and asking for one makes the sampler read a derivative that a pass returning early per pixel (the scatter blur's mark test, the sandpile's open edge, a disoccluded pixel in the temporal resolve) has no whole quad to form; that undefined read is what made a plain lit box render differently on every run on a scene with no picture in it (~450 bytes at up to 12/255). Those passes now name level(0), and only surfaceSampler, bound by the textured-mesh draws, resolves a footprint. A parallax read is the one uv the sampler must not derive from (the march steps it per pixel): its maps read through gradient2d of the geometric uv, or the silhouette moves 3px against its flat control. Load-bearing: generating levels on an _srgb format averages in linear light — a black-beside-white checker's level 1 reads 188, not 128 — which is the light the renderer blends in, so a picture holds its tone as it shrinks instead of darkening: a plane tiled 40× across 256px reads mean 127.5 / sd 127.4 at one level against 187.9 / sd 0.51 filtered. Only minified pixels move; the near half of a receding floor is byte-identical. The per-frame pixel-upload paths keep one level on purpose (Image(width:height:premultipliedRGBA:) and a subscript write re-upload on every frame a pixel changes, and the chain there costs ~0.7 ms/frame at canvas size to smooth a read a full-size picture never makes), as do the pass-through GPU-backed images (camera frame, effects layer) — so a hand-authored texture on a mesh still aliases, by choice. A grazing floor's far band goes from sd 0.51 to sd 8.20 with the taps on, tone held at 187.3 (the counterfactual is the same band of the same frame both ways). The traced export needed no work: its per-hit ray-cone ollin_pt_map_lod already picked a level and had none to read (16 spp: sd 18.2 → 0.51). TextureFilteringTests (11; the reproducibility probes are the ones that caught the global sampler, ParallaxTests.parallaxNeverMovesTheSilhouette the parallax footprint; the projected and detail probes each read the decoded picture against the one-level one, which is the only way to see a sampler that reaches no level at all). Docs/3D/3D.md § When it lands on fewer pixels, Docs/Drawing/Images.md. Wireframe (wireframe(_:)). Normal maps — Mesh.normalMapped(_:scale:) / MeshMaterial.normalTexture+normalScale / Mesh.tangents (MeshTangent, aligned with positions; generated by vendored MikkTSpace, External/CMikkTSpace, zlib — the glTF-named standard, bundled because matching its exact basis is the point). Load-bearing: the normal-mapped mesh is the textured path's twin pipeline (ollin_mesh_nm_vertex/_fragment, a normalMapped PipelineKey variant — never a branch in the shipped textured fragment, the verbatim-plus-twin codegen rule, so unmapped frames are byte-identical by construction); the tangent packs as half4 in OllinMeshVertex's 8-byte tail pad (OllinHalf4: Metal half4 / CPU simd_ushort4 bit patterns — stride stays 64), world-space via the model's linear part (a surface direction, not the normal's inverse-transpose; skins pose it with the blended joint matrix); stored handedness is glTF's sign = −fSign — measured, raw MikkTSpace makes fSign·cross(N,T) point along +∂p/∂v (image-down) on every surface while the spec's maps are green-up (image-up), so the ecosystem stores the negation — verified against the Khronos NormalTangentTest/MirrorTest oracles (authored TANGENTs pass through unchanged); a winding-vs-normals-inconsistent mesh flips fSign (never build a convention probe on one), and a "missing terminator" between spheres at ±x is perspective parallax, not a bug (both cost real debugging); the map samples as data, never color (Image.linearTexture(for:), the raw-bytes twin cache slot — an sRGB decode bends every stored direction); finish.normalScale (a claimed OllinMaterial tail pad) is set per batch only after uvs and tangents verify aligned, doubling as the encode and shader gate, with 0 routed down the plain textured pipeline byte-identically (probe-pinned) and a missing basis degrading honestly (geometric normals + noteOnce); shading uses the bent normal everywhere, the geometric normal keeps driving ray/shadow/transmittance offsets; glTF reads normalTexture (+scale) and authored TANGENT on both merge paths, generating per spec when absent — but a generation that would split vertices (a mirrored-UV seam) is refused on skinned/morph/part-aligned meshes (their arrays follow the vertex order); mesh rebuilds (welded, subdivided) drop tangents and generatingTangents() re-attaches. Example 3D/Materials/NormalMaps (maps authored from height functions); snapshot normal-maps; MeshTangentTests (12: basis pins + the green-direction/transform-bake/scale-0 render probes). Mechanism + the sign-convention record: ARCHITECTURE.md § Normal mapping. Docs/3D/3D.md#normal-maps. Surface maps (the PBR map set) — Mesh.surfaceMapped(metallicRoughness:occlusion:occlusionStrength:emissive:emissiveColor:) / the MeshMaterial map fields: a packed metallic-roughness map (glTF layout: roughness g, metallic b), an occlusion map (r), and an emissive map × emissiveFactor, all through one pipeline (ollin_mesh_maps_fragment over the shared nm vertex, the normal-map bend folded in behind its own gate). Load-bearing: per-pixel metallic/roughness reach the shading tail through meshLitColorMapped / ollin_pbr_ibl_ambient_mapped, mechanical substitution copies of the shipped tails KEPT IN SYNC BY HAND (the biggest such twin; edit the original, re-copy, re-substitute mat.metallic→pxMetal / clamp((float)mat.roughness→clamp(pxRough + the pxAO ambient scales — never grow the shipped originals, whose codegen every unmapped frame depends on); the sampled channels multiply (finish × the mesh material's factors × map — the drawer folds the mesh factors in only under mrGate, so material(.physicallyBased(metallic: 1, roughness: 1)), the new factory, shows a file's maps as authored); occlusion dims indirect light only (flat ambient in the twin, IBL/GI/flat-IBL at the fragment; direct untouched, pinned by a byte-equality counterfactual); emissive adds after lighting, before the atmosphere (a factor with no map routes down the same pipeline against a white stand-in; a black factor with a map emits nothing, the glTF default); mr/occlusion sample as data (linearTexture), emissive as color; gates are stored in the finish (mrGate/occlusionStrength/an emissive float4, claiming the last OllinMaterial pads + one appended row) so unmapped frames keep their exact codegen by construction. glTF reads the maps + factors/strength; USD reads connected metallic/roughness/occlusion/emissiveColor/normal inputs (an ORM-layout tap reuses the image — USDAssetStore caches decodes by path so taps compare === — else channels repack at load, untapped = white; factors are carried in tap scale, occlusion strength scale s / bias 1−s, normal strength scale (2s,2s,2,1) / bias (−s,−s,−1,0), each exact; USD normal maps generate tangents post-skinning under glTF's vertex-split guard); the spatial exporter writes every encoding back (reader's stated inverse; usdchecker --arkit accepts a mapped package, test-pinned). Envelope: primary surface only (reflection hits keep the per-vertex w-slot values; matcap/wireframe ignore the set). A surface glowing in its own color — MeshMaterial.emissiveIntensity (Mesh.glowing(_:) sets it on a copy): the surface-mapped fragment adds intensity × base (the resolved base: fill, base color, vertex color, texture, detail, decals) beside the constant factor, so a vertex-colored mesh keeps each vertex's hue where emissiveColor is one color a mesh; the gate is stored in the finish (OllinMaterial.emissiveIntensity, a row after emissive; usesSurfaceMaps and the drawer's surfaceMapped routing read it under the constant factor's own uv rule), zero leaves the sum untouched (intensity 0 is byte-identical to no glow, probe-pinned). The tracer adds intensity × albedo after the base map at every hit as a separate selfEmissive term credited whole (the mesh-light table weighs the factor alone, so no next-event strategy competes for this share; unbiased, the copy rule). The USD writer has one emissive slot, so it writes factor + intensity × baseColor in linear (a material with no intensity writes the factor to the digit) and connects the slot to the vertexColor primvar reader for a vertex-colored mesh with no other emission. SurfaceMapTests pins the red/blue vertex-colored quad glowing in each hue against a dark control, the zero-intensity byte identity, and the writer's color and connection; PathTraceTests traces the same quad. Example 3D/Materials/SurfaceMaps; snapshot surface-maps; SurfaceMapTests (13: loaders/round-trip/validator + the metallic-split, roughness-spread, indirect-only-AO, unlit-emissive, factor-multiply and gate byte-identity probes — the shader sabotages each verified red). Mechanism: ARCHITECTURE.md § The PBR map set. Docs/3D/3D.md#surface-maps. Height maps (parallax occlusion + CPU displacement) — MeshMaterial.heightTexture/heightScale + Mesh.parallaxMapped(_:scale:) / Mesh.displaced(by:scale:): one height map (r channel, data; white = the authored surface, darker carves in — the shared datum, so the shading fake and the real geometry agree) read as per-pixel parallax (linear search + one secant step in the maps fragment — slice-2 code exempt from the verbatim rule, gated on the packed parallax finish field so mapless frames keep their codegen; the shifted uv feeds every map sample; height at texture 21, usesSurfaceMaps routes it) or as real geometry (weld-aware: one averaged normal + height per welded position group, so the flat-shaded generators' coincident corners move together and a uv seam can't tear; smooth normals recomputed over the welded displaced surface, tangents regenerated). Load-bearing: the uv step per unit depth is scale/e.z · (−e.x, +e.y) — the v sign is opposite u's because the stored-handedness bitangent points image-up while v grows down (the slice-1 green-up fact; probe-pinned per axis, sign-flip sabotage fails each independently); march samples use explicit level(0) (the loop's exit is divergent, implicit derivatives undefined); the 1/e.z stretch floors at 0.1 (grazing views smear boundedly — the documented silhouette envelope); parallax is shading only — depth, silhouette, shadow rays, and reflections keep the flat surface (coverage-mask-equality-pinned, the teaching contrast with displaced); a white map is the exact null (byte-pinned on the same pipeline) and heightScale 0 routes down the plain textured path byte-identically; needs uvs + tangents like the normal map (parallaxMapped generates them; honest degrade with a note). glTF has no height-map slot (core or ratified extensions) so the map is Ollin-authored API there; USD round-trips it via the preview surface's displacement input (writer: a tap at scale s / bias −s, the exact spelling of s·(h−1); reader recovers heightScale from the channel scale, generates tangents for a displacement connection, and ignores a constant unconnected displacement — no relief in it; usdchecker --arkit-validated). Example 3D/Materials/Parallax (one crater map: parallax / displaced / bare, depth as a live parameter); snapshot parallax-relief; ParallaxTests (12: direction per axis, silhouette hold, both nulls, weld/datum/seam, USD round trip — sabotage-verified red). Mechanism: ARCHITECTURE.md § Height maps: parallax occlusion and displacement. Docs/3D/3D.md#height-maps; Guide Ch 27 § Depth from a picture: height maps + HeightRelief figure. Triplanar projection (texture with no uvs) — Mesh.triplanarTextured(_:normal:scale:normalScale:baseColor:) / MeshMaterial.triplanarScale (world units per tile; 0 = uv mapping): the base texture (and a normal map) project flat along the three world axes and blend by the surface normal, for the meshes that carry no uvs at all (isosurface/metaballs, growth, subdivision, reconstruction) — no uvs, no tangents, none generated. Runs in the surface-mapped fragment behind a mat.triplanar gate (tiles per world unit in the tail pad parallax left, stride unchanged; the slice-2 branch-freely exemption re-verified empirically: full snapshot suite unrecorded + guide-figures zero churn). Load-bearing: the sampling space is world space, decided (the drawer bakes the model matrix CPU-side; the envelope stated everywhere: abutting meshes continue one pattern, a transform-animated mesh slides through it, pinned by theProjectionIsAnchoredToTheWorldNotTheMesh; a space: param can widen later); each axis's u flips with the normal's side so the picture reads unmirrored from either direction (keyed on the normal, not the viewer — the back of a single-sided quad legitimately mirrors, a probe-caught lesson), v runs down the image, each frame right-handed so the normal map keeps its green-up decode; weights are |n|⁴ renormalized; the per-plane normal combine adds the tangent-plane push onto the geometric normal expressed in the plane's frame and multiplies the heights (a flat map returns exactly the geometric normal); samples through its own repeat constexpr sampler (the mesh sampler clamps). v1 carries base color + normal only — MR/occlusion/emissive/height stay uv-mapped (noteOnce), parallax is the technique-level cut (one uv-space march can't follow three projections); constant emissive + material(_:) finish compose. Neither glTF nor USD has a triplanar slot → Ollin-authored API only; the spatial recorder notes and strips the projected maps. Example 3D/Materials/Triplanar; snapshot triplanar; TriplanarTests (9, the u-flip / hard-pick / top-v / green-direction sabotages verified red). Mechanism: ARCHITECTURE.md § Triplanar projection. Docs/3D/3D.md#triplanar; Guide Ch 27 § A picture from three sides: triplanar + TriplanarSkin figure. Detail maps — Mesh.detailMapped(_:normal:scale:amount:) / MeshMaterial.detailTexture+detailNormalTexture+detailScale+detailAmount: a finer texture pair tiled detailScale times across the base uv in the surface-mapped fragment (the branch-freely exemption again; textures 22/23, both data reads through a repeat sampler). Load-bearing: the color map's neutral is byte 128 in a data read (the sample ×2 multiplies; an sRGB read would put the neutral at ~188, which no author would guess) so a flat-128 map is near-identity and strength: 0 is the exact off switch (the drawer never raises the gates → plain-pipeline byte-identity, probe-pinned); the detail normal blends by Reoriented Normal Mapping (a flat detail hands back exactly the base normal — the identity an overwrite blend fails, probe-pinned) and its branch recomputes the bent normal from scratch (one redundant base sample only when detail is on) so the shipped base-normal resolve stays textually untouched; OllinMaterial grew 256→272 (detailScale/detailAmount claimed the tail pads + a detailGates row) and the usesSurfaceMaps union gate is detailScale > 0, packed only for a verified uv-mapped mesh (never with triplanar; the detail-normal half needs tangents like a normal map, each half degrading honestly alone); no mips like the whole mesh texture path, so extreme tile counts shimmer (documented envelope); no glTF/USD slot → Ollin-authored only, the exporter notes-and-strips. Example 3D/Materials/Detail; snapshot detail-maps; DetailMapTests (sabotage-verified). Mechanism: ARCHITECTURE.md § Detail maps. Docs/3D/3D.md#detail-maps. Decals — Decal (the LightCookie construction + kept aspect) + the per-frame drawDecal(_:at:direction:width:height:depth:roll:opacity:): a picture stamped through an oriented box onto every lit mesh surface inside it, composited over albedo before lighting (the stamp takes the surface's own finish), capped at OLLIN_MAX_DECALS (8), repeated placements sharing a texture layer, later decals compositing over earlier in call order. Load-bearing: the decal loop lives only in the surface-mapped fragment, and a frame with placed decals routes every lit mesh batch through that pipeline at encode time (wireframe/matcap/grid keep their own) — no shipped fragment grows, so a no-decal frame is byte-identical by construction (a decal-carrying frame shading plain meshes through the maps fragment is the feature's own envelope); solid meshes use the nm vertex safely because OllinMeshVertex() zero-fills; the CPU builds each world→unit-box transform once (Drawer.placeDecal, the cookie projector-frame convention, +y in box space = the image's top); the composite is premultiplied with one scale on both halves (fades can't fringe the edge), a facing fade melts the stamp off edge-on surfaces instead of smearing it, and a depth-end fade keeps the box's far planes from hard-clipping; the list is carried in its own OllinDecals uniform (fragment buffer 2, count-gated) beside a content-hash-cached sRGB texture2d_array at texture 24 (shapingStandIn() when empty); reflections show the undecaled base, fields/particles/point clouds sit outside by design, and the spatial exporter notes placed decals stayed behind. Example 3D/Materials/Decals (declares loopDuration); snapshot decals; DecalTests (placement pinned against a drawn marker at the same world position; sabotage-verified). Mechanism: ARCHITECTURE.md § Projected decals. Docs/3D/3D.md#decals. Cast shadows — castShadows() / noShadows(): directional/spot via a 2D shadow map; point ray-traced on an RT GPU (inline intersection_query behind OLLIN_RT_SHADOWS), else a mid-point cube; rect/disk area panels ray-traced on an RT GPU, else a spot-style 2D map (the Area cast shadows rules above); shadowQuality(_:) maps hardware-relative ray counts. castShadows() alone is the whole ask for an area caster too: AreaShadowTests (2) reads the floor under three spheres against the same set unshadowed and finds the panel's shadow at the default count as deep and as wide as at 48 rays a pixel (mean 62 against 122 unshadowed on the M2, within 2 levels for every count from 2 to 48, and only a single ray reads deeper, having no penumbra); an explicit shadowSamples is for a picture that must match across machines, which is why the Guide's softbox figures name one, never for the shadow to show (a figure had read as needing it when its panel was throwing onto floor the camera could not see). More than one caster per frame — OllinLighting.shadowCasters[4] + shadowCasterCount (the OllinShadowCaster struct, stride 112) beside the single-caster fields, which mirror slot 0, so a one-caster frame is byte-identical. Every dependent system follows the whole list, each paying only for the casters a frame declares: the fog march taps each 2D caster's own layer, the contact pass marches once per caster and writes each factor into the mask channel its slot names (the mesh fragments then read channel cs), the SSS transmittance reads each caster's own layer / cube / traced thickness (meshRTThicknessAll, the meshRTShadowAll shape), the marched fields cast a channel per point/RT caster (meshFieldShadowFactor returns a float4, and the half-res pre-pass carries all four), a field carrier marches its self-shadow toward every caster and receives each one's map, cube, or trace, and the GI probe pass traces occlusion for each. Two exceptions, both deliberate: caustics emit from the primary alone (a frame's photon budget is fixed, so splitting it between lights only makes each pattern noisier), and the path-traced export reads no caster list at all, since it shadows every light physically (which also means a castsShadow: false fill does throw there — an open question, see DESIGN-NOTES.md). Pinned by EveryCasterRenderProbes (6 probes, each verified red with its own fix removed). Slot 0 is the primary caster and keeps the old priority (directional → spot → point → rect/disk); the rest fill slots 1+ in declaration order. A 2D caster's map layer is its slot index, so the shadow map is a depth2d_array (allocated at exactly the frame's caster count, a fresh texture when it must grow) and layer 0 is a constant for whatever still reads the primary; a cube/RT primary just leaves layer 0 cleared. One pass per 2D caster (depthAttachment.slice), each with its own lightViewProjection — the marched fields render into every one. A point light casts from any slot. The cubes are a texturecube_array with one cube per point caster (OllinShadowCaster.cubeIndex, ranked in slot order, so a cube primary is always cube 0), all rendered in one layered pass whose vertex adds a per-caster cubeBase (vertex buffer 3) to its face; the array is sized to the frame's cube count, never the caster cap, because a cube costs 50 MB at the shipped 1024 resolution. A ray-tracing device instead traces every point caster against the frame's one acceleration structure: meshRTShadowAll returns a float4 of per-slot factors and the shading tail reads rtShadow[cs] (slot 0's value and its ray count are the single-caster fields, so a one-caster frame is byte-identical). Two consequences that are easy to get wrong: the structure is now bound whenever any caster traces, so both sites that flip shadowKind to 2 must ask whether the primary is a tracing kind (shadowKind == 1 || casterGPUKind >= 3), or a directional key beside a traced point light loses its own 2D map; and one structure serves them all, so an area primary beside a point caster takes the built one rather than building a second. finalizeShadowCasters drops any extra caster whose own resource (map layer, cube, structure) was not produced. A tube never casts; the cap is 4. The rasterized mid-point cube is the fallback a GPU without render-stage ray tracing takes, and it is the one 3D path an Apple-silicon machine never runs, so it has to be forced to be seen: OLLIN_NO_RAY_TRACING=1 answers "this device cannot trace" (MetalRenderer.rayTracingAvailable(on:), read by both the renderer init and the shader compile that sets OLLIN_RT_SHADOWS), and CubeShadowTests runs its probes under it (also unasked on a GPU that genuinely cannot trace). Three invariants, each found broken and each verified red by sabotage: the face projection's y row is negated — the six face views use the standard cube-face basis, written for a bottom-left framebuffer origin, while Metal's is top-left and a cube face is addressed top-left in both, so rendering them unchanged stores every face upside down and mirrors every shadow to the wrong side of its light (u already agrees, so only that one row moves); the far plane is fitted to what the light reaches, in the renderer, not to the camera's framing radius (pointCasterFar takes the farthest corner from the light of pointCasterBounds, +2%, carried to the fragment by MetalRenderer.pointShadowFars, keyed per casting light, and applied in finalizeShadowCasters; the scan runs once a frame and is shared by every point caster) — the packing's camera-derived guess clips a caster further from the light than the camera is straight out of the cube, so it lights up and throws nothing. Every caster kind is in that bound, including the ones with no vertices to scan: a plain mesh contributes its own vertices, an instanced batch carries its base mesh's local bounding sphere through each placement matrix, and a MeshField answers with its cached localBounds() (each copy's sphere through its own matrix, stretched by the matrix's largestColumnScale — the conservative world sphere the cull kernel takes, so a copy is bounded here exactly as it is bounded there) transformed by the draw-time matrix. A frame drawn entirely from copies therefore fits like any other, where before it had nothing to fit to and every copy past the guess threw nothing. The one caster that still leaves the guess as a floor is a compute-written placement buffer, whose matrices never come back to the CPU by design (CasterBounds.complete reports it); the offset, bias, and tap spread are worked out at the receiver (2·d/OLLIN_POINT_SHADOW_RESOLUTION, the resolution now in the shared header for exactly this), with the depth bias scaled by the tangent of the incidence angle, capped at 12, and sized for the whole 3-texel tap spread — a flat bias measured where the camera looks leaves a grazed floor combing itself with rings, while a surface square on to the light has a tangent of zero and the figure it always had (the normal offset stays flat on purpose: moving the sample is what notches a box's bottom corners). Past the far plane a receiver reads lit, the same envelope the 2D casters state for a receiver outside their fitted frustum. On the M2 the whole cube path is inert, so all of it is snapshot- and figure-neutral (2201 tests, 49 probe figures unmoved); measured against the traced picture of the same scene, the fixed cube path sits at a mean 0.34–0.40 per channel where the old one was visibly wrong. Per-light opt-out Light.castsShadow / castingShadow(_:) / the castsShadow: argument on every factory and bare call — the curated LightingPreset rigs set it false on every fill and rim, which is what keeps .standard/.studio/.threePoint scenes rendering exactly as before (the Guide's 26 3D figures and 209 snapshots pin it). The lit mesh loop resolves a light's slot by scanning shadowCasters, and cs == 0 gates the primary-only terms (rtShadow, fieldShadow, meshFieldShadow); a field carrier takes the primary alone (it is in no map). Renderer-side facts settled after makeLighting (the traced flip, ray/tap counts, whether a map was produced) are carried back by finalizeShadowCasters. Example 3D/Lighting/ManyCasters; MultipleCasterRenderProbes (3 probes, sabotage-verified red) + 10 packing tests in LightingTests; PointCasterSlotTests (3 probes reading the same band with and against the caster, so they run on the traced path and under OLLIN_NO_RAY_TRACING=1 alike; the two feature probes verified red by sabotage, which read exactly equal both ways, the signature of a light that throws nothing); Guide Ch 26 § Two lights, two shadows. Soft (PCSS) shadows — shadowSoftness(_:) (0 hard → the legacy 3×3, byte-identical; 0.5 default contact-hardening; 1 very soft): blocker search → penumbra estimate → Vogel-disk PCF (shadowFactorPCSS, the Fernando/NVIDIA technique); the penumbra ratio must be formed in light-linear depth (a spot map linearizes via the projection's [2][2] term — details in ARCHITECTURE.md); the one parameter also drives the RT point caster's area radius (the 0.5 default reproduces the old look, point-shadows byte-identical); 2D taps via resolveShadowTaps2D (export → .detail); soft-by-default measured sub-tolerance, so no snapshot re-records (the SSAO silhouette-AA precedent). Contact shadows — contactShadows(length:) / noContactShadows(), per-frame state refining whichever caster castShadows() resolved (every caster kind, any Metal GPU): a depth pre-pass re-renders the solid canvas meshes from the camera (the .meshShadow pipeline fed the camera's jittered VP, the scatter-mask filter), then a fullscreen march (ollin_contact_shadow, ShaderCombine) walks each pixel's projected (uv, z) segment toward the caster and the mesh carriers sample the mask (fragment texture 16) into the same meshFieldShadow multiplier the marched fields use, so meshLitColor is untouched. Load-bearing, each a probed defect or a production-reference rule: the ray start is pulled 0.002 of the eye distance toward the camera (the no-normals self-occlusion guard; without it a static speckled crescent forms along every lit silhouette) and the occlusion test is a band around a threshold derived from a same-length view-axis ray's own device-z span scaled by max(0.07, 1/steps) (slope- and density-aware, so a thin edge can't streak); a first-hit binary verdict + static interleaved-gradient-noise start dither (the dither rule; the production reference's own TAA-off configuration) + a clip-space vignette fade; length is world units, nil = 2.5% of the eye-to-target distance (packed in makeLighting's framing block; the renderer zeroes the gate when the pass didn't run, so off / no-caster / targets / accumulation are byte-identical, suite-verified); steps = 8/16/32 by effectiveQuality(.default) (the TAA no-per-feature-parameter rule; measured M2 release 1080²: the whole term ~1.0 ms GPU); the envelope is the screen (off-screen geometry casts nothing, a curved surface picks up a little extra shading inside its silhouette, only the casting light dims; fields and target-drawn meshes sit outside the march by design). Example 3D/Effects/ContactShadows; snapshot contact-shadows; ContactShadowRenderProbes (sabotage-verified) + the LightingTests packing trio. Docs/3D/3D.md#contact-shadows; Guide Ch 26 § The seam under a resting thing: contact shadows + Seated figure. Ray-traced reflections (rayTracedReflections()) — a physically-based metal reflects the actual scene (off-screen geometry included, no SSR contact-streaks), the hybrid-rendering approach written from the technique (Apple's WWDC21 session + Metal sample). It integrates into the IBL specular, not a post-process (the traced hit replaces the prefiltered env sample; a miss shows the sky), with two-bounce, metalness-aware hit shading — the second trace is load-bearing for mirror corners: an unoccluded env sample at the first hit pipes the HDRI's bright floor through as a razor-thin streak no AA can remove (a real fixed bug; dead ends recorded in ARCHITECTURE.md) — and grazing-widened env lobes (ollin_rt_env_lobe), into which a rough hit fades its own traced bounce (smoothstep(0.12, 0.55, s1.rough), the primary surface's rule, so a matte floor reads matte in a mirror and through glass instead of turning into a second mirror; the miss branch already is that lobe, and a hit below the ramp is untouched, so the near-mirror snapshots didn't move). It reuses the shadow-caster accel (per-geometry offsets buffer; encodeShadowPass decoupled so the accel builds when shadows or reflections are active), gated by OllinLighting.rtReflections → byte-identical when off; every solid mesh is reflectable; needs an environment + an RT GPU, off-by-default, non-RT → no-op. Reflection AA is deferred + stochastic: live temporally accumulates (an SSRHistorySlot twin, statefulEncodeIsRepeat-guarded); headless/export instead averages N deterministic in-frame rays (4/8/16 by tier — historyless, so no warmup and video exports can't flicker; snapshot rt-reflections-3d re-recorded once, supersampled). Load-bearing: render targets + the raymarched fields keep the inline single-ray trace (a thin wrapper over the shared hit shade, so the paths can't drift), and no roughness-cone jitter without a spatial denoise stage (a stochastic cone reads as sparkle on brushed metals). Chain length is a parameter (reflectionBounces(_:), persistent, clamped 2…8, packed as OllinLighting.rtReflectionBounces): 2 is the shipped nested pair and stays byte-identical (the branch is > 2, so a default frame runs the prior instructions — the 52 figure probes and the three RT figures re-render identical), and past it a separate forward walk (ollin_rt_specular_tail) carries a throughput rather than nesting, since Metal has no recursion. The pair keeps its nested form on purpose: forward accumulation has to distribute its mix(deeper, lobe, blend) into blend·lobe added now plus 1 - blend carried down, which is the same number in exact arithmetic and not the same order of operations. Each step is one more closest-hit ray per reflected pixel; the walk ends at the env lobe when the budget runs out (a truncated chain reads as a dimmer image, never a hole), and the refraction walk shares the shade, so traced glass gets the same depth. A rough surface's reflection is a traced lobe rather than a mirror ray (glossyReflections(), persistent, off by default, packed as OllinLighting.rtReflectionGloss = the 0.75 roughness ceiling; 0 = the shipped mirror path, verified byte-identical by a clean-HEAD counterfactual on two RT scenes): each ray leaves along a microfacet drawn from the visible-normal distribution (Heitz's routine, alpha = rough²), the trace writes the ray beside its radiance on a second attachment (direction + hit distance, so the pass is MRT either way and one fragment serves both paths), and a resolve gathers its own ray plus 7 neighbors on a 1.25-texel ring, each weighted D_local(H)·NoL / pdf_neighbor and normalized by the summed weights, not the count (the ratio estimator: unbiased under differing roughness/normals, and it absorbs any constant). Two properties fall out of the weight rather than a rule: a mirror gives an off-lobe neighbor ~0 weight and stays sharp, and a rough pixel spreads over the whole neighborhood. Path roughness regularization is load-bearing and it is applied at every surface along the chain (minRough threaded through ollin_rt_reflection_trace → ollin_rt_hit_radiance → ollin_rt_specular_tail, defaulted 0 so the mirror path folds it away): a ray from a rough surface stands for a cone, so a satin floor looking at a mirror ball otherwise reads its pinpoint light back many times brighter than the ray beside it — white speckle. It travels at full width; a fraction was measured and leaves the speckle in (example floor grain 1.53 → 0.82). Lobe truncation was tried and rejected (the published answer to tail noise): measured against a 256-ray reference it is quieter and less right (2.4% error unbiased vs 4.5% at bias 0.4, where the feature off is 7.0%) — that trade was made for a one-ray budget, and this path spends more. Live = 1 ray/frame → resolve → the temporal EMA, whose history guard is a variance-clipping box, never a min/max clamp (mean ± γσ over a 5×5 window, γ and the blend set by the pixel's own reprojected motion, clip on the color channels only: a min/max box holds the live floor at single-frame speckle forever, a 3×3 window reads σ 0 between the sparse fireflies and crushes the history to a point, and alpha's near-constant channel would drag the rest through the clip's shared scale); export = the trace+resolve pair run 4× the mirror tier's rays (16/32/64) accumulated in-frame by ping-ponged targets, since the ray layer holds one ray and cannot describe an inner loop's N. The G-buffer's roughness ceiling widens from 0.55 to the gloss ceiling (a constant float on the G-buffer fragment) and the composite fades over [0.8·ceiling, ceiling] instead of [0.12, 0.55]; both gates protect the hand-back, so a test only reads red with both removed. Example 3D/Effects/RayTracedReflections (its satin row), snapshot glossy-reflections, GlossyReflectionTests (6, each verified red), Docs/3D/3D.md#glossy-reflections, Guide Ch 33. A hit is shaded in its own finish (OllinRTFinish, stride 128, one record per geometry then one per copy slot): the stylized shading models (toon cel bands, the Gooch warm-cool ramp, which replaces the ambient as the primary path does) plus the two light-independent layers that apply on top of any model (the Fresnel rim, the fake-subsurface wrap), so a surface reads in a mirror the way it reads head on, where before every finish reflected as the plain diffuse body underneath it. Load-bearing: the records are stored in the hit table's own buffer past a four-word-aligned base named in its two-word header (no tracing entry point gains an argument — the rule the hit records already follow), and the block is written guarded by that base rather than by the flag that set it (a block written at offset 0 lands on the header it is reached through, which is how the first sabotage read as a run-break artifact); a compact record is also the run-breaking key, so a geometry is finish-uniform without two batches breaking over something the trace ignores, and a frame with nothing stylized to say writes no table, no extra break, and the shade it always ran (zero base = the gate). A surface's picture rides in the same record (base, the texture's GPU resource ID, with the gate and the wrap in cool.w): a mesh drawn with a picture writes the table and breaks its run on picture identity and wrap; every record names a picture (the white stand-in when it has none, since a handle is read whole even behind a closed gate); a hit multiplies its albedo by the picture at its interpolated uv at level 0 (a ray carries no screen footprint). A layer's picture is drawn after the traced scene is built, so its record keeps the gate down in the table every earlier pass reads, and the main passes read a blit copy with the handle filled in (tracedPictureTable, right after encodeEffectTargets in all three render paths; a blit because the hit records of copies a kernel places are written on the GPU earlier in the frame), so no pass reads a texture it was not made resident for (useTracedScene marks rtMapTextures) or one not yet drawn. Copies and triplanar pictures carry none, the other maps stay out of the trace, and the path-traced build never raises the gate (its kernel calls the same fetch and then applies every map from its own table, so a raised gate multiplied the picture twice and read a triplanar one by uv, which PathTraceTests.aTriplanarMeshKeepsItsProjectionThroughTheTrace caught). TexturedReflectionTests (a layer's and a loaded picture against the same floor filled in the picture's colors, within 2 levels; five plants red: no multiply, one corner's uv, the layer copy skipped, the loaded stamp skipped, no run break). The two layered physically-based lobes are carried in the same record (ollin_rt_layer_lobes, applied at the first hit and at the pair's second surface, in the primary shade's own layering order): the sheen keeps the environment sample (its lobe is wide enough that the prefiltered environment is the honest integral) while the coat reuses the radiance the trace already found along that surface's mirror direction, the single-trace tradeoff ollin_pbr_ibl_ambient takes; the sheen's directional albedo comes from the same LUT the primary reads, so the trace carriers bind it at texture 12, the slot every meshLitColor carrier already uses. Out of the trace, and it is one rule not a list: a traced hit computes no light highlight at all, so the standard model's Blinn-Phong, the coat's own hotspot and the sheen's, and a panel's stylized highlight and subsurface wrap all stay with the direct view; iridescence and sparkle need their own fields; the deeper surfaces of a bounce chain keep the tail's plain shade; and Material.scattering's diffusion cannot reach a mirror at all (a screen-space pass over the primary buffer). StylizedReflectionTests (7: the mirror-holds-the-sphere control plus toon / Gooch / rim / subsurface / coat / sheen, each against the same material minus its stylized part so the reading cannot come from the roughness a non-PBR batch bakes from its specularSharpness, and each verified red by the feature-off sabotage at a residual of exactly 0.0); the measured band must clear the object's own raster entirely (a difference map against the control is what finds where it ends). ReflectionBounceTests (4, each verified red by its own sabotage: the tail disabled, a default of 3, the clamp dropped) over a two-mirror corridor probe; no snapshot (the default may not move). Docs/3D/3D.md#rt-reflections. Mechanism: ARCHITECTURE.md § 3D lighting and environments + § Deferred ray-traced reflection AA. Global illumination (globalIllumination(_:intensity:) / noGlobalIllumination() / globalIlluminationQuality(_:)) — real-time diffuse bounce via a re-traced probe field (the DDGI papers, written independently; ShaderGI.metal + encodeGIPass): a probe grid auto-fitted to the mesh geometry's AABB (per-axis counts adapt to the fitted volume's aspect under a fixed 512 budget, axes clamped 2…16, derived at fit time by giAxisCounts' smallest-isotropic-spacing search and held with the volume — a cube keeps 8×8×8, a flat scene spends 16×2×16; the grid is laid so the scene's bounding faces sit half a cell inside the outer probe planes (giFitVolume: on an axis with three probes or more the raw extent spans two cells fewer than there are probes, so no face sits on or a hair off a plane, which put a ragged dark seam on a room's open end, a row of probes at the walls' front edges flipping in and out of the visibility test; a two-probe axis keeps the 15% pad; GlobalIlluminationTests.theFittedGridPutsEveryFaceMidCell); atlases allocate once at budget capacity so a refit never reallocates; the bounds fold over the same batch walk as the accel, target-drawn meshes included, so a scene drawn entirely inside a render target still gets a field), each probe an 8×8 octahedral irradiance tile + 16×16 distance-moments tile (1-texel gutters recomputed in-pass by octahedral wrap), traced per update with a rotated spherical-Fibonacci fan against the shared accel and sampled by every lit carrier (textures 13/14/15) through trilinear × soft-backface × Chebyshev weights from a self-shadow-biased point. Load-bearing: under an environment the probe sample replaces the IBL diffuse irradiance (probes integrate the same sky, occlusion included) and specular is untouched; probes store E/π in perceptual gamma 5 (sample = pow 2.5 per probe, square after the blend, the irradiance-cube-compatible convention); hit shading traces one visibility ray to the casting light (else bounce leaks through the wall whose shadow the direct pass draws) and adds the previous update's field (multi-bounce by recursion, packed at intensity 1 so the artistic dial can't compound); never mix toward an unread history (0·NaN poisoned the whole field, a real first-render bug), visibility moments cap at 1.5× spacing (uncapped far geometry collapses near-surface variance into hard dark blobs), and probe relocation is not optional (wall-slab rooms park whole probe rows inside the slabs; the relocate pass walks them out via backface statistics, the sign of a surfel's stored depth); relocation statistics read the fixed, never-rotated fan (the surfel texture's first 32 columns, distance-only; the blends skip them — fed the rotated radiance fan instead, its thresholds flicker and a probe near one oscillates between positions, visibly pulsing every surface its cage touches, a real live defect isolated by freezing relocation) and a comfortable probe drifts back toward its grid anchor with proposals committing only inside the 0.45-of-spacing ellipsoid; the live irradiance blend's temporal-response pair is asymmetric and live-only (a darkening past 0.25 cuts hysteresis by 0.75, a brightening past 0.10 luminance is rate-limited to a quarter step — with a sun disc in the fan a bright spike is estimator variance to absorb, not news to trust, and un-limited it pulses a sky-lit scene's whole field; headless skips the pair so its iterations converge an unbiased progressive mean — both live stabilizers are verified by live frame-diff measurement, not tests, the artificial-life honesty rule); live = one hysteresis-0.97 update/frame into ping-ponged atlases (statefulEncodeIsRepeat-guarded, volume held until bounds escape it; beginStatefulEncode runs the frame-scoped reset ahead of the GI pass, which precedes the effect-target encode), headless/export = per-frame refit + whole iterations in two phases, a burn-in whose updates replace the field so the recursion reaches its depth, then averaged updates under a progressive-mean hysteresis (6+6 / 10+8 / 16+12 by tier; a mean from the first update weighed the shallow first fields with the deep ones for good, and a light room read 0.77 of a 64-deep trace), with iteration-indexed seeds → byte-deterministic; rays/probe and the headless iteration count resolve through the persistent globalIlluminationQuality(_:) tier (the shadowQuality shape; the probe grid deliberately never follows the tier — a tier-driven grid would move the probes and light an export differently from live); traced mirror interiors and render targets gather too: the shared hit shade (ollin_rt_hit_radiance) swaps its irradiance-cube diffuse for a field sample at the hit, pre-divided by the IBL exposure (the whole traced radiance re-scales by it on composite), atlases bound into the deferred trace pass and threaded through ollin_pbr_ibl_ambient inline, and targets take the field via encodeEffectTargets(gi:) — distinct from the flat-pastel-blobs dead end, which was about the specular secondary; RT devices only (no-op otherwise), byte-identical when off (suite-verified; gi-3d re-recorded once for the adaptive grid); what a hit gives to the bounce follows its finish (ollin_gi_shade_hit): its glow (the emissive factor plus its color times emissiveIntensity, read off a per-slot OllinGIGeo table the accel build fills beside the caustic one, indexed by the hit record's slot, the build breaking a run where the record changes), the direct light and the previous field through its lobes by the path tracer's parity rule (a physically based finish through the microfacet BRDF, the body over π weighted by what the lobe leaves over and the GGX lobe with its multiple-scattering share from the split-sum table, which the pass bakes when no environment has; every other finish the un-normalized Lambert the raster draws it with), so a metal throws its own color along its lobe and a physically based wall bounces what it is lit with (it bounced π times its share before); an emissive map's pattern is not read (a mapped surface glows at its factor over its face); remaining envelope: point clouds / particles don't gather, a metal gathers nothing from the field (its reflection of the room is the traced one), and a surface reads the field from the probe row nearest it, a third of a cell inside the room, so a converged field reads 1.12 to 1.21 of a deep path trace at a ceiling over a lamp's pool (the grid's resolution, filed in DESIGN-NOTES.md). Camera-anchored probe cascades give a vast scene a near field: past the coarseness threshold (fitted spacing > half the eye-to-target distance — calibrated so a room with the camera inside stays single-volume, and it must) giCascadeLadder adds up to 3 windows halving the scene spacing toward a quarter of the working distance, stacked one 512-probe atlas slot each (same widths, so ollin_gi_atlas_uv and the carriers' bindings are untouched); axes whose span covers the scene slab pin (a terrain's cascades never scroll vertically); live they scroll in whole probe planes (phys = (grid + phase) mod counts, one-plane truncation dead zone) with a scroll pass invalidating exactly the scrolled-in planes via the offsets texture's w validity, which the blends read per probe (fresh at full weight, the refit rule per probe); headless refits ladder + volumes per frame (pure function of the frame, no scroll machinery); sampling takes the finest covering cascade, fading over its outermost cell into the next coarser, scene volume the fallback. Codegen rule: ollin_gi_sample and ollin_gi_trace stay verbatim as the single-volume paths with cascaded twins beside them (fast-math re-contracts unchanged expressions when a function's control flow grows, and the ulp is a byte off every dithered frame) — the no-cascade path is byte-identical (pixel-exact reference scenes + a 180-file snapshot re-record matching a clean-HEAD re-record byte-for-byte). Example 3D/Lighting/GlobalIllumination (intensity + the quality tier as live parameters — RenderQuality conforms to ParamOption, so any quality dial can be a segmented @Param); snapshot gi-3d (RT-gated); GlobalIlluminationTests (11: counterfactual probes incl. mirror-interior + render-target + the parameter's export-tier bytes + the exact count derivation, sabotage-verified red). Mechanism: ARCHITECTURE.md § Global illumination. Docs/3D/3D.md#global-illumination. Depth-aware compositing — depth(at:) / noDepth() / project(_:) / withBillboard(at:){} put 2D drawing inside the 3D depth buffer — cameraRay(through:) is project's public inverse (origin + unit direction; perspective rays share the eye, ortho rays are parallel from the view plane; nil for the intrinsics projection — the physics pickers use it), drawText(_:at: Vector3, size:color:align:) is the projected one-call label (skipped behind the camera), and drawGround(size:at:thickness:color:material:) is the floor slab as one call (top surface at y, style scoped to the slab), or composite against a depth feed via drawDepthScene(color:depth:…) / the metric drawDepthScene(_ frame: RGBDFrame) + Camera3D.fromIntrinsics (a drawn cloud, a depth feed, and a placed object share one metric space). Wired depth-correct through live + headless image(of:); accumulation / texture-handoff render 3D without depth sorting (documented limitation), and gradient paint isn't supported on the 3D path (solid color only). Building 3D drove the Pipeline enum → Hashable PipelineKey migration. Docs/3D/3D.md, Docs/3D/DepthCompositing.md; examples the 3D/* set (PointCloud / DepthCloud / Transforms / Solids / Lighting / LoadedMesh / Shadows / RayTracedReflections / Materials / PhysicalMaterials / ImageBasedLighting / EnvironmentGallery / ProceduralSky / Matcap / DepthCompositing / DepthOcclusion (its metric mode) / …); snapshots sparkle-materials / pbr-materials / pbr-ibl / procedural-sky / rt-reflections-3d (RT-gated via Snapshot.hasRaytracing, so it records/runs only on a ray-tracing GPU). Fog as one value (Sources/Ollin/3D/Fog.swift): fog(_ fog: Fog) takes a color, a veil (the share of a surface at the camera's target distance that is fog, clamped under 1) and a pooling (the height falloff in target distances); Drawer.fogValue is resolved at pack time from camera3D's eye-to-target distance (the aerial default's own rule) into the world-unit density (-ln(1 - veil) / distance) and falloff, so it draws byte-identical to the bare call it means there (FogTests pins it on a fogged colonnade) and a preset reads alike at any scene scale; presets .haze / .mist / .thick / .groundMist / .night, tinted(_:), and a ParamChoices conformance that puts them on the inspector's menu (3D/Materials/Explorer behind its haze switch). The bare world-unit call is unchanged and the last call in a frame wins. A traced point light's softness belongs to the light and the scene: its area radius is 0.06 * shadowSoftness times the light's distance to the center of the frame's drawn meshes (Drawer.meshBoundsCenter, walked once per frame on first use; the camera's target only while the frame holds no mesh), and the caster's texelWorld, which the traced ray offsets its origin by and the cube path takes as a floor, is measured at the same distance, so the penumbra no longer changes with where the camera aims (192 levels between two aims along one sight line, 92 of them from the texel size alone) or breathes through a camera move; the cube map's far plane keeps the camera's framing, since it has to reach past what the camera frames (aShadowIgnoresWhereTheCameraAims holds). The camera's matrices are public (viewMatrix, projectionMatrix(aspect:), viewProjectionMatrix(aspect:), matrices(aspect:) as the shared OllinCameraMatrices), so a kernel or a shader of your own projects through the sketch's camera; the look-at and projection builders stay internal.
  • Lines in 3D (drawLine(_:_:) / drawLine(_:_:_:_:_:_:) / drawPolyline(_:closed:) / drawPolyline(_:colors:closed:) over Vector3, strokeWeight(_:in:) + StrokeUnits, drawAxes(length:), drawGrid(size:divisions:); Sources/Ollin/Core/Drawer+Lines3D.swift, ollin_line_vertex/ollin_line_depth_fragment in Renderer/Shader3D.metal, OllinLineVertex in the shared header; Docs/3D/Lines.md, figure Docs/Figures/Lines3D.swift, example 3D/Geometry/FieldLines, Lines3DTests, snapshot lines-3d). The points go through the model matrix and the camera at draw time (a camera set earlier in the frame, as drawMesh needs; none = nothing drawn + a note), what lies behind the near plane is cut in clip space (a closed loop crossing it is opened at a point behind and comes out as one run), and the projected run goes through StrokeExpander.expand's indexed form, which hands each emitted vertex the path point it was laid around (every cross-section, join, and cap is built at one; the plain form wraps it, so the 2D path and the web player are unchanged: the snapshot suite passed unrecorded). A vertex stores that world point plus its canvas-point offset, and the vertex shader projects the point through the pass's own Uniforms3D and adds the offset times w, so a stereo eye and a TAA jitter move it correctly (offsets are the draw-time camera's). Kind .lines3D with explicit core/fringe ranges (lineCoreVertices/lineFringeVertices, one upload, core then fringe): a triangle with all three coverages at 1 is core and tests + writes depth, the rest is fringe and tests only, so a solid drawn later behind a line fills the soft edge instead of leaving a background rim (aLineHidesWhatLiesBehindItWhicheverIsDrawnFirst). The fragment writes the depth ([[depth(any)]]; a plain fragment where the pass has no depth attachment): the centerline's view depth (perspective-correct, exact on the planar ribbon) pulled toward the eye by 0.0005 + min(|offset| + 0.5, 2) · unitsPerPoint · 10 of the distance. The offset term is what a floor's depth does across a line's width; the +0.5 is the MSAA footprint (one fragment depth serves all samples while the floor is tested per sample; without it pale flecks inside solid ink and 91% of the ink on a grazing floor); per-vertex pulled depth was wrong too (a long receding segment is a sliver whose ends differ in distance). Measured: lines on a floor keep 100% of their ink from about 7° up and 91% from 7° to 3°; the cost is a line within a few percent of its distance behind a surface showing through (a fork of the two edges a few points before the exit). .world width = weight/2 · averageScale(model) · P11·viewport.y/2 / w per point; the unit is stated with every weight (strokeWeight(_:in:), default .screen) because a persistent mode outlived its number in the first example and drew the grid a world unit wide. SVG records the projected polyline (2D transform, dash, brush, profile held off; world width = mean projected width; aVectorExportCarriesTheProjectedLine); makeBatch refuses (no camera), spatial export skips with a note, the web page refuses by name; dash/brush/profile note once and draw whole. Overload trap: drawLine(.zero, .unitX) (two implicit members) resolves to the 3D overload, which notes once without a camera. Measured -O on an M2 with round joins: 2.96 ms for 5,232 points and 10.8 for 19,152, against 3.16 and 11.1 for the 2D strokes through the projected points (about half a microsecond a point; the projection is noise). Six planted defects (no sample reach, fringe writing depth, no near clip, world width ignoring distance, no depth state, the 2D transform left on the SVG) each went red.
  • Hidden-line solids (featureEdges(creaseAngle:) / noFeatureEdges(); Sources/Ollin/Core/Drawer+FeatureEdges.swift (FeatureEdgeSet, FeatureEdgeCache), ollin_edge_vertex / ollin_edge_core_fragment / ollin_edge_fringe_fragment / ollin_edge_fragment in Renderer/Shader3D.metal, OllinMeshEdge + OllinEdgeStyle in the shared header; Docs/3D/HiddenLines.md, figure Docs/Figures/HiddenLines.swift, example 3D/Geometry/HiddenLines, FeatureEdgesTests). A mesh's faces draw as always and its feature edges draw over them in the stroke: an edge set found once per mesh on the CPU (corners welded by LineDrawing.weld; a face whose welded corners are fewer than three is skipped, which is what keeps a UV sphere's south pole, a hair apart in float, from reading as 32 branching creases), each edge its two ends in mesh space and its two face normals, kind boundary / crease (past the angle, or a third face) / smooth; kept in an immutable FeatureEdgeSet that makes its GPU buffer once (no per-frame edge upload) behind a 16-entry cache that keys on the mesh's own position and index arrays (== answers at once on the same storage; a mesh changed in place has copied away from the kept storage, so it misses). A draw records a .lines3D batch carrying edgeSet right after the solid (so every .lines3D rule, depth, peel, DOF scissor, applies) that rides the solid draw's own copies: its run of meshInstances (a single drawMesh appends its model matrix there) or the compute buffer. The vertex shader decides per copy: boundary always; crease unless both faces turn away on a closed mesh (the mesh's front hides it, and drawing it lets the pull leak it through a thin solid's front: aCreaseThatTurnsAwayOnAClosedMeshIsLeftOut); smooth only where its faces face opposite ways (the silhouette); normals by the model's adjugate with the determinant's sign. Each edge is a round-capped capsule on the canvas whose coverage is the 2D fringe's ramp computed per pixel from the distance to the segment (anEdgeIsTheLinesStroke at weights 1, 2, 3: within one level of drawLine along the same segment) and whose depth is the line path's pulled depth at the segment's nearest point (inverse view depth linear on screen, so caps take the end's depth). Two rules found by the probes: (1) the pass is multisampled, and a fragment's color reaches only the samples its quad covers, so the core quad reaches past the core by more than half a pixel's diagonal (a core quad cut to the core drew a 2-point edge on a pixel boundary as a gray hairline: 137 levels off); (2) a curved surface's silhouette is set outside the contour by its half width, the side chosen by the screen projection of its two face normals, and the quad's corners move with it (moving only the ends moved the coverage frame too and cancelled: a 20-point shift changed nothing): a silhouette centered on the edge beads, because the face beside it is seen nearly edge on and its depth outruns any pull (65 of 720 rays across a sphere's outline found no ink; aSpheresOutlineIsUnbroken). Core then fringe as for lines; a weight of a point or less in screen units skips the core pass (its ramp never reaches whole coverage). Order: copies in one call draw every face before any edge; meshes drawn one call each draw in call order, so where two solids nearly touch (within the ~1% pull) the first one's edge can show through the second's face (documented; the identity test keeps copies apart on the canvas). Off under wireframe() and noStroke(); a gradient stroke takes its ramp's middle color, with a note; a mesh field draws faces only, with a note. Measured (M2, release, 1080 square, 10,648 boxes): GPU 4.2 ms solids alone, 8.3 ms with 1-point edges, 11.5 ms with 2-point, CPU unchanged at ~5.4 ms; the same edges as drawLine calls cost ~216 ms of CPU. Technique: feature edges classified in the vertex shader from an edge buffer of face normals (McGuire and Hughes, Hardware-Determined Feature Edges, NPAR 2004).
  • Many lights (reach: on every positioned light kind + Light.reach / Light.reaching(_:); ollin_light_cull + ollin_light_set in Renderer/Shader3D.metal, MetalRenderer+Lights.swift). Tile-based forward: the frame's lights are culled per screen tile and still shaded forward, so the 8-light ceiling lifts without deferred's cost to draw-order compositing, blending, or the float-MSAA path. Two halves, and the first is what makes the second possible. reach is the world-space distance past which a light contributes exactly nothing, stored in the free OllinLight.position.w lane (0 = unbounded, so a reachless light packs and shades byte-identically); the window is (1 - d/reach)², which is 1 at the source, 0.25 halfway, and exactly 0 at the edge (an inverse square with both ends made usable), applied on the light's local copy (L.color.rgb *= within, the light-shaping rule) inside a position.w > 0 branch so a frame with no reach never enters it. A light with no reach cannot be bounded and stays in every tile: the reach is what buys the cull. The grid engages past OLLIN_MAX_LIGHTS (the inline array stays 8 and is still what fog, GI, the traced export, and every non-mesh path read), caps at OLLIN_MAX_SCENE_LIGHTS = 256 with a one-time note (the decal-cap rule), and is one compute dispatch of one thread per OLLIN_LIGHT_TILE_SIZE = 16 px tile: the tile's six planes are row combinations of the view-projection (the left plane at NDC x0 is rowX - x0·rowW), so no per-tile matrix multiply and both projections work; the rect grows one pixel so temporal jitter and multisample positions cannot land a fragment in a tile built without them. Light bounds: a point/area kind is a ball, a spot is the smallest sphere of its spherical sector (apex-and-rim below a 45° half-angle, the cap alone above it, the whole ball past a quarter turn), a directional or reachless light returns "cannot be bounded" and every tile keeps it. Load-bearing: the grid decides only which lights a tile lists, never how one shades, and cullingIsTheSamePictureAsShadingEveryLamp renders the same 64-lamp frame culled and unculled and compares the bytes (the internal Drawer.cullsLightTiles fills every tile with every light; sabotage-verified with a 0.85× radius); anEightLightSceneIsByteForByteWhatItWas diffs the committed mesh-lighting reference at exactly 0 (sabotage-verified). The renderer, not the drawer, owns sceneLightCount/lightTileGrid/lightTileStride, set only where the two buffers are bound (fragment 8/9, stand-ins otherwise), so every other pass reads the inline array; a pass takes the grid only when its attachment is the size the grid was culled over, which is why the live geometry pass names its own renderSize: (a temporal upscale renders smaller than the drawable) and why a full-canvas render target is tiled while one of another size falls back to eight. The half-res raymarch carrier divides its position by raymarchScale to land on the same tile. One portability rule came out of the runner: ollin_light_at picks between a light in device memory and one in constant memory with two returns, never a ternary, which a Metal compiler one version back reads as ambiguous (each side converts to the other), and a shader that fails to compile takes the whole renderer down rather than one pass. Measured on an M2 at 1080²: 64 lamps 16.89 ms culled against 42.65 ms brute force (60% saved), 128 lamps 31.10 against 81.24; 16 culled lamps (6.75 ms) beat 8 inline ones (8.39), since the inline path culls nothing. Scripts/benchmark.sh lights is the sweep. Example 3D/Lighting/ManyLights; ManyLightsTests (14) + ManyLightsBenchmarkTests; snapshot many-lights; Docs/3D/3D.md#reach, Guide Ch 29 § A courtyard of lamps: many lights. The bounce trace (ShaderGI.metal, both trace bodies) and the path tracer's direct light (ollin_pt_direct) read the reach as the raster does, through ollin_light_reach, so a lamp lights and bounces nothing past it anywhere (aLampAddsNoBounceBeyondItsReach, aLampLightsNothingPastItsReach).
  • The scene through glass (sceneThroughGlass() / noSceneThroughGlass(); ollin_scene_refraction in Renderer/Shader3D.metal + encodeSceneBehindPass in MetalRenderer+Targets.swift). Screen-space transmission on any Metal GPU: the frame is drawn a second time with every transmissive run left out (GeometryBatch.isTransmissive + encode(skippingTransmissive:), so what the layer holds is the picture with the glass removed, which is what keeps a body from finding itself), and a transmissive PBR fragment reads it along its own refracted exit ray; the read is finished light, so it is divided by iblIntensity before it replaces the refracted environment (the ambient it lands in is multiplied by that on the way out, as the traced walk's direct light is pre-divided): clear glass passes 1 − F of the light behind it at any environment intensity (clearGlassPassesTheLightBehindItAtAnyEnvironmentIntensity: 0.960 at intensity 0.4 and 1.0, a gray wall minus a black one so the pane's reflection cancels; 0.199 and 0.498 without the divide) (the color at fragment texture 27, its resolved depth at texture 28, sceneBehind + sceneViewProjection + sceneInverseViewProjection on the OllinLighting tail, the ltcEnabled stand-in discipline → byte-identical off, pinned at the pixel by aFrameWithNoGlassIsUntouched; the depth slot wants a depth-format texture even unsampled, so the stand-in is a cleared 1x1 depth32Float, ensureDummyDepth). The walk is ollin_env_refraction's, step for step (same entry refract, same analytic interior span, same IOR-remapped roughness fade), so a body reads the same shape whichever backdrop it lands on; then it differs: a thin wall (thickness 0) samples the exit point's projection and touches no depth (byte-identical to the read before the lens); a solid body is a lens: it refracts out through the curvature-blended exit normal and marches that exiting ray across the layer's depth (ollin_scene_march: the ray's screen path runs from the exit pixel to the vanishing point of its direction, VP * float4(dir, 0), and 1/depth is linear along it, so the march is perspective-exact; view depth is clip.w; the stride is min(span / 24, 1 / 40) of the frame, 64 samples at most, and the first crossing is bisected six times), which is what turns what stands past the ball's focus over rather than shifting it (the image-space refraction of nearby geometry, credited in ATTRIBUTION.md). A crossing counts as a hit only where the layer is continuous across it (the ray sits within 2% of the layer depth and the two sides of the final bracket agree within the same 2%); a jump there is a silhouette the ray passed behind, and the march continues without bisecting again until it stands in front of something. The far plane is a surface like any other (a ray reaching it reads the frame's own backdrop, the way a thin wall does). A ray that ends hidden reads the last pixel it stood in front of, snapped to that texel's center (a sub-pixel bilinear read on a silhouette mixes the occluder in by a phase-dependent fraction, and neighboring rays then stripe; measured in the snapshot scene's clear sphere). Two refusals return the environment (coverage 0): a read on something nearer than the glass fragment (d < zFront, so a foreground object never prints inside the body) and a ray leaving the frame; something between the surface and the exit (a snow-globe figure) reads at the exit pixel; an orthographic or cross-view ray (rate ≤ 1e-4) keeps the exit-point read. The march is the one cost the lens adds: ~70 nearest depth taps per solid-glass fragment, none for a thin wall, and three marches for a body with dispersion (the glass entry above). Load-bearing: the pre-pass mirrors the main geometry pass exactly (same size, sample count, depth format, clear, and shadow/environment/probe inputs), which is what lets it share every pipeline already built and what makes the two pictures line up pixel for pixel; its depth resolves with the main pass's .min filter; it resolves into a mipmapped texture and the mip chain is the roughness blur (rough * (mipCount - 1), generated by a blit after the resolve, so frosting softens the scene the way the prefiltered cube softens the sky); the read carries a coverage that eases to 0 across a 2% border band and drops at once behind the camera (clip.w <= 0), so a lookup leaving the frame dissolves back into the refracted environment rather than smearing the edge pixel; the pass is encoded once, unjittered, outside any TAA sample loop and returns the matrix it was drawn with (the deferred-reflection rule; the projection cannot disagree with the layer); rayTracedReflections() wins and the pass is skipped entirely when a reflect accel exists (the trace has none of the limits, so asking for both costs nothing, pinned byte-identical by tracingTakesOverUnchanged). Same envelope as the other pre-passes: main canvas, camera + environment(_:) required (transmission needs one either way, and the ambient's own gate gets there first), targets / accumulation / texture handoff excluded. Documented v1 limits, each a consequence of reading the screen: only what the camera drew can show through, one piece of glass does not appear inside another, 2D chrome drawn after the glass is in the layer too (the rule is the whole frame minus the glass, not what is behind it), and the layer is one surface deep, so where a foreground object hides what a lens ray would reach, the visible side of that silhouette stands in (the traced walk has the geometry). Cost: the scene draws twice. Example 3D/Materials/SeeThrough (hold-space compare); snapshot scene-through-glass; SceneThroughGlassProbes (9: the wall arriving inside the glass, the no-glass byte-identity, a pane staying out of a bottle, a solid body reading past a seam a thin one does not, a solid body turning a far bar and post over where a thin wall shows them upright (red by sabotage of the march), a foreground post refused rather than printed inside the rim (red by sabotage of the refusal; the post must stand where the exit reads land, or the march never meets it), frosting flattening a striped wall, the traced takeover, and an OllinLighting-fits-a-fragment-push guard). Docs/3D/3D.md#scene-through-glass.
  • Light sets (withLights(_:ambient:_:) / withLights(_ preset:_:) / withoutLights(_:) / addLight(_:); Drawer.LightState, GeometryBatch.lightSet, GeometryKind.readsLighting). Lighting is per batch, not per frame: the lamps, the ambient, and the on/off mode are drawing state on the state stack beside the blend mode and the surface finish, so the set in force when a mesh is drawn is the set that shades it and one frame mixes rigs (a lit stage beside an unlit hologram, two rooms each with its lamps). A batch carries a lightSet index and breaks when it changes, like a blend or a finish; the renderer packs one OllinLighting per set and binds the batch's through bindLitMeshFragment(_:set:), the raymarched field, the strand field and the ocean each taking the same index. Load-bearing, and the reason every committed 3D reference still matches at 0: set 0 is the frame's own, resolved at the end of draw(), so a lamp added after a mesh still lights it exactly as the per-frame model always did, and a frame that scopes nothing records no set at all (lightSets.isEmpty, every batch at 0, whole SnapshotTests suite green unrecorded including the 8 RT cases). Only a scoped set is a snapshot, taken when the batch opens; the base is stateStack.first?.lightState ?? lightState, which is what keeps the frame's rig accumulating whatever a block does to its own. A set is a value, so two blocks under identical lamps share one entry rather than appending, and the lit kinds are the only ones that ask (readsLighting), so a 2D frame inside a withState block never builds one. The two questions the design note asked, answered. A caster's own depth pass per set: casting stays with set 0 and a scoped set names no caster at all (makeLighting guards the whole caster block on set == 0), because a caster is a depth pass into its own map-array layer, a cube of the cube array, and a far plane the renderer files under the caster's index in its own light list, all three keyed by a light's place in the set that holds it; a slot pointing into another set's list would sample a projection that has nothing to do with it. A second tile cull per list: implemented. encodeLightCull loops the sets and culls each one that outruns OLLIN_MAX_LIGHTS into its own pair of ring buffers (MetalRenderer.gridKey(frame:set:) keys the ring by slot and set, since an in-flight frame may still be reading either), currentLightGrids holds them by set, and applyLightGrid/bindLightGrid take the set; a set on the inline path builds no grid, byte-identically. The environment, the atmosphere and the flare stay per frame (properties of the world, not of a rig), though an .off set turns its own IBL off with everything else, which is what makes withoutLights flat under an HDRI. The IES/cookie layer lists became a frame-wide union cleared in beginFrame rather than rebuilt per makeLighting call, so two sets' profiles share one baked texture array and repeated calls still agree. Cap: the frame's own plus Drawer.maxScopedLightSets = 15, past which the extras draw under the frame's with a one-time note. Cost, measured in one process alternating (M2, 1080²): splitting costs a batch per set and saves shading, so 8 rooms of 8 lamps run 2.33 ms as 8 sets against 13.34 ms as one 64-lamp set, and 8 rooms of 2 lamps about 1.2 against 4.0, while 2 rooms of 4 lamps in all lands inside the run-to-run noise (two idle runs read +5% and -10%, which is why only the large cases are quoted); Scripts/benchmark.sh lightsets. Example 3D/Lighting/LightSets; LightSetTests (19 CPU tests) + LightSetRenderProbes (3, the leak test rendering the pair and each set alone and demanding the shared pixels equal the solo ones) + LightSetCullTests (3) + LightSetBenchmarkTests; no pixel snapshot (the claim is a relation between renders, which no one reference image states). Docs/3D/3D.md#light-sets with the LightSets figure, pointed from Guide Ch 26 § Light: presets and the kinds of light.
  • Caustics (caustics(intensity:dispersion:) / noCaustics() / causticsQuality(_:); Renderer/ShaderCaustics.metal + MetalRenderer+Caustics.swift). Photon-traced focused light: emit a budget of photons from the caster light, walk each through transmissive (Material.glass) and mirror-polished-metal surfaces with photon differentials (the emission cell's u/v perturbations chain-ruled through every reflect/refract, including the interpolated normal's own triangle-plane derivative, which is what makes curved glass focus), deposit on the first rough opaque hit, splat each as an anisotropic elliptical footprint into a screen-space layer the four lit mesh carriers add by screen position (fragment texture 25, causticsEnabled/causticsScale on the OllinLighting tail, the ltcEnabled discipline → byte-identical off, whole suite unrecorded). Adaptive emission per the published technique: a light-space density map → per-texel perfect-square leaf counts → cumulative-uint4 quadtree (node = running child sums in traversal order r=(0,0), g=+(1,0), b=+(0,1), w=total; the walk's final level read is guarded off) hands every GPU thread its emission cell; deposited photons atomically feed back projected screen area and temporal variance (the live temporal resolve writes variance into the layer's alpha, which next frame's photons sample — the loop closes through it), density updates as d·clamp(aAvg/aTarget) + v·gain with a 3×3 spatial blend and a probe floor; the density total resets every frame in the args-reset kernel (left accumulating it splits the budget over every frame ever rendered and the whole map starves within a second, a real fixed defect: rays fell 65k -> 427 -> 3) and the adaptive leaf counts round stochastically per texel per frame (floor alone zeroes every texel wanting under one ray, which at seed density is the whole map; the uniform/export branch keeps the exact floor, so exports are untouched), and a texel planned below one ray emits on that share of frames, so its photon carries the inverse share (leafWeights, written by the leaf pass, read by the trace): without it the first adaptive frame fell 17% short of the uniform plan's light and the settling frames 5 to 14%, now every frame within 2%. Three rules keep that plan honest, each one measured as a lens's spot going out and staying out: the promotion is solved in rays, not in k (a texel emits k² rays, so promoting on k's fraction asks for ~23% more rays than the budget, the trace dispatches exactly the budget, and the quadtree hands out tasks in traversal order, so the overflow is always the same corner of the map and whatever caustic sits there dies); the plan's running total is ×256 and rounds (×16 truncation counted every texel under 1/16 of a ray as zero, under-reporting the divisor and inflating every texel's plan by a sixth); and the density bounds are shares of the uniform plan, not absolutes — live floor 0.5·d0, idle floor 0.05·d0, ceiling 32·d0, the last being the windup guard (the area target is unreachable where a photon lands at a grazing angle, since the footprint is the receiving surface's doing; unbounded, those texels multiply ×4 a frame to the old 4096 ceiling and take the whole normalized budget within 30 frames, while the sharpest caustic in the scene — small footprints, which the same rule reads as over-sampling — starves first and never recovers). The guard is Ollin's, not the published technique's. Load-bearing: photon flux is the beam cross-section — first-hit footprint area × |cos incidence| (area alone over-weights grazing silhouette rays into a skirt of bright spikes, a fixed real defect); a footprint past the max screen extent is CULLED, never clamped (the min-width clamp would widen a meters-thin streak into a visible hairline combing the floor — the second fixed defect; min-width itself stays area-conserving; the extent is in screen pixels, so the cull takes a little more at a larger canvas: a ball's deposit was 0.837 of its beam at 192 px and 0.829 at 1080); deposit requires ≥ 1 specular bounce (a straight light-to-surface path is the direct lighting the scene already shades — caustics never double it) and drops paths still inside a solid; the accel build breaks its coalesced runs where the caustic materials change and fills the parallel per-geometry OllinCausticGeo buffer (transmission/ior/thin/attenuation; metal/rough/albedo are stored in the baked vertex slots), only while caustics are on; glass = refract-only lineage (Fresnel-weighted, Schlick read at the angle on the lower-index side, the incident one going in and the refracted one coming out: read at the inside angle the exit reflected too little and a ball passed 0.878 of its beam against the full Fresnel equations' 0.836, now 0.848; albedo tint at entry, Beer-Lambert over the traced interior span, TIR bounces inside), polished metal (metallic > 0.5, roughness < 0.25) = reflect, F0-tinted; a point or spot caster has no distance falloff, so a photon carries its cone's light at the length of the path it took (the first-hit cross-section × (path / first-hit distance)², the distance to a mirror's virtual source) and its reach thins it at that length (params.z): at the first-hit cross-section a lamp's ball gave back 0.35 of its shadow and, with a reach, 4.8 times; a mirror's caustic now equals its virtual light within 1% for both kinds; one caster light by the shadow system's priority (shadowLight if punctual, else directional → spot → point; directional = ortho patch fitted to the specular casters' AABB, spot = its own cone, point = the cone subtending that AABB); splat shading uses the caustics G-buffer's own attributes (normal/metal-rough/albedo MRT + depth — the reflection G-buffer's recipe plus albedo, pipeline .causticsGBuffer, with the batch's shading model and F0 in the material target's free z/w) and shades a footprint exactly as the receiver shades a direct light: a physically based receiver takes Lambert over pi kept by one minus the lobe's directional albedo at the view and faded by metalness, plus the shared GGX/Smith/Schlick lobe with its multiple-scattering compensation (the BRDF table bound at texture 4); every other model takes albedo × E, no pi and no lobe; and never × NdotL again, since flux over the footprint (which lies on the receiver) is already the surface irradiance with its cosine in it. Measured before the rule: a physically based floor took its caustic at pi times the light, the cosine counted twice, so a slab's caustic gave back 2.73 of the light its shadow took (0.904 on a Lambert floor, 0.82 under a 0.5 rad light); after, 0.9216, the slab's 0.96², on both models and both angles. A manual slope-adaptive depth compare clips footprints to the geometry they lit; the layer is rgba32Float: additive half floats stall where a pixel reaches a power of two (a focused spot flat-topped at exactly 1.0, 2.0, 4.0, its peak 20 once lifted) and shaved 4% off a flat caustic; every trace thread owns photon slot tid (a thread that deposits nothing leaves it at zero power, which the splat culls) and the splat draws all slots in thread order, never through an append cursor: the cursor handed out slots in arrival order, and the additive splat rounds each order differently (85 pixels of the example at frame 480, run to run, when the layer was half float); export/headless = uniform emission at the detail budget, no history touched, and the jitter seed held at 0 (a frame is a pure function of itself, and every exported frame traces the same photons, so a still caustic holds still across a sequence where a per-frame seed re-rolled its sparks ±5% a frame; anExportedCausticHoldsStillFromFrameToFrame; live = jittered emission + EMA resolve on an SSRHistorySlot twin behind statefulEncodeIsRepeat, the resolve reining history with 5×5 variance clipping, motion-scaled exactly like the reflection temporal, never a min/max clamp, which on the sparse splat layer crushes history wherever a frame lands no photon and holds the flicker forever); dispersion = per-photon IOR perturbation (±3% × the parameter) with energy-preserving RGB weights that sum to white. The kernels live in the main shader library (libraryComputePipeline(_:), its cache cleared on live shader reload); segment order: after ShaderGI (needs Shader3D's ollin_rt_query/ollin_rt_fetch_surface and ShaderEffects' PresentOut), everything traced inside #if OLLIN_RT_SHADOWS; non-RT devices no-op (causticsWanted gates on rayTracedShadows). Documented v1 envelope: screen-space apply (no caustics seen in mirrors/through glass), roughness never widens differentials (sharp caustics from frosted glass), no area-light photons (the soft-caustics covariance extension is the follow-up), a glass surface's own specular spawns no second photon family. Example 3D/Lighting/Caustics (lens spot inside its own shadow + chrome-torus fans, hold-space compare); snapshot caustics (RT-gated); CausticRenderProbes (inert-without-casters byte-equality, lens-brightens-its-shadow on/off diff, green-glass tint) + CausticsLiveTemporalProbes (the headless live-path net: drives encodeCausticsPass(supersample: false) frame after frame — a static camera asserting the layer stays fed and converges toward its settled reference, and 150 frames of the example's own cast asserting that the lens's focused spot keeps its energy, that the whole layer keeps its light, and that the plan never asks for more rays than the trace runs; the profile every export-path test is structurally blind to). CausticEnergyTests walks the energy chain stage by stage against values the chain does not compute (a slab's deposit against I × area × 0.96² within 0.5%, a ball's against the Fresnel band, a slab against its own shadow on both receiver models and two angles, a mirror against its virtual light, every live frame against the uniform plan), each red under a planted copy of the defect it guards; the two-sided lens invariant is under Physical invariants. Docs/3D/Caustics.md.
  • Volumes (Volume + Volume.noise(...) + Medium in Sources/Ollin/3D/Volume.swift/Medium.swift; drawVolume(_:size:medium:) / drawVolume(_:width:height:depth:medium:) recorded by Drawer+Volume.swift into drawer.volumeDraws; Renderer/ShaderVolume.metal + MetalRenderer+Volume.swift; the shared OllinVolumeDraw in OllinShaderTypes.h). A density grid drawn as a participating medium: Max's emission-absorption model (1995) with single scattering, each step integrated exactly under its sample (Hillaire 2015's S·(1−e^(−σΔ))/σ), so a uniform medium is exact at any step count. Load-bearing: volumes are NOT batches. The geometry pass cannot read its own depth attachment, so every volume composites after its pass ends (canvas: right after applySubsurfaceScattering, before TAA/upscaler/DOF/blur/flare/filters/overlay; a geometry layer: right after its pass, in encodeEffectTargets), over the resolved color with premultiplied source-over, reading the pass's resolved depth (mainDepthResolve / the export's sceneDepthResolve / the layer's resolve; every depth-resolve gate includes drawer.hasVolumes(for:)), so the march ends at the first solid and call order does not matter (aVolumeDrawnFirstStillEndsAtALaterSolid). Several volumes sort farthest first by box center (two that interpenetrate order as wholes); equal distances keep call order, the later draw on top, so an absorbing medium drawn after a glow over the same box dims it and drawn before it dims only what is behind both (twoVolumesOnOneBoxCompositeInCallOrder; a glow-only medium adds in any order). The pass's lighting, shadow map, and jittered Uniforms3D are stashed by encode (keepVolumeLight, not from the hidden layer, the overlay, or the scene-behind pass), and encode no longer returns early on an empty batch list when the pass holds a volume. Lattice samples run corner to corner (ollin_volume_uvw), r16Float 3D textures uploaded once per VolumeStorage (copy-on-write; a write after upload copies first, a new contentID). Single scattering of the first eight inline lights (directional, point, spot with cone/IES/cookie; area kinds scatter nothing), phase normalized isotropic = 1 like the fog, the flat ambient plus the environment's irradiance averaged over six axes; self-shadow for the first four lit lights from a light-transmittance grid (ollin_volume_light_kernel, Behrens and Ratering 1998), cached on (content, model, density, light ray) and recomputed into a new texture on any change; solids' shadows through 2D-map casters (ollin_fog_shadow_tap with the caster's strength); point (cube) and traced casters do not shadow the medium. Per-step start offsets (ollin_ign at pixel + step × 5.588, the fog's rule: one shared offset printed streaks along a sharp shadow edge) shifted per temporal sample by volumeJitterPhase (golden-ratio steps), so TAA averages the grain away. Refused by name on the web page (drawVolume); skipped under SVG, the spatial recorder, makeBatch, and withOverlay. Not carried: the medium casts no shadow, fog does not veil it, reflections and glass do not show it, DOF and motion blur read the solid behind it. VolumeTests (10, against closed forms in linear light: Beer by row 0.00045, the emission integral at three densities, the depth probe, single scattering at g 0 and 0.5 per channel, call order against a solid and between two volumes on one box, a solid's shadow inside, a layer, temporal samples, an empty volume byte-identical; eight plants red) + VolumeValueTests (4); snapshots volume-plume + volume-glow. Measured (M2, release): Volume.noise 64³ 4.9 ms across the cores (24.5 ms through the closure), the looping form 5.8/18/40 ms at 32/48/64; the Volumes example's march 2.7 ms GPU at a sixth of 1080². Example 3D/Effects/Volumes; Docs/3D/Volumes.md; Guide Ch 28 § A cloud in the scene: volumes.
  • Reflection resolution tier (reflectionQuality(_:); resolveReflectionScale in MetalRenderer+Targets.swift, the deferredReflection: (texture:guide:scale:) triple through encode). Trades ray-traced-reflection resolution for frame rate: .performance renders the deferred reflection layer at half the drawable in each axis (a quarter of the trace) and the lit fragments read it scaled; .detail/.default keep it full size. The fragments read the layer as position.xy · scale / texture size (rtReflectionScale was reserved in OllinShaderTypes.h), so the tier itself is Swift-only; what the shaders carry is the surface-guided read below. A persistent setting like shadowQuality; an export resolves .default up to .detail, so every snapshot stays byte-identical unless a sketch asks for .performance outright. Measured (M2, the reflections example at 1080², --bench 200 --gpu): GPU 22.19 → 14.02 ms, 45 → 71 fps (37%). The cost is reflected detail, and only that: one traced ray serves four screen pixels facing different ways, so a curved mirror quantizes and fine detail inside a mirror image softens. It does not cost the mirror its strength. What was documented as "~15% weaker at half size" was a trace defect the coarse layer only magnified: ollin_rt_reflect_trace built its jittered sub-pixel sample by unprojecting at the pixel's own depth, which lands on the plane of constant depth rather than on the surface, so on any oblique surface about half the jittered origins fell under the mirror and their rays struck its underside a hair away. Meeting the tangent plane instead (P = eye + dir · (dot(P0 - eye, n) / dot(dir, n)); the direction, and so R, is unchanged) repairs it at every resolution: the probe's mirrored band goes from 81% to 97% of the full-size signal at half size, and the area-reflections reference lost the salt-and-pepper speckle it had been carrying across its whole mirror floor (re-recorded with rt-reflections-3d; the other 13 references that recording rewrote move identically on a clean HEAD, so they are toolchain drift and were restored). The upsample is surface-guided (ollin_rt_reflection_read, Shader3D.metal): below full size the four taps are weighted by the reflection G-buffer's own coverage and normal (the joint-bilateral shape), so a mirror at an edge keeps its own reflection instead of taking a share of what its neighbor reflects — over the pixels where the filter has a choice to make, a flat blend measured 11% brighter than the full-size picture it stands in for. The guide is the normal rather than the depth on purpose: a relative depth test reads a floor running away from the camera as a discontinuity at every step, which turns the whole grazing half of a mirror floor blocky. The G-buffer normal's alpha now means "a traced reflection stands behind this texel" (a surface is here and it is under 0.55 roughness), which is exactly the trace's own two-part early-out, so one flag serves both readers. Full size stays byte-identical (the read early-outs to the same single linear tap above scale 0.999), and so does the common case below it (all four weighted taps agreeing takes the hardware bilinear and leaves three reads unmade), which is what keeps the guide's cost at ~0.5 ms and the tangent-plane meet's at ~0.35 ms. filter::nearest reads closer to full size on a per-pixel metric and was rejected on the look: it is blocky where the guided read is smooth. Two benchmark-harness defects had to be fixed before any tier number was measurable at all, and both were hiding a third of the frame: --bench never applied --render-quality (so no tier reached the render at all), and benchmarkGPUMilliseconds encoded no deferred reflection pre-pass and re-encoded one sketch frame per iteration, which every stateful pass reads as a repeat and skips (lastStatefulEncode is now cleared per iteration) — so the GPU column had been leaving out the reflection trace, the photons, and the probe update. ReflectionResolutionTierTests (3: the mirrored band keeps ≥90% of its signal at half size, a two-faced mirror box keeps each face's own reflection at the joint, and an instanced copy still mirrors at the lower tier; the first two verified red by sabotage). Docs/3D/3D.md#rt-reflections.
  • Temporal anti-aliasing (temporalAntialiasing() / noTemporalAntialiasing(); ollin_fx_taa_resolve in Renderer/ShaderCombine.metal + applyTemporalAA in MetalRenderer+Targets.swift). Sub-pixel projection jitter accumulated across frames (live) or averaged within the frame (export), refining 3D edges past MSAA and absorbing the screen-space/traced effects' residual shimmer. Per-frame state like rayTracedReflections(); main canvas + active 3D camera only (2D no-op with a note; withTarget layers and the accumulation surface keep plain MSAA); any Metal GPU. The jitter is one NDC translate premultiplied onto the projection (MetalRenderer.jittered(_:by:) — exact for perspective/ortho/intrinsic where the add-to-[2][0] trick is perspective-only; z/w untouched, so depth(at:) and every depth test are unchanged; zero jitter early-outs, so the off path is byte-identical by construction — whole snapshot suite + a zero-churn guide-figures.sh run verified it at the byte level), threaded as one taaJitter param through every main-canvas 3D pass (encode's makeUniforms3D, the reflect G-buffer, the scatter mask, both half-res field pre-passes; targets pass zero). Load-bearing, each a measured live defect first: both reprojection matrices are unjittered (the published remove-the-jitter velocity rule — reconstructing through the jittered inverse resamples the history at a different sub-pixel offset every frame and its content random-walks with variance Var(jitter)/(1−feedback²): px-scale oscillation on every hairline edge; the RT-reflect temporal takes the same rule now that its G-buffer carries the frame's jitter); the rectification box is velocity-adaptive (γ 2.5 at rest easing to 0.75 by 15 px/frame — at fixed γ=1 the μ±σ box itself oscillates with the jitter phase and drags a converged hairline edge, the clamp-sawtooth failure); adaptive feedback keys on how far the clip moved the history (untouched → 0.97, dragged → 0.88), never on the instantaneous jittered sample, which permanently distrusts exactly the pixels needing the longest memory. Live accumulates into an SSRHistorySlot twin (taaHistory) behind statefulEncodeIsRepeat, fed by a .min main-depth resolve (attached only when TAA runs) and a Catmull-Rom history resample; headless/export renders N deterministic jittered geometry passes averaged in linear light (4/8/16 by tier; export's automatic tier is .detail = 16) with the pre-passes (shadows, GI, layers, sims, the deferred reflection) outside the loop — byte-stable snapshots, flicker-free video. A traced shadow shares one sample set across those passes (encode's taaPass argument sets OllinLighting.shadowPass/shadowPassCount, which fill the struct's tail padding so the stride is unchanged; shareTracedShadows divides each traced caster's rays by the pass count, rounded up, at least one; a .detail tier, which is what every export resolves to, takes exportDetailShadowSamples, 64, as the set, four rays a pass, where the live 16 showed a wide penumbra as sixteen bands once the passes shared one set, and an absolute shadowSamples(_:) passes through; pass p traces samples p, p + P, … of the rays·P Vogel disk or antithetic R2 set, so the passes together trace each point once): .detail on the M2 traces 16 points in all where it traced 16 per pass (1,000 spheres under a point light at 1080×1920: 25.3 s to 2.3 s a frame, the same 16-step penumbra, 24.0 dB against a 64-point render where the old frame read 24.8 for pairing every point with every jitter offset). One pass (every frame outside that loop, the live path included) is byte-identical by construction. TracedShadowPassTests: the shared penumbra within 4 levels of the whole set traced in one pass (1.2 to 2.2 measured, 0.9 to 1.6 for the old every-pass trace, 41 to 88 for a collapsed set), and a budget of 1 drawing what 16 draws under 16 passes; both red under plants. The live stabilizers carry no test (the artificial-life honesty rule; every deterministic probe restarts the loop): verification is the frame-diff record in ARCHITECTURE.md (TAA-off control exactly 0.0; converged hairline-rod crop p99 3–11/255, down from 14–33). TAATests pins the deterministic half — the kernel-discrimination probe (the export average must match its own analytic construction, built by box-reducing an 8× reference at each declared offset in linear light; a plain box-filtered ground truth is not the export's limit, its kernel is box ⊗ jitter set, and judging AA in gamma space is the classic trap), flat-interior energy conservation, on-then-off byte-equality, two-render determinism, the 2D gate, and the exact-pixel-shift unit — discrimination + conservation verified red by sabotage. Per-object motion vectors — withMotion { } / withMotion("name") { } declares the block's meshes one world-space mover, and scopes the drawing state like withState (the Sketch forms wrap the body; unscoped, a translate per block in a loop accumulated and a 10 by 10 grid drew 2 boxes, VelocityBufferTests.aBlockKeepsItsTransformInside): the drawer remembers each draw's model matrix across frames under call site + occurrence + draw index (the SSRSlotKey rule, never a frame-wide ordinal; history prunes when a key skips a frame, and a first sighting records nothing) and records prevModel·inverse(curModel) per vertex range; encodeVelocityPass writes an rg16Float velocity texture — previous − current, pixels, y-down, both view·projections unjittered (the MetalFX motion contract verbatim; one buffer designed for TAA + the temporal upscaler + motion blur, each now a shipped consumer) with a sentinel clear (OLLIN_VELOCITY_NONE; a NaN from a degenerate transform reads unwritten), in two phases: non-movers depth-only first (nil fragment + empty color mask — a hidden mover must lose the depth test; wireframes skip both phases) then the mover ranges; the resolve samples at its dilated closest-depth neighbor (closest-fetch) and a written texel replaces the matrix fallback, which stays verbatim — population is sparse movers-only by design (every no-mover pixel keeps the shipped live-measured path, byte-identical by construction); a stationary mover writes the camera's own motion (the total-motion contract, probe-pinned). Live-only for TAA (export supersamples within the frame and never reprojects, so under TAA alone withMotion costs an export nothing; the motion-blur chain re-encodes the same pass headless when it needs velocities). VelocityBufferTests (13: registry rules + velocity-texture readback probes over a 1:1 ortho scene + a crafted-texture resolve-branch probe; the velocity-sign / occluder-drop / resolve-sign sabotages each verified red); the live perceptual A/B is measured (static-camera probe, 30-capture bursts per mode: mover edge raggedness 0.006 with exact motion vs 0.048 without, 3–10× at every matched bar length, control rod identical, no ghost either way — record in ARCHITECTURE.md). Example 3D/Effects/TemporalAA (the orbiting bar's exactMotion parameter); snapshot taa. A mesh that changes shape — drawMesh(_:previous:) hands last frame's vertex positions (same count and order; a mismatch notes once and falls back to the transform-only form), and the drawer expands them in the mesh's index order like the vertices, bakes them through last frame's model matrix when the key has one, else this frame's (a mover that also moved whole writes the sum), and records the range with previousStart into the per-frame moverPreviousPositions (previousOfCurrent left the identity; a first sighting records, since the sketch said where the vertices were); the velocity pass uploads those positions itself into a ring of 2 × maxFramesInFlight slots advanced per encode (the pass runs at most twice a frame: the TAA resolve, then the blur or the upscaler) and draws such ranges through ollin_mesh_velocity_deform_vertex (previous world positions at vertex buffer 4, .meshVelocityDeforming beside .meshVelocity, same fragment), switching pipelines only where the kind changes. Outside a withMotion block the Sketch call opens an implicit block keyed by its call site (hasOpenMotionBlock decides), inside one it joins the block's identity. Deforming and transform forms of one displacement write the same field (probe-pinned, no texel differs past 0.05 px); VelocityBufferTests pins the first-sighting record, the per-vertex previous place, the mismatch fallback and its note, the block's placement applied to the previous positions, the per-frame reset, and the readback probes (the displacement, the two forms agreeing, the still mesh). Mechanism: ARCHITECTURE.md § Temporal anti-aliasing. Docs/3D/3D.md#temporal-antialiasing.
  • Specular anti-aliasing (specularAntialiasing(_:strength:) / noSpecularAntialiasing(); ollin_ndf_filter_kernel + ollin_ndf_filtered in Renderer/Shader3D.metal, specularFilter on OllinLighting). The shading half of anti-aliasing, beside temporal AA's geometric half: MSAA takes many samples of the shape and one of the shading, so a highlight narrower than its pixel is a speck the single sample lands on or misses, and fine shiny detail crawls with sparkle as it turns. Each lit mesh fragment measures the spread of shading normals its own pixel covers (the two screen-space derivatives of the world-space normal) and adds it to the squared GGX roughness, which turns the missed speck into a slightly broader highlight that holds still. Per-frame state like temporalAntialiasing(); any Metal GPU; two derivatives and no extra pass. Load-bearing: the kernel is the published less conservative isotropic form (the average of the covariance eigenvalues) at strength 1 rather than the conservative one, since a sketch that asked for polished metal should still get polished metal, and strength 2 reaches the conservative form; the math happens in α² units (α = perceptual roughness squared, so the filtered value is the fourth root of α⁴ + kernel) and is applied at the definition of every local roughness, which is what carries it through the direct-light, LTC area-light, clearcoat, and environment reads in one edit each; zero is the gate at both ends (sigma2 <= 0 skips the derivatives, widen <= 0 returns the roughness untouched rather than through two roots), so an unfiltered frame is byte-identical by construction rather than by tolerance; the derivatives are taken at the top level of each fragment, never where the normal was last written (the maps path writes N inside detail-map and decal branches, and a derivative asks for control flow every pixel of the quad reaches); the parameter cannot be named kernel in MSL (a reserved word); and the widening is capped (the published 0.18 in α²), which is what stops a silhouette quad straddling two surfaces from turning matte. Reaches all four lit fragments (solid, textured, normal-mapped, map-driven) and the meshLitColor/meshLitColorMapped + ollin_pbr_ibl_ambient/_mapped hand-synced pairs, always as a trailing defaulted argument, so every other carrier (the raymarched fields, the traced hit shades, which have no derivatives) passes nothing and is unchanged. Envelope, stated rather than silent: it fixes a highlight that is in range, and a highlight already clipped to white is flat, so widening it spreads that flat area and reads brighter rather than steadier (measured: against a converged reference the filtered frame is further from the truth once the light clips); and the estimate comes from one quad, so detail far below a pixel is guessed rather than measured. SpecularAntialiasTests (8: the strength-to-variance packing and the three ways of saying no, the per-frame reset, and four render probes — the crawl of a sub-pixel surface measured against the converged sequence of the same frames (truth 0.039, unfiltered 0.148, filtered 0.112, so a third of the excess goes; both crawl probes verified red by sabotage), the parameter still buying at strength 2, and three exactness probes: a flat sheet, an off frame, and an unlit frame all byte-identical). Example 3D/Effects/SpecularAntialias (a bed of small polished balls with the toggle, the strength, the ball size, and the polish on parameters, and a flat plate behind them as the counterfactual in the same frame); snapshot specular-antialias. Mechanism (the measurement record and the envelope): ARCHITECTURE.md § Specular anti-aliasing. Docs/3D/3D.md#specular-antialiasing; Guide Ch 33 § Highlights that hold still: specular anti-aliasing.
  • Motion blur (motionBlur(shutter:) / noMotionBlur(); ollin_mb_* in Renderer/ShaderCombine.metal + applyMotionBlur in MetalRenderer+Targets.swift). The published velocity-buffer reconstruction filter (credited in ATTRIBUTION.md): a full-screen velocity fill (the withMotion mover texture composited over depth-reprojected camera motion per pixel — the temporal upscaler consumes the same field through its own unscaled variant, the blur's copy being half-shutter-scaled and magnitude-clamped to the published [0.5px, k]), the TileMax → NeighborMax dominant-velocity pyramid (k = clamp(height/36, 16, 64), the published 20 px at 720 tall made resolution-relative), and the scatter-as-you-gather reconstruction (a depth compare whose two halves sum to one, a spread compare from each side, (S − 1)/2 tap pairs either side of the pixel with S = 9/15/27 by effectiveQuality(.default) — the TAA no-per-feature-parameter rule; the gather jitter is a pure function of pixel position, the dither's rule). Load-bearing: the weights are never renormalized — their mean over the taps is the streak's share of the pixel and the pixel's own color fills the rest, so every result is a convex combination (never outshines) and a mover keeps its light (the renormalized three-case gather it replaced 2026-10-08 kept a mover's interior whole and gained 5%/25% at shutters 0.5/1; measured against --subframes, the region error fell from ~20% to under 2% of the box's value); the published mirrored background reconstruction is left out on measurement (it smeared a still column beside a fast sphere and moved the streak further from the subframe reference); a half-covered mover edge pixel whose center misses the single-sample mover pass keeps the background's velocity (~1% of a 24 px box's light with edges on pixel centers, 0.16% on whole pixels; a multisampled velocity resolve is filed); and 2D drawing over the 3D scene takes the motion of the surface under it (captions belong in withOverlay). Per-frame state like TAA; same envelope (main canvas + active 3D camera only, 2D no-op with a note, targets/accumulation/texture-handoff excluded); any Metal GPU; runs after the TAA resolve, before the frame filters (linear pre-tonemap, so bloom reads the streaks). Load-bearing: the cross-frame camera lives on the Drawer (previousCamera3D, kept in beginFrame), so live, same-frame repeats, and headless export all read the same previous matrix — the TAA slot's copy is the identical value on a live frame, and on a repeat the slot has already advanced, which is exactly why the blur re-encodes the mover pass itself through the shared encodeMoverVelocity core (the TAA encodeVelocityPass gates are untouched, so the shipped TAA path stays byte-identical); export works because the headless drive already runs performDraw() for every warmup frame, so the mover registry and previous camera populate with no extra machinery, and frame 0 (no previous camera) renders unblurred by definition; a still frame skips on the CPU (previous view-projection float-equal + no mover ranges → byte-identical), and a still mover passes through the GPU early-out byte-identically; the backdrop (depth exactly 1) holds still by design (2D drawing, the clear, the environment skybox never smear — the TAA background rule); soft-depth extent = 1% of the eye→target sceneScale proxy (a screen-space constant would hide a scale assumption, the SSS lesson); the depth resolve reuses the TAA .min attachment, gate widened to taaActive || blurActive (export allocates its own). Shutter 0.5 = the 180-degree film standard; the fill stores current − previous × half-shutter (the paper's half-spread, sign-exact). Point clouds/particles take camera blur only (no per-particle velocity). Example 3D/Effects/MotionBlur; snapshot motion-blur (frame 2 — a mover + camera pan, any Metal GPU); MotionBlurTests (12: streak-direction twins, tile-pyramid reach, shutter scaling, camera-only blur, the still/frame-0/2D byte-identity gates, determinism — the neighbor-max skip, velocity-axis swap, and mover-drop sabotages each verified red). Mechanism: ARCHITECTURE.md § Motion blur. The conservation and region invariants are under Physical invariants; the reference is Motion blur from subframes. Docs/3D/3D.md#motion-blur.
  • Depth of field on the canvas (depthOfField(focusRange:maxBlur:quality:) / noDepthOfField(); ollin_fx_lens_* + ollin_lens_gather in Renderer/ShaderCombine.metal, applyDepthOfField in MetalRenderer+DepthOfField.swift). The finished 3D frame blurred by its own depth through the camera's thin lens: Camera3D.aperture (radius, world units), focusDistance (from the eye, along the view axis; the target's distance when nil), apertureBlades. Per-frame state like motion blur; main canvas + perspective camera + aperture > 0 only (a note otherwise; withTarget layers and the accumulation surface stay sharp, .defocus is the layer route). Order: after the TAA resolve, before motion blur on all three chains (live, both export chains, the benchmark), since the gather is a fixed pattern the history need not clean and a moving defocused shape should streak as one soft shape; the depth resolve is allocated when it runs. Five passes: a prepass (blur size R·F·|1/s - 1/d| px with F = P11·H/2, distance read nearest from the resolved depth, the backdrop at depth 1 read as the far plane, which keeps a far highlight's light in balance: held sharp it doubled), a k×k tile pass (widest blur, widest blur in front of the focus, and the tile's color spread), a 3×3 neighbor max, the gather, and a 3×3 median (the nine-input exchange network) blended in by the pixel's own blur so nothing in focus is touched. A pixel gathers only to max(its own blur, the widest near-field blur nearby) (a farther tap is held to the pixel's blur and a nearer one behind the focus is less blurred), and a neighborhood of one flat color returns the pixel (exact: any weighting of one color is itself); k covers the gather's whole reach including a bladed iris's corners. The gather is a scatter written as a gather: each tap is a disc of light with density 1/(pi c²) times the area it stands for, which keeps a highlight's light as it opens and a sharp line's weight against a blurred background (an average of the taps that reach gives both away). Taps read whole pixels (a filtered read invented a blur size and threw a sharp line's color 10 px), distances run between pixel centers and a tap on the pixel itself is skipped (its light is here), and the spiral places tap j at r² = start + 2s(j + 1/2) (stepping r by s/r overshoots the first ring: a thin line kept 95% of its light at a quarter-pixel step), in two zones when a wide near-field blur sets the reach past the pixel's own (half the taps to its own blur, so a small source is found; one zone left the bokeh grainy). The whole spiral is moved per pixel by up to half a tap spacing (interleaved gradient noise of the pixel position, kDofJitter, so exports stay identical): unmoved, the taps finding a small source shift slowly across pixels and leave smooth blotches the median cannot see; moved, the grain is a pixel across and the median takes it. The smallest sources leave the gather (ollin_lens_sprites_collect, ollin_lens_sprites_vertex/_fragment, encodeLensSpriteCollect/encodeLensSprites, PipelineKey.lensSprites): a pixel blurred by 3 px or more, brighter than three times the dim mean of a ring 4 px out with at most two bright samples on it and never an opposite pair (a dot of five pixels passes whole, a line fails, a dot of seven gathers), keeps the ring's dim mean in a flattened copy of the frame the gather reads and hands the rest to its 2x2 block's sprite (one slot per block in block order, never an append cursor: an additive draw rounds by its order and the frame must repeat); the sprite is a quad at the light-weighted center with the light-weighted blur and distance, drawn after the median at the gather's density 1/(pi r^2) through the same iris, its edge softened by sqrt 2 times the radius of gyration of what it took, and covered by a nearer surface by the share of a ring at that surface's own blur whose samples are nearer (all deep inside an occluder, half at its edge, all for a sharp one). The ring's mean is of its dim samples only (a bright one is the source's own far side; counting it left a tenth of the light to gather as blotches). A one-pixel lamp kept 0.2% of its light gathered at .performance and 64% at .default; it keeps 100% as a sprite, and a five-pixel highlight's disc interior is flat within 2%. The pass costs nothing measurable: 400 lamps at 1080 by 1080 on an M2 read 0.3 to 1 ms faster with it at both tiers, within the run-to-run spread (the flattened frame short-circuits more of the gather than the collect and the draw cost). .defocus runs the same collect and draw under its ramp law and iris. A traced export stands the canvas pass down (pathTraced != nil in the headless chain): the traced frame came through the lens already, and the pass over it blurred twice (disc grain std/mean at the export tier 0.104-0.136 with neither, 0.054-0.078 median alone, 0.043-0.061 both; the median costs a highlight 1-3% of its light). A bilateral prefilter before the gather was tried and measured no gain. Three layers by distance against the pixel's own (within 1% is the same surface): taps nearer and in front of the focus are a veil (densities summed as coverage, composited over the rest); the pixel's own surface, when it is itself in front of the focus, covers the pixel by its share of the disc nothing in front takes, own area against the holes the scene behind shows within its blur (one minus the holes counted the surface's nearer part twice, once in the veil and once here, and drew an outline inside a blurred ball's edge: its cover stepped 0.198 a pixel where a 22 px blur allows 0.029; as a share the veil's X and the surface's Y/(1-X) add to X+Y, and a density sum comes out short once the disc is smaller than the taps), else it joins what stands behind, normalized; a farther tap is held to the pixel's own blur. A farther tap counts as another surface only when its blur differs too, compared as the lens's own blur before maxBlur caps it (a ball's edge turning away left a light rim otherwise, and a capped near bar matched its backdrop), and behind a foreground the scene beyond the tap's own blur stands in at a quarter weight (kDofRevealWeight; the inside of a blurred bar matches the pixels beside it, RMS 0.035 against a layered thin-lens reference, 0.042 at full weight). Split at the focal plane alone a subject in focus counts as foreground behind a blurred bar; a veil on both sides of the focus was tried and refused (it kept every light but ringed a sharp subject over a layered far field with blocks, since what hides a far source is usually sharp and must be filled from what shows), and so was folding the own surface into the veil (as close to the traced lens on a floor scene, but a bar over a ball covered it 0.22 where the lens gives 0.30). The published two-layer forms were implemented and measured too: sorting by the tile's closest distance (the 2014 course) sorts a ball behind the floor in front of it as background, since with no scene scale a depth range cannot both hold one object together and split two (its blur stopped spreading: a sharp edge), and sorting by the sample's own near-field blur (the 2013 shader, one averaged layer behind) let a sharp surface behind a slightly blurred one outweigh it forty to one by density (a crescent of the red ball through the yellow). Measured against the thin lens (study, 512 px): radius within 0.5 px at eight depths near and far, light 97 to 100% (behind the focus a bar a tenth as blurred as its backdrop keeps 98.5%, the share model's; averaged by density it kept 92.7%), highlights even discs (peak over mean 1.5), no rim at an in-focus silhouette (1.000; .defocus 1.099 with its earlier gather, 1.001 with this one), a near bar's cover 0.77/0.51/0.30 against 0.76/0.50/0.30, and, on a floor scene with a ball in front of the focus, 0.0064 RMS against an accumulation-buffer lens (512 pinhole renders of the same raster, the eye moved over the aperture through Camera3D.intrinsic with the principal point shifted so the focal plane holds: the reference that shares the raster's shading, 0.0076 before the share fix) and 0.024 against the path-traced lens (its shadows differ). The hidden layer (encodeHiddenLayerPass, hiddenLayerExtent, OllinPeel in the shared header, kOllinPeel + ollin_peel_rejects in OllinShaderLib.metal, PipelineKey.peel): what a blurred foreground hides is read from the 3D scene drawn a second time keeping only what lies behind the first layer, the hidden image of Lee, Kim, and Choi's per-pixel-layer splatting (2008), drawn as a depth peel against the main pass's resolved depth (.min) under the main pass's own jitter, into cached MSAA color + depth resolved like the main pass, through encode (its peel argument) with the same shading inputs. The peel is a function-constant variant of every 3D fragment (kOllinPeelSet, index 15, set by fragmentFunction(named:peel:using:); the lit mesh family incl. textured/normal-mapped/surface-mapped/matcap, points, lines, the ocean, the depth scene), the first layer's depth at fragment texture 29 and the rule at buffer 10: a fragment stays out when it is no farther behind the first layer than 1% of its distance plus how far its own slope carries its depth across 1.5 px (a surface seen nearly edge-on changes depth by many pixels' worth inside one pixel and the nearest sample sits anywhere in it; without the slope term a sphere's own rim joined the layer), and a face turned away from the eye stays out when the kind says so (a closed mesh or copy, a field: a closed shape's inside; blades, splats, lines, water, a depth scene have none). A mesh says whether it is closed (Mesh.isClosed, internal: the generators set it, false for a plane, a strip, an open-ended tube, a parametric surface that does not wrap both ways; nil, the loaded or hand-built case, reads as closed), carried per batch as GeometryBatch.meshClosed (a change opens a fresh batch, like a finish change), so an open surface seen from its back joins the layer (anOpenSurfaceSeenFromItsBackJoinsTheHiddenLayer) while a sphere's inside still stays out. A function that names a function constant at all, even only through is_function_constant_defined, cannot be built the plain way (Metal refuses the pipeline), so every geometry fragment is now made through makeFunction(name:constantValues:) with no value set. A raymarched field peels too (ollin_raymarch_fragment under kOllinPeel): the march starts behind the first layer's distance at the pixel, past the same tolerance (a hundredth plus the slope term, the slope from the field's own gradient there), and where that point lies inside the field's body (the field is the first layer) it first walks out by the field's own distance, then runs clearing until the face it left is a pixel's footprint away, recording no near-miss before (clearing in ollin_raymarch_fragment; without it the face just left read as a grazing hairline over the whole first layer), and only then looks for the next entry, so a solid's inside never joins; with a peel every field marches at full size through its variant, since the reduced-size pre-pass holds the first layer. A replayed batch, a wireframe, and the ink line draw into the layer unpeeled or not at all; the live grid stays out. Drawn only when and where the near field is (hiddenLayerDrawnLastFrame / hiddenLayerExtentLastFrame): GeometryBatch.worldBounds (a mesh batch's union of each mesh's box through its matrix, a list-placed instanced batch's bounding spheres) gives each batch's nearest view-axis distance against the distance where the lens blurs half a pixel in front of the focus, and the near batches' projected corners (two pixels of margin) scissor the pass, cleared whole first so outside it the layer is the backdrop; a kind with no box (GPU-placed copies, fields, strands, water, points, lines, a marched field, a depth scene, a replay holding a point cloud) counts as near and everywhere. The gather reads it under a near-field pixel (textures 3 and 4, the layer and its own prepass; params[3].x says whether one ran, else the frame stands in for it and every hidden tap reads as unknown, which keeps a frame without the pass on the shipped math): a hidden tap is known when it lies behind its tap's own surface by over 1% (the backdrop is known to hide nothing), and a known tap turns the quarter-weight stand-in off; it lands only for a pixel in front of the focus and only under a tap in front of the pixel or under the pixel's own surface, sorted by distance with no turning-away rule; under the pixel's own surface it is behind that surface whatever its distance says (under a floor of boards the layer holds the next boards' lit sides, within a percent of the top, and sorted as the surface itself they darkened the floor 2%), so it goes to a separate back sum that joins the holes' light only when its mean inverse distance stands in front of the holes' own (a ball's top against the backdrop with the floor hidden behind it: yes; a gap between boards showing the next board's side, with the backdrop under the boards never reached: no; soft-edged at ±10%); a known hole's stand-in (the quarter weight past its own) is kept aside and fills whatever share of the holes' light the layer does not, so a pixel never falls back to its own color where the layer is known but not what the holes show (a near ball behind a nearer one kept a hard dark notch inside the nearer one's rim without it); behind the focus the layer is read only under a veil tap, since what a near ball hides scatters past its rim onto the scene beside it (the lens shows the floor behind a ball's top on the backdrop above), and never under a tap behind the focus (a nearer surface there blocks the rays that would reach what it hides; added anyway it diluted a far bar's light to 93%). Behind the focus the same share model holds (mid, the second veil): the pixel's own surface against the holes that show what stands behind it (farther taps, held to the pixel's own blur), a nearer surface at or behind the focus over that by its cover, the near field's veil over everything, with the plane-continuity test on this side too so a receding far floor is not sorted into veils and holes against itself; averaged by density instead, a bar a tenth as blurred as the backdrop behind it kept 92.7% of its light (98.5% now) and a highlight there 94.1% (98.1%), and the near bar over the subject reads 0.0222 RMS against the layered reference (0.0271). Measured on the lens figure's own scene (880 by 420, the accumulation lens again): the halo over the ball's top +0.0049 green over red, the 30 px band around the ball 0.0097 RMS with 0.0020 in its high-frequency part, and the edge profile never steeper than 0.018 a pixel against the lens's 0.024; what remains is a band outside the ball's upper-left rim over the floor behind the focus, 0.020 RMS, darker than the lens by a floor-colored amount (DESIGN-NOTES). A tap at another distance is another surface only when its lens blur lands off the line the pixel's own surface draws across the screen: the signed uncapped blur is affine in inverse distance, so a plane extrapolates exactly, the slope taken as the gentler one-sided difference per axis; before, a floor running toward the eye read its own farther taps as holes (harmless while the stand-in was the same floor, a dark smear once the layer filled them). Measured against the accumulation-buffer lens on the floor scene: the green cast where the ball's top meets the horizon +0.0137 to +0.0036, the halo's RMS 0.0273 to 0.0170, the band around the ball's edge 0.0143 to 0.0108, its high-frequency part 0.0042 to 0.0037, the whole frame 0.0064 to 0.0050 (the near floor's gap lines read a touch darker, 0.0034 to 0.0042, where the undiluted holes show the gather's taps). Cost at 1080² on the M2, a bead sphere: +10.6 ms .performance, +14.8 .default, up to +34 .detail wide open for the gather; the second drawing plus the gather's two extra reads per tap, the figure scene wide open at 1080² with the near field across most of the frame: 33.8 to 38.7 ms at .default, 73.7 to 82.3 at .detail (the frame without the lens 4.6). The live window and an export match byte for byte at the same tier. DepthOfFieldTests (16: thin-lens radius and light incl. a line a hair behind the focus against the far-plane backdrop, an even highlight disc, a small highlight even and whole at the lowest tier (the sprite's 99.5% against the gather's 94.5%), a sharp bar cutting a far highlight's disc, the in-focus edge, the near veil against the analytic disc-strip cover, no outline across a blurred ball's edge (went red on one minus the holes), the floor hidden behind a blurred ball's top showing through it while nothing shows above the ball, the layer drawn only with something in the near field and only over its pixels (and never for a pinhole), the band and the cap, beside TAA and motion blur, the window equal to the export, pinhole and orthographic untouched with their notes, determinism; the hidden-surface clamp, the own-surface veil, the near and far split, count-not-area weights, the stepped spiral, the near-plane focus, and the export skip each went red, and the window test caught the live skip). Snapshot depth-of-field-3d; Docs figure DepthOfField3D. Docs/3D/3D.md#depth-of-field. The Jacobian of the blur's slope (ruled by measurement 2026-10-05): a tap's density 1/(pi c²) is exact only where its surface's blur is flat; where c varies across a surface the map from a lens point to the image is stretched, and the exact density carries 1 + (q - u) · grad c / c (q the pixel, u the tap, grad c the blur's slope along the tap's own surface). ollin_fx_lens_dof_slope writes that slope per pixel beside each blur map (the gentler one-sided difference per axis of the capped blur, zero where the two disagree in sign, so a one-pixel line or a step between surfaces never stands for a surface's slope), the gather reads it once per tap (textures 5 and 6, the hidden layer's too) and holds the factor to 0...2, with 1 where the pixel clamped the tap's spread; offset runs from the pixel to the tap, so the code's sign is minus (the first cut had it plus and doubled the error, which is the signature to look for). And a near surface's share of its own pixel is its light density, not an area: ownWeight / flat / (1 - X), where flat is what a flat surface at the pixel's own blur returns through the same taps (one on a flat field by construction, under one at a sub-pixel blur where whole-pixel taps cover their footprints only in part; without it an in-focus ball read a quarter transparent, 0.084 RMS on a 20-px block), so the share reduces to the area form on a flat surface and follows the lens on a curved one. Why: to a thin lens a ball is a disc at its center's depth (a ray meets it when it passes within the radius of the center), while the gather spread each rim point by its own wider blur. Measured on the lens figure's scene against the accumulation lens with the near ball flat white on a flat black world (the harness's cover-only switch): the canvas covered +0.02 to +0.035 more than the lens over 0...25 px outside the rim in every direction and 0.04 to 0.06 less inside, +0.63 to +0.83 px of cover integrated outside; the ideal scatter without the factor reproduces that to the hundredth (+0.91 px) and with it closes three quarters (+0.21), light conserved; the canvas after: +0.07 to +0.17 px outside, every ring within 0.01 inside and out. On the lit scene the band outside the upper-left rim (the 0.020 RMS item) reads 0.0109 outside-only from 0.0128 with its teal cast gone (mean rgb error -0.0085, +0.0035, +0.0042 → -0.0044, +0.0002, +0.0008), the top halo's green-over-red bias +0.0049 → +0.0014 (the ~10% item), the top edge's steepest step equals the lens's (0.0127 vs 0.0126, from 0.0094), the right and bottom bands 0.0093 → 0.0039 and 0.0025 → 0.0008, the whole frame 0.0071 → 0.0066 per channel; the red ball at the focus, the horizon rows, and the near floor within 0.0003 of before. aBlurredBallsEdgeFollowsTheLens pins the edge within 0.02 of the thin lens's exact sphere cover (thinLensSphereCover, 4096 lens points, nothing of the gather in it) at every 2 px from 30 inside to a blur outside, 0.017 at its worst and 0.072 on the old gather; every other DOF claim and the snapshot suite held. Open, measured: just inside the top rim the canvas reads 0.02 dark (the sphere's own limb, culled from the peel as back faces), and the dotted gap lines barely move with the tap count (0.0012 → 0.0010 high-frequency error, default → detail tier), both in DESIGN-NOTES.
  • The overlay (withOverlay(_:) on Sketch, Drawer.withOverlay, GeometryBatch.overlay, applyOverlay in MetalRenderer+Overlay.swift, ollin_fx_copy in Renderer/ShaderEffects.metal). A block drawn over the finished frame after every pass (the temporal resolve or upscale, the depth of field, the motion blur, the lens flare, the postProcess filters), before the frame interpolation, the extra displays, the frame grab, and the linear capture, so a caption or a readout stays sharp and untinted. The block records into the frame's own batch list and is flagged when the outermost block closes (overlay on every run from the block's start, clamped against a background inside it that dropped the start), so the geometry rides the frame's ring slot: the canvas pass and every layer pass skip flagged runs (the overlay argument of encode), the overlay pass draws nothing else, and the web page writes the canvas's flagged runs last (WebGraph.items(for:)) while a vector export holds the block's commands aside (SVGRecorder.overlayCommands) and writes them after the rest. The pass is a multisampled geometry pass at the finished frame's size and format that starts by writing that frame into every sample and draws nothing but the flagged runs (the environment's backdrop, which encode lays under a frame's geometry, is gated off the overlay pass: drawn there again, full-frame with no depth, it covered every surface under a showcase camera whose far plane let it through, and OverlayTests.aSkyBackdropNeverCoversTheSceneUnderAnOverlay pins it) (PipelineKey.effectInPass, an effect pipeline at the view's sample count with the pass's depth and stencil declared), so a blend mode composites against the picture as a canvas run's does against the clear (a transparent layer composited after would have multiplied against nothing); its depth is its own, cleared, and it carries the clip stencil when the frame clips. Clipping is per surface: the block starts unclipped and a clip inside it clips the overlay alone (ClipFrame.overlay). Over an accumulation surface the pass draws over a copy of the pile, live and headless, so the overlay never joins it. The scans that read the batch list as scene content skip flagged runs where it matters (the hidden layer's near-field extent, the instanced mover upload); a 3D shape in the overlay sorts among the block and sits over the scene, and a frame with no overlay returns its input before touching the GPU, so every other frame is byte-identical (the snapshot suite). Inside makeBatch { } and inside a layer block the body draws as the surrounding content, noted once. drawCaption draws in the overlay wherever one can be opened (Drawer.canDrawOverlay: not in a layer, not in a batch recording) and in place otherwise, so a caption stays sharp over a lens, a streak, or a bloom and never joins a pile. OverlayTests (6): sharp under the lens where the same bar in the frame blurs away, over everything whatever the call order, a multiply reading the finished frame, never joining a pile, a clip per surface, and the vector order. Docs/Drawing/Effects.md#withoverlay; Guide Ch 33 § The frame through the camera's lens: depthOfField().
  • Lens flare (lensFlare(_:) / lensFlare(amount:lens:) / noLensFlare(), Lens + LensInterface + LensFlare in 3D/LensFlare.swift; Renderer/ShaderFlare.metal + applyLensFlare in MetalRenderer+Flare.swift). The camera's own contribution: light that reflects off two of the lens's interfaces instead of passing through them and reaches the sensor in the wrong place. Every ghost is followed ray by ray, written from the published real-time ray-traced flare method, with the published matrix (first-order) formulation beside it for what first order is good for, and the thin-film anti-reflective coating reflectance (all credited in ATTRIBUTION.md). A Lens is a stack of LensInterface (radius / thickness / index after / clear-opening height / iris flag / optional coating wavelength / optional abbeNumber), the same table a lens patent prints. Bundled: Lens.doubleGauss (1950s 100mm f/2 six-element, 11 interfaces → 20 ghosts in a run of sizes, the chain a flare is known for; its last thickness is the paraxial back focus the table works out to, and one radius the patent's scan garbles was settled by the focal length landing on the patent's normalized 100), Lens.heliar (1950s ~100mm, 9 interfaces → 13 ghosts, most of them wide, faint and cut by the barrel: a veil more than a chain), and Lens.standard, what a bare lensFlare() uses: the double Gauss multicoated at f/8 (any lens wide open is haze and a single coating is one color). stopped(to:) closes the iris; multicoated(from:to:) hands the wavelengths out by a golden-ratio walk, never a front-to-back sweep (a ghost is a pair of surfaces and the brightest pairs are neighbors, so a sweep gave the double Gauss five blues). The matrices are checked against real rays, and they were wrong for a month without that. everyGhostAgreesWithRealRays follows a near-axis ray through the real spheres (Lens.trace, exact vector optics) and demands the matrices' sensor point, iris point and both incidence angles to 1e-5, for every ghost of both lenses. As shipped on 08-20 the chain passed through the inner reflecting surface before reflecting off it and mixed two conventions on the backward leg (the inverse of a forward refraction is real-slope; the travel and the reflection were slope-along-travel), so every ghost was misplaced and missized by factors of 2.6 to 7.5 and nearly all landed on the light's side of center; the incidence angle also had the lean's sign wrong (a ray aimed at the center of curvature must meet the surface head on: slope plus lean). The only check then was the straight-through path (directPupilScale ≈ 0, directScale ≈ focalLength), which has no reflection in it. One convention throughout: heights real, slopes measured along the way the ray travels; a surface met from behind is the same sphere with its radius negated and its media swapped, as a refractor and as a mirror; the inner surface is never crossed. Both reflections stay on one side of the iris. The tracer (Swift Lens.trace, mirrored in ollin_flare_trace): surfaces met in the path's strict order, never by nearest, with a negative ray parameter allowed so a sphere carries past its own rim (those rays are never drawn, they feed the interpolation); the root taken is the pole, by the sign of radius · direction.z; a flat glass surface refracts (the Heliar has one; only the iris is a bare plane, and it records where the ray crossed); relativeHeight is the largest r / clear height over every glass surface; each reflection's coating is evaluated at that ray's true angle; and what crosses a surface is what does not reflect there, which is what lets the mesh have holes: a ray's light falls to nothing as it nears the angle it could no longer leave the glass at, so the neighbors of a lost ray are already dark. On the GPU a ghost is one instance of a shared grid (flareGridSides 16/32/64, picked so a cell spans ~16 canvas pixels, where a 300-pixel barrel arc strays a tenth of a pixel from its chord), traced in the vertex stage, three rays a vertex: itself and its two inward neighbors a full cell away, so the light a cell is given is the light it carried in (squeeze = the area ratio; measuring over half a cell while drawing whole ones lit stray triangles). squeeze has a floor from the source's own picture on the sensor plus a pixel (soft.z, what keeps a caustic finite) and a cap at a twelfth of first order's ratio (a fold finer than the grid would come out as beads). A lost neighbor is asked for on the other side, and a ray with none carries nothing; a lost vertex is put where a surviving neighbor landed so its cells close up. The grid always covers the whole front opening (pupil · (1.12 + barrel softness)): trimming it to first order's preimage of the iris is wrong for a strongly bent ghost, whose real rays enter nowhere near there. The fragment clips softly at the barrel (relativeHeight through 1, as soft as the source is wide) and at the iris (the blade walk + ollin_flare_overlap), keeps its early discards (removing them, since the pass adds, measured 10 to 25% slower), and adds into a half-size, 4x multisampled, memoryless canvas resolved to tracedLight (PipelineKey.flareGhostTrace = the caustics splat's additive depthless shape + isFlareTrace): half size because a ghost's edge is never sharper than its source is small, multisampled because a fold caustic is a mesh silhouette no soft-edge test reaches (at one sample, 2-pixel stairs). The composite adds ghostLight + tracedLight + the full-size star. The followed ghosts read the visibility strip in the vertex stage, per light (left out at first, a hidden lamp still flared; the occluder probe caught it). Color: three channels in one pass at the printed index for most ghosts; seven wavelengths, one pass each, only when the colors part by more than 1.5 canvas pixels and the ghost reaches flareColoredFrom (0.02), since wide faint veils part by the most pixels and would otherwise take seven passes each for nothing (that, a 96-grid and five rays a vertex were 2.6M rays a frame, +18 ms). Budget flareMaxTracedDraws 144; over it the faintest give up their colors. Ringing inside a crisp edge is straight-edge Fresnel diffraction read off the iris's edge distance, scaled per ghost by LensGhost.ringScale = sqrt(2|m| / (lambda B)) from the iris-to-sensor matrix, per channel, and worked out only while the source's blur is under one ring. What first order still draws: a ghost whose source is wider than the opening it came through is a picture of the source, its bends smeared away, and the backward per-pixel map has that smear in closed form (ollin_flare_ghosts: per-channel maps, the coating from a per-lens table Lens.coatingTable read at the middle of the stretch of the opening that really passes, never the rim, the source's disc through a cubic overlap ramp that caps at the ratio of the areas, meeting for two openings both smaller than the source). followed cross-fades the two per ghost over max(sP/pupil, sI/inner) 0.3…0.6, so nothing changes hands in one step; MetalRenderer.flareFollowsRays (internal, test-only) turns following off. Level, anchored and the lens's own: set once under standard conditions (iris open, 0.15 rad, a 0.02 rad source) as the lower of two ceilings, no ghost above lensFlareGain 0.1 and all together adding no more than lensFlareVeil 0.025 to a normal frame on average (both lenses are bound by the second). Closing the iris gathers each ghost's light by as much as it shrinks that ghost and no more (gathered, per ghost: a barrel-bound ghost does not shrink, so it does not brighten; one blanket factor turned the Heliar's main veil up 3.5x for nothing), and dims the star's arms by the square as it lengthens them. A directional source's projected depth is held at 1; a point source's visibility depth is pulled 2% toward the eye; visibility is its own 4x1 pass, a 17-tap ring over sourceSize. Composites after motion blur, before the frame filters, on all three chains. Not a defect: the Heliar's (6,8) ghost is a hexagon with curved-in sides and sharp cusps (it passes the iris first and is bent hard after: pincushion distortion of the iris's image), and a near-focus ghost can fold into a small comet. Both were chased as artifacts. Isolate a suspect by drawing one ghost at a time before theorizing. The star (LensFlare.star / starSize / wear, 3D/ApertureStar.swift): the far-field (Fraunhofer) diffraction of the iris, its own power spectrum, so the arm count is the blade count for an even iris and twice it for an odd one, a round iris throws only a halo, and stopping down grows it. CPU bake (vDSP FFT over an anti-aliased 512² aperture, 48 wavelengths at scale 550/lambda with the analytic CIE fit, ~100 ms release), cached by blade count and dust in twentieths, rgba16Float. Load-bearing: the aperture edge is anti-aliased (a hard step lays false arms down the diagonals); scaled so its mean is 1 with a 4000 ceiling; a smoothstep window over the outer 18%; drawn extent starSize * openIris/iris held at 8x. wear is the wear on the opening (uneven and bowed blades split and fray the arms; specks and scratches become needles), seeded from the blade count; wear: 0 is the clean polygon the arm-count tests measure. An edge normal lies along x first for the ghosts, the star and the defocus filter alike. Three extras from outside the spherical glass, all 0 by default: streak (+ streakLength / streakAngle / streakTint) is what cylindrical glass does to a light (the front group of an anamorphic lens, or the fine grooves of a streak filter made to imitate it): a cylinder bends light one way only, so it fans a light across itself into one line through the source, as thin as the source is wide (max(sourceSize · 1.2, 1.3 px)), with a faint skirt and a (1 - t²)^1.5 taper; it is closed form in the full-size composite. (An early draft of the docs claimed an anamorphic lens does not throw this line, on the strength of one write-up; it does, from its own cylindrical elements, and the claim was checked and taken out before it shipped.) dirt is the scattering term of the published dirty-lens image formation model: grime on the front element is too near to focus, so each speck is the defocus kernel, the iris's own shape and size (it takes apertureBlades and shrinks with the f-number), lit by the flare's sources with a forward-peaked falloff 1 / (1 + (d / flareDirtReach)²)^1.5 and by the visibility strip; up to flareDirtSpecks 160 instanced quads from one fixed seed (so dirt stays put), added onto the half-size first-order canvas through PipelineKey.flareDirt. halo (+ haloSize) is a look and not optics, and the docs say so: corona diffraction from fine particles was worked through as its basis and set aside, since it puts a central glow ~57x brighter than its ring; the ring is nine wavelengths with the analytic CIE fit, each at its own radius, spread a third of what diffraction would give (three channels gave three hard bands). Lens.anamorphic(squeeze:) (Lens.squeeze, 1 on an ordinary lens) is opt-in because it changes the look a good deal: what forms behind the cylindrical front group is stretched sideways with the picture (the light reaches the lens at frame.x / squeeze, the ghosts' and the star's x is stretched back by it; for the followed ghosts it is carried in the aspect handed to the tracer, for the first-order ones halo.w), and the dirt is left alone: the tall oval an anamorphic lens is known for belongs to blur far from it. For a point a distance z in front of an afocal cylindrical pair the displayed blur is (z + L + D) / (s z + L + D/s) as wide as tall (s the squeeze, L the pair's spacing, D the way on to the pupil), which is 1/s far off, matching real bokeh, and about 1 at z = 0, on the glass. The specks were drawn tall at first on the assumption that front dirt is bokeh; @eaviles asked whether that was right, and the derivation says it was not. The source stays round on the frame, so it is an ellipse 1 / squeeze wide on the sensor, and every soft edge is worked out against that: a bladed iris's distance is measured on the frame (each blade's distance scaled by bladeStretch before the largest is taken, or a crease runs down every corner's bisector), a round edge narrows the source along its normal (ollin_flare_narrowed, the support function under a crisp edge fading to the outline for a picture of the source), and the area cap carries the squeeze on both sides (ollin_flare_overlap and the CPU's shares, which must agree). Left as a disc on the sensor, a ghost's sides are twice as soft as its top and a near-focus ghost is a 2:1 picture of a round lamp (probe: across-over-up 0.89 of the plain lens's, 0.53 without). The halo stretches with the ghosts (the whole flare does, per the 2015 course in ATTRIBUTION.md); the streak does not. Same glass, same ghosts: optics() does not change. Three rules of the iris distance, all lenses: past a corner it is the distance to the vertex (the top two blade distances and one root; largest-of-planes miters the corner outward, a spoke from every corner of a wide soft rim, and draws a round source through a small opening as a polygon); red and blue take green's distance carried by one dot product only within green's slack (the same blade still first and the second still inside), and ask again past it (a near-focus ghost parts its colors by more than its width, and the unguarded shortcut creased it); and the followed ghosts' light and valid are centroid_perspective, since the pass is multisampled and a plain varying is read at the pixel's center even when only a corner sample of a sliver is covered, which extrapolated to negative light: single black pixels in a veil (probe: no pixel darker with the flare on; 20 were). All of it measured free, old shader against new in one saturated process (7.46 ms both, a 6x blade walk read 25.4). wear was dust for a few hours on the day it was added, and was renamed before release so it would not sit beside dirt. All three measured free (5.55 ms against 5.55). Probes: the streak is one line and nothing a row above it, the halo peaks at its radius with a dark inside, the dirt goes out with a hidden lamp; each seen to fail with its behavior broken. Cost at 1080² on the M2, one source, debug host: +1.2 ms for a stopped-down chain to +4 to +5.5 ms for a lens whose ghosts each fill the frame; the first-order loop is free once every ghost is followed. Measure only with the GPU saturated (frames back to back, wall clock over 120, variants by shader reload in one process, a variant with the triangles thrown off screen to split rays from fill): the drained-GPU read hid a 20 ms regression and read a cubic as slower than two acos. Example 3D/Effects/LensFlare; snapshot lens-flare; Guide figures GhostChain + StarPoints (both probes); LensFlareTests (23 pure: an anamorphic lens asked for and clamped, the extras off by default and clamped, real-ray agreement for every ghost, the departure from first order toward the rim and ghosts on both sides of center, the barrel stopping a ray halfway up the opening, the focal-plane check on both lenses, the iris rule, coating minimum, TIR continuity, cemented junctions, stopping down, multicoating and its neighbors, the matrices, the dispersion curve, the channels parting, the coating table) + ApertureStarTests (7) + LensFlareRenderProbes (12, the anamorphic one reading the ghosts' spread across against up, and the rest in linear light at a fraction of the usual level: adds light, fades rather than ends under an occluder, a wider source softens the ghosts' edges (read only over flat scene, since a ghost crossing the lit bulb steps by its whole level at the bulb's edge), stopping down gathers the light and adds none, following the rays stops a ghost at the barrel (48% of first order's pixels, 73% with the clip taken out, so the line is 60%), the star lands on the source, the blades reshape it, off is byte-identical; the fade, the star pair, the source, the iris and the barrel probes all sabotage-verified red). Docs/3D/LensFlare.md.
  • Temporal upscaling (temporalUpscaling(_ quality:) / noTemporalUpscaling(); MetalRenderer+Upscaling.swift + ollin_fx_velocity_fill in Renderer/ShaderCombine.metal). The live window renders the whole frame at a reduced size and the platform temporal scaler (MetalFX) reconstructs the full canvas from the jittered history: the tier maps to a render fraction (.performance half per axis / .default two-thirds / .detail three-quarters, clamped to the device range), per-pixel cost falls by the square, edges stay temporally anti-aliased. Per-frame state like TAA; same envelope (main canvas + active 3D camera, 2D no-op with a note, targets/accumulation keep MSAA) plus a supportsDevice gate (Apple silicon; elsewhere note + full res). Load-bearing: every geometry-side pass sizes to the render size (the logical canvas never changes and the encode sets no explicit viewport, so shrinking the attachments is the whole change; off, the sizes are equal and the frame is byte-identical by construction); the scaler replaces applyTemporalAA (it is the jittered accumulation — feeding it the averaged output would integrate twice) and drops taaHistory while it runs; its motion texture is the unscaled velocity fill (raw previous − current pixels y-down, both VPs unjittered — the buffer's designed contract at motionVectorScale 1; the raw-not-scaled passthrough is test-pinned), its jitter offsets are the content's own pixel shifts from the shared Halton table extended 16 → 32 (cycling min(32, ceil(8·s²)) phases; TAA's first-8 cycle and the export's ≤16 prefix are untouched, prefix test-pinned); motion blur under it runs at output size reading the render-res depth/mover by UV with the mover values rescaled by a moverScale whose (1,1) default multiplies exactly; a same-frame repeat returns the slot's output without re-encoding (the scaler's internal history must advance once per frame, the taaHistory rule); creation failure flips the support flag (note once, never retry per frame). Headless/export never upscale (a live frame grab carries the upscaled picture the window shows): headlessTemporalAAActive routes an upscaling sketch to the full-res deterministic TAA supersample, pinned byte-identical to asking for temporalAntialiasing() (the export is the full-quality equivalent of the live preview; the GPU benchmark also stays full-res). Live verification is measurement (the artificial-life honesty rule): converged static-scene frame diffs 0.08-0.11/255 mean (inside the TAA-on band, and stiller than un-upscaled because the scaler absorbs the RT-reflection sparkle), FPS A/B 29 → 60 (vsync-capped) at .performance on the M2 probe scene; record in ARCHITECTURE.md. Example 3D/Effects/Upscaling (tier + toggle parameters); UpscalingTests (9, fill + gate sabotages verified red; no pixel snapshot — the scaler is a stateful platform object). Mechanism: ARCHITECTURE.md § Temporal upscaling. Docs/3D/3D.md#temporal-upscaling.
  • Frames between the drawn ones (frameInterpolation() / noFrameInterpolation(); MetalRenderer+Interpolation.swift, sharing encodeFXVelocityFill with the upscaler). The live window runs draw() on every other refresh, and the platform frame interpolator (MetalFX, macOS 26+) builds the frame between the two drawn either side of it from color, depth, and the same motion field the upscaler reads, so a scene that holds 30 drawn frames moves at the display's 60. Per-frame state like TAA. Load-bearing: the drawn frame is held back one refresh and the made one goes to the drawable first (the made frame belongs between the two, so showing the drawn one first would run time backwards), which is where the one-refresh input latency comes from; the runner asks hasHeldFrame at the top of a refresh and then runs no sketch code at all (the clock is untouched, so the next step spans both refreshes and the motion keeps its real-time speed); the two drawn frames are kept in a ping-pong pair (writing the newer one must not touch the older one the interpolator is reading, and the newer one is still needed a refresh later); isDepthReversed is false (Ollin's depth runs 0 near, 1 far, and the platform default is the other way); the jitter offset is 0 because the color handed over is always a resolved frame (TAA's resolve or the upscaler's output) already on the reference grid; hostAllowsInterpolation carries what the renderer cannot see (a take playing or recording, a still noLoop() sketch, a wall of displays), a reload calls dropHeldFrame(), and a sketch that stops the loop inside the draw that just ran has its held frame presented straight away (it would otherwise never be asked for another refresh, leaving a guessed picture as the final still); the depth resolve must be allocated for it (it was gated on TAA / blur / upscaler / flare, so the first live run made nothing at all until interpolation joined that condition — caught by the live-loop probe, not by the seam test). The interpolator repeats the drawn frame rather than smearing when it cannot follow the motion, including the first frame of a run, which is why starting up shows no artifact. Needs a camera with a field of view (verticalFieldOfView: perspective, or derived from a pinhole calibration; orthographic returns nil and the frame is drawn normally with a note). A frame grab never interpolates, and an export writes only the frames the sketch drew unless it is asked for slow motion out of made frames (--made-frames, its own bullet below), which drives the same interpolator through its own export slot; frameInterpolation() itself still leaves an export untouched, test-pinned byte-identical to not asking. The motion field is slot.motion, never what the fill returns (that is the mover pass, whose unwritten pixels carry the sentinel -16384): binding the return value warped every frame that declared a withMotion mover, which nobody saw on screen at 60 fps and the export measurement found at once. Example 3D/Effects/FrameInterpolation (toggle + speed parameters); FrameInterpolationTests (7: the made mark lands halfway (history sabotage verified red), the warm-up repeat, the projection-to-fov mapping, the gate, the held-frame alternation, the live loop over an off-screen MTKView with an off counterfactual, the export contract; no pixel snapshot — the interpolator is a stateful platform object). Mechanism: ARCHITECTURE.md § Frames between the drawn ones. Docs/3D/3D.md#frame-interpolation.
  • Line drawing (Sources/Ollin/3D/LineDrawing.swift; Sketch.lineDrawing(of:creaseAngle:spacing:) over [Mesh] or one, plus Mesh.transformed(by:) for placement). A scene written down as 2D paths with what the surfaces hide taken out, which is what puts a 3D scene on a pen plotter: the paths are ordinary canvas line work, so SVG, PDF, G-code and DXF carry them where a lit mesh reaches none of those. Three passes of CPU work: weld (positions merged on a grain of 1e-6 of the scene's own span, since a hard-edged mesh holds a copy of each corner per face and without it every edge reads as a boundary), classify (an edge is kept when it has one face, when its two faces turn opposite ways on screen, or when their normals differ by more than creaseAngle), and walk (each kept edge sampled every spacing canvas points against the faces that could cover it, found through a grid of the canvas, with each change of visibility pinned by eight halvings, then the stretches chained end to end taking the straightest continuation at a junction). Load-bearing: the facing test is the screen turn of a projected face, not a normal against a view vector, so it needs no winding convention and catches a silhouette on any tessellation; the depth compare interpolates one over the depth under perspective and the depth itself under an orthographic camera, and needs a bias of a thousandth (an edge lies on its own faces); faces with no area are dropped or a lat-long sphere's poles scribble, since the direction of a degenerate cross product is arithmetic noise (the first render showed it as a black blot at the pole); a stretch shorter than a canvas point is dropped, which is the width of that bias where a hidden edge runs into the silhouette it ends on; geometry behind the camera is cut at the near plane, not at the eye, since a point at the eye projects nowhere; and the chaining takes the straightest neighbor at a junction, without which a cube's outline breaks into five paths instead of closing. hidden carries the covered stretches for the draughtsman's dashed line. Ten tests in LineDrawingTests: a cube shows nine edges and hides the three at the far corner (by their middles, since the two corners on the diagonal land on one place); it comes back as few paths with the outline closed; a wall takes out the middle of what stands behind it and leaves the ends; a ball is its silhouette alone at the radius the camera says, once round; no crease angle is the whole tessellation; a plane is its border; nothing behind the camera is drawn and something half behind it is cut at it; the hidden set meets the far corner and is a third of what is seen; an empty scene draws nothing. The occlusion test and the silhouette classification were both verified red by sabotage. Snapshot line-drawing; Examples/Export/LineDrawing; Docs/3D/LineDrawing.md; Guide Ch 42 § A 3D scene on the plotter: lineDrawing(of:) + the SceneAsLines figure.
  • Scene import (Sources/Ollin/3D/Scene.swift + the GLTFDocument split in MeshLoaderGLTF.swift). loadScene / Scene(contentsOf:) is the structure-preserving complement to loadMesh: a glTF/GLB file's default scene as a tree of SceneNode values (name, authored local transform kept verbatim as a matrix — never decomposed, so nothing is lost to a TRS round-trip; an optional Mesh in node-local space, that node's primitives merged by the loadMesh rules) plus the authored cameras (core glTF) and punctual lights (KHR_lights_punctual) resolved through their nodes' world transforms into ordinary Camera3D / Light values the sketch applies itself (camera(scene.camera ?? …), light(_:) each); the USD family (.usdz/.usdc/.usda/.usd, SceneLoaderUSD.swift) is read natively end to end — one USDStage parse serves structure, meshes, cameras, materials, lights, animation, and skinning; Model I/O is out of the USD path entirely. Load-bearing: the node tree keeps authored child order and every node carries per-prim identity (sourceIndex, assigned depth-first in authored order — what every track binds by, so a name duplicated across branches animates exactly the prim that authored the samples); meshes rebuild from the authored data — indexed on the authored points normally, expanded per face corner when normals or primvars:st are faceVarying/per-face (the deforming form must stay indexed, so there such a set drops; primvars:normals wins over normals, primvars:st:indices dereferences and a blocked (None) indices attr reads as unauthored, fan triangulation in authored winding, leftHanded reverses, a bad-index face skips whole); materials read the bound UsdPreviewSurface spec-correctly — inputs:diffuseColor re-encodes linear→sRGB (the glTF-factor treatment; the fixture scripts author linear literals computed from the designed display colors, so the committed assets kept their look and no snapshot re-recorded) and a connected UsdUVTexture resolves its image through the usdz package entries or the layer's folder (USDAssetStore), primvars:displayColor the fallback; cameras resolve from the prims' authored attributes (fov = 2·atan(verticalAperture / 2·focalLength), ortho height = verticalAperture/10, near/far from clippingRange) through the shared resolveCamera(projection:near:far:world:sceneCenter:) tail; visibility = "invisible" hides its subtree and a guide/proxy purpose skips render (nodes stay in the tree; meshes and lights skip — the lights go through the same walk, so a hidden light stays dark); upAxis/metersPerUnit are deliberately not applied (a scene arrives as-authored, the loadMesh contract; probed: the platform importer ignored both too); Material/Shader/NodeGraph/Skeleton/SkelAnimation/BlendShape/GeomSubset prims never become nodes (each is consumed by its own pass); a subdivision-surface mesh draws its control cage (mesh.subdivided(_:) is the user's refinement path); the raw tree's format-dependent shapes (.token vs .string, tuple arrays) normalize at this consumer tier through the shared usd* accessors on USDValue — new USD consumers use those, never re-switch. Mesh.loadUSD is the merged form over the same walk with node transforms baked into the vertices (the platform merge read local vertex data, so a multi-part file's parts piled at the origin — a real fixed defect); first authored material wins, UVs all-or-nothing; STL/PLY/ABC keep loadViaModelIO. USD lights (SceneLoaderUSDLights.swift: the walk attaches specs that move with their nodes for the UsdLux prims — SphereLight→point, +shaping:cone:angle→spot (half-angle° ×2 → coneAngle, softness → penumbra), DistantLight→directional, RectLight/DiskLight/CylinderLight→rect/disk/tube area lights (extents scale with the prim's transform; cylinder length runs local x) — all emitting local -z, worlds from the internal xformOp evaluator, inputs: prefix with bare pre-21.02 fallbacks, the same per-kind brightest=1 normalization over intensity×2^exposure and linear→sRGB colors as glTF; the usdz leg pinned by a test-built 64-byte-aligned stored zip both readers accept). USD transform animation (SceneLoaderUSDAnimation.swift: the same raw-tree read bakes each animated prim's op stack — composed local matrix at the union of its authored sample times via localXform(at:)/USDAttribute.sampled(at:), decomposed to TRS keys with hemisphere-continuous quats — into ONE unnamed SceneAnimation for the layer's whole timeline (scene.animations.first), so pivot pairs / single-axis / matrix ops all play; timebase = timeCodesPerSecond ?? framesPerSecond ?? 24 offset by startTimeCode; interpolation is a runtime stage setting never authored in-file — linear default, quats slerp, non-lerpable holds — so baked tracks are LINEAR; the bake returns path-keyed entries the scene walk maps onto sourceIndex, and each animated prim's decomposed rest pose installs as its node's trs base; crate leg pinned against /usr/bin/usdcat (a reference crate writer, tool-gated) baking byte-identical tracks from both containers). USD skinning (SceneLoaderUSDSkinning.swift: UsdSkel skeletons/skins/blend shapes resolved from the same raw-tree read into the shipped deforming tier — attach-only now, the walk already built every mesh. Load-bearing: each Skeleton prim synthesizes joint nodes — a container named after the prim at its evaluator world transform, one node per joint nested by the joint paths, TRS base = local rest (derived from the world-space binds when restTransforms is unauthored), appended after the tree nodes with joint identities numbered past every prim index, so SkelAnimation joint tracks, blend-shape weights tracks, and xform tracks all bind by the one identity scheme; inverseBind = inv(bindTransform)·geomBindTransform makes worlds[joint]·inverseBind·p exactly the UsdSkel equation through the shipped pose path; elementSize N / constant-interpolation rigid bindings / the skel:joints remap all resolve (past 4 influences the heaviest win); morphs apply before the skin (probe-pinned by the rigid-binding test)); any other loadMesh format degrades honestly to a single-node scene with no cameras or lights. drawScene(_:) is a save/multiply/drawMesh/recurse/restore walk over modelMatrix, so every mesh rule (materials, shadows, reflections, export/batch gating) applies with zero new rules — byte-identical to the equivalent manual transform-stack calls, pinned by SceneLoaderTests. Per-material submeshes: a multi-material mesh (glTF distinct-material primitives; USD materialBind GeomSubsets — first subset claims a contested face, unclaimed faces keep the mesh's own binding) carries internal SceneMeshPart index slices on its node over the one merged vertex order, so morphs/skins pose once and drawScene emits one ordinary drawMesh per part off the posed arrays (CoW struct copies, nothing duplicated); the public mesh stays whole with the first material (standalone draws unchanged), a single-material node carries no parts (the untouched byte-identical path), and partsVertexCount guards a swapped mesh back to the whole-mesh draw (the skin-alignment treatment) — parts-vs-manual byte-equality + swapped-mesh render probes in SceneLoaderTests. Load-bearing: Mesh.loadGLTF's own merge loop is untouched (it bakes world transforms as it appends and smooths missing normals after baking, so rebuilding it over per-node local meshes would not be byte-identical — the two share only the GLTFDocument readers, and MeshLoaderTests pin loadMesh unchanged); light intensities normalize per kind to brightest = 1 (glTF's lux/candela mean nothing without distance falloff, which Ollin's punctual lights don't model), colors re-encode linear→sRGB; the camera's target is the scene-bounds center projected onto the view axis (orbit-friendly), ortho height = 2·ymag; cameras/lights move with their nodes: the loaders attach internal node-local specs (SceneLightSpec/SceneCameraSpec, emitting down local -z, intensities normalized at load) and scene.cameras/scene.lights are computed properties re-resolving through the tree's current transforms on every read, so a moved carrier node — by hand or by an applied animation — carries its camera/light, while assigning either array (tweaking one element in place counts) freezes it to the hand-set world-space values (fixedCameras/fixedLights); node(_:) returns a copy, the subscript writes back in place (scene["part"]?.rotate(dt, axis: .unitY) spins about the node's own pivot); Scene takes the bare public name, so the hosts spell some SwiftUI.Scene (the Image/Environment precedent); the bundled demo assets are generated by Scripts/make-sample-scene.swift / make-animated-scene.swift / make-skinned-scene.swift / make-usd-scene.swift / make-usd-animated-scene.swift / make-usd-skinned-scene.swift (the textured-cube precedent — no third-party license). Scene animation — scene.animations (SceneAnimation, 3D/SceneAnimation.swift) + apply(_:at:) plays the file's node-TRS keyframe tracks on the sketch clock: absolute posing (same time → same pose, so per-frame application converges; loop by wrapping time over duration, one-shots hold their last key per the spec's clamp), samplers per spec (STEP / LINEAR with shortest-arc slerp for rotations / CUBICSPLINE Hermite with segment-duration-scaled tangents and normalized quats; rotation outputs may be normalized ints, decoded by the spec equations). Load-bearing: an animated node's base is its authored trs from the file's own fields — never decomposed from the matrix (a matrix-authored node has trs == nil and no track touches it, per spec) plus sourceIndex (the file node index tracks target); the position setter syncs trs.t so a hand-set position survives a rotation-only track (a hand rotate on an animated node is overwritten by the next apply — standard animation semantics); tracks sort by node index (the determinism rule). Skins & morph targets (3D/SceneSkinning.swift + the localMeshData merge) — the deforming tier, posed by drawScene with no new user API: deform data is stored on the SceneNode, not the public Mesh (skinIndex/vertexJoints/vertexWeights/morphTargets internal; weights public — the hand-drivable blend-shape surface the weights animation channel also writes; node weights override the mesh's authored defaults, the spec's precedence). Load-bearing: joint matrices are scene-root world × inverse bind (one nodeWorldTransforms() pre-walk, only when the scene has skins — a skinless walk is structurally unchanged, byte-identity still pinned) and a skinned mesh's own node chain is ignored (the spec's rule): the posed mesh draws under the drawScene call's model matrix, pinned by render probe; morphs apply before skinning; a misaligned skin falls back to the undeformed draw with a one-time note, never a mis-draw; skin attributes merge all-or-nothing across primitives (the UV rule) and morph data drops whole on a target-count mismatch; the weights channel stores k scalars per keyframe (CUBICSPLINE groups all in-tangents, then all values, then all out-tangents per key — WeightsSampler pins the layout); readVec3 reads sparse and zero-filled no-buffer-view accessors (the morph-exporter staple); vertex weights renormalize over the influences actually used, a zero-weight vertex keeps bind pose. The per-node warning surface is public: wearsSeveralMaterials + placementIsExact (a shear that TRS can't reproduce; drawScene still draws the verbatim matrix), the reads the SceneExplorer example leans on — a read-only lens on a scene file (orbit or the file's camera, stage parameters, per-light toggles, a part stepper that highlights or isolates one part inside a capsule-edge bounds cage, the warnings said on the part that carries them; it never edits the file, ollin new --from-scene is how a scene becomes source; one non-obvious rule it states in a comment: the light rig is frame state, not stack state, so a noLights() inside withState unlights the whole frame). Examples 3D/Geometry/LoadedScene + AnimatedScene + SkinnedScene (each bundles a glTF and a USD stage; F swaps) + SceneExplorer (all honor env override OLLIN_SCENE; the animated ones declare loopDuration, wraps verified byte-identical — the USD ones pinned by Metal-gated render probes); snapshots loaded-scene + animated-scene + skinned-scene + usd-scene + usd-animated-scene + usd-skinned-scene; SceneLoaderTests + SceneAnimationTests + SceneSkinningTests + USDAnimationTests + USDSkinningTests (usdcat crate cross-check + a platform bind-matrix oracle). Mechanism: ARCHITECTURE.md § Scene import. Docs/3D/Scenes.md.
  • USD parser core (OllinUSD) (Sources/Ollin/3D/USD/ — internal, no public API; the substrate of the complete USD-native arc). Ollin's own clean-room reader opens all three containers — .usda text (recursive descent, a deliberate superset of the grammar), .usdc crate 0.8-0.10 (bootstrap + TOC sections + ValueReps, LZ4 via Apple Compression + the delta/varint integer coding), .usdz package (own central-directory zip; default layer = first usd-extension entry; USDStage.open keeps the archive + layer name so the scene loader can resolve package assets) — content-sniffed, never by extension, yielding one raw tree (USDStage/USDPrim/USDAttribute/USDValue) in authored order (crate: path-traversal order overridden by primChildren/properties); this tree is the only source the whole USD loadScene/loadMesh path reads (the scene-import bullet). Envelope: flattened single layers + self-contained packages, variants at defaults, no composition — out-of-envelope constructs (variantSets, references/payloads, splines, relocates) skip string-aware as .unsupported, never a throw. The xformOp evaluator (USDXform.swift, the raw tree's first consumer tier): USDPrim.localXform() + USDStage.visitPrims compose authored ops per xformOpOrder — listed ops outermost-first (= left-to-right column-vector composition), row-vector matrix4d (its four rows load directly as simd columns), degrees, a three-axis rotate's value always (x,y,z) angles with the name picking application order, !invert! + !resetXformStack! honored, dangling ops skipped — cross-checked against Model I/O's own composition of the example camera (USDXformTests). Load-bearing: the raw tree keeps the file's shape — tuple arrays flat with an arity, float arrays stay Float (bit-exact cross-checks), and untyped metadata differs by format (text .stringArray vs crate .tokenArray — consumers normalize, the raw tier doesn't) — with one deliberate exception: quats arrive real-first (w, i, j, k) from both containers (crate stores the raw struct imaginary-first, the one shape where the containers genuinely diverged, so the crate reader reorders scalar + array quats at decode); the crate traps (AssetPath's token-vs-string index asymmetry, Vec2h's raw-half inlining, the 64-bit integer coder's wider vint widths, TimeSamples' double indirection, payload-0 = empty array, sub-16-element "compressed" arrays stored raw) are honored in code and documented — don't re-derive them. Verification is all local: Model I/O exports .usda and .usdc (crate 0.8) from one asset — export both under one file stem (the root prim is named after the file) — the two independent parser paths must agree and match the Model I/O read-back; for a file Model I/O didn't author, MDLMesh.vertexCount is the expanded per-face-corner count (== our faceVertexIndices count), never the authored points count. System vectors: the CoreUSDEdit shaderball (crate 0.9; its authored child order — the one Model I/O alphabetizes — is pinned at both the parser and the loaded-Scene level), the PencilKit pen usdz set (0.8). Reparse pinned byte-deterministic. USDParserTests; format-study credit in ATTRIBUTION.md. Mechanism + format facts: ARCHITECTURE.md § The USD parser core.
  • Camera control & cinematic moves. Two opt-in halves over the shipped Camera3D, both driving the Camera3D.orbiting pose (object-on-a-turntable, not scene-flythrough) and set as the frame's camera — a sketch that calls neither keeps its own. cameraControl() hands the camera to the viewer (drag orbit, scroll dolly, right-/shift-/option-drag pan; exponential damping toward an input-driven goal + a capped momentum flick; the canonical orbit-control model, deltaTime-corrected, studied not ported). cameraMove(_:) plays a curated CameraMove — .turntable / .sway / .pushIn / .pullOut / .tilt / .orbitAndRise / .reveal / .handheld (cyclic ones phase-driven, finite ones eased from the base at the move's clock each frame, handheld a private Perlin field). A move's clock reads the frame being posed (pose, then advance, the order time keeps: a move's first frame is its start and export frame N shows it at N/fps at any rate; advancing first put every exported frame a step ahead, CameraClockTests at 30 and 60 fps). Both take their framing arguments as the rig's home (CameraRig.frame, a CameraFraming): the first call opens on it, and a changed argument moves it, so a @Param in the framing tunes the window the way an export frames it (a move's base glides to each changed part only, so a move that departed from a hand-framed angle keeps it; the controller glides only while it still rests at home, until the viewer orbits, pans, dollies, or snaps away; the lens under every driver; an exponential approach at 8/s, since a dragged parameter changes the framing every frame and an eased glide restarted every frame never moves, landing exactly within 1e-9), and a constant changes nothing, byte for byte (aConstantFramingMovesNothing). They compose over the current pose (CameraRig.lastMode re-syncs the controller's goal after a move, and a move resumed after the hands re-bases from them even when it is the same move), so "frame by hand, then drift" is one call after another, and the rig clamps elevation off the poles. All three calls take a from: Camera3D form seeding the opening shot from any authored camera (a loaded scene's scene.camera — the "open on the authored framing, then hand the viewer the camera" one-liner): Camera3D.orbitPose decomposes eye/target into the orbit pose, an orthographic camera opens the rig flat (frame(orthographic:), frame height mapped to the equivalent fov at the target distance so the projection toggle holds scale), near/far default to the camera's own, and roll is dropped (the rig is y-up by design). The shared Timeline<Value: Tweenable> sequencer (Sources/Ollin/Math/Timeline.swift — timed keyframes + per-segment easing + loops, FrameAdvancing like @Eased, Tweenable over Double/Vector2/Vector3) is the substrate, available to sketches (the future @Param keyframe client too). The rig (Sources/Ollin/3D/CameraRig.swift + CameraMove.swift) is a plain let advanced explicitly, not FrameAdvancing (the Mirror-collection cache only sees pre-first-frame stored properties); its platform-neutral input surface — scrollDeltaY (double-buffered in advance() so a wheel event between frames is never lost), modifiers (mapped behind the AppKit seam like KeyCode), rightMouseIsPressed, the mouseScrolled() hook — is plumbed once through OllinMTKView so it reaches standalone + gallery + OllinLive. cameraShowcase(_:) is the fused default the ~42 auto-orbit 3D/* examples adopt: it plays a CameraMove the viewer can interrupt and, after idleReturn (10s) of no input, eases back over returnDuration (4s, Hermite, shortest-arc azimuth) to the home framing (the last call's framing, not wherever the viewer left it), then resumes — a 3-phase driving/manual/returning state machine, byte-identical to a plain move when uninteracted (so headless/export/snapshot paths are unaffected and core cameraMove/camera are untouched). .autoOrbit(period:) is its curated default move; activeCamera exposes the resolved Camera3D (e.g. placing geometry at the camera eye, as DepthCompositing does). Scene inspection views — cameraView(_:) / resetCamera() snap the rig to canonical angles (CameraView: .reset restores the home framing; the six axis views swing only the orbit angle, keeping target + radius; .isometric is true isometric, .corner the deprecated alias), gliding by default (animated: false cuts) and handing the pose back to the active driver via CameraRig.driver on completion, so motion resumes with no jump; byte-identical when no snap is active (camera-move not re-recorded); surfaced by the host Camera menu (OllinCameraCommands, ⌘0–⌘7 in all three hosts) → OllinActiveSketch → SketchRunner.requestCameraView (applied next frame); a 2D / hand-set-camera() sketch ignores it. Live-only scene chrome: cameraAxis() shows the interactive orientation widget (AxisWidget, a SwiftUI sibling — click an axis to snap, plus reset / isometric / an Ortho⇄Perspective toggle) and groundGrid() a faint reference floor at y=0 (a .grid PipelineKey pass, depth-tested-but-not-writing, outside the deferred render graph) — host chrome like the FPS overlay, never exported, 2D no-op, toggleable from the Camera menu. The Ortho⇄Perspective override is session-scoped: SketchRunner clears ollin.hud.orthographic once per launch (the axis/grid toggles re-sync from the sketch's own flags on reload, but the projection override had no such reset) — persisted, a stale orthographic toggle in a host's private defaults domain read as broken rendering a session later: parallel view rays flatten the environment backdrop to a single average color and reroute every reflection, and nothing on screen names the menu that did it (the gallery is where it bit — the same examples looked correct standalone because each bundleless host has its own defaults domain). Load-bearing grid invariant: the infinite-grid LOD divides the once-computed base world-uv derivative by each decade scale — never dfdx() of a pre-scaled uv (the derivative spike at a LOD boundary shatters the lines into a crawling dotted band, a real fixed bug); major lines are distinguished by width in the one line color, not a brighter color or a separate per-class fade (the old form read as stacked planes at staggered depths — don't reintroduce it). Examples 3D/SceneViews, 3D/CameraControl + 3D/CameraMoves (the lower-level demos); snapshot camera-move (a deterministic .turntable — the input-driven paths are covered by the CameraRigTests / TimelineTests CPU units, since a snapshot can't drive synthetic events). Full mechanism: ARCHITECTURE.md § Camera rig. Docs/3D/Camera.md, Docs/Helpers/Animation.md (the Timeline section).
  • RGBD recordings + live USB (Record3D) (OllinRecord3D satellite). Record3DRecording opens a Record3D .r3d clip and Record3DDevice reads a tethered phone's live USB RGBD stream — both unproject color + depth with true intrinsics into a PointCloud (over the core RGBDFrame / CameraIntrinsics; a cloud measures itself via centroid — nil when empty — and spreadRadius, the RMS distance about it, the two numbers a framing camera eases toward). All Apple-native decode (a minimal ZIP reader + LZFSE + JPEG) and a clean-room USBMux (usbmuxd) client to TCP 1337; the formats read clean-room (Record3D's library is LGPL — read its public structure, never translate; credit as a product reference, vendor nothing). USB only, not WiFi. Gotchas: K is column-major at capture resolution → scale to the depth grid; the depth grid isn't in the metadata (derive from the LZFSE float count + capture aspect); usbmuxd's PortNumber is network byte order; the trailing live-stream slot is a JSON misc blob, not confidence; unprojection lands in ARKit camera space (y-negate + z-negate). RGBDFrame gained depth(atNormalizedX:y:) / unproject(normalized:) (hole-robust median, y-flip). Recorded poses: Record3DRecording.poses / pose(at:) (which throws frameOutOfRange outside the recording exactly as frame(at:) does, and is nil only for a frame with no recorded pose, the Optional and the throw meaning two different things) — per-frame camera-to-world from the metadata poses array ([qx, qy, qz, qw, tx, ty, tz], quaternion order pinned by the identity's 1 in slot 3), the sweep-fusion path for a saved clip; a clip captured without world tracking (TrueDepth) stores every pose as the identity — real motion can't be recovered after the fact, so probe poses for variation before fusing (single frames reconstruct fine). Docs/3D/Record3D.md, Docs/3D/RGBD.md; examples 3D/Record3DCloud (file playback, live USB when a phone streams), DepthLiftedPose.
  • iPhone capture app (Ollin Capture + OllinPhone). Ollin's own iOS app (Apps/OllinPhoneApp, xcodegen + Xcode) streams on-device ARKit perception over USB: a 3D body skeleton (each joint a position and an orientation quaternion with a camera-observed flag — ARKit's rig fills unseen joints in, and those report untracked — plus the anchor's world transform standing the skeleton in the same ARKit world the depth sweep and room mesh use, and the person's estimated scale via automaticSkeletonScaleEstimationEnabled; bodies bundle per frame like faces, so an empty set clears a person who left), a face (deforming mesh with its topology and per-vertex texture coordinates + 52 expression blendshapes + the two eye poses and the look-at point the eyes converge on, up to 3 faces), world-facing rear-LiDAR depth (a PointCloud + the 6DoF camera pose, via the core RGBDFrame), person segmentation (a matte + cutout, via the core-lifted SegmentationImages — fed by the rear camera in Segment mode, ARKit's on-device segmentation, or by the front camera in Selfie mode, the app's one non-ARKit sensor: an AVCaptureSession + Vision pass whose capture connection rotates buffers upright (so the wire's turn count is always 0) and mirrors them like the front-camera preview — the matte is computed from the delivered buffer, so matte/color alignment is by construction, and both modes use the same .segmentation wire kind so the Mac side needed nothing new), the hands in view (up to 4 at once, each a 21-joint skeleton found on-device by Vision over the ARKit frames in Hands mode — every joint an upright 2D image point, plus on a LiDAR phone a metric 3D position in ARKit world space, lifted through the scene-depth map and the camera pose), the text it can read (each line a PhoneText in Text mode — the string, the reader's confidence, and four upright corner points, lifted to world space the same way), the pictures and objects it knows (a reference picture or a scanned .arobject, found in the room as a named 6DoF placement with its real size, in Markers mode), a map of where the picture draws the eye (a coarse attention heat map plus the regions it peaks in, in Attention mode), how the picture is moving (a dense motion field between consecutive frames, in Flow mode), the reconstructed room mesh with the flat planes in it, the room's light, and CoreMotion device motion. The Mac client PhoneDevice (@MainActor, self-retrying) exposes latestBody / latestBodies / latestFaces / latestHands / latestHand / latestTexts / latestText / latestMarkers / latestMarker / marker(named:) / latestSaliency / latestSaliencyHeatMap / latestSaliencyFrame / latestFlow / latestFlowFrame / latestMotion / latestFrame / latestSegmentationMatte / latestSegmentationCutout / sceneMesh / planes / latestLight; PhoneBody / PhoneFace / PhoneHand / PhoneMotion mirror the Vision accessor shape; PhoneBody adds worldPosition(_:)/worldCenter/orientation(_:)/modelTransform(_:)/worldTransform(ofJoint:)/scaleFactor/isJointTracked(_:) and a public init so a pose stages with no phone (the tests and the staged-figure verification do). Transport: the shared OllinUSBMux tunnel + the PhoneWire codec (pure Foundation/simd, compiled verbatim into both ends; streamPort 1338). World-pose fusion — the core WorldCloud accumulator places each depth frame by its pose into one fixed world cloud (sweep the phone, fuse the room); see the Fusing a sweep bullet for it and for the drift correction that keeps a long one registered. Room mesh — ARKit scene reconstruction streamed per anchor block (.sceneMesh, one block per message, keyed by a stable UUID, with a removed retirement notice); PhoneSceneMesh keeps the newest of each in arrival order (apply/remove/reset) and builds mesh (whole room, world space), mesh(of:) (filtered by label, unreached vertices dropped), and mesh(colored:) (unwelded, a color per triangle); PhoneSurface is the per-triangle label (wall/floor/ceiling/table/seat/window/door/unclassified, raw values contiguous from 0). Load-bearing: the phone reads a block's geometry inside the ARKit callback (the Metal buffers are the session's, and nothing promises they outlive the call) and throttles the send instead (a FIFO queue, ~15 blocks/s, so a block that stops changing still gets its turn); a geometry source is read three floats at a time honoring its own stride (a SIMD3<Float> is 16 bytes over a 12-byte entry and would run past the end); phoneSceneChunk places positions with w=1 and normals with w=0, and refuses a whole block whose index runs past its vertices; sceneMeshVersion is the rebuild gate (a room is hundreds of thousands of triangles, so draw() must not rebuild per frame); removal rebuilds the id→slot map, since every later block shifts down; and each block carries a per-run scan number, because session.run(…, .resetTracking) moves the world origin, so the Mac must drop the old room rather than mix two coordinate spaces (leaving Room mode and returning is a new run). Flat planes — ARKit plane anchors stream beside the mesh from the same session (.plane, one anchor per message, same id/scan/removed shape, its own FIFO ~20/s), so surface and planes share one world origin; PhonePlane carries the label (the same PhoneSurface), PhonePlaneAlignment (horizontal/vertical), world-space center/normal/width/height, the convex boundary, plus area/mesh/outline/bounds; PhonePlanes is the id-keyed set (apply/remove/reset) with flat/upright/planes(of:)/largest/largest(of:)/floor/foundSurfaces/bounds/mesh(colored:). Load-bearing: plane detection needs no LiDAR, so the Room session starts either way and adds sceneReconstruction only when supported (the mode is useful on a non-Pro phone); phonePlane reverses an outline whose winding disagrees with the anchor's up axis (a fan built the wrong way faces backwards and the fill vanishes; anOutlineWoundTheWrongWayIsTurnedRound pins it, verified red by sabotage) and synthesizes a rectangle from planeExtent when no outline arrives (turned by rotationOnYAxis), so every reader downstream has exactly one shape to handle; largest measures the outline's area (a fan of cross products), never width × height, or a long thin shelf outranks a floor; floor falls back to the lowest horizontal plane, because a label arrives late. Room light — frame.lightEstimate in every mode via the shared LightSampler (a LightReporting protocol on all six ARKit streamers, throttled to 5/s), so a mode switch never interrupts it; PhoneLight scales lumens to intensity (1000 lm = 1), turns kelvin into color (Color(kelvin:)) and ambient (that color scaled by intensity, clamped at 1.5 for ambientLight(_:)), and exposes direction/keyIntensity/sphericalHarmonics/key only in Face mode (ARDirectionalLightEstimate is face-tracking-only, 27 coefficients as raw Data). PhonePlane and PhoneLight both take a public init so a room and a reading can be staged with no phone (the Guide figure and the tests do). The eyes and the gaze — PhoneFace adds eyePosition(_:)/eyeOrientation(_:)/gazeDirection(_:) (face-local, per PhoneEye), their world twins worldEyePosition(_:)/worldGazeDirection(_:), lookAtPoint/worldLookAtPoint (the convergence point), headTransform (the head pose composed for transform(_:), so the face-local mesh stands with the eyes inside it), and meshUVs; mesh() carries the texture coordinates only when they align with the vertices (a misaligned set is dropped, never shifted onto the wrong vertices), and a public init stages a face with no phone (the Guide figure and the tests do). Load-bearing: the eye poses and lookAtPoint are sent over the wire face-local (the SDK's own convention — relative to the anchor's origin), stood in world through the head pose; the gaze axis is the eye's +z (out of the face; a device-unverified parameter — if a live run reads backward, flip it in gazeDirection, one place). Hands — PhoneHand carries chirality (from the camera's view) / confidence / per-joint has(_:)/confidence(of:), the 2D overlay (point(_:in:)/points(in:)/bones(in:), upright normalized on the wire whichever way the phone is held), the 3D side (position(_:)/bones()/cloud()/center/hasWorldPositions), the gesture staple pinchDistance (thumb tip to index tip, meters, nil unless both lifted), and the static skeleton (20 bones) / tips / fingerChains; a public init stages a hand with no phone (the staged-render verification and the tests do). Load-bearing: the pose model runs behind a drop-if-busy gate on its own queue (a slow pass skips frames, never backs up the camera) and everything the lift needs is copied inside the ARKit callback (the room-mesh lesson); the model is handed the device-hold orientation and its points come back upright, which is what the wire carries, and the 3D lift maps each point back onto the camera-native depth grid through PhoneWire.bufferPoint(fromUpright:quarterTurnsCW:) — pure shared arithmetic, pinned by a Mac test (mapsUprightPointsOntoTheBuffer) — then takes a median depth from a small window (a fingertip's own pixel is often a hole at the silhouette edge), unprojects through the depth-grid intrinsics, and applies the camera pose; a joint over a depth hole keeps its 2D point with hasWorldPosition off, so position(_:) is honest about what lifted; the empty set clears hands that left (the faces precedent); PhoneHandJoint raw values stay contiguous 0…20 (the wire carries a joint as one byte; pinned like the blendshapes); without LiDAR the mode still runs and hands stay 2D. Text — PhoneText carries text/confidence, the 2D overlay (corners(in:)/bounds(in:), upright normalized on the wire), and the world side (worldCorners/worldCenter/worldWidth/worldHeight/hasWorldPlacement, plus worldTransform — an orthonormalized frame for transform(_:): origin at the quad's center, x along the reading direction, y up the line, z out of the surface, nil on a degenerate quad); a public init stages a line with no phone (the Guide figure and the tests do). Load-bearing: TextStreamer mirrors the hand streamer exactly (its own queue behind the drop-if-busy gate, everything the lift needs copied inside the ARKit callback, the device-hold orientation handed to VNRecognizeTextRequest, corners lifted through the shared depth arithmetic — now extracted to the app's DepthLift.swift, which both streamers use, with the median window clamping its pixel first so an edge corner can't build an inverted range); a line lifts all four corners or none (three corners are not a quad; the all-or-nothing normals precedent); it reads at the .accurate recognition level (.fast misses room-scale text; a few finished readings a second, absorbed by the gate); latestText ranks by imageArea (the shoelace area of the normalized quad — recognizer confidences cluster near 1, so area picks the sign you mean); the empty set clears lines that left (the faces precedent); the placement test is sabotage-verified (a flipped normal turns it red); the worldTransform z axis faces out of the surface toward the reader (a device-unverified parameter — if a live run reads backward, flip the cross in worldTransform, one place). Markers — the pictures and objects the phone knows (.marker, bundled per frame in Markers mode): each a PhoneMarker carrying name/kind/isTracked/position/size/corners/facing/across/up and placement, an orthonormal frame with its origin at the middle of the thing, x across the width, y up the height, z out of the face, ready for transform(_:); PhoneDevice adds latestMarkers/latestMarker (the biggest one being followed)/marker(named:), and a public init stages a find with no phone (the Guide figure and the tests do). The library is the app's own Documents folder (UIFileSharingEnabled + LSSupportsOpeningDocumentsInPlace, so Finder and Files reach it), read at mode start and on a Read the folder again button. Load-bearing: a picture's printed width is carried in its file name (poster@30cm.png / card-50mm.jpg / plate 12in.heic / tile_0.4m.png; cm/mm/in/m), since no image file carries one, and the parser lives in the shared PhoneWire (markerWidth(fromName:) / markerName(fromFileName:) / markerStem) so a Mac test pins arithmetic the phone runs — the unit must end the name (diagram, platinum state nothing), only a known reference extension is stripped (teapot.arobject keeps its name; tile_0.4m keeps its decimal point), and a size outside 1 cm…5 m is refused rather than trusted (a wrong width puts the print at the wrong distance rather than losing it); ARKit folds its own scale estimate into the anchor, so PhoneMarker normalizes the basis and reports that scale through width/height instead (a scaled frame would scale everything drawn through it, anEstimatedScaleBecomesSizeNotAScaledFrame); a picture anchor lies in its own x-z plane with +y out of the printed face, so the placement turns a quarter (x = anchor x, y = −anchor z, z = anchor y) — a device-unverified convention, and if a live run draws into the wall, that quarter turn is the one place to flip (sabotage-verified red); an object anchor is not trackable (found once, holds its place, reported tracked), so an object marks a place and a picture marks a moving thing; each reference is validated as it loads, so a picture with too little detail is named on the app's screen rather than looked for in vain; sending is paced (every frame while anything is followed, 5/s while nothing is), so an idle library does not stream at camera rate; and applyMode stops every session from one list, so a tenth mode cannot forget one. The wand — the phone held as a pointer (.wand, one reading per camera frame in Wand mode, plain world tracking so it needs no LiDAR and no depth): PhoneWand carries position/pointing/up/across/placement (orthonormal, x across the screen, y up it, z out of it toward the holder, so pointing is its -z, matching how a camera looks in Ollin's own 3D) plus ray (the core Ray3), point(at:), isTracked, isPressed, pressCount, and touch, with PhoneDevice.latestWand and a public init so a wand stages with no phone (the tests and the Guide figure do). Load-bearing: ARKit fixes the camera basis to the landscape sensor whatever the hold, so the frame turns by the hold's own quarter-turn count, and that arithmetic lives in the shared PhoneWire.wandFrame(fromCamera:quarterTurnsCW:) (a Mac test pins what the phone runs) — the raw pose and the turn are both sent over the wire so the fix is a Mac rebuild rather than a phone one, and a wrong count tips the drawing without moving the aim (device-unverified; that one function is the place to flip it, sabotage-verified red); the press is carried twice, pressed for the state a drag reads and a rising pressCount so a tap that landed and left between two draw() calls is still seen (the poll->=-never-== rule in another dress); the thumb is -1…1 across and up behind a flag, so nothing touching survives the trip as nothing rather than as the middle of the pad, and it reads as a rate (a few centimeters of pad, a whole room); the basis is normalized defensively and a collapsed pose falls back to the world axes rather than handing a sketch a direction full of NaN; the button keeps working while isTracked is off, since tracking comes and goes as the room is recognized; and the phone-side pad is one DragGesture(minimumDistance: 0), so the landing, the slide, and the release are one gesture. Attention — where the picture draws the eye (.saliency, one reading per analyzed frame in Attention mode): PhoneSaliency carries the coarse heat map (salience(at:in:) / salienceNormalized(at:) — the query turns the top-down heat rows over for the lower-left convention and clamps at the edges, sabotage-verified red) and regions (PhoneSalientRegion: bounds(in:)/center(in:)/confidence/hasWorldPlacement/worldCenter), with strongestRegion by confidence; PhoneDevice adds latestSaliency / latestSaliencyHeatMap (a tintable white-alpha glow via the shared SegmentationImages, cached by sequence) / latestSaliencyFrame (the color frame stood upright lazily); public inits stage a reading with no phone (the Guide figure and the tests do). Load-bearing: SaliencyStreamer mirrors the text streamer (its own queue behind the drop-if-busy gate, the device-hold orientation handed to the request, so the heat map and boxes come back upright and are sent over the wire that way; the color stays camera-native with the turn count beside it); only each region's center is lifted (a box's corners land on the background); a heat plane whose stated dims outrun its bytes is refused at init, so a query can never index past it; every reading carries a heat map, so there is no empty-clear and the last reading holds between passes. Device-unverified parameter: whether the model's heat plane arrives upright under every hold (if a live run reads turned, the streamer's orientation hand-off is the one place to fix). What the phone hears — the built-in sound classifier over the phone's own microphone (SoundStreamer, SoundAnalysis, the same 1.5 s window the Mac's classifier settled on by measurement), streamed as the .sound kind (=14): the whole judgment every window (every label with its confidence, strongest first, a two-byte count since the vocabulary is three hundred-odd, the window's start and length on the phone's audio clock counted from when listening began), never the audio. Load-bearing: the wire runs one way, so the threshold and the crossing live on the Mac: PhoneSounds (device.sounds, a @MainActor class over its own lock, fed straight from the read thread) keeps the level (confidence(of:) / topClassification / classifications) and the trigger (draining events() + timeSinceHearing(_:)) with the Mac classifier's own crossing-from-below rule (a held set of what is above, so a continuing sound fires once), over the same SoundClassification / SoundEvent values, which moved from OllinAudio into the core (Core/SoundClassification.swift, the satellites-cannot-depend-on-each-other rule) so a sketch written against either pair of ears reads the other unchanged; an event's time is the end of the window it crossed in, and between readings the Mac carries the phone's clock forward with its own so a fade on timeSinceHearing runs smoothly rather than stepping once a window; isListening is read off the readings (one within 3 s), since nothing on the wire says the switch went off; hear(_:at:duration:) is public so the ears stage with no phone (the tests and the Guide figure do). Hearing is a switch, not a mode (the app's Hear chip, remembered in its defaults): it needs no camera, so it runs beside every mode, survives a mode switch, and asks its own microphone permission (NSMicrophoneUsageDescription); an audio-session interruption pauses it and the end of one restarts it. Device-unverified: the microphone's format on the A17 and the classifier's pace there. How the picture is moving — optical flow between consecutive rear-camera frames, measured on the phone (FlowStreamer, Vision's VNGenerateOpticalFlowRequest at medium accuracy over the ARKit frames, in Flow mode; plain world tracking, so no LiDAR) and streamed as the .flow kind (=15): a dense map of (dx, dy) float pairs at a bounded size (the frame scaled to 256 on its long side before the pass, the field averaged 2×2 to ≤128 across, ~100 KB a reading), the interval between the two frames measured, the model's confidence, and the color JPEG, all camera-native with the device-hold turn count beside them. PhoneFlow (device.latestFlow) reads the way the Mac tracker's field reads — vector(at:in:) / samples(in:every:) / averageFlow(in:) / flowNormalized(at:) / averageFlowNormalized / size / confidence — over the same MotionField value (field), which moved from OllinVision into the core (Core/MotionField.swift, closure-backed: init(width:height:confidence:flowNormalized:), the Vision tracker wrapping its observation in the closure so a camera-resolution map is still never converted up front), so a helper written against either takes both; interval is the one read the Mac's tracker cannot give (its analysis gap breathes with load), so a vector over it is a speed; latestFlowFrame is the frame stood upright lazily; a public init stages a reading with no phone (the Guide figure and the tests do). Load-bearing: the Mac stands the map upright by turning the grid and every vector in it by the same quarter turns (PhoneFlowPlane.upright: one clockwise turn sends the top row to the right column and a rightward vector downward; pinned against the shared rotatedCGImage so the map and the color frame agree cell for pixel), a query reads bilinearly between the four nearest cells (the map is coarse and a particle crossing a cell edge must not snap) and clamps at the edges, and a map whose stated size outruns its vectors is refused at init and at decode; Vision's raw vectors under this request are the motion of the picture itself (forward), the opposite sign from the newer API the Mac's tracker negates — pinned by visionsFlowRequestReportsTheMotionOfThePicture over confetti at the streamer's .medium accuracy, so the phone puts the map on the wire unsigned and wireField in the streamer is the one place to flip if a device ever reads backward; the streamer scales each frame into one of two owned BGRA buffers it alternates (ARKit's capture buffer is never held past the pass that read it, and the previous frame the next pass measures against is the streamer's own copy), behind the drop-if-busy gate every Vision streamer runs, and stop() forgets the previous frame so a mode coming back does not read the gap as one huge motion. Device-unverified: the classical flow's pace on the A17 at 256 across, and whether the hold's turn count stands the field up under every orientation (the raw map and the turn are both sent over the wire, so a fix is a Mac rebuild). Body / Face / World / Segment / Selfie / Room / Hands / Text / Markers / Wand / Attention / Flow / Touch are behind a phone-side toggle (mutually exclusive; motion always streams; light streams in every ARKit mode — Selfie and Touch run no ARKit session and send none; thirteen modes outgrew the segmented control, so the app draws five rows of chips, a short row padded so every chip keeps its width, with Hear and Air beside them as switches rather than modes since neither needs a camera). The screen as a control surface — Touch mode (.touch, kind 16) runs no sensor at all: the glass is the sensor, so no camera session starts and the phone stays cool through a set. TouchStreamer assigns each finger a rising number that is never reused and sends the whole live set every time it changes (PhoneTouchSample: the clock plus a PhoneTouchPoint each — id, -1…1 position, hasForce/force, radius as a fraction of the screen's width, age); PhoneTouches (the PhoneSounds shape: @MainActor, nonisolated lock, public feel(_:at:) so a figure or a test stages it with no phone) gives the level (down, isTouching, touch(id:)) beside the trigger (draining taps(), non-draining tapCount, timeSinceTap), over PhoneTouch / PhoneTap with point(in:) onto a canvas rectangle. Load-bearing: a number the Mac has not seen is the landing, which is the whole tap derivation and is why a drag fires once rather than per frame (aNumberNotSeenBeforeIsALanding, verified red by sabotage); the reader feeds every message into PhoneTouches off the main actor while draw() sees only the latest, so a finger that lands and leaves between two frames is gone from down and still in taps(); an empty list is a real reading (the last release), so the decoder must accept count 0 or the Mac holds a finger that left, and the reader calls releaseAll() on a dropped connection for the same reason (the phone sends only on a change, so a cable pulled mid-press is a note that never ends); hasForce is carried beside force because a screen that cannot weigh a press reports maximumPossibleForce == 0 (most iPhones since 3D Touch), so a sketch reads nil rather than 0; the pad is a plain UIView with isMultipleTouchEnabled (SwiftUI's own gestures follow one finger), all four phases funnel into one report of the whole live set, and a mode switch away sends one empty reading. The air around it — .air (kind 17), CMAltimeter relative-altitude updates behind the app's own Air switch (a switch, not a mode: no camera, and the altimeter asks its own Motion & Fitness permission, so a sheet never opens mid-performance; NSMotionUsageDescription widened to say so). PhoneAir carries pressure (kPa, hectopascals for a weather report) and altitude, relative by design — meters above wherever the phone started, good to about a tenth of a meter, negative going down, which makes lifting the phone a continuous control that costs no camera; CoreMotion's own units are the wire's, so there is no conversion to get wrong in two places. The wire's second direction — the first traffic from Mac to phone. PhoneDevice.use(_:) names a PhoneCaptureMode (the mode list moved into the shared wire file, so it travels both ways) and look(for:) hands over [PhoneReference] (a picture's own bytes plus the printed width no image file carries, or an .arobject), so the .swift sketch carries the whole piece and a phone that has never seen it learns what to look for the moment it connects. Three decisions, made once: the framing is the same 12 bytes both ways (one version byte for the whole wire); the tag space is its own, numbered from 1 beside the sensor kinds, because a frame's direction is decided by which end reads it and never by its bytes; and the magic differs (PhoneWire.requestMagic), the one place the framings part, which is what makes a sensor frame written down the cable fail at its header instead of decoding as whatever request shares its number (aSensorFrameIsNotARequest, sabotage-verified). Declarations, not commands: the device keeps the mode and the library and says both again on every connect, so plugging in (or launching the app) late is the same as doing it first; a tap on the phone still wins, and the sketch reads latestState (kind 18, PhoneStateSample into PhoneState: mode, isSupported, referenceCount, referencesAreDeclared, status, notes, sent on change) to learn where it landed — the only place a picture ARKit judges too plain to find is ever heard about, which is why the answer exists at all. The library protocol is PhoneLibraryInbox, in the file both ends share so a Mac test pins what the phone runs: a library(count:) opens a declaration and throws away a half-staged one, each reference appends, the set commits on the frame that completes it, count 0 commits an empty library at once ("look for nothing"), a stray reference is dropped, and a half-received library never commits, so a cable pulled mid-declaration leaves the phone looking for what it was looking for before; no generation number, because TCP orders the stream. The reading end's framing arithmetic is PhoneWire.takeRequests(from:), in the same shared file for the same reason: it holds a partial frame for the next read, skips one that is framed right but will not decode, and returns nil (drop the connection) only when the bytes are not this framing at all. Requests are drained by the read thread between reads (sendPending), so the socket is touched by exactly one thread and the second direction adds no lifetime hazard of its own; SO_SNDTIMEO matches the read timeout so a phone that stops reading cannot park that thread inside a write. A declared library replaces the app's own folder (whose screen says so, and whose re-read button hides) until the sketch disconnects, when the folder takes over again. A picture is fitted on the Mac: past PhoneReference.maxPictureEdge (1024 px, ARKit wants detail rather than size) it is redrawn and encoded JPEG at 0.9 then 0.6, and within that and one frame it travels byte for byte so a hand-made PNG arrives exact; at most PhoneWire.maxReferences (32) travel. Gotchas: the segmentation upright-rotation must be an accelerated CGContext draw, not a byte loop (debug-build frame rate); the 52 blendshapes are sent over the wire positionally so PhoneBlendShape raw values must stay contiguous 0…51; PhoneWire.version stays 1 pre-1.0 (both ends rebuild together). Docs/3D/Phone.md; examples 3D/PhoneBodyPose / PhoneBodyFigure (a solid mannequin posed by the joint orientations, standing where the person stands) / PhoneCostume (ribbon trails + rotation-twisted plumage worn by the live skeleton; header credits its Super You inspiration) / PhoneFace / PhoneGaze (eyeballs at the streamed eye poses, beams converging on the look-at bead) / PhoneDepthCloud / PhoneWorldScan / PhoneSegmentation / PhoneRoomMesh / PhoneRoomPlanes / PhoneHands (the hands as solid little skeletons in the room, a pinch closing into a bead, a 2D overlay without LiDAR) / PhoneWorldText (each readable line as wire-frame stroke type on a framed panel, standing where it hangs; a 2D overlay without LiDAR) / PhoneMarkers (a city of columns rising off every print the phone recognizes, framed by the print's own edge; a scanned object as the box its scan measured) / PhonePointer (the beam out of the back of the phone landing on a ball, a press picking it up, the thumb pushing it out along the beam, letting go dropping it) / PhoneAttention (the heat map as a warm glow over the live frame, confidence-weighted frames on the regions, an eased bead trailing where the attention has been) / PhoneSounds (every sound that starts ringing out in its own hashed place, the strongest label named, six familiar levels as bars with the threshold a @Param) / PhoneFlow (the field as streaks colored by speed over the live frame, and dust that it carries) / PhoneTouches (every finger as a disc sized by its width, a ring where each landed, the lift down the right edge); PhoneSceneMeshTests (19), PhonePlanesTests (24), PhoneBodyTests (6), PhoneFaceTests (7), PhoneHandTests (8), PhoneTextTests (8), PhoneSaliencyTests (7), PhoneMarkerTests (10), PhoneWandTests (12), PhoneSoundsTests (14: the wire round trips including a 320-label vocabulary and a truncated reading, and the invariants over staged readings), PhoneFlowTests (15: the wire round trips and a truncated map, the three quarter turns against the shared image rotation, the reads over staged maps, and Vision's own sign under the phone's request), PhoneTouchTests (17: both wire round trips including an empty set and two truncations, the landing rule and the drag that does not re-tap, the tap that outlives the frame it fell in, the cable pulled mid-press, the draining split, the canvas mapping, and both kinds' raw values), PhoneRequestTests (36: the request round trips, both crossed-frame tests, the header refusals, the whole inbox rule, the stream reassembled at seven chunk sizes, the mode list, the state round trips, and the Mac-side reference building including the fit; four sabotages red) + MotionFieldTests (4, the core value on its own); Guide Ch 36 figures RoomAsSurface + RoomAsPlanes + HandsAsSkeletons + GazeAsBeams + WordsInPlace + AttentionAsHeat + FlowAsField + PrintAsStage + WandAsPointer + HeardAsMarks + GlassAsPad + DownTheCable (one declaration as a sequence, its byte counts read from the shipped encoder).
  • Fusing a sweep, and keeping it registered (Sources/Ollin/3D/WorldCloud.swift + 3D/CloudRegistration.swift, core, source-agnostic). WorldCloud is a voxel accumulator: add(_:transformedBy:) places a camera-space cloud by its pose and keeps one point per cube, so re-seeing a wall refreshes it in place and the cloud is bounded by surface area rather than frame count; PointCloud.transformed(by:) and Vector3.transformed(by:) are the placement primitives. add(_:correcting:) / align(_:from:) line the frame up against what is already fused before merging, which is what stops a long sweep smearing (a tracker's per-frame error never cancels, so it grows into tens of centimeters); the fix is kept in correction and returned as CloudAlignment (pose, correction, overlap, error, passes, applied). Frame-to-model point-to-plane ICP, implemented from Low, UNC TR04-004 (2004). Load-bearing: the recovered angles must rebuild a true rotation matrix, never the linearized one the system was built from (it is not a rotation, so feeding it back swells the scan a little every round) and only solverAnswersWithATrueRotation catches this, because at a well-tracked frame's angles the two matrices agree; the 6x6 normal equations are solved through their eigenvectors with a pseudo-inverse threshold, so a direction the geometry does not pin down is answered with zero instead of a huge number (one blank wall corrects across itself and never slides along itself); correction is the next frame's starting guess, which is what leaves only the newest error to find and lets one ring of cubes be enough; correspondence is approximate by design, the cube holding the point answers in one hash lookup (the grid keeps one point per cube) and the wider ring is paid only by misses; normals are fitted per voxel on demand and cached, widening to two rings when the neighborhood is thin (a feed sparser than the grid gets none at one ring, found by test); overlap counts points that found a surface, not points that got a normal (conflating them makes the guard refuse good frames); the step's turn is read from sine and cosine, never the trace alone (the trace is flat near zero, so a convergence test on it can never pass in single precision); a fit with too little overlap or one asking for a jump is refused and the cloud goes in at the fix earlier frames established, never at the raw reported pose. Measured at about 5 ms per round in the debug build against a 46k-point model at 1200 samples, roughly half what merging the frame costs. The pseudo-inverse cutoff is a conditioning threshold, not a numerical one (unmeasured = 1e-3 of the largest eigenvalue): set near the floating-point floor the solve answers a direction the geometry does not pin down at all, and a camera facing one flat wall slides along it a few cm a frame while overlap and leftover error both stay perfect (measured: 0.416 m of slide over 20 frames became 0.011 m when the cutoff moved from 1e-8 to 1e-3). CloudAlignment.stability reports the smallest of the six eigenvalues over the largest (Gelfand et al., 3DIM 2003) and Settings.minStability refuses on it (default 0, so the live path is unchanged; the loop matcher raises it). Docs/3D/Phone.md (World fusion, Keeping a long sweep registered); examples 3D/Depth/ClosedLoopScan (no hardware, side by side, C cycles the right half) and 3D/Phone/PhoneWorldScan (C cycles it); WorldCloudTests (8) + CloudRegistrationTests (15), all GPU-free.
  • Closing a loop: recognizing a place already scanned (Sources/Ollin/3D/ScanGraph.swift + 3D/PoseGraph.swift, core). ScanGraph wraps a WorldCloud, corrects every frame the same way, and keeps a keyframe every keyframeDistance / keyframeTurn (pose, reported pose, a cloud thinned to keyframeDetail, and the reach of what it saw). A new keyframe near an old one (searchRadius, maxViewAngle, at least separation keyframes back) is fitted against a patch built from that old one plus its neighborhood; an accepted match becomes a robust edge in a pose graph, which is straightened, and the fused cloud is rebuilt from the keyframes' new poses. add(_:correcting:) returns an Update (alignment, keyframe, Loop); Loop carries keyframe/recognized/overlap/error/moved. The solve is least-squares Gauss-Newton with Levenberg damping on the SE(3) manifold, written from Grisetti et al., A Tutorial on Graph-Based SLAM (2010), with the box-plus retraction and numerically-differentiated Jacobians from Blanco-Claraco (2010/2021); the sparse normal equations are never assembled and are solved by diagonally-preconditioned conjugate gradients. Load-bearing: one pose is held (Grisetti's gauge fix; without it the recovery tests fail, verified red by sabotage, while a "the held pose never moved" test cannot fail because two mechanisms enforce it); the retraction is a body-frame turn plus a world-frame move, and the numeric derivative must go through that same retraction; the matching fit runs twice, wide then narrow (a single narrow pass mis-converges on an error bigger than its reach and reports a confident wrong answer: at 0.5 scale drift a one-pass match made the scan worse, 0.40 → 0.45, where the two-pass one gives 0.37 → 0.08); the matcher sets minStability because a match against one flat wall records drift as though measured (aMatchTheGeometryCannotPinDownIsRefused, verified red by sabotage); rebuild() must reset world.correction to the last keyframe's straightened pose or the live feed keeps adding frames where the map used to be; every odometry edge weighs the same (weighting each by its own fit's stability was built and measured on two scenes, helped one and hurt the other, and was dropped); turnScale defaults to the scan's own median reach (fixed too small in a big room, the scan comes out square but turned). Envelope: drift past roughly matching.range * reach is refused rather than guessed. Measured on a staged 1.15-lap sweep with the per-frame fit off: worst keyframe error 0.223 m → 0.048 m. What it buys is a scan that agrees with itself, which is not the same as a scan in the right place: one closure pulls the two ends of a walk together and shares the difference along the chain, so it earns most of it at the point of return, and in a small room where nearly every frame sees something already fused the per-frame fit has taken most of the drift out before the walk gets back (the Guide figure's staged room is one such: the closed half draws each wall once and holds a quarter fewer points, while its poses score no better against the truth). Docs/3D/Phone.md (Recognizing a place already scanned); examples 3D/Depth/ClosedLoopScan (no hardware, side by side, true walls overlaid) and 3D/Phone/PhoneWorldScan (C cycles reported / lined up / closing); PoseGraphTests (9) + ScanGraphTests (14), GPU-free; Guide Ch 35.
  • Accessibility: color vision and motion (Sources/Ollin/Color/ColorVision.swift + Core/Accessibility.swift + the .colorVision Filter case). The author-facing half of the accessibility tier (the viewer-facing half is the bullet below): ColorVision (a kind + a severity, the Sim struct-plus-Kind shape; .normal / .protanopia / .deuteranopia / .tritanopia + the protanomaly(_:) family; Kind: ParamOption) drives Color.simulated(_:) / Palette.simulated(_:) / Ramp.simulated(_:) and Filter.colorVision(_:) for a whole frame, plus the palette judgment confusions(under:) / confusions() / isColorblindSafe() and the published Palette.colorblindSafe (Okabe-Ito). Sketch.prefersReducedMotion reads the system setting through the AppKit/UIKit seam. Load-bearing: at severity 1 the model is a projection along the missing cone's axis onto the dichromat's surface, built at load from published measurements rather than stored numbers: Brettel, Viénot and Mollon 1997's two half-planes for every kind (the gray axis with 475 nm and 575 nm for protan and deutan, with 485 nm and 660 nm for tritan; a separator through the gray axis and the missing cone's axis picks the half, and no projection crosses it), over Smith-Pokorny 1975 cones and the Vos-corrected sRGB primaries, with the display's white as the neutral axis (every gray comes back exactly). The derived matrices match the reference implementation's printed ones to its own rounding (2.5e-5; it rounded its anchors and its primaries). Viénot 1999's single plane was built and measured, and refused at his ruling: it is exactly r = g in linear RGB, so every blue reads lavender (0.03 to 0.06 OKLab from Brettel's), though it stays inside the display by design. A severity below 1 is the straight line in linear light from the color to its projection: Machado, Oliveira and Fernandes's anomalous table (shipped until 2026-10-09) was measured against a projection end and removed, since its path carries pure blue 0.17 (protan) and 0.20 (deutan) OKLab away from where severity 1 puts it. A result outside the display range is clamped channel by channel (the GPU fragment clamps only the low end, since a layer may carry light brighter than white for the tone map), and that is measured, not inherited: in the viewer's own two cone responses (white = 1) the clamp moves what a protanope sees by 0.011 on average (max 0.035), the nearest in-range point on their surface by 0.066 (max 0.20), and a pull toward the gray of equal luminance by 0.26 (max 0.78, green and yellow near-white, blue navy; built, ruled, and retracted the same session), because the clamp's error runs along the confusion line. So idempotence is exact for every color whose simulation stays in range (202 to 245 of the 343-color cube per kind) and bounded for the rest (a second pass moves what the viewer sees by at most 0.044). It belongs in linear light (pinned against display-value application by theModelIsAppliedInLinearLightNotToDisplayValues); the filter needs no encode/decode; its rows are the positive half (0 to 2), the negative half (3 to 5), and the separator (6). ColorVision.matrix is gone (two transforms per kind); applied(toLinear:_:_:) is the raw model. The judgment is the whole OKLab distance, lightness included, and that is the load-bearing fact for the docs: red against green keeps only 23% of its gap across the hue plane under protanopia and 7.6% under deuteranopia (78% under tritanopia), though the whole distance keeps 90% under protanopia because one is far darker, so two colors that differ only in hue merge, and a lightness difference saves them (measured twin pair: 0.281 → 0.013 matched for lightness, 0.601 stepped apart). The 0.06 default tolerance is measured, not guessed (colorblindSafe's worst pair 0.078 under deuteranopia, a familiar six-color chart set 0.013 under protanopia, both pinned). prefersReducedMotion applies nothing by itself (only a sketch knows which movement carries the piece) and reads false headless, so an export is machine-independent. Example Color/ColorVision (a photograph seen four ways over the two palettes: a picture is checked by filtering a layer, a palette color by color, and under preview every column is drawn in its true colors so the one filter over the finished canvas is the only thing simulating, which is what the mode claims); snapshot color-vision (both paths in one frame, so a drift between the Color call and the filter shows); ColorVisionTests (11) + ColorblindPaletteTests (5) + ColorVisionRenderProbes (5, Metal-gated: the filter pinned against the Color call for every kind at two severities over three patches that leave the range and use both halves of every kind's surface, and a brighter-than-white patch read in linear light); the web page's Simulated probe in WebEffectsTests. Docs/Helpers/Accessibility.md; Guide Ch 2 § Will everybody see it? + the ColorVision figure, Ch 3 § When somebody would rather it stopped: reduced motion. The projection's own invariants (idempotent, the kept cones unchanged, the worked values, the seam between the halves, the monotone dial) are under Physical invariants. Ahead in ROADMAP: complex-script text.
  • Linear frames for compositing (Sources/Ollin/Export/EXRExport.swift, Sources/Ollin/Renderer/LinearFrameCapture.swift). --export-exr <path> writes one frame as an OpenEXR file, and --export-sequence <dir> --exr writes a numbered folder of them off the same fixed-timestep drive; OllinApp.exportEXR(_:to:frame:fps:quality:) and exportSequence(..., writesEXR:) are the calls. The file is the frame before the present pass: linear light, Rec. 709 primaries, half components free to run above 1, premultiplied by coverage in A, plus Z (32-bit float, distance from the eye in the sketch's own units) whenever the frame drew through a 3D camera. Load-bearing: the writer is hand-written, not ImageIO, for two reasons measured here, namely that CGImageDestination passes components through a profile conversion (8.0 came back as 8.0001, with ~1e-4 leaking into channels that held nothing) and that a CGImage has nowhere to put a fifth channel; the format written is the one every reader opens, single-part scanline, NO_COMPRESSION, one block per row, channels in the alphabetical order the format requires (A, B, G, R, Z) with the offset table backfilled after the rows. The capture is opt-in (MetalRenderer.capturesLinearFrame, read in renderedFrame and accumulatedFrame), so every other export is byte-identical; it blits presented (the texture the present pass is about to read) and the resolved scene depth inside the frame's own command buffer, and the depth resolve is forced on for such a frame. Depth is inverted on the CPU over the camera's near/far with the same curve ollin_fx_depth_normalize walks (near*far / (far - d*(far-near)), orthographic linear), in Double: single precision made a far plane of 1000 read 1000.244, since the subtraction cancels. A supersampled export reduces depth to canvas size by the nearest sample under each pixel (the front surface, the multisample resolve's own rule). A flat frame writes no Z (its depth buffer holds a sort key, not a distance); --made-frames is refused with --exr (a made frame is built after the present); the recipe is stored in the standard comments attribute; a cleared pixel reads exactly the camera's far distance; no normal or per-object-matte pass exists, the renderer keeping no geometry buffer to write one from. EXRExportTests (10 tests, with a parser of the test's own: the header and channel list, the system's OpenEXR reader opening the file, the recipe in the comments, linear light against the PNG's encoded byte, a 2x-white overlap the PNG cannot separate, coverage premultiplied on a see-through canvas, no Z on a flat frame, Z as distance with the far plane in the corners, a supersampled export still canvas-sized, and a two-frame sequence). Verified against the reference exrheader / oiiotool. Examples/Export/LinearFrame; Docs/Output/Export.md § Linear frames: EXR; Guide Ch 41 § The frame before the tone map: OpenEXR.
  • Describable output (Sources/Ollin/Core/SketchDescription.swift + the accessibility overrides on OllinMTKView in Core/SketchView.swift). The viewer-facing half: a sketch says what it shows, so a generative piece is not opaque to a screen reader. describe(_:) is the whole piece in a sentence, describe(_:as:in:) one named part with an optional canvas Rectangle, noDescription() clears both; they fill the public Sketch.accessibleDescription (SketchDescription = summary + DescribedElements carrying name/text/region, spoken = "name: text", lines = the summary then the parts). Author-provided only — nothing is derived from the drawn shapes (a list of what was drawn is not a description of what it means; the vector recorder is the seam if that ever changes, noted in DESIGN-NOTES). Load-bearing: parts are keyed by name in first-named slot order and a removed part keeps its slot, so a part that comes and goes returns to its place rather than the end of the list — the reading order must never move under somebody walking it by hand (the defect that only showed when the example was rendered: a sun below the horizon on frame 0 put "the sun" last all day); == compares what reads (summary + present elements), never the slots; the AX surface is pull-based off the sketch (isAccessibilityElement/accessibilityRole/accessibilityLabel/accessibilityChildren overrides), role .image with no parts and .group with them, the child elements kept and updated in place (a screen reader follows an element by identity, so rebuilding them each query drops its place), and .layoutChanged posts only when the set of parts changes (SketchDescription.shape, names + whether there is a summary), never on a reword — else a per-frame description interrupts sixty times a second; a region maps canvas→view through viewRect (scale + the y-flip, AppKit counting up from the bottom), no region = the whole canvas; a sketch that says nothing changes nothing (the view keeps its default AX, and the SVG is byte-identical — role/aria-labelledby/<title>/<desc> are emitted only for a non-empty description). Travels with the work: SVG <title> + <desc> (the standard place, first children, XML-escaped) and the PDF document title; PNG/GIF/video carry nothing (no standard slot — stated, not faked), and VectorRecording carries the description so SVG and PDF cannot drift. OllinMTKView and makeOllinMTKView are internal (were file-private) so the tests drive a real view — the wiring is the feature. Example Basic/Describing (a day over a bay, words written from the same numbers as the picture, the sun dropped from the description when it sets); no pixel snapshot (the output is words); DescriptionTests (18, five sabotages verified red). Docs/Helpers/Accessibility.md; Guide Ch 41 § Saying what it shows: describable output + the SayingWhatItShows figure.
  • Math & motion. map / dist / lerp, the bare shaping scalars clamp / fract / step / smoothstep (spelled + argument-ordered to match the shader library so the vocabulary carries into per-pixel code; Easing.smoothstep is the same curve as an Easing, renamed from smoothStep), their siblings wrap(_:_:_:) (floor-based circular clamp — continuous through negatives, wrap(a, 0, .tau) for angles), the generic clamp over any Comparable (+ clamp(_:to:) range form, so Int indices clamp uncast), unipolar / bipolar (the −1…1 ↔ 0…1 pair; unipolar(signedNoise(x)) is just noise(x)), the sequence builders fractions(_:inclusive:) / angles(_:from:turns:) (endpoint-exclusive by default so a ring never doubles its seam; inclusive: for fence posts), polar(_:_:around:) (the center + Vector2(cos, sin) * r pair as one call; examples Motion/Petals (all three) + Motion/Spokes (fractions + polar)), the numerical slopes derivative(at:step:function:) and gradient(at:step:field:) over Vector2 and Vector3 (Math/Derivatives.swift: central differences divided by the run the coordinates actually hold, the step cbrt(ε)·max(|x|, 1) when nil or not positive; sin, exp, a cubic at 1000 and hand-worked partials to 1e-8, a quadratic exact at a given step, a forward-difference plant red; Docs/Helpers/Math.md § Slopes, Guide Appendix B § A number field's slope: the gradient with the Slope figure), spherical(_:_:_:around:) (its 3D twin, angles first: azimuth about +y from +z, elevation above the xz plane, the same convention as Camera3D.orbiting, whose eye it reproduces) with Vector3.azimuth / .elevation reading the angles back (a 500-sample round trip pinned), the angle units Double.degrees(_:) / .turns(_:) / .focalLength(_:sensorHeight:) (a lens's vertical angle on the 36×24 mm frame lenses are named for, 2·atan(h/2f); fieldOfView: .focalLength(35)) (call-site sugar into the radian API), the loop-phase helpers loopProgress(over:phase:) / pingPong(over:phase:) (0…1 sawtooth / out-and-back fold of the sketch clock; phase is a fraction-of-loop head start), the timers every(_:phase:) / after(_:) / everyFrames(_:) (true on the one frame that crosses a beat, so a periodic event needs no counter; phase shifts a beat the same fraction it shifts a lap. Load-bearing: the previous frame's clock reading is held exactly (Sketch.previousTime, set in advance) rather than worked out as time - deltaTime — those differ by a rounding error, and a crossing a frame lands on exactly then falls inside two windows and counts twice (3 of 6 frame rates read 6 beats where 5 were due). Frame 1 has no frame before it, so it borrows time - deltaTime there, which is what makes the start of the clock a beat at phase: 0 and not one under a phase. A beat is a change of period index (!=, not >), so a clock sent backwards — a replay starting over — beats in the period it lands in, and a stopped clock does not beat. One Bool says "now" once: a frame spanning two crossings reports one. everyFrames counts the first frame as the first beat (1, n+1, 2n+1). sway(over:in:shape:phase:) is the value-source sibling (Math/Sway.swift): low end to high end and back once a lap, over five SwayShapes (.sine default / .triangle / .saw / .square / .wander). Load-bearing: every shape closes its lap exactly, which is what lets a swaying sketch keep a loopDuration — .wander gets there by touring a closed circle through the field (noise(loop:)), never noise(time). The four worked-out shapes start at the low end and .wander starts where its field does; .triangle/.saw are pingPong/loopProgress mapped, which is the second construction their test asserts against. SwayTests (13 tests; the lap-closure one must sample off-boundary — Perlin is exactly zero at every whole number, so checking only at lap 0/1/2 passes a wander that does not loop, which a sabotage proved); example Motion/Sway. wave(_:amplitude:around:phase:) is the raw-sine sibling (center + sin(time * rate + phase) * amplitude, rate in rad/s so a hand-written wave carries over verbatim; closes a loop only when loopDuration * rate lands on whole turns — sway is the loop-exact one). The convenience tier is pinned by ConvenienceHelperTests (22 tests; the sugar forms are checked by byte-identical A/B renders against the blocks they replace, sabotage-verified red). TimerTests (15 tests, the deciding one built from a second construction: a beat is exactly the frame loopProgress wraps on); example Motion/Beats), a resolution-relative scale, seedable random / noise (randomGaussian / randomVector / ring / randomChoice uniform+weighted / shuffled / curlNoise (in the plane and in space: curlNoise(x, y, z) is the curl of a potential made of three offset copies of the field; both forms read the field's closed-form gradient, PerlinNoise.valueAndGradient, so a curl is three lattice walks rather than the six or eighteen a difference stencil costs, and the gradient is of the calibrated 0...1 field exactly, zero where it clips; unnormalized on purpose, since a normalized curl is no longer divergence-free, take .normalized for a direction; CurlNoiseTests pins both forms against differences taken through the public field and the zero divergence, away from the clip; the shader library's float3 curlNoise(float3) mirrors it over the 3D value noise by differences, ComputeTests.shaderCurl3DIsDivergenceFree) / signedNoise, a master seed(); Sketch.randomness exposes the SplitMix64 itself as a settable property, which is what puts the standard library's randomness (randomElement(using:), shuffled(using:), Int.random(in:using:)) and Ollin's own using: R generators (WaveFunctionCollapse.solve, LSystem.expanded, ShapeGrammar.run, WangTiles.fill) on the sketch's seed rather than on the system's dice, the one silent way a seeded sketch stops reproducing. It forwards to the same rng random() draws from, so it is one stream, not a second; &randomness needs the setter, so a direct assignment cannot also keep recordedRandomSeed honest (the setter runs on every use), which is why randomSeed(_:) stays the way to seed and the doc says so. RandomTests (4 new tests); gotcha: zero-width random(a, a) consumes no roll, so parameter-scaled jitter rolls random(-1, 1) * amount), NoiseFields (Math/NoiseFields.swift: the three seeded fields, Perlin, simplex and Worley, as one public Sendable value carrying every noise method the sketch has, so the sketch's own noise / fbm / curlNoise / simplexNoise / worley / the loop and tiling forms are one-line forwarders onto Sketch.noiseFields, the typed core under the bare calls; noiseSeed reseeds the stored value, carryRun copies it, noiseField() closes over it for a Formula. Load-bearing: it is a value, so a copy taken before a reseed keeps reading the old field, which is the property that lets DispatchQueue.concurrentPerform or a task group read it off the main actor with no lock and no isolation hop, the reason it exists: the Rendering/LineSpray example's Drift bends 38k lines a frame across every core through it. NoiseFieldsTests pins the copy against the sketch's own readings for every field, the value semantics across a reseed, and an off-main-thread read landing the same numbers), looping noise (noise/signedNoise loop:+radius: overloads — one lap tours a closed circle through the field and closes exactly, loop is fract-wrapped; the 2-coordinate form uses a 4D lattice, canonical 32-gradient grad4, its own contrast gain gain4 calibrated to the 3D feel; the 3-coordinate form (noise(_:_:_:loop:radius:), with signedNoise/fbm/signedFbm/ridgedFbm/turbulence beside it, on NoiseFields and the sketch) uses a 5D lattice: 32 corners hashed one axis at a time (corners sharing their low coordinates share the lookups) and blended one axis at a time, the 80 gradients with one zero component and four signed (the zero axis by (h * 5) >> 8, 51 or 52 table values each), and gain5 = 1.8 calibrated on 400,000 samples to the 3D field's spread (sd 0.520 against 0.522, 6.4% clipped against 6.0%), so moving a sketch from noise(x, y, z) to its looping form keeps its contrast (LoopingNoise3DTests: every form closes, the lattice equals a corner-by-corner sum to 1e-12, zero at lattice points, the spread pinned; red under a hashing and a gain plant); circle centered at (0.5, 0.5) off the integer lattice on purpose), fbm/signedFbm (1D/2D/3D + 2D-loop forms, octaves/gain/lacunarity, normalized so octaves: 1 == noise, name matches the shader lib), tiling noise (tilingNoise/signedTilingNoise/tilingFbm/signedTilingFbm: both u and v are mapped onto closed circles, so the field meets itself at every edge and a picture repeats with no seam. detail is deliberately a frequency, not the circle radius (tilePoint divides by .tau), so fbm(u * 8, v * 8) becomes tilingFbm(u, v, detail: 8) and keeps its grain; a radius-shaped parameter made stone read as sandpaper. Load-bearing for triplanarTextured, which repeats its picture across the whole surface: a non-tiling map draws a straight line at every wrap, loudest in a normal map, where the two sides light differently and the line reads as a crease. That was a real defect in the Ch 28 Bench figure, isolated by counterfactual to the normal map alone (25.6/255 step, falling to 3.0 with the map off)), the noise-variant family (Math/NoiseVariants.swift + additions in Noise.swift; every name mirrored in the shader lib's noise module, all reseeded by the one noiseSeed): simplexNoise/signedSimplexNoise (1D/2D/3D, Gustavson's structure over the shared seeded permutation, contrast gains 81.3/87.7 calibrated to the Perlin field's ~2%-clip feel), worley(x,y[,z], feature: .nearest/.second/.border, jitter:) (seeded SplitMix-hashed cells; .border is zero on cell walls), ridgedFbm (Musgrave's crease fold with octave-weight feedback, reparameterized onto fbm's gain/lacunarity) / turbulence / warpedFbm (iq's two-level self-displacement; warp: 0 == fbm exactly), each with the 2D loop: form, deterministic + pinned by NoiseVariantTests, Generator legs on the Layered effects bullet, Docs/Generators/Noise.md, examples Randomness/NoiseKinds (simplex, its signed reading, worley and turbulence on one set of coordinates) + Patterns/RidgeLines + Shaders/DomainWarp + Effects/Cellular, snapshots ridge-lines + noise-toolkit; Double.tau; easing (the Easing curve catalog + @Eased, the derive helpers reversed() (1 - f(1 - t); a built-in lands on its family partner by identity, since every catalog ease-out is exactly the reflected ease-in, pinned over all 32) and mirrored() (the curve on the first half, its reverse on the second; the catalog ease-in-out for the eight families built that way, a curve of its own for back and elastic, whose in-outs carry their own constants, and an out-in the catalog lacks for an ease-out); a derived curve's identity is the operation over its origin, so two derivations of one closure curve compare equal and stay off the menu like the closure itself; EasingDerivedTests; example Motion/DerivedCurves; Guide Ch 3 figure DerivedCurves, plus the Timeline<Value: Tweenable> keyframe sequencer — worked example Motion/Timeline), input smoothing (OneEuroFilter + @Smoothed), and the damped spring (Math/DampedSpring.swift: DampedSpring<Value: Smoothable> + @Sprung, the third FrameAdvancing sibling — each step evaluates the exact closed-form damped-oscillator solution across the under/critical/over-damped branches, so any dt is stable and steps compose exactly (frame-rate-independent, pinned by DampedSpringTests); perceptual duration/bounce parameters (bounce 0 = critical, clamped (−1, 1]), velocity carries across retargets, kick(_:) throws; example Motion/Springs), all frame-rate-independent and auto-advanced per frame. Docs/Helpers/Math.md, Docs/Generators/Random.md, Docs/Generators/Noise.md, Docs/Helpers/Animation.md. Complex (Math/Complex.swift, promoted from the limit-set generators' internal scalar, which now reads it): real/imaginary, the polar init(magnitude:argument:), .i/.unit(_:), magnitude/magnitudeSquared/argument/conjugate/reciprocal/rotated(by:)/squareRoot(), the Vector2 bridge both ways (coordinates copied, the y flip left to the framing), Numeric + SignedNumeric + both literals + Codable + a description that reads as written, and Complex.exp/.log/.pow (a Double, a Complex, and an exact Int power by squaring)/.sqrt/.sin/.cos/.tan/.sinh/.cosh/.tanh as static members on the principal branch. Two type-checker facts are load-bearing and were both measured: the transcendentals must never be free-function overloads (a free pow(_: Complex, _: Double)/sin(_: Complex) joins the candidates for every pow(2, x)/sin(x) in the program, and with a literal-taking type in play a plain easing curve took 3 s to type-check and Easing.swift stopped compiling outright; as members they never enter that search, and the spelling matches the Swift numerics package, whose elementary-functions protocol is NOT in the standard library, so there is nothing to conform to), and the mixed Complex/Double operators are @_disfavoredOverload (without the mark lengths.reduce(0, +) over doubles is ambiguous, since 0 can be the type's literal and + (Complex, Double) fits, and three framework files stopped compiling; disfavored, they measured as cheap as no overloads at all). ComplexTests pins both with a literal-heavy Double expression that has to compile. ComplexTests (11 tests); the GPU twin is the shader library's complex module (under User-supplied shaders); examples Shaders/ComplexPlane (the ratio shader with the circles of Apollonius and the circles through both points worked out on the CPU and drawn over it, landing on the rulings), Shaders/ImaginaryLog (the imaginary part of clog of that ratio through a cosine palette that never completes a cycle, so the branch cut between the points is a soft seam; turns closes it at a whole number), and Shaders/Meromorphic (a ratio of two cubics built with cmul from six roots moved as Complex values, the palette's frequency a parameter), snapshot complex-plane, Docs/Helpers/Complex.md, Guide Ch 22 § Multiplying turns: the complex plane.
  • Canvas, capture & tooling. canvasSize (the CanvasSize enum, 1080² default) previewed per windowMode (.auto / .fixed / .resizable); PaperSize (Core/PaperSize.swift: the ISO A series and the US three in millimeters, .landscape / .portrait, init?(named:), and canvasSize(dpi:) landing on the same CanvasSize page presets, test-pinned); FrameRate (Core/FrameRate.swift: every public fps: takes one, a literal stands in (fps: 60), the broadcast rates are exact fractions (.ntsc = 30000/1001, .ntscFilm, .ntscDouble, beside .film / .pal), --fps takes a name, a number, or a fraction, and the two video writers place frame k at k·tick/timescale through mediaClock, which keeps a whole-number rate's 1000× clock byte-identical; internals stay Double through framesPerSecond at the boundary, and Sketch.frameRate, the measured live rate, stays a Double); a maintained Examples/ set incl. a Recreations/ section; headless capture (OllinApp.image(of:), --export) + deterministic frame-sequence export (--export-sequence + --skip warmup; a warmup frame renders whenever the frame holds a persistent picture (noClear, or Drawer.usesFeedback: a feedback, an accumulator or line spray, a sim field, SSR history), in renderFrames and the spatial-video loop alike, or the first written frame starts from an empty layer while the sketch's own state is --skip seconds on; a picture that is its clock alone skips the render; PersistentLayerExportTests); every export holds a sketch that stopped its loop, as the window does (Sketch.exportHoldsFrame = !isLooping && takePlayer == nil, read after a draw: renderImage captures the frame the loop stopped on and runs its settle passes there, renderFrames breaks and writes the last readback for every frame left (rendered once if the hold came in a warmup frame that never rendered), the vector and spatial recorders take a fresh recorder each frame and keep the last, the spatial video writes the held pair, and the web recording ends, so a still's page is one frame and holds still; a replayed take still draws every frame, since it was written only on frames a window drew; --bench untouched; ruled by @eaviles 2026-10-02, and it moved one media still, Shapes/Watercolor's grain, while its export fell from half an hour to one draw; NoLoopExportTests); --at S names a still's moment in seconds (stillFlags: the frame is S × fps rounded, nan reads as 0, and it wins over --frame when both are given; the sheets, the separations, and the spatial file read the same reader), so --at 2 is --frame 120 at 60 fps pixel for pixel (StillRateTests); every one-frame flag reads --fps beside --frame (--export, --export-exr, --export-plates, the vector family, through one stillFlags reader), so a still lands on the sequence's moment at any rate, where until 2026-09-22 only the sequence and grid paths read it and --frame 30 --fps 10 was drawn at half a second (StillRateTests); every headless drive drains its own autorelease pool once per drawn frame (renderImage per settle pass, renderFrames per frame, the stereo, vector and web drives, a contact-sheet tile, a figure render, a replay scrub): a device buffer arrives autoreleased and a synchronous drive never returns to a run loop, so a warmup frame and a written one alike used to be held until the process exited (issue #23 — 3,600 frames at 1080² reached a 15 GB physical footprint on an 8 GB machine and spent the run swapping. A/B on one M2 over 300 frames of a sketch that re-feeds a LineSpray every frame: without the pool, one 4560K readback region per drawn frame (5 at frame 0, 235 by frame 226) and 106 MB climbing to 1.3 GB; with it, one region throughout and a flat 108-127 MB). A continue cannot leave a closure, so the warmup paths return out of the pool instead; HeadlessDrainTests (5) counts the witnesses each drive leaves standing in the frame's pool, all five verified red by sabotage. export supersampling (--render-scale N / OllinApp.exportRenderScale, 1…4 — the frame is drawn N times across the canvas and box-averaged back to canvas size in linear light, ahead of the tone map, so the one 8-bit quantization point stays in the present pass; the viewport stays in canvas units, so it is a sampling rate and the sketch draws in the same canvas either way. --settle N / OllinApp.exportSettle draws each written frame N times with the clock held (advance with the same time, deltaTime 0, frameCount counting on, which is what keeps the renderer's repeat-encode guard from skipping the persistent layers) and writes the last, in both headless drivers (renderImage settles only the captured frame, renderFrames every written one, warmup once); the recipe records settle; refused beside --replay, --record-take, and --made-frames. The split is load-bearing: geometry and the screen-space pre-passes run at N×, everything that treats the frame as a picture (motion blur, flare, postProcess filters, the tone map) runs at canvas size, or a pixel-radius filter would silently shrink to half its asked-for width at 2×; what it sharpens is the tessellated fill, outline text, and fine detail, never the analytic SDF shapes or the fringe strokes, which carry their own coverage; a renderTarget layer keeps its recorded size and a piling canvas (noClear) renders at 1, both said out loud rather than silently; an over-large ask clamps to the ceiling with a printed line. RenderScaleTests (4, both the convergence-toward-4× probe and the blur-keeps-its-width counterfactual verified red by sabotage); Docs/Output/Export.md); video & GIF export (OllinApp.exportVideo / exportGIF, --export-video / --export-gif — AVAssetWriter h264/hevc/prores + GIFWriter over one fixed-timestep drive; codec / --bitrate / --quality control, the last gated #if arch(arm64)); a GIF is written as it is drawn (GIFWriter, Sources/Ollin/Export/GIFWriter.swift, public: init(path:width:height:palette:loops:) / append(_:delay:) / finish(); exportGIF(palette:), --gif-palette shared|per-frame on --export-gif and --export-loop): each frame is drawn into the writer's own sRGB context at the writer's size (which is how --gif-width scales, so the export has no scaler of its own), keyed, diffed against the frame before, quantized and LZW-coded into the open file the moment it arrives, so the buffers are sized once and no frame outlives the call that took it (GIFMemoryTests: heldBytes pinned equal after 1 and 200 frames at 480², and every frame handed over is built on memory whose release callback the test counts, so the run never falls behind by even one). That second test used to be read off the process footprint, and it cannot be: the suite shares its process with every other suite in its shard, so the number is the sum of what all of them do in the same seconds. It went 144 MB past an 87 MB bound on a writer holding nothing (CI, 0.9.0), and it would equally have passed while leaking had a neighbor freed as much in the same window. Both halves are verified red by sabotage (a retained frame, a buffer that grows per frame). VideoExportTests reads the whole export the same way, off memory the test owns: the headless drives take their frame memory from OllinApp.frameMemory when a test installs one (ExportFrameMemory, package, Sources/Ollin/Export/FrameMemory.swift: the readback buffer wraps that memory without a copy, page-aligned as Metal asks, and the frame image is built over a copy of its own, each handed back through a release callback; a normal run installs nothing and the image copy path is the same one either way), so the invariant is that no piece outlives its frame by more than the beat Metal takes to let a readback go (measured in hand-outs, two per frame: a beat's lag is two, a held frame the whole run; a byte bound of one readback plus one image read that beat as a hold on the runner, 2026-09-22) and every piece comes home, over a 120-frame GIF and a 60-frame video at 480², with a 200 MB hog allocating and touching its pages beside the export in the same process to show the number is not the process's; the count of pieces handed out is checked too, so a drive that stopped asking the ledger could not pass by never being counted; verified red by a planted retained readback. The table: .shared (default) is a median cut over 6-bit bins of the first frame, written as the global table sized to its colors (a 60-color frame gets 64 entries and 6-bit codes, which is where the system writer's smaller files came from; the transparent index is the last entry). A box whose bins sit within 3 levels RMS of its mean is not split and entries within 6 levels of each other are merged after the cut (a median split parts a dominant bin from its dithered neighbors first, and the present pass dithers, so without the merge a flat sky flips between two entries a level apart: the garden figure's first frame went 9157 to 4952 B). Colors that arrive later join the table in place: a frame has left its table when its mapped pixels' squared error spread over the whole frame exceeds max(8, 2× the training RMS) (a fade-in) OR at least 8 moved pixels average over max(12, 3× training) (a pale tree of 18 pixels a frame growing over dark soil, which is almost no energy over the frame; the strip a moving shape uncovers reads 10-15 over its pixels against a training of 6.5 and stays under both bars); then the frame's colors farther than 8 levels from every entry are written into the global table's free entries on disk (seek, never touching an entry a written frame points at), a table with no room left is grown to the next size that holds them by one streamed copy of the file so far behind a wider head (a written frame keeps its narrower codes and its own transparent index, both valid under the wider table; moreNewColorsThanFitGrowTheTable), and only past 255 colors in all does the frame, and every frame after it, carry a local table. .perFrame chooses per frame. Diffing: unchanged source pixels take the transparent index under disposal 1 and the frame is cropped to the changed box, a no-change frame is one transparent pixel keeping its delay; a picture whose first frame carries alpha with a clear pixel is written whole with disposal 2 every frame (a pixel can clear again), the cut at half coverage with the color un-premultiplied, decided once from the first frame. loops counts plays as ImageIO reports them (0 forever, 1 no loop block, n stored as n-1). The LZW coder (codes from the table's width plus one to 12 bits, a clear when the dictionary fills, an 8192-slot open-addressing dictionary) is byte-identical to ImageIO's on the same index image (the system's own decoded first frame re-coded lands on its byte count). Measured against the system writer on the same frames: 12% smaller on a drawn-like piece (5500 against 6226 B), within 5% on a full-gamut plane where every pixel is its own color (the first frame's tiling, at a lower error, 2.56 against 2.63 levels), every sub-frame on the same rectangle within a few bytes, every frame decoding within 1.25× the system's error; on the Guide's garden (83 frames at 420², a cream tree and lichen arriving over dark soil) 63.7 against 67.0 KB with no local table and no pixel past 32 levels from the system's file. The crops run a pixel wider than the system's, which diffs after quantizing where this diffs the source. GIFWriterTests (15) + GIFMemoryTests (2) + the export peak test; Docs/Output/Export.md § Animated GIF; Guide Ch 41 § Motion: video and GIF); perfect-loop export (--export-loop + the declared Sketch.loopDuration — renders exactly one period as a seamless GIF or video by extension; the GIF lap is computed against the centisecond-quantized rate exportGIF will actually play, or the frame count drifts off one period; example Motion/PerfectLoop, seam-verified byte-identical one period apart); vector SVG export (--export-svg — a CPU-only serializer, curved SDF shapes traced via marching squares, a cell whose two crossings disagree in normal bent through the corner where their tangent lines meet, so a sharp corner is not chamfered by the cell's diagonal, SDFOutlineTests; the opt-in --hatch / Hatching fill-to-line-work transform for pen plotters); vector PDF export (--export-pdf / OllinApp.pdf / exportPDF — replays the same recorded commands (OllinApp.recordVectorFrame, the shared front half) through a Core Graphics PDF context, so the SVG and PDF of a frame can't drift; hatching and --frame apply, both vector flags may be passed in one invocation; 1 canvas px = 1 PDF pt, so the paper CanvasSize presets (.usLetter / .usLegal / .a3 / .a4 / .a5, points-sized, portrait like the sheet) export true-to-size pages, and .dpi(300) scales the pixel grid for print-res raster while the .page case carries the point size so the PDF page stays the physical sheet; PDFExportTests pins structure + rasterized-pixel probes); print-separation export (--export-separations / OllinApp.exportSeparations — one grayscale master per declared printInks ink + a registration-marked overprint preview; the machinery and invariants live in the Geometry & color bullet); reproducibility metadata — every PNG / SVG / PDF / video export embeds a one-line JSON recipe (ExportMetadata: the recorded seed(s), @Param values, working-dir git hash + -dirty, frame/fps) — PNG Description text chunk, SVG comment, PDF Subject, video description item; GIF has no slot; the video writer defers startWriting to the first rendered frame so the recipe can carry the seed setup() applies (writer metadata is immutable once writing starts); seeds are recorded at init from variation and re-recorded by randomSeed/noiseSeed (Sketch.recordedRandomSeed/recordedNoiseSeed; ExportMetadataTests); the recipe names both commits ("git" the sketch package's, "ollin" the framework's, each -dirty when its tree is, each absent where its repository cannot be read): the framework's is read in the directory this source was compiled from (#filePath), the checkout a path dependency points at or the package checkout SwiftPM made, since a sketch that depends on the framework by path changes its frame with no change of its own (ExportMetadataTests pins the pair equal from inside this repository); parameter values on the command line (--param name=value, repeatable, Core/ParamOverride.swift) — the way back in the recipe opens, on every export path and on the windowed one. The text is read against the parameter's current stored kind (number / bool / menu choice / color / vector / rect / insets / range / text / swatch strip), a menu choice matched loosely against the control's own labels (easeOut = ease-out = "Ease Out", which is what lets one rule serve both ParamOption and ParamChoices without the erased type), a swatch list spread evenly unless a stop names its place (#000@0), and a Ramp keeps the space it mixes in (only the colors move). Applied before the canvas is sized (Sketch.canvasSizeForRun(), which every headless drive, OllinApp.run, and SketchRunner.init read), before setup(), and again after it, before the first frame through AnyParam.restore, so a canvasSize that reads a parameter takes the run's size (a sketch rendering once per aspect sized itself from an environment variable until 2026-09-23) and what setup() builds from a parameter reads the run's value (an extension registered behind a switch never saw the flag until 2026-09-22), it clamps like the row, wins over a value the sketch's own setup() set, and reaches the recipe (captured once the sketch has run). The seam is Sketch.runSetup(), which every headless drive calls instead of setup(); the windowed path applies it once at launch (SketchRunner.applyLaunchParams, after a restored checkpoint) so a reload keeps what the inspector has since turned. Nothing is skipped in silence: an unknown name or an unreadable value stops the run and lists what the sketch has. The sweep's parameter flag moved to --sweep-param (--param sets a value on every tile alike, so naming one parameter both ways stops the run rather than flattening the sheet). ParamOverrideTests (17; the after-setup() order, the before-setup() pass on the drive and on the window path, and the drive seam); Docs/Output/Export.md#setting-a-parameter-for-the-run, Guide Ch 41 § Passing the parameters back in. --capture-source (Export/CaptureSource.swift) turns that -dirty dead end into something checkoutable — the working tree is written into the repo as a commit on no branch, every written path is renamed after it (out.png → out-93ae989.png, a sequence directory too), and the recipe gains a capture field. Load-bearing: nothing the user owns moves — the tree is staged into a throwaway GIT_INDEX_FILE (a copy of the live index, so the stat cache holds and only changed files are re-read; rebuilding from read-tree HEAD re-hashes everything), so index / worktree / HEAD / branches are byte-identical after (theCaptureLeavesTheRepositoryAlone, verified red by dropping the temp index); git add --all is what pulls in untracked files while honoring .gitignore (git stash create cannot, which is why it isn't used); the commit is anchored at refs/ollin/captures/<tree sha> or git gc collects it (verified red by dropping the update-ref), and keying the ref on the tree is also the dedupe — the same source re-exports to the same capture id; identity is forced via GIT_AUTHOR_* env so it never needs a configured user and never signs a person's name; resolved once per process at flag-parse time (before the export branches read their paths), so a 600-frame sequence hashes the tree once; clean tree → the id is HEAD and no commit is written. CaptureSourceTests (14, pure git + Foundation, no GPU); Docs/Output/Export.md#captures-that-know-their-source; Guide Ch 41 § The recipe in the file. The whole export/bench flag surface lives in OllinApp.handleCommandLine(_:makeSketch:) (Sketch.main() routes through it, and OllinLive runs it on a loose watched sketch file — swift run OllinLive Sketch.swift --export-gif … — compiling the file only when a flag matches, so any Guide-style sketch exports without needing a target); the extend(...) SketchExtension seam (setup / beforeDraw / afterDraw / afterFrame / frameRendered, opt-in wantsRenderedFrame / wantsRenderedTexture; afterFrame fires on the headless drives too, once per rendered frame (a settle draw included, a warmup frame only when it renders) with the export clock's rate and step and the frame's own unsmoothed times, the renderer timing its headless encode, GPU run and wait through commitHeadless and the drive handing them over through OllinApp.reportHeadlessFrame, since FrameInfo.profile was unreachable from an export until 2026-09-22 (HeadlessFrameReportTests); the pixel hooks read the on-screen frame, never a second render: the runner asks every extension once before the frame is drawn and hands the renderer a FrameGrabRequest, the renderer tone-maps the finished picture once more into a canvas-sized display texture inside the frame's own command buffer (a larger picture minified through a mip chain, the same size byte-identical to the headless render), blits it to a shared buffer when an image is wanted, and delivers from the completed handler on the main run loop in every common mode (a main-queue block cannot run while another spins the loop, a run-loop block can), in frame order; textures and buffers come from a pool a delivery returns to, so nothing is written while a consumer reads it; a frame being handed out is drawn no smaller than the canvas, an accumulating pile keeps its size and is brought to the canvas size on the way out, and the ground grid leaves the window while a hook is armed; a frame nobody asks for pays nothing. Measured: the lit scene explorer records at its unarmed rate (45.7 against 46 fps at 1080²), where the re-render took it to 19; FrameGrabTests pins the pass count, the byte-identity, the size floor, the minification, the pile, the order, and the grid gate, the floor and the gate verified red by sabotage); live reload (swift run OllinLive, the typed @Param inspector controls (a Palette is a strip of blocks and a Ramp a band with a draggable handle per stop, both one .swatches control kind persisted as .colors(stops:space:); the band is sampled from the value so it is the blend the sketch draws; a stop stops at its neighbors; Easing sits on the ParamChoices menu tier because a built-in curve carries its name as its identity; a number box's text belongs to the typist until Return, Tab, or a click away commits it, clamped (ParamNumberEdit in Live/ParamNumberField.swift: nothing the row clamps or snaps is written back while typing, which is what turned a first "2" over 10...375 into "10.00" and "300" over 50...500 into 500; a partial previews on the canvas only inside the range, and in a min/max pair only while it keeps the order, an end committed past the other pushing it along; Escape puts back the value from before the first key); a box reads the value's own digits with the trailing zeros taken off, no grouping mark, six significant figures for a value from outside (ParamNumberText.display), and a commit never re-reads that text; every drag (slider, scrub, pad, two-thumb track) lands on the power of ten under one point of travel (ParamNumberText.dragged, an end kept exact), so what it leaves, and what Save writes, has the digits a person would type; ParamNumberBoxTests types through the field editor into a real box, red on the old binding with 11 issues) (+ show-rules: $parameter.show(when: $other) { ... } set in setup() hides a row while its source keeps it inert, polled on the rows' 100ms sync cadence; a fully hidden group drops its card; hiding is display-only, so the value still persists/restores and OSC/MIDI keep driving it; AnyParam.isShown/ParamHandle.isShown, the section split testable via ParametersListView.visibleSections; folded groups: group: takes a ParamGroup (ExpressibleByStringLiteral, so the plain spelling holds) and group: .folded("…") renders that section as a disclosure card that starts closed — one folded member folds the whole group, the default (unnamed) group never folds, folding is display-only like hiding, and the open/closed state is remembered per sketch (ParamFoldMemory, keyed on the name each host's monitor card shows, passed as ParametersListView(sketchName:); an untouched group writes no key, so it keeps its declared start); gates: ParamTests + OllinLive --paramtest), the shared FrameStats); the sketch dylib compiles optimized (-O) in every host (SketchLoader.Optimization, .speed by default; --no-optimize on OllinLive / OllinLiveCoding / OllinRun compiles it plain, for debugging: asserts fire, a crash names every frame), because swiftc defaults to -Onone and a CPU-heavy sketch ran several times slower under the live window than in release (issue #17; measured under a release host on 50k points moved by six octaves of noise a frame: 35 ms optimized against 80 ms plain with the framework's noise, 52 ms against 1,170 ms with noise written in the sketch); OllinLive --selftest pins that both compiles render the same pixels; and as one whole-module job (-wmo): the driver otherwise runs one frontend per source (the sketch and the factory shim), each loading the framework's module, which was most of a small sketch's compile — measured on the loader's own line, a 20-line sketch went from 1.00 s to 0.57 s plain and 0.95 s to 0.61 s optimized, a 470-line one from 0.96 to 0.74 and 1.48 to 1.09 (SketchCompileArgumentsTests pins the flag); the live-coding performance host (swift run OllinLiveCoding — evaluate-on-command over the same engine; see the Live coding block under § Live reload below, Docs/Tools/LiveCoding.md); the examples gallery (swift run OllinExamples; Scripts/OllinExamples is the release-by-default wrapper, --debug opts out — the sketches run on the host's build configuration, and under a debug host a heavy example's setup() + first frame stall the whole app on the main thread (worst of a 40-example sweep: ~2.6s debug vs ~0.2s release; a few asset-decoding examples keep ~1–1.6s either way; the sketch dylib compiles optimized here too since issue #17, which does nothing for that stall (framework code) and costs a first-visit compile about half a second on a 479-line example, paid back within the first second of any example that does its own CPU work per frame)) — a two-sidebar host on the OllinLive window recipe (fixed HStack + .contentSize + unified gradient toolbar + title-bar accessory toggles, the chrome shared via the core WindowChrome.swift): left, the example list as one navigator-idiom outline (ExampleCatalog.tree flattened over the persisted disclosure set; folder rows carry a per-category SF Symbol from categoryIcons — unknown categories fall back to folder — plus a trailing count; bottom filter bar, ⌘F focuses it, Esc clears, Return runs the first match) — every row is selectable, folders included, so ↑/↓ sweep the whole tree, ←/→ close/open a folder and step out/in, ↩ toggles a folder or relaunches the running example (the fold arrows and ↩ are handled through onKeyPress, never onMoveCommand — the focused list's table view consumes arrow keys before any move command fires, so onMoveCommand left/right is dead here; .ignored lets ↑/↓ through to the list); the stage follows running, not the row selection (parking on a folder never blanks the canvas), the running example marked in accent purple with level bars while the selection capsule stays the system accent; the canvas takes keyboard focus only on click (SketchView(keyboardFocus: .onClick), a glass "click the sketch to use the keyboard" hint floated only for sketches whose source reads keys, Example.usesKeyboard); the stage is a fixed square and the loaded sketch aspect-fits into it (letterboxed on black, never scaled up — SwiftUI doesn't clip an oversized frame, so a non-square canvas used to paint over both sidebars and swallow their clicks, each click taking the keyboard with it); a fresh instance is covered by a "Starting…" card until its first frame renders (its setup() runs on the main thread inside the first canvas draw, so a heavy one used to leave the stage black for seconds with no word; a per-instance FirstFrameSignal extension clears the cover, so a stale frame from the outgoing sketch can't drop it early); the list holds keyboard focus explicitly (a @FocusState re-planted whenever a fresh example lands on the stage, on a folder row's button toggle, and on the filter's Return — an example switch tears the outgoing canvas down, and a canvas that held the keys can only clear the first responder, so arrow keys used to die after one switch); right, the hidable inspector (⌘/, the shared MonitorCardView + @Param parameters — so the gallery omits OllinHUDCommands/the detached panel). Two leak invariants are load-bearing (both broke for real): the compile cache holds dylib paths, never live Sketch instances — a switch instantiates fresh via SketchLoader.instantiate and the release of the outgoing instance is what stops its audio/capture/players — and the gallery's mutable state lives in the @Observable GalleryModel class, never spread view-struct @State, because escaping closures stored in lazily cached List rows capture whole view values and a view value carrying the stage inline pins outgoing sketches (sound bleeding between examples, resources piling toward a crash; found by --cycletest, traced with leaks --trace). Gated by the windowed harness swift run OllinExamples --cycletest [n] (spread over the catalog, or --cycle-filter a,b + --cycle-hold for pinning one down), which walks the real selection path and fails if any outgoing instance stays alive; --self-shot <png> writes a window self-capture (cacheDisplay, so no screen-recording TCC; the Metal canvas comes out blank by design). Examples/3D is grouped into topic subfolders (Camera/Depth/Effects/Environments/Geometry/Lighting/Materials/Phone/Raymarching), targets renamed to match (Example-3D-<Group>-<Name>); render-correctness snapshot tests (Tests/OllinTests — re-record all via OLLIN_RECORD_SNAPSHOTS=1, or scoped: set the variable to a case-name substring (or several, comma-separated) and only matching snapshots record while the rest still compare, so one new reference no longer rewrites the other 241; 0/empty means compare, the variable's value decides, never its mere presence; recording writes the source tree while comparison reads the built bundle, so the next swift test is what verifies a fresh recording; a failing case writes actual + reference + an 8× diff into $TMPDIR/ollin-snapshot-failures/); CI. The AVFoundation link lives in the core for offline export only. Docs/Output/Export.md, Docs/Core/Canvas.md, Docs/Helpers/Parameters.md. Every OllinApp.export… call throws ExportError (kind unsupported / unrendered / unwritable, the path, the written frame reached, the sentence) and never stops the process: the video, GIF, and spatial-video writers remove a clip stopped partway and reset the soundtrack state, zero frames is unsupported, exportHEIC and exportWidget throw too (the widget run on a run that drew no picture), the calls that hand back a picture throw it too (image(of:), the four contactSheet(of:) forms, plates(of:), separations(of:), each non-Optional, the path empty; one throwing headlessRenderer(for:path:) carries the renderer's own message, where it used to answer nil and print; ExportErrorTests.aRendererThatWillNotStartStopsEveryHeadlessCallTheSameWay through the package OllinApp.plantedRendererRefusal), and handleCommandLine is the one place a failure becomes a printed sentence and exit status 1. OllinApp.plantedVideoRefusal (package) makes the encoder refuse a named frame for a test (ExportErrorTests). A frame the GPU throws away never reaches a file: commitHeadless returns whether the buffer ran and leaves the sentence on lastHeadlessFailure (a headless failure is never printed as the only sign; the live path's GPUFailures.check still notes its own), renderedFrame draws a frame again up to headlessRedraws (2) times when canRedraw (no noClear pile, no persistent layer, no compute dispatch, no subframe moment, any of which the discarded buffer may have half-written), forgetting the TAA, traced-reflection, caustics, and path-trace histories and the same-frame stamp first so the redraw neither blends with nor serves the discarded encode; otherwise it counts unrecoveredFailures, which only grows, and every export drive throws .unrendered with the GPU's reason at the next frame it writes (throwIfFailed(since:), so a warmup or settle draw that failed stops it too). OllinApp.plantedDiscards (package) drops named headless commits unrun for a test; ExportErrorTests pins the redraw byte for byte and both throws, each red under its plant. copyFrame() puts the next frame the window shows on the clipboard as a PNG through the frame-grab hook (a one-shot FrameCopier extension armed per call, so the grab's own rules hold: canvas size, no second render, no host chrome); a still sketch draws one frame for it through redraw(), and with no window nothing is armed (SketchConvenienceTests, read off a named pasteboard, never the person's).
  • Physical invariants (Tests/OllinTests/LightingInvariantTests.swift, FrameInvariantTests.swift, ColorScienceInvariantTests.swift, Tests/OllinAudioTests/SoundInvariantTests.swift; the 2026-10-06 triage of the physically based tiers, 36 tests with the color-vision review's three, 19 more from the 2026-10-09 survey of the photoreal tier, and three from the 2026-10-16 review of the lit surface's energy split). Each pins a statement that holds whatever the implementation and reads it off a render, an image, or rendered samples against a twin or an analytic value, never against the code's own arithmetic, so a plausible-looking defect (the class a snapshot cannot see) turns one red: a lossless surface returns the furnace's constant field (a white diffuse sphere, a clear coat, sheen, a thin film over a perfect mirror at four thicknesses, a clear glass ball, each bounded both sides, and the diffuse disc flat out to its rim), the environment is linear in its intensity, a shadow never adds light (the map and the traced point caster), black walls bounce nothing and bounce never outshines the pool it came from, a caustic never takes light, a lens gives back what its shadow took less what its glass reflects (0.78 to 0.92 of it, under a sun, a lamp, and a lamp with a reach), and dispersion keeps its total, fog follows Beer-Lambert and stays between the surface and its own color, a beam never darkens, a tone curve is monotone, bounded, neutral, and the one its name promises, the dither is unbiased, the jittered average conserves the frame's light within 1% (one unjittered frame misses half a percent of a highlight it samples at pixel centers alone), a streak never outshines its source, keeps the mover's light within 1% at shutters 0.5 and 1, and stays within 2% of the box's value of the subframe export region by region, a proof never widens the gamut and keeps a gray neutral, a dichromat's simulation is a projection (simulating twice is simulating once wherever the result stays in range, the two cones the viewer has respond to a color and its simulation alike, each kind's halves meet on their separator and nothing crosses it, the transforms match the published worked values) and a partial severity never overshoots its end, a plucked string and a struck body only ever lose energy, a driven body stays bounded at any drive, a mirrored place swaps the ears, and placing never amplifies. The survey's: a white dielectric returns the field from every side (a plate head on, at 60 degrees, and at grazing, through both ambient pipelines, and in the trace, whose surface function is the lamps' own), an anti-reflection coating never brightens its glass and dips to the quarter-wave value at its design thickness, a disk light falls off as its closed-form form factor, a spot's shadow meets its caster's base, the bounce on a ceiling the lamp never reaches matches a 64-deep path trace of the room (the light room's bounce against the dark room's within 6% of the trace's own ratio, each inside the grid's 0.85 to 1.3 envelope, for the standard and the physically based finish), a rough copper floor throws copper onto the ceiling by what the trace throws and a fifth of what a matte copper would, the traced furnace holds with the environment importance-sampled, an environment's sun reaches the scene, the frame is linear in its light under aerial perspective (a lamp's, and a sky's when its intensity doubles), a clearer sky casts harder light (the procedural sky's own sun against the model, facing-to-away and sun-to-shadow), the far air is the sky's horizon, a floor gathers the whole sky, an emitter lights its neighbors under global illumination as the trace does, a lamp bounces nothing from past its reach, a panel's shadow is the light-weighted share it blocks, a shadow ignores where the camera aims, a lamp lights nothing past its reach in the window and the trace, an index-matched pane changes nothing in the trace (and a block does not at roughness 0.08), still 2D passes through the live resolve (read off the live loop: one renderer, one drawable, 64 refreshes), a sprayed dot keeps its light at any count and focus, a flat aperture blurs to the thin lens's circle, and a lens coating reflects what the exact single-layer film does. Load-bearing: every invariant that holds (35 of the first 36, and every statement the survey enforces, under ten plants of their own) was verified red under a planted defect of its own (a shader or source edit, restored after; a plant that misses the path a probe runs reads green, which is how two bounce plants had to move from the environment ambient to the lit path, and the spatializer's centered pan sits at 0.6 of the dry stream per ear, so a placed stream must be doubled before placingNeverAmplifies reads; deleting the probe field's recursion read green at albedo 0.5, where an open room's higher bounces fit inside the bound, so that plant drops the hit's albedo), and a pin records its gap by construction; a probe that reads a path trace or the probe field gates on Snapshot.hasRaytracing as well as hasMetal, since a runner without ray tracing has neither; a furnace probe reads the disc against the backdrop so the auto-exposure cancels; a statement about an 8-bit read is judged by row or by block, since the present dithers one level into every pixel and at a mid gray one level moves a pixel's squared transmittance by 0.02; and a probe that measures light brighter than white tone-maps with Reinhard and unrolls it on readback (renderUnrolled), since a clipped read had shown a caustic's excess as 1.08 where the whole of it is 1.5. Measured gaps stand as withKnownIssue pins, failing the day it closes (Swift Testing fails a test whose known issue stops occurring, so a fix must take the pin off; the caustic's, a deposit of 1.5 times its shadow, the motion blur's, a gain of 5% of a 24 px mover's light at the default shutter and 25% at a full one, and the color-vision table's, not a projection (a blue moved 0.064 on a second pass) with a tritan dial that overshot its end, came off that way): the generic CMYK profile does not round-trip (4 to 11 levels on a second proof, yellow the worst). That gap is the system profile's own, since the proof is the system's transform. The survey's pins (2026-10-09, each filed under Photorealistic 3D in DESIGN-NOTES.md, measured, never predicted; the probe field's two, emission and the light room, came off 2026-10-12, the sky's two, its sun with no beam and the aerial veil no light scaled, on 2026-10-14, and the environment's diffuse weight, a panel's unweighted shadow, and the tracer brightening a ball behind a pane of index 1 on 2026-10-16, two more joining them that day): the trace running hot in a lit room (its continuation reads the room blurred to the footprint while the environment's own draw reads it sharp, so a lamp's blurred skirt is counted beside its sharp core: a matte ball 0.475 where the window reads 0.384 and a continuation-only trace 0.372), light behind a solid counted twice in the trace (a block of index 1 brightens the ball behind it 8% at roughness 0.5), the live resolve softening still 2D (a hairline reads 85, 219, and 111), a sprayed dot in focus developing 2.6 times its defocused light at 100 points and 0.03 times at 16,000, and the flare's two-beam coating (0.0086 against the exact 0.0043 head on into 1.67 glass). Survey B (2026-10-11, the sound, the color science, and the solvers): 35 more across SoundInvariantTests (16), ColorScienceInvariantTests (3), FrameInvariantTests (6), and Tests/OllinPhysicsTests/PhysicsInvariantTests.swift (10, new), each read off rendered samples, a file, or a body's own motion. What holds, each red under a plant of its own (twelve plants: the string's tuning allpass dropped, a shape's mode ratios scaled, the delay's neutral mix off by a percent, the strike's push ten times taller, the reed's closing pressure doubled, the driven loops' period stretched by 0.4%, the gamut's cusp chroma scaled by 0.97, the HDR metadata insertion mode dropped, gravity scaled in both bridges, the 2D body damping raised, the restitution scaled in both bridges and on the 3D ground): a plucked string sounds its note within a cent from MIDI 33 to 93 and a bowed or blown one within five cents from 45 to 93 at pressure 0.8, a struck disc rings at a circular membrane's Bessel ratios within 1%, every struck preset is a sum of tones with nothing on the renderer's ceiling and the bell's foreign lines over 50 dB under its prime, a softer strike is darker and never louder and as loud at 48 kHz as at 44.1, an instrument inside a struct in an array, a dictionary, or an object of the sketch's own reaches the export and is advanced, a note asked for on frame 0 or frame 10 starts on that frame's first sample, a hearing range changed at frame 30 is heard from that frame with the earlier note untouched, an unplaced sound plays at the never-placed level in both ears and pans again when placed again, every tube speaks from pressure 0.3 at every pitch, the delay, the room, the equalizer, and the convolution at their neutral settings pass the sound to the byte, the gamut map keeps hue within a ten thousandth of a turn and lands inside the gamut within 0.006 chroma of the true boundary, a free body falls half g t squared within 1% in both worlds, a body in either world coasts at zero drag, drag is one reading in both worlds and at every frame rate (a particle at 0.01 keeps the same share at 30, 60, and 120 Hz and a rigid body the same share as the particle), a hinged bar returns to the pivot's height within 5% for ten seconds in both worlds (the 2D one within 0.1% at one piece a frame, the 3D one at sixteen) and a Verlet bob on a rod does at sixty-four, a ball at restitution 1 returns to its drop within 5% in both worlds and one at 0 does not bounce off a default floor, a body that named a bounce keeps it when the world's changes and one that named none follows, a world's drag reaches its bodies when changed and never a kinematic one, a settled box rises when gravity is turned upward in both worlds, a spin reads back as set at a 1/60 and a 1/30 step, and the HDR export declares its mastering display (a 1000 cd/m² peak on P3 primaries) and its content light level (MaxCLL 1000, MaxFALL 400) in the track, with the PQ codes of a gray within 0.004 of the standard's; and since 2026-10-17 a black pixel separates to black alone, an absolute proof of white shows the stock and the simulated paper is its profile's media white, the dither is unbiased at every alpha and a see-through file keeps its levels, an eight-bit file of a wide frame is dithered, the wide PNG keeps sixteen bits and the HEIC ten, and the gain map rebuilds a highlight's edge within 5%. The survey's pins (each filed under Sound, synthesis, and spatial audio in DESIGN-NOTES.md; the eight physics pins came off on 2026-10-14, the seven color-file and print pins on 2026-10-17): the clarinet and reedy tubes at MIDI 93 sound a weak fundamental under their twelfth (the sound's other eight came off on 2026-10-16).
  • Transparent output (Sources/Ollin/Renderer/ShaderEffects.metal present fragments, MetalRenderer.displayImage(transparent:), VideoCodec.hevcWithAlpha). A canvas whose background alpha is under 1 (background(.clear); Drawer.hasTransparentBackground) leaves the file see-through: --export writes a PNG with alpha, --export-sequence a folder of them, --export-gif a hard one-index cut, and --export-video an alpha channel through the two codecs that carry one, proRes4444 and hevcWithAlpha (VideoCodec.carriesAlpha); any other codec composites over black, as the window does, and the export says so on its first frame. Load-bearing: the canvas composites premultiplied in linear light over its clear color (the normal blend's one / oneMinusSourceAlpha alpha factors), so the resolved texture already holds coverage; the present pass takes a keepsAlpha uniform (encodePresent(alpha:), a PresentAlpha, set by the headless render, the accumulate path, the made-frame path, and the frame grab, never by the window's drawable, which stays opaque and byte-identical) and, with it set, divides the coverage out before the tone map (a half-covered white maps as white at half, not gray at full). An export stores straight (.straight: the encoded color as drawn beside its coverage, the form a PNG holds, the dither on that stored color, so 64 grays at alpha 0.1 are 64 values in the file where premultiplied bytes held 26, and a flat field at alpha 0.2 averages to its fraction where the dither, scaled by the alpha, could not straddle a code); a frame grab stores premultiplied (.premultiplied, the form a texture shared live through Syphon or an LED map is read in: the coverage quantized first, enc(straight) * a in the encoded domain, the convention a reader of an 8-bit image with alpha divides back out, premultiplying in linear light having read a half-covered mid gray ~40% too light, and the dither on that stored value under the alpha's ceiling so white under coverage stays exact and no channel passes the alpha); the float readback premultiplies its linear components in either mode; the read-back CGImage is tagged first (straight) or premultipliedFirst / premultipliedLast only for such a frame, so an opaque frame keeps noneSkipFirst and its bytes; the video and recorder fills clear the pooled pixel buffer before drawing a frame that carries alpha (a pooled buffer still holds its last frame) and mark it kCVImageBufferAlphaChannelMode_PremultipliedAlpha; an extended clip has no alpha channel, so hevcWithAlpha there falls back to hevc. A frame filter runs on the premultiplied frame ahead of the present, so a blur spreads coverage with color. TransparentOutputTests (12 tests: the tag and the corner/center bytes, an opaque canvas unchanged, half cover stored as drawn, the tone map on the straight color, a translucent ground, the PNG and the GIF read back, a blurred frame keeping its coverage, ProRes 4444 and HEVC-with-alpha clips read back through AVAssetReader, h264 over black, the flag spelling); FrameInvariantTests.aSeeThroughFrameKeepsItsLevels / theDitherIsUnbiasedAtEveryAlpha (the file) and FrameGrabTests.aGrabOfASeeThroughCanvasIsPremultipliedAndUnbiased (the grab), each red under its plant. Examples/Export/Cutout; Docs/Output/Export.md § Transparent output; Guide Ch 41 § Leaving the background behind: transparent video.
  • Path-traced export. --path-traced [N] (+ --pt-depth, default 8) switches the still/sequence/video exports to an offline path tracer over the same scene the raster window tunes live — physically soft area shadows, color bleed, mirror-in-mirror reflections, real solid/thin glass with tinted transparent shadows, emissive meshes sampled as lights, base-color textures read at hits, and a thin-lens depth of field (Camera3D.aperture / focusDistance, read by the traced camera only; the focus is measured from the eye and the opening sits at the eye, the ray re-aimed at the focus point and walked back out to the near plane, lens.w naming an orthographic camera whose opening sits at each ray's own foot in the eye's plane; measured from the near point with the opening there, the focus landed a near plane too far and the blur ran s / (s - near) wide, and theTracedLensFocusesAtTheDistanceFromTheEye pins the bar at the focus sharper than one a near plane beyond it). Programmatic switch OllinApp.pathTracedExport (PathTracing); the bare flag's sample count follows --render-quality (64/256/4096; the detail tier is a final still, not a general default, and a sequence multiplies it by the frame count); needs an RT GPU, else a one-line note + the raster render; contact sheets, vector exports, and --bench stay raster by design. Load-bearing: the trace runs in its own completed command buffers before the frame's other encoding (chunked adaptively toward ~1s of GPU work per buffer — watchdog + progress; the still export prints a rewriting sample line, the sequence drivers keep their per-frame line) — only then may the frame's shadow pass re-fill the shared accel/offsets rings the trace used; the scene handoff generalizes the caustic-geo pattern (buildShadowAccel(pathTraceMats:) breaks coalesced runs on any batch-finish change and uploads one OllinMaterial per geometry); matcap batches are excluded from the traced scene and keep rastering (the unlit emissive-look finish — an area light's glowing prop must not swallow the light's own next-event rays); the traced layer composites once, at the first solid mesh batch (premultiplied over, so silhouette edges blend and draw order against 2D holds) with the fragment writing the primary pinhole depth, so wireframe/grid/matcap and the untraced 3D kinds (points, strands, raymarched fields, and any copy batch the traced scene turned away) still raster and occlude correctly. Integrator conventions are raster parity: punctual lights keep the un-normalized legacy Lambert for standard/toon/gooch and the full microfacet terms for the physically-based finish; area lights are physical with color as radiance (a tube radiates by its silhouette ribbon, length × diameter, never its full skin — the full-skin form overshoots by ~π); the environment samples the same equirect at the same normalization × intensity as the skybox/IBL; no environment → the flat ambient is the miss field, which makes an ambient-only scene an exact furnace (PathTraceTests pins the mean within 3/255). A light that declines to throw casts nothing here either. The tracer keeps no caster list and needs no castShadows(), so without the flag it throws from every light in the frame while the raster throws from none, and a preset's fill (which sets castsShadow: false exactly to open the shadow side) would grow a shadow in the export alone. The flag is stored in OllinLight.shaping.w (0 = throws, so a default light packs byte-identically and the raster reads nothing new), and the tracer skips that light's visibility ray; the raster's own resolution is untouched (CPU-side shadowCasters, four slots, gated by castShadows()). So in the raster the flag is a cost dial too, and in the export it decides the picture alone. PathTraceTests pins it: the shadowed floor patch comes back to the lit floor's brightness within 6/255, having sat 139 levels below it. Variance controls, each from the published technique: environment importance tables (512×256 latitude-weighted luminance CDFs + solid-angle pdf grid, cached per equirect texture; the environment strategy reads the table's own filtered mip so one lamp texel can't out-spike its cell), the power heuristic through one shared lobe-pdf function on both sides (bias otherwise), filtered importance sampling on lobe-side environment reads (mip from the sampled pdf's solid angle — mirrors stay sharp) widened by the ray cone (below), path-space regularization (the light list's evaluation at a vertex past the first scatter floors roughness at 0.25 and reprices its compensation and body there — the single-pixel-glitter fix, since a listed light has no pdf division and no partner; the environment and mesh-light strategies keep the sharp lobe, since each is paired with the continuation through the power heuristic and the pair is one estimate only while both price one lobe: a floored lobe on one side lit a ball behind a pane of nothing 12% brighter; the continuation stays exact so mirror chains stay sharp), a thin pane crossed straight is no scatter (it carries the vertex's pdf, its pairing, and the leg's length on, since that vertex's shadow rays already passed it: a reset there credited the light behind a pane in full beside the share its strategies had counted, 8% at roughness 0.5; the regularization and the bounce bound count scatters, not crossings, and a shadow ray through a pane takes 1 - F at the index the continuation crosses with), and Russian roulette from depth 3. An opt-in grain filter over the finished render (PathTracing.denoise, --denoise; off by default on @eaviles's call, so the plain flag always writes the estimate the tracer arrived at, even though the measurement below says leaving it on would cost no accuracy): the trace also fills two guide layers at the first hit (base color after maps + the running sum of each sample's luma²; shading normal + primary distance), written only while pt.meshLights.w is up, else a 1×1 stand-in is bound to texture slots 5/6 and nothing is written or allocated. A four-kernel chain (ollin_pt_denoise_prepare → _atrous ×5 over a ping-pong pair → _finish) filters the demodulated radiance (light ÷ first-hit albedo, re-modulated at the end, so a texture edge or a painted pattern is never blurred, only the light on it), edge-stopping on normal agreement (power 128), relative distance (0.02 × the center's own distance, opened by the tap spacing), and luminance divided by the measured variance (the unbiased sample variance of the mean, /(N-1), 3×3-blurred before use since a pixel's own estimate of its spread is itself noisy; it is stored in the alpha channel and is filtered with squared weights). _finish writes back into accum in accum's own units (radiance × coverage), so the composite fragment is untouched. The load-bearing measurement is that it never trades the picture for smoothness — against an 8192-spp truth render of the example scene, the filtered frame is closer at every count tried: 17.60 → 9.13 RMSE at 64 spp (the accuracy of ~240 samples) and 5.48 → 3.89 at 2048 (the accuracy of ~4070, i.e. ~5 min of M2 tracing saved), so what it removes at the deep end is residual grain, not detail. On a sequence it also cuts flicker rather than adding it (consecutive frames of the moving example at 48 spp: 2.85 → 1.21 RMSE, max jump 249 → 156). Cost does not show against run-to-run noise at 1080² (four compute passes). Settings were swept, not copied: wider luminance blending (σl 8/12/16) buys ~1.5% RMSE and costs ~30% of the kept high-frequency detail, so the published σl = 4 over 5 passes stands. Two envelope facts: a true sparkle (rough metal catching a small bright source) reads softer, since glitter and grain are the same signal to a variance-driven filter; and one sample has no spread to measure, so the pass never runs at samplesPerPixel == 1. A bound on what one bounce may add to a sample (--pt-clamp X, PathTracing.maxBounceLight; 0, the default, is none): every contribution the kernel adds goes through ollin_pt_add, and the bound applies to light that has scattered at least twice on its way to the eye, which is the production definition of indirect light: the environment or an emitter met at depth > 1 (two or more bounces out) and a light strategy run at a bounce vertex (depth > 0); it holds the contribution to X in its brightest channel, scaled whole so its color holds, and sums the luma it took off into the statistics layer's w (allocated full size when bounded, as under the stop). What one scattering shows (a lamp in a mirror, the environment in a bead, the lamps a surface faces) is direct and never bounded, so a lit plane with nothing to bounce light between is byte-identical under any bound (theBoundLeavesTheFirstHitAlone). The off-by-one is load-bearing: bounding everything past the first hit instead took half the light out of a scene of two thousand polished beads mirroring a lit studio (energy 0.35 to 0.52 of the reference at bounds of 2 to 8) for under a decibel, since those beads are their one-bounce reflections. It is a bias and says so: boundedLightShare reads the accumulation and the statistics back once per frame, before the denoiser rewrites the accumulation, and the report's lightDropped (the dropped luma over kept plus dropped) reaches the recipe as "clamp" and "clampDropped" (to a hundredth of a percent) and the sequence export's closing line. The point is the grain a per-pixel statistic cannot read: a lamp caught through a polished surface every few hundred samples, which no count smooths and which keeps every pixel of a polished scene open under the stop; bounded, the fireflies on the floor beside a near-mirror ball (its pixels far above the floor's own level) fall by more than half at the same count while the floor's ordinary sampling noise stays (the lamp's area, sampled), its mean holds within a percent, and it settles at a lower mean count under a threshold (aBoundOnABounceCutsTheFirefliesAndSaysWhatItTook, the firefly probe at 160²: 48 samples, bound 2, 40 pixels more than 24 levels over the floor's median against none, the whole spread 4.1% to 2.4%, the mean 162.2 to 161.6 of 255, the bound taking 0.3% of the frame's light, the stop's mean 45 to 42 of 128 at 0.04). The limit is applied per contribution (the shape of Cycles' indirect clamp), not to the sample's total, so a path that legitimately carries several bounces of ordinary light is untouched. Measured on the scene that asked for it (two thousand polished beads mirroring a lit studio, 128 samples at 1080² against 1024): a bound of 4 took 7.5% of the frame's light (9.3% at 2, 5.5% at 8) for a tenth of a decibel, and the stop's mean stayed at 86 of 128 with or without it; that scene's grain is one-bounce light (the studio's lamps through each bead's glossy lobe), which the bound leaves alone by design, and the recipe's share is what says a render is the wrong case. The probe's floor, speckled by a mesh light caught through a near-mirror ball two bounces out, is the case it is for; a .glowing surface is credited whole wherever a path lands (no light strategy), so a small hot glowing mesh speckles everything that sees it after one bounce and is not a case for the bound either (the first probe used one, and the test read nothing until the lamp became a mesh light). Adaptive sampling (--pt-noise X, PathTracing.noiseThreshold; --pt-min N, minSamplesPerPixel): a per-pixel stop once the pixel has settled, so the count becomes the most a pixel traces. The statistic is the standard error of the brightness the viewer sees (the trace adds each sample's squared luma, the backdrop luma behind a primary miss, and one to a per-pixel stats layer; behind a miss the brightness is the environment along the ray when it draws as the skybox, else the clear color's luma, so a silhouette over a bright sky counts and the same edge over black does not), divided by the square root of the mean below 1 and by the mean above it (Dammertz and colleagues' measure with Cycles' two refinements; ATTRIBUTION.md has the lineage), against the threshold: 0.01 is about one 8-bit level of standard error at any brightness. The rounds fall at fixed sample indices whatever the GPU's timing (the minimum, then every PathTracing.checkStep = 8; the automatic minimum is the square root of the cap rounded up to a multiple of 8 and never under 8), a check between rounds (ollin_pt_converge) closes the settled pixels and a dilation (ollin_pt_dilate, 3×3) keeps an open pixel's eight neighbors open and writes the open pixels into a compacted list (each SIMD group reserves one range with a single atomic add and fills it in lane order; the list's order is whatever the adds made it, which cannot move a pixel's bytes since its sums depend on its own samples alone); from the first check on, a dispatch runs over that list, one thread per open pixel, never over the grid (pt.open.x), because a settled pixel in a grid dispatch keeps its SIMD lane busy for as long as any pixel in its group still traces: measured on the example scene at 512 and a 0.02 threshold, the grid dispatch dropped 40% of the samples and saved 5% of the time. Nothing open ends the frame. The kernel adds every sample onto the running sums in sample order from the texture's value, never a chunk total added at the end, so the frame is the same bytes however the host cuts a round into dispatches by GPU time (theTracedFrameIsTheSameBytesUnderAnyCut forces cuts of 1, 5 and 7 through OllinApp.pathTraceChunkSize, for a fixed count and under the stop); before this the fixed path's last bits followed the clock too. The composite divides each pixel by its own count (the stats layer's z, PathTracedLayer.counts; a fixed count keeps the one reciprocal and its bytes), the denoiser is untouched (its statistics are per hit). MetalRenderer.lastPathTraceReport (reset at the top of every pass, so an untraced frame reads nil) carries the settings, the resolved minimum and the mean count reached; the still, sequence and EXR recipes write it as "pathTraced":{"samples","depth","denoised","noise","minSamples","meanSamples","clamp","clampDropped"} (each only when it applies, the mean to a tenth, the dropped share to a hundredth of a percent), the video recipe the settings alone (one recipe per file, written before its second frame), and renderFrames prints the run's mean once at the end. The progress line adds the share of pixels settled. Measured (adaptiveSamplingSpendsTheCountWhereTheGrainIs, the grain probe's room at 160², cap 256, against 2048): 0.005 reached a mean of 140 for the full count's error (0.77 against 0.75; a fixed 140 errs 1.01), 0.01 a mean of 77 (0.90), 0.02 a mean of 38 (1.31; a fixed 38 errs 1.87); the minimum barely moves it (8/16/32 at 0.01: 73/77/84 for 0.906/0.902/0.897). On the example scene at 1080² against 2048 (release, M2): fixed 512 104 s 29.65 dB; 0.02 mean 306, 105 s, 29.60; 0.04 mean 217, 88 s, 29.20 (fixed 224: 45 s, 26.69; fixed 432 at the same 86 s: 28.94); 0.08 mean 143, 64 s, 27.95 (fixed 144: 29 s, 25.47; fixed 312 at 62 s: 27.75). The stop saves samples first and time second: the pixels that settle are the cheap ones (margins, matte floor) and the open ones (glass, the mirror floor, the blur) cost two to three times the average per sample, so 58% fewer samples bought 15% less time; at equal time it is a quarter of a decibel ahead, at equal samples two and a half. The compacted dispatch measured within the run-to-run spread of the grid dispatch on that scene (0.02: 105 against 96 s, 0.08: 64 against 66) and is kept for the frame that is mostly settled, where a grid dispatch runs a SIMD group for every block that holds one open pixel. The envelope is the rare bright path a pixel has not seen when checked, which no per-pixel statistic can count: two thousand polished beads on black are grain from edge to edge at 128 and at 512 samples (a fixed 128 sits 21 dB from a 1024 reference; each bead mirrors a studio with hot lamps, so every pixel catches a firefly every few hundred samples and its spread never comes under a usable figure), so only the margins settle (mean 87 of 128 at every threshold up to 0.04, 38% of pixels at 512 and 0.06) and the time does not move; a bound on what one sample may carry is the lever there, filed with the other open end in DESIGN-NOTES.md. Reuse across a sequence (--pt-reuse [N], PathTracing.reusedFrames; 0, the default, is none; bare = 8): after the trace and before the filter, ollin_pt_reuse follows every pixel back to where it was in the previous frame and carries that frame's sums in (encodePathTraceReuse, MetalRenderer+PathTrace.swift). The motion is the raster's own: encodeMoverVelocity runs for the frame's declared movers (withMotion, instanced copies included; previous minus current, pixels, y-down, the MetalFX contract) and wins where it wrote, and every other covered pixel reprojects its surface point through drawer.previousCamera3D's view-projection, which also gives the depth it expects there (a mover's previous depth is not known, so its own stands in); a backdrop pixel holds still. Under a lens a pixel follows the movers inside its circle of confusion: the reuse uniforms carry the aperture, the focus distance, and the pixel's footprint and angle, each pixel's circle is R |s − d| / (d s θ) pixels (orthographic: R |s − d| / s over the pixel width), and a pixel blurred past three quarters of a pixel takes the mean of the mover words a 5×5 sampling of that circle finds (a sharp pixel reads its own word), because a blurred mover's light spreads far past the pinhole footprint the velocity pass wrote its motion at, and followed at zero motion a defocused bead sweeping the frame left a trail of its blurred light a frame long (the peer's scene: subject 20.29 → 21.04 dB, carried 221.7 → 239.9 of 256). Each frame of a run traces its own stretch of every pixel's random stream (pt.open.z, the frame's index in the run times carriedCap, carried in PathTraceHistory.frameIndex): the stream is a pure function of pixel and sample index, so without the offset every frame of a still scene retraced the same paths and the history averaged identical samples (the first probe settled to 2.38 levels of error where a fixed 64 read 1.43; with the offset 1.19, under a fixed 64 as a running mean should be); outside reuse the offset is 0 and the stream is what it always was. A pixel is classed by its own samples, not by its center ray: a center hit gives the surface point, a center miss with hits (a silhouette pixel) takes its samples' mean hit distance along the center ray, and only a pixel with no hit is the backdrop; classing silhouette pixels as misses let them hold still under a pan and carry their edge light along as the edge passed, a stripe a frame (the pan probe read 3.36 against 2.89 alone; 1.76 after). The history is read through the four texels around the spot, each admitted only if it held the same surface. Coverage is part of the signal: a texel joins unless its share of hits disagrees with the pixel's by more than 0.6 (a surface that arrived or left), an empty pixel admits only texels under 0.15 coverage (a quarter-covered texel is a silhouette that moved past, and carrying it into the empty pixel behind drew the pan probe's stripe again), and a silhouette pixel keeps the empty texels in its footprint so the bilinear mix gives it its partial coverage; turning away every texel whose hit state differed selected for luck (in a sparsely covered blurred field most texels have no hit by chance at a few dozen samples, the survivors were the bright ones, and the peer's field read 5% bright, more the more it carried, while every sharp bead fattened; the rule took the subject from 21.04 to 21.95 dB). Where both the pixel and the texel hit, the view depth (the pixel's surface point from ollin_pt_pixel_point, measured along the view axis from the eye, so perspective and orthographic read alike) within 2% of the expected plus twice the largest change to a covered neighbor, the facing within 0.16 between unit normals (about nine degrees) plus four times the largest change to a neighbor, both opened by the local change since a bilinear read spans a texel. Light follows the facing, not the material point, so the interpolated history must face the pixel's way: a reflection does not move with its surface (a ball turning in place carries its surface points around while its image of the room, and its shading, stay put), and the spread test cannot tell the wrong light fetched there from a rare path the frame has not caught, so after the per-texel tests the history interpolated at the spot is compared with the pixel's own mean facing within 0.03 (about two degrees) plus three times each side's sampling error (ollin_pt_facing_error: the normal sum of hits unit normals shortens by half the squared spread, so the spread is the root of twice the shortfall and the mean's error that over the root of the count; a small curved bead or a pixel whose lens samples land on several surfaces reads a wide one) plus a quarter of the squared local change (the interpolation's own slack), or the whole history is dropped. A tolerance without the sampling term failed at random on the peer's bead scene under its lens (a mixture pixel's mean normal is noise) and the frames flickered more with the history than without. The per-texel facing test stays loose (0.16 + 4× the local change): it only asks whether a texel is the same surface at all. The neighbors' depth change is measured on the same per-pixel surface depth as the pixel's own (a center hit, else the mean hit distance), so under a lens the change is the smooth one of the blur and not a jump between beads. Trust by spread is the load-bearing rule and it is two-sample: the carried mean may sit three standard errors from the frame's own, the two spreads combined (sqrt(varC/n + varH/nH), each from its own Σluma² in the guide layer), and is clamped per channel to that distance beyond it; the one-sample form (the frame's spread alone) darkens a firefly history, since a frame that caught no rare path reads a tiny spread and pulls the brighter truth down to it. The carried spread follows its mean (Σluma² scaled by the luma ratio squared). The cap is carriedCap (frames × count): past it the history scales so the whole holds the cap, a running mean from then on. The result is written over the frame's own sums and the per-pixel count into stats.z, so the composite divides per pixel (the counts layer is bound under reuse as under the stop), and out as the next history (PathTraceHistory: accum, both guides, a meta layer of count and view depth; one per renderer, dropped on a size or drawer mismatch, the mode off, or a frame that was not traced, so a run never reads a history that is not its own). The guide layers are filled under reuse (the spread and the facing live there) whether or not the filter runs; the filter, when asked, runs on the carried sums and so fades as the history deepens. The first frame of a run traces reusedFrames × samplesPerPixel (no history at this size for this drawer), so a sequence starts settled and a still rendered alone reproduces a sequence frame's settledness rather than its bytes; the recipe writes "reuse" and "carriedSamples" (the mean of stats.z, read back once per frame) and the sequence drivers print the run's mean. Determinism: every step is a pure function of what the earlier frames left, so a sequence is the same bytes twice (TracedReuseTests), and the mode off is byte-identical to the frames rendered alone. Measured on the 160² probes against converged renders (TracedReuseTests): a still scene at 16 × 4 carried reads 1.19 levels where a fixed 64 reads 1.43 and 16 alone 2.95; a panning camera 1.76 against 2.89 alone; a bright box sliding over a dark floor leaves the uncovered strip within the noise of the reference (no ghost) while the far floor settles; a polished ball spinning in place comes out at 4.49 against 4.66 alone while the floor beside it settles; the first frame equals a fixed render of the carried count to the rounding of a division. Measured on the scene that asked for it (two thousand polished beads sweeping under a still camera with a lens, 1080², depth 4, twelve frames at 60 fps of its lap, against a 1024-sample render of the last frame): 32 samples a frame with 8 carried (241 of 256 held) took 57 s for the twelve frames where 128 alone took 136 s; the subject read 21.95 dB (32 alone 20.31, 128 alone 24.05; denoised 22.57 against 25.73), the whole frame 23.66 (22.14, 25.59), and consecutive frames sat 20.62 dB apart (32 alone 18.68, 128 alone 19.90; denoised 21.49 against 21.12), so the sequence is steadier than the baseline at 42% of its time and each frame's fidelity lies between 32 and 128. Two limits, measured and left in: the bilinear history read at a sub-pixel offset every frame a surface moves spreads a pixel-wide lamp speck into its neighbors, and in eight bits that reads bright, since the speck was clipped at white and its spill is not (the mid-tones read +4.5 levels against the reference where 128 alone read −1.4, the bright class −11 against −7.6: light moved from specks to their surroundings; energy holds in linear light); and a blurred field whose movers inside one circle of confusion move at different rates (the back hemisphere of the sweep) is followed by their mean and smears along the sweep. Two guards were measured and refused: dropping the history where the movers in a circle disagree by over a pixel (it refused most of the back field, carried 241 → 146, and the frame fell back to its 32-sample error and flicker), and taking the nearest texel where the four texels disagree beyond their errors (the pan probe's error rose from 1.76 to 2.29 from the positional jitter). The edge-aware or Catmull-Rom read and the differential motion are filed in DESIGN-NOTES.md. What it cannot follow is written in DESIGN-NOTES.md: a reflection on a moving polished surface (the virtual-position vector), and a stop that reads the carried spread. A pixel's worth of a curved surface (the ray cone). A cone (signed width and spread, coneWidth/coneSpread, plus the lens opening's angle lensSpread from the focus plane) travels with each path: it starts at the pixel's width and angle (pt.cone.xy), a thin lens opens the width to the whole opening and turns the spread inward by it (so |width| past the focus is the circle of confusion), the width grows by spread × distance at each hit, and a reflection adds twice the triangle's mean edge curvature times the footprint (ollin_pt_curvature: Reed's per-edge formula from the vertex normals, averaged, world-space for a copy through the query's transforms, the sign flipped on a back face; the footprint foreshortened by |Ng·d| held at 0.1). At each opaque scatter the sampled lobe's footprint (one over its pdf, as a disc's full angle) folds into the spread in quadrature (ollin_pt_widen), so it reaches the next curved surface; a glass reflection takes the curvature term, a crossing keeps the cone. The lobe-side miss reads the environment at ollin_pt_env_lod: the cone's solid angle (spread ⊕ lens, as a cone's, saturating at the sphere) over the mean texel, never coarser than the level whose cell is a sixteenth of the turn (22.5°, level 6 of a 1024-wide chain; at the chain's top the read averages the room before the lobe's Fresnel and cosine weigh it, and the bead field read 9% bright between its beads there against 2% at this cell, for 2% more spread), from the tracer's own latitude-weighted copy of the equirect (ollin_pt_env_mip, one dispatch per level averaging the four source texels by the sine of their own latitude, cached beside the tables in ptEnvTableCache.filtered, bound at texture 9): the plain box chain weighs the poles like the equator and read 2 to 3% dark at levels 4 to 6 and 9 to 12% at 7 to 9 on the bundled interior. The table strategy and the backdrop keep the original texture at the table's level (texture 2): a draw read blurred carried little while the blur's skirt sat in cells the table rarely draws, and lost 13% of the bead scene's light at 128 (measured through the strategy probes), so that is the shape to refuse. The light list is shaded through a lobe widened by the footprint's normal spread (hDirect: σ = curvature × footprint / √12, α² += 2σ², the raster's specular anti-aliasing; its energy compensation re-priced at the widened roughness, or a defocused bead's highlight at roughness 0.5 lost its multiple scattering, 4% of the frame), and only the light list: widening the whole lobe (the continuation, the table pairing) turned the beads satin and read 14% bright at the center (rejected), and the light list has no sampling pair, so its widening costs the pairing nothing. Measured on the bead scene (2048 polished beads, lighting-only interior, thin lens, 1080², against a sharp 1024): per-sample variance over the subject 22.3 → 6.4 (−71%; the jitter and the lens frozen left 7.9, so two thirds was the footprint; at a fixed hit the lobe's draw left 3.4 and the table's 4.5; the point light's spike through the near-delta lobe was the 6.7 common to every strategy probe), 128 against the reference 18.0 → 19.3 dB, 128 against its own 1024 23.7 dB where the sharp pair sat 18.0, and every region within about 2% of the reference (the whole subject +0.6%, the bead interiors −1.0%, the rim −1.6%, the defocused field between beads +0.7%; the unfiltered 128 itself sits +0.8% and +1.4% by the same measure). A flat mirror reads as before (aFlatMirrorReadsTheEnvironmentAsBefore, its reference recorded under the kernel without the cone); the bead field converges (aPolishedBeadFieldConvergesAtItsFootprint: 32 against 512 samples, RMS 10.2 over the beads' pixels, 19.7 before, bound 14). The surface maps keep their footprint of pixel width plus spread × distance (no curvature), so textured snapshots are untouched. Every integrator probe in PathTraceTests, and the path-traced-3d snapshot, names denoise: false explicitly (redundant against today's default, and kept so that flipping the default can never quietly re-aim them) (they measure the sampling, not the filter); the filter has its own five probes (moves-a-thin-render-toward-the-truth, still-helps-a-nearly-converged-one + strength-follows-the-measurement, keeps-a-hard-texture-edge, the filtered furnace, determinism). Deterministic by construction: sampling is a pure function of (pixel, sample index, bounce) and accumulation order is fixed, so the same command renders the same bytes (pinned). Glass (transmission > 0, physically-based): a stochastic weight-free mix by the transmission fraction routes that share of paths to a microfacet dielectric — VNDF micro-normal, exact dielectric Fresnel, TIR reflects, a thin pane (thickness 0) passes straight through with no bend, a solid bends by Snell and pays Beer-Lambert attenuationColor^(d/attenuationDistance) per interior segment (tint applied on the crossing in only, never the exit) — with estimator weight the outgoing Smith G1 alone (the VNDF pdf cancels the rest), so a polished clear sphere is a furnace-exact lossless dielectric (pinned within 4/255). A glass vertex runs no next-event pass and marks prevNEE = false; the next miss or emissive hit then takes full weight — the MIS bookkeeping rule; breaking it under- or double-weights everything seen through glass. Transparent shadows: NEE visibility runs a closest-hit walk that passes through transmissive geometry (albedo × transmission per thin sheet / solid entry + Beer inside, a straight line — the bent, focused kind stays the live caustics() feature's), gated by a scene-level any-transmission flag so glass-free frames keep the cheap any-hit ray. Textures & surface maps: the accel run also breaks on map identity across all five slots (gate-aware: a texture only counts while its finish gate — normalScale/mrGate/occlusionStrength/emissive.w — is up); a per-geometry bindless five-slot resource-ID table (OllinPTTexEntry: base, normal, metallic-roughness, occlusion, emissive; useResources residency; the white stand-in in an unbound slot is the base slot's multiply identity, and the gated slots are never read unbound) is sampled at the hit's interpolated uv at the ray-cone mip (pixel footprint at the eye + spread × path distance, over the triangle's own texel density; per-map resolution folded in per slot). The maps compose exactly as the raster surface-mapped fragment: the normal map bends through the interpolated vertex-tangent frame (bitangent = sign · cross(N, T), bend on the raw frame, flip to the viewed side after) while OllinPTHit.Ng keeps the geometric normal for the ray machinery (epsilon offsets, the below-horizon break — the raster's detail-vs-position split; unmapped scenes are untouched since Ng == N there); the metallic-roughness channels multiply the finish-table factors (which carry the drawing-state × mesh-material product — the vertex w slots carry only the drawing state, so a gated hit must read the table); the occlusion ramp dims the environment share only — the env NEE term at the vertex and, via a carried aoPrev, the paired lobe-side miss pickup, both halves scaling together so the MIS pairing stays consistent; direct lights, emissive hits, and surface-to-surface bounce light stay undimmed (the trace occludes those with real geometry); the emissive map multiplies the factor at hits and at mesh-light NEE sample points, while selection, pdf, and the reverse MIS weight all stay factor-based (map texels never exceed 1, so the factor pdf's support covers the glow). Triplanar base color + normal map project at hits through the raster's own ollin_triplanar_surface (level-0 reads, matching the raster's mipless textures). Emissive-mesh sampling: a CPU power CDF over the glowing geometries' triangles (luminance × area, two-sided); because selection is power-proportional and the point uniform, the area pdf at any sampled point is luminance / total power — no per-triangle lookup — which is what makes the reverse power-heuristic weight at a BSDF-found emissive hit computable from the hit alone (full weight at depth 0). Envelope: height (parallax), the detail pair, and decals stay raster-side (a traced hit reads the flat surface at its plain uv), layered lobes trace as their metallic-roughness base, thin-pane pass-through ignores frosting (roughness shapes its reflection only), and the shadow walk's tint skips the per-interface Fresnel. A time budget a frame (--pt-seconds S, PathTracing.secondsPerFrame; 0, the default, is none): the sample loop stops at the sample where the wall clock runs out (DispatchTime, measured from the first dispatch; the setup, the reuse, the filter, and the readbacks are outside it), sizing each dispatch to the time left from the wall seconds the last one took per sample (to the nearest sample; under half a sample's worth ends the frame), so a frame lands within about a sample of its budget rather than a dispatch (a dispatch is sized toward a second of GPU work). The frame the clock leaves is a fixed render of the count it reached, byte for byte: the kernel adds samples in order onto the running sums and reads nothing of the frame's total (pt.counts.x is the loop's bound alone), the composite divides a fixed render by the count reached (invSamples: 1 / reached), and under the stop the checks fall at the same indices either way, so a budgeted adaptive frame equals the stop at the reached count as the cap with the same minSamples and the same mean; TracedBudgetTests pins both against renders of the recorded count, plus the floor and the terms. --pt-min N is the floor the clock may not cut under (PathTracing.timeFloor: the named minimum, else one sample, clamped to the count; the dispatch stops at the floor so the clock is asked exactly there; the per-pixel stop's firstCheck is untouched). Under reuse the first frame of a run gets the history's worth of time (timeBudget(tracingTheHistory:): reusedFrames × seconds, since it traces that worth of samples), and a budgeted reusing sequence is not the same bytes twice (each frame's count follows the clock and a frame is a function of the frames before it), which the Docs say beside the recipe. The bare flag's cap under a budget is the detail tier's 4096 whatever --render-quality, since the clock decides and a final still's count is the ceiling a fast scene converges to first; a named count stays the cap. The report carries reachedSamplesPerPixel and secondsTraced; the recipe writes "seconds" and "reachedSamples" (a video's recipe drops the count, one frame's figure, as it drops meanSamples); the sequence and video drivers end with the counts reached (least, most, mean) and the mean seconds a frame against the budget; the progress line under a budget reads samples and seconds rather than a share of a count. Measured on the example scene (1080², M2, release build, launch to file): a fixed 128 took 29.5 s; --pt-seconds 10 traced 50 samples in 9.9 s (10.7 s in all) and --path-traced 50 rendered the same bytes; the same budget at --pt-depth 2 reached 93; with --pt-noise 0.04 the same 50 and nothing settled, since the first check under the bare cap of 4096 is 64 (--pt-min brings it forward, and the Docs say so); three frames at --pt-seconds 5 --pt-reuse 4 traced 101, 25, 25 samples in 20, 5, 5 s. PathTraceTests (furnace ×3 incl. glass, parity, texture parity, transparent + tinted shadow, the emissive floor pinned to the analytic small-emitter irradiance ±18% with a low-spp noise bound, determinism ×3, off-is-off, snapshot path-traced-3d; the maps probes: normal-map halves apart + raster parity, mr map byte-equal to its factors, occlusion black/white furnace exact, emissive map head-on to the analytic byte and through the light sampler to the scaled floor irradiance, triplanar trace-vs-raster parity — the flat-face probes stage on Mesh.plane, since Mesh.box carries no uvs and a uv-less mesh silently degrades every uv-gated map); example 3D/Effects/PathTraced; Docs/Output/PathTraced.md.
  • Slow motion in an export (SlowMotion in Sources/Ollin/Export/SlowMotion.swift; --slow-motion N [--made-frames], the slowMotion: argument on exportSequence / exportVideo / exportGIF, and --export-loop through the last two). The file keeps its own --fps while the run is covered by factor times as many frames, so the motion takes that much longer to watch; --seconds still counts the sketch's own time and --frames still counts what the file holds. Two forms. Drawn (the default) steps the clock factor times finer and draws every written frame: exact, any sketch, any factor above 1, and it costs the factor's worth of render time. Made (--made-frames) draws what it would have drawn anyway and fills each gap with the platform interpolator, so it needs a 3D scene under a perspective camera and does half speed only (one frame per gap, and MTLFX offers no phase to ask for). Motion measured in seconds slows under either form; motion measured in frames (a fixed step per draw(), no deltaTime) slows only under the made one, since a finer clock just hands it more frames. Load-bearing: the clock rate and the file rate are separate in renderFrames (SlowMotion.clockRate(playingAt:)), and the recipe's fps is the clock, so a still re-renders from it unchanged; "madeFrames":true is written only when the file really holds them; the refusal is asked after the first performDraw() (the camera cannot be known before) and exits before any file is written; a made frame is built after its render from the picture and depth that render kept (keepForMadeFrame / exportMadeFrame), which is why the export forces the depth resolve; newest moves only when a gap is consumed, so a warmup frame's keep is harmless. The measurement found a shipped bug in the live path (recorded under Frames between the drawn ones): the velocity fill returns the mover pass, not the field, and binding it as motion warped every frame that declared a mover. Measured 2026-08-29 against the same moments drawn for real: a made frame lands ~6x closer than either neighbor (mean 0.13 of 255 against 0.67…0.97), and costs about 11 ms against 20 ms for a drawn frame at 1080 square, so half speed took 1.9 s where drawing every frame took 2.4 s (3.3 s against 5.1 s at --render-scale 3). Limits: the first gap of a clip repeats its neighbor (one pair is not enough history, and six warm-up encodes buy exactly what three do); a flat overlay has no depth, so a caption over a moving scene can smear (the platform's uiTexture slot is unused); --replay is refused (a take carries one input frame per drawn frame); spatial video does not take it. SlowMotionTests (15: the finer clock against the still path, the made form's own drawn frames byte-exact, the closer-than-either-neighbor probe sabotage-verified red, the head-of-clip repeat, the flat-sketch refusal). No pixel snapshot (the interpolator is a stateful platform object). Docs/Output/Export.md#slow-motion; Guide Ch 41 § Slower than it happened: slow motion.
  • Motion blur from subframes (--subframes N [--shutter F], OllinApp.exportSubframes / exportShutter; SubframeShutter + the drives in Sources/Ollin/Export/Subframes.swift, the moment loop in renderFrames, foldSubframe in MetalRenderer+Effects.swift). The accumulation buffer: each written frame drawn at N moments across a shutter centered on the frame's instant (moment i at the middle of its share of the opening, as the canvas blur's streak centers) and averaged in linear light; any sketch, 2D and shaders included, at N times the render cost; applies to --export, --export-exr, --export-sequence, --export-video, --export-gif, --export-loop, --export-grid, --export-sweep, and combines with drawn slow motion (the shutter is then a share of the finer frame). Load-bearing: the fold sits in renderedFrame after the overlay, before the linear capture and the present (a single-precision running mean ping-ponged through ollin_fx_weighted_sum at 1/N; a moment before the last commits without presenting, so the one 8-bit quantization point stays put; an EXR gets the mean with the last moment's depth); every moment is a draw (ruled 2026-10-08): time steps through the opening, deltaTime is the time since the previous draw (a deltaTime sim stays exact), frameCount counts each draw as under --settle and drawn slow motion, so frame-counted motion runs N times as fast; a moment before time 0 is drawn at 0 (the first frame is half blurred); the shutter is exportShutter, else the sketch's last motionBlur(shutter:), else 0.5, read before each frame; the canvas motionBlur() stands down while a moment is drawn (motionBlurActive); a picture the GPU carries between draws (noClear, a feedback layer, an Accumulator/LineSpray, a simulation field, SSR history) is refused by name before anything is written (ruled 2026-10-08), as are --settle, --made-frames, and a take; a held (noLoop) frame renders once as it is; the recipe writes "subframes" and "shutter". SubframeExportTests (9: the analytic streak column by column and its light within 0.2%, a flat shape's light, a still picture byte-identical, the clock of every moment, time 0, the sketch's shutter, the stand-down, the refusal, the recipe; five plants each red). Docs/Output/Export.md#motion-blur-from-subframes; Guide Ch 41 § Slower than it happened: slow motion.
  • Live recording (performance capture) (Sources/Ollin/Export/SessionRecorder.swift + Core/CaptureAudioSource.swift). Records a live run in real time — frames as rendered, the sketch's sound as played — into one movie: startRecording(to:audio:) / stopRecording() / isRecording sugar over the public SessionRecorder extension (audio: .sketch default / .microphone / .sketchAndMicrophone / .none; codec: reuses VideoCodec, h264 default; default file ~/Movies/Ollin/<Sketch> <stamp>.mov); hosts wire ⌘⇧R (OllinLiveCoding stage chip + saved toast; OllinLive menu) and OllinLive takes --record (film from the first frame). Video uses the CGImage frame-grab hook (wantsRenderedFrame, armed only while recording; the frame is the one the window shows, read off the on-screen render, so a take costs one tone-map pass and a copy per frame and never a second render) into AVAssetWriter's real-time mode with host-clock PTS (a slow frame lasts longer; a frame the encoder can't take is dropped, never awaited, and counted). Audio: the core CaptureAudioSource seam (Mirror-discovered off the sketch's stored properties like ExportAudioSource; Synth/AudioPlayer/Tone conform — a node allows one tap per bus, so capture shares installAnalyzerTap's fan-out via CaptureTapRelay, never a second tap) feeds per-source AudioCaptureSink lanes mixed on the writer queue every 100 ms, 150 ms behind the clock, with the offline tanh knee. Load-bearing: a lane places buffers by host-time stamp with a running counter — the counter wins under 50 ms jitter, the stamp wins past it — so a device gap becomes silence, never a slide (the A/V drift killer on long takes); the audio track is declared whenever mode ≠ .none, so a late-entering instrument lands mid-file in sync; a recording survives a live-reload swap (reload(to:) carries sketch.sessionRecorder to the new instance, whose setup hook rescans its instruments; hosts must address the current sketch via SketchSession.currentSketch — core.sketch deliberately stays the first mount, and a recorder wired to a stale instance films nothing); a canvas-size change finishes the file cleanly (a movie's frames are all one size); quit, Control-C, and SIGTERM finalize via stopAndWait() (an unfinished writer is an unplayable file; the recorder installs its own DispatchSource signal handlers once, kept for the process's life); .extended refuses (HDR stays offline), .wide tags P3; a VideoPlayer soundtrack doesn't reach a take yet (its tap carries track time, not host time); headless (isRenderingHeadless) refuses with a pointer at --export-video. Verified live: a 20 s take at a solid 60 fps with sound in every 5 s window across a mid-take hot reload, finalized by SIGTERM. AudioCaptureSinkTests (6, the timeline placement) + SessionRecorderWriterTests (2, a real writer, no audio hardware — CI-safe on purpose); example Export/Record. Docs/Output/Recording.md; Guide Ch 43 § Keeping the take.
  • Per-frame profiler (Sources/Ollin/Core/FrameProfile.swift + Renderer/FrameCapture.swift). The "why is my sketch slow" tier over the shipped FrameStats/extension-seam machinery: a public FrameProfile is a property of FrameInfo and FrameStats, carrying the four-way time split (cpuDrawMS / cpuEncodeMS / gpuMS / waitMS), the submitted work (drawCalls / passes / computeDispatches / batches), and the geometry each path drew (triangle / fringe / SDF instance / field quad / image / glyph / mesh / point splat / particle / clip). Load-bearing: the counts are taken in the encode loop beside each drawPrimitives, never derived from drawer.batches, so a batch the renderer skipped (an image whose texture failed to build) is not counted, and FrameProfileTests pins that difference; every render encoder in the renderer goes through the one countedEncoder(_:_:caller:) choke point (a #function-defaulted label, so a new shadow / probe / filter pass counts itself and can name itself), which is why the pass number stays honest as passes are added; the wait is timed apart from the encode (the frame-time readout's own lesson: timing across the triple-buffer semaphore measures vsync and says nothing), so waitMS is reported as headroom rather than folded into CPU cost; GPU time comes from the command buffer's own gpuStartTime/gpuEndTime in the completed handler, which runs off the main actor, so it passes through an OSAllocatedUnfairLock and reads the timestamps into a local before the lock's closure (MTLCommandBuffer is not Sendable) and lands one or two frames late, hidden by the smoothing SketchView applies to all three measured times; headless renderedFrame resets the counts too, so an export and a test read the same profile. UI: a cost tier on the shared MonitorCardView (so all three hosts and the detached panel get it at once) in the Photos-info-window idiom the card already follows — cells carry facts, the bars carry time, and no number appears twice: one cell grid holds FPS · Canvas, one cell per drawing path in use (SDF / tri / point / particle counts), then the Draws / Passes / Batches counts (per-path breakdown in their tooltips), filling hairline-separated rows four at a time with the last row taking the remainder (eight cells run 4+4); a path's cell stays once it has appeared, so a particle count dipping to zero for a frame doesn't reshuffle the grid, and the set clears when the frame counter restarts on a reload; under the grid, the two bars on one scale (the frame period), deliberately not stacked (the CPU builds frame N while the GPU draws N-1, so the two overlap rather than add) are the only place milliseconds appear (a grid CPU cell would repeat the bar's number, so there is none). GPU frame capture: Sketch.captureGPUFrame(to:) and View ▸ Capture GPU Frame (⌘⇧G, a one-shot OllinHUD.captureFrameKey the runner takes and clears) wrap the frame that asked (the request is taken between performDraw and render) in an MTLCaptureManager .gpuTraceDocument, and print the frame's pass list beside the path; Metal refuses to capture unless the process was launched with MTL_CAPTURE_ENABLED=1, so the refusal prints the exact line to run rather than failing silently. FrameProfileTests (8, incl. the one-time first-frame setup passes, which are real and stay counted); no pixel snapshot (the output is numbers). Docs/Tools/Profiling.md; Guide Ch 19 § Which half is slow: the cost row.
  • Dragging a shape into place (Sources/Ollin/Live/SourcePick.swift + OllinRuntime/SourceEdit.swift + OllinRuntime/ShapeDrag.swift). In both live hosts, Command-drag moves, resizes, or turns the shape under the pointer by rewriting the numbers that placed, sized, or aimed it, in the sketch's own source, and ⌘]/⌘[ put it in front of or behind its neighbor by moving its line; under OllinLive the edit goes to the file and the watcher reloads it like any other save, and on the OllinLiveCoding stage it goes to the editor buffer, which the host then evaluates, so nothing is held in the window that the text does not say. Three parts: a SourceSite (#fileID / #line / #column default arguments on ~74 Sketch shape methods, plus a SourceMove naming which arguments carry the position (.scalars for bare numbers, .points for Vector2 ones), the size (.uniform for sizes that scale together, .axes for a width and a height), the anchor the position numbers stand at (.center / .corner / .apex), and any angle arguments); the drawer's per-frame pick list (recordSourcePick from appendSDF and drawLine, cleared in beginFrame) read back by Sketch.sourcePick(at:); and the rewriter (SourceEdit.moving / .resizing / .turning / .planningMove). All of it is package, so it is host machinery rather than sketch API. Load-bearing: #column is the call's ( and columns are UTF-8 bytes (measured, not assumed), so the whole scan works on Array(source.utf8) and an accented comment ahead of the call cannot shift the edit by a byte; the site is matched by file name, never path (a live compile builds from a copy under the same name, the rule diagnostics already carry); every edit replaces the text of a number already there and edits apply back to front, so spacing, comments, and line breaks survive (a call spread over lines is one call); a whole number stays whole (a drag under half a point writes nothing, which is what keeps hand-placed shapes on the grid) and a fraction keeps the author's digits; clearSite() on return is not optional (an unmarked draw, e.g. text, drawShape, the plural helpers, would inherit the previous line and pick as the wrong one); the drag is converted through the inverse CTM's linear part, so a shape under scale(2) writes half the drag; regions are shape-aware (ellipse / box / apex-up wedge / capsule, a line's grab radius at least 5 points) and the topmost is the last drawn (sabotage-verified); the outline and its handles are a SwiftUI sibling of the canvas, never ink, so they cannot reach an export; a refusal names what stands where the number would be (width / 2, cx) instead of failing silently, and a computed argument leaves every number alone. A resize changes the size numbers only and holds the position numbers still, so a shape grows from wherever its call says it stands, which is what the anchor is for: a corner-placed rectangle offers three handles because the fourth sits on its own anchor and can scale nothing, and a corner on the anchor's row or column moves one side only (the right edge is where a width is dragged). The factor is a ratio, not a distance (a corner's offset from the anchor, projected along itself for a uniform size), so it reads the same at any scale, and it is floored at 0.05 so a corner pulled through the middle leaves something to grab again; a corner radius and a count (sides:, points:) are not sizes and stay as written. A turn swings a shape's points about the centroid of those points (computed from the file's own numbers, never the pick region, so a multi-target call like drawArrow still turns about its centerline) or moves its angle arguments (an arc's start:/stop:), and a shape with one point and no angle offers no knob at all, because its direction lives in a rotate this does not touch; an angle is written to at least three decimals (a whole radian is most of a quarter turn) and a turn never writes -0 (the sine of a half turn is not quite nothing). Handles are canvas-space: corners on the outline, the turn knob 26 points clear above the top edge, grabbed within 11; hovering a handle keeps the shape it belongs to (a handle stands outside its shape, so a fresh pick there finds nothing and would clear the very outline that put it on screen). A coordinate that is a bare name adjusts a parameter instead (SourceEdit.planningMove reports names rather than refusing, so a mixed call writes its number and adjusts its parameter): the host matches a Param<Double> of that name whose value equals the coordinate the frame drew with (SourcePick.placedAt, single-point shapes only, which is what keeps a local variable shadowing a parameter's name from moving the wrong thing), writes it through the control the inspector row uses (so it clamps to the row's range), and records it as tuned, so nothing recompiles and the value survives the next reload. Tracking is off unless the modifier is held: cost with it off is one branch per draw call, measured at +3.96 ms per 100k circles in a debug build (57.47 against 53.51, an A/B against the same tree without the arguments), which is the argument passing itself, not the branch. Reordering by key (OllinRuntime/SourceReorder.swift, SourceEdit.reordering(_:line:column:by:) over SourceReorderStep .forward/.backward/.toFront/.toBack): with a shape outlined, ⌘] / ⌘[ move its line past the neighboring draw call's in its own block, Shift all the way, taken in OllinMTKView.performKeyEquivalent before any menu and only while a host is installed and answers reorderHovered. The ink travels: the block is read as items (a draw call spanning its lines, an ink verb with its kind, blank, comment, or an opaque nested {…}/other statement); the ink kinds the neighbor changed (fill/noFill one kind, stroke/noStroke one, strokeWeight, strokeCap, strokeJoin, strokeAlign, hollow/solid, blendMode) are restated for the shape where it lands (from the ink in force at it, or the drawer's own start for fill/stroke/strokeWeight: fill(.white), stroke(.black), strokeWeight(1)), put back after it only where a later shape uses them before they are set again, and an ink line left with nothing to color is removed (its kind re-set before any shape), plus a post-pass that drops an ink line saying what is already in force within the stretch the move touched (a reverse move then leaves the file byte-identical, pinned); a shape under noStroke() carries no stroke details; a comment directly above the shape moves with it. Refusals name the line: .blockedBy(statement:) for anything between that is not ink (a translate, a let, a nested block; a move to the front stops there after ≥1 step and reports stoppedBy), .inkUnknown(statement:) for a kind nobody can read off the block, .notAlone(line:) for a call sharing its line, .alreadyAtTheEdge at either end. One controller, two hosts: ShapeDragController lives in OllinRuntime over a ShapeDragHost that names its source, reads it, writes it, and records a parameter (LiveSession reads and writes the file; PerformanceSession the buffer). On the stage, writeSource is replaceBuffer (an ordinary edit, so it marks the document and one ⌘Z takes it back) followed by evaluate() (the clock carries), and readSource refuses by sentence whenever the buffer is not the text the stage was built from (sourceOnStage, set on each successful evaluation): typed since ⌘↩ asks for an evaluation, and mid-compile asks to wait, because a site names a line of the evaluated text and a drag during a compile would add to numbers the stage has not shown yet. The modifier reaches the canvas through a local .flagsChanged monitor in OllinMTKView (installed beside the pressure monitor, key window only, the flagsChanged override standing in only where no monitor is up), since on the stage the editor holds the keys and the override never fires; a modified press is taken before the canvas claims first responder, so a drag never takes the keys from the editor; the editor stops taking hits while the drag isArmed, which is what lets the pointer through the text to the stage; and the overlay sits above the code, measured against the letterboxed frame (onGeometryChange) and the stage's own canvas (stageCanvas, updated per evaluation, so a sketch that changes its canvas keeps the outline true). Gates: SourceReorderTests (18, pure), SourceEditTests (38, pure text, in OllinRuntimeTests; the negative-zero rule verified red by sabotage), SourcePickTests (16), and swift run OllinLive --dragtest, which compiles a sketch, picks its circle, writes the file, recompiles, finds the shape where the drag left it, resizes it by a corner, swings a line about its middle, drives the move, the corner, and the parameter-name drag through ShapeDragController against a stub host, and brings a covered circle forward by the key, recompiles, and finds it on top at the overlap (the one proof text cannot make), then sends it back to the file as written, and swift run OllinLiveCoding --dragtest, the buffer half: the drag writes the buffer and evaluates it while the file on disk stays as written, the shape is found where the drag left it, a drag mid-compile and a drag over typed-and-unevaluated code are each refused by their sentence and leave the buffer alone, a parameter's name sets the parameter with no evaluation, and the bracket key reorders through the buffer and back. Example Live/DragToEdit; no pixel snapshot (the output is a file). Docs/Tools/DragToEdit.md (§ On the performance stage), Docs/Tools/LiveCoding.md; Guide Ch 1 § Moving something by hand, Ch 43 § Moving a shape on stage: the Command-drag.
  • The performance host on a controller (Sources/OllinLiveCoding/PerformanceControls.swift + ControlsCardView.swift + ControlTest.swift). OllinLiveCoding's own actions (evaluate, evaluate fresh, hide the code, the code's backdrop and size, record) answer to MIDI and OSC beside the sketch-level @Param bindings, as a host preference, never the sketch's. Load-bearing: the host polls both satellites' inboxes (MIDIInput.messages() / OSCReceiver.messages()) on a 60 Hz main-thread timer, so no host action ever runs on a Core MIDI or Network callback thread; OSC has a fixed namespace (/ollin/evaluate, /ollin/evaluate/fresh, /ollin/code/hidden, /ollin/code/backdrop, /ollin/code/size, /ollin/record) live whenever the port (ollin.livecoding.oscPort, 0 = off) is set, while MIDI is learned per action (learn(_:): the next note, controller, or OSC message binds; a note is refused for a fader's action; one control serves one action, so a control learned twice moves; the map persists as JSON under ollin.livecoding.controls); a note fires once per press on its channel, a controller button fires on the rise through 64 (its release ignored), a fader maps 0…127 onto the backdrop's 0…1 or the size's 9…32; over OSC a bare message turns a toggle over, a value sets it, and a button's 0 release is ignored. The view settings are written to the same defaults the @AppStorage views read, which is what moves the menu's picker with a fader. OllinInspector.Palette went public for the card. Gate: OllinLiveCoding --controltest (learning, MIDI matching, the fixed addresses, persistence, a UDP loopback, and a virtual-source MIDI loopback that is passed over where Core MIDI is absent). Docs/Tools/LiveCoding.md § The host on a controller; Guide Ch 43 § The host on a controller: OSC and learned MIDI.
  • Code completion at the caret (Sources/OllinRuntime/CodeCompleter.swift + Sources/COllinSourceKit + Sources/OllinLiveCoding/CodeCompletion.swift). The performance host completes names from the Swift toolchain's own completion service, reached at runtime: CodeCompleter.load() finds the two frameworks beside swiftc, opens the out-of-process one first (a crash there comes back as a refused request, never as the stage going down) and the in-process one as the fallback, and binds each entry point by name over Ollin's own C header (COllinSourceKit: the handles, the one struct that crosses by value, a typedef per call; the toolchain ships no header, and nothing is linked, so a toolchain without the service loses completion and nothing else). A completion is a session at one point in the text: open(_:source:arguments:filter:limit:) at the start of the word (or right after a member dot) checks the buffer once and answers the first list, update(_:filter:limit:) filters the same session with more of the word typed (about a millisecond), close(_:) ends it; the rows keep the service's own order (an exact and a short match first, the buffer's own class before a global), matched by prefix, never fuzzily (a fuzzy match on a word with no real match dredges the longest system constants up from the bottom of the namespace; an empty list is the honest answer on a stage), underscored names hidden, not_recommended rows dropped. The arguments come from SketchLoader.completionArguments(sourceFile:): the compile's own module search paths plus what the service does not infer (-sdk, -parse-as-library, and a module name that is never the file's own, since a module called Sketch shadows the class every sketch extends). Load-bearing: offsets cross as UTF-8 bytes while the editor counts UTF-16 (CompletionText.utf8Offset/utf16Offset); the word start is scanned back over identifier characters and a run that starts with a digit is a number, not a word; a dot triggers a member list after a name or a bracket, never after a digit or another dot (0.5, ..<); the service's <#T##display##type#> placeholders are reduced to the editor form <#display#> (the compiler recognizes it and names its line when one is left behind; Tab finds the next, wrapping); every request runs on the controller's own serial queue, never the main thread and never the cooperative pool, tagged with a generation so an answer for a caret that has moved on is dropped; the first request costs seconds while the service reads the framework's modules, so the host warms it up at launch, off the main thread. In the host the list is a subview of the editor's own container, never a window, so it goes into fullscreen with the editor and hides with the code; Escape opens it and closes it (and so never leaves fullscreen), Up and Down move, Return and Tab take the row, Tab walks the placeholders when no list is open, and the text view's own predictive completion is off. View ▸ Complete as You Type is the preference; Sketch ▸ Complete Name (⌥Esc) opens it regardless. Gates: CodeCompletionTests (10, pure text: word starts, member dots, placeholders, offsets, kinds, the arguments) and the completion phase of OllinLiveCoding --selftest (drawCi offers the positional drawCircle as drawCircle(<#x: Double#>, <#y: Double#>, <#radius: Double#>) beside its labeled overload, narrowing keeps drawCircles, Color. filtered by re offers red typed Color). Docs/Tools/LiveCoding.md § Completing a name; Guide Ch 43 § Finishing a name as you type: completion.
  • Two-speed evaluation (SketchSession.landsPlainBuildFirst, on in OllinLiveCoding, --single-build turns it off; OllinLive stays single-build). An evaluation under a loader that optimizes starts both compiles at once, plain and optimized, from one source snapshot: the plain build lands first with the clock as asked and is the one landing the host hears about (onSuccess once; reloadCount and lastBuildSeconds record it), then the optimized build swaps in silently through the same landing path with the clock always carried and the tuned parameters re-applied as at any swap; isOptimizing is up between the two, which the performance host's chip shows as a muted "Optimizing…" (InspectorStatus.optimizing). Load-bearing: one task owns both builds, so a newer evaluation's cancel refuses both of the older one's and an older optimized build can never land over newer code (pinned by starting the newer evaluation from inside the older one's landing hook); a plain build that fails fails the evaluation at once and the optimized result of the same text is refused, never reported twice; an optimized build that fails on its own leaves the plain build on stage and prints; with no runner yet (the first mount) the second build replaces sketch, which is what lets the headless gates see both. The second swap does to the run whatever the first did (the same keepRun, since it is the same code): an evaluation that carries the run carries it across both, and one that starts it over runs setup() and reloaded() twice a fraction of a second apart, which is the flag's remaining reason. What it buys is the optimizer's share of the compile, measured on the M2 with -wmo (medians of three, evaluation to swap): 20 lines 0.63 s plain / 0.68 s optimized against 0.60 s for the single build; 150 lines 0.68 / 0.84 against 0.76; 470 lines 0.78 / 1.16 against 1.09; the two compiles together cost each about 0.07 s. The pixel pin compares decoded pixels, never PNG bytes: the export recipe carries the @Param values and the gate's sketch reads its build level into one on purpose, so the files differ while the pictures (with the sketch's own sine and cosine arithmetic) are identical, and the optimizer does not fuse multiply-adds (probed, 0 of 100,000 differing). Gates: OllinLiveCoding --selftest (both landings timed against the single build, the pixel pin, the single-build control) and --sessiontest (every carry under two-speed, supersede against both builds, the clean failure). Docs/Tools/LiveCoding.md#the-evaluate-loop; Guide Ch 43 § Performing the code itself: live coding.
  • Evaluating what changed (Sources/OllinRuntime/SourceRegions.swift + Sketch.carryRun(from:) in Sources/Ollin/Core/RunCarry.swift + SketchRunner.reload(to:keepClock:keepRun:) + PerformanceSession.evaluate). The performance host reads each evaluation against the text the stage was built from, and when the edit moved nothing but method bodies the run underneath carries on instead of starting over: setup() does not run again, the canvas keeps what is piled on it, and the drawing state, the random streams and the @Saved properties come across, so a long exposure keeps its ink and a piece mid-set does not go back to its first frame. What per-block evaluation can honestly mean in a compiled language: the whole buffer still compiles and the whole instance is still replaced (the code is in a new dylib, so no method can be swapped on a live object); what is per-block is the decision and the editor's pulse, which lights the declaration the caret stood in. Load-bearing: the carry is carryRun(from:), and it adopts the running Drawer whole rather than copying fields out of it (a drawer holds no reference back to its sketch, so the accumulated canvas, the style, the fonts, the images and the retained batches all come across, including anything added later) beside the generators, view2D, isLooping, the @Saved set matched by name (each property its own success), and the two things setup() registered rather than computed: the extensions the sketch installed for itself and the automation it wrote (adopted without doubling what the runner re-attaches, and the fresh instance seeds the pointer in the same branch that fires reloaded(), or a cursor-following sketch reads (0, 0) until the mouse next moves); the decision is the host's, never the session's, because only the host has both texts. SourceRegions is a bracket-counting line scan, not a parser (comments and bare attribute or modifier lines join the declaration under them; braces inside strings, raw literals and comments are skipped), comparing header-for-header with whitespace collapsed and comments dropped, so a reworded note or a rewrapped signature is the same declaration. It refuses to carry the run whenever a declaration changed, when setup() or anything reachable from it by a fixed-point name walk changed, and when setup() names a stored property that is not @Saved (the Cracks shape: state built in setup() would come back nil and the sketch would draw nothing or crash, so a restart is the safe answer and the failure mode of being wrong the other way is one unnecessary restart). A carried swap fires reloaded() from the no-setup branch of the draw loop, and hands the pause back to a noLoop() sketch through pauseHoldover (the flag the camera snap already used, renamed for the second caller: it is the same "un-paused for one piece of work" idea). Both swaps of a two-speed evaluation carry it, or the silent second one would restart the piece a fraction of a second later. --single-build is unchanged. Gates: SourceRegionsTests (17, text alone), OllinLiveCoding --selftest (the block under the caret at five positions, the three edit kinds, and the runner A/B where a carried swap shows six accumulated bars and a restart shows one; verified red by sabotage), --sessiontest (supersede with a runner attached, against both a carrying and a restarting evaluation, and the two-speed carry; verified red by sabotage). Docs/Tools/LiveCoding.md#what-the-edit-changed; Guide Ch 43 § Performing the code itself: live coding.
  • Parameters written back into the sketch (Sources/OllinRuntime/ParamWrite.swift + ParamSaveAction on the shared ParametersListView + LiveSession.saveParams / PerformanceSession.saveParamsIntoBuffer). The other half of the shape drag: the values you turned in the inspector go into the @Param lines that declared them, so a good set outlives the run instead of being typed back by hand. The destination is the source and never a preset file, and the method is SourceEdit's byte scanner pointed at a property's default rather than a call's arguments. Load-bearing: only the parameters the host recorded as turned are written (SketchSession.tunedParams), each carrying what it holds right now, so a parameter turned by hand and then moved by a fader saves where the fader left it; the written names are dropped from the tuned store (forgetTunedParams) the moment they stand in the text, or the stale tuned value keeps winning and the next hand edit of that default reads as ignored; declarations are found by walking forward from each @Param attribute (past its own parentheses and any modifiers to var <name>) rather than back from a var, because forward is where a string or a comment can be skipped safely, and two parameters of one name in a file are refused as ambiguous rather than guessed; the hunt for the = stops at the end of the line, so a declaration carrying no default cannot reach forward and take the next statement's value. Two rules keep the written file compiling: a Double default written whole is a Double only by the range beside it, so a fractional value keeps its point (86.0, never 86), and a ClosedRange writes both ends with a point (40...50 reads as Int); a whole value under a whole default stays whole, so a round number reads as its author wrote it; and a call around one literal keeps its wrapper (Opacity(0.5) becomes Opacity(0.75), Tempo(120) becomes Tempo(96), the form a type of your own takes, for the number / bool / text payloads), since the bare literal put over the whole call compiles only for a type that takes a literal. What it refuses it names in one sentence under the button: a default that is worked out (side / 3, houseRadius, and arithmetic over numbers alone, 600.0 / 2, which is a sum the author meant), a parameter declared in another file, a choice with no name in code (a closure-built Easing carries a serial), a call around more than one value (Tempo(120, beatsPerBar: 3), which used to be written as the bare 120 and silently drop the bar). A color writes to four decimals, finer than one 8-bit channel step; an option keeps the spelling it was written with (.dots or Style.dots). In OllinLive the button writes the file and the watcher reloads it like any other save; in OllinLiveCoding it writes the editor buffer through EditorController.replaceBuffer (undo, caret, and scroll kept, the document marked dirty), because there the buffer is the truth and ⌘S is the only thing that touches the disk. ParamWriteTests (34, pure text; the fraction rule and the string skip each verified red by sabotage) and swift run OllinLive --savetest, which compiles a sketch, turns nine kinds of parameter, writes them, recompiles, and reads them back off a fresh sketch with nothing restored into it; no pixel snapshot (the output is a file). Docs/Helpers/Parameters.md § Saving what you turned; Guide Ch 1 § Saving the values you tuned.
  • Putting a parameter back (Param.declaredValue / Param.reset() / AnyParam.reset() in the core, Sketch.resetParameters(named:) with the package parametersReset hook, SketchSession.resetParams(_:) + the observable tunedNames, ParamResetAction on the shared ParametersListView, RemoteReset on the remote wire). A tuned parameter goes back to the value its @Param line declares without quitting the host. The declared value is kept beside the current one, taken once from the clamped wrappedValue at init, and a reset jumps to it the way restore(_:) does (a smoothed parameter never glides back). The sketch does the resetting and tells the host which names went back (resetParameters(named:) fires parametersReset), one route for the Mac inspector, the remote surface, and a sketch's own call, so the session's tuned store follows every one of them: the names leave paramValues (and tunedNames, kept observable where the values are not, so the marks move), the row stops reading as tuned, the save button skips it, and the next reload takes the file's value. Nothing else moves: no reload, no setup(), the same sketch instance, so the clock, the seed, and an accumulating canvas carry on, a value only setup() read keeps what setup() built from it, and an automation track wins its parameter again on the next frame. Inspector: a 5-pt accent dot after the name on tuned rows only (a click resets that row), Reset in every row's context menu (the keyframe diamond keeps its own menu, since the nearer one wins under the pointer), Reset on a group header once one of its rows is turned, and Reset all beside the save button (every parameter, tuned or moved by a wire); the action reaches the twelve row kinds through a private environment value, so no row kind threads it. The detached panel and the gallery, which keep no tuned record, show no marks and offer no reset. The remote page's Reset all sends {"kind":"reset"} (names for a few), queued and applied at the frame boundary like a set. Command-Z and Shift-Command-Z stop at the canvas (OllinMTKView.isUndoKey): a host with nothing to undo left the pair unclaimed and AppKit handed it to the first responder, so a sketch's keyPressed() read a plain z (the material explorer advanced its preset under an undo the inspector never offered); other Command combinations still reach the sketch. ParamTests (+2), ParamResetTests, UndoKeyTests, RemoteWireTests (+1), and two --sessiontest phases (tune, reset, the name gone, the run unmoved, a reload after a reset taking the edited file's value, reset all; and under a runner, a reset under a track with the frame and the clock held and the track winning back). Docs/Helpers/Parameters.md § Putting a parameter back; Guide Ch 1 § Saving the values you tuned.
  • The generated API reference (.spi.yml + Sources/Ollin/Ollin.docc/Ollin.md + Scripts/api-docs.sh). Every public symbol of the 22 library modules as a DocC page, written from the source itself and hosted by the Swift Package Index for the modules .spi.yml names. The written Docs/ pages stay the curated reference and this is the completeness net under them. Load-bearing: the catalog is the core's landing page and curates all 470 of its top-level names into 18 groups, because uncurated DocC sorts 4,618 declarations by kind and the page reads as a wall; no DocC plugin is a dependency (the index injects its own, and a plugin dependency is resolved by everybody who depends on Ollin), so the script drives swift package dump-symbol-graph plus docc convert directly, and it is that one dump rather than swift build --target X -Xswiftc -emit-symbol-graph, since a warm build recompiles nothing and emits no graph at all. .spi.yml has a 1,500 byte ceiling (SPIManifest's own maxByteSize), so it carries the target list and nothing else. The script fails on a warning, and on a library product missing from .spi.yml (which is how the two stay in step, and it caught OllinSamplePhotos the first time it ran), which is what makes it the only gate here that reads a doc comment: it found 40 on its first run, every one invisible to every other gate (a link at a symbol that had gone, and parameters still documented under the names the naming sweep dropped, intensity for amount, rotation for angle, strength for amount). CoreGraphics is re-exported, so the standard math functions appear in the generated surface and the landing page says why. In Scripts/preflight.sh on a framework change, about a minute and a half warm; no pixel snapshot (the output is pages).
  • Names in the prose are real (Scripts/check-names.sh + Scripts/check-names-allow.txt, on every preflight run; ~2 s). The gate for the defect the 2026-09-12 review found by hand: a rename sweeps the code and leaves the prose behind, and nothing else reads an inline backtick (the snippets never used the name, the links still resolve, a grep still hits the prose itself). It reads every code span in Docs/, Guide/, the root README and this file, with fenced blocks set aside, against an oracle of every identifier in every tracked code file, every tracked path and its components, and the public surface under API/; a page's own fenced Swift widens the oracle by the names it defines, a fenced shell or text block by every word. Six checks: (1) every identifier in a span exists somewhere in the oracle (arguments inside parentheses, quoted strings, hex colors, and My…/Your… placeholders are the reader's; a trailing-underscore or glob prefix matches by prefix); (2) a call written with labels (.bloom(threshold:amount:radius:)) matches a public overload by name and by label in order, with omissions allowed and a variadic taking a run, Type.name looked up on the type when Ollin declares it and left alone when the platform does (String(describing:)), and the contributor files also allowed an internal declaration from Sources/; (3) a flag such as --export-exr is spelled by a host, a script, or a workflow (the extensionless wrappers included); (4) a repository path exists, a Group/Name names an example at either level of the two-level catalog, and a .md#anchor lands on a heading or an explicit <a id>, for backticked spans and for the bare Docs//Guide/ pointers in this file, CLAUDE.md, DESIGN-NOTES.md, ROADMAP.md, ARCHITECTURE.md, AGENTS.md and the Guide's PLAN and AUTHORING pages, none of which check-links reads; (5) Appendix D's Contents line lists its sections in order; (6) every bullet here that names a Docs/ page is findable in Appendix D by a stemmed word of its name (a bullet with no Docs/ page is internal and only noted). What it cannot see: a stale name whose spelling survives as an unrelated identifier (physarum(agents:) after the facade became makePhysarum, while physarum lives on as a variable) and a call written without labels, which is why prose about a call writes the selector with its labels. The allowlist holds foreign names only, one per line under its class (p5, Processing, openFrameworks and OPENRNDR outside the comparison tables of Appendices C and E, whose foreign first column is skipped by construction; Apple types the code never spells as an identifier; tools and shells; techniques cited by another library's spelling; notation): a name that used to be Ollin's is never allowed, the prose is fixed. First run found 70-odd stale names (intensity/strength where the label is amount, the minimum…/maximum… family after the min/max rule, the make… facades, addStaticColliders for addStaticBodies, a frameRendered hook written without its image: label, a mesh loader written without its withExtension: label) and 31 hand-written anchors in this file that resolve only through the <a id> tags the Docs pages carry. Sabotage-verified red on a stale selector, a misspelled identifier, a dead anchor, and a dead flag planted in one page, all four reported by line. No tests of its own beyond that; --list prints every note.
  • The surface holds its names (Scripts/check-api-names.sh + Scripts/check-api-names-allow.txt, on every preflight run; under a second). The naming pass's settled rules as a lint over the listings under API/: limits are min…/max…; intensity only where the type or name says light; a resource-making method is make… and a value-type dot static stays bare; a scalar Bool property reads as a third-person assertion or as is…/has…/wants…; advance(by:) against step(_:); a loader spells withExtension: and its in: bundle takes no default; a public seed: is Int (a Filter/Generator/Sim shader seed stays Double, a generator's state word UInt64); a highlight exponent ends in Sharpness (specularSharpness); Sketch hooks are past tense, its geometry calls carry draw, its stack is pushState/popState; knob never appears; the value vocabulary is number/int/text/bool; a destination is a String path (a URL overload beside the String form passes); start() reports isRunning, connect() isConnected and is undone by disconnect(); a Boolean label is a participle or an assertion, never a bare order (inverted:, not invert:); update… is not a house verb (a per-frame advance is advance(by:)/advance(to:), a discrete run is step…); a no-argument listing of what the system has is available…(). Load-bearing: it reads the listings, never the tree, so it sees exactly what api-surface.sh --record recorded; an allow entry that matches nothing fails, so the list cannot outlive its exceptions; a name that is Ollin's own drift is renamed (pre-1.0, no shim, named in the changelog), never listed. Its first run found 53 (ten minimum…/maximum… names, twenty-four UInt64 seeds, seven bare-adjective Booleans, five URL-only writers, an extension: label, two intensity effect amounts, a timeline step with a labeled count, two ping-pong advance()s, a Syphon isActive/stop()), all renamed the same day. The read-for-meaning pass the lint cannot do runs beside it (2026-09-16, the whole surface read against its own doc comments): it found twelve GPU sims spelled update… whose every doc comment began "Step … one frame", a set(_:) on four smoothed values whose act is jumping (assignment already sets, and glides), a ring(innerRadius:outerRadius:) that returns a random point and collides in spelling with the SDF.ring that is one, an index: for a refractive index beside the ior: every other surface spells, five bare-order Boolean labels, a strength fading an effect, two Core MIDI listings and a screen-capture one that ask the system on every call, and one Easing label spelled two ways. Those three classes became rules here, so they cannot come back. The second such pass read the satellites (2026-09-19, module by module rather than whole-surface, since the first had read the core): twenty-nine names moved, and the classes it found are ones a lint over a listing cannot see at all, because each needs the implementation read beside the name. A magnitude named for the wrong axis: a hearing range over what is a distance, a bare scatter beside positionJitter and pitchSpread, a wavetable's length over one cycle's samples, now hearingDistance, timingJitter and samplesPerCycle. A word the same module already spends on something else (labels for scored results where a vocabulary is labels two types away, note for a rounded Pitch where Note is a type, spin for an angle where the framework's spin is a rate). A name that asserts more than the value holds: an isConnected reporting that points are playing, a standard lookup answering for uncatalogued values too, an isValid checking a filter rather than a topic, a confidence over a softmax share that sums to one across a set, now isPlaying, known(for:), isValidFilter(_:) and share(of:). A unit that lives only in a doc comment: a sampling interval taken as milliseconds in an Int, now sampleEvery(_ seconds: Double). And a loose match under the exact-match label, which the lint cannot judge because both spellings are legal: a Bluetooth device and a window were both found by substring under a named:-shaped label, and both now take matching:. Its other half settled how a failure is reported (the rule is in CLAUDE.md), which no listing shows either, since a signature cannot say what a body does with its failure; the violations it fixed and the ones it left are named in the changelog and under Developer experience in DESIGN-NOTES.md. No pixel snapshot (the output is text). API/README.md.
  • The surface reads as a version (Scripts/api-diff.sh + Scripts/api-diff.py, on every preflight run with a framework or API/ change; pure text, under a second). The listings under API/ at two points (the last tag against the tree by default; --from/--to any two refs) read as what changed, what it costs, and whether the shim rule holds. Load-bearing: a line is keyed by its container's kind and name, never its head line, so a type that gains a conformance does not move every member under it; a removal is paired with an addition in the same container before either is reported alone, surest first (the same name with other labels or types is changed, the same types under another name renamed, the leftovers paired on the name alone, one inside the other or mostly alike, and said to be likely on their line, since pairing whole lines matched any two short declarations on their keyword); a removed type folds its members into itself and a removed type whose members reappear under another name is a renamed type; a member whose text, with the type's name read as Self, sits under a protocol the type now conforms to is moved, not removed, the protocol the type gained preferred when several supply it (the 0.8.0 Vector protocol: 24 lines a plain diff calls removed); a member a standard-library protocol still supplies is moved too, found in the STANDARD table in api-diff.py (what Sequence, the Collection family, Equatable, Hashable, Comparable, Identifiable, CustomStringConvertible and the string literals give a conforming type), read whenever the type conforms at the end (a type that was a collection all along and drops its own isEmpty breaks no call site either) and never while it still declares that name and labels (that is a change, and the pairing reports it); Self, Element, Index and the literal types are read off the type's own listing at the end, and a placeholder it does not settle matches nothing, so a gap in the reading reports a removal and never a move; the table leaves out every member whose meaning a type may own (contains(_:), since Contour's is a hit test and the library's a membership test, plus min()/max()/sorted()/reversed() and randomElement()/shuffled(), which draw from the system generator where a seeded sketch draws from its own); --selftest compiles one call per table line against a type that declares only its protocol's requirements, each binding its result inout to the claimed type, which admits no conversion (a plain let x: T? = passed a planted removeLast() -> Element? by optional promotion); a type head that only gains is extended, an addition, and one that drops a conformance or changes its generic clause is a change. The bump follows the from-version's major: at zero any public change is a minor and a release with no public change a patch; from 1.0 a removal of any kind is a major, an addition or a deprecation a minor. The shim rule reads as: a line present at the start, gone at the end, and not deprecated at the start is a violation; a line deprecated at the start and gone at the end finished its grace cycle, allowed by the rule and still a major. Before 1.0 the violations are a count in the summary and the run exits clean; from 1.0 on, or under --strict, they fail (--warn forces the count). A second rule reads the tree in either era: every @available(*, deprecated in a module lives in Sources/<Module>/Deprecations.swift and carries renamed: or message:, so the shims are audited in one place and every one offers the fix-it (the one shim that existed, CameraView.corner, moved there). --changelog prints the entry's skeleton in the changelog's own "old is new" form (an Added bullet per new type and per container with new members, a Changed bullet per rename, signature change, move and removal, a Deprecated bullet per shim with its replacement), for a person to group and explain; the facade's own members print bare, everything else qualified by its type. Measured: 0.7.0 against 0.8.0 reads 23 renamed, 35 changed, 24 moved, 3 extended and 60 removals with no twin, and every rename in that release's Changed section lands on one skeleton line; a listing line deleted by hand goes red under --strict naming the selector and is counted under --warn; a Bag gaining Collection and dropping count/first/isEmpty read as Bag.count is Bag.endIndex, a major and three shim violations before the table, and reads as three moves, a minor, and a holding rule with it; five planted table lines go red, each named; no release pair from 0.1.0 to 0.10.0 reads differently, except that the rename count stopped counting a type that only gained conformances (0.9.0 to 0.10.0 said six renames, all six such types). --selftest runs fixtures for every class first on each preflight, so a change to the reader cannot pass on a wrong reading. No pixel snapshot (the output is text). API/README.md.
  • Every public name has a page (Scripts/check-api-pages.sh + Scripts/check-api-pages-allow.txt, on every preflight run; a couple of seconds). The reverse of check-names.sh: that one reads a backtick in the prose back to the tree, this one reads every declaration under API/ forward to the reference, and asks whether any page names it. Nothing did that before, and nothing could, because every other gate starts from the pages: check-links walks the navigation, check-snippets compiles the fences, guide-coverage counts the matrix rows, api-docs builds the generated reference from the doc comments. A public name no page mentions was invisible to all five, which is how Mesh.klein, Material.rimSharpness, Installation.Projection, MPEZone and Controller.dpad shipped with nothing written about them anywhere. What counts as named: a type by its name in some page's code, where the code is the page's code spans, its fenced blocks, its headings, and a one-word bold run (- **roughness** runs from 0 to 1, which is how a page of settings names its fields); a member by .name or name( anywhere in the reference, or by its bare name on a page that also spells its owner or one of that owner's ancestors. Three rules keep it from lying in either direction: a member of Sketch is looked up bare wherever it appears, since the facade is called with no receiver and keyIsPressed lives on the Input page, which has no reason to spell Sketch; a nested type counts when the reference names something under it, since a page writes channel: .magnitude and never spells FourierChannel; and a type with no page swallows its members, so the gap is the page, reported once, not one line for each of forty-five members. Exempt by construction, each class named for the allow list: a member a declared conformance requires, Ollin's own public protocols included, read from the listing (Vector is documented once and every conformer's component(_:) is that same name); an operator, a subscript, and callAsFunction, which have no identifier to write; a conformance typealias; wrappedValue/projectedValue on a property wrapper and buildBlock and the rest on a result builder; an @objc override of a platform method; a deprecated shim; and an initializer of a type the reference hands over another way (a makeSwarm(count:), a Light.spot(…), a documented property that already holds one). Everything else deliberately bare is listed with its reason in Scripts/check-api-pages-allow.txt, by dotted path, by glob, or by the source file that declares it, which is the honest form for a whole tier that is public because another module compiles against it: the phone, room, remote, laser and lighting wire protocols, the host types the inspector and the timeline are built from, and a catalog the reference names as a set (the 78 risograph inks, ARKit's 52 blend shapes, Vision's joints). An entry that matches nothing fails, so the list cannot outlive what it excuses; a name missing because the reference is a page short is never listed, the page is written. First run: 1,198 gaps. 542 were the wire and host tiers and the catalogs, allowed in 27 lines; the other 656 were written into 89 pages, among them Mesh's whole generator list, Material's rim and gooch and subsurface colors, StrokeFont's metrics, FrameProfile's ten counters, ScanGraph.Settings' ten, Installation's projection and schedule, Take.Event's nine cases, SwarmChemistry.Recipe, every satellite's error type at the call that throws it, and a stale minimumSize: label on RectangleDetector that no gate had caught. What it cannot see: whether the mention teaches anything (a name inside one fence is named, and the Guide's coverage matrix is where teaching is tracked), and two owners' members that share a spelling. Sabotage: a public func and a public type added to a listing with nothing written about them both go red, named by file and line. No pixel snapshot (the output is text). API/README.md.
  • The stranger's build (Scripts/release-smoke.sh, at a release, by hand; a cold run is a clone plus a debug build, about two minutes on the M2). Every other gate reads this repository, so nothing exercised the one route somebody else takes: a line in a manifest, a tag fetched from GitHub, a framework that has to build from that checkout alone and draw. The script generates what the generator writes for a stranger (ollin new --remote, a mac-sketch pinned .upToNextMinor), points SwiftPM at a cache of its own so the fetch is a real clone rather than whatever the machine already holds, builds it, and renders one frame headless. Five tests, each printed as it passes: the tag exists on origin, because that is where SwiftPM looks; the manifest pins the published framework in the form the README tells a consumer to use; the resolve answers from that repository at a version on the pinned minor (a newer patch is the pin working); the build compiles with the dependency fetched rather than pathed; and the frame is the canvas the sketch declares with ink on it. The ink test is the one that would rot into a check of whether a file exists, since a framework that renders nothing still writes a file, so the frame is read back through ImageIO and a pixel counts as ink when it differs from the corner by more than two levels; --selftest runs that reader against a blank fixture and an inked one and refuses the run if the blank one passes. What it catches that a green tree cannot: a resource left out of a target, a product that is not public, a tag never pushed, a manifest naming a version nobody published, a dependency reachable only from this machine. When it runs: the tag has to be on the remote, so before a release it is a rehearsal against the tag already out there and the proof for a release comes right after its tag is pushed. --template/--with widen what the project pulls in, --warm-cache makes a re-run quick, and the honest run is the cold one. Green on 0.8.0 (twice, the same 3,936 inked pixels); red on an unpublished tag and on a blank frame. No pixel snapshot (the output is a report). Contributing.
  • The suite runs in an order that keeps it honest (Scripts/test.sh + Scripts/shard-tests.sh + Tests/README.md). One rule decides the order: a suite that measures the outside world against a clock, or that holds a device or a name only one process can have, runs on a quiet machine; everything else runs together. Four lists are its cases (sensitive the wall-clock and device suites, loopback the ones talking to a real system service, crowded the ones that cannot share the machine with the drawing shards, runner what CI cannot do at all), and the desk's shard run now uses the same quiet phase the runner's recipe does (four phases: the device suites, then the loopback and crowded ones, then OllinTests across four processes, then every other target). Load-bearing: .serialized orders one suite's tests and says nothing about a sibling, so two suites sharing a machine-global name still collide; Core MIDI is that case (the endpoints are global and an input connects to every source), and both loopback suites are therefore nested under one serialized parent (CoreMIDILoopback), the trait being recursive. A phase only helps a suite competing with other targets: one that fails while running alone has a defect, and the four measured non-load failures (a suite starving inside its own target, a parked cooperative pool, a deadline read before its probe, a main-actor hop from a nonisolated suite) are written down beside the rule rather than re-derived. Tests/README.md is where a new test's placement is decided; Scripts/test.sh names the lists and points there. The handoffs run under Thread Sanitizer (Scripts/test.sh tsan, a milestone check preflight.sh --milestone runs, never a per-commit gate, by cost): every target whose tests run without the GPU (the wires, the phone's codecs and readers, the audio engine, the mutation harness, the two feeds), built with --sanitize=thread into .build/tsan so no object is shared with the plain build; the reports go to files (log_path) and print after the run, and any report fails it whatever the tests said, since the sanitizer sets its exit status only when its runtime finalizes. The drawing suites stay out (a render's threads are Metal's, uninstrumented; a snapshot compares pixels; a drawing suite is main-actor bound, so the slowdown lands on the one thread it waits on). One class of report is excused in Scripts/tsan-suppressions.txt, an ordering that runs through a system framework the sanitizer cannot see into (Core MIDI moves the receive block to its thread over a lock-free queue in its C++ runtime, so the initializer's stores report against the callback's reads), by the narrowest frame (MIDIInput.init) so a real race on the same object still reports; a race in Ollin's own handoff is fixed in the source, never listed. The child an exit test spawns does not come up under the sanitizer, so the harness's dying-case self-test refuses itself there (underThreadSanitizer, read off the loaded images). Measured 2026-09-23 on the 8 GB M2: the cold instrumented build 3m51s at a 1.7 GB peak, the run 9m28s for 1,310 tests over 17 bundles (four minutes of it the listing, each bundle loaded twice under the sanitizer), free memory never under 2.4 GB, 5.7 GB of disk. A planted race (the serial port's descriptor store read by isOpen without its lock) reported at both lines; the committed tree reports nothing. No pixel snapshot (the output is the run).
  • What the tests reach (Scripts/test.sh coverage + Scripts/host-gates.sh; Tests/README.md § Coverage, and the hosts' own gates). A reading tool, never a gate and never a percentage target: the shard run's four phases and every host gate under an instrumented build, one report by file in .build/coverage/ (report.txt, summary.json, merged.profdata for llvm-cov show). Load-bearing: it builds in place (a second whole build under its own scratch path is 13 GB), so the next plain build recompiles everything, about four minutes; every process writes its own profile (%p-%m) and they merge once at the end, because swift test --enable-code-coverage sets its own path and clears it at the start of every invocation, so the phases run plain --skip-build over the instrumented products; a failing test still counts what it ran. The report reads every test bundle and every host executable and leaves out Tests/, External/, Examples/, and .build. What a read cannot see: a path the desk's device never takes (the raster point-light shadow runs only without ray tracing, which the CI runner has and the desk does not). The hosts test themselves through flags of their binaries that no swift test reaches (OllinLive's five, OllinLiveCoding's four, OllinRun's, the generator's --stagetest, the gallery's --cycletest); host-gates.sh runs the eleven in about 71 s and the gallery's cycle with --all (about two minutes more), on every preflight that touches a host or OllinRuntime, with --all on a gallery change and at a milestone. First read, 2026-10-10: 83.2% of 186,385 lines (78.6% before the host gates counted), the core Ollin module 86.7%; what stays unreached is mostly SwiftUI views, device and network backends, and the command fronts the scripts drive.
  • Bytes off a wire never trap (Tests/OllinMutation/Mutation.swift, the OllinMutation target, Foundation only, a regular target under Tests/ like the browser gate; Tests/OllinMutationTests/, one suite per wire, 35 tests, GPU-free, about 17 s in the quick pass; the section in Tests/README.md). A seeded mutation harness over every decoder that reads a network or a cable: OSC packets and bundles, TUIO, the OSCQuery message and request routes, MQTT and its remaining-length field, Link datagrams and the engine that acts on them, the phone wire both ways and the picture framing, the room's frames, the HTTP head and the WebSocket frames of the remote surface, MIDI bytes and words and the timecode walk, Art-Net and sACN and the receiver, the Record3D stream and codec, the usbmux reply, Firmata, the serial line assembler, the Bluetooth formats. The one invariant: every case returns a value, returns nil, or throws; a trap is the failure. The case list per seed is fixed (MutationRun.run): the seed itself (a seed that does not decode is reported in seedsRefused, since it tests nothing), the empty input, every truncation (a spread of 288 past 512 bytes), the field sweep (every value in Mutator.extremes(width:) at widths 1, 2, 4 and 8 in both byte orders at every offset in the first 128 bytes and the last 32, which is where the length fields live and what found four of the seven), then count random mutations of one to three stacked edits (truncate, flip a bit or a byte, splice, insert, delete, an extreme field, swap, repeat a run, and .decimal, a number as text such as 1e300 or nan written over a run of digits, the edit for a protocol that carries numbers as words, which found the MQTT one). A trap ends the process, so the log is the proof: MutationLog writes the case about to run (name, seed, case number, the bytes in hex) with pwrite and ftruncate before every call and removes the file when the run completes, so a log under $TMPDIR/ollin-mutation/ always means a run that died and holds the input that killed it; the harness's own exit test (#expect(processExitsWith:)) runs an unsafe decoder in a child process and reads the dying case back from the child's log, which the child shares through $TMPDIR. Seven traps found by the harness and six by the reading pass beside it, all fixed by the failure rule: a one-byte Firmata command (a program change) read a second data byte; a number off a wire narrowed with Int(_:) in nine places (OSCArgument.int, MQTTMessage.int and int(named:), RoomValue.int, Int.restored(_:), the OSCQuery stepper and menu apply, the remote menu apply, and Table.int, a file's), which now go through the package Double.int(rounded:) (Sources/Ollin/Math/Narrowing.swift: nil when not finite or too large, the house rule for any number that arrived from outside); a WebSocket length one short of Int.max overflowed the check that added to it (a frame past maxFrameBytes, 1 MiB, drops the connection); a Link beat or time past LinkWire.maxMagnitude (2^50) overflowed the join's tempo math (the entry is skipped at the parser like one of the wrong size); a phone depth, matte, saliency or flow grid whose width times height overflows an Int, which every reader multiplies, is refused by its decoder (gridFits). What ran clean: every packet decoder proper, the phone's 18 message and 4 request decoders and the picture framing, both DMX formats and the receiver, the Record3D header and codec, MIDI and the timecode engine, the line assembler, the Bluetooth formats, the usbmux plist. Gotchas: a stateful reader under the harness is fresh per case or small (a room that kept every mutated block rebuilt a mesh of thousands per case, 439 s for one test); a decoder reached only through state (the Link pong, which lands on a measurement in flight) has that state set up in the closure; a message a stream parser reads as nothing (Firmata's protocol version) is not a seed; a run over a large input passes sweeps: false, since the sweep is offsets times 121 values times a hex line of the whole input. The readers of files run under the same harness; see Bytes in a file never trap.
  • Bytes in a file never trap (Tests/OllinMutationTests/, nine file suites beside the wire ones, 72 tests in the target, about two and a half minutes in parallel; Tests/README.md § Files, and what a file declares). The wire harness over every reader of a file a sketch is handed: .cube looks, palettes (hex lines, CSV, TSV, JSON, ASE), ICC profiles, takes, automations, cue sheets, formula text, Standard MIDI Files, IES profiles, SVG, glTF and GLB, OBJ and its material, PLY and STL (through Model I/O), USD as text, crate, and package, .r3d recordings, sound files (WAV, AIFF, CAF) through every loader, SFZ instruments, the GLSL importer, the shader include resolver, tables, JSON, and physics snapshots (whole, and the payload mutated and sealed again with a matching checksum so the edits reach the body readers). Each case is read the way a sketch reads it (a scene posed by its skins and morphs and animated across its length, a palette's colors turned through hue, a take stepped on a sketch, an instrument played). Two added invariants. The declared-size class: a count or a size in a file is checked against the bytes that remain before anything is set aside; MutationRun.run(allocations:) watches the largest single block the decoding thread asks for through the allocator's logging hook (AllocationWatch, C in COllinAllocationWatch, since the hook runs inside the allocator and generic Swift allocates in a debug build) and names every case over its AllocationBound, while a block past a gigabyte stops the process before it can be filled; the self-test's reserve-first twin goes red and its check-first twin stays green, and the .cube reader's reserveCapacity(LUT_3D_SIZE³) was the first real case (265 MB for an 856-byte file). The depth class: an input nested past a reader's limit is refused or read to the limit, never recursed until the stack runs out, measured in DeepInputTests on a 512 KB stack (a background thread's, where a debug build's frames are large), and on 256 KB for a scene: a scene nests at most 256 levels in both formats (Scene.maxDepth, which GLTFDocument.maxNodeDepth and USDStage.maxDepth read), and every walk of a loaded scene keeps its pending nodes on a list (Scene.walk, Scene.visitWorlds, Scene.first, Scene.mutateFirst, Scene.mutateAll, the draw, the USD writer's primitiveTrees, the USD prim walks, and both readers' tree builders), so no code of Ollin's spends stack on a level. What does is the runtime's own release of a nested value when the scene is dropped, on whatever thread drops it last: the array destroy of each node's children runs inside the node's own destructor, about 440 bytes a level in the standard library's frames (measured from the frame pointers under lldb), which no walk removes and which is the same under any compiler of Ollin's code; a 1,024-deep tree took 450 KB to release, more than a background thread holds, so the limit is checked against that teardown: a scene at the limit is parsed, built, walked, merged, written out, and dropped on 256 KB, half a background thread's stack, and a stage one level past it is refused. A USD value (a dictionary in a dictionary, a tuple in an array) nests at most 64 levels (USDStage.maxValueDepth), since the value readers do recurse; a formula at most 1,024 tokens nested 64 deep. The glTF and USD scene builders never recurse while holding a node (a node is a large value: the recursing builders took 13 and 20 KB a level, so a 40-deep glTF or a 26-deep USD stage loaded on a background thread already overflowed); the automation plan walks its tracks with a stack of its own. Plus the text sweep (numberSweep:, every number in a text input replaced by each of Mutator.numberSpellings), the named-file rule (NamedFile.data(at:) / NamedFile.text(at:), package: a path a file names is read only when it is a regular file, so a model, a material, or a shader include naming /dev/zero is refused), and one mesh promise checked beside the no-trap invariant: a loaded mesh never names a vertex it does not have (GLTFDocument.triangles(_:vertexCount:) drops such a triangle; Model I/O's own normal generation is asked for only when every triangle fits). What the harness and the reading found, all fixed: by the harness, a NaN ASE swatch trapping in Color(hue:), a curve of infinite length trapping Path's flattener (an SVG translate(1e999 …)), noise() at a coordinate past 2^63 (an automation looping over a 9.2e18-second length), a glTF node listing itself as a child, a PLY face index Model I/O's normals read past, a binary STL declaring four billion triangles (Model I/O asked for more than a gigabyte in one block before reading one), a negative USD face count, a USD text integer past Int64, a crate section count past Int.max, zip entries declaring gigabytes (.r3d and USDZ inflate, now held to deflate's 1032 to one), a CAF packet size of four gigabytes that ExtAudioFile sets aside a buffer for on opening, JSON.int on 1e300, and in snapshots a 4 GB declared payload, an infinite ground, a velocity past what Jolt's single precision squares, 32-bit group and skin indices, and the bridge destroying a body Jolt never created; by reading, the glTF accessor arithmetic (negative indices, offsets that overflow, a buffer-less accessor's declared count, base + idx and out-of-range triangles), the crate reader's declared counts and LZ4 capacities and an exponential path walk, the SVG arc's narrowing, the audio loaders allocating a header's declared length (AudioFileFrames now reads a stretch at a time and checks the rate, and the channel count against maxChannels, 1,024, the most the system's reader converts, before any buffer is made: a WAV declaring 32,768 made macOS 26's buffer initializer throw a C++ exception that no Swift code can catch; a stretch shrinks with the channels so its buffer stays at 4 MB), SFZ keys, tunings, and loop points past their ranges, and snapshot skin, ragdoll, and rope indices Jolt takes unchecked. What ran clean: MIDI, IES, takes, cue sheets, formula values, the GLSL importer, the include resolver, tables, ICC. Stays out: stepping a restored physics world, since Jolt's debug build asserts on configurations a world built by hand reaches too; Every loader fails one way: a file, a resource, or bytes handed to Image, Mesh, Scene, SVG, Palette, Table, JSON, IESProfile, ICCProfile, the three fonts, or ComputeKernel (and their load… sugar) throws a FileError (missing for a path or resource that is not there, unreadable for bytes that are not the format, with the path and a sentence), and the mesh and scene writers throw unwritable where they returned false and printed; a name lookup (OutlineFont(name:)) and a wire packet stay nil, since there nil means "not one of these" (FileErrorTests).
  • Same seed, same pixels (Scripts/check-determinism.sh + Scripts/example-devices.zsh; in preflight.sh --milestone, never a per-commit gate; about 40 minutes on the M2 with the GPU to itself). Every example that runs alone is exported twice, in two separate processes, at the frame its media still is taken at (the manifest's frame, else 480) under --seed 7 --photo, through the release OllinLive loose-file route (no examples build), and the two pictures are compared by bytes, then by decoded pixels. Load-bearing: two processes, never two renders in one, because a Set or Dictionary walks its keys in an order hashed afresh per process (the self-test's Set walk is red across processes); the device hold rule lives in example-devices.zsh, shared with media.sh, so a new device library is learned once; LIVE_WORLD (a live feed, today's weather) and OVER_UDP (the DMX and OSC loopbacks, whose frame draws whichever packet had landed) are held with their reasons, an example in RUNS_ALONE (it plays its own far end: a virtual MIDI source, a stand-in tracker, a dancer of its own) is checked like any other, NOT_EXACT holds the one detector that answers identical frames differently (TrajectoryTracking: Vision's trajectory fit varies a few millionths run to run, and over 900 frames that sometimes changes which sightings join an arc), a ray-traced scene with two surfaces at one distance along a reflection ray (a base cap on the floor) can differ by a pixel between processes, since the intersector's tie follows the driver's acceleration structure build (filed under Developer experience in DESIGN-NOTES.md; the Upscaling example lifts its bases a thousandth), and a sketch importing OllinVision is checked like any other since 2026-10-02 (an export reads its detectors on its own clock; see Computer vision), where its difference used to be reported as filed; a "same" is not proof a detector answered: before that day most vision examples matched only because neither run had an answer, so read the pictures when a detector path changes; the render watcher reaps its own sleep. --selftest plants a Date() read and a Set walk (both red) beside the same drawing from the seeded generator (green), 12 s. What the first pass found (594 examples; 522 the same, 52 held, 1 too slow, 19 differed), each fixed at its cause: GPU work whose result followed thread arrival (the SpatialHash scatter under seven sims, the caustic photon append, the MeshField cull append; each now ordered under Drawer.runsOnFixedClock, true for an export or a playing take and false live and under --bench via OllinApp.isBenchmarking, so a live frame never pays for an order no wall clock can use), four examples that opened the machine's camera under --photo (Camera.start() now throws there; the examples take Camera.orStill), two live-world examples (held), and seven detector examples (filed then, fixed 2026-10-02 bar TrajectoryTracking). No example read the wall clock or the system's dice where it mattered, bar one stand-in camera's launcher (seeded).
  • The code in the prose compiles (Scripts/check-snippets.sh, the OllinGuideSnippets tool, Guide/Snippets/known-gaps.txt + Docs/Snippets/known-gaps.txt; on a prose or framework change in preflight, about two minutes for both trees). Every Swift block in the Guide's chapters and in the reference pages under Docs/ is wrapped by shape (a whole file as written, a declaration under an import, a fragment inside a probe sketch with members and statements told apart) and typechecked through SketchLoader against the built framework, across the cores. Load-bearing: a fragment that cannot stand alone is recorded, keyed on its own text, with the first error it gives, and the gate fails on a change (a block that starts failing, an error that becomes a different one, a recorded block that now compiles), which is what turns a public rename into a failure here instead of a reader's problem; the two trees keep separate gap lists and per-page preambles (Guide/Snippets/<chapter>.swift, Docs/Snippets/<Group>-<Page>.swift, the Docs key dashed because three pages share a stem); a block's imports are its own page's (Foundation, the preamble's, and any a block on that page writes), never a union across pages, since two chapters mean two types by one name (OllinPhysics' and OllinVision's Body3D) and a union made both ambiguous, so a chapter's baseline never moves with another page's; a scoped --record keeps the lines of the pages it did not check; a block holding a bare ... is illustration and passed over, and <!-- snippet: skip why --> above a fence exempts one on purpose. Two name checks run with it: an ollin docs Page#section in a shell block resolves to a real heading, and a backticked name beside an Examples/ link names a real example. Guide/AUTHORING.md § The code in the prose.
  • Checking the machine (Sources/Ollin/Core/Doctor.swift + the OllinDoctor target, behind ollin doctor; Sources/OllinReference/CommandFlags.swift + ShellCompletions.swift behind ollin completions; Sources/Ollin/Core/ParamListing.swift behind --list-params). Three answers a broken setup otherwise gives somewhere else. ollin doctor reports the system against the 26 floor, swiftc and which one, the Metal device with ray tracing / mesh shaders / temporal scaling each said either way, the shader compiler asked by compiling (Ollin builds its library from source at start, so a findable compiler is not the question), whether the ollin on the PATH is this checkout's, whether _ollin is on an fpath, and the camera / microphone / screen grants. Load-bearing: the report is the framework's (package, not public), so the tests read exactly what the command prints; every read is a preflight form that cannot raise a dialog (a doctor is run on a machine that is projecting); a problem always carries its fix line and is the only level that exits nonzero (a refused permission is a note, since it only stops a sketch that wants it); the CLI finds the checkout by walking up from its own executable to the directory holding both Package.swift and Scripts/ollin, since the build layouts differ. Completions: Scripts/completions/_ollin is generated from one flag table (139 entries, each with scope, what follows it, and a one-line summary) and checked in, so ollin install links it beside the command and a git pull carries a new flag to the prompt; a flag a host spells with no table entry, a table entry nothing spells, and a stale _ollin each fail Scripts/check-flags.sh (preflight), whose source set is the named list of files that parse a command line (a flag literal elsewhere belongs to git, xcodebuild, or a browser) with foreign flags inside those files listed in check-flags-allow.txt; an internal flag is in the table marked internalUse rather than left out, so the table stays complete rather than curated. A value a flag may be left without takes zsh's :: form, or offering it makes it required, and an apostrophe in a summary is closed and reopened ('\'') or it ends the quoting. --list-params prints name / kind / value / what it accepts, grouped as the inspector groups its cards, after setup() and after any --param / --cue / --seed beside it (so it describes that run), each value in the spelling --param reads back and pinned by a round-trip test: exact for every kind but a color, which is said in hex and lands on the nearest 8-bit step. DoctorTests (4) + ParamListingTests (5) + OllinReferenceTests.CommandFlagsTests (6); Docs/Tools/Doctor.md; Guide Ch 1 § What you need + Ch 1 § A shorter way to run things + Ch 41 § Passing the parameters back in.
  • The reference offline (Sources/OllinReference/ + the OllinDocs command line, behind ollin docs <topic> and ollin examples <filter>). The written reference and the examples set read in the terminal, out of the checkout you build against, for anybody with no network or no wish to open a browser: a page by name / path / part of either, topic#heading for one section, --search across every page, the examples with what each shows and the command that runs it, --source to print a sketch, and ollin api <name> for one public name (every declaration it has from the listings under API/, each printed as its source writes it where it was found (the parameter names carry units, rotate(_ radians: Double), and the defaults are real, phase: Double = 0; access level, the compiler-filled source-location parameters and the body dropped) and as the listing writes it otherwise, with the /// comment above it in Sources/ and its file and line; then, per type the name sits on (the first three), up to three pages that write it as code and the three shortest examples that use it, each read the way that type's member is reached; a dotted name narrows by type from the inside out, a type lists its members (names only past sixty), a name nothing is called lists the public names containing it or a letter off it, two for a long one, fuzzy only when nothing matched exactly, and a bare name that is no sketch call leads with the sketch calls spelled like it, since a name carried from elsewhere can be a real member of another type (strokeWidth is an SVG element's; the call is strokeWeight, found by the name's first camel-case word and ranked by the longest shared start). A source declaration pairs with one listing line only, so two lines the listing writes alike (one initializer for a ParamOption value and one for ParamChoices) take their own declarations, each printed with what tells it apart: its extension's where clause, or requirement inside a protocol. A type's page is ranked by how much the page writes it (an exact page name first), where a member's is ranked by a heading naming it, since a heading naming a type is as likely another page's section on binding it; and a type that finds no page or example by name is looked for through its cases and static members after an argument label the listing gives that type (style: .segmented is a ParamMenuStyle because a call takes style: ParamMenuStyle). Load-bearing: the folders are the truth about what exists and the indexes (Docs/README.md, each category's own README.md) are the truth about what each one is for, read separately so a page or a sketch added today is findable before anybody writes its line, and it simply arrives with none; the lookup runs exact → loose and stops at the first rank that finds anything, which is the whole reason color answers with the color page rather than the nine pages that mention color; a topic matching no page falls through to the search rather than failing, so a wrong guess still lands. Three renderer facts were defects first, found by rendering every page in the checkout in the test: the breadcrumb's --- is dropped only when it is right under the trail (a page carries the trail and no rule, and hunting for one took the whole page with it); wrapped paragraphs are joined before anything is read (one page hard-wraps, splitting a link in half, and each half alone prints as family](#combining)), with fenced and four-space-indented lines never joined; and a page's title is the first # or ## (both spellings are in the tree). Wrapping counts visible columns, so the escapes that color a line occupy none; plain output keeps an identifier's backticks and colored output drops them, since the color says the same thing. Example listings are read in both shapes the listings use (a category's table row, and the recreations' prose list under the artist, where the artist is the subject and a table would flatten that); the picture grid's name row is passed over (a row whose cells are nothing but links describes nothing, and it had described the first sketch of each grid row by its neighbors' names), and a row linking Sketch.swift wins over one linking the folder. A search answers an exact name alone and unions everything looser in rank order (name prefix, folder, description, header credit, imports), so complex lists ComplexPlane beside the sketches described as complex arithmetic rather than hiding them behind the name; the header credit (the // lines a sketch opens with, before its /// documentation) is read into each entry, printed as credit when it reads as one (Ported from, Inspired by, Recreation after, Based on), and searchable, so an article's author finds the sketches written after it. ExampleCatalogTests.ownWords reads every listing in the checkout for a description made of sibling names. The pager is the shell's job, not the command's (only the shell knows whether a person is looking), which is why Scripts/ollin pipes into ${PAGER:-less -FRX} and passes --color through the pipe; --no-pager, --plain, --width override, and a pipe gets plain text so --list | grep composes. It links no framework (OllinReference depends only on OllinProjects, for ExampleSource.discover rather than a second walker), so the first run is a second or two. Scripts/ollin runs the reader straight from its build (0.08 s against 3.9 s through swift run, which also holds the checkout's build lock, so a lookup through it waits out any build running there): a stamp at .build/ollin-reference-<config>.checked records when it was last brought up to date, written at the time the build starts so an edit made during it still counts, and only a file under the reader's four source folders newer than the stamp builds again. The binary's own time cannot stand in for the stamp, because the build leaves a binary alone when a source changed and its output did not, and every lookup after would rebuild. A build that fails answers from the last one that worked, with a line on stderr, since a checkout shared with a session mid-edit still has pages to read. ollin api pairs a listed declaration with its source by name, the types it sits in, and its argument labels (SourceComments): a reader for this tree's shape, not a Swift parser. It follows every brace outside strings (a shader held as Swift text has braces) and comments, and takes a declaration only at a type's member level or the top level, so a method's locals are never members; an extension line never stands for the type's own declaration; a subscript's or an operator's parameter has no label unless it is given two names; a case list may run over lines or sit on the enum's own line. APILookupTests.coverage pins that more than 99.9% of the public declarations are found (10,824 at the time), so a shape it cannot follow shows up as a drop rather than as comments quietly missing. A page counts as documenting a name only where it writes it as code in a Swift, Metal, or unlabeled fence, a code span, or a heading's code (a shell or output fence shows a lookup, not a use), and a member of a type other than Sketch counts only reached with a dot, so tube: as a label is not Mesh.tube. The commands print for a person; two maps serve a machine. No JSON and no server: whether Ollin runs a server of its own over these pages is the open question in ROADMAP.md under A third-party extension ecosystem. What a machine gets is a map pointing at the same pages. The site writes one (below), and llms.txt at the checkout's root is its twin for a local assistant, written by the same generator (SiteBuilder.checkoutIndex, through the hidden OllinDocs llms that Scripts/llms.sh calls): links are paths from the root, never addresses; a Start here section leads with Docs/QuickReference.md and Docs/Swift.md (neither sits under an index group, so neither would be listed otherwise), and the preamble names the three lookups by their Scripts/ollin spelling, which a site cannot run. A title's em dash is written as a colon in both maps. It is generated and committed, so every line derives from a page title, an index line, or a group, and an edit to any of them stales it: Scripts/llms.sh --check is a standing preflight check for that reason, and SiteTests.checkoutIndex pins that the committed file is current and that every link is a file in the checkout. The website (ollin site [folder], HTML.swift + Site.swift + SiteStyle.swift in the same target, published by .github/workflows/site.yml) writes the same pages out as a static site: the README as the front page (its title and bold tagline lifted into a hero beside the ring, three cards, then part of the README, not all of it — its opening less the badge row and the navigation row the chrome stands in for, and the sections SiteHome.rows names by heading, Hello, circle / Run it + Install side by side / What's in it, each in a home-<slug> block the stylesheet lays out for the width of a page (Hello, circle is a .carousel row rather than a .section, so its block is home-band: the README's own program leads a band of sketches you page through, the rest named by showcase in the manifest, each running beside the whole program that draws it. The band's heading is the site's own (Examples, carrying its own #examples anchor as the README's sections do), since the README's heading names one sketch and cannot cover four; that heading is rendered a level down instead, inside the slide it came with, so it still names it and still carries #hello-circle, and every other slide is named by its example's own title. The run command under Run it names the sketch on screen: the page arrives spelling the README's own command, each slide carries the target that runs it, and the script rewrites that one word as you page. It is marked, never rewritten (the first occurrence of the lead's target in the first shown section that carries it), so the README's text stands untouched on the About page and in the markdown twin, and a README that changes its command simply keeps it. The moving is the browser's own (a snapping scroll container, so a swipe, a trackpad and the arrow keys work with the script off) and what the script adds is the buttons, the dots, and the rule that only the sketch on screen plays: the others carry neither autoplay nor a preload, so a megabyte and a half is not fetched on arrival. One at a time is what puts the band back on a phone, where three stacked was a wall of code and the whole band had to be hidden), the spaced bullets of What's in it as a grid of cards, and a More about Ollin row linking every other section), the same README whole as the About page (about.html, kind .about, the same source file under a second site path; byRepoPath keeps README.md on the front page, a README.md#anchor link from anywhere and a bare #anchor on the front page at a section it does not show both go to the About page, and the sidebar marks the current page by site path, since the two pages share a source), the Guide with the chapter list beside it, the reference with the index's groups beside it, every example on a page of its own with its highlighted source and the command that runs it, and the project pages the README links. Load-bearing: the site is a lens on the markdown, never a second copy (prose reaches the page exactly as the file spells it; only the chrome is the site's own); paths mirror the repository in lower case with README.md as each folder's index.html, so every relative link resolves to the same neighbor it does on GitHub, and a link at something the site does not render (a source file, a folder of figures, CLAUDE.md) goes to that file on GitHub rather than dying; heading anchors follow the link checker's slug rule (tags and punctuation dropped, runs of space collapsed to one dash), which is what makes the corpus's #anchor links hold, and a run of dashes in a link's anchor is read as one (SiteBuilder.anchor), since GitHub keeps a dash per space (status--contributing) and nine links in the tree are written its way; the front page names its sections by heading and reports a heading the README no longer carries (Report.notes) rather than dropping it, and a README with no About page keeps its anchors on the front; the HTML renderer shares the terminal renderer's line joiner, list, link, and emphasis scanners, and the emphasis rule they share is CommonMark's (a * followed by a space opens nothing, so 1 + F0 * (1/E - 1) stays a multiplication rather than an italic that swallows the line, a defect the corpus found on the attribution page); raw figure blocks pass through with src/srcset/href resolved and pictures copied to the mirrored path, so the <picture> dark variants switch with the reader's setting as the chrome does. Design: system type, hairline rules, a translucent bar whose active section hangs as a tab, light and dark schemes following the system, a sidebar of <details> groups (the current one open) that drops under the page on a phone, and an "On this page" rail; the only scripts are the front page's ring and the search. Every page unfurls with a card: og:url and og:image are written against the custom domain or, before there is one, the address GitHub Pages serves the repository at (SiteBuilder.pagesAddress, https://owner.github.io/name/), and left out when neither is known; the card is Logo/ollin-social.png (1200 by 630, the full mark in paper on an ink tile), copied in as assets/social.png, with Logo/ollin-touch.png beside it as apple-touch-icon.png, both bitmaps written by Scripts/logo.sh (AppKit reads an SVG as an image) rather than at build time, since the text target draws nothing, and a missing one is a Report.notes line. The search (2026-09-07) is a button in the bar, /, or ⌘K opening a dialog over the page: SiteSearch writes one entry per section of every page and one per example into assets/search-index.js (title, heading, the site path with the heading's own anchor, the first sentence as an excerpt, and the section's words, lower-cased and deduplicated, stop words and words under three characters dropped, minus the title's and heading's own words, and minus any word common to more than a tenth of the entries, which is what keeps color from answering with 900 sections; a page's Contents section is skipped, and the project pages beside the front page (.about) list headings and excerpts with no words, since their sections run to thousands of words each and would double the index), and the matching runs in the browser through MiniSearch (MIT), pinned to one release on cdnjs with its published SRI hash and loaded on the first search, never with the page, so a reader who never searches asks the network for nothing: boosts title 6 / heading 4 / excerpt 2 / words 1, prefix from two characters, one edit of fuzz from five, every word required, a page's own entry boosted over its sections, at most three rows per page of the thirty shown, the matched terms marked in what is shown. Load-bearing: the index is a script setting one global, never JSON fetched, so it loads from a folder opened off the disk (a fetch of a file: URL is refused) and every < in it is written as \u003c so no excerpt can close the script that carries it; the sections come from HTML.render itself (Page.sections, the plain text of each block accumulated as the renderer walks, code and figures skipped, table cells and list items in), which is the only way an entry's anchor is the page's own (#ramps-1 for a second Ramps); the excerpt's sentence rule ends at . only before a capital, a digit, or an opening quote, and never after a one-letter word or a dotted name (e.g., p5.js), so 0...1. ends and 0.5 does not; the button is hidden until the script has run, so no script means no dead control; the dialog carries data-root, the path back up to the site root, and every result is prefixed with it. The library is the one thing on the site that comes from outside it: a folder opened offline has every page, and a status line says the search library did not load. This is a reader's search, not the machine-readable index the --json question (still open) is about. SiteSearchTests (6) pin the words, excerpts, anchors, pruning, and the file's shape; realSite checks every entry against a written page and its ids and holds the index under 3 MB; searchInABrowser runs the shipped script in headless Chrome over a small index (prefix, fuzz, every word required, page over section, marks), skipped without a browser or the network. The mark and the favicon are the logo's own masters under Logo/, read from the checkout at build time (SiteLogo), never a copy: the masters are black ink on nothing with no color written in them, and the site sets the color three ways (inline for the bar, which shows the full mark at 30 px on every page (the small form is for icon sizes only: the favicon and Safari's pinned-tab mask-icon.svg, the master itself in ink), the ink as currentColor so one drawing follows both schemes and takes the page's text color; colored for a file that cannot take a color from a page, which is the README's dark twin on GitHub, written by Scripts/logo.sh and checked against the master by SiteTests; tiled for the favicon, the small form in paper on an ink tile, since a tab bar is whatever color the browser makes it). A master still carrying a written-out black fill from an export is stripped of it on the way in, or the color set on the root would reach nothing; Scripts/logo.sh is the one cleaning path (SVGO at five decimals, since three moves edges visibly), and SiteTests fails on an export's leftovers. The picture the README opens with is lifted off the front page rather than rendered as prose (the bar shows the mark there; the About page shows the README's own <picture>), and a missing file is a Report.notes line, which SiteTests fails on. The front page's ring is the sketch's own web page (2026-09-04): Examples/Web/BreathingRing recorded as the inline form of --export-web and committed as one raw string literal in SiteHero.swift, written only by Scripts/site-hero.sh (never by hand; the export is deterministic, so an unchanged input rewrites the file byte for byte, and Scripts/preflight.sh reruns the script when the sketch, the web exporter, the shader rewriter, or the shaders change, so the rewrite lands in the same commit). The builder pastes the fragment verbatim inside an aria-hidden wrapper and follows it with the site's own inline script (SiteStyle.heroScript), which sets the ring's ink and paper through the handle from the stylesheet's --text / --bg and again on a color-scheme change; that script is inline right after the fragment, never a deferred file, or the recorded white paper paints first in the dark scheme. Reduced motion is the player's own rule (the first frame, still). An empty fragment builds a hero with no ring and a Report.notes line naming the script, and SiteTests fails on it; ringFollowsTheSite plays the hero in headless Chrome under the dark scheme's variables and reads the corner as the page's paper and the ring as its ink, against the recorded black on white. Every example can show what it does (2026-09-10): Scripts/media.sh renders a clip at the canvas size, a small clip for a grid, and a still at both sizes for one example (or --all), encodes each once from a ProRes master that is then deleted, uploads them to the host, and merges one row into Media/media.json (Scripts/media-manifest.py, which keeps a per-example seconds/frame override across runs); the example's page plays the clip with the still as its poster (SiteBuilder.clip). Load-bearing: a sketch declaring loopDuration records one lap and everything else ten seconds, since a longer file of a looping sketch is the same lap again at N times the bytes while the page's own loop repeats it free; the clips are H.264 High 4.0, yuv420p, tagged bt709, faststart, no audio track, which is the combination an iPhone decodes, and the tag muted + playsinline is what keeps iOS Safari from taking a playing clip fullscreen; the poster is a real rendered frame, so low power mode, where nothing autoplays, still shows the sketch; 60 fps costs about 15% more bytes than 30, not twice, so the page clip runs at 60 and only the grid clip drops to 30; the manifest is the one place the address is written, an example with no row simply has no picture (which is what lets the set fill in a folder at a time), and both link checkers already skip an external address so no gate had to learn one. What is passed over is written down, never silently missing, and the three rules are read rather than remembered: a sketch whose imports name a camera / phone / controller / MIDI / serial / lighting rig / second machine waits on hardware; the Input / Live / Installation / Data folders wait on a hand; everything else is measured (the master sampled once a second, the largest change between samples must clear MOTION_FLOOR 1.0 — a static print scores ~0.2, a slowly growing sketch ~3). An unchanged example is left alone, media and verdict both (a digest of the whole example folder, not Sketch.swift alone, since a font/mesh/photo beside it changes the picture; --force overrides; note shasum hashes the paths too, so a digest taken by hand must be taken the way the script takes it). Every listing opens on a picture grid (Scripts/example-grids.py, written from the manifest, idempotent, four across with names on the row beneath): the grid is markdown, never HTML (the renderer passes a few tags and escapes the rest, and GitHub shows the same file), a listing owns the sketches beneath it except those a nested listing owns (the check-links rule), and HTML.gallery renders a first row of all-[![ cells as figures in a grid rather than a table (a table scrolls sideways on a phone) with a hover swap to the small clip after ~120 ms, preloading nothing, (hover: hover) and reduced-motion gated, the clip's address derived from the still's name. A grid above a table of links broke the site's own reading of that table and both guards are load-bearing: categories(readingIndexAt:) steps over a row whose first link is a picture, and dedupes by path, since the names under the pictures link the same folders and every category otherwise appeared twice in the sidebar. A page can open on a grid of sketches playing (heroes in the manifest, SiteBuilder.playingHeroes): the markdown carries the still, since that is what GitHub and a plain clone show and a page should say something before a megabyte of video lands, and the site swaps an <img> whose address matches a hero for that clip playing with the same picture as its poster, carrying the alt text over as the label; a page with no hero is untouched. The guide's is the sketch each of its thirty-two chapters builds (each figure marks its own Guide payoff in source, which is what makes the selection the book's rather than a guess, and 32 chapters is what makes 8x4 the shape), the README's is thirty-two examples across every area. Each cell is filmed with a running start (--skip), or a sketch that accumulates opens on an empty canvas; three needed their age tuned individually rather than their content changed (a swarm that converges wants 2 s, a particle drift 30, and a grid simulation reads as isolated rings at 4 s and solid at 40, so it is caught at 10). Group covers (covers in the manifest, one per category / 3D group / artist) are a choice, not a rule, so they are written down; Examples/3D was split into ten topic pages because 137 sketches cannot take one grid. Measured over the whole sweep: 353 with media, 173 passed over, 1.49 GB at 4.32 MB per example, seconds of render each after a release build. ExampleMedia (Sources/OllinReference/SiteMedia.swift); SiteTests (3: the manifest reads and its absence is not a failure, a clip carries what a phone needs, and no row names an example that has gone). An index and a markdown twin for every page (2026-09-10, the llms.txt convention v2): the site writes /llms.txt (a title, a blockquote, then curated H2 link lists) and a .md beside every .html carrying the markdown actually written, with its links rewritten through the same resolver the HTML uses and pointed at the twins, so a machine following one keeps reading markdown rather than falling into HTML halfway; every page's head carries rel="alternate" type="text/markdown" and rel="describedby", the two relations the convention names. Load-bearing: the index is curated, never a sitemap (526 examples would bury the reference, so they are one link to their listing), each entry is cut to its first sentence and capped at 180 characters, since the whole point is fitting in context while the detail waits behind the links; an example's twin is its summary, its run command and its whole source in a fence, because an example is its source. A convention rather than a standard (proposed 2024; Anthropic, OpenAI and Gemini serve one for their own docs), so nothing fetches it automatically: it earns its keep when an agent with a browsing tool is pointed at the site. SiteTests.llmsIndex builds the whole checkout and fails if any link in the index is not markdown. SiteTests builds the whole checkout into a temp folder and walks every page: no dead relative link, every picture in place, no markdown left in prose (code and attributes excluded), a canonical address per page; HTMLTests pin the anchors, the nesting, the figures, and the highlighter. The workflow skips its push trigger while the repository is private (a macOS runner bills 10x and Pages needs a paid plan there) and always runs by hand; SITE_DOMAIN as a repository variable writes the CNAME. OllinReferenceTests (60, pure but for the reads of this repository and the browser run of the ring: every page renders and opens on its own heading, every page is linked from the index, every example carries a listing line, every example target the catalog prints is one Examples/Package.swift declares, and the whole site builds clean); no pixel snapshot (the output is text). Docs/Tools/Reference.md (§ In a browser); Guide Ch 1 § Looking something up without leaving the terminal.
  • Installation mode (Sources/Ollin/Core/Installation.swift + Live/InstallationHost.swift + the clock in Core/SketchView.swift / Core/Sketch.swift). The unattended-run tier: open var installation: Installation declares a piece left running for days, .on filling the screen (native full screen, so the menu bar and Dock go with it and the window keeps the keyboard — a borderless one cannot become key without a subclass), hiding the pointer, and holding the display awake; --installation / --no-installation put any sketch either way with no edit. Load-bearing: the sketch clock is the sum of its own frame steps, each capped at SketchRunner.longestFrameStep (0.25 s), never now - startTime — a gap in the frames (a display asleep overnight, a drag, a stall) is then a pause rather than one eight-hour deltaTime that throws every integrator into the far distance; a capped step also never feeds the fps readout (a stall is not a frame rate). The GPU clock restarts (Sketch.shaderClock, the one Float(time) choke point in performDraw): a 32-bit clock in seconds cannot hold a week (measured: at 604,800 s, Float(t + 1/60) == Float(t), so shader-driven motion stops dead; at 86,400 s the ULP is already half a frame step), so .automatic wraps at a whole number of loopDuration laps (invisible by construction, capped near 1000 s), while a sketch with no declared loop keeps its clock running rather than take a silent jump; installation off is identity, so nothing else changes. frameCount reaches the GPU with UInt32(truncatingIfNeeded:) (a plain init traps after ~2.3 years at 60 fps). Power is both a held ProcessInfo.beginActivity([.idleDisplaySleepDisabled, .idleSystemSleepDisabled]) and a 30 s repeat of IOPMAssertionDeclareUserActivity — the assertion alone stops display sleep but not the screen saver, which arms off the input-idle timer that declaring activity resets (verified live in pmset -g assertions: PreventUserIdleSystemSleep + PreventUserIdleDisplaySleep + UserIsActive). A screen change arrives as a burst (a display waking reports many arrivals in a second), so didChangeScreenParametersNotification is debounced 0.5 s, then the window is put back on a screen that still exists and SketchRunner.displayChanged(to:) retimes preferredFramesPerSecond (read once at view build otherwise). The log flushes on the spot (stdout to a file is buffered, and a killed run then loses the whole trace). Applies on the standalone OllinApp.run path only — the live host and the gallery own their windows — and an export has no window for any of it; the shader-clock restart does follow the sketch into an export, landing on a whole lap there too. A loose single-file sketch reaches that path through OllinRun (Sources/OllinRun/): ollin <file> --installation compiles the file once through SketchLoader and hands the sketch to OllinApp.run, so the window, the supervisor, the checkpoint and the warp are the shipped ones and nothing downstream knows the sketch was loose; the script routes on the flag (--calibrate too), and the live host, which cannot honor one, says so once per session when a sketch it is hosting declares an installation. It reloads nothing, by design: a wall runs the code it was started with, and the live host is where a piece is worked on. Load-bearing: the checkpoint is filed under the unqualified type name (what String(describing:) gives), so the fresh per-compile module a loose load carries (OllinRuntimeSketch_<token>, different every time) never reaches the file name and a piece resumes across launches instead of silently starting over every morning; the compile runs before OllinApp.run so a broken file fails once with the compiler's own message rather than in each supervised child until the watch gives up. OllinRun --selftest pins the name across two compiles (printing both modules, so the check cannot pass for the wrong reason) and the declaration surviving one. Verified live on a loose file: full screen from the flag alone, a checkpoint every 2 s, kill -9 on the child bringing the piece back resumed with its wall intact, and the watch stopping with no orphan left. Checkpoint and restore: Installation(checkpoint: .every(seconds:)) writes the run down on a cadence, @Saved (a Codable property wrapper found by the same mirror walk as @Param) marks what to keep, and a relaunch puts back the seed, the clock, every @Param, and every saved property. Load-bearing: the restore is two-phase — seed and parameters before setup() (so a sketch builds from the numbers it built from last time), state after (that is where setup() fills its properties in); each property carries its own JSON, so a renamed one finds nothing, a retyped one fails alone, and neither costs the properties behind it (pinned by a property declared after the failing one, verified red by sabotage); the file is atomic (a power cut cannot leave half of one), JSON a person can read (nested as real values, not escaped strings inside strings), keyed on the sketch type name + a format version, and nothing here ever stops a piece from starting — every mismatch is logged and skipped. --fresh ignores a file without deleting it; saveCheckpoint() / forgetCheckpoint() are the manual pair. Saves are timed by the sketch clock (a paused piece does not come back owing a pile), plus one on applicationWillTerminate and one from a DispatchSourceSignal on SIGTERM/SIGINT (signal(_, SIG_IGN) first, or the default action kills the process before the source runs) — verified live: 10 tiles at the last cadence save, 14 in the file after a kill -TERM. A save is never written before the run has read the file it replaces (didSetup): every save asked for from outside the frame loop (a schedule closing for the night, a quit, a stop signal) can arrive before the first frame, which is where the restore happens, and writing then puts an empty run over a piece that has been growing for days (found live, by launching a piece inside its own dark hours). Restoring is off even under .on (it changes what a piece does on launch), and the live host and gallery never checkpoint at all (restoring old state on every reload is the opposite of iterating). GPU-held state is out of reach by construction (accumulation, feedback, sim fields, compute buffers are framework textures) and the docs say so. Vector2/Vector3/Color/Rectangle/Insets gained Codable for this. Restart on failure (Live/Supervisor.swift): Installation(restarts: .onFailure) turns the process into a supervisor that owns no window (the branch sits in OllinApp.run before the window, so the process owning the window is the replaceable one) which runs the piece as a child and starts another when a run ends badly. Load-bearing: the child is marked by an environment variable (OLLIN_SUPERVISED), never an argument — the sketch's own flag surface stays untouched, and the round trip through the env the supervisor actually builds is what pins that a child never supervises in turn; a stall is found by a heartbeat written from the main run loop (Heartbeat, a Timer in .common), never from the frame loop, because a noLoop() still and a piece dark for the night both legitimately draw no frames while the run loop keeps turning, and a main thread stuck in a frame is exactly what stops it; a stalled child that exits tidily still counts as a failure (it can catch the SIGTERM, save, and exit(0), which by status alone is indistinguishable from somebody quitting the piece — read that way, the wall stays dark; sabotage-verified red); the first heartbeat gets a 120 s startup grace (a piece has not answered at all until its first frame, and killing a heavy setup() there makes such a piece unable to start at all; sabotage-verified red); the stop is SIGTERM then SIGKILL after 5 s (a hung main thread never services the signal, and CheckpointSignals has already set SIG_IGN on it); RestartPolicy counts a run shorter than 30 s as a strike with a growing wait (1/5/15/30/60 s) and gives up after five, one real run clearing the record, so a nightly crash recovers for ever while a piece broken at launch does not spin all night; the supervisor forwards SIGTERM/SIGINT to the child (a signal sent to it by name alone would orphan the piece) from a DispatchSourceSignal built non-isolated. Off even under .on (a piece that crashes while you work on it should stay crashed), --no-installation takes it off with everything else, and it exists only on the standalone path. Verified live: kill -9 and kill -STOP on the child each brought the piece back within a second or two, resumed from its checkpoint, with no orphan after the supervisor itself was stopped. Behavior by time of day (Core/Schedule.swift): Installation(schedule:) is a list of Periods (ExpressibleByArrayLiteral, so the call site never names the type), each a start Time (ExpressibleByIntegerLiteral, .at(9, 30) to the minute) + a name + shows, with .open(from:to:) the sugar for one showing period and one dark one; a sketch reads scheduledPeriod / scheduledProgress anywhere, at a desk as much as on a wall (the read is off the declared value, so only the darkening needs the installation). Load-bearing: the current period is the last one that has started, falling back to the last of the day when now is before the first — the small hours belong to last night, and read instead as "the first that has not started yet" a piece would come on at midnight in an empty building; that same rule is what makes a night crossing midnight one period rather than two. Time.wrappedMinutes treats a time to itself as zero, not a whole day (progress at a start must be 0), so the one-period case restores the day length explicitly, and Time itself wraps into the day (.at(24) is midnight, .at(9, 90) is 10:30) because every reading compares minutes since midnight and a time outside the day would simply never come round. The host pushes the state to the runner on every reading, not only on a change, and holds the runner directly (InstallationHost.attach) rather than through OllinActiveSketch.runner, which a piece only registers with on its first frame and a piece opening outside its hours has none — the defect in the first build, caught live: the log said dark while the frames kept advancing. Going dark pauses the frames (SketchRunner.setShowing: isHidden + isPaused, and a still sketch keeps its own pause across it), so the clock stops with them and the morning resumes where the night stopped, writes the state on the way down, and releases the power assertions so the panel can rest. Verified live with a boundary a minute out: frames advanced, froze flat across the dark period (a screenshot of a black window), and advanced again when it opened. Fitting the wall (Core/Projection.swift + Core/ProjectionCalibration.swift + Live/ProjectionCalibrator.swift + Geometry/Homography.swift + the projected present twins in ShaderEffects.metal): Installation(projection:) corner-pins the presented frame onto what it is thrown at (shows the part of the canvas this machine carries, corners where that lands, blend how far the edge fades reach, gamma the projector's own answer to the standard curve), with ⌘K raising four draggable handles over the running piece and the numbers kept per display. Load-bearing: the installation window always presents through the projected pass — fit corners reproduce the old SwiftUI letterbox exactly, so ⌘K has corners to drag on any piece and no if can swap view identity (and the MTKView with it) mid-run; the geometry then renders at the canvas's own proportions (MetalRenderer.pictureSize, reusing the render-size seam the temporal upscaler opened) because the view now fills the display and a drawable-shaped target would carry oval dots and strokes of two widths. The fade is applied to light, not to a code value (after the tone map, before the encode), which is what makes two beams weighted w and 1 - w sum to exactly one coat; gamma 2.2 is the standard curve and yields exponent 1, no correction (sabotage-verified: weighting as a code value reads 0.87 coats mid-band). blend widths are canvas fractions, the same units as shows, so both machines are told the same number about the same band and their two fades are the same function of the same wall. The warp runs in only the two paths that present into a drawable (encodePresent(projected:)), so an export, a frame grab and a Syphon feed stay unwarped (pinned by a test that diffs an export against itself with a warp set). The two present fragments are new functions, not a branch in the shipped pair (a grown fragment re-contracts under fast math, the rule the wide-gamut twin already carries). fwidth is read in uniform control flow — a lane that returned early has no neighbors to read — so the behind-the-horizon guard multiplies the answer rather than returning. Corners are filed under the display's own UUID (CGDisplayCreateUUIDFromDisplayID, which survives a replug where the display number does not) because a keystone belongs to the room and not to the work, so every piece shown there opens square; nothing about reading that file may stop a piece from starting (the checkpoint's rule). A drag that folds the quad has no map, so the handles read from the corners themselves and the last good placement stays up: reading them back through the map throws all four across the display, and dropping the placement makes the picture vanish under the hand. The pointer takes the warp backwards, so a piece being lined up still reads mouseX in its own canvas, and a described part's frame goes through it forwards (the box around its four mapped corners, since a screen reader takes a rectangle) so an accessibility element still points at the piece of wall it is talking about. --calibrate opens with the handles up, and ⌘K is handled by a local key monitor (a full-screen window has no menu bar to hang a command on) that passes every other key through untouched. InstallationTests (11, the clock restart verified red by sabotage) + CheckpointTests (11, the per-property isolation verified red by sabotage) + SupervisorTests (15) + ScheduleTests (13) + ProjectionTests (22, both pixel claims verified red by sabotage: a flipped axis and a fade in the wrong space); no pixel snapshot (the warp is screen-only, so it is presented into an off-screen display texture through the same encodePresent instead). Where the canvas is on the desk (Sketch.canvasOnScreen / screenFrame, pushed each frame by the runner): the rectangle this canvas covers in screen points, and the screen it covers it on, so several windows of one sketch can look into one world (and, ahead, several displays can). Load-bearing: it is measured the way the canvas is (origin at the top-left of the primary screen, y down), because a sketch mixing a y-up desk with a y-down canvas draws its world upside down; NSScreen.screens.first anchors that origin, never .main, which is the screen with the key window and is a different screen on two displays. It reports the view's rectangle rather than the window's, so a host with a title bar or a sidebar says where the drawing is and not where its furniture is. It is read every frame (a window being dragged is current in the frames drawn during the drag), and is nil with no window, which is what lets a piece fall back to being its own world in an export. PlacementTests (3, the turn-over verified red by sabotage). Several displays from one machine (Core/Displays.swift + Live/DisplayWall.swift): Installation(displays:) (.one / .spanning / .mirroring / .parts([Projection])) puts one canvas on every display the machine drives, as a window and a drawable per display fed from one canvas render, presented once per display in the same command buffer, so every beam of a wall carries the same frame. Load-bearing: the placement travels with the display (encodePresent(placement:)) and is never read off the renderer's own, or every beam carries the drawing display's part (pinned on pixels, sabotage-verified red); .spanning derives each part from where the displays actually are (WallPlan, pure arithmetic over desk rectangles, so a wall of six is checked with none of them plugged in), dividing the machine's own projection.shows rather than the whole canvas (four beams over two machines is then two declarations of two), and the gap between two monitors carries canvas nobody sees, which is what the arrangement looks like from in front of it; exactly one part is the primary one and the piece's own window is moved to it, or a run whose display got no declaration would draw frames with nowhere to put them (sabotage-verified red); the canvas is drawn at the size the wall asks for (MetalRenderer.wallDemand, the largest drawable size over its own shows), capped at SketchRunner.largestPicture (16384) and the cap said out loud, since a wall quietly drawn at half the resolution somebody paid for is noticed at the opening; a wall window takes the keyboard only while the handles are up (OllinDisplayWindow.canBecomeKey, so a key-reading piece never goes deaf on the display it is drawn in), joins all spaces (a swipe must not take the piece off the wall), and never counts as a real window for the quit-on-last-close rule; ⌘K raises the handles on every display at once (the key monitor moved to InstallationHost, one per app) while the corner keys go to the window last clicked, each display still filed under its own UUID. --displays one|spanning|mirroring puts any piece on a wall, and --rehearse [n] lays that wall out as n windows on one desk (it implies an installation, keeps the pointer and the title bars, and reads a .one declaration as .spanning, since asking for a rehearsal is asking to see a wall): the parts go side by side with a gap, never overlapped (two beams sharing a band add their light, two windows only hide each other), and a rehearsed display is filed under nothing, so nothing dragged there can reach the room's calibration. A display plugged in or unplugged rebuilds the wall on the debounce the window move already waits out; a rehearsal ignores it, its windows standing for displays that are not here. DisplayWallTests (16) + ProjectionTests.everyDisplayCarriesItsOwnPart (the per-display placement, sabotage-verified red). Examples Installation/Unattended + Installation/Watched + Installation/Hours + Installation/Fitted + Installation/ManyDisplays (a long canvas spread over every display, and rehearsable on one desk) + Installation/ManyWindows (one world through as many windows as are opened, kept in step with no messages at all: every window reads the time of day, so the world is the same function everywhere); Docs/Output/Installation.md + Docs/Core/Sketch.md; Guide Ch 45 § Leaving it running: Installation + § Several windows, one world: canvasOnScreen + § Several displays, one machine: .spanning.
  • Screen saver (Sources/Ollin/Core/ScreenSaver.swift + Core/Resources.swift + OllinProjects/ProjectGenerator.planScreenSaver). A sketch wrapped as the machine's screen saver: ollin new <name> --kind screen-saver writes the package, the @objc principal class, the property list, and build.sh, which assembles and signs the .saver and installs it to ~/Library/Screen Savers. SketchSaverView is the host (an open ScreenSaverView whose one override is makeSketch()), building an OllinMTKView + SketchRunner on startAnimation(). It lives in the core rather than a satellite, behind #if canImport(ScreenSaver), because it needs the internal seams (makeOllinMTKView, setProjection) and is the same class of thing as OllinApp.run; the framework it drags in is in the dyld shared cache, so every sketch linking it costs nothing. Load-bearing: the package builds a .dynamic library product with -Xlinker -bundle, which is what makes the Mach-O an MH_BUNDLE (filetype 8) rather than a dylib — verified, and the reason no hand-written swiftc link step exists. Bundle.module cannot serve a plug-in: the accessor SwiftPM generates checks only Bundle.main.bundleURL and the machine's own build path, and in a plug-in Bundle.main is the host's (legacyScreenSaver.appex), so every core resource lookup goes through OllinResources.bundle instead, which adds Bundle(for:)'s own resourceURL to the candidates and keeps Bundle.module as the last of them (an ordinary run resolves to exactly the bundle it always did); build.sh copies every *.bundle the build produced into Contents/Resources, so a satellite's resources travel too. Counterfactual-verified both ways: with the build path hidden the saver still renders (its own copy served it), and with both gone it stops at unable to find bundle named Ollin_Ollin. The script asks swift build --show-bin-path rather than assuming .build/release (the two build systems the toolchain ships differ). The canvas takes no input (OllinMTKView.ignoresInput: hitTest nil + acceptsFirstResponder false) — a screen saver ends on a key or a click, and a canvas that answered would eat the one that gives the machine back. stopAnimation() is not the end: it fires for the System Settings preview and stays quiet when somebody wakes the machine, and the system builds a fresh view every time while releasing none, so teardown runs from viewDidMoveToWindow (window gone → drop canvas, delegate, runner) plus the com.apple.screensaver.willstop distributed notification; a copy left drawing competes with its own replacement for the GPU. A sketch that is not .resizable is fitted, not stretched (setProjection(.direct), the corner-pin path's .fit corners), since a display is almost never the shape of a canvas; the generator writes .resizable into the sketch unless a canvas was asked for. The plist name and the @objc name are the only link between the two files, and a mismatch fails quietly: Bundle.principalClass hands back some class rather than nil, so the test compares the name (sabotage-verified red). The host disables library validation and holds a read-only exception for the whole filesystem, so an ad-hoc signature loads and any asset opens, but a saver cannot write (no export, take, or checkpoint). ScreenSaverHostTests (7, the input opt-out and the teardown each verified red by sabotage) + ScreenSaverProjectTests (8, pure) + GeneratedProjectBuildTests.aGeneratedScreenSaverBuildsAndLoads (runs the emitted script, then loads the .saver and reads back its principal class — safe because that target links the generator and never the framework); no pixel snapshot (the output is files). Open: the real host has not run one — screenLock delay is immediate on this machine, so starting the saver locks the Mac. Docs/Output/ScreenSaver.md; Guide Ch 44 § Living in the system: a screen saver.
  • Sketch as an app (OllinProjects/ProjectGenerator.planMacApp + ProjectKind.macApp). A finished piece wrapped as a signed, double-clickable .app for a Mac without the toolchain: ollin new <name> --kind mac-app writes the mac sketch's package unchanged (the sketch keeps @main and its window; an ordinary executable target) plus the whole wrapper, which is Info.plist + build.sh and nothing else — the pair is self-contained by design, so it can be copied into any sketch folder of the same shape. Load-bearing: build.sh copies every *.bundle from swift build --show-bin-path into Contents/Resources (run from a bundle, the binary resolves the framework's resources through the app's own Resources folder; without them the app opens and renders nothing); the icon is the sketch — the freshly built binary renders itself (--export … --frame 120), sips squares and scales it, iconutil folds the iconset into AppIcon.icns — with an author's own AppIcon.icns beside the script always winning and a failed render never failing the build; signing is ad-hoc by default (--sign - --timestamp=none) and the script states the this-machine-only limit at every build, while --sign <identity> applies --options runtime --timestamp (the two things the notary checks) and --notarize <profile> (refused up front without --sign — ad-hoc can never pass) submits the ditto zip via notarytool --wait, staples, and re-zips to hand over; the plist is APPL with NSHighResolutionCapable (without it the canvas renders scaled on Retina), and camera/microphone usage lines follow the wired capabilities (vision → NSCameraUsageDescription, audio → NSMicrophoneUsageDescription; a bundled app that asks without one is killed, not prompted). AppProjectTests (8, pure) + GeneratedProjectBuildTests.aGeneratedAppBuildsSignsAndRuns (runs the emitted script, codesign --verify --strict, then renders from inside the bundle — the proof the resources traveled); no pixel snapshot (the output is files). Docs/Output/App.md; Guide Ch 44 § An app to hand somebody: signing and notarizing.
  • The sketch on a phone (Apps/OllinSketchApp the reference app; OllinRuntime/PhoneProject.swift + Sources/OllinTether behind ollin phone; Core/SketchCanvasUIKit.swift the touch twin). First light 2026-09-02 on an iPhone 15 Pro Max: the framework builds for iOS 26 and a sketch renders on the phone's own GPU through SketchView in a WindowGroup (the app owns @main; some SwiftUI.Scene, because the framework has a Scene), one finger driving mouseX/mouseY/mouseIsPressed and a stylus pressure. Load-bearing: a phone loads no pushed dylib (refused outside the signed bundle since iOS 18, DTS-confirmed), so the live host's swap cannot run there and ollin phone is a rebuild and reinstall on every save (measured: the framework ~80 s once, then 5 to 7 s build + ~2.5 s install over Wi-Fi), with the runner's checkpoint (beginInstallation(Installation(checkpoint: .every(seconds: 1))), public now) carrying the clock, the seed, the @Param values, and the @Saved state across each relaunch, so it reads as a reload; an app's container survives a reinstall under the same bundle id, which is what keeps the checkpoint file. The build targets generic/platform=iOS, never the phone by id (a phone that has left the network would otherwise fail the build rather than the install, which is the step that needs it); the sketch folder and the checkout are symlinked into the project folder (Sketch/, Ollin/) and the spec names everything through those links, because xcodegen's relative-path routine loops forever when a source path and the project folder share only the root (an absolute path to the sketch hung it from /var/folders and from /private/tmp alike; measured 2026-09-02), the tether rewrites a diagnostic's link path back to the author's file, sibling assets are carried as resources through the same link, and the host defines Bundle.module as .main so in: .module resolves; one bundle id (dev.ollin.OllinPhone), so the phone holds one app that is the last sketch tethered, its display name the sketch's type; the parameters are the shipped RemoteInspector on 9330, proxied to localhost through usbmux when the phone is on the cable. Ten satellites reach the phone (Remote, Physics, OSC, MIDI, Controller, Video, Bluetooth, Audio, Vision, Haptics); five are refused by nature (Camera, Syphon, Screen, Record3D, Phone); the rest link untried and are named. The simulator cannot run it (family 2: no cube arrays, no MetalFX); devicectl launches nothing while the phone is locked. Mac Catalyst runs it (an iPad app on the Mac, the shape a Playgrounds App Project previews in there), held by two invariants: a platform seam keys on os(macOS), never canImport(AppKit), since Catalyst imports AppKit and still cannot call NSWorkspace; and every runtime shader compile passes ollinShaderCompileOptions() (Renderer/PlatformGPU.swift), since the Catalyst compiler defaults to Metal 2.2 where the desk defaults to 4.1, and the built-in library needs the ray-tracing namespace (2.3) and the mesh stages; the helper raises the version to 4.0 only when the default sits below it, so the desk and the phone compile exactly as before. Proven on the M2 (the rings sketch in a Catalyst window); no gate compiles for Catalyst yet. Scripts/preflight.sh compiles the framework for iOS on every framework change (a macOS-only SwiftUI call broke it within a day of the last hand check). PhoneProjectTests (12, pure) + PhoneProjectBuildTests (1, the emitted host compiled for generic iOS, skipped without xcodegen); no pixel snapshot (a device). Docs/Tools/OnThePhone.md; Guide Ch 44 § In your pocket: the sketch on the phone. Failure shape (2026-09-22): isRunning, unavailableReason and start(), the same shape as OSCQueryServer; a .failed listener is dropped so start() can try again, and stop() resets boundPort (it used to keep the old port). RemoteWireTests.aPortAlreadyTakenIsSaidAndTriedAgain.
  • The phone as the sketch's screen (OllinPhone: PhoneScreen.swift, PhonePictureEncoder.swift, PhonePictureSender.swift, the shared PhonePictureCoding.swift; the app's SketchScreen.swift and Sketch mode). device.show(self) keeps the sketch on the Mac (its live reload, inspector, console) and shows its frames full screen on Ollin Capture, with nothing built or installed for the phone: it answers how a piece looks and plays in the hand, never whether the phone's GPU keeps up (the installed loop answers that). Load-bearing: the frame is copied from the frameRendered(_:texture:) texture into an IOSurface pixel buffer from the compressor's own pool by one Metal pass (an 8-bit canvas read through a plain view, bytes passed through; a float canvas clamped and sRGB-encoded in the pass) and the copy waits for completion inside the hook (the renderer takes the texture back on return, and hazard tracking does not reach another queue); HEVC in real time with no reordering, a keyframe at most a second apart, 709 primaries and matrix with an sRGB transfer tag, fitted under 1920 px and rounded to even, about 0.125 bit a pixel a frame clamped to 4 to 40 Mbps; pictures are sent over their own connection, port 1339 (PhoneWire.picturePort), in the request framing as request kind 4, so a picture never waits behind a sensor read and a mode request never waits behind a picture; a keyframe carries its parameter sets (VPS, SPS, PPS) and nothing else does, and a keyframe with no sets is refused at decode; a frame is skipped before it is compressed, never after (wantsRenderedTexture asks for a frame only while the phone reports Sketch mode, the sender is connected with nothing queued, and nothing is in flight; a P-frame already made is never dropped, since every later picture is built on it), and a picture too big for one wire frame is dropped with the next forced to a keyframe; keyframes are also forced on (re)connect and when the phone comes back to Sketch mode; the sender dials only while a drawn frame wants it (keepWanted() from beforeDraw, released after two seconds) and the phone's picture listener keeps only its newest client, which together hand the phone to a reloaded sketch without a ping-pong; every usbmux socket sets SO_NOSIGPIPE (a write to a phone that just unplugged otherwise ends the process). The first finger is the pointer through the pure PhonePointer rule fed on the reader thread (so a tap between two draws still presses and releases): a second finger never moves it, a finger still down when the holder lifts never takes over, fingers resting at arming are passed, the fingers press only while the phone shows the sketch, and a held press is always let go; the phone's touch pad in Sketch mode is the picture view itself (aspect-fit, one view whatever the size so the display layer is never rebuilt), so -1…1 is the canvas; Sketch.setMouse/handleMouseButton/setPressure are package for it. On the phone the sample buffer is rebuilt by the same shared file the Mac tests run and decoded by AVSampleBufferDisplayLayer's renderer on the server queue, starting at a keyframe after an attach or a failure. PhonePictureRoundTripTests (4, device phase beside SpatialVideoTests: rendered frames through the encoder, the wire in uneven chunks, the phone's rebuild and a decoder, mean error about 1.3 levels and 38.6 dB, the plain-view rule red by sabotage; a mid-stream join waits for a keyframe; the extension and its sender thread over a socket pair, the backpressure rule red by sabotage) + PhonePictureRequestTests (10) + PhonePointerTests (6) + 3 PhoneTouchTests; the app built for generic iOS; the cable and the eyes are owed at the desk. Example 3D/Phone/PhoneCanvas; Docs/3D/Phone.md § The sketch on the phone's screen + Docs/Tools/OnThePhone.md § Without installing it; Guide Ch 44 § Leaving it on the Mac: the phone as the sketch's screen.
  • Sketch as an app for the phone (OllinProjects/IOSAppSource.swift + ProjectKind.iOSApp; the CLI's --team, the window's Signing team field). ollin new <name> --kind ios-app writes a sketch as an iPhone and iPad app: the sketch (its @main dropped, .resizable declared, since the screen is the canvas), a host in the reference app's shape (<Name>App.swift: @main on the App, some SwiftUI.Scene, SketchView(<Name>()), safe areas ignored, and Bundle.module defined as .main, which an Xcode target never synthesizes), the property list (full screen, both device families, arm64 + metal required, camera/microphone/Bluetooth usage lines following the wired capabilities), and the project.yml that xcodegen generate turns into the project; no manifest and no script. Load-bearing: it is an Xcode project spec, not a package (an app is installed, never run), and the framework is named by its absolute path in packages: the way the manifest kinds name it, which xcodegen takes in a second from any folder; only a cross-root source trips its relative-path loop, and every source here sits inside the project folder. Resources are named before the group and excluded from it (a folder entry named after the group adds nothing, measured; a folder is given buildPhase: resources so its files land flat where a name-and-extension lookup finds them), and a .metal goes through a copyFiles phase (destination: resources), never the resources phase: Xcode runs its build rules over that phase too and compiled effect.metal into a library (unknown type name 'ShaderInfo', 2026-09-07), while the framework reads a shader's source at run time; the built app now carries it as ASCII text, and PhoneProject got the same fix. A capability that wraps a Mac service is refused at plan time with its reason (Capability.staysOnTheDesk: Syphon, screen capture, the virtual camera, both ends of the phone link), an example's imports included; every template fits. The team is the one question no other kind asks: the --team flag, then OLLIN_TEAM (the variable ollin phone reads), else the spec carries a marked, commented DEVELOPMENT_TEAM line and the README says Xcode's own answer is lost on the next generate; never the reference app's team from the checkout, which is somebody's own. A --remote framework pins minorVersion:. IOSAppProjectTests (17, pure) + GeneratedProjectBuildTests.aGeneratedPhoneAppBuildsForIOS (the spec generated and built for generic iOS unsigned in the tether's shared derived data, then the app checked for the framework bundle, the shader as text, and the flat folder; skipped without xcodegen); no pixel snapshot (the output is files); not yet run on a device (the desk backlog). Docs/Tools/ProjectGenerator.md + Docs/Tools/OnThePhone.md § In an app of your own; Guide Ch 44 § In your pocket: the sketch on the phone.
  • Wallpaper (Sources/Ollin/Core/Wallpaper.swift + OllinProjects/ProjectGenerator.planWallpaper + ProjectKind.wallpaper). A sketch run as the desktop wallpaper: OllinApp.runAsWallpaper { Sketch() } opens one borderless window per display at desktop level (CGWindowLevelForKey(.desktopWindow) — under the icons, over the system's picture), each display getting its own sketch instance from the closure; ollin new <name> --kind wallpaper writes the project — the mac app's package, plist, and build.sh plus a Main.swift wrapper carrying @main (the kind strips the sketch's own entry point, carriesEntryPoint: false, since two refuse to build). Load-bearing: the windows take no input at all (ignoresMouseEvents + ignoresInput; borderless, so never key) — the desktop must stay the desktop; collectionBehavior is [.canJoinAllSpaces, .stationary, .ignoresCycle, .fullScreenNone] so wallpaper never scrolls away with a space swipe; the app is an accessory (LSUIElement in the emitted plist, .accessory policy in the host — it must NOT route through OllinApp.run, whose delegate forces .regular), so the sparkle status item's Quit is the only way out and that menu is load-bearing, not chrome; panes rebuild on didChangeScreenParametersNotification only when a screen frame actually moved (the notification also fires for changes that move nothing, and a rebuild restarts the piece); fitted-vs-filling follows windowMode exactly as the screen saver (sketchKeepsItsShape; the kind injects .resizable via fillsTheDisplay); the wrapper calls handleCommandLine first, which keeps every export flag working and is how build.sh renders the icon. WallpaperHostTests (5) + WallpaperProjectTests (5, pure) + GeneratedProjectBuildTests.aGeneratedWallpaperBuildsAndRuns (runs the emitted script, checks LSUIElement and the signature, renders from inside the bundle); no pixel snapshot (the surface is the desktop). Open: the eyes-on desk check — the piece visibly behind the icons — waits for an unlocked screen. Docs/Output/Wallpaper.md; Guide Ch 44 § Wallpaper and a menu-bar strip.
  • Menu-bar strip (Sources/Ollin/Core/MenuBar.swift + OllinProjects/ProjectGenerator.planMenuBar + ProjectKind.menuBar). A sketch as a small live strip among the status items: OllinApp.runInMenuBar(width:) { Sketch() } (56 pt default, height the bar's own) runs the canvas inside the item's button with the quit menu hung on the item; ollin new <name> --kind menu-bar writes the same app-shaped project as the wallpaper kind, pointed at this host. Load-bearing: the canvas is ignoresInput so a click falls through to the button — the click opening the menu is the way out, and the strip never takes the keyboard from whatever is being typed; the strip draws at 30 fps and is never retimed to its display — a runner.displayChanged call after the rate is set silently restores the display's maximum (a real bug, caught by theStripDrawsAtAWorkdayRate); under .resizable the canvas is the strip's own points (the sketch sees ~56 by 22 and width/height just work), a declared canvas is fitted into the strip; with no Metal device the item still goes up with its menu, so the program stays quittable rather than kill-only. MenuBarHostTests (5) + MenuBarProjectTests (3, pure) + GeneratedProjectBuildTests.aGeneratedMenuBarStripBuildsAndRuns; no pixel snapshot (the surface is the menu bar). Open: the eyes-on desk check — the strip beside the clock — waits for an unlocked screen. Docs/Output/MenuBar.md; Guide Ch 44 § Wallpaper and a menu-bar strip.
  • Widget (Sources/Ollin/Core/Widget.swift + --export-widget in Core/SketchView.swift + OllinProjects/ProjectGenerator.planWidget + ProjectKind.widget). A sketch shown as a desktop widget: ollin new <name> --kind widget writes an app with a WidgetKit extension packed inside it, and the framework draws the run the system shows. The design answer this surface forces, since a timeline refreshed in minutes cannot carry motion: a widget piece is one that changes rather than one that moves, so one picture is drawn per moment and the sketch says how far apart the moments sit (open var widgetTimeline: WidgetTimeline, .every(minutes:count:) / .every(hours:count:), 15 minutes by 4 the default). Load-bearing: the clock is the time of day (time is seconds since midnight of the moment being drawn, deltaTime the spacing, frameCount always 1) because the system throws a run away and asks for another whenever it likes, and an elapsed clock would jump the piece back to zero every time it did; the moments land on a grid counted from midnight (WidgetTimeline.gridMoment), so two passes agree and a quarter-hour piece steps at the quarter hours, with one short step at midnight for a spacing that does not divide the day. Each picture gets a fresh sketch, set up and drawn once, so a moment draws the same picture whether it opened a pass or closed one, and nothing carries between them (one renderer is shared across the pass, the contact sheet's rule). Sketch.date is the moment the frame stands for (now at a desk, the future moment in a widget) and scheduledPeriod/scheduledProgress read it, since a widget's pictures are drawn before their moments come and Date() is then wrong, by up to 45 minutes under the default run. The generated project is three targets because a widget is two programs (the system finds a widget through its app) and two targets cannot share a folder of sources: the sketch is a library both reach through one generated public line (<T>Piece.make()), never a public sketch class, which would force public override func draw() on every override and make this kind's sketch unlike every other kind's. build.sh puts the extension at Contents/PlugIns/<T>Widget.appex and signs it before the app (signing the app seals what is inside it), with Widget.entitlements declaring the sandbox an extension is refused without; both bundles get a copy of every resource bundle, since each is a running program. The generated file spells WidgetKit.Timeline and SwiftUI.Color out, because Ollin has a Timeline and a Color too and all four are in scope there. --export-widget <dir> [--size WxH] [--frames N] writes the whole run as PNGs named by the moment, on any sketch, which is how a piece on this surface is judged in a second rather than a quarter of an hour. WidgetTimelineTests (10, the grid and the same-moment-same-picture invariant verified red by sabotage) + WidgetProjectTests (11, pure) + GeneratedProjectBuildTests.aGeneratedWidgetProjectBuilds + .aGeneratedWidgetBuildsAndRuns (milestone-gated: the appex inside the app, its extension point, its sandbox entitlement, the app's signature with it inside, and the run drawn from the bundled binary); no pixel snapshot (the output is files). Open: nothing here has been placed on a desktop — the widget gallery and how a picture reads at widget size want eyes. Docs/Output/Widget.md; Guide Ch 44 § A widget, and what it does to a sketch.
  • Single-file sketches (the ollin command, Scripts/ollin). One loose .swift file is a whole sketch, run from any directory: ollin <file.swift> opens it in the live host (all flags pass through, so every export flag works on the loose file; --installation / --calibrate route instead to OllinRun, which compiles the file once and hands the sketch to OllinApp.run — the only path that reads a declared Installation, so it is how a loose file goes on a wall, and it reloads nothing by design), ollin new [name] writes an executable starter file (class named from the file name), ollin install [dir] symlinks the script onto PATH. Load-bearing: the script resolves the repo from its own symlink-resolved location and execs swift run --package-path, so a PATH symlink is the whole install and git pull self-heals on the next run — capture ${0:A} at top level (inside a zsh function $0 is the function name) — and sketch paths are absolutized before handoff (the host process runs with the repo as its package, not the caller's cwd). Hashbang: #!/usr/bin/env ollin + chmod +x makes a sketch directly executable; SketchLoader neutralizes a leading #! to // in place (same length, so diagnostics keep exact line/column) and compiles from the work-dir copy, since swiftc allows a hashbang only in a main file — applies to .file and .source inputs (the live-coding editor buffer carries the line too). Pinned by two --selftest phases (hashbang renders; a broken hashbang sketch reports Sketch.swift:5:). Docs/Tools/SingleFile.md; the Guide's Ch 1 points at it.
  • A package's sketch, live (SketchTarget, SketchChoice, SketchLoader.Scope, all in Sources/OllinRuntime/SketchTarget.swift and SketchLoader.swift). A sketch file inside a SwiftPM target's folder compiles with every .swift file of that target, under the target's Swift settings, and a save to any of them reloads; ollin <package folder>, a bare ollin inside a package, and --target <name> choose the sketch, read the same way by OllinLive, OllinRun, and OllinLiveCoding (SketchChoice.arguments / .resolve). Load-bearing: .target is opt-in per loader, set only by the three hosts that run what a person names; the gallery, the Guide figures, and the generator preview stay .file. The manifest is read by swift package dump-package under a private --scratch-path (sharing the package's .build waited out a whole swift build, 32 s against 0.25 s), cached per package root until Package.swift's modification date moves, and a read that fails fails the compile with SwiftPM's words rather than falling back to one file. Paths compare through realpath (Foundation's resolver strips /private where the folder walk keeps it, and the sketch compiled twice: "filename used twice"). The walk is fresh per compile (a new file joins), skips hidden entries, exclude:, and resource paths, honors sources:, and leaves main.swift out (a library compile refuses top-level code, and the factory is the entry point). Flags: -swift-version (the target's setting, else the newest listed mode the compiler has, else 6 from tools 6.0), -D, upcoming and experimental features, default isolation (nonisolated spelled out), strict memory safety, the Swift tool's unsafe flags; a setting conditioned on another platform is dropped, one on a configuration follows the compile (.none is debug). The factory shim is Swift 6 clean: both declarations nonisolated, the address crossing MainActor.assumeIsolated as a UInt, since a pointer is not Sendable and the old shim failed under a package's Swift 6 mode. Declared resources are linked into <work>/Resources as the bundle lays them out (.process flattened, .lproj whole, .copy whole, the first name wins, embedInCode skipped) and Bundle.module points there; a target declaring none keeps the sketch's folder. A compile that fails on no such module 'X' with X a manifest dependency gets the sentence naming the limit (another target or package needs swift run). Sketch classes are read with comments and strings blanked (SourceRegions.code(in:), SketchDeclarations), and a folder offers a target only when a Sketch subclass carries @main, or one exists and nothing else does (a host app's test sketches never surface). OllinLive watches the target's folder when the sketch sits below it; OllinLiveCoding carries the run only when the target's other files read as they did at the stage's build, and records an edit to one as the change itself (.restarts), so the toast says "restarted" rather than naming a block. Measured before the change on a generated project with a helper file: cannot find 'breath' in scope, and a .process("Images") picture missing; after, both render. SketchTargetTests (27, three planted defects red), SketchCompileArgumentsTests, the OllinLive --selftest target phase and the --watchtest save a folder below, the OllinLiveCoding --selftest target phase (red with the guard off). Docs/Tools/SingleFile.md § Growing into a package and § Where the live route stops; Guide Ch 1 § When one file isn't enough.
  • Project generator (ollin new + ollin generate). A ready-to-run project from a few questions, over three targets: OllinProjects (the model and emitters, Sources/OllinProjects/), OllinNew (the CLI behind ollin new), and OllinProjectGenerator (the SwiftUI window). Three catalogs are the extensible axes: ProjectKind (.singleFile / .macSketch / .macApp / .inPackage / .extensionPackage / .screenSaver / .wallpaper / .menuBar / .iOSApp available; .visionOSApp / .arEffect declared-but-gated, each carrying the platform leg it waits on), ProjectTemplate (10 ready-made starting points, each a whole small sketch), and Capability (asset folders + satellite links, contributing an import, a manifest entry, a folder, and one commented starter line in setup()). Load-bearing: OllinProjects depends on nothing but OllinShaderText, never Ollin (both only produce text), so it builds in ~1s and its tests are pure; the plan/write split (ProjectGenerator.plan is pure, write only puts planned files down) is what lets the window preview the exact file list and the tests assert without disk; write refuses any file that already exists rather than overwriting; a generated manifest points at the framework's own path (the CLI/window walk up from their binary to find it) so the project builds with nothing to fetch, with --remote/--framework-path the overrides (--remote pins the newest tagged minor, FrameworkSource.latestRelease — bump it when a new minor is tagged); asset folders are .process (SwiftPM flattens a processed folder into the bundle root, which is what puts a dropped file within reach of Bundle.url(forResource:withExtension:), since Ollin's loaders pass no subdirectory:) while a .metal is a flat .copy file (a processed .metal compiles to a metallib, and a copied folder keeps its nesting) — so the per-folder placeholder note is named after its folder, never README.md, or two folders collide on one bundle name; @main is inserted on the class, under the template's doc comment; a template that declares its own canvasSize never gets a second one. The window is three resizable columns (stage takes the slack) built to the design review: one sectioned source list (Templates group + the examples' own two-level catalog, one filter matching group names too, selection scrolled into view), the kind menu in the title bar with the gated kinds under a divider (a roadmap in a menu reads as a roadmap; in the main flow it reads as a broken control), a status line under the stage (built-in-Ns vs from-cache), the 3D chain as a strip under the stage laid along the constraint's own direction (Geometry → Finish → Extras, arrows, chips; a blocked chip names its cause and the blocker reports blocks N via ThreeDRecipe.blocker(of:)/blocks(_:); the reason line sits outside any scroll view, which is how the old clipped-hover-text gap disappears), everything else behind one Wire in disclosure that still reports what the starting point brought, the file list above the fold with the no-overwrite guarantee under it, and the dated serial as the field's placeholder rather than a value. OllinInspector.accent (purple) is applied where it means something (list selection, chips, Create), never as a root .tint, which would color every neutral native control. It previews a starting point by compiling and running it through SketchLoader (the gallery's path, hence the satellite links + -export_dynamic), so one that stopped compiling shows up there. Starting from an example (--from Group/Name, the Examples tab) copies a maintained sketch instead of a template: the type is renamed by whole-word replacement (a sketch may name its own type inside itself), its imports decide the linked satellites, its sibling files are copied (GeneratedFile.copiedFrom) and declared .copy, and the header comment travels verbatim (for a ported or homage sketch that is where the credit lives). ExampleSource.discover compares path components after resolving symlinks (a /var temp dir walks out as /private/var, and a string-prefix test then matches nothing). Dated serial names (ProjectNaming.nextSerial) are the default when none is given: Sketch<year><nnn>, one past the highest already in the destination counting both folders and .swift files, the year passed in rather than read from a clock so it is testable. The window remembers the destination (@AppStorage) and re-suggests the next serial after each Create. The 3D options surface Docs/3D/Combining.md as data (ThreeDOption/ThreeDRecipe): each piece carries requires/conflicts plus the one-sentence rule, and the rules are a hierarchy, not a flat set (geometry is never blocked and settle() fixes the finish and extras under it; a finish is blocked by its geometry; an extra by either) which is what keeps the panel navigable rather than refusing the first click; ThreeDSource writes the sketch from the recipe, and everyGeneratedSketchCompiles compiles every extra on its own and every allowed geometry and finish with all its extras at once (18 recipes, in the one generated package that also holds the templates and the imported shader and scene sketches); ThreeDSource writes each extra as a fragment that reads neither the finish nor the geometry, which is what lets those 18 stand for every combination the rules permit (a rule that allows something uncompilable is the worst failure here, since the window would offer it). A fragment that starts reading the finish or the geometry needs the full sweep back. The build test is the load-bearing one (GeneratedProjectBuildTests): all 10 templates compile as targets of one generated package (one framework build), plus one full generated project built end to end. Scripts/ollin new routes to OllinNew (a .swift name still writes one loose file, unchanged), ollin generate opens the window. Joining an existing package (ProjectKind.inPackage + PackageHost) is the one path that changes a file rather than only creating them, so it is deliberately timid: it refuses a host that does not already depend on Ollin (a target added there could not import the framework), inserts one target by matching the targets: array's own brackets (never the first ], which would land inside a target's dependencies:), preserves the closing line's indentation by inserting after the last entry rather than before the bracket, and when it cannot read the list confidently it writes the sketch and hands the stanza over to be pasted instead of guessing. GeneratedEdit carries the whole new text (worked out at plan time so the window shows it first) and edits apply after the new files. The CLI and the window both default to it whenever the destination sits inside such a package, since a folder of sketches is one package with a target each (the framework then builds once for all of them). Gotcha: relativePath(of:) compares twice, because symlink resolution only applies to path components that exist, so a folder about to be created keeps a /private prefix its own parent has already lost and a single comparison silently yields an empty path:. Two catalogs grow by different rules, and the difference is load-bearing: the examples list is discovered (ExampleSource.discover walks Examples/ for Sketch.swift at launch), so a new example appears with no generator change, and an example's import lines are read as the union with the capability catalog, never filtered by it (a filter drops a satellite that has no Capability entry yet, and since the import is copied verbatim the new project then fails to build; unknownSatellitesSurvive pins it). The 3D catalog is hand-kept (ThreeDOption.all + ThreeDSource's fragments), mirroring Docs/3D/Combining.md: a 3D capability that changes what combines updates that page AND this catalog, the same hand-sync rule ollin_sdf_distance carries, and everyGeneratedSketchCompiles then compiles a new option alone and in every pair's full stack automatically. ProjectGeneratorTests (20) + ExampleAndNamingTests (12) + ThreeDRecipeTests (7) + PackageHostTests (8) + GeneratedProjectBuildTests (11); no pixel snapshot (the output is files). Docs/Tools/ProjectGenerator.md; Guide Ch 1 § When one file isn't enough.
  • Bringing a GLSL shader over (ollin new --from-shader). A pasted fragment shader translated to Metal and handed over as a project (Sources/OllinProjects/GLSL/: GLSLTranslator / GLSLCompat / ShaderImport / ImportedShaderSource over the ShaderLexer / ShaderScan pair in Sources/OllinShaderText/, the text target the web page's reverse rewriter shares, plus ShaderSourceReader on the CLI side). The shape read is the common web one — mainImage(out vec4, in vec2) over the iTime/iResolution/iMouse/iFrame/iChannel0-3 values a site supplies — mapped onto Ollin's shipped shade(uv, info); how many channels it reads decides the routing (0 generator / 1 filter / 2 combine, the user-shader path's own rule), so no new render plumbing exists. Load-bearing: it is a lexer plus token rewriter, never a parser and never regex (a full grammar is far more to keep right than the rules need; blind substitution is the thrash trap), and every edit replaces the text of a token already there — nothing is inserted or removed — which is what keeps a pass's indices valid for the next one and preserves the author's spacing, blank lines, and comments (the property that makes the output editable source rather than machine output; theAuthorsLayoutAndCommentsSurvive pins it); a token carries its original bracket in a field beside its text (ShaderToken.bracket) so a rule may hang a whole argument off a ) or turn a ( into a { and every later structural walk still reads the shape the author wrote — matching on text breaks exactly this; an emptied token stops counting (hasText), or a blanked precision …; swallows the statement after it and mis-attributes its line. mod is the one rule that silently changes a picture rather than failing to build (GLSL floors the quotient, Metal's fmod truncates, so they disagree for negative operands — the ordinary case when tiling a plane reaching left of the origin): the flooring form is written out as ollin_glsl_mod, never the built-in, measured at max 255 / mean 63.65 over 576k pixels on a tiled ramp — and note a sin/cos tile spanning exactly one period cannot see the difference, so a counterfactual probe must not go through one (the first one did and read inert). A layer read turns the coordinate over (a texture is measured from its bottom edge, an Ollin layer from its top) or every imported filter arrives upside down; info travels as a parameter only as far as it is needed (a supplied value is a global in GLSL and cannot be one in Metal, so a call-graph closure adds it to the functions that read one and the functions that call those, leaving pure sdf/noise helpers untouched); alpha is held at 1 (the source site shows every pixel opaque, so that is what reproduces the look). Nothing is dropped in silence — what cannot be carried lands in the file as NOTE(ollin) / TODO(ollin) and prints before anything is written (several passes, >2 channels, a self-declared texture, a written file-scope global (a const one becomes constant), texelFetch, iDate/iSampleRate, the mouse button, a 4x4 inverse). A plain main with gl_FragCoord/gl_FragColor or a declared out is reshaped into the same form. License hygiene is the point, not a footnote: nothing is bundled, the emitted header carries title/author/URL and says the terms travel with it, and a URL is fetched only through the site's own API with the user's own SHADERTOY_API_KEY. A .macSketch/.inPackage project keeps the shader as a flat .copy imported.metal (a processed .metal compiles to a metallib — the shipped rule) so it hot-reloads with no swiftc pass; a single loose file carries it inline in a raw #""" literal (no target to declare a resource on, and a raw literal keeps a backslash a backslash). ShaderImportTests (17) + ImportedShaderProjectTests (4) in OllinProjectsTests (pure), and ShaderImportRenderTests (10) in OllinTests compiles every claimed shape through composeUserShaderSource + makeLibrary on the real device — a rewrite that merely looks right is worth nothing (which is why OllinTests depends on OllinProjects). No pixel snapshot (the output is files). Docs/Tools/ShaderImport.md; Guide Ch 18 § Somebody else's shader: GLSL import.
  • Bringing a 3D scene over (ollin new --from-scene). A glTF or USD scene written out as the sketch that draws it (Sources/OllinProjects/Scene/: ImportedScene + ImportedSceneSource; the reader in the new OllinSceneImport target; ScenePlacement.swift in the core). The split is the design: structure becomes source, geometry stays in the file. The camera prints as a Camera3D with its own numbers, each light as its own factory, and each node as a withState block of the moves that put it where the file put it, nested as the file nests (so a group still turns as one thing) — while the sketch reads the file once for its meshes and places them itself through a generated drawPart(_:), because a mesh is not something a person edits as text. Load-bearing: OllinProjects still depends on nothing, so the reader is its own tiny target (Ollin + OllinProjects) that both faces of the generator share, and ImportedScene carries plain numbers, never framework types; the placement split is SceneNode.placement (package, Sources/Ollin/3D/ScenePlacement.swift) reusing the shipped Scene.decomposeTRS rather than a second decomposition, and it reports isExact == false by recomposing and comparing, so a shear is said rather than silently mis-drawn; the emitted walk and the generator's naming walk must agree (each node before its children), which is why the part names are written into the sketch as a literal list rather than re-derived at runtime (a repeated or blank name still gets its own key); a move under 0.00005 is not printed (it would print as a call that does nothing), an axis-aligned turn reads as rotateX/Y/Z (with the negative axis flipping the angle), and scale always takes the three-argument form (the one-argument one is the 2D call and would leave z alone). withState saves modelMatrix, which is what makes the nested blocks reproduce drawScene's own composition exactly. What cannot come over is noted per node where it happens (a mesh with several materials draws in the first; a shear) and per scene in the header (animation, skins, blend shapes, and that materials come with their meshes, which is why no fill is emitted). A scene with no geometry needs no file at all. Verified against drawScene on the bundled LoadedScene: identical but for the one multi-material slab, which is the documented loss. SceneSourceTests (13, pure) + SceneImportTests (11, against the loader; the placement probe verified red by sabotage, naming the node it moved) + a GeneratedProjectBuildTests case compiling every light kind and both projections. No pixel snapshot (the output is files). Docs/Tools/SceneImport.md; Guide Ch 27 § A scene you can take apart: the scene written as source (a pointer; the SceneAsSource figure is on the reference page).
  • GUI / parameters (typed). The generic @Param wrapper/registry in the core (Param.swift) drives the shared inspector (Inspector.swift) in all three hosts; the control follows the property's type — Double slider / Int stepper / Bool toggle / ParamOption enum menu / Color well / Vector2 fields (+ style: .pad) / Vector3 / Rectangle / Insets / ClosedRange<Double> two-thumb slider / String field / style: .segmented / Palette swatch strip / Ramp gradient band / ResponseCurve graph and a list of numbers as bars (their own bullet below) / the ParamChoices named-catalog menu (Easing among them) — through the type-erased AnyParam / ParamControl surface, so hosts never touch Param<Value>; ParamValue is public, so a user type can conform by mapping onto an existing control kind (no custom rows), wrapped through the public constraints: initializer, the primitive the built-in kinds' shorter spellings stand on (the bare form serves a Void constraints type; ParamOwnTypeTests imports Ollin without @testable on purpose, since only a plain import sees what a sketch sees). icon: / group: decorate rows into titled group cards; numeric value boxes scrub (Option fine, Shift steps on round marks, click to type), and while typed in the up/down arrows step them, all through the pure ParamStepping (ParamNumberField.swift): a plain drag covers the range in a 250-point track and a stepped box goes no slower than a step per 8 points (an Int over 0...10000 crossed in thousands of points before), Shift keeps the plain pace on marks leaving four to forty across the range (a power of ten, in whole steps on a stepped box, counted from zero when zero is on the step's grid), an arrow takes the next mark strictly past the value (the step, or a tenth of Shift's mark; binary noise on a mark counts as the mark) and starts from a half-typed number, and the Int −/+ buttons repeat while held (buttonRepeatBehavior); a save's sentence (ParamSaveNote) clears on the next hand edit or any reset. ParamNumberFieldTests (the math) + ParamNumberBoxTests.theArrowsStepABoxToItsNextMark (arrow events through a real panel's event path). Rules: ParamChoices needs Equatable to find the current selection (a built-in Easing carries its name as its identity, and a closure-built curve a serial of its own, which is what puts the curves on the menu and what a tuned choice persists under; LightingPreset, Material, Fog, and Environment conform — Material puts the whole curated finish library on the menu, while the parameterized Material helpers (.glass(...), .metal(...), .skin(radius:)) stay off it, since a menu needs fixed values; Environment's roster is allBuiltins in browse order, so a .sky(...), an .hdri(...), or a built-in with lightingOnly() / highResolution(_:) applied is off it and reads as the first entry, which is why a sketch keeps the parameter on the plain built-in and applies the modifier at use; EnvironmentMenuTests); parameter values persist across reloads as ParamStored payloads (syncParams; a property that changed type drops its stale value so the new default wins); OSC/MIDI bind to Param<Double>, and smoothing stays Double-only. The swatch strip (Palette/Ramp, one ParamControl.swatches kind, count: bounding its add/remove) draws a ramp's band by sampling the value itself, never by fading between the stops, so the row shows the blend the sketch draws whatever space it mixes in; a dragged stop stops at its neighbors, so strip order is ramp order; both persist as one .colors(stops:space:) payload (a palette's space is nil, so a palette's payload restores into a ramp), and it has nothing between two values, so an automation holds it through a key. The SwiftUI layout gotchas (the scrub-pill .fixedSize(), the segmented ViewThatFits wrap) are in ARCHITECTURE.md. Headless gates: OllinLive --paramtest + ParamTests. A parameter read costs what a stored property costs (0.4 ns against 40.9 before, seven parameters read 160,000 times each on the M2, release): the getter is @inlinable, so a sketch compiles it for its own value type, and for a Double, an Int, or a Bool it is one relaxed atomic load of the value's bits (ParamValue.bits(of:) / value(fromBits:), stored by every writer inside the lock right after it moves current); a color, a vector, a palette, or text reads through the lock (lockedValue()). Every writer stores the value clamped and snapped, so no read clamps. Left generic across the module boundary the read cost 40 ns however it was stored, through the lock and the clamp or through the atomic alone, so the inlinable getter is the whole of the gain.
  • Curves and lists as parameters (ResponseCurve, Math/ResponseCurve.swift; the list conformance Array: ParamValue where Element: ParamValue & Comparable, Element.Constraints == ParamNumericConstraints<Element> in Live/Param.swift; the rows CurveParamRow/BarsParamRow in Live/Inspector.swift). Two ParamControl kinds, .graph and .bars, with payloads .curve([Vector2]) and .numbers([Double]) (coding keys curve, numbers). ResponseCurve is Fritsch-Carlson monotone cubic through its points: inner slopes the mean of the two secants (zero where they disagree in sign), a flat interval flattens both ends, a pair past the radius-3 circle scaled back, so it passes through every point, is C1, and never leaves a pair's band; out of range it holds the end heights; the final clamp is to 0...1, never to the pair's band (a band clamp hid a planted slope error as a flat top); points are held to the unit square, sorted stably by x, and a later point at an x already held is dropped; equality and hash are the points'. The graph draws by sampling the value itself (the ramp's rule), so the row shows what curve(t) returns. A dragged point, a typed place, an OSC point, and a phone drag all stop short of their neighbors (a handle's width, 0.001, 0.001, 0.01), because on a neighbor's place the curve keeps one of the two and the point an address names vanishes (an OSC test found it). A press away from every point inserts one on the curve and keeps dragging it; + inserts in the widest gap on the curve (curveGraphAdding), so nothing jumps; count: (default 2...12) bounds the buttons, and a clamp past it keeps both ends and drops the last inner points. A list's length is the declaration's: restored(_:keeping:) takes a longer payload's first values and keeps the current values past a shorter one's end; each element clamps and snaps through its own kind, Int rounds on the way in (a whole list's bars step by max(step ?? 1, 1), BinaryInteger tested on the element type), a payload with a value its element refuses restores nothing. Bars paint by drag and fill the bars a fast stroke skipped (barsPainting); a range holding zero grows bars from the zero line; beat marks only on a count of 8 or more divisible by four. Blends: Automation.blend and CueSheet.canFade move a list bar by bar and a curve point by point when the lengths match, and hold otherwise. Write-back (ParamWrite): a curve as ResponseCurve([(x, y), ...]) with bare whole numbers (the pairs' type is fixed by the call); a list by the scalar rule element-wise (whole stays whole while every value is whole and the literal had no point, otherwise a point on every value, since [1, 0] beside 0...1 reads as [Int]). --param reads a curve as height@place pairs (1@0,0.7@0.4,0@1, heights without places spread evenly, the color strip's spelling) and a list as comma-separated numbers (an empty field refused), and the listing writes both back pasteable. OSCQuery serves each as a container (f per bar, ff per point); the remote surface paints bars and drags points but adds none. The web page leaves both out (no control). CurveAndListParamTests, ParamWriteTests, OSCQueryTests.aListAndACurveAnswerPerBarAndPerPoint, RemoteWireTests.aCurveAndAListTravelBothWays, OllinLive --paramtest and --savetest (a curve, a fraction list, and a whole list written, recompiled, and read off a fresh sketch); Examples/Live/Parameters; Docs/Helpers/Parameters.md#response-curve and #bars, Docs/Helpers/Animation.md#response-curve; Guide Ch 38 § Shapes you draw: a curve and a row of bars. The rows' drag by hand is a desk check.
  • Performance stats (a shared FrameStats feeding every host surface). The runner fills one FrameStats (@Observable: fps, CPU frame time, draw counts, clock, canvas size) via the built-in StatsExtension on the extend seam; the OllinLive sidebar, the gallery's right sidebar, and — standalone — a detached frosted NSPanel (⌘/ via OllinHUDCommands) read it. A host with its own inspector sidebar opts the panel off (SketchView(showsInspectorPanel: false)) and omits OllinHUDCommands. Stats are debug chrome, not in-canvas drawing — SwiftUI siblings of the Metal view, so they never land in exports. Gotcha: the CPU-ms must time performDraw() alone — timing across renderer.render() includes the triple-buffer semaphore (vsync) wait and pins to ~1/fps, telling you nothing. SketchView ZStacks host chrome over a private MetalCanvas — the NSViewRepresentable MTKView, still the AppKit/UIKit-portability seam.
  • Writing an extension (ollin new --kind extension; Sources/OllinProjects/ExtensionSeam.swift + ProjectGenerator.planExtension; in the window, Sources/OllinProjectGenerator/SourceStage.swift + GeneratorHarness.swift; SketchLoader.typecheck in OllinRuntime). The third-party tier's convention half: a library other sketches import, named ollinx-<thing> with the module Ollinx<Thing> (ExtensionNaming, a capital run staying one word — SVGTools → ollinx-svg-tools), written by planExtension over the ExtensionSeam catalog (the data-not-a-code-path shape the kinds and templates already use). Four --seam starters, each a worked file plus real tests plus a publishing checklist: a drawing call on Sketch, a Shader wrapped as a Filter, a FrameSource, a SketchExtension. Two surface facts run the other way from what the design note assumed: Drawer is internal (so an extension extends Sketch and the public value types, and reaches the GPU through a Shader; @eaviles has not ruled on opening it), and Filter/Generator/Sim/Combine are structs with no public initializer, so the effect catalogs take no new cases and .shader(_:) is the one way into the render graph (Filter.Kind and Shader.params are internal too, so an extension can build a filter and never read one back, which is what its tests can and cannot check). Load-bearing: the kind bypasses template.fits(_:) before the switch (a template's fit is about sketches, and asking would refuse a good library) and the CLI refuses every sketch-shaped flag (--template/--from/--3d/--canvas/--with) rather than writing a package where they went nowhere; a seam's shader constant is emitted internal, never private (@testable import reaches internal and never private, so a private let makes the generated test fail to compile). The generator window offers the kind through a source stage (shipped 2026-09-02): a library has nothing to run, so for the extension kind the left list holds the seams in place of the templates (no examples, since the plan ignores them), the stage shows the planned starter's text through a read-only NSTextView colored by the shared SwiftTokens scanner (the performance editor's highlighter draws from the same eight patterns, so the two hosts cannot drift), a file strip under the stage switches between the files the plan will write (starter, tests, manifest, README, .gitignore; a non-Swift file is left uncolored), and typing a name re-plans, so the file renames itself on the stage as you type (the scroll position holds unless the file changes). The stage keeps the window's honesty promise: the starter is type-checked against the framework through SketchLoader.typecheck (swiftc -typecheck -parse-as-library over the loader's own module discovery, moduleArguments() shared with the sketch compile; no dylib, no link, ~0.4 s) so the chip says Compiles, a seam that stopped compiling shows its log over the source, and the verdict is cached per seam, never per keystroke (a legal name cannot change whether a seam compiles). Canvas and wiring are hidden for the kind (a library declares neither; the CLI refuses the same flags), the name hint shows the two spellings the name becomes, --seam <id> opens the window set (and implies the kind), and the request carries seam: only for this kind. Gates: OllinProjectGenerator --stagetest runs every seam through the stage's own check and requires a sabotaged starter to fail at its line (a check that cannot go red is worth nothing); --self-shot <png> captures the window (the source stage is AppKit text, so unlike a running sketch it comes out whole); everyExtensionSeamCompiles builds all four seams and their tests in one package, aGeneratedExtensionPackageBuilds runs swift test on a real emitted package, ExtensionProjectTests (10 pure), SwiftTokensTests (5 pure, in OllinRuntimeTests). Discovery (a curated index, DocC, a Swift Package Index listing) waits on the repository being public and stays in ROADMAP. No pixel snapshot (the output is files). Docs/Tools/Extensions.md, Docs/Tools/ProjectGenerator.md#an-extension-package-in-the-window; Guide Ch 44 § Giving it to somebody else: an extension package + the ExtensionShape figure.
  • Metal to GLSL for the web page (Sources/OllinShaderText/: ShaderToken / ShaderLexer / ShaderScan, the lexer the GLSL import shares, plus WebShaderTranslator / WebShaderCompat / WebShaderLibrary; Tests/OllinShaderTextTests/). The first slice of the recorded web page (DESIGN-NOTES.md § New output surfaces): the shader helper library rewritten from Metal to GLSL ES 3.00 by the import's lexer run the other way, so a translated unit keeps the author's layout and its line numbers. Load-bearing: it is a token rewriter, never a parser, and every edit replaces a token's text in place (the token count is the lexer's, pinned). The two rules that change a picture rather than failing to build are fmod and select: Metal's fmod truncates where GLSL's mod floors, so it becomes ollin_fmod written out (never mod), and select becomes ollin_select over mix(a, b, bvec) with its comparison turned into an overloaded helper call (c > 0.5 inside it reads ollin_gt(c, 0.5)), which is what lets a type-blind rewriter cross a vector comparison at all, since GLSL's greaterThan wants both sides the same width; any/all take the same rewrite. A thread T& parameter is inout; a brace initializer becomes the constructor GLSL spells (vec3[12](…) with its trailing comma dropped, a struct's own name, the function's return type after return); Metal's named constants stay names and arrive as #defines; a word GLSL reserves is respelled with a trailing underscore at its declaration and at every read; static / inline / constexpr, the metal:: prefixes, using, includes, pragmas, and #line go; a #define body has its types renamed; as_type<T> maps by the target's width. Nothing is dropped in silence: a pointer or buffer reference, a texture type or a .sample( read, an entry-point word, an attribute, a namespace, a template, an enum, and the Metal-only calls with no GLSL ES 3.00 form (sincos, ldexp, the bit tricks) each land as an unsupported diagnostic naming the line, and isClean is false. The library crosses as its marked sections only (base always, noise pulls hash, visual both), never the Metal preamble or the compute-only tail, since a page runs no kernel. Gates: WebShaderTranslatorTests (27, one per rule) and WebShaderLibraryTests (7): every section translates with no diagnostic at all, every defined function comes out the other side on the same line, and the browser gate compiles and links the whole library and each section alone in headless Chrome (--headless=new --dump-dom on a file URL, read from the DOM; never pass --user-data-dir, which made the browser dump the DOM and then never exit; skipped where no browser is installed, OLLIN_CHROME overrides the path), proves itself live on a sabotaged shader (the log names the line), and reads pixels back to check the fmod sign and the select shape against Metal's answers. The browser is waited on asynchronously through its termination handler, with stdout to a file, never a pipe read on a test thread: the first CI run parked three cooperative-pool threads in a blocking pipe read on a runner whose browser never exited (its children kept the pipe open past the kill), and the whole run stalled to its timeout. A probe finds once whether the browser yields WebGL2 (the GPU path, then --use-angle=swiftshader), the three browser tests skip with that reason where it does not, and a browser past its deadline is terminated and then killed. The framework's own segments mark sections for the page the same way (WebShaderLibrary.segmentSectionNames: shapes in ShaderShapes.metal, present in ShaderCore.metal + ShaderEffects.metal; the closure adds sdf under shapes and hash under present), pinned by theSegmentSectionsTranslateClean + a browser compile. The user surface is the next bullet.
  • A sketch as a web page (Sources/Ollin/Export/WebExport.swift: OllinApp.recordWebFrames / captureWebFrame, WebTrack, WebShaders, WebPlayer, OllinApp.web(of:frames:fps:skipSeconds:form:) / exportWeb, --export-web <path> [--frames N | --seconds S] [--fps F] [--skip S] [--inline]; the shared browser gate in Tests/OllinWebGate/; Tests/OllinTests/WebExportTests.swift). The recorder and the player of the recorded page (DESIGN-NOTES.md § New output surfaces) through the primitives door: the export drives the sketch headlessly at a fixed rate like the video export, records what the renderer received each frame, and writes a WebGL2 page that draws it with the framework's own shape shader carried to GLSL, so a frame on the page is the Mac's frame. Load-bearing: the recorder reads the drawer after performDraw() and never renders (the call-ordered batches and their sdfInstances runs, backgroundColor with accumulates / backgroundSetThisFrame for the clear, toneMapMode / toneMapExposure), so it is no Drawer hook and nothing GPU-specific lands in the page; a .retained batch of shapes alone replays with the draw-time transform composed onto each instance's own. A refusal names the call family from the batch kind (webRefusalName: the rest of 3D, withClip, depth(at:), a live texture drawn as a picture, wide-gamut output; the doors since opened took the triangle and stroke paths, layers, blend modes, drawImage, the glyph atlas, gradient paints, and the two field kinds off the list) and the frame it was met at; nothing partial is written, and the flag exits nonzero. The shape coverage is one text in both places: ollin_sdf_coverage in ShaderShapes.metal (the fragment's per-shape switch pulled into a function the Metal fragment calls, marked OLLIN_LIB_BEGIN shapes with the coverage helpers and ollin_sdf_distance, resolvePaint left outside since it reads a texture) is what the page translates, so a new region shape joins that switch as it joins ollin_sdf_distance; the extraction was measured pixel-identical on five exports (HelloCircle, BreathingRing, StrokeAlignment, Polygons, Gradients: 0 bytes differing at 1080²). The present section carries ditherTriangle / finalizeColor and toneMapACES. The wire record is 30 float32 per shape (WebInstance: the affine CTM's two axis columns and its translation, center, size, the two paints, the three params, stroke width, extra, band width, the tag as a float, the two gradient rows renumbered into the recording's strip), bound as ten instanced attributes; the page's vertex shader mirrors ollin_sdf_vertex, its fragment tail ollin_sdf_fragment with resolvePaint written for the page, blended SRC_ALPHA / ONE_MINUS_SRC_ALPHA + ONE / ONE_MINUS_SRC_ALPHA into an RGBA16F intermediate (EXT_color_buffer_float, RGBA8 where absent), and the present pass writes finalizeColor's encoded value straight to the canvas (an sRGB drawable re-encodes it on the Mac, the canvas takes it as is) with the dither's fragCoord turned y-down. Weight, in WebTrack (Sources/Ollin/Export/WebTrack.swift): consecutive identical frames fold to one record (a still costs one frame); a stable cast (same count and tags every frame) stores the first frame as the base in float32 and travels only the moving columns, each one of three ways, tried in this order: live (an affine image of a formula-driven parameter's recorded values, affineFit, least squares with a max-error acceptance at 1e-4 of the column's range; the page then works it out every frame), fitted (on a lap recorded from loopDuration, FourierFit over the column's period through DiscreteFourier, an any-length mixed-radix transform with the plain sum for a prime factor; terms added biggest first until the reconstruction is within the same tolerance, refused past a frame-count/8 term budget; the Nyquist bin of an even length is one cosine at half weight; coefficients travel as float32 and the page evaluates them at the fractional frame, so the motion between records is the signal's own), or sampled (16-bit inside the column's own range, quantize, half a 65,535th of the range at worst, interpolated on the page); a changing cast travels every instance of every unique frame as 16-bit samples with a range per field (the tag column recovers exactly since its error is under a half). The meta JSON escapes <. A formula crosses as the formula (Sources/Ollin/Export/WebFormula.swift): webFormulas(of:) reads the automation's worked-out tracks (whole formulas and parts) in an order where a formula that reads a driven name comes after it (a ring stays behind, as it does on the Mac), skips a formula that reads noise, and carries a slider's or stepper's range and step (Fx.param on the page applies the same clamp, the same step snap with half-away-from-zero rounding, and the integer rounding) while a part has none; FormulaJS.compile writes the tree as JavaScript over v (the names) and Fx (helpers) keeping FormulaNode.value's semantics (truth is a number that is not zero and not NaN, mod floors, round is half away from zero, step reads its edge first, &&/|| short-circuit); the recorder reads each formula's value back from its parameter every frame (webFormulaValue, so what the sketch drew with is what a column is matched against), keeps the non-driven parameters as constants from the first recorded frame, the automation's clock (start/speed/loops/duration) so the page stands time where the Mac's position(at:) does, frame as index + skip + 1, and the pointer at the first frame as the page's starting mouse (a pointermove on the canvas moves it after). The formulas travel only when a column reads one live. Measured against Swift in the browser for seven expressions covering every function family: identical to 1e-9. Per-frame facts that never change (the clear, the tone map, the exposure, an identity frame map) travel once. Measured 2026-09-03: the ring's 60 s lap is 83 KB at 10 fps and at 30 (521 KB and 1.4 MB before the fit; 140 columns to 252 sines; 27 KB gzipped in transit), the breathing circle's 6 s is 75 KB with two sampled columns; the shaders and the player are about 70 KB of any page. The player interpolates between records when the cast is stable (frame k toward k+1, wrapping to 0 on a lap recorded from loopDuration), draws every frame in order for an accumulating track, holds frame 0 under prefers-reduced-motion, pauses in a hidden tab, and leaves canvas.ollin / window.ollin (play, pause, seek, showFrame, frames, rate, duration, loops). The inline form is the whole of what the standalone wraps; the script finds the canvas just ahead of it. Gates (WebExportTests, 16): the recorder's frame (tag, radius, stroke, the identity transform), the lap flag with every column fitted and nothing sampled, the fit rebuilding a two-sine signal (and refusing a ramp), the transform against the direct sum at five lengths, a sampled column inside its range, the driven circle wired to its formula with its range, the fold and the stable cast, the changing cast, the refusals with their frames, the two forms, the shaders' provenance; and the browser gate: the page is played to a chosen frame inside the shared HeadlessBrowser (a probe script calls showFrame and writes canvas.toDataURL into the DOM, so no screenshot flag, no window size, no device scale), decoded, and diffed against OllinApp.image(of:frame:fps:) under Snapshot.tolerance (2.0) for HelloCircle, BreathingRing (sampled, and its fitted lap), a still, a moving disc, and a formula-driven circle, and the means print on every run (measured 2026-09-03 before the fit: 0.003 for the circle and the ring, 0.027 for the rounded rect and star, 0.013 for the moving disc, so a later drift is visible long before the line); the two forms compared byte-equal; the gate proven live (frame 0 against the Mac's frame 19 reads as different). OllinTests depends on OllinShaderText and OllinWebGate for it; OllinWebGate is a regular target under Tests/ because a test target cannot depend on another test target. The browser's clock is measured, not named (HeadlessBrowser.budget(_:launch:)): a test names what its page costs at a desk, the capability probe times its own launch (one browser, one trivial shader), and a page gets at least eight times that, capped at 600 s, with the number printed once per run. A run whose time ran out is tried once more at four times the budget. Both exist because the runner has no GPU, so every WebGL pixel is rasterized on the CPU and the wall clock swings with the load: the same one-page test takes 2.0 s here and took 229.7 s and 450.9 s there in runs that passed, while the eight-page thePagePlaysWhatTheMacDrew sat at a flat 90 s per page and finally crossed it (CI run 35051898614; the page itself was correct, and the same commit went green on a re-run). WebGateTests (4) pins the budget; the retry was verified both ways by starving the first attempt. The shader door (2026-09-03): the layered effects cross. The recorder (WebGraphRecorder, Sources/Ollin/Export/WebGraph.swift) writes down each frame's pass graph beside the shape vector: the layers in the renderer's own fill order (generators, drawn targets, feedback, fields, then the filter and combine outputs in record order), what each was filled from, the canvas's items, and the whole-frame filters; a surface's items are shape runs and image quads (drawImage(layer.image)'s six OllinImageVertex, 48 floats, under the batch's blend mode, drawn by the page's own image program with the Mac's factor table per mode); a pass's parameter rows join the frame's float vector, so a generator's phase fed the clock fits on a lap and samples otherwise, and a graph equal from frame to frame travels once (WebTrack reads stability as equal graphs plus equal tags; a changing cast stores shapes and quads as 16-bit samples by field and the rows as float32 in extra, with the graphs deduped). The pass description is one table for both sides: EffectPass (Sources/Ollin/Effects/EffectPass.swift: Filter.singlePass, Combine.singlePass, Generator.pass, the EffectQuality tap tables) is what applyFilter / applyCombine / encodeGenerator encode from and what the recorder writes down, so the packing of a filter's numbers cannot drift between them; the refactor was measured 0 bytes differing on 25 exports (six FilterCatalog families, four GeneratorCatalog families, Glitter, SoapFilm, MeshGradient, Cellular, Chladni, DomainColoring, EscapeTime, Relight, Aside, PigmentMix, FlowStreaks, Ripples, Feedback, Layers, VisualSynth) before the exporter read it. A filter that is more than one pass answers nil and keeps its orchestration in the renderer; the recorder refuses it by name (the diffuse filter), except .gaussianBlur and .bloom, which cross as the page's own passes (a separable Gaussian to three and a half sigma, normalized in the shader; measured on a one-pixel line against the Mac's hardware kernel, the same normalized Gaussian to within a level or two out past three sigma; bloom = the translated bright pass, the page's blur, the translated add-back). A whole fragment entry point crosses (WebFragmentTranslator, Sources/OllinShaderText/): the reachable closure over the file-scope declarations (functions with every overload, structs, constant globals, #define macros, whose bodies are re-lexed for the texture rewrite) in the framework's order; the entry's parameter list is read by attribute (PresentOut in [[stage_in]] becomes the page's in vec2 uv with in.position a macro over gl_FragCoord turned y-down, texture2d<float> x [[texture(n)]] a sampler2D in binding order, sampler dropped and its uses 0, constant float4 *params [[buffer(0)]] a uniform vec4 array sized per fragment by #define OLLIN_PARAM_ROWS); t.sample(s, q) becomes ollin_tex(t, q) and level(n) the LOD form, t.read(c) a fetch, get_width a size query, every read turning the coordinate over because the page keeps its layers bottom-up (the effect vertex stage writes uv = (p.x, 1 - p.y) exactly as ollin_present_vertex does, and ollin_tex reads (q.x, 1 - q.y)); a pointer walk over the params array (constant float4 *basis = params + 3 and a helper taking constant float4 *rows, the spectral family) becomes int offsets into the uniform array; a scalar initializing a vector (float2 p = 0.0) is wrapped in the constructor (a rule the cellular generator needed). All 129 door fragments compile in headless Chrome (WebFragmentTranslatorTests, which also found that the gate's compile page had to output uv from its vertex stage or the very first program failed to link). A user shader crosses by the same rewriter (WebUserShaderGLSL): the wrapper is written for the page by hand (a ShaderInfo struct without the texture members, sample_ / sampleAux / sampleRaw readers over ollin_src0/ollin_src1, param overloads, the clock, pointer, frame, and resolution as uniforms), the library sections the shader asked for and the user's body are translated (a thread const ShaderInfo & parameter becomes a value), and main converts straight sRGB to premultiplied linear as ollin_user_fragment does; sample is a GLSL reserved word, so the reserved-word rule's sample_ is the name the wrapper declares. A compiled Visual chain is a user shader and crosses as one. info.time is the track's clock, info.mouse the live pointer, and a Shader's params are recorded rows. Feedback and single-field simulations keep persistent pairs on the page (keyed per object, allocated at the rest state or the seed fill, flipped per frame; a sim's inject and its substeps run as translated fragments, its age counted per key); such a track is stateful and the player draws every frame in order from the first, as it does for an accumulating one, and --skip refuses it. Gates (WebEffectsTests, 10): the graph's shape per kind of pass (a generator's four rows and its moving phase column, a filter chain's inputs in order, a table traveling once, a combine's base and aux, a user shader's rows and its GLSL, feedback and field nodes, whole-frame filters), the refusals by name, and the browser parity of every kind of pass at a chosen frame against OllinApp.image(of:frame:): a moving generator, a two-filter chain, a gradient map, a mix under a mask, a user generator, a user filter, a user combine with params, a chain, a compose stack with a half-size layer and add/screen blends, a feedback tunnel at frame 11, reaction-diffusion at frame 15, whole-frame filters, and blur under bloom; measured on the first run at means of 0.006 to 0.16 (the feedback tunnel the largest) against the 2.0 line, and the gate proven live by playing the filtered page against the Mac's unfiltered layer. The examples, measured (2026-09-03, --export-web --frames 12 --fps 30 against --export --frame 14, the still export counting at 60): Cellular 0.010, MeshGradient 0.012, Chladni 0.043 (a few sand grains land a pixel over, its max 127), DomainWarp 0.013, ShaderFilter 0.030, VisualSynth 0.030, Glitter 0.109, SoapFilm 0.091, every page 98 to 165 KB (the shaders and the player are most of it; a filter's rows add a few floats a frame), which is the effects examples that draw no caption; DiffusionCurves stops on its curve's stroke and Ripples on its radial-gradient dab, both named. A gotcha the gate met: the hash-driven passes (the plate's sand, a voronoi's jitter) are exact too, so a page that reads far from the Mac is a clock mismatch, not a shader one; the still export ignores --fps and counts at 60, so its frame 14 is the page's frame 7 at 30. The triangle door (2026-09-03): strokes and fills cross as their vertices. The recorder reads a .triangles or .fringe batch's run of drawer.vertices (the fringe expander's bands, libtess2's fills, outline text, the elliptical arc) as WebVertex records (7 float32: the position, aa.x, the straight color with its alpha; a gradient on this path was baked into the colors on the Mac, so it crosses as they are), a third region of the frame vector between the quads and the parameter rows (WebGraph.vertexCount / vertexOffset); a .retained batch of shapes, strokes, and fills replays run by run in the recording's order with the draw-time transform applied to each vertex on the CPU, as the replay's vertex stage applies it. A blend mode crosses on shapes, strokes, and fills (WebDrawItem.shapes / .triangles carry the WebBlend index; the page's setBlend mirrors BlendMode.blendState(premultiplied:), SRC_ALPHA for a straight fragment and ONE for an image). The page draws a run with ollin_vertex's mapping and the two fragment tails (ollin_fragment's linearized color; ollin_fringe_fragment's paint alpha times perceptualCoverage of the interpolated coverage), both cut from the base section. Multisampling is load-bearing, and its orientation is the trap: a fill's edge has no analytic coverage, so a track whose graph hasTriangles sets msaa and the page rasterizes every drawn surface (the canvas, a geometry layer, a feedback back, a sim seed) into a four-sample renderbuffer (RGBA16F under EXT_color_buffer_float; four is what this Mac's canvas uses) and resolves it by blitFramebuffer; an accumulating canvas keeps its samples across frames, as accumTarget does. The first parity run put 0.43% of a polygon's pixels more than 32 levels off the Mac along every diagonal edge while an axis-aligned rectangle with fractional edges matched exactly: the page keeps its pictures bottom-up, so in the GPU's own texture space the picture is mirrored and the fixed rotated-grid sample pattern meets a diagonal edge mirrored (each axis's offsets are their own mirror, which is why an axis-aligned edge cannot tell). The items are therefore rasterized upright into the multisampled buffer (ollin_flip = -1 on the three item vertex stages, 1 otherwise, so a page without triangles is unchanged) and the resolve flips on its way into the surface's texture through a same-size single-sample surface, since a multisampled read allows no flip; the polygon then matches to two levels with no far pixel. Weight, and the second trap: a stable cast's vertices travel once, the positions exact in float32 (WebTrack.vertexPositions) and the coverage and color as 16-bit samples by field (vertexBase + meta.vranges), moving vertex columns fit or sample like a shape's, a changing cast stores every vertex of every unique frame the same way (meta.positionOffsets beside offsets), and the exporter prints the fullest frame's vertex count. A position is never quantized over the frame's extent: the first pack quantized every field, and a 16-bit step over a canvas-wide drawing is a sixtieth of a pixel, enough to flip a multisample on a fill's edge once in about twenty edge pixels; on DeBruijn's captions that put a sixth of the glyph edge pixels a sample off the Mac (0.07% of the picture, under the line, and invisible, but not parity), while a caption alone read exact because its extent was the glyphs' own. Float32 positions cost four bytes a vertex more and made the captions exact. Gates (WebTriangleTests, 9): the runs per kind in call order and a vertex field for field, outline text as fills, blend indices on shapes and strokes, a recording's replay under its transform (the first vertex where the frame's turn puts it), the stable pack (the base holding the vertices as samples, the moving columns few) and the changing one, the pipelines' provenance; the browser parity of seven cases (joins and a round cap, a bezier, a gradient stroke, a concave polygon, a holed shape, a moving vertex, outline text, add and lightest, a turning recording, an accumulating canvas, a growing cast) at means of 0.004 to 0.042, under a second criterion the mean cannot feel, no more than 0.2% of the pixels past 32 levels (the figure gate's own line for a visible move), proven live twice on the pre-flip page: multisampling off read 0.25 to 0.66% far on the four fill-bearing cases (the stroke-only ones stayed under the line, as they should), the perceptual remap dropped 0.43 to 3.4% on every case but the accumulating one (its half-alpha strokes move under 32 levels); and the sabotage of a page's first frame against the Mac's fullest. The Patterns set, measured (2026-09-03, all 76 examples through --export-web --frames 4 --fps 60 --seed 1, the page's frame 3 played in headless Chrome against --export --frame 3): 71 cross and every one reads under both lines, 61 of them through this door at means of 0.005 to 0.160 with no pixel more than 32 levels off on any page (the ten shape-only pages at 0.005 to 0.129); the five that stop name a clip (Anamorphosis, Caustic, FordCircles) or drawImage (Buddhabrot, FractalFlame). Weight is now the door ahead: 21 of the 61 pages are under 1 MB and the median is 3.3 MB, but a dense line drawing runs far past a page (Penrose 51 MB, Roses 72 MB, Spirograph 142 MB, ParametricLSystem 234 MB, Kleinian 309 MB, Guilloche 325 MB at 3.4 million vertices), a median of 36 times what the same frame weighs as SVG and 388 times at most, which is the measurement the WebAssembly expander option under the weight door (DESIGN-NOTES.md § New output surfaces) was waiting for; the exporter writes the file and prints the size either way until that door lands. The asset door (2026-09-03): pictures, atlas text, and gradients on shapes cross. A picture travels once as an asset (WebAssetEncoder, Sources/Ollin/Export/WebAssets.swift; WebPicture on the recording): a file the sketch loaded travels as the file when a browser decodes its format (browserFormat reads the magic bytes for PNG, JPEG, GIF, WebP; Image.sourceURL is set by init(contentsOf:) and drawsSourceFile says the pixels are still the file's), anything else as a PNG of premultipliedPixels() (the buffer the Mac's texture is built from once pixels were written); the recorder keys an image by ObjectIdentifier + Image.pixelGeneration (bumped on every pixel write) with a weak reference to catch a reused identifier, and dedupes by content, so two images with equal pixels are one asset and an edit is a second (Repainted in the tests). A .image batch's run of quads (one, or one per symmetry copy) becomes WebDrawItem.image(source:quad:count:blend:) with WebImageSource.picture(i) beside the layer sources; a live texture (a camera, a video, a compute texture) refuses by name where it is read. Atlas text travels as its quads over the font's page (WebDrawItem.glyphs(atlas:quad:count:blend:); WebAtlas on the recording): the recorder holds each atlas page it meets and reads it at the END of the recording (WebGraphRecorder.finish, GlyphAtlas.Page.webPage(): the rows down to the last packed shelf, 1024 wide), since a page fills as frames draw; a page is never cleared, so a font that fills one mid-recording crosses as several pages, each its own asset (WebAssetTests.aFontThatFillsItsPageCrossesAsSeveralPages). The rows travel as an 8-bit gray PNG and land at the top of a page-sized RGBA8 texture (texStorage2D + texSubImage2D), read raw (colorSpaceConversion: 'none') by the page's glyph fragment, ollin_glyph_fragment written for the page (the sampled distance to coverage through fwidth, perceptualCoverage, straight alpha over the corner tint, so a gradient fill on atlas text crosses as its corner colors). A gradient on a shape is carried in its record: the two gradient rows are renumbered from the frame's per-frame, first-use-ordered table (and a retained batch's own table) into the recording's one strip (WebGraphRecorder.gradientRows, deduped by content), which the page uploads as an SRGB8_ALPHA8 texture 256 wide (BakedGradient.width) bound to the shape program; the fragment tail carries resolvePaint written for the page (the same t mapping, (t * (w - 1) + 0.5) / w, (row + 0.5) / h through textureSize) and passes fillKind == 3 || strokeKind == 3 to ollin_sdf_coverage for the along-path coordinate. Three things the browser decided, all measured: (1) the decode is asynchronous, so the player draws its first frame when Promise.all of the loads resolves (player.ready; the canvas is cleared to the first frame's paper meanwhile, encoded in JavaScript; a page with no assets starts synchronously as before; pause() before the start holds the autoplay) and the test probe awaits ready; (2) each asset loads through an image element from a data URL, never createImageBitmap: an image element in flight holds the document's load event, which the headless dump waits for, where a bitmap decode holds nothing (measured on a 179 KB picture under two filter passes: the dump beat the bitmap decode two runs in six with --virtual-time-budget=30000 and six in six without it; the image element read twelve in twelve with and without the budget, so the gate carries no budget), and a bitmap source ignores the upload flags where an image element honors them (UNPACK_PREMULTIPLY_ALPHA_WEBGL asked on a bitmap upload left a half-alpha pixel too bright, 1.4% of a test picture's pixels past 32 levels; UNPACK_COLORSPACE_CONVERSION_WEBGL = NONE and image-orientation: none on the element keep the decode raw, as ImageIO's is); (3) the browser's mip level 1 of an sRGB picture matches a box filter within two levels, as the Mac's does, so a picture drawn small reads the same level on both. A picture's edge is a raster edge: WebGraph.needsMultisampling (triangles or image quads) is what sets msaa now, since a turned picture's edge on the Mac is multisampled and read 0.23% far without it; glyph quads keep their edge outside the field and need none. The Mac had a defect the gate caught: ImageIO decodes a PNG with alpha to alphaInfo == .last (straight) and MTKTextureLoader keeps it straight while ollin_image_fragment blends premultiplied, so a translucent PNG pixel drew too bright; Image.texture(for:) now rebuilds a straight-alpha source through the premultiplying sRGB context (sRGBTexture(from:)), and a loaded PNG then matches the page at a mean of 0.011 with no far pixel. A JPEG is decoded twice (ImageIO on the Mac, the browser on the page) and the decoders disagree by a few levels along a sharp chroma edge (a checker of 8-pixel cells reads a mean of 0.71 and 2.4% of its pixels past 32 levels), which the far criterion sees and the mean does not; the file crosses as it is since a photo re-encoded as PNG weighs several times more, and the test asserts the mean alone for it. Gates (WebAssetTests, 9): a picture once across six draws (whole, small, tinted, multiplied, covering, turning) with the quad on the wire field for field, an edit as a second asset and equal pixels as one, a PNG and a JPEG file as their own bytes and a TIFF as a PNG, atlas text as glyph quads within the page's rows, a gradient's kind and row per shape with the rows one table on every frame, text and a picture in a recording turning, the page's assets and strip in the script, the browser parity of nine cases at means of 0.004 to 0.044 with no far pixel (the JPEG at 0.71 by its decoder), and the sabotage of the pictured page against the paper alone. OLLIN_WEB_DUMP=<dir> on that test writes each case's page and Mac frames to disk. The Text and Images examples, measured (2026-09-03, the same gate as the Patterns set): all 29 sketches of Examples/Text and Examples/Images cross and read under both lines, at means of 0.003 to 0.163 with at most 0.015% of the pixels past 32 levels (PhotoMosaic's 25 tiny pictures scaled), TextVolume's wall of atlas text at 0.126 with its whole page 9 KB, and Buddhabrot and FractalFlame, the two Patterns sketches the triangle door had stopped on drawImage, at 0.057 and 0.092; the pages run 157 KB to 10.8 MB with a median of 774 KB, the heavy end a changing cast of outline glyphs (GlyphMosaic) and, for a picture that changes every frame, one PNG per version (Autostereogram, four frames, four pictures, 2 MB), which is the weight door's case. Two things the gate found on the way: a first run put the headless dump ahead of a 179 KB picture's decode on one page (LuminanceMelt), which is why the loader is an image element, and a gate script that greps Test run for a suite's end matches Test run started first. The field door (2026-09-03): composed and raymarched fields cross. The VM arithmetic is one text in both places: ShaderCombinator.metal marks ollin_emod, the stairs and columns joint ops, ollin_sdf_combine, and ollin_sdf_xform as OLLIN_LIB_BEGIN combinator, and ShaderRaymarch.metal marks every ollin_sd3_* distance function, ollin_sdf3d_eval, ollin_sdf3d_combine, ollin_sdf3d_unrotate, and ollin_sdf3d_xform as raymarch (its closure pulls combinator, noise for the roughening's valueNoise, and sdf for the cone's dot2); the instruction records the header declares (SDFNode, SDFNode3D) are declared again inside each section under #ifndef __METAL_VERSION__, which Metal never enters and the page's rewriter keeps, so a section compiles standalone in the browser gate. A fractal leaf's iteration count reads int(x + 0.5) on both sides now, since a count that crossed a wire a hair under its integer used to lose a level. The VM loops, the march, and the lighting are written for the page (WebShaders.groupFragmentTail, fieldFragmentTail, fieldLightingSource, upsampleFragment, groupVertex, fieldVertex): the program sits in a layout(std140) uniform Nodes { vec4 nodeRows[1024]; } block, four rows a node and 256 nodes at most (the framework's own cap, and the 16 KB WebGL2 guarantees), uploaded per field with bufferSubData; the two stacks, the coverage tail, and resolvePaint (one resolvePaintSource shared with the shape tail) mirror ollin_sdfgroup_fragment; the field fragment mirrors ollin_raymarch_fragment (the ray through the inverse view-projection, the AABB slab test, the sphere trace with the pixel-cone coverage, the tetrahedron normal, the analytic self-shadow toward each caster, the leaf color or the screen-space gradient, gl_FragDepth = clip.z / clip.w under LEQUAL) and its litColor is the punctual half of meshLitColor (models 0 to 3, directional/point/spot with the wrap and the cone, the caster's strength, ollin_sss_translucency, both iridescence modes over the library's fbm, the sparkle over hash33, the rim); a physically-based finish reads ollin_pbr_ess from the same split-sum table the Mac bakes, baked once per process on the CPU (WebBRDFLUT, 256 a side, 512 Hammersley samples a texel, ollin_ibl_brdf_lut sample for sample with the Mac's own tangent frame, stored as half floats through WebHalf so a texel rounds as the Mac's rg16Float does) and carried as an RG16F asset only when a field uses one. The wire: four regions join the frame vector between the quads and the vertices (WebGroup 26 floats in the seven rows the group stage reads, WebNode 16, WebField 36, WebNode3D 16; WebGraph.groupOffset … node3DOffset; the vertices stay last so the track's base split holds), items ["g", start, count, blend] and ["f", start, count, blend, materialRow] with the finish as nine parameter rows (WebMaterial), a program placed once per source range however many symmetry copies or replayed recordings share it, and the scene block travels apart from the vector, whole and exact (WebFrame.scene, WebGraphRecorder.sceneBlock: the three camera matrices, viewport and the two step budgets, the march scale with the reduced pass's size and the scene scale, ambient, eye with the lit flag, counts, 20 floats a light, one row a caster; WebTrack.scene with meta.sceneOffsets / sceneLengths; the page lerps it between records) because a camera matrix quantized inside its range moves a silhouette; the fold reads the scene too (a still field under a turning camera is a new frame every frame, though its vector never moves: the first gate played frame 0's camera at frame 3 and read 0.7% of RaymarchedShadow far). The stable-cast test also compares WebGraph.structuralColumns (a node's kind and op, a field's program length and boundedness). The scene resolves as the export's renderer would: MetalRenderer.raymarchBudget / raymarchScale / fieldScreenCoverage are nonisolated static now (the instance methods call them with automaticQuality; the recorder passes .detail, the export lift), so .default marches at (192, 72), raymarchResolution keeps (128, 48) and traces at the coverage-adaptive fraction into a halfSurface with a DEPTH_COMPONENT24 texture, upsampled once by the page's upsampleFragment (bilinear color, point depth, gl_FragDepth re-emitted, premultiplied over, region = ((w-.5)/w, (h-.5)/h) on an exact-size target, which is the Mac's subrect clamp on a grow-only one); a page whose track hasFields keeps a depth renderbuffer on the canvas (and on its multisampled buffer), cleared each frame. Only the primary caster reaches a field (finalizeShadowCasters keeps slot 0 and drops every other slot that has no rendered map or cube, and a fields-only frame renders none), and its map term samples the 1×1 dummy map cleared to 1, so the page marches the analytic self-shadow alone and the caster row carries shadowLight and shadowStrength. Refused by name where the page's lighting stops (sceneBlock, WebMaterial.refusal): an environment, fog and aerial perspective, an area light, a profile or cookie, contact shadows, GI, ray-traced reflections, the scene through glass, caustics, and a transmissive, clear-coated, sheened, thin-film, brushed, or scattering finish; the rest of 3D (a mesh, a point cloud, a field inside a layer) as before. Gates (WebFieldTests, 10): a composed field's quad and program field for field (the identity transform, a 3-point stroke, a linear fill on row 0, the second program following the first), symmetry copies and a turning recording sharing one program, a raymarched field with its scene (the export's (192, 72), a full-resolution march, one directional light, no caster), a still field under a turning camera as a new frame every frame, a reduced march's scale and a shadowed plane's caster, the split-sum table's presence and its half floats, the refusals by name, the shaders' provenance; and the browser parity of seven cases (a melt with a carve, a morph, a gradient, a stroke, and a tiling; symmetry, additive, a recording; a turntable blob; jade under a spot and a point with a gradient field beside it; a self-shadowing plane; a half-resolution march; a metal, a toon, and a Gooch) at means of 0.006 to 0.13 with no pixel past 32 levels (the reduced march read 0.38 with 0.1% far until the fold read the scene), and the sabotage of frame 0 against the Mac's frame 11. The Raymarching and Combinators examples, measured (2026-09-03, the same gate as the Patterns set, frame 3 at 60 fps): all five Combinators sketches and twelve of the seventeen Raymarching sketches cross and every one reads under both lines, at means of 0.051 (RaymarchedPlane) to 0.153 (RaymarchedDomain) with no pixel more than 32 levels off on any page, the five Combinators pages at 0.064 to 0.142 with their outline captions; every page weighs 220 to 306 KB, the shaders and the player nearly all of it, since a field's program is a few dozen rows and a still field under a turning camera travels one scene block a frame. The five that stop draw a mesh beside the field or outside a sculpt block (RaymarchedCastShadow, Distort, ReceiveShadow, SDF) or an environment (RaymarchedEnvironment), each named. The first run read RaymarchedShadow 0.7% far, RaymarchedClay 0.3%, and RaymarchedPlane 0.17%, with frame 0 exact and frame 1 already drifting: the fold, which is what found the missing scene compare (a camera probe on both drives printed identical eyes first, and was removed). Parameters as controls (WebControls.swift: WebControl / WebAxis / WebAxisColumn, OllinApp.webControls; --no-controls; the handle's params / get / set / reset). After the baseline, each part of each @Param (a number, an integer, a switch, a color's four channels, a point's parts, a pair of ends; a menu, text, a swatch strip, a rectangle, and insets are not offered) is set on a fresh sketch from remake (the flag site passes the command-line factory under withoutActuallyEscaping, so the seed, a replayed take, and the automation apply; web(of:) defaults to type(of: sketch).init()) after runSetup(), recorded over the same frames through runWebFrames, and every column of the vector, the scene block, and the per-frame facts (the clear's channels, the exposure) that moved is fitted to a line in the setting at every frame, three settings a part (the range's ends, then two golden-section points, webProbeSettings, so no symmetric curve passes as a line; a switch has one), the tolerance a ten-thousandth of the column's spread or a few float32 steps at its size. Load-bearing: a discrete column that changes (a shape's tag or gradient rows, a program's kinds, a paint kind: webDiscreteColumns), or a graph, a length, the clear's presence, or the tone map, is a cast change and fails the part, except at an end of the range, which is passed over and the line fitted inside it (drawCircle skips a radius of 0 and a stroke with no alpha skips the shape, which took radius and every color's alpha out of the hero until the ends were passed over); a color channel is tried as itself first and then through Color.srgbToLinear, per column, which is how a paper color reaches the clear while the same color reaches a fill straight; the wired parts are then moved together once, and a column that moved by other than the sum of its slopes drops every part touching it (webReasonCoupled: a stroke alpha written as ink.alpha * fade is exactly this, and the first fixture had it); a fresh unmodified run is compared to the baseline first, and a sketch that differs (unseeded randomness) loses every control with that reason; a part with no touched column is inert and the control is not offered, except a color's alpha, which the well drops. In the track, a column a formula drives is left to the formula when the formula's transitive reads include the axis (WebTrack.formulaClosure), since the page evaluates formulas over the controls' values under their names; slopes pack as a moving column does (packSlopes: one float, the sines of a lap, or a sample per frame) into @AXES@. On the page, applyAxes adds slope × (value − base) after the base, the varying, the fits, and the drives, onto the vector, the scene block (copied first, since sceneBase hands out a view), and the clear and exposure; a change on a sequential track replays from the first frame. The standalone page builds the panel under the canvas (PANEL; --ollin-panel sizes the canvas to leave room; the panel's text color follows the paper's luminance); the inline form never does and carries the handle. Gates (WebControlTests, 11): the wired slopes (a radius at one, a turning center at half the cosine, a color at one per stroke, the clear through linear light, a switch by the fill's difference, a point's part), each refusal by name (nonlinear, cast, coupled, inert, no control, driven, nondeterministic), a shader's row, the packing modes and the driven-column exclusion, the panel's presence per form, the probe settings, the browser parity of ten moved-control cases against the Mac run with --param at the same values (means 0.003 to 0.067), the sabotage of a moved radius against the recorded one, and the handle's report through the DOM. Measured on the hero: ink and paper live (115 columns, each alpha fitted to one sine), 4 KB on the page, 26 more recordings of the lap. The weight limit (2026-09-04): a page past OllinApp.maxWebPageBytes (25 MB, the largest file the static hosts serve and what a phone opens in seconds; --max-page-size MB raises it, 0 lifts it, maxBytes: on web(of:) / exportWeb) is refused with a WebWeightRefusal: the weight, the heaviest part (WebTrack.weights counts the shapes and passes, the stroke and fill vertices, the scenes, and the controls as the bytes they take written out, split into stored-once and per-frame; webInlineFragment adds the pictures and the atlas pages; the remainder is the shaders and the player), whether it grows with the frames (per-frame > once, so the advice is --fps 10 / a shorter --seconds, else "would not help"), the video pointer, and the flag that writes it anyway. Load-bearing: the weight is worked out before the page's text exists (WebPlayer.filledCount over the slot sizes plus the wrapper measured empty), and the recording bounds the page from below as it goes (WebWeightBound in runWebFrames: the first frame's vertices and shapes once, every differing frame whole once the cast has changed, every picture, and, on a recording that is not a lap with no formula, two bytes per unique frame for every column that has moved so far plus twelve once; a lap or a formula leaves the moving columns out, since a fit or a drive can carry them for less), stopping at the frame the bound passes the limit with seen: (frames, of) and "at least" in the message, which is what keeps a 3-million-vertex drawing from taking 17 GB of frames it would never use on an 8 GB machine (the guilloche refuses at frame 1 in 0.4 s; the Penrose tiling at frame 5 where the old exporter recorded 2 min 16 s first). Two weight fixes found on the way: a sampled column's range and index travel as float32/uint32 base64 (WebTrack.ranges / .varying, @RANGES@ / @VARYING@) rather than JSON text (43 bytes a column, which outweighed the samples on a short recording: a 2.5 MB scribble page became 1.5 MB), and the player's slots fill in one pass (WebPlayer.filled / slots(in:); each replacingOccurrences copied the whole page through NSString, 0.7 s a frame on a 400 MB page, 0.05 s now). Gates (WebExportTests, 20): the refusal past a tiny budget naming the vertices with the frame count and the flag that then writes the page, a still's "would not help", the parts summing to the page with the vertices absent from a shapes-only sketch, the first-frame refusal of a dense still with the bound under the page's true weight, the within-frames refusal of a moving line with its moving columns counted against the same line as a lap weighed once assembled, and the one-pass fill (a lone @, a lower-case name, a value holding a slot's spelling). Measured 2026-09-04 on the six heaviest pattern examples (six seconds at 30 fps): all refused in under a second, vertices 15 to 70 times the same frame's SVG (the numbers are in Docs/Output/Web.md § The weight limit). Docs/Output/Web.md § Fields on the page, § Parameters as controls, and § The weight limit; Guide Ch 41 § A page that plays it: the web export. The points door (2026-09-06): strokes and fills cross as the points and the style the sketch gave, and the page expands them itself. The fringe expander's geometry stage moved out of Drawer+Tessellation into the OllinExpander target (StrokeExpander.expand / innerCrossings / spread / circleSegments; FillExpander.fan / .triangulate over libtess2; WebSourceLayout / WebSourceExpander, the wire's record and the walk that turns records into vertices), plain Swift over the C tessellator with nothing from Foundation, and Scripts/build-web-expander.sh compiles the same files plus Scripts/web-expander/Entry.swift with Embedded Swift for wasm32-unknown-wasip1 through the swift.org toolchain and its _wasm-embedded SDK (Xcode's toolchain has neither the embedded stdlib for wasm nor a wasm linker) into Sources/Ollin/Resources/WebExpander.wasm (72,622 bytes: a reactor module, --strip-all, libtess2's setjmp shimmed to a trap in Scripts/web-expander/shim/, its one import random_get) beside WebExpander.json (toolchain, SDK, the SHA-256 of the sources in the script's order). Load-bearing: the Mac's expander is untouched by the move (a dump of 38,274 vertices over every join, cap, weight, transform, gradient, profile, and fold is byte-identical before and after; Drawer keeps the measuring half, dropping repeats, subdividing for a gradient or a profile, the fringe width 1/ctmScale, the half-width and the color per point, and hands the expander Point2Ds); the collinearity guard is StrokeExpander.collinear = 1e-4, a hundredth of a degree, because a point carried in single precision bends a straight run by a sine near a millionth and the old 1e-6 made a gradient stroke's inserted subdivision points read as joins on the page (+360 vertices on the fixture) while the Mac's doubles stay collinear to 1e-16 either way (the dump stayed byte-identical under the new guard); the committed module must come from the sources in the tree (WebSourceTests.theCommittedExpanderWasBuiltFromTheseSources recomputes the hash with CryptoKit, and Scripts/preflight.sh runs build-web-expander.sh --check when Sources/OllinExpander/, Scripts/web-expander/, or External/CLibtess2/ change, so an edit without a rebuild fails on CI without the toolchain). The record (WebSource, Sources/Ollin/Export/WebSources.swift): while Drawer.recordsWebSources is on (armed by recordWebFrames and the controls probe before runSetup(), so a batch made in setup() carries its sources on Batch.webSources; off on the live path, one Boolean per stroke), appendFringeStrokeSingle keeps each stroke as its deduplicated, subdivided points, closed/join/cap, fringe, ctmScale, one half-width or one per point, one color or one per point (a gradient, a varying opacity), the transform, and the vertex range it became; drawPolygon and the elliptical drawArc keep a fan, drawShape its fillable contours with the winding rule, under a solid paint only (a fill under a gradient keeps its vertices, as do outline glyph fills and glyph strokes through appendStrokedPath); replicated copies each record per fold with fold * transform, and the recorder passes over a record that expanded to nothing. On the wire a record is a 12-float header (kind, flags, count, fringe, ctmScale, the six transform floats, 0) and its body; the region sits between the 3D nodes and the vertices, always float32 (a position is never quantized), its header columns and a fill's contour lengths in structuralColumns so a join that changes is a change of cast. The recorder walks each .triangles/.fringe run by binary search over the records (a running cursor breaks because items are gathered per surface) and interleaves .sources(start, floats, count, fringe, blend) items (["x", ...] in the meta) with the leftover vertex spans in draw order; a retained batch replays its records under retainedTransform * transform, so a turning batch moves six floats a frame. On the page the module is instantiated synchronously (new WebAssembly.Module + Instance, 5.5 ms in Chrome; a pending asynchronous compile holds no load event and raced the headless dump, so that path stays only as the fallback), drawSources copies an item's floats into the module (ollin_input / ollin_expand / ollin_output, seven floats a vertex), keeps a buffer and a VAO per item until its floats change (a still expands once, a mover once a frame, a turning recording once per turn since the transform is in the record), and sweeps entries not drawn this frame. The page carries the module (97 KB as base64, its own weight part, the stroke expander, and the tessellator's SGI notice beside it in the script) only when a frame has points to expand; a missing resource throws WebExpanderMissing. The bound and the track count strokes and fills as points as a part beside the vertices. Gates (WebSourceTests, 9): the recorder (every stroke of a fixture with every join, cap, weight, transform, gradient, profile, and arc is a record and none is left as vertices; a fan, a holed shape as [[5, 4]], and arcs, with outline text's fills and strokes as vertex spans between them; twelve folds of a stroke and a fill are twelve records each and one moving point moves twelve floats; a turning batch moves only its transforms), the round trip on the Mac (WebSourceExpander.expand over the wire's region against drawer.vertices: positions within 2e-3, coverage and color exact, with a culprit report naming the first record that expands to a different count), the track (a still's records in the base as float32, a changing cast's in the exact stream, a restyled join two unique frames), the provenance, the browser parity of seven cases at means 0.004 to 0.019 with no far pixel, and the sabotage (the same page with EXPANDER blanked draws no strokes). WebTriangleTests keeps the vertex wire on outline text. The six heaviest Patterns, measured (2026-09-06, --export-web --frames 4 --fps 60 --seed 1, the page's frame 3 against --export --frame 3): all six cross under both lines where their vertices had been refused: Guilloche 5.2 MB (was 325 MB; 42 strokes, 3.4 M vertices expanded in Chrome in 74 ms) at 0.060, Spirograph 1.8 MB (was 142 MB) at 0.077, Roses 386 KB (was 72 MB) at 0.006, Penrose 452 KB (was 51 MB) at 0.057, Kleinian 1.2 MB (was 309 MB; 3.2 M vertices in 64 ms) at 0.136 with 0.0005% far, ParametricLSystem 8.7 MB (was 234 MB; 73,261 strokes, a still) at 0.100 with 0.0003% far; the expansion runs about 20 ns a vertex in Chrome on this Mac, against the Mac's own 50 ns in a release build. Over six seconds (--seconds 6 --no-controls --seed 1, the default 25 MB limit): Spirograph 24.5 MB at 30 fps and 9.0 MB at 10 (its 50,733 point columns all move and sample per frame), Roses 441 KB at 30 and 404 KB at 10 (its roses turn, so only the 20 records' transforms move), Penrose 1.2 MB at 30 and 709 KB at 10, Kleinian 2.1 MB at 10, ParametricLSystem 8.7 MB at 10 (a still, one distinct frame); Guilloche's 161,238 moving point columns are refused at 10 fps by frame 53 of 60 (a lower bound of 25.1 MB, the samples of a drawing whose every point moves) and cross over three seconds, which is the honest edge of the door: a dense drawing whose every point moves every frame still weighs its samples, and the refusal now charges those moving columns to strokes and fills as points rather than to the vertices (the bound classifies a moving column by the region it sits in).
  • Fabrication export (Sources/Ollin/3D/MeshExport.swift + MeshPrintCheck.swift + ZipWriter.swift). The write half of the Mesh story, the 3D counterpart of the plotter path: mesh.write(to:) / data(as:) / the Sketch sugar saveMesh(_:to:) emit MeshFileFormat .stl (binary) / .obj / .threeMF, sized by a declared ModelUnit (3MF records it, STL/OBJ have nowhere to put it) with normalized(scale:) the documented sizing step, plus printCheck() → MeshPrintCheck (closed / consistently oriented / inside-out / boundary + non-manifold edge counts / degenerates / size / volume / problems in plain words). Load-bearing: every format shares one FabricationMesh preparation (weld → drop degenerates → settle winding → turn z-up), so an STL and a 3MF of one mesh describe the same solid rather than each format's own idea of it; welding is a precondition, not a nicety (Ollin's generators are flat-shaded, so an unwelded mesh is 100% boundary edges and a slicer sees loose triangles, the same discovery the soft-body tier made, pinned by the raw-vs-prepared counterfactual); winding follows the normals, not the authored order, by area-weighted agreement over the whole surface flipping every triangle together or none (the subdivision-surface rule: Mesh.extrude winds inward while shading outward), falling back to signed volume only when a mesh carries no normals; the z-up turn is the rotation (x, -z, y) (a quarter turn about x, determinant +1) so winding and normals are carried along untouched; the STL header must not begin with solid or a sniffing reader takes the binary file for the text form; OBJ is geometry-only (a merged corner has no one normal, and writing an averaged one would round off the very edges the merge just joined); printCheck() describes the prepared mesh, never the raw arrays (on those every generator reads as broken, which is the whole reason the counts differ from Mesh.triangleCount); a failing mesh is written with a note, never refused (an open surface is a fine thing to draw). 3MF is an OPC package written by ZipWriter (deflate-when-it-pays else stored, own CRC-32, fixed 1980 timestamps so output is byte-reproducible), the write counterpart of USDZipArchive and verified against both it and the system unzip; the spec's manifold + distinct-corner + counter-clockwise rules are exactly what the shared preparation leaves true. Deliberate v1 cuts: no vertex colors / uvs / materials (the 3MF materials extension is unwritten, so no multi-material or full-color print), no ASCII STL, and no CLI flag (a sketch draws many meshes, so there is no single "the mesh" for a flag to name). Example 3D/Geometry/Fabrication (a tunable torus knot whose sealed parameter opens the tube's ends: the shape barely changes and the report turns red); MeshExportTests (20, the weld / winding / z-up treatments each verified red by sabotage, STL and OBJ read back through the shipped loaders, 3MF parsed through the shipped ZIP reader + XMLDocument); no pixel snapshot (the output is files, not frames). Guide Ch 42 § Something you can hold: a 3D print + the closed-versus-open figure; Docs/Output/Fabrication.md.
  • G-code export (Sources/Ollin/Export/GCodeExport.swift). The machine-facing vector sink beside SVG and PDF — the third reader of recordVectorFrame's VectorRecording: GCode (a Machine profile — .plotter(pen:feed:) with a .zAxis/.servo pen, .laser(power:mode:feed:passes:powerScale:), .mill(depth:depthPerPass:safeHeight:feed:plungeFeed:spindleSpeed:) — plus explicit physical width, margin, ordered, joinTolerance, or a named PaperSize through GCode(_:paper:margin:optimizesTravel:joinTolerance:) / --gcode-paper a4[-landscape]: width becomes the sheet's less the margins, scale(for:) also holds the drawing to the sheet's height, scaling down never up, so a tall canvas fits the page instead of running off it, the header names the sheet, and the drawing keeps the margin corner as its origin rather than centering) plans a Toolpath (toolpath(_:in:) / program(_:in:) / .program), with OllinApp.gcode(of:settings:) / exportGCode / --export-gcode (--gcode-machine|-width|-margin, sharing --hatch) the sugar over it. Load-bearing: the whole pipeline stays in canvas space — millimeters and the y flip (a canvas grows down, a bed grows up from its origin at the canvas's bottom-left) enter only at emission; width is explicit by design (the fabrication rule: a machine needs real units, and a silent default cuts at a silent size); strokes plot centerlines (strokeWeight does not travel) and fills contribute outlines unless a Hatching shades them (the SVG transform reused, not reimplemented); clipping is split by role — a fill clips as a region through the Clipper2 booleans (keeping the closing edge the render shows at a clip or canvas boundary) while a stroke clips as a line through the span clipper (segment crossings paired by Shape.contains midpoint tests), with the all-points-inside fast path only where the region is convex (the canvas rect); merge runs before order (open ends within joinTolerance mm weld so the pen stays down; a chain meeting itself closes into a loop) and the greedy nearest-neighbor walk may reverse an open path or enter a closed one at any vertex — ordering changes travel only, never ink (pinned by drawnLength equality); dialect facts are encoded once, not re-derived per machine: a laser fires only on feed moves, so travels are bare rapids with no off-word between paths (M4 dynamic / M3 constant, S scaled by powerScale, M5 only in the footer), a servo pen uses the spindle word with a settle dwell (G4 P0.15), and a mill's closed loop plunges deeper each lap without retracting while an open path retracts and returns; S/F are modal (written only on change); every program is absolute millimeters (G21/G90/G17), ends G0 X0 Y0 + M2, and is byte-deterministic for a seeded sketch (the recipe header carries the roll otherwise). Sketch.isVectorExporting is public so a preview sketch keeps its chrome (pen marker, caption, travel overlay) out of the plotted line work — it reads true during SVG/PDF/G-code recording. Example Export/Toolpath (draws its own Toolpath, walks a pen along it at machine speed, exports chromeless); GCodeExportTests (16, each program replayed through a small in-test interpreter asserting machine facts — pen up on every travel, power only on cuts, the depth ladder exact, nothing off the bed; the y-flip, clip, and depth-clamp treatments each verified red by sabotage; cross-checked by an out-of-project replay that reproduced the header's measured draw/travel mm); no pixel snapshot (the output is files). Guide Ch 42 § Driving the machine itself: G-code + the MachineRoute figure; Docs/Output/GCode.md.
  • DXF export (Sources/Ollin/Export/DXFExport.swift). The drawing-exchange sink beside SVG, PDF, G-code, and embroidery: DXF(width:margin:joinTolerance:) (or DXF(paper:margin:joinTolerance:) over a PaperSize, the same height hold G-code makes; --dxf-paper) plans a Drafting (drafting(_:in:) over contours on the default layer 0; OllinApp.drafting(of:settings:) / dxf(of:) / exportDXF / --export-dxf (--dxf-width, --dxf-margin, sharing --hatch) over a whole frame), whose entities (.line / .polyline(closed:) / .circle) and layers (name, color, the nearest of the nine standard color indices) stay in canvas space, size in millimeters, and text is the file. Load-bearing: each color is a layer named by its display bytes (color-1B1040), in the order first drawn on, since a shop program reads a layer as a job and the sketch's colors are the handle; the file is the R12 dialect (AC1009, $INSUNITS 4, $EXTMIN/$EXTMAX the drawing's size, an LTYPE table with CONTINUOUS, POLYLINE/VERTEX/SEQEND for a polyline, no handles, no objects, no text), group codes padded to three columns and lines ended \r\n, because the oldest dialect is the one every reader takes; millimeters and the y flip enter only at emission (the canvas's top edge is the drawing's top, the origin the canvas's bottom-left), as for G-code; the flattener is the embroidery one (stitchBlocks, strokes as centerlines and fills as regions, clips applied exactly) and the clip-and-merge is the G-code planner with ordering off, so nothing is reordered and touching ends merge only when consecutive; a circle stays a CIRCLE when its transform is a similarity (the images of the unit vectors equal in length and perpendicular), no clip is active, and it lies wholly inside the canvas, else it is flattened like everything else; a fill is a closed polyline unless a Hatching turned it into line work at record time. DXFExportTests (9 tests through a small reader: the skeleton, the flip and the extents with the margin, a layer per color with black as index 7, the circle kept and the squashed one flattened, touching paths merged and a two-point path a LINE, a clip cutting a line to its window, a fill as one closed polyline and hatched as lines, the public planner on layer 0, the file written). Examples/Export/Drafting; Docs/Output/DXF.md; Guide Ch 42 § A drawing for the shop: DXF + the LayerStack figure.
  • Embroidery export (Sources/Ollin/Export/EmbroideryExport.swift). The thread-facing vector sink beside G-code, the fourth reader of recordVectorFrame's VectorRecording: Embroidery (explicit physical width, margin, stitchLength clamped to 0.3…12 mm, fillSpacing (nil sews a fill's outline), fillAngle, optimizesTravel, joinTolerance) plans a Stitching (stitches(_:in:) / data(_:in:label:); stitches as Stitch values of kind .stitch / .jump / .colorChange in canvas space, threads, stitchCount / jumpCount / colorChanges, threadLength / jumpLength in mm, size, dst(label:)), with OllinApp.stitching(of:settings:) / embroidery(of:) / exportEmbroidery / --export-embroidery (--embroidery-width|-margin, --stitch-length, --fill-spacing, 0 = outlines) the sugar over it. Load-bearing: the pipeline stays in canvas space; millimeters, the tenth-millimeter grid, and the y flip enter only in dst(label:), which rounds absolute grid positions and takes their differences (accumulating rounded deltas walks the design off its drawing); a record moves at most ±121 grid steps (81+27+9+3+1 on its own bits, the jump and color-change flags in the third byte), so a long jump is split into equal records and a stitch never exceeds the pitch; the design starts at the canvas center, so a centered hoop sews it whole; every corner is a penetration (each segment is divided into equal steps no longer than the pitch); a hop between paths within the pitch is sewn and beyond it is a jump; a fill is rows through the shipped hatchLines scan-fill, serpentined by the same nearest-neighbor orderedPaths walk G-code uses so the rows connect without lifting, or its outline when fillSpacing is nil; each color is a thread in draw order (a new block whenever the color changes, never regrouped, so what was drawn on top is sewn on top; a gradient sews as the middle of its ramp; the optimizer never crosses a block); the header's ST counts every record before the end, CO the color changes, and the extents are the sewn extents; no lock stitches or trims are written (the machine's own settings or the embroidery program add them, said in the Docs). Written from the published Tajima record layout, credited in ATTRIBUTION.md. EmbroideryExportTests (10, CPU-only, through a reader that decodes every record back onto the grid); no pixel snapshot (the output is a file). Examples/Export/Embroidery; Docs/Output/Embroidery.md; Guide Ch 42 § Thread instead of ink: embroidery.
  • Spatial export (Sources/Ollin/3D/SceneExport.swift + SceneWriterUSD.swift + Sources/Ollin/Export/SpatialExport.swift). The write half of the USD story, per-scene where fabrication is per-mesh: scene.write(to:) / data(as:metersPerUnit:) / the Sketch sugar saveScene(_:to:) emit SceneFileFormat .usdz (package) / .usda (text layer), and OllinApp.spatialScene(of:frame:) / exportSpatial / --export-usdz <path> (--frame, --meters-per-unit) drive a sketch headlessly and collect the frame's 3D draw calls. The architecture is one writer with two ways in: the frame recorder's output is an ordinary Scene (the same value loadScene hands back, so a recorded frame can be read, edited, and redrawn), which is what makes it structurally impossible for the frame path and the file path to drift. Ollin writes the USD itself (Model I/O is out of the USD path entirely, and both halves of a writer were already here): a .usda text layer plus ZipWriter, which gained stored:/alignment: (defaulted, so the 3MF path is byte-identical) for the spec's zero-compression, 64-byte-aligned, layer-first package. Every mapping is the stated inverse of the reader's and the tests hold the two against each other, because a writer that drifts from its reader is wrong in a way nothing else notices: colors sRGB→linear, focal = aperture / 2tan(fov/2), ortho verticalAperture = height × 10, spot cone as a half-angle in degrees, texture v → 1 − v, and a USD matrix's four rows are the simd matrix's four columns. Load-bearing, each a real defect the system validator caught: a Mesh needs subdivisionScheme = "none" or every schema-honoring viewer shows a smoothed, shrunken cage; a prim that binds a material must prepend apiSchemas = ["MaterialBindingAPI"]; a primvar reader's varname is a string, not a token. A preview surface has one diffuse slot, so whichever of the mesh's base color and its per-vertex colors is present carries the composed product (vertex colors go out as displayColor fed to diffuse through a UsdPrimvarReader_float3; a texture's tint is carried in UsdUVTexture.inputs:scale) and the other steps aside with a note. MeshMaterial grew the format-expressible finish (metallic/roughness/opacity/ior/clearcoat/clearcoatRoughness, spec defaults, carried-not-rendered) so a Scene can hold what USD can say; a non-PBR finish maps specularSharpness → roughness through the shipped area-light curve (2/(s+2))^(1/4), and Drawer.activeLights was extracted from makeLighting so the renderer and the export can't disagree about which lights a frame had. Nothing is dropped quietly: 2D drawing, point clouds / GPU particles / raymarched fields (skipped at their own funnels, isosurface/particleSurface named as the way through), stylized finishes, animation and skinning, and the environment (deliberate, and usually right: a viewer lights a model with its own surroundings, which in AR is the actual room) each note once. Textures package as PNG entries in a .usdz at an anchored relative path and are noted-and-skipped for a bare .usda (one text file has nowhere to put an image). Output is byte-reproducible. Example 3D/Geometry/SpatialExport; SceneExportTests (29: round trips through the shipped reader for geometry / materials / textures / cameras / every light kind / transforms, the written-color-is-linear and half-angle checks that a matching pair of mistakes would slip past, package layout, determinism, the 3MF-untouched guard, and usdchecker --arkit over a package carrying every light kind, a texture, per-vertex colors, and a PBR material, tool-gated like the usdcat cross-check); no pixel snapshot (the output is files). Guide Ch 42 § Something you can walk around: USDZ + the drawn-versus-reopened figure; Docs/Output/Spatial.md.
  • Spatial video (Sources/Ollin/Export/SpatialVideoExport.swift + Sources/Ollin/3D/Stereo.swift). The moving counterpart of the USDZ model: --export-spatial <path.mov> / OllinApp.exportSpatialVideo renders a stereo pair per frame and muxes it as MV-HEVC spatial video, the format Apple's platforms play with real depth. A sketch declares stereoGeometry (StereoGeometry(interocular:convergence:), .automatic), either number derivable and either overridable per export (--interocular, --convergence, --meters-per-unit, plus the video export's --frames/--seconds/--fps/--skip/--bitrate/--quality); Camera3D.stereoPair(_:aspect:) / stereoEye(_:interocular:convergence:) / StereoGeometry.resolved(for:aspect:) are the public geometry. Load-bearing: the pair comes out of one performDraw() rendered twice with Drawer.aimStereoEye(_:previous:) swapping the camera between renders, never two draws (a second draw rolls the rng and steps every sim again, and the artificial-life tier is documented as not frame-exact even at one seed, so the eyes would see different worlds); the repeat render is free because statefulEncodeIsRepeat already exists for the live frame grab, and previous: moves with the eye or motion blur reads the eye separation itself as a sideways lurch. The convergence lean is one matrix entry, [2][0], correct for all three projections by two separate routes (perspective/intrinsic: clip w is the view distance, so a constant NDC shift collapses into it, the asymmetric frustum a parallel rig is defined by, the jittered identity; orthographic: no such w, so it needs a depth-proportional shear, which works out to the same value), and stereoLateral defaults to 0 so an ordinary camera's projection is untouched by construction. The eyes step along the camera's own right axis and stay parallel (toeing in leaves unfusable vertical parallax at the corners). The derived spacing is a stated criterion: 1% of the frame width at the convergence plane, which for a camera with a vanishing point is also 1% at infinity, the cinema comfort figure; the frame width is read off the built projection's [0][0] so an intrinsic lens measures the same way. Five metadata keys are what make the system call it spatial (HasLeft/HasRightStereoEyeView, HorizontalFieldOfView in millidegrees, StereoCameraBaseline in micrometers, HorizontalDisparityAdjustment, which is 0 because convergence is already in the pictures); drop one and AVAssetPlaybackAssistant reports .stereoMultiviewVideo but not .spatialVideo, so that near neighbor is not the test. All three of MVHEVCVideoLayerIDs/MVHEVCViewIDs/MVHEVCLeftAndRightViewIDs are written: naming the middle without the last hangs the encoder rather than refusing. Buffers must be IOSurface-backed, from the receiver's own pool; the write path uses the macOS 26 TaggedPixelBufferGroupReceiver.appendImmediately, not the adaptor already deprecated at the deployment floor; settings are built at frame 0 because the lens and the spacing are only known once draw() has set the camera; and the video input is expectsMediaDataInRealTime = true (the ordinary video export does not need it) because a file-paced multi-layer input stops accepting frames at the end of the first ~1 s chunk and never recovers (measured at frame 36 of a 30 fps export: status still .writing, error nil, readiness false forever), the same exemption the soundtrack input already takes. Envelope, stated not faked: an accumulating sketch (one persistent surface) and a 2D sketch read flat with a one-time note, and anything the sketch flattened during draw() (project(), depth(at:), a billboard) lands on the screen plane in both eyes. Sound comes along via the shipped soundtrack path. Example 3D/Geometry/SpatialVideo; SpatialVideoTests (11: the geometry against counterfactual twins, the system's own .spatialVideo reading, the metadata numbers, and a wider-eyes-disagree-more probe over both decoded layers; the convergence-lean sabotage is red in three); no pixel snapshot (the output is a file). Mechanism: ARCHITECTURE.md § Spatial video. Guide Ch 42 § Something you can look into: spatial video; Docs/Output/Spatial.md#spatial-video.
  • Variations & seed exploration. A sketch is a generator, so its seed is first-class: Sketch.init rolls variation (a five-digit Int, 1…99_999) and seeds both random and noise from it, so an unseeded run still varies yet always names the number that brings it back (every export's recipe now carries a seed). Load-bearing: seed(_:) sets variation (it's the one host-navigable seed); randomSeed/noiseSeed are partial reseeds that leave variation alone (so the recipe still splits them). Two unseeded exports of a randomness-free sketch differ only in the recorded seed (SVGExportTests pins body-equality; snapshot pixels unaffected, none re-recorded). Live navigation is SketchRunner.restart(variation:), an in-place restart on the same instance (reseed → frameCount = 0 → resetAccumulation() → re-run setup() next frame), so @Param values survive; it must call sketch.loop(), not just unpause the view — a still noLoop() sketch's fresh noLoop() is only a change (and only re-pauses) if the running state was restored first. The shared VariationCardView (core Live/) drives it from all four inspector surfaces via OllinActiveSketch.runner; its seed box fills while focused-but-empty, and the detached panel drops the keyboard it is handed on becoming key (AppKit gives an ordered-front window's first text field the keyboard whether or not the window is key, and SwiftUI's default focus agrees, so a standalone run's seed box used to open empty, take the first click into the panel whatever was clicked, and keep the keyboard until another field was clicked; InspectorPanelFocusTests pins the sequence against a real panel, verified red by sabotage), a click on any inspector's bare surface ends typing (endsTypingOnBackgroundTap, on all four surfaces), and a value pill's scrub surface stays in the tree while the pill is typed in rather than being removed (a hover view taken out from under the pointer never pops the cursor it set, which left the resize cursor on the whole screen); and SketchSession.recordSeed carries a navigated seed across hot reload the way tuned parameters do (applied in syncParams before setup(), so a sketch that pins its own seed still wins; pinned by OllinLiveCoding --sessiontest). Export side: --seed N reseeds before setup() on every export path (one make() wrapper in handleCommandLine), and --export-grid / OllinApp.contactSheet(of:seeds:) / exportContactSheet tile one frame per seed into a labeled proof sheet, with --export-sweep / contactSheet(of:sweeping:values:seed:) the same sheet along one named @Param instead (or by key path, sweeping: \.$radius / \.$rings, the projected Param box found by identity among parameters() so the compiler checks the name and the sheet is the named one; VariationTests.sweepingByKeyPathIsTheNamedSweep; values apply via the hosts' ParamStored restore path so a tile matches the dragged parameter; every tile pins one seed — given, or rolled once and recorded — so the parameter is the only thing changing; unknown name → nil + the sketch's actual parameter list on stderr; both sheets share one private tiling core; the framing is deliberate, don't re-blend it: seeds = a piece's identity (the Variations story, matching the generative-art seed culture — @eaviles keeps Docs/Core/Variations.md seed-pure) while the sweep is a tuning tool, documented on the Parameters/Export pages) — a fresh sketch per tile (no state leak) over one reused renderer (OllinApp.renderImage(of:frame:fps:renderer:), factored out of image(of:)), resetAccumulation() between tiles, and the renderer's same-frame stamp keyed on Drawer.serial, never the drawer's address (a tile's drawer can be handed the freed address of the tile before it, at the same frame, and read as a repeat: its feedback served empty textures, so every tile from the third drew black; PersistentLayerExportTests forces the reuse), tile width floored at 64px, the sheet's own recipe carrying the seed list (ExportMetadata.sheetRecipe). Example Randomness/Variations (a whole composition per seed — the decisions live in setup()); VariationTests. Docs/Core/Variations.md, Docs/Output/Export.md.
  • Record & replay. A run written down as a Take (Sources/Ollin/Core/Take.swift, versioned Codable JSON: the variation seed, per-frame clock samples exactly as the display drove them — live jitter included, every input event and @Param change frame-stamped, plus the starting parameter values) and played back exactly, to the byte. Load-bearing: both halves live on two choke points — every driver (live window, all export loops, benchmark) funnels through Sketch.advance, where the player overrides the caller's clock after applying that frame's events and parameter changes and the recorder writes down whichever clock applies; and all input enters Sketch through its internal handlers, each of which taps the recorder and gates external calls off while a player is attached, with the player re-ingesting through the same paths so the hooks (mousePressed() et al.) fire again on replay. The stamp contract: an event or change stamped k was received after frame k drew and applies at the advance where pre-increment frameCount == k (frames[k] is that advance's clock) — recording and playback must share the pre-increment stamp or replays shear by a frame. Take.install(on:) must land before setup() (seed and starting parameters first, so setup builds from them); --seed beside --replay re-seeds after install on purpose (same gestures, different variation; pinned). Windowed: --record-take <file> (standalone + OllinLive — the recorder attaches in the runner's first-frame block ahead of setup(), autosaves on a doubling cadence since a growing take re-encodes whole, value-copy written off-main; quit writes the final file, and a reload or seed-restart ends/restarts the take) and --replay <file> (live input gated, keyboard becomes the transport: space pause/start-over, arrows step, shift-arrows jump 30, Home/End; a backward scrub rewinds and re-simulates, stepReplayFrame mirroring renderImage's stateful-GPU stepping; a rewind swaps in a fresh instance (type(of: sketch).init() through the reload path, reloaded suppressed) because stored-property state (a rate gate, a particle array) otherwise carries the first pass's end into the second and the re-simulation cannot walk the recorded path, which is exactly how reseeding the same instance measured wrong; a scrub's crossed frames are held quiet through the TransportMutable seam (Synth drops noteOns; releases pass) except a forward jump of at most the shift step (30), and the landed frame always speaks — accumulate/feedback frames render offscreen, everything else stepComputes). Headless: --replay composes with every export flag through the one make() wrapper plus the advance override (video-shaped exports default their length to the take's). Determinism scope: time, inputs, parameters, random/noise replay; live feeds, OSC/MIDI, and Date() reads do not (documented). Verified byte-identical through the CLI (replay-export twice, hashes equal; non-replay differs) and TakeTests (12: exact trace + hook replay, input gate, parameter landing on its frame, past-end advance, seed-override, file round-trip + version refusal, GPU byte-identity, and the runner-level transport: End-then-space restarts, the end reached twice (the first of which is a rewind from past the end replaying the whole run), rewound scrub quiet with the landed frame loud, short forward jump audible, long jump quiet, the five transport claims verified red against the stale-instance rewind; player sabotage verified red) + TransportMuteTests (a muted noteOn never starts the engine). Docs/Core/Replay.md.
  • Keyframed parameters. A sketch's @Param values written down over time as an Automation (Sources/Ollin/Core/Automation.swift, versioned Codable JSON): one Track per parameter, each a list of Keys (time + ParamStored value + the Curve that leaves that key), read every frame at start + time * speed (a negative speed runs it backwards, loops wraps at duration, length holds past the last key before the wrap). Written in code as automate($parameter) { track in track.key(at:_:curve:) } (the projected value names the property by identity; a second call replaces that parameter's track) or read from a file (--automation <file>, honored by standalone runs and by every export path, through configureTransport plus the export make() wrapper). Load-bearing: the player applies inside Sketch.advance, after the take player resolves the clock and before takeRecorder.recordFrame — a run recorded under an automation has to write down the values the curves actually held, or a plain replay shears by a frame (sabotage-verified red); the parameter is set, not retargeted, so a smoothing: parameter never glides twice (the curve is the glide); the parameter handles are cached once (the @Param set is fixed and reflecting per frame costs real time), which is why the automation setter mutates the existing player rather than building a new one. Blending is by kind: number/color/vector/vector3/rect/insets/range interpolate (color through Color.mix's even path), while boolean/option/text and any mismatched pair hold what they left; both ends short-circuit, so a key reads back as exactly itself. Curve is .hold/.linear/.easeIn/.easeOut/.easeInOut over the shipped Easing catalog plus .bezier(x1:y1:x2:y2:), the editable one, solved by Newton with a bisection fallback — its handles bend the clock, not only the value, so reading it means inverting x for the parameter first and only then taking y (the test measures against an independent bisection; a symmetry-only test cannot see the difference and passed under sabotage). Keys sort on construction with ties keeping declaration order; outside its span a track is a constant. AutomationTests (20 tests: curve monotonicity and endpoints, the Bezier inversion, hold, per-kind blending, span clamping, sort ties, speed/direction/loop/length, file round-trip and version refusal, the sketch reading the curve including a smoothed parameter, the recorder stamp, an unknown parameter skipped). Example Motion/Automation (four kinds of track, with the sketch plotting its own tracks under the stage). Docs/Core/Automation.md, Docs/Output/Export.md.
  • Parameter timeline. OllinLive's authoring surface over Automation (Sources/Ollin/Live/TimelineModel.swift + TimelinePanel.swift; the clock transport on SketchRunner): a floating resizable panel (the Timeline chip / ⌘T, OllinHUD.showTimelineKey), one lane per track — number lanes plot the track's sampled value normalized over the span, color lanes draw the blend as a band sampled from the track itself, switches step, and a formula track shows its rule and takes no keys — plus a per-row keyframe diamond in every inspector row (KeyframeDiamond, rendered only where a host injects \.automationTimeline, so the gallery and other hosts show nothing, and only while the panel shows or the parameter already has a track or a rule (showsMark(for:) over isPanelShown, set by beginFollowingClock/endFollowingClock), so a sketch that never keyframes keeps a clean inspector; ruled 2026-10-13). The authoring loop is scrub → adjust the parameter → click the diamond (places a key at the playhead holding the parameter's value; on a key it re-keys a changed value and removes an unchanged one, compared against what the track gives at that moment); keys drag in time, double-click removes, a click on a key selects it and scrubs to it (select(track:index:)), a ruler scrub within 6 pt of a key snaps to it (snapped(_:within:)), a selected key's value shows in the footer beside its moment and curve (readout(of:param:), a whole number with no decimals), .bezier grows drag-handles on the lane, and every edit registers on the model's own UndoManager (groupsByEvent off; a lane drag is one group, opened by its first edit through beginDrag(span:)/endDrag; the panel's delegate hands the manager to the Edit menu, so ⌘Z/⇧⌘Z work while the panel is key, for an edit made from a row too). A hand edit under a hold draws a hand pass (2026-10-13): SketchSession.recordParam asks refreshClockFrame(keepingHandValues: true), a nudge pass whose advance(appliesAutomation: false) skips the player, so the picture follows the inspector and the track does not take the value back before the diamond keys it; the next scrub, step, or plain refresh applies the tracks again (the first audit found step 2 of the loop blind and a re-key impossible). Every edit reads the clock, never the ticked playhead (toggleKey syncs first, hasKeyAtPlayhead reads livePosition): the ticker runs only while the panel shows, so a diamond clicked with the panel closed keyed the moment the panel was closed. The scale includes the playhead (Scale(contentDuration:playhead:lockedSpan:), so a clock run past the last key stays on the ruler, where it left it and the panel read as frozen) and holds its span through a drag on the ruler or a lane (lockedSpan; the last key dragged past the span grew the ruler under the pointer and jumped ahead of it, and a scrub back from a far playhead shrank the ruler under the pointer). loopRegion is observed (as @ObservationIgnored state the region and the loop button appeared only on the next playhead change). The panel's hosting view accepts the first mouse, so a drag coming from the inspector acts on its first click. The diamond sits at the trailing edge of every row (KeyframeDiamond after the control, KeyframeDiamondGap under it on a second line of fields), one column whatever the control's width, where before controls of differing widths zigzagged it. Load-bearing: the transport is the runner's own (setClockPaused/scrubClock/stepClock/clockLoopRegion, package): a hold stops the display link and is enforced at the top of draw(in:) like the replay pause (a queued tick must not advance a clock the panel just placed, and an accumulating canvas must not composite the held frame twice), while a reload's first frame, a setup rerun, and a camera snap still pass under a zero step; a scrub or step is one deliberate pass (pendingClockNudge) that waits for the frame ring rather than dropping (the take exception's shape — a dropped pass ate a backward step, caught red) and drops any interpolator-held frame first (also caught red); a backward nudge places the clock and hands advance a zero step, never a negative one; a take owns the whole transport (beginTake/replay release the hold). The panel's axis is the automation's position, never the raw clock (position(at:) forward, clockTime(forPosition:) inverting start + time * speed for scrubs), so a looping pass shows the playhead coming around while the clock runs on — the first live run had the playhead silently off the ruler's right edge — and toEnd lands a frame short under loops, since the exact end wraps to zero. The loop region lives on the transport in position space, mapped onto the clock; it is never written into the file. Edits land on the sketch's own automation (tracks rebuilt through Automation.Track(name:keys:) so the sort invariants hold; a worked-out track is refused), debounce to the file (<Sketch>.automation.json sibling by default, --automation overrides; OllinLive reads it on launch and SketchSession.automation re-installs it before setup() on every swap, keeping the documented file-before-setup precedence), and refreshClockFrame redraws the held frame so an edit made while paused shows. The model holds its runner weakly (the session owns it — a test must keep the harness alive). The row is also the field that takes a rule (RuleLine in Inspector.swift, over TimelineModel.setRule / checkRule / removeRule / beginRule / rule(param:) / takesRule): right-click the diamond of a number, whole number, or switch for Write a Rule, a monospaced field opens under the row, Return installs the text as a formula track through the same apply (file, reload carry, held-frame refresh), an emptied field removes it, Escape reverts, and a ruled parameter shows the function mark with its control disabled (the sync pull keeps it following; a hand would be undone next frame). The refusals happen where the text is, never at play time: the parser's FormulaError.offset (a Character index, Array(source)) puts a caret under the character in the field's own column grid (same mark, padding, and monospaced face); a name nothing supplies is refused at the name (its token offset, since a discovered variable would read as 0) rather than reported on stderr as drive does; a self-reference and a ring through another parameter are refused at the closing name by building the candidate automation and asking Automation.rings() (package, over the player's own plan) before anything is written; a kind no formula can drive (menu, text, color, the parts kinds) and a track that already holds keys are refused outright, and the menu never offers them. Never install a formula parsed against the declared name list (Formula(_:variables:)): its variables would be every readable name, so readNames would make every ruled parameter depend on every other and the plan would see rings everywhere; parse discovered, then check. writingRule is model state (the row open with no rule yet), never a write. A track the sketch writes itself reaches the row: the diamond and the rule line observe only editCount and playhead, and a drive/automate in setup() bumps neither (the panel's ticker runs only while it shows), which left every slider row hollow and ruleless on the first capture while the stepper and switch beside them showed theirs; the model now watches the sketch's automation from the moment a runner is attached (250 ms, an Automation ==) and bumps automationVersion, which the rows read beside editCount. ParameterTimelineTests (38: the transport tests including the ring wait and the reload hold, the edit tests, the position mapping, the scale and plot tests, six rule-field tests, three of them verified red under a zeroed offset, and the hold-loop tests: the hand pass, the re-key, the clock read without the ticker, the scale with the playhead and under a drag, the snap, the select, undo and redo with a drag as one step, the marks' visibility, and the readouts) + the rule phase of OllinLive --paramtest (the write, the file, four offsets, the refusals). Docs/Tools/Timeline.md § Writing a rule in the row; Guide Ch 43 § Writing the parameter as a rule: formulas.
  • Cues (Sources/Ollin/Core/Cues.swift, Sources/Ollin/Live/CuesCard.swift, the carry in OllinRuntime.SketchSession). A Cue is one named set of every @Param's value ([String: ParamStored], the inspector's own payloads, so every kind is carried); saveCue(_:) stores the parameters as they stand (replacing a same-named cue where it stands), cue(_:over:) by name / by index from 0 / nextCue / previousCue (wrapping) / cue(CueRequest, over:) calls one back at once or over an ease-in-out fade run from Sketch.advance after the automation and before the take recorder (so a track still wins its parameter and a take holds the fade); a kind with nothing between two values (bool, option, choices, text, colors) jumps on the fade's first frame, the rest blend through Automation.blend; a cue called mid-fade starts from where the parameters are; a name the sketch no longer declares is skipped and a parameter not in the cue keeps its value. CueSheet is the ordered list with a version (a newer file is refused), Sketch.cueSheet the one carried, currentCue the last called, isCueFading, loadCues(from:) / saveCues(to:) for a sketch on its own, --cues <file> / --cue <name> on every export path and the window (applied in applyCommandLineParams after setup(), before --param; without --cues the sketch's own sheet is searched, which is how OllinLive's sibling file serves an export: its export path loads Sketch.cues.json beside the automation and installs it on the loaded sketch). Load-bearing: the hosts keep the sheet in Sketch.cues.json beside the sketch (the automation's precedent), through SketchSession.adoptCueFile(_:), which re-installs the sheet on every freshly loaded sketch before setup() and files every save and delete through the sketch's package var cuesChanged hook (a path with no file yet is where the first save lands; an untitled performance buffer keeps its cues in memory until saved); CuesCardView (a public inspector card in both hosts, under the parameters and above the Controls card) lists the cues, calls one over the fade it keeps under CuesCardView.fadeKey in the defaults (read by the performance host's controls too), saves the parameters under a typed name or "Cue N", updates a cue whose name is typed, deletes, and polls currentCue every 100 ms so a cue the sketch calls itself lights up; the performance host answers a MIDI program change with cue(.number(program)) on any channel with nothing learned, /ollin/cue with a name or a number and an optional fade argument, and two press actions cueNext / cuePrevious (/ollin/cue/next, /ollin/cue/previous, learnable). "Snapshot" was not used (the render-image tests and the remote surface's wire image own it), nor "preset" (a named built-in value of one type). CueTests (11 tests), the --sessiontest cue-carry phase, the --controltest cues phase; Examples/Live/Cues with its Sketch.cues.json; Docs/Helpers/Cues.md; Guide Ch 43 § A look you come back to: cues.
  • Parameters driven by a formula. A @Param set from a rule read as text rather than from Swift source (Sources/Ollin/Math/Formula.swift + the formula leg of Automation.swift): drive($radius, "190 + sin(time * tau / 6) * 80"), for Double / Int (rounded) / Bool (on for anything but zero). Formula is a public, Sendable, pure value: a hand-written tokenizer plus recursive descent, names resolved at parse time to positions (so a per-frame evaluation touches no dictionary), variables reporting the free names in discovery order, value(_:noise:) taking them positionally or by name, and a second initializer taking a declared name list so an unknown name is refused with what was available. FormulaError carries a message and the character offset, which is what a typed-in field needs to point at the spot. Vocabulary: the arithmetic/comparison/logic operators, ~45 functions spelled and argument-ordered to match the framework's own and the shader library's (clamp/lerp/mix/step/smoothstep/map/fract/saturate, variadic min/max, if(c, a, b), noise/signedNoise), and pi/tau/e. Two spellings are load-bearing because they change a picture rather than fail to build: ^ binds tighter than unary minus and associates right (-2^2 == -4, 2^3^2 == 512) — the ordinary calculator table, and deliberately not LSystemExpression's, which binds unary tighter per its published formalism (the test asserts both sides); and %/mod floor (-1 % 3 == 2), matching fract and the shader spelling, not Swift's %. There is no random (a formula must answer the same number for the same moment, or a directed run stops rendering twice the same); noise reads a supplied NoiseField, and Sketch.noiseField() hands over the sketch's own so noiseSeed() reaches it. A formula is a Track like a keyed one (same Automation, same loops/speed/start/length, same --automation file, encoded as its source text so a person edits it there; format version went to 2 and the load guard relaxed to <= so v1 files still read). It names time (always overridden to the automation position, inside Track.value), frame (frameCount + 1, because the player runs before the sketch steps its counter), width/height/mouseX/mouseY, and the sketch's other number/switch parameters by name — built-ins win a spelling clash. Load-bearing: tracks apply in dependency order, not declaration order (a depth-first plan cached per track set), so a parameter worked out from another lands on the same frame — a snapshot-then-apply design was built first and rejected, because its one-frame lag makes the picture depend on the frame rate and a 30 fps export stops matching the 60 fps window; a parameter read back from param.stored after restore so the next formula sees the clamped value; and a ring (two parameters naming each other, or "n + 1") cannot settle on one frame, so the whole ring is reported once and left alone rather than played at a frame-rate-dependent value. drive never throws: a parse error or a name nothing supplies is reported on stderr and costs that one parameter (pass a try Formula(...) to handle it yourself). A parameter of more than one number takes one rule per part (drive($eye, x:y:), plus Vector3, Color, Rectangle, Insets, ClosedRange<Double>; Track.parts, format version 3): the part names are Automation.parts(of:)'s keys (x/y/z, red/green/blue/alpha, x/y/width/height, top/right/bottom/left, lower/upper) and Automation.applying(_:to:) writes them back into the parameter's current stored value, which is what leaves a part with no rule alone (the hand keeps it while the part beside it plays). parameter.part is a readable name too ("frame.x + frame.width / 2"), so the tokenizer joins a dot to the name it follows (a dot with no letter after it is still an error, and 1.5 is still a number), the plan follows a dotted name to the parameter it names (Automation.baseName(of:)) so ordering and ring detection both hold, and the player feeds every part of a multi-number parameter back as name.part — including one a keyed track is carrying. Three rules that only a test can pin: one call carries the whole parameter (a second drive replaces the first, so give every part at once), a pair of ends stays ordered (upper is lifted to meet a lower that climbs past it, because ClosedRange.restored refuses a crossed pair outright and the parameter would silently freeze), and a part naming its own parameter is a ring (refused whole, since parts read one shared snapshot). A rule that cannot be read costs that one part; a call naming no part at all writes no track. FormulaTests (20 tests) + FormulaTrackTests (10) + FormulaPartTests (14); sabotages verified red for the numbers (precedence, floored %, the NaN guard, eager if counted through the noise field, declaration order, the ring, frame, the time override) and four more for the parts (a misnamed part in applying, the dotted-name-to-parameter mapping, the crossed-pair lift, the joined dot). Examples Motion/Formula (six driven parameters, printing the text that drives each) and Motion/FormulaParts (a ruled size over an unruled place, a dot reading its rectangle back part by part). No pixel snapshot (the claims are numbers). Docs/Helpers/Formula.md, Docs/Core/Automation.md.

↑↓ move ↩ open