accel

package module
v0.0.0-...-248e3a0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 27, 2026 License: BSD-3-Clause Imports: 21 Imported by: 0

README

accel

Run compute on the GPU from Go. No cgo, no vendor SDK, no toolchain.

Go Reference CI cgo-free


You write a kernel in a subset of Go. accel compiles it ahead of time and runs it on whichever backend the machine has — today the CPU or Metal — with CGO_ENABLED=0 the whole way.

[!IMPORTANT] The two halves of accel are at different stages, and one hedge for both would mislead you either way.

Compute and tensors are settled. The surface is frozen — names, shapes and behaviour are the contract — and an inference framework is built on it and files what it finds. Changing something here costs an argument, a deprecation and a release note. specs/036-documentation.md §5 is the record, symbol by symbol, including the few marked provisional and the event that moves each.

Graphics works and is younger. It runs on the CPU backend and on Metal, compared pixel by pixel — render passes, depth, blending, indexed and indirect draws, and attachment formats — and it presents to a window you create; accel does not create windows.

The one change that was going to break callers has landed: an attachment names a texture view rather than a buffer view, so it can carry a format, a mip level and an array layer. What is left is additive: mip levels above one, and rejecting a subresource used as an attachment and read by a stage at once. specs/045-texture-attachments.md §8 is the ledger. Treat the graphics API as settling rather than settled: it is outside the freeze record below, and a name here may still move where one in the compute half will not.

Vulkan, D3D12, OpenGL and WebGPU are designed and not started. The status table says which is which.

Install

go get golang.design/x/accel
go get -tool golang.design/x/accel/cmd/accel-kernel

The second line registers the kernel generator as a tool dependency, which is what lets go generate build it. Without it the generator cannot resolve its own dependencies from your module and go generate fails — the generator is a Go program that type-checks the package it compiles, so it needs the type checker in your module graph.

Run something

Kernels go in their own package. Write one:

// kernels/scale.go
package kernels

import "golang.design/x/accel"

//go:generate go tool accel-kernel .

//accel:kernel workgroup=64
func Scale(t accel.Thread, in []float32, out []float32) {
	i := t.GlobalID().X
	if i < uint32(len(out)) {
		out[i] = in[i] * 2
	}
}

go generate ./... turns that into kernels.ScaleKernel: a compiled record carrying the workgroup size and the bindings, with the read and write access of each one inferred from the body, so you never declare them.

[!TIP] Keep kernels in a package of their own, as above. The generator type-checks the package it compiles, so a package that already refers to ScaleKernel cannot be generated for the first time — the symbol does not exist yet.

Then use it:

// main.go
package main

import (
	"fmt"
	"log"

	"example.com/hello/kernels"
	"golang.design/x/accel"
)

func main() {
	// The best device this machine has, falling back to the CPU backend
	// when there is no GPU. The same program runs either way.
	dev, err := accel.OpenBest(accel.Policy{AllowCPU: true})
	if err != nil {
		log.Fatal(err)
	}
	defer dev.Close()

	pipe, err := dev.NewComputePipeline(accel.ComputePipelineDescriptor{
		Kernel: &kernels.ScaleKernel,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer pipe.Close()

	const n = 256
	usage := accel.BufferStorage | accel.BufferCopySrc | accel.BufferCopyDst
	in, _ := dev.NewBuffer(accel.BufferDescriptor{DType: accel.F32, Count: n, Usage: usage, Label: "in"})
	out, _ := dev.NewBuffer(accel.BufferDescriptor{DType: accel.F32, Count: n, Usage: usage, Label: "out"})
	defer in.Close()
	defer out.Close()

	src := make([]float32, n)
	for i := range src {
		src[i] = float32(i)
	}
	if err := dev.Queue().WriteBuffer(in, 0, src); err != nil {
		log.Fatal(err)
	}
	inView, _ := in.View(0, n)
	outView, _ := out.View(0, n)

	err = dev.Queue().Run(func(r *accel.Recorder) {
		r.Dispatch(pipe, []accel.Binding{
			{Index: 0, Buffer: inView},
			{Index: 1, Buffer: outView},
		}, nil, accel.WorkgroupCount{X: n / 64})
	})
	if err != nil {
		log.Fatal(err)
	}

	got := make([]float32, n)
	if err := dev.Queue().ReadBuffer(out, 0, got); err != nil {
		log.Fatal(err)
	}
	fmt.Println(got[:4]) // [0 2 4 6]
}

That program runs on a GPU where there is one and on the CPU backend where there is not, and the kernel is the same either way.

AllowCPU is what makes the fallback legal, and leaving it out is the other useful default: accel.OpenBest(accel.Policy{}) fails on a machine with no GPU rather than quietly running on the CPU. Use that when a CPU run would be a misconfiguration you want to hear about — a benchmark, or a deployment that is supposed to have a device. A device you asked for is never silently substituted.

To pin the CPU backend for a test, accel.OpenCPU(accel.CPUOptions{}) opens it directly.

The tutorials take this apart one idea at a time: what the kernel subset allows, where memory comes from, how to record work once and replay it, and how to run the same code on a device you do not own.

Two layers, four packages

Package You get Use it for
accel buffers, kernels, command graphs, textures simulation, image and signal processing, anything with custom kernels
accel/tensor dtypes, shapes, operators, a computation graph inference — you never touch a bind group
accel/quant int8 weights with a per-block scale fitting a model in less memory
accel/kmath scalar math callable from a kernel inside kernel bodies

The tensor layer contains no backend-specific code. Everything it does, it does by asking the device layer.

Replay instead of re-issuing

One transformer layer is roughly a hundred operations, and a model has dozens of layers. Re-issuing thousands of commands per token, and asking for every intermediate allocation again, is most of the cost.

So you record work into a Graph and keep it:

rec := dev.NewRecorder()
rec.Dispatch(pipeline, bindings, nil, accel.WorkgroupCount{X: 1024})
g, err := rec.Build() // validates, plans memory, computes barriers

for range steps {
	g.Bind(nextInputs...)
	dev.Queue().Submit(g).Wait()
}

Build does the analysis once; every submission after that is a replay. It also works out where the barriers go, so you do not write one.

What it costs you: errors move. Under an immediate API a bad call fails at the call; here it fails at Build, possibly far from where you wrote it. A build error names the node, the binding slot and the numbers involved, so you can find the recording call — but it does not yet carry that call's source position.

What works today

You want to Today
Run a compute kernel on the CPU yes
Run the same kernel on a GPU yes, on Metal
Cross-compile with CGO_ENABLED=0 yes, every GOOS
Test without a GPU yes — the CPU backend is a full implementation, not a stub
Use every core on a machine with no GPU yes — a dispatch runs its workgroups at once, about 7.5x on eight cores, and the answer does not change with the core count
Use shared memory, barriers and atomics yes
Use subgroup reductions, votes, broadcasts, shuffles and scans yes, on both backends
Multiply matrices (tiled GEMM) yes, on both backends
Build a tensor graph and run inference yes — decode and prefill, with a KV cache
Use int8 quantized weights yes, against f32 or f16 activations
Mix widths in one product yes — f32 activations against f16 or int8 weights, with no cast between. A transformer's two operands are never the same width, and casting them was four dispatches per layer
Keep the KV cache at f16 yes — written, read, and paged. It halves the largest allocation a serving process has after the weights
Load a bf16 checkpoint yes — Cast widens bf16 to f32 exactly, which is a shift. No GEMM reads bf16, so convert on load
Sample a token (argmax, categorical, top-k, top-p) yes, batched — one row per sequence, with the random draw supplied so a token is reproducible
Run a whole sampling policy on device yes — one Sample call records penalties, temperature, top-k, top-p and the draw, so a decode step reads back a token rather than a vocabulary of logits. At a 152k vocabulary that is 4 bytes instead of 608 KB per token
Reproduce a generated sequence from a seed yes — the draw is a pure function of a seed and the token index, so there is no generator to copy, share or advance, and resuming a sequence costs nothing
Page a KV cache, and batch several sequences in one step yes
Draw triangles yes, on both backends: vertex and index buffers, per-vertex and per-instance attributes, uniforms, depth, blending, indexed and indirect draws
Render into a chosen pixel format yes — an attachment names a texture view, so it carries its own format, and sRGB converts on write and on read
Read a texture from a shader stage yes, integer texel fetch — so one pass can read what another drew, which is what deferred shading and shadow maps are. There is no sampler and there will not be one: a filtered sampler cannot be reproduced exactly by the CPU reference, so a stage that wants filtering builds it from fetches
Render a frame loop with acquire and present yes, headless — the pixels come back in a buffer
Present to a window yes on Metal, into a CAMetalLayer you own; accel does not create windows
Use Vulkan, D3D12, OpenGL or WebGPU not yet

Every "yes" has tests that fail without it and an end-to-end case through the public API. Every kernel in the corpus runs on both backends and the two are compared, most of them bit for bit.

That sentence is the standard this table is held to, and it has been wrong twice: two rows here claimed a capability whose kernels existed while no operator reached them, so nothing a caller could write would have exercised either. A kernel is not a capability, and this table is about what a caller can do.

When it fits, and when it does not

It fits if you want GPU compute from Go without cgo: cross-compilation that works, fast builds, no toolchain on the build machine, and a test suite that runs anywhere.

It does not fit if:

  • You need training. The tensor layer targets inference. There is no autodiff.
  • You are training on NVIDIA. No CUDA backend is planned for v0.
  • You need peak throughput. cgo-free rules out cuBLAS, cuDNN and GGML. Every kernel is written here, and it will not beat vendor libraries for a long time, possibly ever.
  • You want accel to open a window. It will not. You create the window with whatever toolkit you like and hand accel the layer; it owns everything from the swapchain inward. Only Metal has an on-screen path so far.
  • You expected wgpu. The submission model is deliberately different and the API does not aim to match it.

Documentation

Tutorials Eight short pages, one idea each. Start here
Architecture How it fits together, and the decisions behind it
Backend conventions Where GPU backends actually disagree. Useful even if you never use accel
Specs Internal design documents, full reasoning, open questions
Contributing What would help most right now

Testing

CGO_ENABLED=0 go build ./...
go test -race ./...

No GPU required, which is deliberate and should stay true.

[!NOTE] On a Mac, go test ./... skips the Metal tests when it finds no adapter, and says so only in the skip message. Set ACCEL_REQUIRE_METAL=1 to turn that skip into a failure, which is what CI does.

Contributing

The design is still soft, so an argument against one of its decisions is worth more than a patch right now. Every spec ends with the questions we have not resolved. See CONTRIBUTING.md.

Acknowledgements

The design draws on lessons from polyred, whose cgo-free GPU abstraction proved out the Go-to-shader approach and, just as usefully, made the mistakes that docs/conventions.md now records. ollama's ml package informed the graph-based execution model.

License

BSD-3-Clause © The golang.design Initiative Authors

Documentation

Overview

Package accel runs compute work on a GPU from Go, with no cgo.

You write a kernel in a subset of Go. It is compiled ahead of time by cmd/accel-kernel under go generate, and runs on whichever backend the machine has: Metal, or a pure-Go CPU device that produces the same results and needs no GPU at all.

go get golang.design/x/accel

This is the device layer. Its vocabulary is buffers, textures, workgroups and barriers; it knows nothing about tensors or meshes. For inference, use golang.design/x/accel/tensor, which is built on this package and deals in shapes and operators instead.

Getting started

Kernels live in a package of their own, because the generator type-checks the package it compiles and cannot run on a package that already refers to the symbol it is about to define. Given a generated kernels.ScaleKernel:

dev, err := accel.OpenCPU(accel.CPUOptions{})
if err != nil {
	log.Fatal(err)
}
defer dev.Close()

pipe, err := dev.NewComputePipeline(accel.ComputePipelineDescriptor{
	Kernel: &kernels.ScaleKernel,
})
if err != nil {
	log.Fatal(err)
}
defer pipe.Close()

err = dev.Queue().Run(func(r *accel.Recorder) {
	r.Dispatch(pipe, bindings, nil, accel.WorkgroupCount{X: 4})
})

The README has the whole program, buffers included.

To select a GPU instead, use OpenBest. It does not choose the CPU backend unless Policy.AllowCPU is set, so a caller who asked for a GPU and has none gets an error rather than a silent substitution.

What runs today

Two backends: Metal on darwin, and a pure-Go CPU backend everywhere. Enumerate reports what this machine has. BackendVulkan, BackendD3D12 and BackendOpenGL exist in the Backend enumeration and are not built.

On both backends: buffers, textures, memory pools, uploads and readbacks, compute dispatch both direct and indirect, command graphs, cooperative kernels with workgroup-shared memory and barriers, atomics, and the subgroup operations: reductions, votes, broadcasts, shuffles and prefix scans. The two agree exactly where the kernel's arithmetic is exact, and within a stated ceiling where it reaches a bounded primitive such as exp.

Uniform blocks are encoded by a generated std140 codec, so you supply a UniformBuffer and never write a padding offset. Texture data is tightly packed at this API's boundary — row r begins at r*width*bpp — so a readback sized width*height*bpp is right whatever pitch the device stores.

A kernel the Metal target cannot lower is refused by name at Device.NewComputePipeline rather than run on the CPU instead, so a device you selected is never quietly bypassed.

What does not

The API is under construction and will change.

Graphics runs on the CPU backend and on Metal, and the two are compared pixel by pixel: render pipelines, passes, vertex and index buffers, by-value stage parameters, depth, blending, indexed and indirect draws, and a surface that presents to a window the caller owns.

It is outside specs/036-documentation.md's freeze record. The change that made that necessary has landed: an attachment names a TextureView rather than a BufferView, so it carries a format, a mip level and an array layer, which a buffer view cannot express. specs/045-texture-attachments.md section 8 records what shipped and what is still owed -- mip levels above one, and the rejection of a subresource used as an attachment and read by a stage at once. Both are additive to what a caller writes today.

So this half is settling rather than settled, and it stays outside the freeze until section 8's ledger is empty.

A stage fetches a texel from a texture a pass binds (specs/032-stage-abi.md section 5), so one pass reads what an earlier pass drew -- which is what deferred shading, shadow maps and post-processing are. There is no sampler and there is not going to be one: a filtered sampler cannot be reproduced exactly by the CPU reference, so it is a feature the oracle could not check, and a stage that wants filtering builds it from fetches.

The model

Work is recorded into a Graph, which is immutable once built and can be submitted many times with its inputs rebound in between ([Graph.Rebind]). Validation, memory planning and barrier placement happen once, at Recorder.Build, not on every submission.

rec := dev.NewRecorder()
rec.Dispatch(pipeline, bindings, nil, WorkgroupCount{X: n})
g, err := rec.Build()   // validate, plan memory, compute barriers, lower
...
f := dev.Queue().Submit(g)
f.Wait()

You do not write barriers. Each node declares what it reads and writes, and the builder compares those as byte ranges, ordering only the pairs that really conflict. Graph.Edges, Graph.Hazards and Graph.Barriers report what it decided, so a graph that does not overlap can be explained rather than timed.

Buffers the builder owns share memory when their uses cannot overlap. Graph.Memory reports what that saved and Recorder.BuildNaive rebuilds the same graph with aliasing off, to isolate a suspected planning bug. Several graphs can share one TransientPool, sized to the largest rather than the sum, at the price that they cannot execute at the same time.

Memory comes from pools rather than one allocation per resource, because a model has thousands of tensors and drivers cap the number of allocations. A pool is exactly one device allocation and never grows.

The reasoning behind these choices is in specs/: 003 for the graph, 001 for memory, 000 for the decisions the rest follow from.

Index

Constants

View Source
const (
	StageVertex   = kernel.StageVertex
	StageFragment = kernel.StageFragment
)

The two graphics stages a Stage can be.

View Source
const (
	SubgroupBasic      = driver.SubgroupBasic
	SubgroupVote       = driver.SubgroupVote
	SubgroupBallot     = driver.SubgroupBallot
	SubgroupShuffle    = driver.SubgroupShuffle
	SubgroupArithmetic = driver.SubgroupArithmetic
)
View Source
const (
	FactorZero             = driver.FactorZero
	FactorOne              = driver.FactorOne
	FactorSrcColor         = driver.FactorSrcColor
	FactorOneMinusSrcColor = driver.FactorOneMinusSrcColor
	FactorSrcAlpha         = driver.FactorSrcAlpha
	FactorOneMinusSrcAlpha = driver.FactorOneMinusSrcAlpha
	FactorDstColor         = driver.FactorDstColor
	FactorOneMinusDstColor = driver.FactorOneMinusDstColor
	FactorDstAlpha         = driver.FactorDstAlpha
	FactorOneMinusDstAlpha = driver.FactorOneMinusDstAlpha
)

The blend factors. Named for what they scale by, not for where they appear: FactorSrcAlpha on the destination side is a legal and useful combination.

View Source
const (
	BlendAdd             = driver.BlendAdd
	BlendSubtract        = driver.BlendSubtract
	BlendReverseSubtract = driver.BlendReverseSubtract
	BlendMin             = driver.BlendMin
	BlendMax             = driver.BlendMax
)
View Source
const (
	LoadClear    = driver.LoadClear
	LoadKeep     = driver.LoadKeep
	LoadDontCare = driver.LoadDontCare

	StoreKeep    = driver.StoreKeep
	StoreDiscard = driver.StoreDiscard
)
View Source
const (
	// NativeMetalLayer is a `CAMetalLayer*` the caller created and attached to
	// a window. It must be created and resized on the main thread.
	NativeMetalLayer = driver.NativeMetalLayer

	// NativeNSView is an `NSView*`. accel makes the layer, and that call must
	// itself be on the main thread.
	NativeNSView = driver.NativeNSView
)

WriteAll is every channel, and is what a zero ColorWriteMask means.

Variables

View Source
var (
	ErrOutOfDeviceMemory = errors.New("accel: out of device memory")
	ErrFragmented        = errors.New("accel: pool has space but not contiguous space")
	ErrAlignment         = errors.New("accel: alignment violation")
	ErrUsage             = errors.New("accel: usage violation")
	ErrLifetime          = errors.New("accel: lifetime violation")
	ErrFormat            = errors.New("accel: format not usable this way")

	// ErrNoAdapter reports that nothing was enumerated at all.
	//
	// Distinct from ErrPolicy because the two need different fixes and a caller
	// branches on which: nothing enumerated means no driver, no device, or a
	// build without the backend, and no policy change helps.
	ErrNoAdapter = errors.New("accel: no adapter was enumerated")

	// ErrPolicy reports that adapters were enumerated and the policy excluded
	// every one.
	//
	// The fix is the policy, and [SelectionReport] says which clause rejected
	// which adapter -- so a caller can widen exactly the one that cost them the
	// device rather than dropping the policy wholesale.
	ErrPolicy = errors.New("accel: no adapter satisfied the policy")

	// ErrGraphInFlight reports an attempt to rebind or resubmit a graph while a
	// submission of it is running. A graph's transients are one pool, so two
	// overlapping submissions would write each other's intermediates, and a
	// rebind between them races on which submission sees it. Build one graph per
	// concurrent user. See specs/003-command-graph.md.
	ErrGraphInFlight = errors.New("accel: a submission of this graph is already in flight")

	// ErrRebindOverlap reports a binding update in which two resources supplied
	// to one graph name overlapping bytes and at least one of them is written
	// somewhere in the graph.
	//
	// It cannot be a build-time error: hazards are inferred against the slot,
	// because a slot's eventual resource is unknown then. Two slots resolving to
	// the same bytes means the builder may have omitted an edge between nodes far
	// apart in the graph, which is a missing barrier and therefore a race. See
	// specs/003-command-graph.md, check V24.
	ErrRebindOverlap = errors.New("accel: bound resources overlap where the graph assumed they did not")

	// ErrDeviceLost reports terminal device loss. It is not recoverable: every
	// subsequent call on the device and on every resource under it reports it,
	// and every outstanding fence is signalled with it so that nothing waits
	// forever. Recovery is a full rebuild from Enumerate. See
	// specs/001-device-resources.md section 7.4.
	ErrDeviceLost = driver.ErrDeviceLost
)

Sentinels for errors.Is. Each typed error in this package unwraps to one of these, so a caller can branch on the class without depending on the struct.

Every failure carries the numbers needed to fix it. An error saying only that an allocation failed, or only that a type did not match, cannot be acted on, and one of those in this package is a defect rather than a terse style. See specs/001-device-resources.md section 9.

View Source
var ErrAcquireTimeout = errors.New("accel: acquiring a frame timed out")

ErrAcquireTimeout reports that no image became available in time.

View Source
var ErrNoPresent = driver.ErrNoPresent

ErrNoPresent reports that a device has no on-screen path.

Reported rather than discovered, which is decision 6: a caller asks and is told, instead of finding out when a frame does not appear.

View Source
var ErrNotImplemented = errors.New("accel: not implemented (design stage)")

ErrNotImplemented marks a declaration that exists so its shape is fixed but whose implementation has not arrived.

It has no user today: its only one was the sampler family, withdrawn with the rest of the unbuilt graphics surface. It stays because specs/032-stage-abi.md and specs/033-render-api.md will give it users again, and because the rule it carries is worth keeping — a declaration that exists only for its shape **panics** rather than returning, so a caller cannot write a plausible handler for a path that can never succeed.

View Source
var ErrSurfaceOutOfDate = errors.New("accel: the surface is out of date")

ErrSurfaceOutOfDate reports that a surface's images no longer match its configuration, so the frame loop must resize and rebuild.

An error value rather than a silent reallocation: a graph is built against a specific extent, so reallocating underneath it would leave the graph describing an image that no longer exists, and the failure would appear later as a size mismatch with no cause attached.

View Source
var ErrUnsupported = errors.New("accel: unsupported by this device")

ErrUnsupported reports that a device cannot perform an operation because it lacks a capability. The error names the capability and the device: absence is always explicit, never a silent wrong result. See specs/000-decisions.md decision 6.

Functions

func AddF32

func AddF32(b []float32, i uint32, v float32) float32

AddF32 is a capability, not a guarantee: see CapAtomicFloatAddStorage. A device without it refuses the kernel at pipeline creation rather than producing a wrong sum.

func AddI32

func AddI32(b []int32, i uint32, v int32) int32

func AddU32

func AddU32(b []uint32, i uint32, v uint32) uint32

Atomic operations, as a kernel author writes them.

Every one returns the value the location held *before* the operation, which is what makes an atomic add usable as a ticket dispenser rather than only as a counter. See specs/020-cooperative-atomics.md.

func AndU32

func AndU32(b []uint32, i uint32, v uint32) uint32

func CompareExchangeI32

func CompareExchangeI32(b []int32, i uint32, cmp, v int32) int32

func CompareExchangeU32

func CompareExchangeU32(b []uint32, i, cmp, v uint32) uint32

CompareExchangeU32 stores v only if the location holds cmp, and returns what it held either way — so a caller learns whether the swap happened by comparing the result with cmp rather than by a second read that could race.

func ExchangeI32

func ExchangeI32(b []int32, i uint32, v int32) int32

func ExchangeU32

func ExchangeU32(b []uint32, i uint32, v uint32) uint32

ExchangeU32 and ExchangeI32 store a value and return the previous one.

func MaxI32

func MaxI32(b []int32, i uint32, v int32) int32

func MaxU32

func MaxU32(b []uint32, i uint32, v uint32) uint32

func MinI32

func MinI32(b []int32, i uint32, v int32) int32

func MinU32

func MinU32(b []uint32, i uint32, v uint32) uint32

func OrU32

func OrU32(b []uint32, i uint32, v uint32) uint32

func SubI32

func SubI32(b []int32, i uint32, v int32) int32

func SubU32

func SubU32(b []uint32, i uint32, v uint32) uint32

func XorU32

func XorU32(b []uint32, i uint32, v uint32) uint32

Types

type Access

type Access int

Access is how a binding is used by a kernel. The graph builder infers dependency edges and barriers from declared access, so this must be accurate: under-declaring is what turns a missing dependency into a race.

const (
	AccessRead Access = iota
	AccessWrite
	AccessReadWrite
)

func (Access) String

func (a Access) String() string

type AdapterID

type AdapterID struct {
	// contains filtered or unexported fields
}

AdapterID identifies one enumerated adapter for this process. It is opaque, comparable, stable across repeated enumerations while the adapter is present, and intentionally not serializable.

func (AdapterID) String

func (id AdapterID) String() string

String is the adapter's stable identity, as hex.

Exported because a layer above this one needs it and cannot reach the token: specs/007-tensor-layer.md requires a plan cache's key to include "the device identity", and without this the best a caller could do is the device's name, which two identical GPUs in one machine share.

Stable within a process and comparable across enumerations, which is what driver.Adapter promises of the token underneath. Not stable across machines or driver versions, and not meant to be: it is an identity, not a fingerprint.

type AdapterRejection

type AdapterRejection struct {
	ID     AdapterID
	Reason string
}

AdapterRejection explains why automatic selection skipped one adapter.

type AlignmentError

type AlignmentError struct {
	What     string // "view offset", "copy offset", "row pitch"
	Resource string
	Offset   int
	Required int
	Source   string // the Limits field that imposed it
}

AlignmentError reports an offset or pitch that does not meet a device requirement.

Required always comes from Limits and Source names the field it came from, never a constant, because the number differs by two orders of magnitude across devices of one backend and a caller who hard-codes what worked locally has written a bug that only appears elsewhere.

func (*AlignmentError) Error

func (e *AlignmentError) Error() string

func (*AlignmentError) Unwrap

func (e *AlignmentError) Unwrap() error

type AllocError

type AllocError struct {
	Label       string
	Pool        string
	Kind        MemoryKind
	Requested   int
	Alignment   int
	Free        int
	LargestFree int
	PoolSize    int
}

AllocError reports a failed suballocation.

Free and LargestFree together are what distinguish exhaustion from fragmentation. They are different problems with different fixes and a bare failure tells the caller nothing about which one they have: a pool with plenty of free space and no contiguous run of it is behaving exactly as specs/001-device-resources.md section 5.3 says a non-compacting allocator does, and the fix is separating pools by lifetime class rather than retrying.

func (*AllocError) Error

func (e *AllocError) Error() string

func (*AllocError) Unwrap

func (e *AllocError) Unwrap() error

Unwrap reports fragmentation and exhaustion as distinct classes, since a caller who can react at all reacts to them differently.

type AttrFormat

type AttrFormat uint8

AttrFormat is the type of one vertex attribute in memory.

Only the float32 vector widths exist, because a stage's attribute parameter is [N]float32 and nothing else can receive a fetch. Normalized integer formats convert on fetch, which is a conversion the CPU rasterizer would have to match bit for bit to stay an oracle; they arrive with a spec that states the rounding.

const (
	AttrInvalid AttrFormat = iota
	AttrFloat32
	AttrFloat32x2
	AttrFloat32x3
	AttrFloat32x4
)

func (AttrFormat) Components

func (f AttrFormat) Components() int

Components is how many float32 values the format holds, and 0 if it holds none — which is what makes an unset AttrFormat a refusal rather than a single-component fetch.

func (AttrFormat) Size

func (f AttrFormat) Size() int

Size is the format's size in bytes.

func (AttrFormat) String

func (f AttrFormat) String() string

type BFloat16

type BFloat16 = kernel.BFloat16

Narrow storage types.

A kernel parameter of []accel.Float16 is a binding of 16-bit floats. The types carry no arithmetic operators at all, which is not an omission: it is what forces f.F32() on the way in and ToFloat16 on the way out, so f32 accumulation is the only thing that compiles rather than a convention an author has to remember. Native narrow arithmetic is a separate capability (spec 002's CapF16Arithmetic) and arrives as explicit intrinsics later.

They are not named F16 and BF16 because those are the DType constants for the same formats. A dtype is metadata a descriptor carries and a storage type is what a parameter is made of; both wanted the short name and the constants shipped first.

func BFloat16FromBits

func BFloat16FromBits(b uint16) BFloat16

func ToBFloat16

func ToBFloat16(x float32) BFloat16

ToBFloat16 converts an f32 to bf16 storage, rounding to nearest with ties to even. Truncating the low bits instead would be round-toward-zero, which biases every value in one direction and compounds over a reduction.

type Backend

type Backend int

Backend identifies a device implementation.

const (
	// BackendCPU is a pure-Go implementation. It is a first-class backend and the
	// correctness oracle every other backend is verified against, not a fallback.
	// It is always available on every platform. See specs/000-decisions.md
	// decision 3.
	BackendCPU Backend = iota

	BackendMetal
	BackendVulkan
	BackendD3D12
	BackendOpenGL
)

func (Backend) String

func (b Backend) String() string

String returns the backend's name.

type Binding

type Binding struct {
	// Index is the entry in the pipeline's binding layout this binds to.
	//
	// The pipeline's layout, and nothing else. It used to name the kernel's
	// by-value list instead whenever Uniform was set, so one dispatch could
	// carry two entries both spelled Index: 0 meaning different things — and
	// neither matched the authored parameter position a reader was looking at.
	// By-value parameters are [UniformValue] now, and have their own argument.
	Index int

	Buffer  BufferView
	Texture *Texture

	// Slot supplies the resource before submission instead of at record time. Its
	// zero value is not a slot, so a Binding that set none of the three is
	// rejected rather than silently referring to the first one.
	Slot Slot
}

Binding binds one resource to one entry of a pipeline's binding layout.

A binding is what varies between submissions of the same Graph: pointing it at a different resource of the same type, dtype, and access is allowed, and anything more than that is a different graph.

Exactly one of Buffer, Texture, Sampler and Slot is set, and setting none or several is a validation error naming the binding. Slot is the indirection that makes a graph replayable: naming a Slot instead of a resource says the resource arrives before submission rather than at record time, which is how a swapchain image that does not exist yet, or one sequence's cache out of many, reaches a recorded node.

Two vocabularies meet in this struct and they are not the same thing. Index is an entry in the *pipeline's* binding layout, declared by BindingSlot and fixed when the pipeline is created. Slot is a *graph's* rebindable input, declared by Recorder.Slot and bound per submission. A pipeline's binding is where a resource is used; a graph's slot is where it comes from.

type BindingKind

type BindingKind int

BindingKind is what sort of resource fills a slot.

const (
	BindingStorageBuffer BindingKind = iota
	BindingUniformBuffer
	BindingSampledTexture
	BindingStorageTexture
	BindingSampler

	// BindingAttachment is a texture a render pass writes as a colour or depth
	// attachment. An attachment is not bound to a pipeline slot the way the kinds
	// above are, and it is in this enum for one reason: a graph slot has to be able
	// to name one, which is how the swapchain image reaches a recorded pass
	// (specs/005-graphics.md).
	BindingAttachment
)

type BindingSlot

type BindingSlot struct {
	Index  int
	Kind   BindingKind
	Access Access

	// DType constrains storage buffer slots. A resource bound to this slot must
	// match, which is checked when the graph is built.
	DType DType

	Name string
}

BindingSlot declares one entry of a pipeline's binding layout.

type BlendFactor

type BlendFactor = driver.BlendFactor

BlendFactor scales one side of a blend.

type BlendOp

type BlendOp = driver.BlendOp

BlendOp combines the two scaled sides.

type BlendState

type BlendState struct {
	// Enabled is what separates blending from replacing. A zero BlendState with
	// Enabled true would multiply everything by zero, so the flag is not
	// inferred from the factors.
	Enabled bool

	SrcColor, DstColor BlendFactor
	ColorOp            BlendOp

	SrcAlpha, DstAlpha BlendFactor
	AlphaOp            BlendOp
}

BlendState is fixed-function attachment read-modify-write.

It is fixed at pipeline creation because D3D12, Vulkan and Metal all take it as a compile-time input: changing it per draw would mean a backend that silently recompiles mid-frame, which is a notorious stutter source, or one that fails at a call the API said was legal.

Colour and alpha have separate factors and operations because premultiplied compositing needs them to differ: the usual "over" blend is source alpha on colour and one on alpha, and a single pair cannot say that.

func AlphaBlend

func AlphaBlend() BlendState

AlphaBlend is the "over" operator, which is what most callers mean by blending: source colour weighted by source alpha, over what is already there.

type Buffer

type Buffer struct {
	// contains filtered or unexported fields
}

Buffer is a typed, sized range of device memory.

func (*Buffer) Access

func (b *Buffer) Access(fn func([]byte) error) error

Access hands the buffer's host mapping to fn, for the duration of the call.

What this is for

A model loader reads a shard, converts it, and uploads it — and without this it holds all three at once: the shard, a fully converted host tensor, and the device allocation. On a multi-gigabyte checkpoint that middle term is the largest transient allocation in the process, and it exists only because the converted bytes have nowhere to go but a slice of the caller's own.

With this the loader converts *into* the destination. The middle allocation does not shrink; it does not exist.

On unified-memory hardware — which Capabilities.SharedMemoryKind reports — the mapping *is* device memory, so a MemoryShared pool makes the upload free rather than fast. On a discrete device the mapping is the staging buffer, so the copy that remains is the one the hardware requires.

Why a callback and not a returned slice

A returned slice is a promise about a lifetime accel cannot see. The pool can be closed, the device can be lost, and a slice that outlived either would be a use-after-free whose symptom is a plausible tensor. Scoped to the call, the borrow is bounded by something the compiler and the reader can both see, and the buffer's lifetime stays accel's.

The slice is exactly this buffer's bytes: writing past its end is not possible, and writing to it is visible to the device without a further call.

When it refuses

Memory that is not host-visible has no mapping to hand out. That is a property of the pool's MemoryKind, reported rather than discovered: MemoryDevice refuses here even on the CPU backend, where the memory physically could be mapped, because a rule only one backend enforces is a rule that fails in production.

Do not retain the slice. Do not call this while a graph reading this buffer is in flight; the bytes are the device's during a submission.

func (*Buffer) Bytes

func (b *Buffer) Bytes() int

Bytes reports the buffer's size in bytes, which is DType.Size() times Count. It is the caller's number and is never rounded; the pool's allocation size, which includes alignment padding, shows up in PoolStats instead.

func (*Buffer) Close

func (b *Buffer) Close() error

Close releases the buffer.

Closing while something using this buffer is still outstanding is reported rather than crashing: the implementation keeps a resource alive until every hold on it is gone, so the memory comes back later and the caller learns that their teardown ordering was wrong.

func (*Buffer) Count

func (b *Buffer) Count() int

Count reports the buffer's element count.

func (*Buffer) DType

func (b *Buffer) DType() DType

DType reports the buffer's element type.

func (*Buffer) Usage

func (b *Buffer) Usage() BufferUsage

Usage reports what the buffer declared at creation.

func (*Buffer) View

func (b *Buffer) View(offset, count int) (BufferView, error)

View returns a sub-range of the buffer as a BufferView.

Views are what let a caller slice a KV cache or address one attention head without copying. A view does not own memory and must not outlive its buffer.

Offset and count are in elements of the buffer's dtype. Creating a view is not an operation the device sees and carries no alignment requirement of its own: the alignment a view owes depends on what it is *for*, so it is checked where the view reaches a binding or a copy rather than here. See specs/001-device-resources.md section 6.1.

func (*Buffer) ViewAs

func (b *Buffer) ViewAs(d DType, offset, count int) (BufferView, error)

ViewAs is Buffer.View with a reinterpreted element type. It reports an error if the byte ranges do not divide evenly at the new dtype.

Offset and count are in elements of d, the *new* dtype, not of the buffer's. Anything else would make ViewAs require the caller to do the arithmetic ViewAs exists to do.

It reinterprets and never converts. The bytes are unchanged, so a u32 view of an f32 buffer sees the IEEE 754 binary32 encoding of each value: that is what lets a kernel read a quantized plane as u8 and a scale plane as bit-packed u16, and what lets a debug path dump any buffer as u8. Reinterpreting across widths is exact byte-wise and is only meaningful because accel requires the host and device to share byte order, which every supported platform does.

type BufferDescriptor

type BufferDescriptor struct {
	// DType and Count give the buffer's element type and length. Size in bytes is
	// DType.Size() * Count.
	DType DType
	Count int

	Usage BufferUsage

	// Label appears in validation errors and in backend debug tooling. Worth
	// setting: a build error naming "kv_cache_layer_7" beats one naming a pointer.
	Label string
}

BufferDescriptor describes a buffer to create.

type BufferUsage

type BufferUsage uint32

BufferUsage declares how a buffer will be used.

Usage is declared at creation because backends need it: it decides the underlying allocation flags. Using a buffer in a way it did not declare is a validation error when the graph is built, not undefined behaviour when it runs.

const (
	BufferStorage BufferUsage = 1 << iota
	BufferUniform
	BufferIndex
	BufferVertex
	BufferIndirect
	BufferCopySrc
	BufferCopyDst
)

The bits are named for the resource they apply to, so BufferCopyDst and TextureCopyDst are distinguishable at a glance. They used to be Usage* and Texture*, which put UsageCopyDst and TextureCopyDst side by side meaning the same thing for different resources.

func (BufferUsage) String

func (u BufferUsage) String() string

String returns the usage set as a |-separated list, which is how validation errors name what a buffer declared against what it needs.

type BufferView

type BufferView struct {
	Buffer *Buffer
	DType  DType
	Offset int
	Count  int
}

BufferView is a sub-range of a Buffer, possibly at a different dtype. It is a value: copying it is fine and does not copy any memory.

type CPUMode

type CPUMode int

CPUMode selects the CPU backend's reported capability and limit profile.

const (
	CPUDeveloper CPUMode = iota
	CPUStrict
	CPUMimic
)

type CPUOptions

type CPUOptions struct {
	Mode          CPUMode
	StrictTargets []Backend
	Mimic         *DeviceProfile
	SubgroupSize  int
	ShuffleSeed   uint64

	// LoseAtSubmission marks the device lost when it reaches this submission,
	// counting from one. Zero never loses.
	//
	// It is fault injection, and it is here rather than in a test helper because
	// specs/001-device-resources.md section 7.4 requires it: the CPU backend
	// cannot lose a device and Metal rarely does, so without a way to ask for it
	// the whole terminal-loss path is code nobody runs until a caller's driver
	// restarts in production. It has no effect on any other backend.
	LoseAtSubmission int
}

CPUOptions configures the CPU oracle. StrictTargets is required in strict mode; Mimic is required in mimic mode.

type Capabilities

type Capabilities struct {
	// Subgroups reports whether subgroup operations exist. Numeric lane bounds
	// live in [Limits].
	Subgroups bool

	// SubgroupOps reports which subgroup operations are present. Vulkan exposes
	// these as an independent feature set rather than one flag, so a device can
	// have ballot without shuffle.
	SubgroupOps SubgroupOpSet

	// F16Arithmetic and BF16Arithmetic report native narrow arithmetic. These are
	// separate from being able to *store* those dtypes, which several backends do
	// without being able to compute in them.
	F16Arithmetic  bool
	BF16Arithmetic bool
	I8DotProduct   bool

	// AtomicFloatAddStorage and AtomicFloatAddShared are separate because backends
	// genuinely differ between them: a device can have the storage form and not
	// the shared one. They are called out at all because atomic float add is what
	// people reach for when writing a reduction, and because it makes a reduction
	// non-deterministic (the hardware picks the accumulation order), so a test
	// asserting an exact total is wrong for floats even where it is right for
	// integers.
	AtomicFloatAddStorage bool
	AtomicFloatAddShared  bool

	// DenormF32Preserved and DenormF16Preserved report whether denormals survive
	// rather than being flushed to zero, and InfNaNProduced whether infinities and
	// NaNs are generated rather than being treated as undefined. All three vary by
	// backend and all three change numeric results, so they are queryable rather
	// than assumed.
	DenormF32Preserved bool
	DenormF16Preserved bool
	InfNaNProduced     bool
	ContractionControl bool

	// SharedMemoryKind reports whether MemoryShared is real on this device, which
	// is true on unified-memory hardware and removes a staging copy.
	SharedMemoryKind bool

	// Graphics reports whether this device can rasterize. A compute-only backend
	// is legitimate.
	Graphics                bool
	Presentation            bool
	Multisampling           bool
	RasterizerOrderedAccess bool
	IndirectDispatch        bool

	// NativeGraphReplay reports whether recorded graphs lower to a native
	// replayable object (Vulkan secondary command buffers, D3D12 bundles, Metal
	// indirect command buffers) rather than being replayed in software. Replay
	// works either way; this says how much it saves.
	NativeGraphReplay bool
}

Capabilities is what a device can actually do.

It is queried before use so an absent feature is a typed answer rather than a dispatch-time failure. A graph requiring something absent fails when it is built, with an error naming the capability and the device. See specs/000-decisions.md decision 6.

func (Capabilities) Has

func (c Capabilities) Has(want Capability) bool

Has reports whether this device offers every capability in want.

if !dev.Capabilities().Has(accel.CapAtomicFloatAddStorage) {
	// pick another kernel, or another device
}

Which one is missing is often the interesting half; Device.Missing answers that against a whole Requirements.

func (Capabilities) Set

func (c Capabilities) Set() Capability

Set is what a device offers, expressed as the Capability set a kernel requires — so the two halves of the same question can be compared.

A device reports Capabilities, a struct of flags plus a SubgroupOpSet. A kernel requires a Capability bitmask, and so does Policy.Require. Without this bridge a caller holding a DeviceInfo could not answer "does this device satisfy what I am about to require" except by reimplementing the mapping, including the part that is easy to miss: every subgroup bit is gated on Subgroups as well as on its own op bit.

type Capability

type Capability uint32

Capability names something a kernel can require and a device may lack. It is a requirement set, not a feature list: the values here are exactly the ones a kernel body can imply, and they are inferred from that body rather than declared by its author, because a declaration can be forgotten.

const (
	CapSubgroupBasic Capability = 1 << iota
	CapSubgroupVote
	CapSubgroupBallot
	CapSubgroupShuffle
	CapSubgroupArithmetic
	CapF16Arithmetic
	CapBF16Arithmetic
	CapAtomicFloatAddStorage
	CapAtomicFloatAddShared
	CapI8DotProduct
)

func (Capability) String

func (c Capability) String() string

type Clip

type Clip = kernel.Clip

Clip is the clip-space position a vertex stage returns, with z in [-w, w].

One convention, presented to every backend: that becomes NDC z in [-1, 1], and the backends whose native range is [0, 1] fold the remap into emitted code. A caller never adjusts a projection matrix for the backend. The depth attachment stores window depth in [0, 1], which is a different range that clears and compares use.

type ColorAttachment

type ColorAttachment struct {
	// View names the attachment directly, and Slot names it through a
	// rebindable binding point. Exactly one is set, the way exactly one of
	// [Binding]'s two is: a swapchain image cannot be named at record time,
	// because which one the frame gets is decided at acquire.
	//
	// A [TextureView] and not a [BufferView], because the view is what carries
	// the format the pass writes through and the subresource
	// specs/033-render-api.md section 3.3 compares. See
	// [RenderPassDescriptor].
	View TextureView
	Slot Slot

	Load  LoadOp
	Clear [4]float32
	Store StoreOp
}

ColorAttachment is one colour target of a render pass.

type ColorTargetState

type ColorTargetState struct {
	Format Format
	Mask   ColorWriteMask

	// Blend combines a fragment with what the attachment already holds. The
	// zero value does not blend, which is what makes a target that says nothing
	// about blending replace rather than accumulate.
	Blend BlendState
}

ColorTargetState is one colour attachment's compiled state.

Blend and the write mask are here, on the pipeline, rather than on the attachment: a pass holds one set of attachments and many draws with different pipelines, so putting them on the attachment would mean rewriting the attachment object before every draw. Every backend agrees — Vulkan puts them in an array on the pipeline, Metal on the render pipeline's colour attachment descriptor, D3D12 in the blend description's render-target array.

type ColorWriteMask

type ColorWriteMask uint8

ColorWriteMask is which channels of an attachment a fragment may write. The zero value writes every channel, because a target nobody configured should be written rather than silently dropped.

const (
	WriteRed ColorWriteMask = 1 << iota
	WriteGreen
	WriteBlue
	WriteAlpha
)

type CompareFunc

type CompareFunc uint8

CompareFunc is a depth or stencil comparison.

const (
	CompareNever CompareFunc = iota
	CompareLess
	CompareEqual
	CompareLessEqual
	CompareGreater
	CompareNotEqual
	CompareGreaterEqual
	CompareAlways
)

type ComputePipeline

type ComputePipeline struct {
	// contains filtered or unexported fields
}

ComputePipeline is a kernel compiled for a device with a fixed workgroup size.

func (*ComputePipeline) Close

func (p *ComputePipeline) Close() error

Close releases the pipeline. It fails while a graph still names it, because a graph submitted afterwards would dispatch a kernel whose pipeline is gone.

func (*ComputePipeline) Kernel

func (p *ComputePipeline) Kernel() *Kernel

Kernel is the compiled kernel this pipeline was created from. It is the source of truth for everything static: workgroup extent, binding layout, inferred access, and requirements.

func (*ComputePipeline) Workgroups

func (p *ComputePipeline) Workgroups(n int) WorkgroupCount

Workgroups is how many workgroups cover n invocations of this pipeline's kernel along X, with Y and Z at one.

Why this is a method and not arithmetic a caller writes

The arithmetic is one line -- ceiling division by the kernel's workgroup size -- and every caller writing that line has to know the size, which is the kernel's and not theirs. So the line is written wherever the size is edited, and the two drift silently: too few workgroups leaves a tail of the data untouched, which looks like a kernel bug at the boundary, and too many runs invocations past the end, which the kernel is supposed to guard but that guard is exactly what a caller forgets to check.

It is deliberately not a thread count. WorkgroupCount counts workgroups because a thread count makes the workgroup size invisible, which is how a predecessor ended up dispatching one thread per workgroup.

r.Dispatch(pipe, binds, nil, pipe.Workgroups(len(data)))

func (*ComputePipeline) WorkgroupsFor

func (p *ComputePipeline) WorkgroupsFor(x, y, z int) WorkgroupCount

WorkgroupsFor is ComputePipeline.Workgroups over three dimensions.

A zero or negative extent in any axis yields a zero count in that axis, which is the specified skip: specs/003-command-graph.md makes a zero in any dimension a dispatch of nothing rather than an error, so covering "no work" produces "no work" rather than one workgroup that reads past the end.

type ComputePipelineDescriptor

type ComputePipelineDescriptor struct {
	// Kernel is the compiled kernel. See the kernel authoring package for how one
	// is produced from Go source. It owns the workgroup, shared-memory, binding,
	// access, and requirement metadata.
	Kernel *Kernel

	Label string
}

ComputePipelineDescriptor describes a compute pipeline to create.

type CopyStats

type CopyStats struct {
	Bytes    int
	Repacked bool // an intermediate padded-pitch buffer is used
	RowPitch int  // the pitch the backend uses on the device side
}

CopyStats reports what a transfer does.

It is a plan-time fact rather than a measurement: a backend knows its own pitch rules before anything executes, so a recorded copy carries this from Graph.NodeStats as soon as the graph is built. The immediate transfer path has no node, so its repacks are counted in QueueStats instead of returned per call, which keeps an observability concern out of two signatures every caller touches.

type CullMode

type CullMode uint8

CullMode is which faces are discarded.

const (
	CullNone CullMode = iota
	CullFront
	CullBack
)

type DType

type DType int

DType is the element type of a buffer.

Arithmetic inside a kernel is f32 unless the kernel asks otherwise: narrow types are storage formats that convert on load and store. That default is a correctness choice, not a convenience, since accumulating a long dot product in f16 loses accuracy badly. See specs/002-compute-model.md.

const (
	F32 DType = iota

	// F16 and BF16 storage are universal. Native arithmetic on them is separately
	// gated by CapF16Arithmetic and CapBF16Arithmetic. BF16 trades precision for
	// the range of f32 at the same width.
	F16
	BF16

	// I32 and U32 are universal. Atomics operate on these.
	I32
	U32

	// I8 and U8 are storage and conversion types, for quantized weights.
	I8
	U8
)

func (DType) Size

func (d DType) Size() int

Size returns the dtype's size in bytes.

func (DType) String

func (d DType) String() string

String returns the dtype's name.

type DepthAttachment

type DepthAttachment struct {
	View TextureView
	Slot Slot

	Load  LoadOp
	Clear float32
	Store StoreOp
}

DepthAttachment is a pass's depth target.

type DepthStencilState

type DepthStencilState struct {
	Format  Format
	Test    bool
	Write   bool
	Compare CompareFunc
}

DepthStencilState is the depth test and write.

Test and Write are separate because read-only depth — test on, write off — is a real configuration: it is how a second pass shades exactly the surfaces the geometry pass kept.

type Device

type Device struct {
	// contains filtered or unexported fields
}

Device is an opened accelerator.

A Device is safe for concurrent use. A Recorder obtained from it is not; see Device.NewRecorder.

func OpenBest

func OpenBest(p Policy) (*Device, error)

OpenBest opens the best available device under an explicit policy. Unlike OpenDevice this is a request to choose, and the policy is what it chooses by: it fails rather than descending into something the caller did not sanction.

func OpenCPU

func OpenCPU(opts CPUOptions) (*Device, error)

OpenCPU opens the CPU backend with an explicit oracle profile.

func OpenDevice

func OpenDevice(id AdapterID) (*Device, error)

OpenDevice opens exactly the enumerated adapter id names.

It never falls back to another adapter or backend. Use OpenBest to ask for automatic selection explicitly.

func (*Device) AlignedRowPitch

func (d *Device) AlignedRowPitch(f Format, width int) int

AlignedRowPitch returns the row pitch a texture-to-buffer copy of the given format and width will use on this device.

Why a caller does not need this

specs/001-device-resources.md section 4.2 guarantees that at the accel API boundary texture data is tightly packed: row r begins at r*width*bpp with no padding. A caller sizes a readback as width*height*bpp and is always right, and accel pays for any repack.

This exists so a caller sizing its *own* staging buffer can size it right the first time, and so a caller on a performance path can see the repack coming rather than measuring it.

func (*Device) AlignedRowPitchRepacks

func (d *Device) AlignedRowPitchRepacks(f Format, width int) bool

AlignedRowPitchRepacks reports whether a copy of this shape needs an intermediate padded buffer on this device.

Exposed alongside Device.AlignedRowPitch for the same reason: a caller on a performance path can see the extra full-size copy coming rather than measuring it.

func (*Device) Capabilities

func (d *Device) Capabilities() Capabilities

Capabilities reports the optional features this device has.

The sibling of Device.Limits, and it exists because its absence was a trap: a reader who learned dev.Limits() reasonably wrote dev.Capabilities() and got a compile error, with dev.Info().Capabilities the undiscoverable answer.

func (*Device) Close

func (d *Device) Close() error

Close releases the device. Resources created from it must be closed first.

Closing is ordered rather than recursive: a device with live pools reports a *LifetimeError counting them and frees nothing. The API could close children on the caller's behalf and deliberately does not, because a caller who closed a device out from under a pool they still hold has a bug, and turning that bug into a silent success makes the next use of the pool undefined instead of reported.

The implicit pool behind Device.NewBuffer is the exception, because the caller never named it: it has no handle to close, so the device owns it.

func (*Device) FormatInfo

func (d *Device) FormatInfo(f Format) FormatInfo

FormatInfo reports what this device can do with a format.

An unsupported format reports a zero FormatInfo and no error: absence is a capability answer rather than a failure, which is decision 6 applied to formats. A caller checks the fields and picks a different format; it does not discover the answer at a draw call.

func (*Device) Info

func (d *Device) Info() DeviceInfo

Info reports what this device is and what it can do.

func (*Device) Limits

func (d *Device) Limits() Limits

Limits reports the device's numeric bounds.

func (*Device) Missing

func (d *Device) Missing(r Requirements) []Unmet

Missing reports every feature or numeric requirement this device does not meet, in stable order. It consults both Capabilities and Limits.

Stable order matters: a caller printing the result should get the same text twice, and a test asserting on it should not depend on map iteration.

func (*Device) NewBuffer

func (d *Device) NewBuffer(desc BufferDescriptor) (*Buffer, error)

NewBuffer allocates a single buffer from an implicit pool. It is a convenience for callers with a handful of buffers; anything allocating at scale should use Device.NewPool.

The implicit pool is the one thing here that grows, and it grows the only way a device allocation can: by adding another one. It is a *set* of fixed-size blocks, and when none can serve a request the set adds one. Nothing moves and no address is invalidated, which is why this is expressible where growing an explicit pool is not. See specs/001-device-resources.md section 5.5.

func (*Device) NewComputePipeline

func (d *Device) NewComputePipeline(desc ComputePipelineDescriptor) (*ComputePipeline, error)

NewComputePipeline compiles a kernel into a pipeline.

Workgroup size is fixed here rather than at dispatch, because backends need it at compile time: it appears in the GLSL layout qualifier and in Metal's threads-per-threadgroup. See specs/002-compute-model.md.

func (*Device) NewHeadlessSurface

func (d *Device) NewHeadlessSurface(desc SurfaceDescriptor) (*Surface, error)

NewHeadlessSurface makes a surface with no window.

Not a mock. It has the same generation counter, the same acquire timeout, the same rotation and the same out-of-date behaviour as a windowed one, which is what lets the whole frame path run in CI with no display. A mock would agree with the interface and disagree with the state machine — and the state machine is what a frame loop is.

func (*Device) NewPool

func (d *Device) NewPool(desc PoolDescriptor) (*Pool, error)

NewPool allocates a memory pool of the given kind and size, from which buffers are suballocated. See specs/001-device-resources.md for why allocation is pooled rather than per resource.

It is [Device.NewPoolWith] with the general-purpose policy. A pool never grows: a pool is one device allocation, no backend can resize one in place, and moving it would invalidate every address already handed out.

func (*Device) NewRecorder

func (d *Device) NewRecorder() *Recorder

NewRecorder returns a recorder for building a Graph.

A Recorder belongs to one goroutine and is used once: Build consumes it. The Graph it produces is immutable but permits only one in-flight submission; build one graph per concurrent user.

func (*Device) NewRenderPipeline

func (d *Device) NewRenderPipeline(desc RenderPipelineDescriptor) (*RenderPipeline, error)

NewRenderPipeline compiles a render pipeline.

Everything checkable is checked here rather than at draw: the descriptor against the stages' generated records, and both against the device's limits. A pipeline that survives this is one a pass can only get wrong by pairing it with mismatched attachments, which graph build catches.

func (*Device) NewTexture

func (d *Device) NewTexture(desc TextureDescriptor) (*Texture, error)

NewTexture creates a texture. Bytes per pixel always derives from the format, and depth formats carry backend constraints the implementation enforces; see docs/conventions.md.

func (*Device) NewTransientPool

func (d *Device) NewTransientPool(label string) (*TransientPool, error)

NewTransientPool creates a pool this device's graphs can share.

func (*Device) NewWindowSurface

func (d *Device) NewWindowSurface(h NativeHandle, desc SurfaceDescriptor) (*Surface, error)

NewWindowSurface makes a surface that presents to a window the caller owns.

What is yours and what is accel's

accel does not create windows. specs/034-surface-present.md section 6 puts the window, its event loop, input, focus and DPI on your side of the line, and everything from the swapchain inward on accel's — because window creation is an operating system concern with no relation to GPU work, and absorbing it would drag a windowing library and an opinion about event loops into a library whose subject is device work.

So you create the window and hand over a native handle. accel owns the drawables, acquire, present and resize.

Main-thread obligation on macOS

A `CAMetalLayer` must be created and resized on the main thread. accel cannot check this and does not try: call this from the main thread, and call Surface.Resize from it too. Given a NativeNSView this call creates the layer, so the obligation applies to this call itself.

What the frame loop looks like

The same as a headless one, which is the point of the headless surface existing: Surface.Acquire, Graph.BindPresent, submit after the frame's fence, then Surface.Present. Nothing in the loop changes when the pixels start going to a screen.

func (*Device) Queue

func (d *Device) Queue() *Queue

Queue returns the device's default queue, which is always QueueUniversal.

func (*Device) QueueFor

func (d *Device) QueueFor(kind QueueKind) *Queue

QueueFor returns a queue able to run kind.

It never fails and never invents a queue: on a device with one universal queue it returns that queue, and the caller sees which one they got through Device.Queues. That is not the silent substitution OpenDevice refuses, because nothing about the result is weaker than what was asked for, only less parallel.

func (*Device) Queues

func (d *Device) Queues() []QueueInfo

Queues reports every queue this device exposes, in a stable order whose first entry is what Device.Queue returns.

Queue topology is reported rather than inferred from the platform, because the backends disagree completely: Vulkan exposes queue families with capability bits, D3D12 has typed command queues, and Metal, GL and the CPU backend have exactly one. Ordering between submissions depends on which queue they went to, so a caller who cannot enumerate them cannot use that rule.

func (*Device) SelectionReport

func (d *Device) SelectionReport() (SelectionReport, bool)

SelectionReport reports how OpenBest selected this device. The bool is false for a device opened explicitly with OpenDevice or OpenCPU.

type DeviceInfo

type DeviceInfo struct {
	ID           AdapterID
	Backend      Backend
	Name, Vendor string
	Software     bool

	// Capabilities is what this device can actually do. Queried before use.
	Capabilities Capabilities
	Limits       Limits
}

DeviceInfo describes a device that could be opened, without opening it. Callers choose on reported capabilities and limits rather than by trying and catching failures.

type DeviceProfile

type DeviceProfile struct {
	Info DeviceInfo
}

DeviceProfile is a captured device contract used to reproduce another adapter's capability and numeric-limit behavior on the CPU backend.

type Draw

type Draw struct {
	VertexCount   int
	InstanceCount int
	FirstVertex   int
	FirstInstance int
}

Draw is one non-instanced or instanced draw's counts.

Instancing is the instance count and not a separate call, which is why the non-instanced case is this one with a count of one: a caller never picks between two entry points for the same drawing.

type DrawIndexed

type DrawIndexed struct {
	IndexCount    int
	InstanceCount int
	FirstIndex    int
	BaseVertex    int
	FirstInstance int
}

DrawIndexed is one indexed draw.

BaseVertex is added to each fetched index before the attribute fetch and is not added to the index the stage sees. specs/032-stage-abi.md section 2.1 declines to expose a base-vertex built-in for that reason: backends disagree about whether theirs reports the pre-offset or post-offset value, so the ABI exposes only the one a caller can act on.

type Enumeration

type Enumeration struct {
	Devices     []DeviceInfo
	Diagnostics []ProbeDiagnostic
}

Enumeration separates openable adapters from probe failures.

func Enumerate

func Enumerate() Enumeration

Enumerate reports every openable synchronous adapter and all probe failures.

type Extent

type Extent struct{ Width, Height, Depth int }

Extent is a texture's size in pixels.

type Fence

type Fence struct {
	// contains filtered or unexported fields
}

Fence reports the completion of a submission.

func FailedFence

func FailedFence(err error) *Fence

FailedFence returns a fence that has already failed with err.

It exists for layers above this one. A tensor plan validates its bindings before submitting, and specs/007-tensor-layer.md requires that failure to arrive the same way every other submission failure does -- through the fence -- so that a caller checks one thing rather than a second return value they will forget. Without this, every layer above would either invent its own two-value convention or reach into this package.

A nil err is a programming mistake rather than a success, and says so.

func (*Fence) C

func (f *Fence) C() <-chan struct{}

C returns a channel closed when the submission completes, for selecting on it.

func (*Fence) Done

func (f *Fence) Done() bool

Done reports whether the submission has completed, without blocking.

func (*Fence) Stats

func (f *Fence) Stats() (SubmissionStats, error)

Stats reports what the device counted during this submission. It is valid only after the fence has signalled, and calling it earlier is an error rather than a stale read.

Collection is off unless the graph asked for it with Recorder.CollectRunStats, because the numbers are written by the device and reading them back costs a transfer, a barrier, and a Readback allocation. A graph that did not ask still clamps an indirect count against its recorded maximum; what it loses is being told that it did.

func (*Fence) Wait

func (f *Fence) Wait() error

Wait blocks until the submission completes, reporting its error if it failed.

type Float16

type Float16 = kernel.Float16

Narrow storage types.

A kernel parameter of []accel.Float16 is a binding of 16-bit floats. The types carry no arithmetic operators at all, which is not an omission: it is what forces f.F32() on the way in and ToFloat16 on the way out, so f32 accumulation is the only thing that compiles rather than a convention an author has to remember. Native narrow arithmetic is a separate capability (spec 002's CapF16Arithmetic) and arrives as explicit intrinsics later.

They are not named F16 and BF16 because those are the DType constants for the same formats. A dtype is metadata a descriptor carries and a storage type is what a parameter is made of; both wanted the short name and the constants shipped first.

func Float16FromBits

func Float16FromBits(b uint16) Float16

Float16FromBits and BFloat16FromBits reinterpret storage bits without converting, for a caller who already has encoded data.

func ToFloat16

func ToFloat16(x float32) Float16

ToFloat16 converts an f32 to 16-bit storage, rounding to nearest with ties to even and overflowing to a signed infinity. A NaN becomes the canonical quiet encoding, per specs/008-numerics.md section 4.

type Format

type Format int

Format is a texture's pixel format.

Formats are separate from DType even where they name the same width, because a texture format carries sampling and colour-space meaning a buffer dtype does not. Bytes per pixel always comes from the format: assuming a value is a real bug, and one that shows up as an out-of-range panic during readback rather than a wrong image. See docs/conventions.md.

const (
	// The constants below are the formats' own names, unprefixed, because that
	// is how they read at the point of use: `Format: accel.RGBA8Unorm`. The
	// zero value is the one exception, and carries the type name because "no
	// format" has no name of its own.
	//
	// FormatInvalid is not a creatable format. It is the zero-value sentinel used
	// by optional format constraints such as a graph slot.
	FormatInvalid Format = iota
	RGBA8Unorm
	RGBA8UnormSRGB
	BGRA8Unorm
	R16Float
	RG16Float
	RGBA16Float
	R32Float
	RG32Float
	RGBA32Float

	// Depth formats carry backend constraints colour formats do not, including a
	// macOS requirement that they be device-private. The implementation enforces
	// that rather than the caller discovering it.
	Depth32Float
	Depth24PlusStencil8
)

func (Format) BytesPerPixel

func (f Format) BytesPerPixel() int

BytesPerPixel returns the format's size, or zero when the layout is device-defined.

Always ask rather than assume. Assuming is a real bug and it surfaces as an out-of-range panic during readback rather than as a wrong image, which is at least loud — but only on the machine whose format happened to differ.

func (Format) IsDepth

func (f Format) IsDepth() bool

IsDepth reports whether the format is a depth or depth-stencil format.

func (Format) String

func (f Format) String() string

String returns the format's name.

type FormatError

type FormatError struct {
	Format Format
	Want   string // "renderable", "storage", "host copyable", "filterable"
	Device string
}

FormatError reports a format the device cannot use as asked.

func (*FormatError) Error

func (e *FormatError) Error() string

func (*FormatError) Unwrap

func (e *FormatError) Unwrap() error

type FormatInfo

type FormatInfo struct {
	Format        Format
	BytesPerPixel int // 0 when the layout is device-defined, as for Depth24PlusStencil8
	Channels      int

	IsDepth   bool
	IsStencil bool
	IsSRGB    bool

	Renderable bool
	Sampleable bool

	// Filterable is not implied by Sampleable. The 32-bit float formats are
	// sampleable everywhere and linearly filterable only where the device says so,
	// and assuming otherwise is how a resolve ends up with nearest-neighbour
	// artefacts on one vendor only.
	Filterable bool

	// StorageRead and StorageWrite are separate because an sRGB format is neither:
	// its transfer function is applied by fixed-function hardware that a storage
	// write bypasses.
	StorageRead  bool
	StorageWrite bool

	Blendable    bool
	HostCopyable bool
}

FormatInfo describes what a Format is and what a device can do with it.

Capability is per device, not per format: a format that is renderable on one backend may be sampleable only on another. Asking the device is the only correct way to find out, which is why this is a method rather than a table constant.

type Fragment

type Fragment = kernel.Fragment

Fragment carries one fragment invocation's window coordinate and facing, and is a fragment stage's first parameter.

//accel:fragment
func Shade(f accel.Fragment, in Varyings, mat Material) Targets

The returned struct's fields map, in declaration order, onto the pipeline's colour attachments — one field per attachment, which is how MRT is expressed.

Its second parameter is the varyings struct **by position**. A varyings struct and a uniform struct are both structs, so nothing else could tell them apart.

func NewFragmentForTest

func NewFragmentForTest(coord Vec4, front bool) Fragment

NewFragmentForTest builds a fragment receiver. See NewVertexForTest.

type Frame

type Frame struct {

	// Acquired is signalled when the image is safe to render into. On a
	// headless surface it is already signalled, because nothing else holds the
	// image; a windowed surface signals it when the compositor releases one.
	Acquired *Fence
	// contains filtered or unexported fields
}

Frame is one acquired image.

It is not a naked view: BindPresent takes a Frame so it can check the surface identity and the generation, which a view cannot carry. A frame from another surface with the same format and extent is the case a format check alone accepts.

func (*Frame) Index

func (f *Frame) Index() int

Index is which of the rotating images this is, for a diagnostic that wants to say the loop is rotating rather than reusing one.

func (*Frame) View

func (f *Frame) View() BufferView

View is where the frame's pixels are, for a caller that wants to read them back rather than present them.

type FrontFace

type FrontFace uint8

FrontFace is which winding is front-facing.

There is no zero value meaning "the backend's default". Metal's default disagrees with GL's, and getting it backwards keeps back faces instead of front faces: the silhouette stays right while every per-pixel attribute comes from the wrong surface, so it reads as a shading bug. A default would make that the easiest thing to write. See docs/conventions.md.

const (
	// CounterClockwise is front-facing when the vertices wind counter-clockwise
	// in clip space, which is what a caller reasons about. The viewport's y flip
	// reverses the sign of the window-space area, so the implementation's test
	// is not the caller's convention.
	CounterClockwise FrontFace = iota
	Clockwise
)

type Graph

type Graph struct {
	// contains filtered or unexported fields
}

Graph is validated, planned work that can be submitted many times.

A Graph is immutable. Immutability is the point: it means validation, memory planning, barrier computation, and lowering happen once rather than per submission. Between submissions only three things may vary: buffer contents, which resource is bound to a declared slot, and dynamic dispatch or draw counts. Anything else is a different graph.

Note that a per-step address is none of those three and travels as buffer contents. A KV cache write offset, for example, is passed as a value a kernel reads rather than by rebinding a view, because rebinding would cost a binding update per layer per step.

A Graph may have only one submission in flight at a time. That is narrower than immutability suggests, and the reason is memory planning: its transients are aliased into a single pool, so two overlapping submissions would write each other's intermediates, and a rebind between two in-flight submissions races on which one sees it. To run the same work concurrently, build a graph per concurrent user: they share pipelines and caller-owned buffers, and only the transient pool is duplicated. Wait on a Fence if you need ordering.

func (*Graph) Barriers

func (g *Graph) Barriers() int

Barriers is how many barriers the plan emits in total.

It is reported separately from the per-node count because the number a reader wants first is the whole-graph one. It is far below Graph.Hazards because a barrier is queue-wide: one emitted for a hazard on one resource also orders every earlier write on every other.

func (*Graph) Bind

func (g *Graph) Bind(b ...SlotBinding) error

Bind points one slot at a resource. Rebind does several at once and is the hot-path form.

Both validate kind, dtype, access, size, device ownership and liveness, and then check that no two slots have been bound to overlapping ranges unless both are read-only. That last check cannot happen at build, because hazards are inferred against the slot rather than against whatever will occupy it: bind one buffer to two slots the builder treated as independent and the inferred edge set is wrong, which is a missing barrier and therefore a race. A batch is rejected as a batch rather than half applied.

Binding while a submission is in flight reports ErrGraphInFlight, for the same reason submitting twice does.

func (*Graph) BindPresent

func (g *Graph) BindPresent(slot Slot, f *Frame) error

BindPresent binds an acquired frame to a present slot.

It takes a Frame and never a naked view, so it can check the surface identity and the generation. An ordinary render target with the same format and extent is the case a format check alone accepts, and a frame from before a resize is the case an extent check alone accepts when the two generations happen to share a size.

func (*Graph) Close

func (g *Graph) Close() error

Close releases the graph, including the transient memory it owns.

It fails while a submission is in flight rather than freeing memory the device is reading.

func (*Graph) Edges

func (g *Graph) Edges() [][]NodeID

Edges reports the inferred dependency DAG as one successor list per node, in record order within each list.

It is exposed because a plan is the thing worth asserting on: a test that only compares results cannot tell a graph that overlapped correctly from one that serialized and got the same answer.

func (*Graph) Hazards

func (g *Graph) Hazards() int

Hazards is how many read-after-write, write-after-write, and write-after-read dependencies the declared accesses imply.

Reported alongside Graph.Barriers because the gap between them is what batching bought, and a caller asking why a graph does not overlap wants both numbers rather than either alone.

func (*Graph) Memory

func (g *Graph) Memory() GraphMemory

Memory reports what the graph needs to run: its transient pool size and its peak usage.

Callers need this before submitting, to size a KV cache or decide how many layers fit in device memory.

func (*Graph) NodeStats

func (g *Graph) NodeStats(id NodeID) NodeStats

NodeStats reports what the builder decided about one node, and Nodes reports all of them. Both are valid as soon as Build returns and are identical for every submission: these are the plan, not a measurement, so they cost nothing.

func (*Graph) Nodes

func (g *Graph) Nodes() []NodeStats

func (*Graph) SetUniform

func (g *Graph) SetUniform(n NodeID, index int, v any) error

SetUniform replaces one by-value parameter of a recorded dispatch.

Why a graph can be told this after it is built

A kernel's by-value parameters are compiled into the plan when the graph is built, which makes them fast and makes them fixed. Most of them should be: specs/007-tensor-layer.md draws the line at whether the value changes the *shape* of the work, and one that does needs another plan because the barriers and the transient layout were computed from it.

A value that changes nothing structural is different. A softmax scale, a RoPE base, a current sequence length: these vary every step and rebuilding a graph for each would defeat the point of building one. So they are set here, between submissions, exactly as a slot is rebound.

It is refused while a submission is in flight, for the reason Graph.Bind is: a value changing under a running graph would give the first half of it one number and the second half another, and no caller could tell which they got.

The type must be the one the kernel declares. A struct of the same shape and a different name would encode identically and read correctly, which is why the check is on the type rather than on the size: the pair that encodes the same today diverges the first time either gains a field.

func (*Graph) Slots

func (g *Graph) Slots() []GraphSlot

Slots reports the stable IDs and descriptors a graph expects, so a caller holding a graph they did not record can bind its inputs.

func (*Graph) TransientPlacement

func (g *Graph) TransientPlacement() []TransientPlacement

TransientPlacement reports the pool layout the builder chose.

It is exposed because a placement is the thing worth asserting on: an output comparison can pass on a backend that executes serially while the layout is unsound, since such a backend cannot observe the race the layout would create on one that overlaps. See specs/017-graph-aliasing.md.

type GraphMemory

type GraphMemory struct {
	// TransientBytes is what the builder needs for transients after aliasing.
	TransientBytes int

	// UnaliasedBytes is what they would have needed without it. The gap is what
	// planning bought.
	UnaliasedBytes int

	PeakBytes int
}

GraphMemory reports a graph's memory requirement.

type GraphSlot

type GraphSlot struct {
	Slot       Slot
	Descriptor SlotDescriptor
}

GraphSlot pairs a discoverable graph slot ID with its descriptor.

type ID3

type ID3 = kernel.ID3

ID3 is a three-dimensional invocation identifier, with X, Y, and Z of type uint32.

Ids are three-dimensional rather than scalar because a two-dimensional shared tile cannot be addressed from a scalar id without index arithmetic the compiler then cannot prove uniform. See specs/002-compute-model.md section 1.

type IndexFormat

type IndexFormat uint8

IndexFormat is the width of one entry in an index buffer.

const (
	// Index16 is the common case and half the bandwidth. It caps a mesh at
	// 65536 vertices, which is why it is not the only option.
	Index16 IndexFormat = iota

	// Index32 addresses any mesh the device can hold.
	Index32
)

func (IndexFormat) String

func (f IndexFormat) String() string

type IndirectStats

type IndirectStats struct {
	Node    NodeID
	Actual  [3]uint32 // device-supplied count before clamping
	Max     [3]uint32
	Clamped bool // Actual exceeded Max on at least one axis
}

IndirectStats is one indirect node's actual count and whether it was clamped.

type Kernel

type Kernel = kernelabi.Kernel

Kernel is a kernel compiled to whatever form the target device consumes.

An alias for kernelabi.Kernel, which is where generated code names it and where its fields are documented. A caller does not construct one and does not read its fields: `go generate` emits one package-level variable per kernel, and the address is the whole of the interface — `ComputePipelineDescriptor{Kernel: &kernels.ScaleKernel}`.

The alias stays so that spelling remains available at the point of use, while the thirty-odd names a *generated file* needs live in kernelabi rather than in this package's index. See specs/036-documentation.md's freeze record.

type LifetimeError

type LifetimeError struct {
	Op       string // "Close", "WriteBuffer", "Bind", ...
	Resource string // the resource's Label
	Reason   string // "in flight", "closed", "has live children", "pending transfer"
	InFlight int    // submissions still holding it, when Reason is "in flight"
	Children int    // live children, when Reason is "has live children"
}

LifetimeError reports a resource used or released at the wrong time.

A resource freed while something using it is still outstanding is a use-after-free, so the implementation keeps it alive and reports instead: the caller's handle is gone and the memory will come back, and the caller learns their teardown ordering was wrong. Nothing crashes and nothing leaks. See specs/001-device-resources.md section 7.2.

func (*LifetimeError) Error

func (e *LifetimeError) Error() string

func (*LifetimeError) Unwrap

func (e *LifetimeError) Unwrap() error

type LimitConstraints

type LimitConstraints struct {
	AtLeast Limits
	AtMost  Limits
}

LimitConstraints filters automatic selection. Zero fields are unconstrained; array components are compared independently.

type LimitValue

type LimitValue struct {
	Name  string
	Value int
}

LimitValue is one numeric bound from Limits, named.

func LimitValues

func LimitValues(l Limits) []LimitValue

LimitValues flattens Limits into a stable, named sequence, expanding a per-axis limit into one entry per axis.

It exists because a limit is one kind of thing repeated twenty-odd times, and every consumer wants to treat them uniformly: Policy filters on them, diagnostics print them, and the conformance requirement that an opened device has no zero-valued limit is one loop rather than one assertion per field. That last check is the cheapest possible catch for a backend that forgot to fill a limit in, which is the failure spec 001 section 1.1 is written against.

The order is the declaration order of Limits and is part of the contract: two calls are index-comparable.

type Limits

type Limits struct {
	// MinStorageBufferOffsetAlignment and MinUniformBufferOffsetAlignment are the
	// alignments a bound buffer range must satisfy. They constrain suballocation
	// directly: a pool hands out offsets that satisfy the strictest alignment any
	// declared usage requires. A multiple of 256 is always sufficient on every
	// backend; these report what this device actually needs, for callers who want
	// the waste back.
	MinStorageBufferOffsetAlignment int
	MinUniformBufferOffsetAlignment int

	// MinBufferCopyOffsetAlignment and MinBufferCopyRowPitchAlignment constrain
	// transfers rather than bindings, which is why a texture readback can cost a
	// repack. accel guarantees tightly packed rows to the caller and pays for the
	// repack itself, so these are reported for callers sizing their own staging,
	// not imposed on them. See docs/conventions.md.
	MinBufferCopyOffsetAlignment   int
	MinBufferCopyRowPitchAlignment int

	// MinTexturePlacementAlignment is the alignment a texture's backing memory must
	// start at inside a pool. It is far coarser than any buffer alignment on some
	// backends, which is why a pool is either a buffer pool or a texture pool and
	// never both.
	MinTexturePlacementAlignment int

	// MaxBufferBytes is the largest single buffer, MaxPoolBytes the largest single
	// device allocation, and MaxPools the driver's cap on live allocations.
	// MaxPools is the number that makes pooling mandatory rather than merely
	// efficient.
	MaxBufferBytes int
	MaxPoolBytes   int
	MaxPools       int

	MaxTextureExtent2D    int
	MaxTextureExtent3D    int
	MaxTextureArrayLayers int

	// MaxUniformBlockBytes is the largest std140 block a uniform binding may
	// carry. A kernel's uniform struct is encoded to a block whose size the
	// generator bakes into the pipeline, so without this there is no device number
	// to validate that size against, and a struct that is too large for the device
	// would be discovered at pipeline creation on somebody else's machine.
	MaxUniformBlockBytes int

	// Compute limits constrain generated kernel metadata and dispatches. Feature
	// availability remains in Capabilities.
	MaxWorkgroupSize             [3]int
	MaxWorkgroupInvocations      int
	MaxWorkgroupCount            [3]int
	MaxSharedMemoryBytes         int
	MaxStorageBufferBindingBytes int
	MaxBindingsPerKind           int

	// MaxColorAttachments is how many colour targets one render pass may have.
	// Zero means the backend does not report one, which at v0 means it has no
	// render path — a limit of zero and "no limit" are the same answer for a
	// backend that cannot draw.
	MaxColorAttachments int

	// Devices without subgroups report 1/1 while Capabilities.Subgroups is false,
	// so every opened device still has positive numeric limits.
	MinSubgroupSize int
	MaxSubgroupSize int
}

Limits are the device's numeric bounds.

They are separate from Capabilities on purpose. A capability is a boolean that gates a code path; a limit is an integer that appears in arithmetic, and the failure modes differ. A backend that forgets to report a capability leaves it false, and the affected path is simply not taken. A backend that forgets to report a limit leaves it zero, and zero is a divide-by-zero or an alignment of one, which is worse than useless. Keeping them in separate structs makes the missing-limit case obvious at the point a backend is written.

See specs/001-device-resources.md.

type LoadOp

type LoadOp = driver.LoadOp

LoadOp and StoreOp are what happens to an attachment at the start and the end of a pass. They are driver.LoadOp and driver.StoreOp: a backend acts on the value and cannot import this package, so one definition lives where both sides reach it. Two definitions with nothing pinning them together is how LoadKeep and LoadDontCare -- which a backend cannot tell apart by their effect -- would swap silently.

type MemoryKind

type MemoryKind int

MemoryKind is where a pool's memory lives and who can reach it. It is the property that actually decides performance, so it is chosen explicitly rather than inferred. See specs/001-device-resources.md.

const (
	// MemoryDevice is fast for the GPU and not host-visible. Weights, activations,
	// render targets.
	MemoryDevice MemoryKind = iota

	// MemoryUpload is host-writable and GPU-readable. Staging.
	MemoryUpload

	// MemoryReadback is GPU-writable and host-readable. Results.
	MemoryReadback

	// MemoryShared is host-visible and device-local at once. This is a real
	// capability on unified-memory hardware, where it removes a copy entirely,
	// not an alias for something else. Reported per device rather than assumed
	// from the platform: check Capabilities.SharedMemoryKind.
	MemoryShared
)

func (MemoryKind) String

func (k MemoryKind) String() string

String returns the memory kind's name.

type NativeHandle

type NativeHandle = driver.NativeHandle

NativeHandle is a platform-tagged pointer to a window resource the caller owns.

Tagged rather than bare, because a backend given the wrong kind of pointer sends a message to an object that does not answer it, and the crash names neither the caller nor the mistake.

type NativeHandleKind

type NativeHandleKind = driver.NativeHandleKind

NativeHandleKind says what a NativeHandle points at.

type NoVaryings

type NoVaryings = kernel.NoVaryings

NoVaryings is the empty varyings struct, for a vertex stage that returns only a position.

A named empty struct rather than allowing a stage to return one value, because the no-varyings case being a different signature shape is how a caller ends up writing the two-varying case twice.

type NodeID

type NodeID int

NodeID identifies a recorded node, for referring to it in errors.

type NodeKind

type NodeKind uint8

NodeKind identifies the public operation family represented by a graph node.

const (
	NodeDispatch NodeKind = iota
	NodeDispatchIndirect
	NodeRenderPass
	NodeCopyBuffer
	NodeCopyTextureToBuffer
	NodeCopyBufferToTexture
	NodeHostWrite
)

type NodeStats

type NodeStats struct {
	Node  NodeID
	Kind  NodeKind
	Label string

	// Copy is non-nil for copy nodes. Whether a texture copy repacks is decided at
	// build, not observed at run time, because the backend knows its own pitch
	// rules before anything executes.
	Copy *CopyStats

	// BarriersBefore is how many barriers the builder emits immediately before this
	// node. It is here so a caller can ask why a graph does not overlap, and so the
	// builder's own tests can assert on the plan rather than on results.
	BarriersBefore int
}

NodeStats is one node's plan-time facts.

type Policy

type Policy struct {
	Prefer        []Backend // tried in order; empty means every compiled-in backend
	AllowCPU      bool
	AllowSoftware bool
	Require       Capability // a device lacking any of these is not a candidate
	Limits        LimitConstraints
}

Policy is what OpenBest is allowed to select.

Two defaults are deliberate. The CPU backend is never selected unless AllowCPU says so: it is a first-class backend and it is not a fast path, and a caller who wanted a GPU and got it should hear about that as an error. Software GPU devices are their own class rather than being lumped in with hardware, because lavapipe and WARP are real devices that may well be slower than the CPU backend, and automatic selection has to be able to see the difference.

type Pool

type Pool struct {
	// contains filtered or unexported fields
}

Pool is a device memory allocation that buffers are suballocated from.

Pooling exists because one device allocation per buffer is fine for a renderer with a handful and wrong for a model with thousands: allocation is expensive, drivers cap how many you may hold, and per-resource allocation forecloses the transient aliasing a Graph does when it plans memory.

func (*Pool) AllocBuffer

func (p *Pool) AllocBuffer(desc BufferDescriptor) (*Buffer, error)

Alloc suballocates a buffer from the pool.

func (*Pool) AllocTexture

func (p *Pool) AllocTexture(desc TextureDescriptor) (*Texture, error)

AllocTexture suballocates a texture from a pool created with Textures set. Buffer pools reject it, and texture pools reject [Pool.Alloc].

func (*Pool) Close

func (p *Pool) Close() error

Close releases the pool. Buffers suballocated from it must be closed first: closing a pool with live buffers reports a *LifetimeError and frees nothing, because closing children out from under a caller who still holds them turns a bug into a silent success.

func (*Pool) Kind

func (p *Pool) Kind() MemoryKind

Kind reports the pool's memory kind.

func (*Pool) Reset

func (p *Pool) Reset() error

Reset releases every allocation in a linear pool at once. It rejects general pools and a linear pool with resources retained by an in-flight submission.

func (*Pool) Stats

func (p *Pool) Stats() PoolStats

Stats reports the pool's size, how much is in use, and how much is free.

type PoolDescriptor

type PoolDescriptor struct {
	Kind   MemoryKind
	Bytes  int
	Policy PoolPolicy

	// Textures reserves the pool for textures. Texture placement alignment is far
	// coarser than buffer alignment on some backends and some forbid the mixture
	// outright, so it is a pool property rather than a per-allocation one. Mixing
	// them would apply a texture's granularity to a model's thousands of tensors,
	// which is not a tax but a fatal multiplier.
	Textures bool

	// Label appears in allocation errors and in backend debug tooling.
	Label string
}

PoolDescriptor describes a pool to create.

type PoolPolicy

type PoolPolicy int

PoolPolicy selects how a pool carves itself up. See specs/001-device-resources.md.

const (
	// PoolGeneral is a general-purpose pool: arbitrary allocation and free order,
	// O(1) allocate and free through a two-level segregated fit allocator, and
	// bounded internal fragmentation. The default, and what a caller holding
	// weights and caches wants.
	PoolGeneral PoolPolicy = iota

	// PoolLinear allocates by bumping a cursor and frees only by resetting the
	// whole pool, so an individual Close is a no-op against the memory. This is
	// what a Graph's transient pool uses: the graph computed every offset at build,
	// so that pool needs no runtime allocator at all.
	PoolLinear
)

type PoolStats

type PoolStats struct {
	Size int
	Used int // sum of allocation sizes, which includes alignment padding
	Free int // Size - Used

	// LargestFree is the biggest single allocation this pool can still serve. The
	// gap between Free and LargestFree is fragmentation, and it is what predicts
	// an allocation failure rather than reporting it afterwards. A pool never
	// compacts, because a device address is already baked into descriptor sets and
	// recorded commands, so fragmentation inside a pool is permanent for its life.
	LargestFree int

	// Allocations is the live count and Blocks the number of free blocks. Rising
	// Blocks against flat Allocations is fragmentation accumulating.
	Allocations int
	Blocks      int
}

PoolStats reports a pool's occupancy.

type PrimitiveState

type PrimitiveState struct {
	Topology  Topology
	FrontFace FrontFace
	Cull      CullMode
}

PrimitiveState is the fixed-function state before the fragment stage.

type ProbeDiagnostic

type ProbeDiagnostic struct {
	Backend Backend
	Stage   ProbeStage
	Err     error
}

ProbeDiagnostic explains why a backend or adapter did not produce an openable device without hiding healthy adapters from other backends.

type ProbeStage

type ProbeStage int

ProbeStage identifies the native-probe phase that failed.

const (
	ProbeLoadLibrary ProbeStage = iota
	ProbeCreateInstance
	ProbeEnumerateAdapters
	ProbeQueryDevice
)

type Queue

type Queue struct {
	// contains filtered or unexported fields
}

Queue accepts submitted work.

func (*Queue) Flush

func (q *Queue) Flush() *Fence

Flush submits this queue's pending immediate writes without a graph. When no writes are pending it returns an already-signalled fence.

It exists for the caller who wants to flush without reading. Without it a caller who wrote and then expected the bytes to be there some other way would wait forever, because nothing else forces the batch out.

func (*Queue) ReadBuffer

func (q *Queue) ReadBuffer(src *Buffer, offset int, into any) error

ReadBuffer flushes this queue's pending writes, waits for prior work on the queue, and copies a buffer range into host memory.

It blocks, which is what makes it wrong in a hot loop and right for reading final results. offset is in elements of the buffer's dtype and into must be a slice of that dtype.

A read orders only the queue it is called on. If another queue owns a pending write to the same resource, flush that queue first: an immediate read never silently searches or drains unrelated queues.

func (*Queue) ReadTexture

func (q *Queue) ReadTexture(src *Texture, into []byte) error

ReadTexture flushes this queue's pending writes, waits for prior work, and returns the base mip and sole array layer as tightly packed top-origin rows.

func (*Queue) Run

func (q *Queue) Run(record func(*Recorder)) error

Run records a one-use graph, submits it, and waits.

It exists for readability in simple cases and carries the full cost of building a graph every call, so it is the wrong choice in a hot loop.

func (*Queue) Stats

func (q *Queue) Stats() QueueStats

Stats reports cumulative queue counters since device open. They are counters, not a profiler: nothing here is per node and nothing here costs a readback.

func (*Queue) Submit

func (q *Queue) Submit(g *Graph) *Fence

Submit submits a graph and returns immediately with a Fence. Nothing in this API blocks implicitly.

func (*Queue) SubmitAfter

func (q *Queue) SubmitAfter(g *Graph, after ...*Fence) *Fence

SubmitAfter submits a graph that begins only once every given fence has signalled.

func (*Queue) WriteBuffer

func (q *Queue) WriteBuffer(dst *Buffer, offset int, data any) error

WriteBuffer copies data into queue-owned staging and appends the transfer to this queue's next submission prologue. It returns once data no longer aliases the caller's value, not when the device has consumed it.

offset is in elements of the buffer's dtype and the write touches only the range it names. data must be a slice of the buffer's dtype: []float32 for f32, []uint16 for the 16-bit float storage types, []int32, []uint32, []int8, or []byte.

The caller may reuse or modify their slice the moment this returns, which is what "asynchronous" has to mean for it to be safe. Every write issued before a flush is visible to that flush, so waiting on the returned fence proves the bytes landed.

type QueueInfo

type QueueInfo struct {
	Kind  QueueKind
	Index int
	Label string // the backend's own name for it, for logs
}

QueueInfo describes one queue a device exposes.

type QueueKind

type QueueKind int

QueueKind is what a queue accepts.

const (
	// QueueUniversal accepts everything: compute, graphics, and transfer.
	QueueUniversal QueueKind = iota
	QueueCompute             // compute and transfer, no rasterization
	QueueTransfer            // transfer only
)

type QueueStats

type QueueStats struct {
	Submissions    int64
	BytesStaged    int64
	StagingWaits   int64 // times WriteBuffer blocked waiting for a recycled block
	ImmediateReads int64
	Repacks        int64 // immediate-path texture copies that needed a padded pitch
}

QueueStats are cumulative since device open.

type Recorder

type Recorder struct {
	// contains filtered or unexported fields
}

Recorder accumulates nodes for a Graph. It executes nothing.

A Recorder belongs to one goroutine. The Graph it builds is immutable, but immutability does not make it concurrently submittable: see Graph for the one-submission-in-flight rule and why its transient pool requires it.

Each recorded node declares the resources it reads and writes. Dependency edges are inferred from those declarations rather than from the order calls were made, which is what lets the builder compute barriers correctly and overlap independent work, and what makes a missing dependency a validation error instead of a race.

func (*Recorder) Build

func (r *Recorder) Build() (*Graph, error)

Build validates the recorded nodes, infers the dependency edges between them, computes the barriers those edges require, packs the transients into overlapping memory where their live ranges allow, and lowers the result for the device.

The graph it returns is immutable and replayable: what varies between submissions is what slots are bound to, the contents of buffers, and device-supplied dispatch and draw counts. Nothing else, which is what lets a plan be built once and submitted every frame.

A build error is a statement about the recording rather than about the device: an operand outside its resource, a slot smaller than the use it is bound to, a transient nothing wrote. Every one of them is cheaper here than at submission, which is why they are here.

Recorder.BuildNaive produces the conservative plan this one is checked against.

func (*Recorder) BuildNaive

func (r *Recorder) BuildNaive() (*Graph, error)

BuildNaive builds the graph under the conservative plan of specs/015-graph-recording.md: nodes in record order, a barrier before each, and no transient aliasing.

Nodes in record order with a full barrier between them is correct rather than merely safe, and the reason is worth stating: every dependency edge a hazard analysis could infer runs from a lower node id to a higher one, because an edge exists only where a later node's declared access conflicts with an earlier one's. Record order is therefore a topological order of that DAG, and a barrier between consecutive nodes covers every read-after-write, write-after-read and write-after-write it could classify.

It exists so that an optimized plan has something to be compared against. specs/003-command-graph.md defines the whole-plan oracle as executing a graph a second time under exactly this plan and comparing results, and any disagreement is a planner or barrier bug localized to the builder rather than to a kernel, because both sides ran the same kernels over the same inputs.

This is not scaffolding written alongside the optimizer. It is what Recorder.Build produced before edge inference existed, retained: an oracle written after the thing it checks, by whoever just wrote it, is under constant pressure to share its reachability code and therefore its mistakes.

It is exported for testing and for a caller who suspects a planning bug. It is slower by construction and never the right choice otherwise.

func (*Recorder) CollectRunStats

func (r *Recorder) CollectRunStats(on bool)

CollectRunStats makes the graph carry back the counters only the device knows: an indirect node's actual count and whether it was clamped. It is off by default because it adds a readback buffer, a transfer, and a barrier to every submission.

func (*Recorder) CollectTimings

func (r *Recorder) CollectTimings(on bool)

CollectTimings makes each submission of this graph report how long the device took, through SubmissionStats.Elapsed.

Why this is opt-in

The same reason Recorder.CollectRunStats is: it costs the backend a query it would not otherwise make, and a library whose subject is throughput should not spend a caller's time measuring itself unless asked.

Why it is the whole submission and not per node

specs/003-command-graph.md predicts per-node timestamps and warns in the same paragraph that barrier batching merges the boundaries a timestamp would sit on. Per-node therefore needs the planner to stop merging, which changes what a graph *is* rather than what it reports. This answers "how long did this take" and does not pretend to answer "which node is slow".

A backend with no device clock reports zero rather than wall-clock time. The difference between "the GPU ran for 3ms" and "the call took 3ms" is the whole question a caller asking about throughput has, and a substitute would answer the wrong one convincingly.

func (*Recorder) CopyBuffer

func (r *Recorder) CopyBuffer(dst, src BufferView) NodeID

CopyBuffer records a device-to-device buffer copy.

func (*Recorder) CopyBufferToTexture

func (r *Recorder) CopyBufferToTexture(dst *Texture, src BufferView) NodeID

CopyBufferToTexture records an on-device copy from a buffer into a texture.

func (*Recorder) CopyFromSlot

func (r *Recorder) CopyFromSlot(dst BufferView, src Slot, offset, count int) NodeID

CopyFromSlot records a device-to-device copy whose source arrives before submission.

func (*Recorder) CopyTextureToBuffer

func (r *Recorder) CopyTextureToBuffer(dst BufferView, src *Texture) NodeID

CopyTextureToBuffer records an on-device copy from a texture into a buffer.

This is what lets a rasterized G-buffer feed a compute pass without going out to the host and back. Readback follows caller row order regardless of the backend's native origin; see docs/conventions.md.

func (*Recorder) CopyToSlot

func (r *Recorder) CopyToSlot(dst Slot, offset, count int, src BufferView) NodeID

CopyToSlot records a device-to-device copy whose destination arrives before submission.

func (*Recorder) Dispatch

func (r *Recorder) Dispatch(p *ComputePipeline, b []Binding, u []UniformValue, count WorkgroupCount) NodeID

Dispatch records a compute dispatch.

func (*Recorder) DispatchIndirect

func (r *Recorder) DispatchIndirect(p *ComputePipeline, b []Binding, u []UniformValue, count BufferView, max WorkgroupCount) NodeID

DispatchIndirect records a dispatch whose workgroup count is read from a buffer written on the device.

This is how anything data-dependent is expressed, and a wholly device-decided count would leave an immutable graph with nothing to validate and nothing to size transients against. So the node also records max, a build-time upper bound checked against the device's workgroup count limit. The device supplies the actual count and it is clamped to max: on device in strict mode, and as a documented caller obligation otherwise.

func (*Recorder) PresentSlot

func (r *Recorder) PresentSlot(s *Surface, name string) Slot

PresentSlot records a rebindable binding point for a surface's image.

A dedicated slot type rather than an ordinary slot plus a format, because a format cannot prove that the eventual image is presentable, belongs to the right surface, or comes from the generation the graph was built for. The slot records the device, the surface identity, the generation and the extent, and Graph.BindPresent checks all of them.

specs/034-surface-present.md section 2.

func (*Recorder) RenderPass

func (r *Recorder) RenderPass(desc RenderPassDescriptor) *RenderPass

RenderPass begins recording a render pass. The pass becomes one node.

func (*Recorder) Slot

func (r *Recorder) Slot(desc SlotDescriptor) Slot

Slot declares a binding point whose resource is supplied before submission rather than at record time.

Slots are what the three-kinds-of-variation rule means by a bound resource changing: a swapchain image that does not exist until it is acquired, one sequence's KV cache selected per submission, an adapter swapped between runs. The descriptor carries everything the builder needs in order to validate and to infer hazards without a resource in hand, which is why MinCount and Access are there rather than being discovered at bind.

func (*Recorder) Transient

func (r *Recorder) Transient(desc BufferDescriptor) BufferView

Transient reserves a buffer whose lifetime the builder owns.

Transients are the memory the builder may alias: it computes each one's live range across the graph and packs those that do not overlap into shared storage. Buffers the caller created are never aliased. This is why a model can run without allocating per operation.

func (*Recorder) UploadToBuffer

func (r *Recorder) UploadToBuffer(dst BufferView, src any) NodeID

UploadToBuffer records a host-to-device transfer, copying src at record time.

The copy happens here rather than at submit because a Graph is immutable, and holding the caller's slice would make the bytes a submission writes depend on when the caller last touched them. The graph therefore owns those bytes, its build-time footprint includes them, and every submission rewrites the same values. That makes this the wrong entry point for bulk upload, which wants a staging buffer and Recorder.CopyBuffer, and for anything varying per submission, which wants an Upload buffer written between submissions. It is here for small constants baked into a graph.

func (*Recorder) UploadToSlot

func (r *Recorder) UploadToSlot(dst Slot, offset, count int, src any) NodeID

UploadToSlot records a host-to-device transfer into a slot's eventual resource, over its first count elements from offset.

It exists because Recorder.UploadToBuffer takes a view and a slot has no resource to make a view of. Splitting it out rather than overloading a BufferView with an optional slot keeps a view a thing that names bytes.

func (*Recorder) UseTransientPool

func (r *Recorder) UseTransientPool(p *TransientPool)

UseTransientPool makes this graph plan its intermediates into a caller-owned pool rather than allocating its own.

Several graphs may share one pool, which is what makes a set of plans over one model cost one plan's transients rather than all of them: five prefill buckets at 200 MiB is a gigabyte of device memory of which 800 MiB is idle.

The rule that makes it safe is one a graph already has, widened to the set: graphs sharing a pool cannot be in flight together, and a second submission is refused rather than queued -- a pool that queued silently would turn a design mistake into a latency mystery.

See TransientPool and specs/031-shared-transients.md.

type RenderPass

type RenderPass struct {
	// contains filtered or unexported fields
}

RenderPass records draws into one graph node.

One pass is one node and a draw is not, because the pass is the unit at which synchronisation is expressible: Vulkan cannot barrier inside a render pass in the general case, tile-based hardware physically cannot — attachment contents live in tile memory until the pass ends — and Metal's encoder has the same shape. Draw granularity would promise an ordering the hardware cannot provide.

Two rules follow, and both are caller-visible. Draws execute in recorded order and the builder never reorders them, because blending is order dependent. And the builder inserts no barriers inside a pass, because per-pixel ordering between draws is the ROP's job.

func (*RenderPass) Draw

func (p *RenderPass) Draw(d Draw)

Draw records one draw.

It executes in the order recorded, and the builder never reorders it: blending is order dependent, and so is any reasoning about overdraw.

func (*RenderPass) DrawIndexed

func (p *RenderPass) DrawIndexed(d DrawIndexed)

DrawIndexed records one indexed draw. See RenderPass.Draw.

func (*RenderPass) DrawIndirect

func (p *RenderPass) DrawIndirect(args BufferView, bound Draw)

DrawIndirect records a draw whose counts the device supplies.

Why a build-time maximum

The same reason indirect dispatch has one: a device-written count sits awkwardly with an immutable graph, since without a bound there is nothing to validate at build and exceeding a backend's limit is undefined rather than a clean error. The node records a maximum, the device supplies the actual, and the backend clamps.

**Every build mode clamps.** Correctness does not depend on a flag. What Recorder.CollectRunStats adds is being told that a clamp happened, which costs a readback — so a graph that did not ask is still protected, and what it gives up is knowing.

args names four uint32 in a device buffer: vertex count, instance count, first vertex, first instance. That is the layout Vulkan, D3D12 and Metal all use, so a caller filling it from a compute kernel writes the same four values whatever the backend.

func (*RenderPass) Node

func (p *RenderPass) Node() NodeID

Node is the graph node this pass records into, for a caller who wants to name it in a diagnostic.

func (*RenderPass) SetFragmentUniform

func (p *RenderPass) SetFragmentUniform(index int, v any)

SetFragmentUniform supplies one by-value parameter of the fragment stage. See RenderPass.SetVertexUniform.

func (*RenderPass) SetIndexBuffer

func (p *RenderPass) SetIndexBuffer(v BufferView, format IndexFormat)

SetIndexBuffer binds the index buffer subsequent indexed draws read.

The format is given rather than inferred from the buffer's dtype: an index buffer is bytes, and a caller packing uint16 indices into a buffer of uint32 elements is doing something ordinary rather than something wrong.

func (*RenderPass) SetPipeline

func (p *RenderPass) SetPipeline(pipe *RenderPipeline)

SetPipeline selects the pipeline subsequent draws use.

func (*RenderPass) SetTexture

func (p *RenderPass) SetTexture(slot int, v TextureView)

SetTexture binds a texture a stage fetches from, at one slot.

The view names the subresource, which is the whole of what a stage reads: the mip and the layer are the view's rather than the fetch's, so specs/032-stage-abi.md section 5's Fetch takes a coordinate and nothing else and specs/033-render-api.md section 3.3 has one shape to compare an attachment against.

The slot is a stage's dense texture index, counting the textures its signature declares and not its parameters -- the rule StageAttribute follows. Both stages read the same slot space, for the reason [RenderPass.textures] gives.

Its two refusals are RenderPass.SetVertexBuffer's, and for that method's reason: a negative slot indexed a slice and took the caller's process down from inside a recording call, and a panic is the one diagnostic a caller cannot handle, cannot attribute to a slot, and cannot see beside the rest of a build's errors.

func (*RenderPass) SetVertexBuffer

func (p *RenderPass) SetVertexBuffer(slot int, v BufferView)

SetVertexBuffer binds a vertex buffer at one slot.

The slot is checked rather than trusted, and that is worth a note because it was not: a negative slot skipped the grow loop and indexed the slice, which took the caller's process down from inside a recording call. Every other method on this type reports through the recorder, and a panic is the one diagnostic a caller cannot handle, cannot attribute to a slot, and cannot see alongside the rest of a build's errors.

func (*RenderPass) SetVertexUniform

func (p *RenderPass) SetVertexUniform(index int, v any)

SetVertexUniform and SetFragmentUniform supply one by-value parameter of the stage named, for every draw recorded after the call.

Why two calls and not one

The two stages are compiled independently, so each indexes its own uniform space from zero: a vertex stage's parameter 0 and a fragment stage's parameter 0 are different parameters with the same index. One shared slice cannot hold both, and a single call taking an index would have to guess which stage a value was for. specs/033-render-api.md deviation 1 is what happened when it did not: values were placed in the order the caller wrote them, so two passed out of order bound to each other's parameters.

Why pass state and not a draw argument

It matches SetPipeline and SetVertexBuffer, which are the calls a reader already knows, and a value shared by several draws is written once. A draw captures whatever is set when it is recorded, so a later call does not reach back and change an earlier draw.

type RenderPassDescriptor

type RenderPassDescriptor struct {
	Color []ColorAttachment
	Depth *DepthAttachment

	// Width and Height are the render area, validated against every attachment
	// at build.
	Width, Height int

	Label string
}

RenderPassDescriptor describes one render pass.

Attachments are texture views

They were buffer views, and this comment used to say the shape a caller writes would not change when textures landed. It did. specs/042-surface-completion.md section 5.2 found that eight of the largest findings in a review of this surface were consequences of that one decision, and specs/045-texture-attachments.md is the change: a buffer view carries a dtype, so ColorTargetState.Format reached no backend, sRGB had no owner, the V13 check was unimplementable, and 033 section 3.3's feedback rejection had no subresource to compare.

A TextureView names one subresource of one texture and carries a concrete format, and it is the same type a shader-visible binding takes -- which is what makes the feedback rule compare one shape rather than two.

type RenderPipeline

type RenderPipeline struct {
	// contains filtered or unexported fields
}

RenderPipeline is a compiled render pipeline.

func (*RenderPipeline) Close

func (p *RenderPipeline) Close() error

Close releases the pipeline.

func (*RenderPipeline) Label

func (p *RenderPipeline) Label() string

Label is the pipeline's label, which appears in every error about it.

type RenderPipelineDescriptor

type RenderPipelineDescriptor struct {
	Vertex   *Stage
	Fragment *Stage

	// VertexBuffers is the vertex input layout. A stage that reads no attribute
	// needs none; one that does needs every attribute declared exactly once.
	VertexBuffers []VertexBufferLayout

	Primitive    PrimitiveState
	DepthStencil *DepthStencilState
	Targets      []ColorTargetState

	Label string
}

RenderPipelineDescriptor describes a render pipeline to create.

A pipeline is compiled once, outside the frame loop, and referenced by nodes: creating one is expensive on every backend, because it compiles shaders and specialises fixed-function state.

type Requirements

type Requirements struct {
	Caps                 Capability
	WorkgroupSize        [3]uint32
	WorkgroupInvocations uint32
	SharedBytes          uint32
}

Requirements is what a compiled kernel needs from a device. It is derived from the kernel body by the kernel compiler, never written by hand. The //accel:requires directive is an assertion checked against this, not a source of it: a mismatch in either direction fails generation.

type SelectionReport

type SelectionReport struct {
	Selected AdapterID
	Rejected []AdapterRejection
}

SelectionReport makes automatic selection reproducible in logs.

type Slot

type Slot int

Slot names a rebindable binding point within one graph.

The zero value is not a slot: ids start at one, so a Binding that forgot to set Slot is a validation error rather than a silent reference to the first one.

type SlotBinding

type SlotBinding struct {
	Slot   Slot
	Buffer BufferView
}

SlotBinding supplies one graph slot with the resource it names.

Its own type, because the bind path and the dispatch path check different things: a slot's descriptor is what a bound resource has to satisfy, and a pipeline's binding layout is what a dispatch has to fill. Binding used to serve both, which meant Bind required Slot *and* Buffer and never read Index — the exact opposite of the one-of-three rule Binding's own documentation states. See specs/036-documentation.md's freeze record.

type SlotDescriptor

type SlotDescriptor struct {
	Name   string // appears in every error about this slot
	Kind   BindingKind
	DType  DType // for buffer kinds; a bound view must match exactly
	Access Access

	// MinCount is the smallest bound range the recorded nodes can be given, in
	// elements of DType. It is the size check moved from build to bind, because at
	// build there is no buffer to measure.
	MinCount int

	// Format constrains a texture slot. FormatInvalid accepts any format the
	// recorded nodes accept.
	Format Format
}

SlotDescriptor declares a rebindable binding point.

type Stage

type Stage = kernel.Stage

Stage is a compiled graphics stage.

An alias for the generated record, the same way Kernel is. A caller does not construct one: `go generate` emits one per stage and the address is the whole of the interface.

type StageAttribute

type StageAttribute = kernel.StageAttribute

StageKind, StageAttribute, StageUniform, StageOutput and StageTexture describe a compiled graphics stage. A caller reads them from a generated Stage and never builds one.

type StageKind

type StageKind = kernel.StageKind

StageKind, StageAttribute, StageUniform, StageOutput and StageTexture describe a compiled graphics stage. A caller reads them from a generated Stage and never builds one.

type StageOutput

type StageOutput = kernel.StageOutput

StageKind, StageAttribute, StageUniform, StageOutput and StageTexture describe a compiled graphics stage. A caller reads them from a generated Stage and never builds one.

type StageTexture

type StageTexture = kernel.StageTexture

StageKind, StageAttribute, StageUniform, StageOutput and StageTexture describe a compiled graphics stage. A caller reads them from a generated Stage and never builds one.

type StageUniform

type StageUniform = kernel.StageUniform

StageKind, StageAttribute, StageUniform, StageOutput and StageTexture describe a compiled graphics stage. A caller reads them from a generated Stage and never builds one.

type StepMode

type StepMode uint8

StepMode is what advances an attribute's index.

const (
	// StepVertex advances per vertex, which is per-vertex geometry.
	StepVertex StepMode = iota

	// StepInstance advances per instance, which is how a per-object transform
	// reaches a stage without a uniform.
	StepInstance
)

func (StepMode) String

func (m StepMode) String() string

type StoreOp

type StoreOp = driver.StoreOp

LoadOp and StoreOp are what happens to an attachment at the start and the end of a pass. They are driver.LoadOp and driver.StoreOp: a backend acts on the value and cannot import this package, so one definition lives where both sides reach it. Two definitions with nothing pinning them together is how LoadKeep and LoadDontCare -- which a backend cannot tell apart by their effect -- would swap silently.

type SubgroupOpSet

type SubgroupOpSet = driver.SubgroupOpSet

SubgroupOpSet reports which subgroup operations a device provides. Vulkan exposes these independently, so presence of one does not imply the others.

It is an alias rather than its own type so that Capabilities and the backend-facing struct it is converted from have identical field types, which makes that conversion a compile-time check on the two staying in step. See internal/driver.

type SubmissionStats

type SubmissionStats struct {
	Indirect []IndirectStats

	// Elapsed is how long the device took, and is zero unless the graph asked
	// with [Recorder.CollectTimings] or the backend has no device clock.
	//
	// Zero rather than a wall-clock substitute: a caller measuring throughput
	// needs "the GPU ran for this long", and the time the call took includes
	// queueing, driver work and whatever else the process was doing. A
	// substitute would answer the wrong question convincingly.
	Elapsed time.Duration
}

SubmissionStats is what one submission's device-written counters reported.

type Surface

type Surface struct {
	// contains filtered or unexported fields
}

Surface hands out images to render into and takes them back to present.

One type rather than an interface per backend, because the state a frame loop depends on — the generation counter, the rotation, what counts as out of date — is the same everywhere and is the part that must not vary. A backend supplies only where the pixels live.

func (*Surface) Acquire

func (s *Surface) Acquire(timeout time.Duration) (*Frame, error)

Acquire hands out the next image.

It takes a timeout and can report expiry, because it can genuinely block: the swapchain may be full, or the compositor may not have released an image. A call described as non-blocking that waits on a compositor is worse than one that says so.

func (*Surface) Close

func (s *Surface) Close() error

Close releases the surface's images.

func (*Surface) Discard

func (s *Surface) Discard(f *Frame) error

Discard hands a frame back without presenting it.

Every acquired frame is either presented or discarded. On a windowed surface the frame holds a drawable the compositor lent it, and one abandoned rather than returned exhausts the pool -- whose symptom is a frame loop that stops, with no error and no stack pointing at the cause.

A caller reaches this when a frame cannot be rendered after all: a graph that failed to build, a resize noticed between acquire and submit, a frame skipped for timing.

func (*Surface) Extent

func (s *Surface) Extent() (int, int)

Extent is the surface's current size.

func (*Surface) Generation

func (s *Surface) Generation() uint64

Generation is how many times the surface has been reconfigured.

Exposed because a stale-frame error names two of them, and a caller reconciling a rebuild wants to see the number the graph was built against.

func (*Surface) Invalidate

func (s *Surface) Invalidate()

Invalidate marks the surface out of date, so the next Acquire reports it.

This is what a windowed surface's compositor does when the window changes under it. Exposed because the headless surface is not a mock: a test of the out-of-date path needs the same entry point the real event takes.

func (*Surface) Label

func (s *Surface) Label() string

Label is the surface's label, which appears in every error about it.

func (*Surface) Present

func (s *Surface) Present(f *Frame, after *Fence) error

Present hands the frame back, to be shown once the fence signals.

The fence is the ordering: it is the submission that rendered into the image, and present must follow it. Passing nil presents immediately, which is only correct when nothing was submitted.

func (*Surface) Resize

func (s *Surface) Resize(w, h int) error

Resize reconfigures the surface, which increments the generation and invalidates every graph built against the old one.

specs/034-surface-present.md section 4.1: attachment extents stay validated at build, so a resize forces a rebuild. That is the cost of a build-time check that catches a mismatched attachment before any device work happens.

type SurfaceDescriptor

type SurfaceDescriptor struct {
	Width, Height int

	// Images is how many rotate. Two is double buffering, which is the least
	// that lets the device render one frame while the compositor holds another.
	Images int

	Label string
}

SurfaceDescriptor configures a surface.

type Texture

type Texture struct {
	// contains filtered or unexported fields
}

Texture is an image in device memory.

func (*Texture) Bytes

func (t *Texture) Bytes() int

Bytes reports the texture's device footprint, which counts the padding a backend's row alignment adds and is therefore at least width*height*bpp.

func (*Texture) Close

func (t *Texture) Close() error

Close releases the texture.

func (*Texture) Format

func (t *Texture) Format() Format

Format reports the texture's format.

func (*Texture) Size

func (t *Texture) Size() Extent

Size reports the texture's extent.

func (*Texture) View

func (t *Texture) View(d TextureViewDesc) (TextureView, error)

View names a subresource of t.

Format reinterpretation, and the one pair it exists for

A non-zero Format reads the same bytes as another format, and is legal only within a compatible family: the same bytes per pixel, the same channel count, and a difference only in the numeric *encoding*. The format table has exactly one such pair today, RGBA8Unorm and RGBA8UnormSRGB, and that pair is why this exists:

write linear through one view, present through an sRGB view of the same
texture

which is how every target expresses it -- Vulkan's image-view format, D3D12's view casting, Metal's newTextureViewWithPixelFormat:, WebGPU's viewFormats -- and what makes sRGB a property of the *view* rather than of the texture. specs/035-cpu-rasterizer.md section 5 says sRGB converts on write and on read; this is what owns that conversion.

Anything outside a family is refused by name, saying both formats and which clause failed, because "incompatible" alone sends a caller to guess.

func (*Texture) Whole

func (t *Texture) Whole() (TextureView, error)

Whole is the view of a texture's base level and first layer, in its own format.

It exists because that is what almost every attachment is, and a caller writing three zero fields to say "all of it" is a caller the API is making work for.

type Texture2D

type Texture2D = kernel.Texture2D

Texture2D is a texture a stage reads, as a stage signature spells it.

//accel:fragment
func Blit(f accel.Fragment, in accel.NoVaryings, src accel.Texture2D) Solid {
	c := f.Coord()
	return Solid{Colour: accel.Fetch(src, int32(c[0]), int32(c[1]))}
}

A distinct type from a slice binding and from a by-value uniform, which is what lets the compiler tell an image binding from a storage buffer without asking the author to say which is which.

It names one subresource. A mip level and an array layer belong to the TextureView a pipeline binds, not to the fetch: one shape names a subresource, so the feedback rule that compares an attachment against a shader-visible binding has one thing to compare.

func NewTexture2D

func NewTexture2D(width, height int, texels []float32) Texture2D

NewTexture2D builds a texture binding over four-component texels in row-major order, row zero being the top row.

It is for a backend binding a texture to a stage and for the tests that check the two lowerings agree, the way NewVertexForTest is: a stage body cannot construct one, because a texture a stage invented is a texture no backend bound.

type TextureDescriptor

type TextureDescriptor struct {
	Format Format
	Size   Extent
	Usage  TextureUsage

	// MipLevels of 0 means one level. Values greater than one are still
	// rejected; see [TextureViewDesc] for what changed and what did not.
	MipLevels int

	// ArrayLayers of 0 means one layer. Values greater than one are rejected
	// for the same reason MipLevels are.
	ArrayLayers int

	// Kind is the memory the implicit pool behind [Device.NewTexture] is taken
	// from. The zero value is [MemoryDevice], which is the right default and the
	// wrong one for exactly one thing: [Queue.ReadTexture] needs mappable
	// memory, so a texture you intend to read back on the host must ask for
	// [MemoryReadback] here.
	//
	// The field exists because without it the convenience constructor produced
	// a texture the only readback method could never read, and nothing said so
	// until the call failed. It is ignored by [Pool.AllocTexture], where the
	// pool already fixed the answer.
	Kind MemoryKind

	Label string
}

TextureDescriptor describes a texture to create.

type TextureUsage

type TextureUsage uint32

TextureUsage declares how a texture will be used.

const (
	TextureSampled TextureUsage = 1 << iota
	TextureStorage
	TextureRenderTarget
	TextureCopySrc
	TextureCopyDst
)

func (TextureUsage) String

func (u TextureUsage) String() string

String names the usages set, so a diagnostic reads as what a caller wrote rather than as a number they have to decode.

type TextureView

type TextureView struct {
	Texture *Texture
	Mip     int
	Layer   int

	// Format is always concrete here, resolved from the texture when the
	// descriptor left it zero. A view that carried the zero value would make
	// every consumer repeat the resolution, and one of them would forget.
	Format Format
}

TextureView is one subresource of a texture, read as one format.

Why a view rather than fields on an attachment

specs/033-render-api.md section 3.3 forbids an overlapping subresource being both an attachment and shader-visible, and permits disjoint ones -- a different mip or a different array layer is a different subresource. That comparison is sound only if both sides name a subresource the same way.

Spelling the mip and the layer on the attachment, and again on a stage's binding, would make the rule compare two shapes. This project has withdrawn two validation rules already -- check V23, and 033 section 6's undeclared-slot rule -- and both were written against a shape nobody had tested a caller against. One type is how the third is avoided.

It is a value rather than a handle for BufferView's reason: a view owns nothing, so there is nothing to close and no lifetime to get wrong. The texture it names is the thing with a lifetime.

type TextureViewDesc

type TextureViewDesc struct {
	// Mip is the mip level, zero being the base.
	Mip int

	// Layer is the array layer, zero being the first.
	Layer int

	// Format reinterprets the same bytes. Zero means the texture's own, which
	// is what almost every view wants; see [Texture.View] for what a non-zero
	// value may be.
	Format Format
}

TextureViewDesc selects a subresource of a texture, and optionally reads it as a different format.

type Thread

type Thread = kernel.Thread

Thread is a kernel's first parameter. It carries the invocation's ids and, once cooperative kernels exist, the CPU backend's rendezvous state.

A kernel author writes accel.Thread and never names an internal package. It is an alias because the backend that executes kernels cannot import this package, so the type has to be declared below it; that also makes the generated code's accel.Thread and the runtime's kernel.Thread one type rather than two that have to be converted. See specs/012-kernel-pipeline.md.

type Topology

type Topology uint8

Topology is how vertices group into primitives.

const (
	TriangleList Topology = iota
	TriangleStrip

	// LineList, LineStrip and PointList are named so a caller's value maps onto
	// the full enumeration, and are refused. specs/035-cpu-rasterizer.md
	// section 10 leaves their rules open: lines have a diamond-exit rule on some
	// backends and a Bresenham-ish rule on others, and points have a size and a
	// centre convention. Guessing one would put an unstated rule in the oracle
	// every backend is checked against.
	LineList
	LineStrip
	PointList
)

func (Topology) String

func (t Topology) String() string

type TransientPlacement

type TransientPlacement struct {
	Label string

	// Offset and Bytes are its extent in the pool. Two placements sharing bytes
	// is aliasing, and it is only sound when every user of one is ordered
	// against every user of the other.
	Offset int
	Bytes  int

	// Users are the nodes that touch it, in record order. Reported because the
	// interference relation quantifies over exactly this set, so a caller
	// checking a placement by hand needs it.
	Users []NodeID
}

TransientPlacement is where one builder-owned intermediate landed in the graph's transient pool.

type TransientPool

type TransientPool struct {
	// contains filtered or unexported fields
}

TransientPool is device memory several graphs plan their intermediates into.

pool, _ := dev.NewTransientPool("buckets")
defer pool.Close()

r := dev.NewRecorder()
r.UseTransientPool(pool)
...

Why you would want one

A set of graphs over one model — a plan per prefill bucket, say — has one set of intermediates each, and only one of them ever runs at a time. Five buckets at 200 MiB is a gigabyte of device memory of which 800 MiB is idle. Sharing a pool makes that 200 MiB.

What it costs

Graphs sharing a pool cannot execute together, and the second one is **refused** rather than queued: its fence carries the error, which is how every other submission failure arrives. For a bucket set that costs nothing, because a request runs in one bucket. For two graphs you wanted to overlap it is the wrong tool, and the error says so — a pool that queued silently would turn a design mistake into a latency mystery.

Submitting them one after another is always fine. A claim covers execution and not the wait in front of it, so a queue that runs them in turn never trips the rule.

It grows to fit the largest graph built into it and never shrinks. Building is the only moment it may resize, because a submission holds device addresses into it.

See specs/031-shared-transients.md.

func (*TransientPool) Bytes

func (p *TransientPool) Bytes() int

Bytes reports what the pool has allocated, which is the largest requirement among the graphs built into it.

A different question from Graph.Memory, which reports what one graph's transients need. One is "what did we reserve" and the other is "what does this plan cost".

func (*TransientPool) Close

func (p *TransientPool) Close() error

Close frees the pool's memory.

Refused while any graph built into it is open, because those graphs hold offsets into memory this would free. Closing a graph built into a pool does not free the pool: the pool is the caller's, which is what makes it shareable.

func (*TransientPool) Graphs

func (p *TransientPool) Graphs() int

Graphs reports how many built graphs share this pool.

type UniformBuffer

type UniformBuffer[T any] struct {
	// contains filtered or unexported fields
}

UniformBuffer owns a generated-codec uniform allocation.

It exists so that a value may change between submissions without changing graph structure: UniformBuffer.Write encodes through the queue, and the graph's binding does not move. A caller who wrote std140 bytes into an ordinary buffer would be doing the codec's job with the padding hidden.

Nothing consumes one yet, and that is stated rather than discovered

UniformBuffer.View returns a binding no draw and no dispatch is parameterised by today. A compute kernel takes its uniform block as a by-value parameter, and a render stage takes one as pass state through RenderPass.SetVertexUniform — neither reads a buffer. The mechanism that would use this is a draw at a recorded byte offset, which specs/033-render-api.md deviation 1 removed and did not replace, and specs/042-surface-completion.md §3.1 records as outstanding.

So a caller can allocate one, write through it, and read the bytes back — UniformBuffer.Buffer and an ordinary read do that — and cannot yet hand it to anything that draws. It is exported because the encoding half is correct and is what a caller would otherwise reimplement with the padding hidden, and this paragraph exists because a type that advertises a capability it does not have is worse than one that says so.

func NewUniformBuffer

func NewUniformBuffer[T any](d *Device, codec UniformCodec[T]) (*UniformBuffer[T], error)

NewUniformBuffer allocates storage sized and aligned by codec.

func (*UniformBuffer[T]) Buffer

func (u *UniformBuffer[T]) Buffer() *Buffer

Buffer is the underlying allocation, for binding it into a graph.

func (*UniformBuffer[T]) Close

func (u *UniformBuffer[T]) Close() error

Close releases the allocation.

func (*UniformBuffer[T]) View

func (u *UniformBuffer[T]) View() (BufferView, error)

View is the whole block as a binding range.

func (*UniformBuffer[T]) Write

func (u *UniformBuffer[T]) Write(q *Queue, value T) error

Write encodes value into the buffer through a queue.

type UniformCodec

type UniformCodec[T any] interface {
	// EncodedSize is the block's size in bytes, rounded up to sixteen. It is
	// what a uniform buffer is allocated at and what a pipeline's declared block
	// size is checked against.
	EncodedSize() int

	// Encode writes value into dst, which must be at least EncodedSize long.
	Encode(dst []byte, value T) error
}

UniformCodec is generated for one by-value kernel parameter type. It owns the std140 size, alignment, field offsets, and typed encoder.

A caller never writes one and never spells an offset. The padding std140 imposes is not Go's, and a caller who computed it by hand would be right for a struct of four floats and wrong for the first one containing a three-component vector. See specs/001-device-resources.md section 3.3.

type UniformValue

type UniformValue struct {
	Index int
	Value any
}

UniformValue is one by-value parameter of a dispatch.

Index names the kernel's by-value list — its own space, separate from Binding.Index's. A kernel's signature interleaves the two, so neither index is the parameter position; the generated record carries both lists and the error names the kernel when they disagree.

type UniformWriter

type UniformWriter struct {
	// contains filtered or unexported fields
}

UniformWriter is the low-level half a generated codec is built from.

It exists so a generated encoder is a list of field writes rather than a list of byte offsets: the offsets are computed once, by the layout, and the generated code names members. A caller who already manages a uniform arena may use it directly and still never spells padding.

func NewUniformWriter

func NewUniformWriter(dst []byte) *UniformWriter

NewUniformWriter wraps a destination buffer.

func (*UniformWriter) Err

func (w *UniformWriter) Err() error

Err reports the first failure, so a generated encoder can write every field and check once rather than checking after each.

func (*UniformWriter) F32

func (w *UniformWriter) F32(offset int, v float32)

F32 writes a 32-bit float at a std140 offset.

func (*UniformWriter) I32

func (w *UniformWriter) I32(offset int, v int32)

I32 writes a signed 32-bit integer at a std140 offset.

func (*UniformWriter) U32

func (w *UniformWriter) U32(offset int, v uint32)

U32 writes an unsigned 32-bit integer at a std140 offset.

type Unmet

type Unmet struct {
	Cap       Capability // zero when the unmet requirement is a limit
	Limit     string     // the Capabilities field that was exceeded, if any
	Required  uint64
	Available uint64
}

Unmet is one requirement a device does not meet. It carries what was required and what the device reports, because an error saying only that a capability is missing does not tell a caller whether to change the kernel or the device.

type UsageError

type UsageError struct {
	Resource string
	Node     NodeID
	Slot     int
	Declared BufferUsage
	Needed   BufferUsage
	Site     string // the recording call site, per spec 003
}

UsageError reports a resource used in a way it did not declare.

Over-declaring a usage costs alignment and possibly a stricter memory type; under-declaring is a bug. The error therefore lands on the side that is wrong, and it names the recording call site so the fix is at the declaration rather than at the use.

func (*UsageError) Error

func (e *UsageError) Error() string

func (*UsageError) Unwrap

func (e *UsageError) Unwrap() error

type Vec2

type Vec2 = kernel.Vec2

Vec2, Vec3 and Vec4 are the vector spellings a kernel signature uses.

Aliases for Go arrays rather than new named types, because the kernel language already spells a vector that way: std140 maps [3]float32 to a three-component vector consuming twelve bytes aligned to sixteen, and the uniform encoder is generated from exactly those array types. A parallel set of named vector types would give the compiler two spellings for one thing, and the second is the one nobody teaches the layout code about.

type Vec3

type Vec3 = kernel.Vec3

Vec2, Vec3 and Vec4 are the vector spellings a kernel signature uses.

Aliases for Go arrays rather than new named types, because the kernel language already spells a vector that way: std140 maps [3]float32 to a three-component vector consuming twelve bytes aligned to sixteen, and the uniform encoder is generated from exactly those array types. A parallel set of named vector types would give the compiler two spellings for one thing, and the second is the one nobody teaches the layout code about.

type Vec4

type Vec4 = kernel.Vec4

Vec2, Vec3 and Vec4 are the vector spellings a kernel signature uses.

Aliases for Go arrays rather than new named types, because the kernel language already spells a vector that way: std140 maps [3]float32 to a three-component vector consuming twelve bytes aligned to sixteen, and the uniform encoder is generated from exactly those array types. A parallel set of named vector types would give the compiler two spellings for one thing, and the second is the one nobody teaches the layout code about.

func Fetch

func Fetch(t Texture2D, x, y int32) Vec4

Fetch reads the texel at an integer coordinate. There is no sampler.

Integer fetch is admitted and filtering is not, because a fetch has one definition every backend agrees on and a hardware sampler does not: its half-texel addressing, its LOD rounding and its per-tap integer lerps are per-vendor, so a filtered sample is a feature the CPU oracle cannot check. A stage that wants filtering builds it from fetches, where the arithmetic is the stage's own and is compared like any other.

**A coordinate outside the texture returns Vec4{}**, on every backend, with negative coordinates included — the coordinates are signed so that the ordinary neighbourhood read at the left or top edge is representable rather than wrapping to an enormous unsigned index. See specs/032-stage-abi.md section 5.

type Vertex

type Vertex = kernel.Vertex

Vertex carries one vertex invocation's identity, and is a vertex stage's first parameter.

//accel:vertex
func Geometry(v accel.Vertex, xf Transforms,
	pos accel.Vec3, uv accel.Vec2) (accel.Clip, Varyings)

Deliberately not Thread: a vertex stage has no workgroup, no barrier, no shared memory and no subgroup, so handing it Thread would make three quarters of that type's methods a compile-time trap rather than an unavailable one.

func NewVertexForTest

func NewVertexForTest(vertex, instance uint32) Vertex

NewVertexForTest and NewFragmentForTest build a stage receiver directly.

They exist because a stage's receiver has unexported fields — an index a caller can set is an index the backend does not own — and the differential test that checks a generated lowering against its authored source has to hand both the same one. Nothing else should call them: a real invocation's identity comes from the rasterizer, which is specs/035-cpu-rasterizer.md's.

Named ForTest rather than hidden behind a build tag, so that the name says what it is at every call site rather than only in the file header.

type VertexAttribute

type VertexAttribute struct {
	Location int
	Format   AttrFormat
	Offset   int
}

VertexAttribute is one attribute inside one buffer.

Location is the stage's attribute index, not the parameter position: the receiver and any uniforms are interleaved with the attributes in the authored signature, so the two numbers differ as soon as a stage takes a uniform.

type VertexBufferLayout

type VertexBufferLayout struct {
	Stride     int
	StepMode   StepMode
	Attributes []VertexAttribute
}

VertexBufferLayout is one bound buffer and what is packed inside it.

Stride is the distance between consecutive elements, which is not the sum of the attribute sizes: a caller may pad for alignment, and a buffer that feeds two stages carries attributes neither one alone reads.

type WorkgroupCount

type WorkgroupCount struct{ X, Y, Z int }

WorkgroupCount is how many workgroups a dispatch runs.

This counts workgroups, not threads, deliberately. A thread count makes the workgroup size invisible to the caller, which is how a predecessor project ended up dispatching one thread per workgroup and leaving the hardware idle. For a direct dispatch, omitted Y or Z values normalize to one. X must be positive. Indirect dispatches keep zero as the specified skip mechanism.

Directories

Path Synopsis
cmd
accel-kernel command
Command accel-kernel compiles kernels written in the Go subset.
Command accel-kernel compiles kernels written in the Go subset.
internal
alloc
Package alloc carves a pool into allocations.
Package alloc carves a pool into allocations.
conformance/cover
Package cover reports per-package statement coverage under spec 011 section 10's checked exclusions.
Package cover reports per-package statement coverage under spec 011 section 10's checked exclusions.
conformance/cover/covercheck command
Command covercheck reports per-package coverage and fails below the gate.
Command covercheck reports per-package coverage and fails below the gate.
conformance/device
Package device is the conformance harness's device runner: discovery, profiles, modes, and skips.
Package device is the conformance harness's device runner: discovery, profiles, modes, and skips.
conformance/direct
Package direct runs a generated flat kernel without a device.
Package direct runs a generated flat kernel without a device.
conformance/numeq
Package numeq compares results against references.
Package numeq compares results against references.
conformance/probe
Package probe measures what a backend's arithmetic actually does.
Package probe measures what a backend's arithmetic actually does.
cpu
Package cpu is the pure-Go backend.
Package cpu is the pure-Go backend.
driver
Package driver is the seam between the public accel API and a backend.
Package driver is the seam between the public accel API and a backend.
kernel
Package kernel is the vocabulary a compiled kernel executes in.
Package kernel is the vocabulary a compiled kernel executes in.
kernelc
Package kernelc drives the kernel compiler: load, check, digest, emit, write.
Package kernelc drives the kernel compiler: load, check, digest, emit, write.
kernelc/emit
Package emit turns the typed IR into the generated Go lowering.
Package emit turns the typed IR into the generated Go lowering.
kernelc/front
Package front turns type-checked Go into the typed IR, or into positioned rejections.
Package front turns type-checked Go into the typed IR, or into positioned rejections.
kernelc/intrin
Package intrin resolves an intrinsic by object identity.
Package intrin resolves an intrinsic by object identity.
kernelc/ir
Package ir is the typed structured IR every target is emitted from.
Package ir is the typed structured IR every target is emitted from.
kernelc/std140
Package std140 lays out a Go struct the way a uniform block is read.
Package std140 lays out a Go struct the way a uniform block is read.
kernelc/uniform
Package uniform decides which values are equal across a workgroup.
Package uniform decides which values are equal across a workgroup.
metal
Package metal is the Metal backend.
Package metal is the Metal backend.
mslabi
Package mslabi is the buffer numbering the MSL emitter writes and the Metal backend binds against.
Package mslabi is the buffer numbering the MSL emitter writes and the Metal backend binds against.
mtl
Package mtl is the Objective-C shim the Metal backend is built on.
Package mtl is the Objective-C shim the Metal backend is built on.
raster
Package raster is the fixed-function half of the CPU reference rasterizer.
Package raster is the fixed-function half of the CPU reference rasterizer.
testkernels
Package testkernels is the kernel corpus the compiler is developed against.
Package testkernels is the kernel corpus the compiler is developed against.
Package kernelabi is the contract between a generated kernel and the runtime that executes it.
Package kernelabi is the contract between a generated kernel and the runtime that executes it.
Package kmath is the scalar math a kernel may call.
Package kmath is the scalar math a kernel may call.
Package quant turns weights into the form a quantized kernel reads.
Package quant turns weights into the form a quantized kernel reads.
Package tensor is the layer that turns a model into device work.
Package tensor is the layer that turns a model into device work.
internal/pagetable
Package pagetable is the block pool behind a paged KV cache.
Package pagetable is the block pool behind a paged KV cache.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL