Skip to content

Renderers

Turning a scene into pixels on a Vulkan device. Three renderers live in afield.graphics.scene.fabricated, and they disagree about what a frame costs.

StandardRenderer is the one that runs at frame rate. A small deferred renderer — g-buffer, shadow map, ambient occlusion, lighting composite — built on the Field 4 primitives rather than hiding them, so you feed it meshes and wire a RenderTarget around the passes it hands back. FabricateFrameGraph is that pass list made ordered and named, and it does not hide the underlying Target objects: inserting a pass and manipulating one directly are both available. FabricatePostStack and FabricatePassChain are the two post-processing surfaces, and they differ in one thing: a FabricatePostStack pass inherits its source's resolution and format, while a FabricatePassChain lets every pass declare its own, which is what a half-resolution reduce or a downsample pyramid needs.

PathTracingRenderer is the one that converges. A progressive GPU path tracer with a stackless software BVH that works on every Field Vulkan device and KHR ray queries where the device has them. It accumulates: the image improves while nothing moves, camera motion resets or reprojects that history, and a lighting edit always starts again. Its scene and environment are snapshots — PathTraceScene and PathTraceEnvironment are read once at construction, so editing a material means building a new tracer rather than mutating a live one. Output is a fixed-resolution linear HDR render target, and blitToScreen is a separate call because exposure and tone mapping are a presentation decision. PathTraceGltf loads glTF into it, sRGB-decoding base colour while leaving metallic-roughness linear.

GaussianSplatRenderer is the one that renders something measured rather than modelled. Splats from a capture, sixteen floats each, sorted and blended rather than rasterized as triangles; SplatSequence reads a directory of .compressed.ply frames in any order through a bounded most-recently-used cache, because scrubbing a shot is random access and a forward-only reader would be no use for working.

Underneath all three sits FabricateScene, a Three.js-shaped scene graph that owns no Vulkan resources at all: nodes carry transforms, renderables carry material and rendering intent, and FabricateScenePlanner turns that into the opaque, alpha-masked and back-to-front transparent draw queues a renderer consumes. That independence is what lets the scene graph, the pass graph and the render plan be exercised without a GPU: FabricateSceneGraphTestMain, FabricatePassGraphTestMain and FabricateRenderPlanTestMain are plain mains that do, carrying their own assertions. They are not in the FIELD_TEST smoke catalog, so run them by hand.

Two constraints apply to the renderers themselves. A renderer is constructed, installed and closed on Field's render thread, like any other Field 4 module — installing under a name already in use retires the previous occupant, so there is no coroutine lifetime bookkeeping to do. And a device is required: nothing here that draws does so into an FLine, or in a headless test, the way the Math page's contents do. For the same ground with pictures, see The Fabricate renderer, Path tracing and Field 4 graphics.

46 source file(s), 797 public symbol(s), 427 of them carrying KDoc. Everything below is the documentation that lives beside the code — the same text the completion documentation panel shows in the editor. A blank summary means the declaration has no KDoc yet, not that this page dropped it.

FabricateDirectLighting.kt

What connects StandardRenderer's raster bundle to the ray-traced DirectLighting pass.

That pass needs a G-buffer and a ray-visibility scene that agree about where the surfaces are. registerOpaqueTriangles fills the raster G-buffer from the same indexed OpaqueTriangles snapshot the ray acceleration structure keeps, expanding it once for the pulling shader instead of uploading geometry every frame. directLighting then hands the bundle's G-buffer images and its post-stack output to the pass, and rtxdiSettings supplies the camera contract the optional RTXDI path wants, read from the bundle's own raster projection.

Opaque diffuse surfaces only: transparency, textured emission and latent pigments want an adapter nothing here provides yet.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateDirectLighting.kt — read the source

Used in: Direct lighting

Top-level functions

Declaration Summary
fun StandardRenderer.Bundle.directLighting(visibility: OpaqueRayScene, name: String = "direct-light"): DirectLighting Build the experiment from Fabricate's existing G-buffer. Call bundle.attachTo first, then lighting.attach; present lighting.output. SOURCE reads the unchanged post-stack output. Opaque diffuse only: transparency, textured emission and latent pigments require a future adapter.
fun StandardRenderer.Bundle.rtxdiSettings() Camera contract for the optional RTXDI pass, matching this bundle's raster projection.
fun StandardRenderer.Bundle.registerOpaqueTriangles(name: String, geometry: OpaqueTriangles, color: Vector3f = Vector3f(0.7f), transform: Matrix4f = Matrix4f()): StandardRenderer.Bundle.PullingMeshPair Populate the existing raster G-buffer from the SAME indexed snapshot used for ray visibility. Fabricate's current pulling shader consumes expanded vec4 triangles; the adapter expands once. The ray BLAS keeps indexed XYZ data. No per-frame geometry upload or silent alpha approximation.

FabricateFrameGraph.kt

The renderer's off-screen passes as a named, ordered list, and the one place that order is written into a RenderTarget.

A StandardRenderer.Bundle holds its five built-in passes as five separate fields, which is a set rather than a sequence. FabricateFrameGraph takes a bundle and makes it a sequence: g-buffer, shadow map, ambient occlusion, lighting, transparent composite, then one entry for each effect currently in the bundle's FabricatePostStack. Every built-in name is a constant on the companion, and passNameFor supplies the two that arrive as post effects rather than as bundle fields, motion blur and anti-aliasing; anything else is named after its effect. So insertAfter("my-pass", PASS_LIGHTING, target) works without knowing how the bundle was built.

attachTo is where the order stops being advisory. A RenderTarget renders its preTargets before its own pass and in map order, so attaching removes and re-adds every pass by name, which leaves preTargets iterating in the graph's order. It also remembers, per target, which names this graph put there last time and drops the ones that have since gone, while a pass some other code added under a name this graph never used is left alone. Attaching a second time is therefore a resync rather than a duplicate.

The graph is a view. StandardRenderer.Bundle.frameGraph() builds a fresh one on each call, it does not own the targets it names, and it does not stop a box reaching past it to a RenderToTexture directly.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateFrameGraph.kt — read the source

FabricateFrameGraph

class FabricateFrameGraph private constructor(private val graph: FabricatePassGraph<Target>)

Ordered, inspectable pass contract for StandardRenderer.

Declaration Summary
fun passNames(): List<String>
fun passes(): List<Pair<String, Target>>
operator fun get(name: String): Target?
fun replace(name: String, target: Target): FabricateFrameGraph
fun append(name: String, target: Target): FabricateFrameGraph
fun insertBefore(name: String, before: String, target: Target): FabricateFrameGraph
fun insertAfter(name: String, after: String, target: Target): FabricateFrameGraph
fun remove(name: String): Target?
fun attachTo(renderer: Renderer, windowIndex: Int = 0): RenderTarget
fun attachTo(renderer: Renderer, window: Window): RenderTarget
fun attachTo(target: RenderTarget): RenderTarget

FabricateGltfBridge.kt

The glTF side of the fabricated scene model: one call that turns a loaded GltfSupport.GltfModel into a FabricateScene, and two conversions beside it.

There are two routes from a loaded model into the PBR renderer and they differ in what they touch. FabricateGltfBridge.fromModel builds a scene graph and stops, so a caller can read or rewrite the imported nodes before any GPU resource exists. StandardRenderer.Bundle.registerGltf does that same import and then registers a pulling mesh per node, which is what the renderer draws from and what a tool inspecting node names does not need.

Nothing here loads a file. GltfSupport.loadGltfBasic is the loader and MeshFromDisk.kt describes the route in; this is the conversion at the end of it.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateGltfBridge.kt — read the source

Top-level functions

Declaration Summary
fun GltfSupport.MaterialTextured.toFabricateMaterial(): FabricateMaterial FabricateMaterial.fromGltf as an extension, for a glTF material already in hand.
fun Int.toFabricateTextureSemantic(): String An Assimp texture-type constant as the glTF name for that slot: baseColor, normal, metallicRoughness and so on, with an unrecognised type rendered as texture- and its number rather than dropped.

FabricateGltfBridge

object FabricateGltfBridge

Converts the existing Assimp/GLTF loader model into the fabricated scene/material data model.

Declaration Summary
fun fromModel(model: GltfSupport.GltfModel, name: String = "gltf"): FabricateScene A FabricateScene holding model's meshes, with no renderer and no GPU resources attached.

FabricateMaterialLibrary.kt

The flattened per-material record a render queue carries, and the table that hands out its ids.

FabricateMaterial is the authoring shape: nineteen fields, six of them texture handles. FabricateMaterialRecord is nine fields wide, and toRecord is the conversion. It keeps base colour, metallic, roughness, alpha cutoff, alpha mode and double-sidedness as they are, turns receiveAo into a 0-or-1 occlusion scale, and leaves the textures, clearcoat, the UV transform, receiveShadow and sortOrder behind. Two of the record's fields are placeholders rather than conversions: emissive is written as zero instead of the material's emissiveFactor, and textureMask as 0.

FabricateMaterialLibrary keys materials by identity rather than by equality, so a material shared between nodes gets one id and two materials with matching values get two. An id is a position in a dense list, which is why record(id) is a plain index and clear() invalidates every id handed out before it. idFor converts a material the first time it sees one and returns the existing id afterwards without re-reading it; refresh re-reads into the same slot. FabricateScenePlanner keeps a library and calls refresh for each visible renderable as it plans, so FabricateRenderPlan's materials list is current for the frame it was planned in, and each item's materialId indexes it.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateMaterialLibrary.kt — read the source

Top-level functions

Declaration Summary
fun FabricateMaterial.toRecord(): FabricateMaterialRecord

FabricateMaterialRecord

data class FabricateMaterialRecord(val baseColor: Vector4f, val metallic: Float, val roughness: Float, val occlusion: Float, val emissive: Vector3f, val alphaCutoff: Float, val alphaMode: FabricateAlphaMode, val doubleSided: Boolean, val textureMask: Int)

One material flattened to nine fields, addressed by id from a render queue.

Declaration Summary
val baseColor: Vector4f
val metallic: Float
val roughness: Float
val occlusion: Float
val emissive: Vector3f
val alphaCutoff: Float
val alphaMode: FabricateAlphaMode
val doubleSided: Boolean
val textureMask: Int

FabricateMaterialLibrary

class FabricateMaterialLibrary

Stable CPU-side material table. This is the handoff shape for the future GPU material buffer: scene nodes carry material objects, while render queues address compact material records by id.

Declaration Summary
fun idFor(material: FabricateMaterial): Int
fun refresh(material: FabricateMaterial): Int
fun record(id: Int): FabricateMaterialRecord
fun records(): List<FabricateMaterialRecord>
fun clear()

FabricatePassChain.kt

The vocabulary for declaring a chain of post passes that are not all the same shape, and the pure planner that resolves one.

A chain is a list of FabricatePassSpec. Each spec names its pass, carries its fragment shader, and says how big it wants to be (FabricatePassSize), in what format, which earlier pass feeds its primary sampler, and which other earlier passes it wants bound to named samplers. A size may be absolute, a fraction of the pass's own input, or a fraction of the texture entering the chain, which is what lets one chain hold a full-resolution pass, a quarter-resolution blur and a half-float intermediate.

FabricatePassChain.plan turns that list into a FabricatePassChainPlan without making a Vulkan call: it resolves every target shape and every sampler binding, works out which pass last reads each pass's output, and puts two passes on the same physical target when their outputs never overlap in time and their shapes match. Being arithmetic and reachability rather than GPU state is what lets FabricatePassChainTest exercise all of it on the CPU, which this layer needs more than most: a resolution resolved one texel wrong renders a picture that looks entirely plausible.

FabricatePassChainPlan.report prints the resolution it arrived at — every pass with its shape, samplers and target slot, and the memory total against one full-resolution target per pass.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePassChain.kt — read the source

FabricatePassChain

object FabricatePassChain

Declarative, heterogeneous pass chains for FabricatePostStack — every pass may have its own resolution, its own format, and its own named inputs.

Declaration Summary
const val CHAIN_INPUT Reference name for the texture entering the chain — the stack input, not a pass.
const val GBUFFER_MOTION Reference name for the renderer's per-pixel motion vectors — g-buffer attachment 3, not a pass in this chain.
const val GBUFFER_DEPTH Reference name for the renderer's depth buffer — the g-buffer's sampled depth, not a pass.
val EXTERNAL_REFERENCES: Set<String> The reference names that resolve outside the chain rather than to one of its passes.
const val PRIMARY_SAMPLER The sampler name a pass's primary input is always bound to.
const val PERSISTENT_IMAGE_COUNT Images a persistent pass's target holds — the ping-pong depth.
fun plan(passes: List<FabricatePassSpec>, inputWidth: Int, inputHeight: Int, inputFormat: Int): FabricatePassChainPlan Plan passes against a chain input of inputWidth x inputHeight in inputFormat.

FabricatePassShape

data class FabricatePassShape(val width: Int, val height: Int, val format: Int, val mipLevels: Int = 1)

The resolved shape of a pass target: what FabricatePassSize and the format request came to.

Declaration Summary
val bytes: Long Bytes of color attachment this shape occupies, mip chain included.
override fun toString(): String

FabricatePassSize

sealed interface FabricatePassSize

How a pass's target is sized.

Declaration Summary
fun resolve(source: FabricatePassShape, chainInput: FabricatePassShape): Pair<Int, Int>
val independentOfSource: Boolean True if resolve ignores its source argument.
data object MatchSource : FabricatePassSize The size of the pass's own primary input — the historical, and still the default, behaviour.
data object MatchChainInput : FabricatePassSize The size of the texture entering the chain, whatever the pass immediately before did.
data class Absolute(val width: Int, val height: Int) : FabricatePassSize A fixed size in texels, independent of the chain.
data class FractionOfSource(val numerator: Int, val denominator: Int) : FabricatePassSize numerator/denominator of the pass's primary input. Chain these to build a pyramid.
data class FractionOfChainInput(val numerator: Int, val denominator: Int) : FabricatePassSize numerator/denominator of the chain input — a stable absolute rung, whatever precedes it.

FabricatePassRef

sealed interface FabricatePassRef

Which texture a pass sampler is bound to.

Declaration Summary
data object ChainInput : FabricatePassRef The texture entering the chain.
data class Pass(val index: Int, val name: String) : FabricatePassRef The output of an earlier pass.
data class PreviousFrame(val index: Int, val name: String) : FabricatePassRef A FabricatePassSpec.persistent pass's output as of the end of last frame — the other half of its ping-pong pair.
data class External(val reference: String) : FabricatePassRef A texture the renderer owns rather than the chain — FabricatePassChain.GBUFFER_MOTION or FabricatePassChain.GBUFFER_DEPTH.

FabricatePassInput

data class FabricatePassInput(val samplerName: String, val fromPass: String, val previousFrame: Boolean = false)

An extra named sampler a pass wants bound to another pass's output.

Declaration Summary
val samplerName: String
val fromPass: String
val previousFrame: Boolean

FabricateResolvedInput

data class FabricateResolvedInput(val samplerName: String, val from: FabricatePassRef)

A resolved FabricatePassInput — sampler name plus the texture the planner bound it to.

FabricatePassSpec

data class FabricatePassSpec(val name: String, val fragment: String, val size: FabricatePassSize = FabricatePassSize.MatchSource, val format: Int? = null, val source: String? = null, val extraInputs: List<FabricatePassInput> = emptyList(), val push: Map<String, Any> = emptyMap(), val mipLevels: Int = 1, val persistent: Boolean = false, val sourcePreviousFrame: Boolean = false, val frameHistory: Boolean = false)

One declared pass.

Declaration Summary
val name: String
val fragment: String
val size: FabricatePassSize
val format: Int?
val source: String?
val extraInputs: List<FabricatePassInput>
val push: Map<String, Any>
val mipLevels: Int
val persistent: Boolean
val sourcePreviousFrame: Boolean
val frameHistory: Boolean

FabricatePlannedPass

data class FabricatePlannedPass(val index: Int, val spec: FabricatePassSpec, val shape: FabricatePassShape, val inputs: List<FabricateResolvedInput>, val lastConsumer: Int, val slot: Int)

A pass target after planning: its shape, its bound inputs, its lifetime and the slot it shares.

Declaration Summary
val index: Int
val spec: FabricatePassSpec
val shape: FabricatePassShape
val inputs: List<FabricateResolvedInput>
val lastConsumer: Int
val slot: Int
val name: String
val persistent: Boolean True if this pass's target is a ping-pong pair rather than a single image.
val frameHistory: Boolean True if this pass asked for the chain's frame history.

FabricateTargetSlot

data class FabricateTargetSlot(val index: Int, val shape: FabricatePassShape, val passIndices: MutableList<Int>, val persistent: Boolean = false)

A physical target, and every pass that takes a turn writing to it.

Declaration Summary
val index: Int
val shape: FabricatePassShape
val passIndices: MutableList<Int>
val persistent: Boolean
val imageCount: Int Images this slot allocates — two for a ping-pong pair, one otherwise.
val bytes: Long Color-attachment bytes this slot occupies, ping-pong pair included.

FabricatePassChainPlan

class FabricatePassChainPlan(val passes: List<FabricatePlannedPass>, val slots: List<FabricateTargetSlot>, val input: FabricatePassShape)

A resolved chain: what to allocate, in what order to run it, and what it costs.

Declaration Summary
val passes: List<FabricatePlannedPass>
val slots: List<FabricateTargetSlot>
val input: FabricatePassShape
val targetCount: Int Physical targets this chain needs.
val passCount: Int Targets a one-target-per-pass allocator would need.
val plannedBytes: Long Color-attachment bytes this chain needs.
val naiveBytes: Long Color-attachment bytes one full-resolution, chain-input-format target per pass would need.
val unsharedBytes: Long Bytes the multi-resolution shapes alone save, before any slot sharing.
val persistentPasses: List<FabricatePlannedPass> Passes whose target is a ping-pong pair — the chain's state, if it has any.
val historyPasses: List<FabricatePlannedPass> Passes that asked for the chain's frame history — the chain's reprojecting passes.
val output: FabricatePlannedPass The chain's final pass.
operator fun get(name: String): FabricatePlannedPass?
fun passesOn(slot: Int): List<FabricatePlannedPass> Passes sharing slot's target, in the order they write it.
fun report(): String A human-readable audit of the plan: every pass with its resolved shape, its bound samplers, the slot it writes and when its output dies, then the memory totals against the counterfactual.

FabricatePassChainBuilder.kt

Materializes a planned pass chain onto the GPU, through the post stack that already exists.

The division of labour is strict. FabricatePassChain.plan decides everything: every resolution, every format, which texture every sampler reads, which passes can share a target. This file decides nothing. It walks the plan and calls StandardRenderer.Bundle.addShaderEffect with sizes already resolved to FabricatePassSize.Absolute, so there is no second, subtly different implementation of the arithmetic sitting where only a GPU can reach it. That matters more here than it usually would, because a resolution bug in a pass chain renders a picture that looks fine.

The passes land in the same FabricatePostStack as everything else, so FabricateFrameGraph schedules them, the stack's output() is the chain's output, insertBefore and remove still work on them by name, and a chain composes with addBloom and setToneMapping rather than replacing them.

FabricateBuiltChain comes back holding the plan, the built effects and the chain input, and is how a push constant is retuned afterwards without a rebuild or a reallocation.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePassChainBuilder.kt — read the source

Top-level functions

Declaration Summary
fun StandardRenderer.Bundle.addPassChain(passes: List<FabricatePassSpec>, renderer: Renderer = fromContext(), printReport: Boolean = false): FabricateBuiltChain Materialize passes as a new chain or replace their existing named passes.
fun FabricatePostEffect.setPush(name: String, value: Any): Boolean Set one push constant on a post effect's fullscreen quad.
fun FabricatePostEffect.setPushBinding(name: String, value: () -> Any): Boolean Bind one push constant on a post effect's fullscreen quad to a supplier re-read every frame.

Top-level values

Declaration Summary
const val GBUFFER_MOTION_ATTACHMENT The g-buffer colour attachment holding per-pixel motion vectors.

FabricateBuiltChain

class FabricateBuiltChain(val plan: FabricatePassChainPlan, val effects: List<FabricatePostEffect>, val input: RenderToTexture, val history: FrameHistory = FrameHistory())

A chain that has been planned and built: the plan, the effects, and live push-constant access.

Declaration Summary
val plan: FabricatePassChainPlan
val effects: List<FabricatePostEffect>
val input: RenderToTexture
val history: FrameHistory The chain's frame history — shared by every pass that declared FabricatePassSpec.frameHistory, and inert if none did.
val output: RenderToTexture The chain's final image.
fun names(): List<String> Pass names, in run order.
fun pass(name: String): FabricatePostEffect? The named pass, or null if this chain has no such pass.
operator fun set(pushName: String, value: Any) Set a push constant on every pass in the chain that declares it.
fun targetOf(name: String): RenderToTexture? The target a named pass renders into, or null if there is no such pass.
fun stateOf(name: String): PingPongRenderToTexture? The ping-pong target behind a FabricatePassSpec.persistent pass, or null if name is not one.
fun stateNames(): List<String> Persistent pass names, in run order — the chain's state fields.
fun historyNames(): List<String> Names of the passes that receive frame history — the chain's reprojecting passes.
fun setPush(passName: String, pushName: String, value: Any): Boolean Retune a pass's push constant in place, with no rebuild and no reallocation.
val allocatedBytes: Long Sum of every pass target's color-attachment bytes, as actually allocated.
override fun toString(): String

FabricatePassGraph.kt

The ordering primitive the Fabricate renderer's pass collections are built on.

Both FabricatePostStack and FabricateFrameGraph hold a FabricatePassGraph, so what a pass name means, and where a new pass may go relative to one already there, is settled once here rather than once per collection. The same file carries the rule that decides where a replacement chain picks up its input: the output of the pass before the first one it replaces, or the chain's original input when that pass comes first.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePassGraph.kt — read the source

FabricatePassGraph

class FabricatePassGraph<T>(initial: Iterable<Pair<String, T>> = emptyList())

The ordered, name-keyed map both of this package's pass collections are built on.

Declaration Summary
val size: Int
fun names(): List<String>
fun entries(): List<Pair<String, T>>
operator fun contains(name: String): Boolean
operator fun get(name: String): T?
fun addLast(name: String, target: T): FabricatePassGraph<T>
fun replace(name: String, target: T): FabricatePassGraph<T>
fun putOrReplace(name: String, target: T): FabricatePassGraph<T>
fun insertBefore(name: String, before: String, target: T): FabricatePassGraph<T>
fun insertAfter(name: String, after: String, target: T): FabricatePassGraph<T>
fun remove(name: String): T?
fun clear(): FabricatePassGraph<T>

FabricatePassGraphTestMain.kt

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePassGraphTestMain.kt — read the source

FabricatePassGraphTestMain

object FabricatePassGraphTestMain

Tiny JVM smoke test for the pure ordered-pass data structure behind FabricateFrameGraph. Run with the backdoor's run-jvm-main or any JVM launcher that includes jvmMain classes.

Declaration Summary
fun main(args: Array<String>)

FabricatePassLibrary.kt

Worked multi-resolution passes, as lists of FabricatePassSpec ready to hand to a chain.

Six recipes and two chains assembled from them: a downsample pyramid, a separable Gaussian blur, a disc aperture blur, an upsample-and-add, a bright pass and a composite, then FabricatePassLibrary.apertureChain and FabricatePassLibrary.bloomPyramid. Every one returns specs rather than touching the GPU, so a chain can be composed from several of them, planned, printed and asserted on before anything is allocated, and a caller can splice a rung, reorder one or drop one.

Four of the fragment shaders live here as string constants; the separable blur reuses the nine-tap kernel the bloom chain already ships rather than keeping a second copy in step with it. All of them follow the post-stack conventions: a fullscreen triangle, vUv texel-aligned so v = 0 is image row 0, the primary input bound as sourceTex, and every offset scaled by the source's texel size, so a pass reads its input's texels rather than assuming its own resolution matches.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePassLibrary.kt — read the source

FabricatePassLibrary

object FabricatePassLibrary

A library of worked multi-resolution passes, expressed as FabricatePassSpec fragments.

Declaration Summary
const val MAX_APERTURE_TAPS Hard cap on aperture taps, so the disc loop is bounded at compile time.
const val MAX_HALF_EXACT_RESOLUTION Largest jumpFloodDistance resolution whose texel coordinates a half-float holds exactly.
fun downsamplePyramid(prefix: String = "reduce", levels: Int = 3, source: String? = null, format: Int? = null): List<FabricatePassSpec> A downsample pyramid: levels halvings of the source, each a 13-tap filtered reduce.
fun separableBlur(prefix: String = "blur", radius: Float = 2.0f, source: String? = null, size: FabricatePassSize = FabricatePassSize.MatchSource, format: Int? = null): List<FabricatePassSpec> A separable Gaussian blur: a horizontal pass then a vertical one, both at the source's size.
fun aperture(name: String = "aperture", radiusTexels: Float = 8f, taps: Int = 32, source: String? = null, size: FabricatePassSize = FabricatePassSize.MatchSource, format: Int? = null): FabricatePassSpec A disc (aperture) blur of radiusTexels, the shape a real lens makes out of a point of light.
fun upsampleAdd(name: String, source: String, over: String, weight: Float = 1f, radiusTexels: Float = 1f, format: Int? = null): FabricatePassSpec A tent upsample of source added onto over at over's resolution, weighted by weight.
fun composite(name: String = "composite", blurred: String, sharp: String, blend: Float = 0.5f, gain: Float = 1f, add: Float = 0f, format: Int? = null): FabricatePassSpec A composite: blurred blended over sharp at sharp's resolution.
fun brightPass(name: String = "bright", threshold: Float = 1f, source: String? = null, size: FabricatePassSize = FabricatePassSize.MatchSource, format: Int? = VK_FORMAT_R16G16B16A16_SFLOAT): FabricatePassSpec A soft-knee bright pass, reusing the kernel addBloom already ships.
fun apertureChain(nearRadius: Float = 4f, farRadius: Float = 12f, taps: Int = 32, blend: Float = 0.7f, format: Int? = null): List<FabricatePassSpec> The chain the hand-wired version is made of: source, reduce ½, reduce ¼, two aperture blurs at different radii on the cheap ¼ image, and a full-resolution composite.
fun bloomPyramid(levels: Int = 4, threshold: Float = 1f, intensity: Float = 0.7f, format: Int? = VK_FORMAT_R16G16B16A16_SFLOAT): List<FabricatePassSpec> A multi-resolution bloom: bright pass, a pyramid, then the pyramid walked back up with upsampleAdd, then composited additively.
fun stableFluid(prefix: String = "fluid", resolution: Int = 256, iterations: Int = 20, timestep: Float = 1f / 60f, velocityDissipation: Float = 0.999f, dyeDissipation: Float = 0.997f, vorticity: Float = 12f, format: Int = VK_FORMAT_R16G16B16A16_SFLOAT): List<FabricatePassSpec> A Stam-style stable fluid: a velocity field and a dye field that persist across frames, with a splat you paint into either of them.
fun jumpFloodDistance(prefix: String = "jfa", resolution: Int = 512, source: String = FabricatePassChain.CHAIN_INPUT, threshold: Float = 0.5f, channel: Int = 3, signed: Boolean = false, plusOne: Boolean = true, scale: Float = 1f, format: Int = VK_FORMAT_R16G16B16A16_SFLOAT): List<FabricatePassSpec> A jump flood distance transform: a mask in, a distance field out, in 1 + log2(resolution) passes.
fun distanceBands(name: String = "distance-bands", source: String? = null, spacing: Float = 16f, inside: Vector3f = Vector3f(0.15f, 0.45f, 0.95f), outside: Vector3f = Vector3f(0.95f, 0.55f, 0.15f), format: Int? = null): FabricatePassSpec Bands and an isoline over a distance field, so a chain that ends in one shows something.
const val DOWNSAMPLE_13TAP_FRAGMENT Jimenez 13-tap downsample. Four inner taps at ±1 source texel carry half the weight; four overlapping 2x2 blocks on the ±2 ring carry an eighth each.
val APERTURE_FRAGMENT Disc (aperture) blur. Taps on a golden-angle spiral with r = sqrt(t) so they are uniform over the disc's area, weighted equally so the result is flat-topped with a hard rim — the bokeh shape, not a Gaussian.
const val UPSAMPLE_ADD_FRAGMENT 3x3 tent upsample of lowTex added onto sourceTex. Offsets are in lowTex texels, so the tent is a genuine reconstruction filter for the small image rather than a blur of the big one.
const val COMPOSITE_FRAGMENT Join blurTex onto sourceTex at sourceTex's resolution: crossfade by blend, then lay add of the blurred image over the top. Depth of field is blend near one; bloom is blend zero and add positive.
val FLUID_ADVECT_VELOCITY_FRAGMENT Semi-Lagrangian advection of the velocity field by itself.
val FLUID_CURL_FRAGMENT ∇×v of the velocity field, one channel, for the vorticity confinement force.
fun fluidForceFragment(vorticity: Boolean): String The force stage: vorticity confinement, then your splat.
val FLUID_DIVERGENCE_FRAGMENT ∇·v, the right-hand side of the pressure equation. One channel, hence its own format.
const val FLUID_ZERO_FRAGMENT Writes zero.
const val FLUID_JACOBI_FRAGMENT One Jacobi iteration of ∇²p = ∇·v.
val FLUID_PROJECT_FRAGMENT The projection: subtract ∇p from the velocity field, leaving it (approximately) divergence-free. The pass that writes the fluid's persistent velocity state.
val FLUID_ADVECT_DYE_FRAGMENT Advects the dye by this frame's projected velocity and adds the mass splat — the fluid's other persistent field, and the one you actually look at.
const val JUMP_FLOOD_SEED_FRAGMENT Jump flood seeding. Writes the texel's own centre as a seed coordinate where the mask says so, and a z = 0 "no seed" sentinel where it does not.
const val JUMP_FLOOD_FRAGMENT One jump flood round: keep whichever of the nine candidates at ±stride carries the seed closest to this texel.
const val JUMP_FLOOD_DISTANCE_FRAGMENT Turns a flooded seed field into a distance: r is the distance, gb the seed coordinate it came from, a the mask.
const val DISTANCE_BANDS_FRAGMENT Even bands and a dark isoline over a signed or unsigned distance field, with the two sides coloured differently.
const val MAX_MOTION_BLUR_TAPS Hard cap on cameraMotionBlur taps, so the streak loop is bounded at compile time.
const val MOTION_DISPLAY_MAX_MODE Highest temporalDebug mode — availability.
const val FRAME_HISTORY_GLSL The GLSL every FabricatePassSpec.frameHistory pass shares: clip/UV conversion and the reprojection lookup.
fun temporalAccumulate(name: String = "taa", source: String? = null, blend: Float = 0.9f, rejection: Float = 0f, ndcDepth: Float = 0.5f, perPixelMotion: Boolean = true, resolution: Pair<Int, Int>, format: Int = VK_FORMAT_R16G16B16A16_SFLOAT): List<FabricatePassSpec> A TAA-style temporal accumulation: blend this frame into a reprojected history, with a rejection rule.
fun cameraMotionBlur(name: String = "camera-motion-blur", source: String? = null, strength: Float = 1f, taps: Int = 9, ndcDepth: Float = 0.5f, format: Int? = null): List<FabricatePassSpec> Camera motion blur: smear each pixel along the direction it came from.
fun temporalDebug(name: String = "motion-display", source: String? = null, mode: Int = 0, motionScale: Float = 8f, ndcDepth: Float = 0.5f, perPixelMotion: Boolean = true, format: Int? = null): List<FabricatePassSpec> The debug view, in four modes — and it is not garnish.
fun temporalAccumulateFragment(perPixelMotion: Boolean = true) Neighbourhood-clamped temporal accumulation against a reprojected history.
fun temporalDebugFragment(perPixelMotion: Boolean = true) The debug view's four modes, sharing one shader because they share one reprojection.
fun cameraMotionBlurFragment(taps: Int): String Camera motion blur over taps samples along the backward-motion direction.
enum class SummaryStatistics How a cell's value statistics are carried up the pyramid.
const val SUMMARY_VALUE Plane carrying (weight, value mean, value variance, occupancy) — see SummaryStatistics.
const val SUMMARY_MOMENT Plane carrying the first and second spatial raw moments, (Σwx, Σwy, Σwx², Σwy²).
const val SUMMARY_EXTENT Plane carrying (Σwxy, min value, max value, ·) — the cross moment and the two extremes.
const val SUMMARY_RANGE Plane carrying (first, count, prefix, ·) — a node's range in the sorted array.
val SUMMARY_PLANES: List<String> The three planes both producers write, in the order summaryPyramid emits them.
const val MAX_EXACT_MORTON_BITS Largest Morton code an fp32 channel holds exactly, as a bit count.
const val MAX_POINT_QUADTREE_POINTS Largest point count mortonPointQuadtree accepts.
fun summaryPlane(prefix: String, plane: String, depth: Int): String The pass name a plane's level lands on: $prefix-$plane-$depth.
fun pointSearchSteps(count: Int): Int Binary-search steps needed to bracket count sorted keys — 1 + ⌈log₂ count⌉.
fun summaryPyramid(prefix: String = "summary", resolution: Int = 256, source: String = FabricatePassChain.CHAIN_INPUT, coarsest: Int = 0, weightChannel: Int = -1, valueChannel: Int = -1, statistics: SummaryStatistics = SummaryStatistics.POOLED, format: Int = VK_FORMAT_R32G32B32A32_SFLOAT): List<FabricatePassSpec> A summarised quadtree over a raster: every 2×2 block reduced to its parent, all the way up, carrying mergeable sufficient statistics instead of a colour.
fun mortonPointQuadtree(prefix: String = "quadtree", points: String, pointGrid: Int = 32, depth: Int = 4, coarsest: Int = 0, densityScale: Float = 1f, statistics: SummaryStatistics = SummaryStatistics.POOLED, format: Int = VK_FORMAT_R32G32B32A32_SFLOAT): List<FabricatePassSpec> A summarised quadtree over a point set: Morton codes, a sort, one node level found by bracketing prefixes in the sorted array, and then the same reduction ladder as summaryPyramid.
fun summaryEllipses(name: String = "summary-ellipses", over: String = FabricatePassChain.CHAIN_INPUT, prefix: String = "summary", depth: Int = 4, sigma: Float = 1f, weightGain: Float = 1f, underGain: Float = 0.35f, tint: Vector3f = Vector3f(0.35f, 0.9f, 1f), format: Int? = null): FabricatePassSpec Draws a level of a summarised quadtree as geometry: a centroid, a 1σ covariance ellipse and a weight tint per cell, over the image it summarises.
fun summaryCovariance(name: String = "summary-covariance", prefix: String = "summary", depth: Int = 4, gain: Float = 1f, format: Int? = null): FabricatePassSpec The spatial covariance of every cell of one level, as (cxx, cyy, cxy) — the statistic the moment planes exist to make available, computed once.
enum class SubdivisionCriterion What makes a cell worth splitting — the question a summarised quadtree exists to answer.
fun adaptiveSummaryCells(prefix: String = "summary", name: String = "adaptive", over: String = FabricatePassChain.CHAIN_INPUT, coarsest: Int = 1, finest: Int = 5, criterion: SubdivisionCriterion = SubdivisionCriterion.VALUE_VARIANCE, threshold: Float = 0.02f, weightFloor: Float = 1e-4f, statistics: SummaryStatistics = SummaryStatistics.POOLED, sigma: Float = 1f, weightGain: Float = 1f, underGain: Float = 0.45f, tint: Vector3f = Vector3f(0.35f, 0.9f, 1f), format: Int? = null): List<FabricatePassSpec> An adaptive cut of a summarised quadtree: descend from the root, stop where the cell is a good enough description of its contents, and draw the cells you stopped at.
fun summaryReadout(name: String = "summary-readout", from: String? = null, channel: Int = 0, gain: Float = 1f, bias: Float = 0f, format: Int? = null): FabricatePassSpec One channel of one plane, scaled into 0..1 — the instrument every assertion about a summary is made through.
fun summaryVariance(name: String = "summary-variance", from: String? = null, statistics: SummaryStatistics = SummaryStatistics.POOLED, gain: Float = 1f, format: Int? = null): FabricatePassSpec The cell variance of a SUMMARY_VALUE plane, scaled — which takes different arithmetic for the two SummaryStatistics, and is the only consumer that has to know which it is looking at.
const val SUMMARY_CHANNEL_GLSL Channel selection and the leaf's weight/value pair, shared by every seed pass.
val SUMMARY_VALUE_SEED_POOLED_FRAGMENT Seeds (weight, mean, variance, occupancy) from one texel.
val SUMMARY_VALUE_SEED_RAW_FRAGMENT Seeds (weight, Σwv, Σwv², occupancy) — the raw-moment alternative.
val SUMMARY_MOMENT_SEED_FRAGMENT Seeds the spatial first and second moments, (Σwx, Σwy, Σwx², Σwy²), from one texel.
val SUMMARY_EXTENT_SEED_FRAGMENT Seeds (Σwxy, min v, max v, 0).
const val SUMMARY_CHILDREN_GLSL The 2×2 gather every reduction does: the four children of the texel being written.
val SUMMARY_MEAN_REDUCE_FRAGMENT Reduce by averaging: correct for every channel that is a per-area sum.
val SUMMARY_VALUE_REDUCE_POOLED_FRAGMENT Reduce (weight, mean, variance, occupancy) by the weighted pooled-variance identity.
val SUMMARY_EXTENT_REDUCE_FRAGMENT Reduce (Σwxy, min, max, ·): the cross moment averages, the extremes take their own operator.
val SUMMARY_RANGE_REDUCE_FRAGMENT Reduce (first, count, prefix, ·): the parent's range is its children's.
val SUMMARY_READOUT_FRAGMENT One channel, scaled and biased into something an 8-bit readback can carry.
val SUMMARY_COVARIANCE_FRAGMENT (cxx, cyy, cxy) from the three planes, scaled — and cxy biased, because it is signed.
fun summaryVarianceFragment(statistics: SummaryStatistics): String The variance of a SUMMARY_VALUE cell, however that plane happens to carry it.
const val MORTON_GLSL Bit interleave, and the reason a Morton code is a tree path rather than a hash.
fun mortonCodeFragment(depth: Int, count: Int, width: Int): String Quantise a point into the leaf grid and interleave — (code, index, ·, ·), both normalised.
fun pointRankFragment(count: Int, width: Int): String Each element counts the elements that precede it — the gather half of a sort.
fun pointGatherFragment(count: Int, width: Int): String Each slot picks up the element whose rank it is — the other half of the sort.
fun pointNodesFragment(depth: Int, count: Int, width: Int, steps: Int): String One node per texel: its Morton prefix, and the range of the sorted array holding its points.
fun pointLeafValueFragment(count: Int, width: Int, pooled: Boolean): String Seeds the value plane for a node, by Welford over its points.
fun pointLeafMomentFragment(count: Int, width: Int): String Seeds (Σwx, Σwy, Σwx², Σwy²) for a node, from the points in its range.
fun pointLeafExtentFragment(count: Int, width: Int): String Seeds (Σwxy, min v, max v, 0) for a node, with the same empty-cell sentinels as the raster.
fun summarySelectFragment(criterion: SubdivisionCriterion, statistics: SummaryStatistics, hasParent: Boolean): String One level of the descent: does this cell stop here, or do its children get a turn?
fun summaryAdaptiveDrawFragment(cells: Int): String Composites one level of the cut over the levels above it: a cell outline always, an ellipse and a centroid where there is mass.
fun summaryEllipsesFragment(cells: Int): String A centroid, a 1σ ellipse and a weight tint per cell, over the image underneath.

FabricatePostEffects.kt

High-level post-processing helpers for StandardRenderer.Bundle, layered on top of the ordered FabricatePostStack. These are the "make HDR look right" and "make emissive glow" effects that Three.js users expect from an EffectComposer: tone mapping + exposure, and bloom.

All passes are built with StandardRenderer.Bundle.addShaderEffect, so they chain automatically (each samples the previous stack output as sourceTex). Build the post stack once, then call bundle.attachTo(renderer) so the new passes are synced into the window's frame graph.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePostEffects.kt — read the source

Used in: Fabricate Renderer

Top-level functions

Declaration Summary
fun StandardRenderer.Bundle.setToneMapping(mode: FabricateToneMap = FabricateToneMap.ACES, exposure: Float = 1f, renderer: Renderer = fromContext()): FabricatePostEffect? Set (or replace) the final tone-mapping pass. Tone mapping always runs last in the post stack, so it maps HDR lighting into a displayable range after every other effect. Re-invoking this replaces the previous tone-map pass; FabricateToneMap.NONE removes it. Remember to re-attach the frame graph afterwards so the pass is scheduled.
fun StandardRenderer.Bundle.addBloom(threshold: Float = 1.0f, intensity: Float = 0.6f, radius: Float = 2.5f, renderer: Renderer = fromContext()): List<FabricatePostEffect> Add a bloom chain: a bright-pass extract, a separable Gaussian blur, and an additive composite back over the pre-bloom image. Bloom runs on HDR values, so it is inserted before any tone-mapping pass. Calling this again rebuilds the chain. Re-attach the frame graph afterwards.

Top-level values

Declaration Summary
const val TONEMAP_EFFECT_NAME

FabricateToneMap

enum class FabricateToneMap

Tone-mapping operator passed to setToneMapping.

Constants: NONE, REINHARD, FILMIC, ACES, AGX.

FabricatePostStack.kt

The ordered post-processing stack a Fabricate pass lands in, and the record describing one pass.

One source texture enters the stack, named passes run in order, and the last one's target is the image to present; a stack with no passes presents its input unchanged. Ordering is delegated to FabricatePassGraph, so passes are addressed by name: a feature can insert before or after a pass it did not add, and two features that each append a pass compose rather than overwriting one another.

FabricatePostEffect is what the stack stores: a name, the target the pass renders into, the module holding its pipeline, and a kind tag. That tag is how FabricateFrameGraph recognises the two passes with a fixed place in the standard pass order, motion blur and anti-aliasing; every other pass is named after its effect.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricatePostStack.kt — read the source

FabricatePostStack

class FabricatePostStack(val input: RenderToTexture, initial: Iterable<FabricatePostEffect> = emptyList())

Ordered post-processing stack for Fabricate, following Three.js' EffectComposer model: one source texture enters the stack, named passes run in order, and the final pass is the image to present.

Declaration Summary
val input: RenderToTexture
fun names(): List<String>
fun effects(): List<FabricatePostEffect>
operator fun get(name: String): FabricatePostEffect?
fun append(effect: FabricatePostEffect): FabricatePostStack
fun insertBefore(effect: FabricatePostEffect, before: String): FabricatePostStack
fun insertAfter(effect: FabricatePostEffect, after: String): FabricatePostStack
fun replace(name: String, effect: FabricatePostEffect): FabricatePostStack
fun remove(name: String): FabricatePostEffect?
fun output(): RenderToTexture

FabricatePostEffect

data class FabricatePostEffect(val name: String, val target: RenderToTexture, val module: ModulePointer?, val kind: Kind = Kind.SHADER)

One pass in a FabricatePostStack: its name, output target, optional shader module and kind. Shader effects own a module. Native effects record compute from their target and leave module null. FabricateFrameGraph gives motion blur and anti-aliasing their reserved names in the standard pass order; other effects use their own name. Native effects are excluded from shader inspection and reload.

Declaration Summary
val name: String
val target: RenderToTexture
val module: ModulePointer?
val kind: Kind
enum class Kind Which reserved slot in the standard pass order this pass claims, if any.

FabricateRenderPlan.kt

Render queue data structures and the FabricateScenePlanner that builds them from a FabricateScene.

The planner walks the scene graph, assigns each visible renderable to one of three queues based on its material's alphaMode (Opaque, AlphaMask, or Transparent), and sorts each queue for correct draw ordering: opaque and masked items front-to-back by sort order, transparent items back-to-front by camera distance. Shadow casters are collected from the opaque and masked sets. FabricateScene.planScene calls the planner; StandardRenderer.Bundle.applyScenePlan consumes the resulting FabricateRenderPlan.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateRenderPlan.kt — read the source

Used in: Fabricate Renderer

Top-level functions

Declaration Summary
fun FabricateNode.isEffectivelyVisible(): Boolean

Top-level values

Declaration Summary
val FabricateMaterial.renderQueue: FabricateRenderQueue

FabricateRenderItem

data class FabricateRenderItem(val node: FabricateNode, val renderable: FabricateRenderable, val materialId: Int, val queue: FabricateRenderQueue, val world: Matrix4f, val previousWorld: Matrix4f, val cameraDistanceSquared: Float)

One visible renderable node, resolved with its material ID, world transforms, and camera distance.

Declaration Summary
val node: FabricateNode
val renderable: FabricateRenderable
val materialId: Int
val queue: FabricateRenderQueue
val world: Matrix4f
val previousWorld: Matrix4f
val cameraDistanceSquared: Float

FabricateRenderPlan

data class FabricateRenderPlan(val opaque: List<FabricateRenderItem>, val alphaMask: List<FabricateRenderItem>, val transparent: List<FabricateRenderItem>, val shadowCasters: List<FabricateRenderItem>, val materials: List<FabricateMaterialRecord>)

Declaration Summary
val opaque: List<FabricateRenderItem>
val alphaMask: List<FabricateRenderItem>
val transparent: List<FabricateRenderItem>
val shadowCasters: List<FabricateRenderItem>
val materials: List<FabricateMaterialRecord>
val allDrawItems: List<FabricateRenderItem>

FabricateRenderQueue

enum class FabricateRenderQueue

Constants: Opaque, AlphaMask, Transparent.

FabricateScenePlanner

class FabricateScenePlanner(private val materialLibrary: FabricateMaterialLibrary = FabricateMaterialLibrary())

Builds deterministic opaque/masked/transparent queues from a FabricateScene.

Declaration Summary
fun plan(scene: FabricateScene, cameraPosition: Vector3f? = null): FabricateRenderPlan
fun plan(root: FabricateNode, cameraPosition: Vector3f? = null): FabricateRenderPlan

FabricateRenderPlanTestMain.kt

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateRenderPlanTestMain.kt — read the source

FabricateRenderPlanTestMain

object FabricateRenderPlanTestMain

Pure JVM smoke test for material records and opaque/masked/transparent queue planning.

Declaration Summary
fun main(args: Array<String>)

FabricateScene.kt

The scene graph: nodes, transforms, materials, lights, animation playback, and the glTF import that fills them.

This file owns no Vulkan resources and imports no device type. A FabricateNode carries a transform and children; a renderable carries a FabricateMaterial and the intent to be drawn; updateWorldTransforms composes the hierarchy into world matrices. None of that needs a GPU, so the scene graph, the animation sampler and the draw-order planning in FabricateRenderPlan.kt are all exercisable from a plain main, which FabricateSceneGraphTestMain and FabricateSceneTestMain do without a device.

FabricateTransform is a position/rotation/scale triple rather than a matrix, so handing the graph a composed Matrix4f is not enough on its own: FabricateTransform.set(Matrix4f) decomposes it back into the triple. A transform that cannot be expressed as one, such as a shear, will not survive the round trip.

Lights are a list, and an empty list is not the same as no lighting: it keeps the single default directional light, so a scene that never touches lights is still lit. One shadow-casting directional light is treated as the key light and drives the shadow map; the rest reach the deferred lighting pass through a lights buffer.

FabricateAlphaMode is the field that decides which of the three draw queues a renderable joins, and it is the same OPAQUE/MASK/BLEND distinction glTF makes, carried through rather than collapsed.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateScene.kt — read the source

Used in: Fabricate Renderer, Geometry from disk

Top-level functions

Declaration Summary
fun StandardRenderer.Bundle.applyRenderProfile(profile: FabricateRenderProfile = FabricateRenderProfile.outOfBox()): StandardRenderer.Bundle
fun StandardRenderer.Bundle.sceneDrawLists(cameraPosition: Vector3f = Vector3f(scene.camera.at)): FabricateDrawLists
fun StandardRenderer.Bundle.planScene(cameraPosition: Vector3f = Vector3f(scene.camera.at), planner: FabricateScenePlanner = FabricateScenePlanner()): FabricateRenderPlan
fun StandardRenderer.Bundle.applyScenePlan(plan: FabricateRenderPlan = planScene()): FabricateRenderPlan
fun StandardRenderer.Bundle.registerGltf(path: String, namePrefix: String = "gltf", config: GltfSupport.GltfLoadConfig = GltfSupport.GltfLoadConfig(), renderer: afield.graphics.vulkan.Renderer = fromContext()): FabricateRegisteredGltf
fun StandardRenderer.Bundle.registerGltf(model: GltfSupport.GltfModel, namePrefix: String = "gltf", renderer: afield.graphics.vulkan.Renderer = fromContext()): FabricateRegisteredGltf
fun StandardRenderer.Bundle.PullingMeshPair.applyFabricateMaterial(material: FabricateMaterial): StandardRenderer.Bundle.PullingMeshPair
fun StandardRenderer.PullingMesh.applyFabricateMaterial(material: FabricateMaterial): StandardRenderer.PullingMesh

FabricateScene

class FabricateScene(val root: FabricateNode = FabricateNode("root"))

A small Three.js-like scene layer for Fabricate, independent of Vulkan resources: nodes own transforms, renderables own material/rendering intent, and renderer bindings consume the ordered draw lists.

Declaration Summary
val root: FabricateNode
var renderProfile: FabricateRenderProfile
var animationTime: Float
val lights: MutableList<FabricateLight> Scene lights, Three.js style. The renderer treats one shadow-casting directional light as the key light (it drives the shadow map and the built-in LightData uniform); every other light is uploaded to the deferred lighting pass through a lights buffer. An empty list keeps the historical single default directional light untouched, so existing scenes render unchanged.
fun addLight(light: FabricateLight): FabricateLight
fun node(name: String, parent: FabricateNode = root, configure: FabricateNode.() -> Unit = {}): FabricateNode
fun mesh(name: String, parent: FabricateNode = root, material: FabricateMaterial = FabricateMaterial(), configure: FabricateNode.() -> Unit = {}): FabricateNode
fun updateWorldTransforms(time: Float = animationTime): FabricateScene
fun updateWorld(): FabricateScene
fun traverse(visit: (FabricateNode) -> Unit)
fun advance(seconds: Float, loop: Boolean = true): FabricateScene
fun animationDuration(): Float
fun drawLists(cameraPosition: Vector3f = Vector3f(0f, 0f, 0f), time: Float = animationTime): FabricateDrawLists
fun importGltf(model: GltfSupport.GltfModel, namePrefix: String = "gltf", parent: FabricateNode = root, material: (GltfSupport.MeshInstance) -> FabricateMaterial = { FabricateMaterial.fromGltf(it.material) }): List<FabricateNode>

FabricateNode

class FabricateNode(val name: String)

Declaration Summary
var parent: FabricateNode?
val children: MutableList<FabricateNode>
val transform: FabricateTransform
var worldMatrix: Matrix4f
var previousWorldMatrix: Matrix4f
var renderable: FabricateRenderable?
var visible: Boolean
var animationSamples: List<GltfSupport.TRSSample>?
var source: Any?
fun add(child: FabricateNode): FabricateNode
operator fun plusAssign(child: FabricateNode)
fun remove(child: FabricateNode): Boolean
fun walk(): Sequence<FabricateNode>
fun translate(x: Float, y: Float, z: Float): FabricateNode
fun setTranslation(x: Float, y: Float, z: Float): FabricateNode
fun updateFromRoot(): FabricateNode
fun updateWorld(parentWorld: Matrix4f? = parent?.worldMatrix, force: Boolean = true): FabricateNode
fun worldPoint(local: Vector3f = Vector3f()): Vector3f
fun worldPosition(dest: Vector3f = Vector3f()): Vector3f

FabricateTransform

data class FabricateTransform(val position: Vector3f = Vector3f(), val rotation: Quaternionf = Quaternionf(), val scale: Vector3f = Vector3f(1f, 1f, 1f))

Declaration Summary
val position: Vector3f
val rotation: Quaternionf
val scale: Vector3f
var matrixOverride: Matrix4f? The exact matrix set was handed, kept only when the TRS triple above cannot reproduce it.
fun clearMatrix(): FabricateTransform Discard matrixOverride and go back to composing the TRS fields.
fun matrix(): Matrix4f
fun set(matrix: Matrix4f): FabricateTransform
fun set(position: Vector3f, rotation: Quaternionf = Quaternionf(), scale: Vector3f = Vector3f(1f, 1f, 1f)): FabricateTransform

FabricateAlphaMode

enum class FabricateAlphaMode

Constants: OPAQUE, MASK, BLEND.

FabricateMaterial

data class FabricateMaterial(val baseColor: Vector4f = Vector4f(1f, 0.56f, 0.24f, 1f), var metallic: Float = 0.08f, var roughness: Float = 0.42f, var baseColorTexture: LoadedTexture? = null, var normalTexture: LoadedTexture? = null, var metallicRoughnessTexture: LoadedTexture? = null, var occlusionTexture: LoadedTexture? = null, var emissiveTexture: LoadedTexture? = null, var clearcoatTexture: LoadedTexture? = null, var emissiveFactor: Vector3f = Vector3f(0f, 0f, 0f), var clearcoatFactor: Float = 0f, var clearcoatRoughnessFactor: Float = 0f, var uvTransform: Vector4f = Vector4f(0f, 0f, 1f, 1f), var uvTransformMaps: Int = UV_ALL_MAPS, var alphaMode: FabricateAlphaMode = FabricateAlphaMode.OPAQUE, var alphaCutoff: Float = 0.5f, var doubleSided: Boolean = false, var receiveShadow: Boolean = true, var receiveAo: Boolean = true, var sortOrder: Int = 0)

Declaration Summary
val baseColor: Vector4f
var metallic: Float
var roughness: Float
var baseColorTexture: LoadedTexture?
var normalTexture: LoadedTexture?
var metallicRoughnessTexture: LoadedTexture?
var occlusionTexture: LoadedTexture?
var emissiveTexture: LoadedTexture?
var clearcoatTexture: LoadedTexture?
var emissiveFactor: Vector3f
var clearcoatFactor: Float
var clearcoatRoughnessFactor: Float
var uvTransform: Vector4f UV offset (xy) and scale (zw), applied to the maps in uvTransformMaps.
var uvTransformMaps: Int Which maps uvTransform applies to, UV_BASE_COLOR … UV_CLEARCOAT; the rest are sampled at their own UVs.
var alphaMode: FabricateAlphaMode
var alphaCutoff: Float
var doubleSided: Boolean
var receiveShadow: Boolean
var receiveAo: Boolean
var sortOrder: Int
fun transparent(opacity: Float = baseColor.w): FabricateMaterial

FabricateRenderable

data class FabricateRenderable(val material: FabricateMaterial = FabricateMaterial(), var castShadow: Boolean = true, var boundsCenter: Vector3f = Vector3f(), var boundsRadius: Float = 1f, var userData: Any? = null)

Declaration Summary
val material: FabricateMaterial
var castShadow: Boolean
var boundsCenter: Vector3f
var boundsRadius: Float
var userData: Any?
fun sortKey(node: FabricateNode): Int

FabricateDraw

data class FabricateDraw(val node: FabricateNode, val renderable: FabricateRenderable, val worldMatrix: Matrix4f, val worldCenter: Vector3f, val distanceToCamera: Float, val sortKey: Int)

Declaration Summary
val node: FabricateNode
val renderable: FabricateRenderable
val worldMatrix: Matrix4f
val worldCenter: Vector3f
val distanceToCamera: Float
val sortKey: Int

FabricateDrawLists

data class FabricateDrawLists(val opaque: List<FabricateDraw>, val transparent: List<FabricateDraw>, val shadowCasters: List<FabricateDraw>)

Declaration Summary
val opaque: List<FabricateDraw>
val transparent: List<FabricateDraw>
val shadowCasters: List<FabricateDraw>
val allVisible: List<FabricateDraw>

FabricateRenderProfile

data class FabricateRenderProfile(var ambientOcclusion: Boolean = true, var aoStrength: Float = 0.6f, var aoRadius: Float = 0.35f, var softShadows: Boolean = true, var shadowStrength: Float = 1f, var shadowSoftness: Float = 1.75f, var motionBlur: Boolean = true, var antiAlias: Boolean = true)

Declaration Summary
var ambientOcclusion: Boolean
var aoStrength: Float
var aoRadius: Float
var softShadows: Boolean
var shadowStrength: Float
var shadowSoftness: Float
var motionBlur: Boolean
var antiAlias: Boolean
fun applyTo(bundle: StandardRenderer.Bundle): StandardRenderer.Bundle

FabricateLightType

enum class FabricateLightType

Constants: DIRECTIONAL, POINT, SPOT.

FabricateLight

data class FabricateLight(var type: FabricateLightType = FabricateLightType.DIRECTIONAL, var position: Vector3f = Vector3f(0f, 3f, 0f), var direction: Vector3f = Vector3f(-0.35f, -1f, -0.25f), var color: Vector3f = Vector3f(1f, 1f, 1f), var intensity: Float = 1f, var range: Float = 0f, var innerConeDegrees: Float = 20f, var outerConeDegrees: Float = 30f, var castShadow: Boolean = false)

A light source. Directional lights model the sun (parallel rays, no falloff); point lights radiate from a position with inverse-square falloff clamped by range; spot lights add a cone defined by innerConeDegrees/outerConeDegrees. Only one directional light with castShadow can drive the shadow map at a time (the first such light wins); the rest are shaded without shadows.

Declaration Summary
var type: FabricateLightType
var position: Vector3f
var direction: Vector3f
var color: Vector3f
var intensity: Float
var range: Float
var innerConeDegrees: Float
var outerConeDegrees: Float
var castShadow: Boolean

FabricateRegisteredGltf

data class FabricateRegisteredGltf(val nodes: List<FabricateNode>, val opaqueMeshes: List<StandardRenderer.Bundle.PullingMeshPair>, val transparentMeshes: List<StandardRenderer.PullingMesh>, val meshBindings: List<FabricateGltfMeshBinding> = emptyList())

Declaration Summary
val nodes: List<FabricateNode>
val opaqueMeshes: List<StandardRenderer.Bundle.PullingMeshPair>
val transparentMeshes: List<StandardRenderer.PullingMesh>
val meshBindings: List<FabricateGltfMeshBinding>
fun animate(time: Float): FabricateRegisteredGltf
val meshes: List<StandardRenderer.Bundle.PullingMeshPair>

FabricateGltfMeshBinding

data class FabricateGltfMeshBinding(val index: Int, val node: FabricateNode, val opaque: StandardRenderer.Bundle.PullingMeshPair? = null, val transparent: StandardRenderer.PullingMesh? = null)

Declaration Summary
val index: Int
val node: FabricateNode
val opaque: StandardRenderer.Bundle.PullingMeshPair?
val transparent: StandardRenderer.PullingMesh?
fun applyMaterial(material: FabricateMaterial): FabricateGltfMeshBinding

FabricateSceneGraphTestMain.kt

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateSceneGraphTestMain.kt — read the source

FabricateSceneGraphTestMain

object FabricateSceneGraphTestMain

Pure JVM smoke test for hierarchical transform composition.

Declaration Summary
fun main(args: Array<String>)

FabricateSceneTestMain.kt

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateSceneTestMain.kt — read the source

FabricateSceneTestMain

object FabricateSceneTestMain

JVM smoke test for Fabricate's pure scene graph: transform hierarchy, animation playback, and opaque/transparent draw ordering. This intentionally avoids a Vulkan device.

Declaration Summary
fun main(args: Array<String>)

FabricateShaderBoxes.kt

Expose Fabricate's built-in shaders the same way user-authored shaders work: each pass becomes an editable box with vertex/fragment GLSL tabs, live introspection, and alt r hot reload.

Nothing here is new low-level machinery — it reuses Box.attachShader, which installs the reload actions, copies the module's source into the box's editable properties, and (through reloadShader) rebuilds by copying the module's existing Parameters, so the renderer's own textureResolver, attachment formats, and push-constant seeding survive every edit. Only the GLSL source changes.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/FabricateShaderBoxes.kt — read the source

Used in: Fabricate Renderer

Top-level functions

Declaration Summary
fun StandardRenderer.Bundle.shaderModules(): LinkedHashMap<String, ModulePointer> All built-in graphics ModulePointers in this bundle, keyed by a stable pass name. Includes the deferred passes, the post-stack effects (motion blur, FXAA, plus any tone-map/bloom/user effects added so far), and any per-material textured g-buffer variants registered by glTF import. Modules are de-duplicated by identity, so passes that also appear in the post stack are listed once.
fun StandardRenderer.Bundle.shaderSources(): LinkedHashMap<String, Pair<String, String>> CPU-only view of the built-in shader sources, for inspection/diffing without a session or GPU. Returns pass name -> (vertex source, fragment source).
fun StandardRenderer.Bundle.bindShadersToBoxes(namePrefix: String = "fabricate", root: SessionInformation = thisSessionInfo.get(), origin: Rect = Rect(40.0, 40.0, 340.0, 200.0), columns: Int = 3, gap: Double = 24.0): LinkedHashMap<String, Box> Bind every built-in shader in this bundle to its own editable box (see shaderModules). Each box is named "$namePrefix/$passName", gets the pass's GLSL in its vertex/fragment tabs, and can be edited and hot-reloaded with alt r (or ABI-preserving alt shift enter) like any user shader.
fun StandardRenderer.Bundle.bindShaderToBox(name: String, module: ModulePointer, namePrefix: String = "fabricate", root: SessionInformation = thisSessionInfo.get(), frame: Rect = Rect(40.0, 40.0, 340.0, 200.0)): Box Bind a single built-in module to a named box. Useful when you only want to hack on one pass, e.g. bundle.bindShaderToBox("std-lighting", bundle.lightingModule).

GaussianSplatRenderer.kt

Rendering a Gaussian splat capture: PLY reading, packing, depth sorting and the built-in splat shaders.

Splats are not triangles and the differences drive the whole file. Each one is an anisotropic Gaussian of sixteen floats (position, a covariance, colour and opacity) drawn as a screen-aligned quad whose falloff the fragment shader evaluates, and the result is only correct if the quads arrive in depth order, because alpha blending does not commute. So a frame here is a sort followed by a draw, and most of the machinery below exists to make the sort affordable: SortMode.ExactRadix runs a GPU radix sort over per-splat depth keys, DepthBuckets trades exactness for a cheaper bucketed approximation, and None builds none of it, which is the right answer when the data was written back-to-front already or the blend is order-independent.

A Cloud is sized by capacity rather than by the data it was built from, so one cloud can be rewritten every frame from different files. That is what makes a splat sequence playable at all, with the frame cache on the disk side in SplatSequence.kt, and it is why a fifteen-source frame is one upload into one pre-sized buffer instead of fifteen allocations.

configureModule sets the raster state the built-in shaders need and does not set the shader source, because a box using moduleHere owns its vertex and fragment text through its own properties. simpleGaussianSplatSupport.kt uses that to hand a box an editable copy of the shaders.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/GaussianSplatRenderer.kt — read the source

Top-level functions

Declaration Summary
fun gaussianSplats(path: String, renderer: Renderer = fromContext(), target: RenderTarget = renderer.renderList, camera: Camera = Camera(renderer), config: GaussianSplatRenderer.Config = GaussianSplatRenderer.Config(), name: String = File(path).nameWithoutExtension, renderToTexture: RenderToTexture? = null, renderModule: ModulePointer? = null): GaussianSplatRenderer.Cloud
fun RenderTarget.addGaussianSplats(path: String, camera: Camera = Camera(renderer), config: GaussianSplatRenderer.Config = GaussianSplatRenderer.Config(), name: String = File(path).nameWithoutExtension, renderToTexture: RenderToTexture? = null, renderModule: ModulePointer? = null): GaussianSplatRenderer.Cloud

GaussianSplatRenderer

object GaussianSplatRenderer

Declaration Summary
const val SPLAT_FLOAT_STRIDE
const val SPLAT_SOURCE_FLOAT Where the source index lives inside a splat's SPLAT_FLOAT_STRIDE floats: the w of the colour vec4, which the shading has never used. Written by packInto as a float — exact to 2^24, so vastly more than the 256 sources a pack can address — and reachable in GLSL as
data class Config(val scaleMultiplier: Float = 1f, val scaleIsLog: Boolean = true, val fallbackScale: Float = 0.01f, val minScale: Float = 1e-5f, val maxScale: Float = 1_000f, val opacityIsLogit: Boolean = true, val opacityMultiplier: Float = 1f, val covarianceInflation: Float = 1f, val sizeScale: Float = 1f, val alphaScale: Float = 1f, val minScreenRadius: Float = 0.75f, val maxScreenRadius: Float = 128f, val alphaCutoff: Float = 1f / 255f, val sortMode: SortMode = SortMode.DepthBuckets, val depthKey: DepthKey = DepthKey.Bounds, val depthBucketBits: Int = 16, val sortEveryNFrames: Int = 1, val sortOnlyWhenViewChanges: Boolean = true, val sortViewEpsilon: Float = 1e-5f, val stableBucketSort: Boolean = false, val viewport: Rect? = null, val blend: Blend = Blend.Over, val colorGamma: Float = 2.2f)
data class Blend(val srcColor: Int, val dstColor: Int, val srcAlpha: Int = srcColor, val dstAlpha: Int = dstColor, val colorBlendOp: Int = VK_BLEND_OP_ADD, val alphaBlendOp: Int = colorBlendOp, val orderIndependent: Boolean = false) How a splat composites with what is already there. The default Over is the physically motivated one — premultiplied alpha, which is what a depth-sorted Gaussian scene wants — but the rest are here because an unsorted draw under Additive or Max is order-independent, which turns "I have not sorted these" from an artefact into a choice.
data class PackedSplats(val floats: FloatArray, val count: Int, val boundsMin: Vector3f, val boundsMax: Vector3f)
enum class DepthKey How the depth-key pass turns a splat's view depth into the 32-bit key the sort orders by.
enum class SortMode The float bits of the view depth itself, which order the same way as the depth for any non-negative float. Needs no range, so it stays right however far the splats move from where they were uploaded; use it with SortMode.ExactRadix, because the top 16 bits SortMode.DepthBuckets keeps are only 7 bits of mantissa per octave of depth.
class Cloud internal constructor(val renderer: Renderer = fromContext(), val camera: Camera = fromContext(), val table: PlyFloatTable?, val packed: PackedSplats?, val config: Config, val sourceName: String, val capacity: Int = packed?.count ?: 0, val perFrameSplatData: Boolean = false, val renderToTexture: RenderToTexture? = null, suppliedRenderModule: ModulePointer? = null) A splat cloud on the GPU.
fun configureModule(parameters: Parameters, config: Config = Config(), renderToTexture: RenderToTexture? = null, name: String = "gaussian_splats"): Parameters Apply the raster state expected by the built-in splat shaders to either an internal module or a caller's live moduleHere module. Shader source is not assigned here, because moduleHere owns source through the box's vertex and fragment properties.
fun load(path: String, renderer: Renderer = fromContext(), camera: Camera = Camera(renderer), config: Config = Config(), name: String = File(path).nameWithoutExtension, renderToTexture: RenderToTexture? = null, renderModule: ModulePointer? = null): Cloud
fun fromTable(table: PlyFloatTable, renderer: Renderer = fromContext(), camera: Camera = Camera(renderer), config: Config = Config(), name: String = "gaussianSplats", renderToTexture: RenderToTexture? = null, renderModule: ModulePointer? = null): Cloud
fun streaming(capacity: Int, renderer: Renderer = fromContext(), camera: Camera = Camera(renderer), config: Config = Config(), name: String = "splatStream", renderToTexture: RenderToTexture? = null, renderModule: ModulePointer? = null): Cloud An empty cloud sized for capacity splats, to be filled by Cloud.reupload every frame.
fun pack(table: PlyFloatTable, config: Config = Config()): PackedSplats
fun parametric(table: PlyFloatTable, config: Config = Config()): SplatSet table decoded as pack decodes it, but kept in parametric form — scales and a rotation rather than a covariance — as a SplatSet, for a SplatShader insert to edit. The rotation is stored (x, y, z, w); a PLY's rot_0 is its w.
data class SplatSource(val raw: CompressedSplatPly.RawSplats, val transform: Matrix4f? = null, val sourceIndex: Int? = null) One compressed payload plus where it goes in the world.
data class PackedSpan(val source: Int, val offset: Int, val count: Int) Where one source's splats landed in a shared destination. Contiguous only when unsorted.
data class PackedFrame(val count: Int, val boundsMin: Vector3f, val boundsMax: Vector3f, val spans: List<PackedSpan>?) What packInto wrote. spans is null when a sort interleaved the sources.
class PackScratch Reusable per-frame working memory for packInto. A streaming caller should keep one and pass it every frame; the sort keys are 8 bytes per splat, which at 15 files and 24 fps is 72 MB/s of garbage otherwise.
fun packInto(sources: List<SplatSource>, dest: FloatBuffer, config: Config = Config(), sortAlong: Vector3f? = null, scratch: PackScratch? = null, parallelSort: Boolean = false): PackedFrame Decode compressed splat payloads straight into dest in the 16-float GPU layout, with no PlyFloatTable in between.
fun packInto(source: CompressedSplatPly.RawSplats, dest: FloatBuffer, config: Config = Config(), transform: Matrix4f? = null, sortAlong: Vector3f? = null, scratch: PackScratch? = null): PackedFrame Single-source convenience.
fun matrixPaletteBuffer(renderer: Renderer = fromContext(), name: String = "splatMatrixPalette", capacity: Int = MAX_PACK_SOURCES): BufferParameters A storage buffer sized to hold capacity 4x4 matrices, for the per-source palette that SPLAT_SOURCE_FLOAT indexes. Allocate once, rewrite with writeMatrixPalette as often as you like — a palette is 15 matrices where the splat buffer is hundreds of thousands of splats, so this is the cheap end of the frame by four orders of magnitude.
fun writeMatrixPalette(matrices: List<Matrix4f>, into: DynamicBuffer, capacity: Int = matrices.size): Int Write matrices into into as a flat palette, column-major and sixteen floats each, which is the layout a GLSL mat4 palette[] in a buffer_reference block expects.
val depthKeyComputeShader
val splatVertexShader Default single-view shader, retained as a public source for callers and tests.
val splatMultiviewVertexShader Multiview variant: one draw, with camera matrices selected by gl_ViewIndex.
val splatFragmentShader

PathTraceEmitterGpuSmoke.kt

Real GPU checks for PathTracingRenderer.emitterSampling, the path tracer's direct sampling of emissive triangles: four scenes, each measured against a number that exists outside this file.

Unbiasedness is checked before variance. A variance win from a biased estimator reduces noise and changes the image, which is what a wrong area-to-solid-angle Jacobian, a missing cosine and a double-counted emission arm all do as well, so the noise reduction on its own says nothing about which of the two happened.

Which scene a check uses decides what it can resolve. A small bright lamp in the dark is where BSDF sampling barely works, so its own converged image stays noisy, and a reference that noisy cannot resolve a two-percent bias at any tolerance. Three of the four arms below are therefore uniform-radiance scenes, where both sides converge and the comparison is sharp:

  • inertWithoutEmitters — a scene with nothing emissive in it, where on and off must agree bit for bit. Which means the two runs have to stop on the same sample, and a polled sample count cannot be made to land there, so the second run targets whatever the first reached and the pair is retried until they match.
  • enclosureAgainstEnvironment — a sphere inside a uniformly emissive cube, one bounce. Every direction carries radiance one, so this is the same integral as the same sphere under a constant unit environment map, which the tracer solves through a separate, independently checked next-event path. One bounce means neither run applies an MIS weight at all, so what is under test is the new pdf alone: its normalisation and its Jacobian. Both furnace means are also asserted against 1.0, so the arm is absolute and not a self-comparison.
  • enclosureComposition — the same enclosure with six differently coloured faces and two bounces, emitter sampling on against off. Both arms fire and both MIS weights matter, and a selection probability that has come off its triangle tints the answer. Uniform near-field radiance again means the off run converges, so this resolves a fraction of a percent.
  • the lamp box, driven from run — two small lamps in the dark, which is the scene the variance claim is measured in. Its bias residual is reported at two reference budgets: a residual that falls like one over the square root of the budget is the reference's own noise, and one that holds is bias. That decay is asserted, because a fixed tolerance there would be a tolerance on noise.

The lamp box is built around the three failures the uniform scenes cannot show. Its two lamps differ in size, colour and power, so a table whose probability and triangle index have come apart shows as a colour shift rather than as noise. One of them is one-sided and faces down, because a back face culled on a BSDF hit has to be dark to the direct sampler too. And a sphere sits on the floor, where its shadow is a shadow ray: the integrator's occlusion query stops a thousandth short of the sampled point, so that a lamp cannot occlude its own connection, and a query that ran the whole way would discard every direct contribution and leave a correct image with no variance improvement in it. The RMSE ratio asserted here is what would notice.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceEmitterGpuSmoke.kt — read the source

PathTraceEmitterGpuSmoke

object PathTraceEmitterGpuSmoke

Four arms measured at 96x72 on each enabled backend, then three figures at 480x360. Each arm prints a PASS line naming the population it measured. Run through RayTracingGpuAudit's --stage=emitters or dev_docs/agents/demos/path_tracing_emitters/verify.yaml in a live box.

Declaration Summary
suspend fun run(renderer: Renderer, outputDirectory: String) Drives every check and writes the before/after captures under outputDirectory. Call from a suspend body in a box below a live renderer, as with the other path-trace smokes.

PathTraceFrameOutput.kt

The path tracer's per-frame image set, for reconstruction rather than for display.

PathTracingRenderer normally accumulates samples into one progressively cleaner picture. A denoiser wants the opposite: this frame's noisy radiance on its own, together with the buffers that say what each pixel is looking at. enableFrameOutput on the tracer builds a PathTraceFrameOutput, which converts the packed per-pixel records the tracer writes into seven attachments: radiance, depth, motion, diffuse and specular albedo, normal and roughness, and specular hit distance. inputs and guides hand those over in the shape ReconstructionInputs and RayReconstructionGuides expect.

The images stay on the device and nothing is read back, so a consumer is a Target recorded after the tracer's own render. Opaque materials only, which is why the tracer refuses the call on a scene with glass in it.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceFrameOutput.kt — read the source

PathTraceFrameOutput

class PathTraceFrameOutput internal constructor(val tracer: PathTracingRenderer) : Target, AutoCloseable

GPU-resident output of a single path-tracing dispatch, before progressive accumulation. Six vec4s per pixel (96 bytes). No readback or native library is needed to produce these images. The tracer owns this object; consumers borrow the images until the tracer closes. Jitter is projection displacement, positive right/down: primary rays sample pixel-jitter. Depth is RH perspective Vulkan [0,1], clear=1, using near/far. Motion is previous-current in input pixels without jitter; geometry is the tracer's immutable scene snapshot.

Declaration Summary
val near
val far
var jitter
val images
fun inputs()
fun guides()
override fun render(command: CommandBuffer,index: Int) Record after tracer.render, before any reconstruction consumer.
override fun close()

PathTraceGlassDemo.kt

All four of the path tracer's glass materials in one scene: clear, absorbing, frosted, and a thin pane.

public/docs/path_tracing.md prints this scene as its glass figure and dev_docs/agents/transcripts/path_tracing_glass.yaml launches it, so what the file builds is what those two are written about. Left to right: a sphere of PathTraceMaterial.glass(), one given an attenuationColor, one given a roughness of 0.32, and a quad of thinGlass.

Most of the geometry here is background, since what a transparent material shows is mostly whatever is behind it. The floor is a checkerboard of 0.55-unit tiles and the wall behind the spheres is eighteen horizontal stripes, both built from the same two diffuse greys, one ten times the reflectance of the other. Each material moves that pattern its own way: refraction displaces the stripes, absorption tints them by how far the ray travelled inside the solid, roughness scatters them, and the thin pane leaves their positions where they are, because thinGlass transmits per crossing and has no thickness to refract across.

The returned renderer is set to ten bounces rather than the default five, because a ray that enters and leaves a sphere has spent two of them before reaching anything else, and to an exposure of 1.5 for a floor this dark. With no environmentPath the light is a constant white environment of radiance one; a path is read by PathTraceEnvironment.fromFile, which takes EXR, Radiance and ordinary image files.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceGlassDemo.kt — read the source

PathTraceGlassDemo

object PathTraceGlassDemo

Clear glass, absorbing green glass, frosted glass and a thin blue pane, from left to right.

Declaration Summary
fun install(renderer: Renderer, environmentPath: String? = null, width: Int = 640, height: Int = 360): PathTracingRenderer Builds the scene, points a camera at it and installs a PathTracingRenderer under renderer, returning it so a box can blitToScreen or reconfigure it.

PathTraceGltf.kt

glTF and GLB into a PathTraceScene: hierarchy baked to world space, PBR parameters mapped onto PathTraceMaterial, and an extension check that refuses rather than renders the wrong thing.

The tracer has no scene graph and holds world-space triangles, so this importer flattens the glTF node hierarchy as it reads, composing each node's transform with the caller's. Textures are copied out and every Assimp resource is released before the call returns, which is why no renderer or device is involved and an import is testable from a plain main.

Two behaviours are easier to find here than to rediscover from a wrong picture. The first is that the extension set is read from the JSON directly, ahead of Assimp: an extension in extensionsRequired that this importer does not implement fails the call, while one in extensionsUsed produces a warning and an import of the base material. Declining to load is the right answer for the former because a required extension means the asset is wrong without it. The second is that the import is all-or-nothing: geometry accumulates into a staging scene and is appended only on success, so a failure leaves the target scene exactly as it was rather than half-populated.

Colour space is the detail that most often looks like a renderer bug. Base colour and emission textures are sRGB and are decoded; metallic-roughness and normal maps are linear data and are not. Vertex colours and the base colour factor multiply after decoding, alpha is never squared, and PathTraceGltfGpuSmoke pins all of it against known byte values through real GPU traversal.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceGltf.kt — read the source

Top-level functions

Declaration Summary
fun PathTraceScene.importGltf(path: String, transform: Matrix4f = Matrix4f(), material: (name: String, value: PathTraceMaterial) -> PathTraceMaterial = { _, value -> value }, warning: (String) -> Unit = { println("glTF: $it") }, pose: GltfSkinPose? = null): PathTraceGltfImport Append a static glTF/GLB scene, baking hierarchy and transform into world-space geometry. Copies textures and releases Assimp resources before returning; no renderer is required. material can replace or amend imported PBR parameters, for example to choose solid glass. An import failure leaves this scene unchanged.

GltfSkinPose

data class GltfSkinPose(val clip: String? = null, val seconds: Float = 0f)

Which pose a skinned mesh is traced in.

PathTraceGltfImport

data class PathTraceGltfImport(val triangleCount: Int, val materialIds: Map<String, Int>, val boundsMin: Vector3f, val boundsMax: Vector3f, val warnings: List<String>)

Details of one static import. Material IDs also select materials in shadeMaterial().

Declaration Summary
val boundsMin: Vector3f, val boundsMax: Vector3f, val warnings: List<String>)

PathTraceGltfGpuSmoke.kt

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceGltfGpuSmoke.kt — read the source

PathTraceGltfGpuSmoke

object PathTraceGltfGpuSmoke

Known texture values checked through actual GPU traversal, material evaluation and accumulation.

Declaration Summary
suspend fun run(renderer: Renderer, backend: PathTracingRenderer.Backend)

PathTraceGltfTestMain.kt

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceGltfTestMain.kt — read the source

PathTraceGltfTestMain

object PathTraceGltfTestMain

Declaration Summary
fun main(args: Array<String>)

PathTraceGpuSmoke.kt

GPU smoke tests and a demo entry point for PathTracingRenderer.

PathTraceGpuSmoke.run sets up known scenes (a constant HDR environment, emissive quads, a white-furnace diffuse surface, and glass slabs) and reads back linear pixels from real GPU dispatches to assert accumulation behaviour, lighting and camera resets, HDR material correctness, Beer-Lambert attenuation, Fresnel, indirect clamping, and transparent-background compositing. Where the device supports ray queries, both the compute-BVH and ray-query backends run, and their pixel outputs are compared.

PathTraceGpuSmoke.runCamera covers the thin-lens model: pinhole sharpness, aperture blur, focus-distance changes, pixel jitter, and accumulation invalidation on target or parameter change.

PathTraceDemo.install builds a three-sphere scene (diffuse, metallic, rough metal over a floor) and returns a live PathTracingRenderer for interactive use from a box.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceGpuSmoke.kt — read the source

Used in: Progressive path tracing

PathTraceGpuSmoke

object PathTraceGpuSmoke

Real GPU readback assertions. Run from a Field box below a live renderer; no placeholder captures.

Declaration Summary
suspend fun runSeams(renderer: Renderer) A closed quad must have no pinholes along either diagonal, from either side. The camera-centred rays land exactly on shared edges where u/v rounding used to miss both triangles on NVIDIA. Black misses make this visible without temporal reuse.
suspend fun run(renderer: Renderer)
suspend fun runCamera(renderer: Renderer) Thin-lens focus, coverage, jitter, and automatic reset checks on real GPU pixels.
suspend fun awaitSamples(tracer: PathTracingRenderer, count: Int)

PathTraceDemo

object PathTraceDemo

Small reusable material study; the environment file is optional, with a constant white map as fallback.

Declaration Summary
fun install(renderer: Renderer, environmentPath: String? = null, width: Int = 640, height: Int = 360): PathTracingRenderer
fun install(renderer: Renderer, environmentPath: String? = null, width: Int = 640, height: Int = 360, backend: PathTracingRenderer.Backend): PathTracingRenderer

PathTraceInstrument.kt

The path tracer's instrument: what it is doing, why it restarted, and how fast it is converging.

Answering any of that from a box by hand means a frames { repeat(180) { frame() } } loop with a println inside: it costs frames of its own, it misses a reset that lands between frame boundaries, and it cannot see the camera basis the tracer actually built. What is here instead is a .h printer, a .h2 drill-down and PathTraceConvergence, which the tracer appends to from its own render. Recompiling the integrator in place, the other thing such a loop gets written for, is PathTracingRenderer.reloadIntegrator.

The two renderings

registerPathTraceInstrument gives PathTracingRenderer both an afield.ui.visualization.registerValuePrinter and an afield.ui.visualization.registerH2Value entry, because the class is not a data class and reflection alone serves the two panels unevenly. H2Reflect reflects any type, so .h2 reaches every public tuning property without help; .h with no printer falls through to ValueVisualizer's formatFallbackObject, which prints the class name and an identity hash and nothing else — while the Field 4 types the tracer is built out of (Renderer, Camera, ComputeModulePointer, RenderToTexture, VulkanBuffer, …) all have printers in FourVisualizers.kt.

So the two renderings do different jobs, and the split is why the .h panel stays short:

  • .h is the glance. One header line and a handful of rows, with the rows that are at their defaults dropped — so a plain tracer shows five rows, and each of the five optional groups (a non-agreeing up, a lens, temporal reuse, fog, an attached convergence log) costs one more, to a maximum of ten. It is meant to fit in the gap above the next box.
  • .h2 is the drill-down. A strict superset of what reflection reaches on its own: the same values, grouped rather than flat, with the derived facts beside them and the sub-objects still reachable so each one lands on its own printer.

The up vector the tracer actually uses

The tracer does not use camera.up. updateFrame builds its own up as right × forward, i.e. the part of the declared up that is perpendicular to the view direction, normalised, and that is what reaches the Frame uniform. A declared up of (0, 3, 0), or one tilted 20° out of the view plane, gives a different vector from the one the image is built with, and no property tree can show it, because the value is a local inside a private method. PathTraceCameraBasis computes both and reports the angle between them, and the .h row appears only when they disagree.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceInstrument.kt — read the source

Used in: Progressive path tracing

Top-level functions

Declaration Summary
fun PathTracingRenderer.status(): PathTraceStatus Read this tracer's PathTraceStatus now.
fun PathTracingRenderer.recordConvergence(maxPoints: Int = 4096): PathTraceConvergence Start logging convergence on this tracer, replacing any existing log, and hand it back.
fun Box.showConvergence(log: PathTraceConvergence, title: String? = "path tracer convergence", channel: String = "output", marks: Int = 12): Plot? Plot a PathTraceConvergence — samples against wall seconds, with a labelled dashed rule wherever accumulation restarted and why.
fun registerPathTraceInstrument() Register both renderings. Discovered by di.kt's ClassGraph scan of @RunsAt, like registerFourVisualizers and registerH2DomainAdapters.

PathTraceCameraBasis

data class PathTraceCameraBasis(val at: Vector3f, val target: Vector3f, val declaredUp: Vector3f, val effectiveUp: Vector3f, val right: Vector3f, val forward: Vector3f, val fovRadians: Double, val distanceToTarget: Float)

The camera as the tracer resolves it, declared and effective side by side.

Declaration Summary
val at: Vector3f
val target: Vector3f
val declaredUp: Vector3f
val effectiveUp: Vector3f
val right: Vector3f
val forward: Vector3f
val fovRadians: Double
val distanceToTarget: Float
val upTiltDegrees: Double Angle between declaredUp and effectiveUp, in degrees, or NaN when either is degenerate.
val upAgrees: Boolean Whether the declared up is the one the image is built with, to within a twentieth of a degree.

PathTraceConvergenceSummary

data class PathTraceConvergenceSummary(val points: Int, val seconds: Double, val samples: Int, val resets: Int)

Whatever a PathTraceConvergence has recorded, reduced to the five numbers worth a row.

Declaration Summary
val points: Int
val seconds: Double
val samples: Int
val resets: Int
val samplesPerSecond: Double Fresh samples per wall second over the recorded window, or NaN when nothing has elapsed.

PathTraceStatus

data class PathTraceStatus(val name: String, val backend: PathTracingRenderer.Backend, val width: Int, val height: Int, val triangleCount: Int, val samples: Int, val samplesPerFrame: Int, val paused: Boolean, val resetGeneration: Long, val lastResetReason: String, val camera: PathTraceCameraBasis, val tuning: Map<String, Any?>, val recorded: PathTraceConvergenceSummary? = null)

An immutable snapshot of everything the instrument shows, read once off a live tracer.

Declaration Summary
val name: String
val backend: PathTracingRenderer.Backend
val width: Int
val height: Int
val triangleCount: Int
val samples: Int
val samplesPerFrame: Int
val paused: Boolean
val resetGeneration: Long
val lastResetReason: String
val camera: PathTraceCameraBasis
val tuning: Map<String, Any?>
val recorded: PathTraceConvergenceSummary?

PathTraceConvergence

class PathTraceConvergence(val maxPoints: Int = 4096)

A per-frame convergence log: samples against wall time, plus every reset and why it happened.

Declaration Summary
val seconds: List<Double> Wall seconds since the first recorded frame, one per retained point.
val samples: List<Int> Fresh samples per pixel, one per retained point, aligned with seconds.
val points: Int How many points are retained. Less than the number of frames once decimate has run.
val framesPerPoint: Int The sampling stride: one point per this many frames. Doubles each time the log fills.
fun resetMarks(limit: Int = 12): List<Pair<String, Double>> Reset marks as "reason" to seconds, the shape showSignals wants for marks.
val resets: List<Pair<String, Double>> Every reset kept, newest last, as "reason" to seconds. resetMarks is the plottable form.
val summary: PathTraceConvergenceSummary? Five numbers for the instrument, or null before the second point (one point spans no time).
fun clear() Forget everything and start again, keeping the attachment.
override fun toString(): String

IntegratorReload

class IntegratorReload internal constructor(val status: Status, val sourceLength: Int, val errors: List<String>)

What PathTracingRenderer.reloadIntegrator did: three outcomes, not a boolean.

Declaration Summary
val status: Status
val sourceLength: Int Characters of source offered. Not the source itself: an integrator is ~15 KB and this ends up in logs.
val errors: List<String>
enum class Status
val reloaded: Boolean True only for Status.RELOADED — i.e. the image is about to change.
val ok: Boolean True unless it failed to compile, so check(result.ok) does not fire on a no-op.
override fun toString(): String

PathTraceMediumGpuSmoke.kt

Real GPU checks for the path tracer's participating medium: PathTracingRenderer.mediumDensity and everything that is sampled along a ray rather than at a surface.

A medium adds three probability densities to an integrator: a distance density, a phase function, and the transmittance that multiplies a light connection. Each has a failure that darkens or brightens the image without otherwise distorting it, so a render cannot be told from a correct one by looking at it — a distance pdf missing its extinction factor, a phase function normalised to 1 instead of 1/4π, a transmittance applied twice, once analytically on the connection and once through the collision event that already estimated it. So each arm is checked against a number that exists independently of this code:

  • inertWithoutExtinction — extinction zero, with albedo, anisotropy and bounds all set to something conspicuous, must reach the previous integrator bit for bit. The distance sample has no Sobol dimension of its own, it shares the BSDF lobe selector's and the escape branch remaps it back to uniform, so "the medium is off" and "the sample stream is untouched" are two different claims and this is the second one.
  • absorptionAgainstClosedForm — the absolute anchor. A purely absorbing medium (albedo zero) in front of an emissive plane must attenuate by exactly exp(-σt·d), per pixel, with d the camera-to-plane distance. That is a closed form, not a converged reference, so no common-mode error can hide in it. Run at two densities an order of magnitude apart in transmittance, and in a bounded configuration too, where the exponent is the slab-crossing length and what is under test is the shader's ray-to-bounds clip rather than the exponential.
  • absorptionAgainstDepthFog — the same scene and the same closed form, reached through fogDensity, which computes its own exp(-σ·primaryDepth) and shares nothing with the medium but the scene. It is also the arm that tests the closed form itself: the analytic d is a Kotlin reconstruction of the ray the shader builds, and a wrong reconstruction fails here instead of agreeing with itself.
  • scatteringFurnace — energy conservation, asserted at 1.0. An albedo-one medium embedded in a uniform unit radiance field is in exact equilibrium, since L = 1 everywhere solves the transport equation. The scene has no geometry in it, so no BSDF, no area pdf and no surface reflectance can contribute to the answer or account for it. Repeated across six anisotropies, which extends it from a normalisation check to a check that PathTracingRenderer.mediumAnisotropy's density and its sampling routine describe the same distribution: were they to disagree, the phase arm's implicit weight of one would be wrong and the furnace would not close.
  • furnaceBounceSweep — the same furnace at four times the density, where the truncation deficit is large enough to characterise. The mean rises monotonically with the bounce budget and never passes 1.0, which is the signature of truncation; a normalisation error would sit at a fixed offset and a double-counted arm would overshoot.
  • singleScatteringAgainstQuadrature — the arm that shares no code and no scene with anything else. A slab of medium under a constant environment, one bounce, is a one-dimensional integral of the radiative transfer equation that a Kotlin quadrature can evaluate to eight figures. It pins the distance pdf, the albedo weight, the phase normalisation and the transmittance on the environment connection simultaneously and absolutely, against numerics written from the equation rather than from the shader.
  • emitterConnectionIsInvisible — the cross-mechanism arm for the one mechanism the furnace and the quadrature cannot reach: the connection from a scattering point to emissive geometry, with its area-to-solid-angle Jacobian and its own transmittance. A uniformly emissive enclosure is a radiance field in which an albedo-one medium is invisible, so the reference is the same scene rendered with the medium off, and the medium's emitter pdf appears in one of the two arms only. That premise is exact only for a non-reflecting emitter, and every dielectric this BSDF can express returns a few percent of what it is lit by, so the arm runs three configurations — a black metal, a dielectric, and the dielectric pair with the connection under test switched off in both — and ends in a bound on that connection's error rather than in an identity.
  • lampInFog — two small lamps in fog, which is the scene the variance claim is measured in. Its bias residual is reported at two reference budgets, because a fixed tolerance on a residual is a tolerance on noise; and its variance ratio is asserted at a floor, not printed.

Every arm then runs again on each traversal backend the device exposes. The medium touches no traversal code and the analytic anchors are backend-independent, so a backend that misses them when the other does not has put the disagreement in traversal rather than in the estimator, which is a distinction no single pass can draw. Two further arms render the figures, and the numbers are written to a report.txt per backend after every arm as well as printed, because a box's println reaches the server log through a buffer a backdoor teardown does not wait for.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceMediumGpuSmoke.kt — read the source

PathTraceMediumGpuSmoke

object PathTraceMediumGpuSmoke

Eight arms measured at 96x72 and two that render figures, all repeated on each backend. A live renderer is required throughout, so this carries no FIELD_TEST annotation and is driven from a box instead: dev_docs/agents/demos/path_tracing_medium/verify.yaml is the transcript that does it.

Declaration Summary
fun godRays(): PathTraceScene The demo scene, kept here so its picture is rendered by the same code that measures everything else: a bright strip light with four slats under it, the whole room hazed, viewed down the axis everything is extruded along so the shafts are seen in cross-section.
fun godRaysCamera(renderer: Renderer) Level, and square on to the x-y plane the shafts fan out in, so the slats are seen nearly edge-on and the shafts across rather than down their length.
val GOD_RAYS_BOUNDS Bounds for godRays, in the form mediumBounds takes: the floor's full width, the z extent inset a tenth of a unit from the ends the room is extruded to, and a ceiling at 5.2, above the strip light and below the top of the back wall.
const val GOD_RAYS_DENSITY The demo's extinction, and a figure that wants a medium you can see through rather than the densest one the integrator handles. At 0.30 the haze is about two and a third optical depths along the view and obscures itself, so the wall, the floor and the slats' shadow wedges wash into one grey.
suspend fun run(renderer: Renderer, outputDirectory: String) Every arm, on every backend this device exposes.

PathTraceScene.kt

What the path tracer traces: triangles, materials, textures and an environment map, all as plain Kotlin with no device attached.

Nothing in this file needs a GPU, which is why it is separate from PathTracingRenderer.kt. A scene is assembled by calling triangle, quad, mesh or sphere, each of which appends to flat lists and returns the scene so calls chain; material and texture intern a value and hand back the integer index the triangles refer to it by. pack then flattens the result into the float and int arrays the shader expects, and building a BVH over them is the renderer's job, not this file's. The consequence for a test is that scene construction, indexing and packing are all checkable headlessly, which is what PathTraceTestMain does.

PathTraceMaterial is linear-light throughout, with no sRGB anywhere in the struct, and carries the transmission parameters as well as the metallic-roughness ones, so glass is a material rather than a separate path. It carries one geometric precondition, and breaking it shows up as a dark result: solid (non-thin-walled) glass assumes closed, outward-wound, non-overlapping meshes, because the attenuation distance is measured between surface crossings.

PathTraceEnvironment is both the sky and, usually, the main light. It builds the importance distribution the sampler draws from, which is why an environment is a snapshot like the geometry is: changing it changes a precomputed table. PathTraceTexture decodes through stb_image and TinyEXR and keeps its pixels linear, sRGB-decoding base colour while leaving metallic-roughness alone. That asymmetry is what glTF requires, and PathTraceGltfGpuSmoke checks it against known byte values.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceScene.kt — read the source

Used in: Progressive path tracing, A polytope, cut open, and traced

PathTraceMaterial

data class PathTraceMaterial(val red: Float = 0.8f, val green: Float = 0.8f, val blue: Float = 0.8f, val metallic: Float = 0f, val roughness: Float = 0.5f, val emissionRed: Float = 0f, val emissionGreen: Float = 0f, val emissionBlue: Float = 0f, val transmission: Boolean = false, val ior: Float = 1.5f, val thinWalled: Boolean = false, val attenuationRed: Float = 1f, val attenuationGreen: Float = 1f, val attenuationBlue: Float = 1f, val attenuationDistance: Float = 1f, val baseColorTexture: Int = -1, val metallicRoughnessTexture: Int = -1, val emissionTexture: Int = -1, val normalTexture: Int = -1, val opacity: Float = 1f, val alphaCutoff: Float = 0.5f, val alphaMode: AlphaMode = AlphaMode.OPAQUE, val doubleSided: Boolean = true, val normalScale: Float = 1f)

Linear-light surface materials. Solid glass requires closed, outward-wound, non-overlapping meshes.

Declaration Summary
val red: Float
val metallic: Float
val emissionRed: Float
val transmission: Boolean
val attenuationRed: Float
val attenuationDistance: Float
val baseColorTexture: Int
val emissionTexture: Int
val opacity: Float
val alphaMode: AlphaMode
val normalScale: Float
enum class AlphaMode

PathTraceScene

class PathTraceScene

Build a static world-space triangle scene, then pass it to PathTracingRenderer.

Declaration Summary
fun texture(image: PathTraceTexture): Int Add a linear RGBA image; returned index can be used in a material's texture fields.
fun material(value: PathTraceMaterial): Int
var degenerateCount: Int Triangles triangle skipped for zero area or a non-finite coordinate, over this scene's life.
fun triangle(a: Vector3f, b: Vector3f, c: Vector3f, material: Int, na: Vector3f? = null, nb: Vector3f? = null, nc: Vector3f? = null): PathTraceScene Append one triangle. A zero-area triangle, or one with a non-finite coordinate, is skipped and counted in degenerateCount rather than aborting the scene — Manifold's Booleans leave a few such slivers. A missing or zero-length vertex normal falls back to the face normal.
fun quad(a: Vector3f, b: Vector3f, c: Vector3f, d: Vector3f, material: Int): PathTraceScene
fun mesh(positions: List<Vector3f>, indices: IntArray, material: Int, normals: List<Vector3f>? = null): PathTraceScene Indexed positions, with optional per-vertex normals; transforms should be applied before upload.
fun sphere(center: Vector3f, radius: Float, material: Int, segments: Int = 32, rings: Int = 16): PathTraceScene

PathTraceEnvironment

class PathTraceEnvironment(val width: Int, val height: Int, rgba: FloatArray)

Linear RGBA equirectangular image, top row = north pole; longitude atan2(z,x).

PathTraceTexture

class PathTraceTexture(val width: Int, val height: Int, rgba: FloatArray, val wrapU: Int = 0, val wrapV: Int = 0, val nearest: Boolean = false)

CPU-owned linear RGBA texture. Wrap modes: 0 repeat, 1 clamp, 2 mirrored repeat.

Declaration Summary
val wrapU: Int

PathTraceShaderBoxes.kt

pathTracingHere: a path tracer whose material function is the box's own editable compute shader, authored and hot-reloaded exactly like any other Field shader.

The integrator, trace.comp, calls one GLSL function at every surface hit, shadeMaterial(Material, PathTraceSurface), and the packaged copy of it returns its argument unchanged. This file puts an editable copy of that function in the box, so a material is written where shader code is normally written and reloads through the ordinary Reload Compute Shader action. Replacing the whole integrator instead is the other direction, and that is PathTracingRenderer.reloadIntegrator.

Two comment markers in the packaged source are what make that possible. pathTraceTemplates cuts it at // MATERIAL_BEGIN and // MATERIAL_END and puts the halves into the box's shaderTemplate map as pathTracing.preamble and pathTracing.integrator, so the shader the macro seeds is those two template slots with an identity shadeMaterial between them to edit. Everything such a function needs is already in the preamble: the Material and PathTraceSurface structs, the frame uniform, and the buffer references the scene is read through.

The material shader is compiled twice, once at expansion and once on the first frame, and which source each of those sees is the thing to understand before reading _pathTracingHere. A macro compiles at expansion time, because the reflected interface is what the generated Kotlin accessors are built from, and at that moment no renderer has been resolved and no device asked whether it supports ray queries. So the templates are seeded from the portable integrator, whose reflection differs from the ray-query one only by an optional acceleration-structure binding; then _pathTracingHere re-seeds them for the device it actually found and edits the HARDWARE_QUERY define in the already-seeded _compute source, rather than recompiling through the macro. Every later reload goes through the normal Field path and gets the device-specific template.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceShaderBoxes.kt — read the source

Top-level functions

Declaration Summary
fun Box.pathTracingHere(scene: PathTraceScene, environment: PathTraceEnvironment, width: Int = 640, height: Int = 360, camera: Camera? = null, name: String = "path-tracer", target: RenderToTexture? = null, backend: PathTracingRenderer.Backend = PathTracingRenderer.Backend.AUTO, configure: Parameters.() -> Unit = {}): PathTracingRenderer A named tracer whose material function lives in this box's normal compute shader property.
fun Box._pathTracingHere(scene: PathTraceScene, environment: PathTraceEnvironment, width: Int = 640, height: Int = 360, camera: Camera? = null, name: String = "path-tracer", target: RenderToTexture? = null, backend: PathTracingRenderer.Backend = PathTracingRenderer.Backend.AUTO, configure: Parameters.() -> Unit = {}): PathTracingRenderer What pathTracingHere rewrites to, and where the device is finally consulted: the Renderer comes from the ambient context, hardware traversal is taken whenever the device advertises ray queries and backend has not pinned the software BVH, and the tracer is built with this box's compute module as its material module.
fun registerPathTraceShaderBoxes() Registers the pathTracingHere rewrite, once per new root. The default shader it plants is the preamble slot, a shadeMaterial that returns its material unchanged, and the integrator slot, with a comment saying where push constants and includes go.

PathTraceTemporalGpuSmoke.kt

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceTemporalGpuSmoke.kt — read the source

PathTraceTemporalGpuSmoke

object PathTraceTemporalGpuSmoke

Real GPU history/visibility checks, including a measured moving-camera noise comparison.

Declaration Summary
suspend fun runFog(renderer: Renderer)
suspend fun run(renderer: Renderer, verifyKeyboard: Boolean = true)

PathTraceTestMain.kt

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceTestMain.kt — read the source

PathTraceTestMain

object PathTraceTestMain

Pure scene/importance-distribution contracts plus compilation/reflection of BOTH GPU variants.

Declaration Summary
fun main(args: Array<String>)

PathTraceTriMesh.kt

TriMesh into PathTraceScene, so that anything in Field which produces a mesh can be traced.

PathTraceScene.mesh already takes indexed positions, so the arithmetic here is small. The difference is what it does with the triangles a real producer emits and PathTraceScene.triangle rejects: zero-area slivers where two vertices coincide, and vertex normals that came out of a degenerate face as zero-length. A Conway operator chain, a marching-cubes surface, a Manifold Boolean and a cloth simulation all produce some, and one of them aborts the whole append. Here they are counted and skipped, and the count comes back in PathTraceTriMeshImport.degenerateCount.

The producers whose output lands here:

source route
a polytope, including Conway chains afield.geometry.polytope.triangulate .mesh
a Manifold solid, after Booleans afield.geometry.solid.manifold.Solid.toTriMesh
a scalar field afield.geometry.mesh.marchingCubes, or afield.geometry.solid.manifold.Solid.levelSet
a mesh off disk afield.geometry.mesh.readObj and the rest of mesh_io.kt
a heightfield, a tube, a revolve mesh_generate.kt

Normals decide whether it reads as faceted or smooth

TriMesh.normals is optional, and when it is absent every triangle gets its own face normal, so the surface reads faceted. That is the right look for a polyhedron and the wrong one for a sphere. Both Manifold and Field's polytope triangulator can supply either: toTriMesh(normals = true) and triangulate(normals = NormalPolicy.Crease(30.0)) split a vertex per face at a hard edge and average across a soft one.

Splitting vertices for shading also breaks index-sharing, so the mesh that shades well is not the one to measure topology on. Take two: the split mesh for the scene, the plain one for afield.geometry.mesh.genus and afield.geometry.mesh.isWatertight.

A material per triangle

The overload taking a (triangle: Int) -> Int selector is handed each triangle's index in mesh. That numbering does not close up over a skipped triangle, so it stays lined up with the per-triangle side tables a producer hands out: afield.geometry.polytope.TriangulatedGeometry.sourceFace names the polygon a triangle came from, and its triangleAttributes carry whatever the triangulator was asked to record. So a Conway chain's lineage can pick the material, and a face's provenance becomes something you can see in the render.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTraceTriMesh.kt — read the source

Top-level functions

Declaration Summary
fun PathTraceScene.importTriMesh(mesh: TriMesh, material: Int, transform: Matrix4f = Matrix4f(), vertexColors: Boolean = false, warning: (String) -> Unit = { println("TriMesh: $it") }): PathTraceTriMeshImport Append mesh to this scene under material, baking transform into world-space coordinates.
fun PathTraceScene.importTriMesh(mesh: TriMesh, material: (triangle: Int) -> Int, transform: Matrix4f = Matrix4f(), vertexColors: Boolean = false, warning: (String) -> Unit = { println("TriMesh: $it") }): PathTraceTriMeshImport importTriMesh with the material chosen per triangle.

PathTraceTriMeshImport

data class PathTraceTriMeshImport(val triangleCount: Int, val degenerateCount: Int, val boundsMin: Vector3f, val boundsMax: Vector3f, val warnings: List<String>)

What one importTriMesh call did: the triangles that reached the scene, and the ones that did not.

Declaration Summary
val triangleCount: Int Triangles appended to the scene.
val degenerateCount: Int Triangles skipped for zero area or a non-finite coordinate.
val boundsMin: Vector3f Corner of the appended geometry's world-space bounding box. Both corners are zero when nothing was appended.
val boundsMax: Vector3f The opposite corner.
val warnings: List<String> One line per distinct reason something was skipped or substituted.
val size: Vector3f Extent of the appended geometry, which is what a camera distance is usually derived from.

PathTracingRenderer.kt

The path tracer itself: BVH upload, the two traversal backends, accumulation, and the history reprojection that keeps an image usable while a camera moves.

This is the device-facing half of the path tracing stack, and the only file in it that owns Vulkan resources. The scene it traces is built elsewhere, by PathTraceScene.kt, which holds the geometry, materials and environment as plain arrays, and PathTraceGltf.kt, which fills them from a glTF file. It arrives here as an immutable snapshot that is packed into device buffers once. That split is why editing a material means constructing a new tracer: there is no path from a changed Kotlin value back into the buffers the shader reads.

Two traversal backends compile from the same source. COMPUTE_BVH walks a stackless software BVH and runs on any Vulkan device Field supports, MoltenVK included; RAY_QUERY uses VK_KHR_ray_query hardware traversal where the device advertises it. They share the lighting, material evaluation and accumulation code, so an image should not depend on which one ran, and PathTraceGpuSmoke checks that it does not.

The output is a fixed-resolution linear HDR render target rather than something presentable. Exposure and Reinhard tone mapping happen in a separate blitToScreen pass, which keeps the accumulated radiance readable: readLinear is what the smoke tests measure, and a tone-mapped buffer would have nothing useful in it.

For the same material with pictures, see public/docs/path_tracing.md.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/PathTracingRenderer.kt — read the source

PathTracingRenderer

class PathTracingRenderer(val renderer: Renderer, scene: PathTraceScene, environment: PathTraceEnvironment, val width: Int = 640, val height: Int = 360, var camera: Camera = Camera(renderer), requestedBackend: Backend = Backend.AUTO, val name: String = "path-tracer", target: RenderToTexture? = null, moduleFactory: (Parameters) -> ComputeModulePointer = { ComputeModule(it).pointer() }) : AutoCloseable, Target

Progressive GPU path tracer: stackless software BVH on every Field Vulkan device, optional KHR ray queries. Construct/install/close on Field's render thread, like other Field 4 modules. Scene and environment are snapshots; recreate the tracer to edit them. Camera motion resets or optionally reprojects history; lighting edits always reset accumulation. Output is a fixed-resolution linear HDR RTT. Call blitToScreen separately for exposure + Reinhard presentation. Installing under the same name retires the previous tracer; no coroutine lifetime bookkeeping is needed.

Declaration Summary
val renderer: Renderer
val width: Int
val height: Int
var camera: Camera
val name: String
enum class Backend
val backend: Backend
var samplesPerFrame: Int
var pixelJitter: Float Subpixel sample width in pixels: one covers the pixel, zero always samples its centre.
var apertureRadius: Float Circular thin-lens radius in world units. Zero gives a pinhole camera with everything in focus.
var focusDistance: Float? Focus-plane distance along camera.forward in world units; null follows camera.target.
var temporalReuse: Boolean Reproject compatible surface history on camera motion. Approximate, bounded, and off by default.
var temporalMaxSamples: Int Maximum old samples carried across a camera move; glossy surfaces or a changing view may retain fewer.
var temporalMotionSensitivity: Float Camera-angle penalty: one uses conservative view rejection; zero ignores view-angle changes.
var temporalRoughnessSensitivity: Float Roughness penalty: one rejects polished surfaces and reduces glossy history; zero removes both restrictions.
var temporalMaxBlurRadius: Float Largest estimated defocus radius in pixels allowed to retain surface history during motion.
var fogDensity: Float Exponential depth-fog density per world unit. Zero disables fog.
var fogStartDistance: Float Clear distance from the camera, in world units, before depth fog starts.
var fogColor: Vector3f Linear HDR fog colour; returned by copy. Depth fog preserves image alpha.
var fogBackground: Boolean Treat background misses as infinitely distant when fog is enabled; transparent backgrounds stay transparent.
var mediumDensity: Float Extinction of the participating medium, per world unit. Zero disables it, and is the default.
var mediumAlbedo: Vector3f Single-scattering albedo: the fraction of mediumDensity that scatters rather than absorbs, per channel. Returned by copy.
var mediumAnisotropy: Float Henyey–Greenstein anisotropy of the phase function, in (-1, 1). Zero scatters isotropically; positive is forward scattering, which is what turns a light source in haze into a halo rather than an even glow; negative is backscattering, which is what a headlight in fog does to the driver behind it.
var mediumBounds: Pair<Vector3f, Vector3f>? The axis-aligned box the medium occupies, as low to high, or null to fill all space. Returned by copy.
var maxBounces: Int
var environmentIntensity: Float
var environmentRotation: Float
var showEnvironment: Boolean Hide only primary-ray misses. Lighting, reflections and refraction still use the environment.
var background: Vector4f Linear, straight RGBA background; alpha zero gives a transparent background for compositing.
var indirectClamp: Float Maximum summed indirect luminance per path sample, in linear HDR units before exposure. Zero disables this biased firefly filter (the default). RGB is scaled together; camera-visible emission, background and first-surface lighting are preserved. Deeper glass transmission and caustics can darken. Changing this value resets accumulation.
var emitterSampling: Boolean Sample emissive triangles directly, in addition to the environment, and combine that estimate with the emission a BSDF sample finds using the same power heuristic. This is unbiased: a converged image matches one rendered with it off. It costs one extra shadow ray per non-delta surface interaction, and it is what makes a small, bright lamp converge at all. Emitters behind stochastic alpha coverage, and emissive glass, stay BSDF-only. Turning it off reaches the previous estimator exactly. Changing this value resets accumulation.
val emitterCount: Int Emissive triangles this scene offers the direct sampler; zero leaves emission to BSDF hits.
var rayEpsilon: Float
var exposure: Float Exposure affects display only and never discards samples.
var samples: Int Fresh samples per pixel since the last view change/reset, excluding reprojected history.
var paused: Boolean
var convergence: PathTraceConvergence? Optional per-frame convergence log. Attach one and every render appends a (wall time, samples) point plus any reset it observes, which is what a frames { repeat(n) { frame() } } loop with a println in it was doing by hand. Detach by setting it to null; the tracer holds no other reference to it.
val materialModule: ComputeModulePointer
val materialMesh: ComputeMesh
val output: RenderToTexture Premultiplied linear HDR RGBA; alpha is primary visibility coverage, not sample count.
val triangleCount: Int
var frameOutput: PathTraceFrameOutput? Optional unaccumulated radiance and matching primary guides. The tracer owns their lifetime.
fun enableFrameOutput(): PathTraceFrameOutput Create this tracer's frameOutput, or return the one it already has; the first call resets accumulation. Reconstruction guides currently require opaque, non-transmissive materials, so a scene with glass or partial coverage has to stay on the progressive output.
fun install(): PathTracingRenderer Register compute and RTT passes. Reusing a name retires the previous tracer.
override fun render(command: CommandBuffer, index: Int)
fun blitToScreen(blend: Boolean = true): ModulePointer Optional second pass: sample the RTT, apply exposure/Reinhard, and composite on screen.
fun reset(reason: String = "property edit") Discard accumulated samples and start the estimate again.
val resetGeneration: Long How many times accumulation has been discarded. samples counts within one generation, so a generation that has not advanced is the proof that a sample count is comparable to an earlier one — which four hand-written printf loops in one live session were trying to establish.
val lastResetReason: String Why accumulation was last discarded. See reset.
fun integratorSource(): String The integrator source this tracer compiles: the packaged trace.comp with whatever HARDWARE_QUERY state this backend needs. The starting point for reloadIntegrator.
fun integratorSource(file: File): String integratorSource rebuilt from file — the working-copy trace.comp you are editing, which the packaged resource will not reflect until the next Gradle build. The backend define is applied for you, so a file that already carries one, or lacks one, both come out right.
fun reloadIntegrator(source: String = integratorSource()): IntegratorReload Recompile the integrator on this running tracer, keeping the scene, the environment, the camera pose and every tuning property.
fun reloadIntegrator(file: File): IntegratorReload reloadIntegrator from a working-copy trace.comp; see integratorSource.
fun readLinear(): FloatArray Synchronous GPU readback for export/verification. Call on the render thread, outside frame recording.
override fun close() Detach now; retire GPU resources through Field's frames-in-flight cleanup queue.

ProceduralSplats.kt

Six Gaussian-splat clouds built from code, for a splat shader to deform: a shell, a fern, a nautilus, coral, a bloom and a braid.

Each builder returns a SplatSet of a few thousand flat, oriented splats and uses a fixed hash where it needs variation, so a call gives the same cloud every time and index in an insert means the same splat on every run. all maps the six under the demo's names, in its order.

The helpers below them are ports rather than conveniences. hsl is three.js's Color.setHSL down to the sRGB-to-linear conversion its colour management applies by default, quaternionFromUnitVectors is its Quaternion.setFromUnitVectors down to the axis it chooses when the two vectors are opposite, and previewHash is the fract(sin(x) * 43758.5453) hash the builders draw their variation from.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/ProceduralSplats.kt — read the source

ProceduralSplats

object ProceduralSplats

The six procedural clouds of the madp splat-shader demo, as SplatSets: built from code, deterministic, and the same splat for splat as the demo's (same order, so index in an insert means the same splat in both).

Declaration Summary
val all: Map<String, () -> SplatSet> The six clouds by the demo's names, in the demo's order.
fun icosphereShell(radius: Double = 1.05, depth: Int = 4, pointRadius: Double = 0.024, pointThickness: Double = 0.003): SplatSet Points on a subdivided octahedron pushed out onto a sphere, each a thin disc facing out and coloured by longitude: Spark's constructSpherePoints with radius 1.05, depth 4. 1 026 splats.
fun fernPlume(): SplatSet The Barnsley fern played as the chaos game for 7 200 steps, each point after the first 28 a small leaf-shaped splat, arched and given depth, darker green at the stem.
fun nautilusShell(): SplatSet A logarithmic spiral tube rising 2.8 turns, fourteen plates around each of 240 rings, warm ochre fading lighter toward the open end. 3 360 splats.
fun coralBranches(): SplatSet A branching coral grown recursively four levels deep from one trunk, each branch a wavy row of discs facing along it, thinning toward the tips, red-orange shading to coral.
fun phyllotaxisBloom(count: Int = 2800): SplatSet 2 800 petals on the golden angle, rising and opening like a flower head, warm yellow-green at the base to gold at the rim.
fun torusBraid(): SplatSet Ten strands of beads braided around a (2, 3) torus knot, cyan through violet. 3 400 splats.
fun previewHash(seed: Double): Double The demo's previewHash: the classic fract(sin(x) * 43758.5453) hash, 0..1.
fun quaternionFromUnitVectors(fx: Double, fy: Double, fz: Double, tx: Double, ty: Double, tz: Double): DoubleArray The rotation taking unit vector from to unit vector to, as (x, y, z, w) — three.js's Quaternion.setFromUnitVectors, including its choice of axis when the two are opposite.
fun hsl(h: Double, s: Double, l: Double): DoubleArray three.js's Color.setHSL: h wraps, s and l clamp to 0..1, and the result is converted from sRGB to linear, as three.js's colour management does by default.

RayTracingDocsHarness.kt

The figures on Field's ray tracing pages: the StandardRendererDocsHarness shape, over the ray tracing pipeline instead of the rasterizer.

Every public render method runs a deterministic ray tracing example and captures the output to a PNG. If ray tracing is unavailable, or the module fails to build, the method throws rather than writing a stand-in image, so a page's figure is either a render or nothing. A placeholder is a valid PNG, and a build that accepts one cannot tell a capture from a picture of its own failure.

tools/check_doc_assets.py guards the other side of that: it rejects a published figure whose pixels match the "Ray Tracing / Not Available" card a harness could once write in place of a render on a device without VK_KHR_ray_tracing_pipeline.

From a box connected beneath a renderer, a figure is three lines:

val image = RayTracingDocsHarness.renderUvGradient("/tmp/rt-uv.png")
display("render", RayTracingDocsHarness.htmlForImage(image, "UV gradient"))
println("ray tracing example ready")

src/jvmMain/kotlin/afield/graphics/scene/fabricated/RayTracingDocsHarness.kt — read the source

RayTracingDocsHarness

object RayTracingDocsHarness

Renders the ray tracing documentation's examples offscreen, or throws if the device cannot.

Declaration Summary
class LiveMirrorBallsScene internal constructor(val name: String, private val renderer: Renderer, private val image: Image, private val imageView: ImageView, private val floorVertexBuffer: VulkanBuffer, private val sphereAabbBuffer: VulkanBuffer, private val floorBlas: AccelerationStructure, private val sphereBlas: AccelerationStructure, private val tlas: AtomicReference<AccelerationStructure>, private val rayTracingPointer: afield.graphics.render.RayTracingModulePointer, private val blitPointer: afield.graphics.render.ModulePointer, private val sampler: TextureSampler, var camera: Camera, private val spheres: MutableList<MirrorSphere>)
suspend fun renderUvGradient(outputPath: String, renderer: Renderer = rendererFromContextOrDocument()): File Renders a UV-gradient image using a raygen-only ray tracing pipeline.
suspend fun renderTriangleScene(outputPath: String, renderer: Renderer = rendererFromContextOrDocument()): File Renders a triangle scene using a full ray tracing pipeline: raygen → miss → closesthit.
suspend fun renderShaderTileScene(outputPath: String, renderer: Renderer = rendererFromContextOrDocument()): File Renders a simple plane where checker tiles are computed in shaders.
suspend fun renderMirrorBallsScene(outputPath: String, renderer: Renderer = rendererFromContextOrDocument(), camera: Camera = cameraFromContextOrDefault(renderer)): File Renders an Amiga Juggler-inspired scene: three reflective spheres above a checkerboard floor.
fun installMirrorBallsSceneInWindow(renderer: Renderer = rendererFromContextOrDocument(), camera: Camera = cameraFromContextOrDefault(renderer), name: String = "rt-live-mirror-balls", width: Int = renderer.window.width.coerceAtLeast(1), height: Int = renderer.window.height.coerceAtLeast(1)): LiveMirrorBallsScene Installs the mirror-balls ray tracing scene into the renderer window.
fun installMirrorBallsSceneInWindow(renderer: Renderer, name: String, width: Int = renderer.window.width.coerceAtLeast(1), height: Int = renderer.window.height.coerceAtLeast(1)): LiveMirrorBallsScene
fun renderMirrorBallsSceneInWindow(renderer: Renderer = rendererFromContextOrDocument(), camera: Camera = cameraFromContextOrDefault(renderer), name: String = "rt-live-mirror-balls", width: Int = renderer.window.width.coerceAtLeast(1), height: Int = renderer.window.height.coerceAtLeast(1)): LiveMirrorBallsScene
fun renderMirrorBallsSceneInWindow(renderer: Renderer, name: String, width: Int = renderer.window.width.coerceAtLeast(1), height: Int = renderer.window.height.coerceAtLeast(1)): LiveMirrorBallsScene
fun htmlForImage(imageFile: File, caption: String? = null): String Wraps a captured PNG as an HTML block suitable for display(...).
val UV_GRADIENT_RAYGEN Raygen that writes UV colours to a storage image (no traceRayEXT call).
val TRIANGLE_RAYGEN Raygen for the triangle scene: traces one ray per pixel.
val TRIANGLE_MISS Miss shader: returns a dark-blue background.
val TRIANGLE_CLOSESTHIT Closest-hit shader: visualises barycentric coordinates as RGB.
val SHADER_TILES_RAYGEN Raygen for shader-tile scene: traces one ray per pixel toward a floor quad.
val SHADER_TILES_MISS Miss shader for shader-tile scene.
val SHADER_TILES_CLOSESTHIT Closest-hit shader that computes checker tiles procedurally from world hit position.
val MIRROR_BALLS_RAYGEN Raygen for the mirror-balls scene: traces one primary ray per pixel.
val MIRROR_BALLS_MISS Miss shader for the mirror-balls scene.
val MIRROR_BALLS_FLOOR_CLOSESTHIT Closest-hit shader for the checkerboard floor.
val MIRROR_BALLS_SPHERE_INTERSECTION Analytic intersection shader for a unit sphere inside the procedural AABB BLAS.
val MIRROR_BALLS_SPHERE_CLOSESTHIT Closest-hit shader for reflective spheres.

RayTracingGpuAudit.kt

The ray-tracing audit: one JVM entrypoint that drives Field's path-tracing and ray-pipeline smokes against a real GPU and writes down what it found.

This is the four.raytracing.gpu-audit catalog test. There is no document on disk and no Field server: the audit opens a window, builds a Renderer, makes a bare Box for the smokes to run under, and steps the frame loop itself, resuming each check only between complete frames, because the renderer records with a runBlocking of its own. Every stage writes its verdict and elapsed time into report.json as it finishes, so a run that dies part way through still says where it got to.

The stages selected by default are seams, base, camera, temporal, fog and pipeline; --all adds emitters, medium for participating media, and benchmark, and --stage=NAME runs a single one. A device with no VK_KHR_ray_tracing_pipeline records the pipeline stage as SKIPPED and its reason, unless --require-pipeline says to fail there instead, and --fallback turns the optional ray-tracing negotiation off so the ordinary compute path is what gets measured.

The pipeline stage reads pixels rather than checking that a file was written: a UV gradient against the value its dispatch should have computed, a barycentric triangle that has to reach all three vertex colours, checker geometry that has to come out both dark and light, and a sphere whose traced image has to change when its instance transform moves and come back identical when the transform is put back.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/RayTracingGpuAudit.kt — read the source

RayTracingGpuAudit

object RayTracingGpuAudit

Runs the existing Field renderer and readback smokes on one thread, without a saved document. Default: seams, base/glTF/glass, camera, temporal, fog, pipeline captures. --all adds emitters, medium and GPU benchmarks. --stage=NAME selects one stage. --require-ray-query and --require-pipeline fail closed on an unsuitable device; otherwise unsupported pipeline captures are explicitly SKIPPED in report.json. --fallback disables optional RT negotiation to exercise ordinary compute rendering.

Declaration Summary
fun main(args: Array<String>)
suspend fun capturePipeline(renderer: Renderer, out: File) Fresh files plus assertions on shader output: a screenshot of an empty box cannot pass.

RectSLGalleryHarness.kt

The RectSL gallery on the docs site, rendered offscreen: the StackSLGalleryHarness shape, applied to programs that describe a whole scene rather than one fullscreen fragment.

A RectSL program carries a wall, a duration, an fps, and its rects, tracks, groups and StackSL materials, so the render path here is the one afield.lang.rectsl.RectSLRenderRunner already uses: compile the program, lower each sampled frame to a triangle mesh, and draw it through RECT_ANIMATION_VERTEX_SHADER plus a StackSL-compiled material fragment into an offscreen RenderToTexture.

Both entry points hold one renderer and one graphics context open and walk the program list, which is the "one Field document, many Field 4 renders" pattern. A fresh target is built per example, because each program's wall has its own aspect ratio; the StackSL gallery can reuse one fixed 512² target instead.

renderGallery reads back a single representative still per program. renderAnimations sweeps the program's own timeline frame by frame and encodes each animate: true program to an mp4, since the docs runner can screenshot the native GPU window but never record it, which leaves the readback and the ffmpeg encode to the harness. The programs themselves are editable Field content under this harness's WORKSPACE_DIR, not literals in this file.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/RectSLGalleryHarness.kt — read the source

RectSLGalleryHarness

object RectSLGalleryHarness

Renders the RectSL gallery stills and animations offscreen, one scene program at a time.

Declaration Summary
const val WORKSPACE_DIR Repo-relative workspace whose *_rectsl_.rectsl files are the examples.
data class Example(val name: String, val source: String, val stillTime: Double? = null, val animate: Boolean = false) One gallery entry loaded from the workspace: name is the box slug (and the PNG/mp4 stem rectsl-<name>), source its RectSL program. stillTime is the timeline moment sampled for the still. animate marks programs that also get an mp4.
fun loadExamples(workspaceDir: File = defaultWorkspaceDir()): List<Example> Load every example from the workspace: one box per <name>_rectsl_.rectsl file, ordered by the docOrder in the sibling <name>_metadata_.yaml (which also carries optional animate: / stillTime:). The programs live in the workspace, not in this file.
suspend fun renderGallery(outputDir: File, renderer: Renderer): File Render every workspace example into <outputDir>/rectsl-<name>.png (a single representative frame), reusing a single renderer. Returns the hero PNG.
suspend fun renderAnimations(outputDir: File, renderer: Renderer): List<File> Render an mp4 for every animate: true workspace example into <outputDir>/rectsl-<name>.mp4, reusing one renderer. The scene is sampled over its own timeline; each frame is read back and handed to ffmpeg.
suspend fun renderFitToTarget(outputDir: File, renderer: Renderer): File A dedicated illustration of the hardest bridge idea (#5): a RectSL distribution is a Functions graph, so the host can fit it to a target and RectSL just samples the winner. We fit a two-lobe target with fitBest, bind the seed/target/fitted curves by name into a real RectSL program, and render three arrays — target (reference), seed (before), fitted (after) — so the gold "after" row visibly lands on the target silhouette that the muddy "before" row misses. The layout is the ordinary array + sample path; only the curve was searched for by the host.
val FIT_SCENE_SOURCE: String The scene rendered by renderFitToTarget; target/seed/fit are host-bound curves.
val SDF_SCENE_SOURCE: String The scene the SDF looks are demonstrated on.
val SDF_PATTERN_SOURCE: String A scene whose motion comes from the pattern language, so the differential covers the two tracks emitSdf cannot lower symbolically and samples instead (both pattern queries; ->degrade's 64-bit hash lowers exactly now).
val SDF_CARVE_SOURCE: String The field that gets subtracted by the carve look.
data class SdfAgreement(val label: String, val comparisons: Int, val maxAbsError: Double, val meanAbsError: Double, val quantisation: Double, val coordinateResidual: Double) What measureSdfAgreement found, so the caller can print it and assert on it.
suspend fun renderSdf(outputDir: File, renderer: Renderer): File Everything the SDF page needs, in one graphics context: a still per look, an animated clip, the raster-vs-SDF counterfactual, and the GPU↔CPU differential. Returns the hero still.
suspend fun measureSdfAgreement(renderer: Renderer, program: SceneProgram, times: List<Double> = listOf(0.0, 0.37, 0.95, 2.05, 3.6), ranges: List<Pair<String, Double>> = listOf("near" to PROBE_NEAR_RANGE, "far" to PROBE_FAR_RANGE)): List<SdfAgreement> Ask the compiled shader what the field is, and compare it against the CPU sampler that the raster path draws from.
suspend fun measureGallerySdfAgreement(renderer: Renderer, maxScenes: Int = 8, times: List<Double> = listOf(0.0, 0.41, 1.7)): List<SdfAgreement> The same differential, over a diverse subset of the real gallery rather than the two scenes this page draws.

SplatSequence.kt

Playing a splat capture over time: the frame reader, the play head, and the box controls that drive it.

A splat sequence is a directory of per-frame .compressed.ply files, and the reason this file exists separately from GaussianSplatRenderer.kt is that the interesting problem is on the disk side rather than the GPU side. SplatSequence reads frames on demand into a bounded most-recently-used cache, sized in frames rather than bytes, because a working session scrubs: a 124-frame capture is 44 MB and would fit whole, a five-minute one will not, and a forward-only reader would make the short case pleasant and the long case impossible. Nothing prefetches, which is a decision and not an omission — read-ahead only pays during playback, and doing it properly costs a thread and a drop policy.

SplatTransport is the play head and holds no data at all: a frame index, a rate, and the playing/looping state. Keeping it separate from the reader is what lets the same transport drive a sequence on disk or a frame set already resident, and its source fps is independent of the display rate, so a 12 fps capture shown at 60 does not become a 60 fps capture. Box.transportControls wires it to a control group, and the ToHTML rendering puts the play head in a box's output where it can be read at a glance.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/SplatSequence.kt — read the source

Top-level functions

Declaration Summary
fun Box.transportControls(transport: SplatTransport, channel: String = "transport") A transport you can drive from an output area: scrub, speed, play/pause, loop and single steps.

SplatSequence

class SplatSequence(val files: List<File>, val cacheFrames: Int = 96)

A directory of .compressed.ply frames, readable in any order.

Declaration Summary
val files: List<File>
val cacheFrames: Int How many decoded frames to keep. Each is roughly the file's size again as ints.
val frameCount: Int
val stats: String Cache hits and misses since construction, for deciding whether cacheFrames is right.
fun raw(frame: Int): CompressedSplatPly.RawSplats The payload for frame, read if it is not already cached. Indices outside the sequence wrap, so a caller doing arithmetic on slot offsets does not have to.
fun prime(): SplatSequence Read every frame now. Only sensible when the whole sequence fits in cacheFrames.
fun peakWindowSplats(window: Int): Int The largest total splat count any window consecutive frames can reach, which is what a GaussianSplatRenderer.streaming cloud's capacity has to be.

SplatTransport

class SplatTransport(val frameCount: Int, var fps: Double = 12.0) : ToHTML

A play head over frameCount frames: play, pause, scrub, loop, and run backwards.

Declaration Summary
val frameCount: Int
var fps: Double Source frames per second. Independent of the display rate.
var playing: Boolean
var presentationPaced: Boolean When true, advance only services clock bookkeeping; a frame-set presenter moves time with commitPresented. This keeps the public playhead identical to the complete asset set actually shown instead of letting it run several frames ahead during an I/O underrun.
var speed: Double 1.0 is forward at fps; negative runs backwards; 0.0 is a pause that keeps playing true.
var loop: Boolean Wrap at the ends. When false the play head clamps and playing is left alone.
var seekGeneration: Long Increases on an explicit scrub/seek, not on ordinary wall-clock advancement.
var position: Double
val frame: Int The frame position resolves to.
var normalized: Double position as 0..1 over the sequence, for a slider.
fun advance(nowNanos: Long, maxCatchUpSeconds: Double = 0.25): Boolean Advance the play head to nowNanos and report whether frame changed.
fun seek(target: Double): Boolean Jump to target frames, fractional. Reports whether frame changed.
fun scrubBy(delta: Double): Boolean Jump by delta frames, fractional — the scrub-by-a-drag case.
fun invalidate() Force the next advance to report a change, after something else invalidated the upload.
fun commitPresented(frame: Int, nowNanos: Long) Move the playhead after one complete externally-paced frame set has committed. Not a seek.
fun resetClock() Drop the clock, so the next advance does not count the time since the last one.
override fun toString(): String
override fun toHTML(): String A play head you can read at a glance: the track, where the head sits on it, the frame count, and the state that decides what happens next.

SplatSet.kt

The CPU side of a splat cloud: how one is built, where it can be loaded from, and how it is framed before a camera looks at it.

SplatSet keeps the splats in a flat FloatArray laid out the way the GPU reads them, and grows as push appends. Its companion loads a cloud instead of building one: fromPly reads a Gaussian-splat capture, decoding the log scales, logit opacity and spherical-harmonic colour those files store, and fromGltf turns the vertices of a mesh into discs facing along their normals, sized from how densely they cover the surface.

fit is the framing a SplatShader uses: a median centre and a 90th-percentile radius, so a handful of stray splats cannot decide where the camera points. A cloud that arrives in metres needs fitted before an insert written for a unit-sized cloud reads on it as it does on ProceduralSplats.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/SplatSet.kt — read the source

SplatSet

class SplatSet(initialCapacity: Int = 1024)

Gaussian splats on the CPU in their parametric form — centre, radii, rotation, colour and opacity — rather than the covariance GaussianSplatRenderer draws from.

Declaration Summary
var floats: FloatArray The packed splats; only the first count × 16 floats are live.
var count: Int How many splats have been pushed.
fun push(x: Double, y: Double, z: Double, sx: Double, sy: Double, sz: Double, qx: Double, qy: Double, qz: Double, qw: Double, opacity: Double, r: Double, g: Double, b: Double): SplatSet Append one splat. qx..qw need not be normalised; a zero quaternion means no rotation.
fun centre(index: Int): Vector3f The centre of splat index.
fun bounds(): Pair<Vector3f, Vector3f> The smallest box holding every splat centre; both corners are zero for an empty set.
fun fit(): Fit Where the cloud is and how big, measured so that outliers do not count: Fit.centre is the per-axis median of the splat centres and Fit.radius90 the distance from it that nine splats in ten fall within. Non-finite centres are skipped. An empty set fits the unit ball.
fun fitted(radius: Double = 1.0): SplatSet A copy moved and scaled so fit comes out centred on the origin with a 90th-percentile radius of radius: centres and radii both scale, rotations and colours stay.
data class Fit(val centre: Vector3f, val radius90: Float) A robust centre and radius for a cloud; see SplatSet.fit.

SplatShader.kt

The splat-shader box: a cloud of Gaussian splats uploaded once, a few lines of GLSL that rewrite every splat each frame, and Field's Vulkan splat renderer drawing the result.

SplatShader does the work. It uploads the parametric splats, binds them to the box's compute module as splatsIn and the renderer's cloud as splatsOut, dispatches the insert once per splat before the sort runs, and installs the cloud in an accumulation target, so a trail is a setting rather than a second code path. SplatShaderSettings is everything a viewer can change while it runs, from the camera and the turn rate to the opacity and gamma the splats are blended with, and showControls puts the demo's sliders in the box's controls output.

The clouds come from SplatSet and ProceduralSplats; the sort, the packed covariance form and the splat shaders themselves are GaussianSplatRenderer's.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/SplatShader.kt — read the source

Top-level functions

Declaration Summary
fun Box.showControls(shader: SplatShader, channel: String = "controls") The demo's viewer toolbar for shader, in this box's controls output: the auto-rotate speed, the size, a splat-size multiplier, play/pause for t and the turn, and a button that starts t again from zero. The panel opens, as the demo's toolbar is always in view.

SplatShader

class SplatShader(val module: ComputeModulePointer, val splats: SplatSet, val renderer: Renderer = fromContext(), configure: SplatShaderSettings.() -> Unit = {})

A Gaussian-splat cloud whose splats are rewritten every frame by a few lines of GLSL in a box: the madp splat-shader demo's "insert", on Field's Vulkan splat renderer.

Declaration Summary
val module: ComputeModulePointer The compute module holding the insert, usually computeModuleHere {}.
val splats: SplatSet The splats the insert starts from each frame.
val renderer: Renderer The renderer to draw on; by default the one this box is running in.
val settings The settings after configure ran; the framing, rotation and splat look can change live.
val source: FrameBufferGroup The parametric splats, as uploaded: what the insert reads.
val cloud: GaussianSplatRenderer.Cloud The renderer's cloud: what the insert writes, and what is sorted and drawn.
val trail: PointAccumulation What the cloud is drawn into, and what leaves the trail.
val mesh: ComputeMesh The dispatch in module that runs the insert, one invocation per splat.
val fit: SplatSet.Fit The SplatSet.fit the framing uses.
var t: Double Seconds since this was made: the insert's t. Set it to scrub.
var angle: Double The cloud's current turn about +y, in radians.
var playing: Boolean When false, t and the rotation stand still and the insert sees the same t each frame.
fun model(): Matrix4f The model matrix this frame: the fit, the size, then the turn about +y.
fun applyLook() Push settings' camera and splat look onto the cloud; called once a frame, so edits show.
fun remove() Take the cloud and its trail off the renderer. Launching another SplatShader with the same name does this too.

SplatShaderSettings

class SplatShaderSettings

What a SplatShader can be told; see each property.

Declaration Summary
var name: String The renderer module name. Two SplatShaders with one name replace each other.
var autoRotate: Double Turn about +y, in the demo's slider units: 0.32 is its default, 0 stands still.
var size: Double The demo's size slider: a multiplier on the fitted size.
var trail: Boolean The demo's trail mode: lay each frame over a fading copy of the last, so moving splats leave streaks. fade is how much of the old picture goes each frame.
var fade: Double With trail on, the part of the previous frame that fades away each frame: the demo's 0.12.
var fit: Boolean Frame the cloud by its median and 90th-percentile radius; false draws it as it is.
var splatSize: Float Multiplies every splat's screen size. The default, 1/√2.2, is what makes a splat look the size it does in the demo. Spark blends in display space, so a Gaussian's faint tails stay dark; Field blends in linear light and the window encodes the result to sRGB, which lifts those tails, and at the same radius the visible dot comes out about twice as wide. For a splat over black, encode(c · a') = c · a wants a' ≈ a^2.2, which is the same Gaussian narrowed by √2.2. Set 1 for the splats' true size.
var alphaScale: Float Multiplies every splat's opacity.
var colorGamma: Float See GaussianSplatRenderer.Config.colorGamma; 2.2 is right for Field's window.
var cameraAt: Vector3f
var cameraTarget: Vector3f
var fovDegrees: Double Vertical field of view.
var sortMode: GaussianSplatRenderer.SortMode GaussianSplatRenderer.SortMode.ExactRadix by default: the keys are view-depth float bits.

SplatShaderSortSmoke.kt

src/jvmMain/kotlin/afield/graphics/scene/fabricated/SplatShaderSortSmoke.kt — read the source

SplatShaderSortSmoke

object SplatShaderSortSmoke

Does the splat depth sort see where a SplatShader insert put the splats? Measured on the GPU.

Declaration Summary
fun main(args: Array<String>)

StackSLGalleryHarness.kt

The StackSL gallery on the docs site: one fullscreen fragment per example program, rendered offscreen into a fixed 512² (OUTPUT_DIM) target.

It holds one renderer, one offscreen RenderToTexture and one fragment slot open, then walks the StackSL programs in a canonical_graphics-style workspace (WORKSPACE_DIR). Each program is injected through the ordinary {{ stack.auto.program }} template path, the same live-coding mechanism the StackSL pages describe, and the fullscreen module is rebuilt under a single name, so a new module replaces the previous one in the target's module map. The resulting texture is then read back.

renderGallery reads back a single still per program. renderAnimations drives a time push constant across frames and encodes each animate: true program to an mp4, since the docs runner can screenshot the native GPU window but never record it, which leaves the readback and the ffmpeg encode to the harness. Both are "one Field document, many Field 4 renders": a whole page's assets come out of a single graphics context rather than a JVM restart apiece. The example programs are editable Field content in the workspace, not literals in this file.

Both targets use GALLERY_FORMAT, an sRGB format, for the same reason the window's swapchain is B8G8R8A8_SRGB: the fragment writes linear colour and the attachment encodes it on write, so the bytes read back are the bytes a viewer sees in a live window. A UNORM attachment stores the linear values unencoded, and reads back markedly darker than the same program looks in a window. The feedback programs sample their own previous frame, and an sRGB ping-pong hands that sample back decoded, so they still compute in linear light; that is also PingPongRenderToTexture's default.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/StackSLGalleryHarness.kt — read the source

StackSLGalleryHarness

object StackSLGalleryHarness

Renders the StackSL gallery stills and animations offscreen, one fullscreen program at a time.

Declaration Summary
const val OUTPUT_DIM
const val GALLERY_FORMAT The colour format of every gallery target: the swapchain's encoding, in RGBA order so TexturePreviewCache.capture copies it straight to the PNG without a blit or a swap.
const val WORKSPACE_DIR Repo-relative workspace whose *_stackSL_.stacksl files are the examples.
data class Example(val name: String, val program: String, val time: Float = 1.75f, val animate: Boolean = false, val timeSpeed: Float = 1.0f, val feedback: Boolean = false) One gallery entry loaded from the workspace: name is the box slug (and the PNG/mp4 stem stacksl-<name>), program its StackSL source, time is baked into input.w for the still. animate marks time-driven programs that also get an mp4, and timeSpeed scales how fast input.w sweeps in that clip.
fun loadExamples(workspaceDir: File = defaultWorkspaceDir()): List<Example> Load every example from the canonical_graphics-style workspace: one box per <name>_stackSL_.stacksl file, ordered by the docOrder in the sibling <name>_metadata_.yaml (which also carries optional animate:/timeSpeed:). The programs live in the workspace, not in this file.
suspend fun renderGallery(outputDir: File, renderer: Renderer): File Render every workspace example into <outputDir>/stacksl-<name>.png, reusing a single renderer + target. Returns the hero PNG for the docs fence.
suspend fun renderAnimations(outputDir: File, renderer: Renderer): List<File> Render an mp4 for every animate: true workspace example into <outputDir>/stacksl-<name>.mp4, reusing one renderer + target. The shader is built once per program; a time push constant sweeps over ANIM_FRAMES frames, each read back and handed to ffmpeg.

StandardRendererDocsHarness.kt

The offscreen renders behind Field's graphics documentation: deterministic StandardRenderer scenes captured to PNG, plus the HTML wrapper that puts one in a box output.

A page gets its figure by calling one of the render functions from a box, passing the returned file to htmlForImage, and letting the docs build screenshot the result through the ordinary backdoor path. The scenes take the renderer's features one at a time: metalness, shadow and ambient-occlusion comparisons, a camera and depth study, glTF loading, blended transparency, and the post-stack vignette and tone-map passes. Each takes its renderer from the box's context by default, so a box connected beneath a renderer needs to pass only an output path.

renderStackSLGallery and renderRectSLGallery, with their animation counterparts, are wrappers rather than scenes of their own: the rendering belongs to StackSLGalleryHarness and RectSLGalleryHarness, and the wrapper copies the hero image that harness produced to the path the docs fence checks.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/StandardRendererDocsHarness.kt — read the source

StandardRendererDocsHarness

object StandardRendererDocsHarness

Renders the graphics documentation's StandardRenderer figures offscreen, one scene per call.

Declaration Summary
suspend fun renderDefaultScene(outputPath: String, renderer: Renderer = rendererFromContextOrDocument(), window: Window = renderer.window): File
suspend fun renderStackSLGallery(outputPath: String, renderer: Renderer = rendererFromContextOrDocument()): File Renders the whole StackSL gallery (see StackSLGalleryHarness) inside a single graphics context, writing every stacksl-*.png into public/docs/assets/stacksl/, then copies the hero image to outputPath so the docs fence can embed one representative capture. This is the "one Field document, many renders" path: the entire gallery is produced without restarting the JVM per screenshot.
suspend fun renderStackSLAnimations(outputPath: String, renderer: Renderer = rendererFromContextOrDocument()): File Renders an mp4 per animated StackSL example (see StackSLGalleryHarness.renderAnimations) into public/docs/assets/stacksl/, then writes a still hero to outputPath for the docs fence to validate. Same single-graphics-context path as renderStackSLGallery.
suspend fun renderRectSLGallery(outputPath: String, renderer: Renderer = rendererFromContextOrDocument()): File Renders the whole RectSL gallery (see RectSLGalleryHarness) inside a single graphics context, writing a representative still rectsl-*.png per program into public/docs/assets/rectsl/, then copies the hero image to outputPath. The RectSL twin of renderStackSLGallery.
suspend fun renderRectSLAnimations(outputPath: String, renderer: Renderer = rendererFromContextOrDocument()): File Renders an mp4 per animated RectSL example (see RectSLGalleryHarness.renderAnimations) into public/docs/assets/rectsl/, then writes a still hero to outputPath for the docs fence to validate. Same single-graphics-context path as renderRectSLGallery.
suspend fun renderRectSLFit(outputPath: String, renderer: Renderer = rendererFromContextOrDocument()): File Renders the "evolving a distribution" (#5) still — the host fits a curve to a target and RectSL lays out the array from it (see RectSLGalleryHarness.renderFitToTarget) — to rectsl-fit-to-target.png, then copies it to outputPath for the docs fence to validate.
suspend fun renderRectSLSdf(outputPath: String, renderer: Renderer = rendererFromContextOrDocument()): File Renders a RectSL scene as a signed distance field on the GPU (see RectSLGalleryHarness.renderSdf): one still per look preset, a motion-blur clip, the raster-vs-SDF counterfactual, and the GPU↔CPU differential — which throws before any image is written if the compiled shader and the CPU sampler disagree. Everything lands in public/docs/assets/rectsl/; the hero is copied to outputPath for the fence to validate.
suspend fun renderMetalnessComparison(outputPath: String, renderer: Renderer = rendererFromContextOrDocument(), window: Window = renderer.window): File
suspend fun renderShadowComparison(outputPath: String, renderer: Renderer = rendererFromContextOrDocument(), window: Window = renderer.window): File
suspend fun renderAoComparison(outputPath: String, renderer: Renderer = rendererFromContextOrDocument(), window: Window = renderer.window): File
suspend fun renderShinyToriCheckerboard(outputPath: String, renderer: Renderer = rendererFromContextOrDocument(), window: Window = renderer.window): File
suspend fun renderShaderGeneratedCheckerboard(outputPath: String, renderer: Renderer = rendererFromContextOrDocument(), window: Window = renderer.window): File
suspend fun renderCheckerboardCameraDepthStudy(outputPath: String, renderer: Renderer = rendererFromContextOrDocument(), window: Window = renderer.window): File
suspend fun renderBlendedTransparency(outputPath: String, renderer: Renderer = rendererFromContextOrDocument(), window: Window = renderer.window): File
suspend fun renderPostStackVignette(outputPath: String, renderer: Renderer = rendererFromContextOrDocument(), window: Window = renderer.window): File
suspend fun renderToyCarGltf(outputPath: String, renderer: Renderer = rendererFromContextOrDocument(), window: Window = renderer.window, gltfPath: String = File("public/docs/assets/fabricate_demo/ToyCar.glb").absolutePath, environmentPath: String = File("public/docs/assets/fabricate_demo/venice_sunset_1k.exr").absolutePath): File
suspend fun renderMultiLightSkybox(outputPath: String, renderer: Renderer = rendererFromContextOrDocument(), window: Window = renderer.window, environmentPath: String = File("public/docs/assets/fabricate_demo/venice_sunset_1k.exr").absolutePath): File
suspend fun renderToneMapBloom(outputPath: String, renderer: Renderer = rendererFromContextOrDocument(), window: Window = renderer.window, environmentPath: String = File("public/docs/assets/fabricate_demo/venice_sunset_1k.exr").absolutePath): File
fun htmlForImage(imageFile: File, caption: String? = null): String

simpleGaussianSplatSupport.kt

A Gaussian-splat shader pair seeded into a box as editable starter source, so the splat shader is live Field code rather than source hidden inside GaussianSplatRenderer.

The two entry points are call-shaped macros, so their Kotlin bodies only throw and the work is in the expansion: it writes the renderer's starter vertex and fragment source into the box's vertex and fragment properties, compiles them, reflects their interface into generated bindings, and rewrites the call to _moduleHere. What comes back is an ordinary graphics module, with the reload actions and shader-error reporting every other shader box has, and GaussianSplatRenderer.Cloud takes it as suppliedRenderModule.

There are two of them because multiview is a different vertex shader and not a flag. gaussianSplatMultiviewModuleHere seeds the variant that requires GL_EXT_multiview and selects a model-view and projection matrix per eye from gl_ViewIndex; use it when the cloud draws into a RenderToTexture whose multiviewViewMask is non-zero, and the mono one otherwise. The fragment shader is the same in both cases.

The seeding happens once. macro_moduleHereWithDefaultShaders writes the starter source only where the box has no shader source of its own, so an edited shader survives re-running the macro.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/simpleGaussianSplatSupport.kt — read the source

Top-level functions

Declaration Summary
fun Box.gaussianSplatModuleHere(p: Parameters.() -> Unit): ModulePointer A box-owned Gaussian-splat graphics module, initially seeded with Field's built-in mono shader.
fun Box.gaussianSplatMultiviewModuleHere(p: Parameters.() -> Unit): ModulePointer The multiview counterpart to gaussianSplatModuleHere. Use this for a RenderToTexture whose multiviewViewMask is non-zero; the seeded vertex shader exposes left/right model-view and projection matrices and selects them with gl_ViewIndex.
fun Box.macro_gaussianSplatModuleHere(args: List<String>): Insert? The macro expansion behind gaussianSplatModuleHere: the ordinary graphics-module expansion, with GaussianSplatRenderer's mono splat shader pair as the source to seed.
fun Box.macro_gaussianSplatMultiviewModuleHere(args: List<String>): Insert? macro_gaussianSplatModuleHere with the multiview vertex shader, behind gaussianSplatMultiviewModuleHere.
fun registerGaussianSplatModuleMacros() Puts both splat macros into MacroRegistry.callRewrite, under the names box code writes.

standardRenderer.kt

The deferred renderer the rest of Fabricate is arranged around: the pass bundle, its shaders, and the configuration that sizes them.

This is the file to read first, because the objects it returns are what almost everything else here consumes. StandardRenderer.build(...) builds seven passes (g-buffer, shadow map, ambient occlusion, lighting composite, transparent composite, motion blur, anti-aliasing) and hands back a Bundle holding each as a RenderToTexture, along with the scene defaults, the scene graph and the post stack. Nothing is hidden behind that: FabricateFrameGraph wraps the same targets to give them names and an order, FabricatePostStack appends to them, and a box that wants to reach past all of it and manipulate a RenderToTexture directly still can. Throughout, the existing Field 4 primitives are composed rather than replaced.

Deferred is the choice that explains the pass list. Geometry is rasterized once into a g-buffer of positions, normals and material parameters, and lighting is then evaluated per screen pixel, so cost scales with lights rather than with lights times triangles, which is what lets FabricateScene's light list be arbitrary. It is also why transparency needs a separate pass at the end: a g-buffer holds one surface per pixel, and a blended surface is a second one.

Scenes are not built here. FabricateScene owns the graph and FabricateScenePlanner produces the draw queues; this file renders what it is given. For the renderer working, with captures of each pass, see public/docs/fabricate_renderer.md.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/standardRenderer.kt — read the source

Top-level functions

Declaration Summary
fun postEffectTargetParameters(name: String, shape: FabricatePassShape, rtt: RTTParameters.() -> Unit = {}): RTTParameters.() -> Unit The RTTParameters every StandardRenderer.Bundle.addShaderEffect pass target is built with.

StandardRenderer

object StandardRenderer

One-stop shop for a small deferred renderer: g-buffer, shadow map, AO, and a lighting composite. Everything here is built on top of the existing Field 4 primitives so users can just feed meshes in and wire a RenderTarget around the passes.

Declaration Summary
data class Config(val renderWidth: Int = 0, val renderHeight: Int = 0, val shadowWidth: Int = 1024, val shadowHeight: Int = 1024, val hdrFormat: Int = VK_FORMAT_R16G16B16A16_SFLOAT, val normalFormat: Int = VK_FORMAT_R16G16B16A16_SFLOAT, val positionFormat: Int = VK_FORMAT_R16G16B16A16_SFLOAT, val motionFormat: Int = VK_FORMAT_R16G16B16A16_SFLOAT, val colorFormat: Int = VK_FORMAT_B8G8R8A8_SRGB, val environmentTexture: LoadedTexture? = null, val environmentStrength: Float = 0f, val environmentRotation: Float = 0f, val backgroundColor: Vector3f = Vector3f(0f, 0f, 0f))
data class SceneDefaults(val camera: Camera, val projection: Projection, val rasterJitterNdc: afield.math.linalg.Vector2f = afield.math.linalg.Vector2f(), val lightDirection: Vector3f = Vector3f(DEFAULT_LIGHT_DIRECTION).normalize(), val lightTarget: Vector3f = Vector3f(0f, 0f, 0f), val lightColor: Vector3f = Vector3f(DEFAULT_LIGHT_COLOR), var lightDistance: Float = DEFAULT_LIGHT_DISTANCE, var lightIntensity: Float = DEFAULT_LIGHT_INTENSITY, var shadowBias: Float = DEFAULT_SHADOW_BIAS, var shadowExtent: Float = DEFAULT_SHADOW_EXTENT, var shadowNear: Float = DEFAULT_SHADOW_NEAR, var shadowFar: Float = DEFAULT_SHADOW_FAR, var shadowStrength: Float = DEFAULT_SHADOW_STRENGTH, var shadowSoftness: Float = DEFAULT_SHADOW_SOFTNESS, val ambientColor: Vector3f = Vector3f(DEFAULT_AMBIENT_COLOR), var aoStrength: Float = DEFAULT_AO_STRENGTH, var aoRadius: Float = DEFAULT_AO_RADIUS, var aoBias: Float = DEFAULT_AO_BIAS, var aoIntensity: Float = DEFAULT_AO_INTENSITY, var environmentTexture: LoadedTexture? = null, var environmentStrength: Float = 0f, var environmentRotation: Float = 0f, val backgroundColor: Vector3f = Vector3f(0f, 0f, 0f), var showEnvironmentBackground: Boolean = false, var environmentBackgroundIntensity: Float = 1f, var motionBlurStrength: Float = DEFAULT_MOTION_BLUR_STRENGTH, var motionBlurClamp: Float = DEFAULT_MOTION_BLUR_CLAMP, var motionBlurDepthReject: Float = DEFAULT_MOTION_BLUR_DEPTH_REJECT, var aaBlend: Float = DEFAULT_AA_BLEND, var fxaaEdgeThreshold: Float = DEFAULT_FXAA_EDGE_THRESHOLD, var fxaaEdgeThresholdMin: Float = DEFAULT_FXAA_EDGE_THRESHOLD_MIN, var fxaaSpanMax: Float = DEFAULT_FXAA_SPAN_MAX)
data class Bundle(val gBuffer: RenderToTexture, val shadowMap: RenderToTexture, val ao: RenderToTexture, val lighting: RenderToTexture, val transparentComposite: RenderToTexture, val motionBlur: RenderToTexture, val antiAlias: RenderToTexture, val gBufferModule: ModulePointer, val shadowModule: ModulePointer, val aoModule: ModulePointer, val lightingModule: ModulePointer, val transparentCopyModule: ModulePointer, val transparentModule: ModulePointer, val motionBlurModule: ModulePointer, val antiAliasModule: ModulePointer, val scene: SceneDefaults, val sceneGraph: FabricateScene = FabricateScene(), val postStack: FabricatePostStack = FabricatePostStack(transparentComposite))
fun build(renderer: Renderer = fromContext(), config: Config = Config()): Bundle
fun ModulePointer.ensureFullscreenTriangle(name: String = "fullscreen"): Mesh Quick fullscreen triangle helper to avoid having to allocate vertex buffers for post passes.
data class PullingMesh(val mesh: Mesh, val position: FrameBufferGroup, val normal: FrameBufferGroup, val uv: FrameBufferGroup? = null, val tangent: FrameBufferGroup? = null) Build a mesh wired for vertex-pulling: creates per-frame position/normal buffers and writes their GPU addresses into the mesh push constants. Buffers are host-visible + addressable so you can stream data each frame using DynamicallyWriteable semantics.
fun ModulePointer.makePullingMesh(name: String = "pulling-mesh", maxVertices: Int = 10, renderer: Renderer = current.parameters.renderer, positionBuffer: FrameBufferGroup? = null, normalBuffer: FrameBufferGroup? = null): PullingMesh
fun ModulePointer.makeTexturedPullingMesh(name: String = "textured-pulling-mesh", maxVertices: Int = 10, renderer: Renderer = current.parameters.renderer, positionBuffer: FrameBufferGroup? = null, normalBuffer: FrameBufferGroup? = null): PullingMesh

standardShapes.kt

Ten par_shapes primitives, written straight into vertex-pulling buffers as triangle soup.

StandardShapes is the layer under StandardRenderer.Bundle.registerCube, registerSphere and the eight other register* shape helpers, one per function here. Each builds a par_shapes mesh, scales it, translates it if the call gave a centre, and writes it into two buffers the caller supplies, one for positions and one for normals. The write is non-indexed: the source mesh's triangle list is expanded, so three vertices go out per triangle and a vertex-pulling shader reads a position and its normal at the same index with no indirection. ShapeWriteResult comes back carrying the source mesh's vertex and triangle counts alongside what was actually written, which is how a caller sizes its mesh afterwards instead of predicting the triangle count up front.

WriteConfig covers the two decisions a caller usually has to make. padVec3To4 writes each vector as four floats for std430 alignment and is on by default; append chooses between continuing after whatever is already in the buffer and starting at its beginning, and all ten register* helpers pass append = false. Normals come from par_shapes, computed there if the mesh arrived without any, with an index that falls outside the normal buffer reading the triangle's face normal instead. Each call frees its par_shapes mesh before returning.

src/jvmMain/kotlin/afield/graphics/scene/fabricated/standardShapes.kt — read the source

StandardShapes

object StandardShapes

Thin Kotlin helpers around par_shapes that emit non-indexed positions + normals into caller-provided DynamicallyWriteable buffers (useful for vertex-pulling tests).

Declaration Summary
data class WriteConfig(val append: Boolean = true, val padVec3To4: Boolean = true, val positionW: Float = 1f, val normalW: Float = 0f)
data class ShapeWriteResult(val sourceVertices: Int, val sourceTriangles: Int, val writtenVertices: Int, val positionFloatsWritten: Int, val normalFloatsWritten: Int)
fun sphere(positions: DynamicallyWriteable, normals: DynamicallyWriteable, radius: Float = 1f, slices: Int = 32, stacks: Int = 32, center: Vec3? = null, config: WriteConfig = WriteConfig()): ShapeWriteResult
fun hemisphere(positions: DynamicallyWriteable, normals: DynamicallyWriteable, radius: Float = 1f, slices: Int = 32, stacks: Int = 32, config: WriteConfig = WriteConfig()): ShapeWriteResult
fun cylinder(positions: DynamicallyWriteable, normals: DynamicallyWriteable, radius: Float = 1f, height: Float = 1f, slices: Int = 32, stacks: Int = 1, config: WriteConfig = WriteConfig()): ShapeWriteResult
fun cone(positions: DynamicallyWriteable, normals: DynamicallyWriteable, radius: Float = 1f, height: Float = 1f, slices: Int = 32, stacks: Int = 1, config: WriteConfig = WriteConfig()): ShapeWriteResult
fun disk(positions: DynamicallyWriteable, normals: DynamicallyWriteable, radius: Float = 1f, slices: Int = 32, stacks: Int = 1, config: WriteConfig = WriteConfig()): ShapeWriteResult
fun cube(positions: DynamicallyWriteable, normals: DynamicallyWriteable, size: Float = 1f, center: Vec3? = null, config: WriteConfig = WriteConfig()): ShapeWriteResult
fun torus(positions: DynamicallyWriteable, normals: DynamicallyWriteable, slices: Int = 32, stacks: Int = 32, tubeRadius: Float = 0.25f, majorRadius: Float = 1f, config: WriteConfig = WriteConfig()): ShapeWriteResult
fun trefoilKnot(positions: DynamicallyWriteable, normals: DynamicallyWriteable, slices: Int = 64, stacks: Int = 16, radius: Float = 1f, config: WriteConfig = WriteConfig()): ShapeWriteResult
fun kleinBottle(positions: DynamicallyWriteable, normals: DynamicallyWriteable, slices: Int = 32, stacks: Int = 32, scale: Float = 0.1f, config: WriteConfig = WriteConfig()): ShapeWriteResult
fun rock(positions: DynamicallyWriteable, normals: DynamicallyWriteable, seed: Int = 1, subdivisions: Int = 4, radius: Float = 1f, config: WriteConfig = WriteConfig()): ShapeWriteResult