The GPU API and its backend proofs
Contract, mechanism, and open proof stay distinct #V9H2RK
This is the current map of luv's GPU layer. It follows the portable command vocabulary through the Vulkan and Metal implementations, with particular attention to asynchronous work, ownership, synchronization, and destruction. Older implementation snapshots and the sequence of experiments that produced this arrangement remain available in Git; they are not separate entry points in the living wiki.
Three kinds of statement appear below:
- A portable contract is observable through
hal/gpu.lisp, such as the documented asynchronous meaning ofsubmit. - A backend proof is the concrete ownership or synchronization mechanism by which one implementation upholds some part of that contract.
- An open proof is a useful name or operation whose current implementations do not yet establish a complete backend-neutral guarantee.
This distinction matters because a WebGPU-shaped name is not itself a WebGPU promise, and a working Vulkan or Metal path is not by itself a portable contract. The source links and limitations on this page are part of the map, not footnotes to it.
Mentioned in: Scope and method
The portable GPU API is a tested seam, not a complete contract #A7N4XP
The luv system's modules now make the intended boundaries literal without
turning every implementation layer into a public ASDF system:
hal/gpu.lisp descriptors, commands, generic operations
|\
| +--> hal/vulkan/ -----> owned Vulkan ABI
|
`----> hal/metal/ ------> LUV.METAL / Objective-C / Metal 4
hal/canvas.lisp --> hal/sdl/ --> backend presentation contexthal/gpu.lisp contains no Vulkan or Metal calls. It names providers,
devices, queues, resources, descriptors, command structs, encoders, passes,
finish, submit, submitted-work-done, host reads and writes, and explicit
destroy. Presentation remains in the separate canvas protocol.
Both backends now render the block world through the same command lifecycle:
create a general encoder, record commands and passes, finish it into a
one-shot backend command buffer, and asynchronously submit that product to a
queue. Metal's canvas follows this lifecycle too; its only private extension
is the drawable signal and presentation handshake after commit. The backends
still do not implement equal feature surfaces, but luvcraft no longer needs to
know which command-submission or lifetime mechanism its selected provider uses.
This is a useful small-system boundary. It permits direct backend composition and does not require a capability framework, adapter hierarchy, or lowest common denominator before a concrete need appears.
Mentioned in: 4. The HAL boundary is only half-owned, What sb-simd teaches now that it can run here
WebGPU is a landmark, not luv's specification #W6P3JH
Luv began with a deliberately WebGPU-shaped vocabulary because WebGPU makes several hard GPU obligations unusually visible: commands are validated future actions, usage declarations carry advance information, submission is asynchronous, and logical object state is distinct from physical native lifetime. It is a source of questions and worked semantics, not an authority that luv implements by resemblance.
Current luv deliberately shares some of that shape:
- command and pass scopes are explicit;
- finished Vulkan command buffers are one-shot;
- recorded commands name semantic usages rather than native barriers;
submitis documented as scheduling rather than completion;submitted-work-donenames an explicit completion wait; and- resource wrappers have explicit invalid states and explicit destruction.
Luv does not currently promise WebGPU's adapter and limit model, validation scopes, device-loss model, mapping state machine, deterministic zero initialization, browser security boundary, or complete usage-scope rules. Those omissions are decisions still to make or work still to prove, not implicit inheritance from the borrowed nouns.
The useful reading discipline is therefore: ask what observable promise a name makes in luv, then identify the component that owns its proof. When there is no such component, the resemblance is only vocabulary.
Mentioned in: Scope and method
GPU work crosses several timelines #T4V8QM
The caller can finish describing a command before the queue accepts it, and the queue can accept it before the GPU finishes executing it. These are orders of events, not necessarily distinct OS threads:
flowchart LR A["application creates and encodes"] --> B["finished command owns recorded work"] B --> C["queue submission owns in-flight work"] C --> D["completion frontier passes submission"] D --> E["backend may retire retained native state"]
The generic submit documentation states the consequence directly: returning
does not mean GPU completion; the implementation must retain everything the
work depends on until it completes. submitted-work-done blocks at the
queue's current completion point when a caller really needs that proof.
This separation is why several tempting equations are wrong. A Lisp object becoming unreachable is not GPU completion. A wrapper being marked destroyed is not necessarily native release. The end of a presented frame is not a general reclamation boundary. A completion proof also says nothing by itself about whether the right memory barriers were encoded.
Mentioned in: The wiki itself
Inspectable commands are protocol inputs, not a retained command IR #D8F3QM
The portable command structs and double dispatch through encode and
enqueue are a real architectural seam. Command kinds are separated by
scope: queue, command encoder, render pass, and compute pass. Convenience
functions such as draw and prepare-texture merely construct those command
objects. Unsupported command/encoder pairs fail through the generic
fallback, so a backend grows by specializing concrete relationships rather
than extending one opcode switch.
The objects are nevertheless transient inputs. Each backend lowers a command
immediately into its native command buffer. finish transfers native command
memory and retained wrappers into either a one-shot
vulkan-gpu-command-buffer or metal-gpu-command-buffer; it does not preserve
the high-level command sequence for replay or another lowering.
That choice is coherent as long as the purpose is legible dispatch, validation, tracing, and dependency capture. A durable backend-independent command IR would be a different feature with different ownership and transformation requirements; the current structs should not be described as one merely because they are inspectable while dispatched.
Mentioned in: 2. McCLIM implements one open command vocabulary four times, Events are buffered facts, polled after the step
Recorded and submitted work must own their dependencies #L8R2WF
One GPU resource participates in several overlapping lifetimes:
- application code can still reach its Lisp wrapper;
- the wrapper is logically valid for new operations;
- an encoder or finished command may retain it as a recorded dependency;
- a queue submission may retain it for work the GPU can still execute; and
- its native object and allocation can finally be released.
Vulkan makes those ownership transfers explicit. Encode methods retain every
wrapper referenced by the current command vocabulary: pipelines, bind groups,
views, samplers, buffers, and textures. finish moves the set into the
command buffer. Submission stamps the wrappers with the new submission index
and keeps the command buffers and their captured resources in a queue-owned
record until completion.
Metal now makes the same ownership transfers. Encode methods capture every
wrapper referenced by its implemented render, clear, copy, preparation, and
binding vocabulary. finish moves that set, the native command buffer, and
its allocator into one-shot work. Public submission stamps all dependencies
with a shared-event value and retains them in the queue record. The residency
set keeps their allocations resident until deferred destruction removes them.
Garbage collection concerns only the first lifetime. It cannot replace the recorded and submitted ownership edges, and native API retention cannot by itself give a Lisp wrapper a useful logical destroyed state.
Mentioned in: The wiki itself
Each backend has a completion frontier of a different extent #T9K4RC
Both live backends now represent completed work as a monotonically valued queue frontier, but the records attached to that frontier are different.
| Vulkan | Metal 4 | |
|---|---|---|
| Native completion value | timeline semaphore | shared event |
| Submitted value | queue submission index | queue submission value |
| Retained until completion | command buffers, captured wrappers, native temporaries | command buffers, allocators, captured wrappers |
General public submit | yes | yes |
| Maintenance points | submission, explicit wait, and eventual retirement service | submission, explicit wait, and eventual retirement service |
submit-vulkan-command-buffers signals the queue timeline, installs a
vulkan-gpu-submission record, and returns its index. Queue maintenance reads
the semaphore counter and retires the completed prefix. Vulkan 1.4,
vkQueueSubmit2, timeline semaphores, and synchronization2 are deliberate
minimums; there is no fence or legacy-submit fallback.
submit-metal-command-buffers commits a batch of finished Metal 4 work,
registers command memory and captured dependencies before presentation can
fail, and records the shared-event signal itself as a retryable post-commit
obligation. Reclamation drops the retained record when the event value passes
it. The current Metal page #NABRMK follows that backend's larger compiler and
rendering story.
A nonempty submission or retirement ledger process-roots its queue. A lazy service thread polls only those custodians, under their backend queue locks, and exits when the registry becomes empty. Explicit submission and wait calls remain useful low-latency maintenance points; the service exists so abandoning the last application reference cannot turn safe delayed reclamation into a permanent root cycle.
Mentioned in: Determinism is a property of the whole design, not a mode, Select the phase once, then run closed loops, Questions to keep open
Referenced from code: Commit finished Metal work and retain its dependencies to Register queue ownership before presentation can fail. Presentation must be ordered before completion, but failure to
enqueue that completion is now a rooted retryable queue obligation.defun submit-metal-command-buffers gpu.lisp:604 ↗
queue's frontier.after-commit performs the drawable signal and presentation handshake before
the completion event is enqueued. Presentation is the only caller of that
backend-local extension; ordinary callers use submit. #T9K4RC
Destroy is logically immediate and physically conditional #D3H7VK
Both resource implementations now give destroy this shape. While a backend
queue is live, the operation first transfers a complete native teardown into a
FIFO retirement ledger, tagged with the resource's last submitted use, and
only then marks the wrapper destroyed. The ledger runs the eligible prefix as
the queue frontier advances. Without a live queue, teardown must succeed
synchronously before logical invalidation.
Native teardowns are sequences of individually resumable calls. A failure therefore retains that entry and its dependency-ordered successors without repeating calls which already succeeded. The process custodian keeps the queue reachable until submissions, post-commit publication, and retirement are all quiescent; device destruction closes admission and refuses to pass a nonempty ledger.
This division is essential rather than stylistic. Native destruction requires every submitted use to have completed, while a pleasant API should not make every explicit destroy drain the queue. Complete command dependency capture is what makes the deferred half safe. Lightweight semantic wrappers with no independent native ownership still only change logical state.
Mentioned in: String figures, The wiki itself
Explicit destruction is the discipline; collection is only a safety net #G6C2TX
Every backend wrapper has an explicit destroyed state. That gives a live Lisp object an invalid state which later operations can reject; continued CLOS reachability does not make a destroyed GPU object usable.
Vulkan wrappers also install finalizers. Explicit destruction cancels the
finalizer. If the collector instead finds a live wrapper, the finalizer first
transfers its progress-tracked native teardown into a process-global ledger,
then warns with gpu-resource-leaked and records the condition in
*leaked-gpu-resources*. Thus even a warning handler which exits cannot lose
native custody. Submission records hold in-flight wrappers strongly, so an
ordinary collected child is already past the frontier; its fallback teardown
still shares queue/device exclusion and the Vulkan driver environment.
Metal currently has explicit destruction but not the equivalent finalizer
audit. Dropping the last Lisp reference without destroy can therefore leak
native ownership without a warning. Both backends durably service ownership
which was explicitly transferred, but neither maintains a general child
registry that turns device teardown into automatic orderly destruction of
every still-live wrapper.
The portable discipline is consequently simple and honest: callers explicitly destroy what they create. Vulkan's collector path is failure recovery and a REPL diagnostic, not the normal resource manager.
Mentioned in: An id is an index, a generation, and a world, The sparse–dense split is the row/column distinction with a forwarding address
Vulkan frame slots are pacing clients of the frontier #M6P8XW
The Vulkan canvas keeps a bounded ring of frame slots. Each slot owns an image-acquisition binary semaphore and, while occupied, the frame's command buffer plus its queue submission index. Reusing a slot waits for the queue frontier to reach that index, then destroys the retained command buffer. Render-finished semaphores are indexed separately by acquired swapchain image.
The distinction is load-bearing:
- the queue frontier proves when submitted dependencies may retire;
- binary semaphores coordinate swapchain acquisition and presentation; and
- the slot count bounds how far CPU frame production can run ahead.
Because Vulkan resource destruction now defers through the general queue records, the slot wait is a pacing choice rather than a second lifetime system. A resource can also be used by compute or copy work with no frame at all, so “end of frame” is not the general reclamation unit.
Metal presentation has no parallel Lisp frame-slot abstraction. Its drawable pool bounds presentation resources, while the same general queue submission records used by offscreen work retain command memory and dependencies. This keeps drawable pacing separate from the lifetime proof completed by #JAM5XR.
Mentioned in: The wiki itself, Memory is four allocators, none of them general
Usage names hazards; each backend still owns the synchronization proof #X2D7MF
Descriptors and recorded commands give the backend advance information:
buffer and texture usage, attachment roles, bindings, copy directions, and the
semantic destination of prepare-texture. Application code does not emit
Vulkan stage masks or Metal barriers directly. That simplicity relocates the
proof into each backend.
Vulkan currently tracks one scheduled layout for each whole texture. An
encoder records the initial layout it assumed and its final layouts; submit
validates those assumptions against earlier queued work, installs the new
scheduled state, and emits a synchronization2 image barrier when the layout
changes. This fits today's single-mip, single-layer textures. It is not a
complete access-hazard model: two uses can remain in general while a write
followed by a read still needs a memory dependency.
Metal emits concrete barriers for the command relationships exercised by the
renderer, including producer/consumer transitions installed by
prepare-texture. It likewise does not maintain a general subresource access
history derived from all usage scopes.
The completion frontier proves that old work has finished. It does not prove that work was ordered correctly while in flight. Lifetime capture, scheduled state, and access hazards are related bookkeeping, but none can stand in for the other two.
Host access is still a deliberately narrow path #B9Q4LC
Both backends create CPU-visible buffers and keep them mapped: Vulkan uses
host-visible coherent memory; Metal uses shared storage. write-buffer
copies a one-dimensional single-float array into that mapping. It does not
wait, acquire a mapped state, track a byte range, or exclude simultaneous GPU
reads. Current rendering code supplies the missing rule by rotating
per-frame or per-drawable buffers and writing only the one safe for the frame.
read-buffer is conservative in the other direction: it calls
submitted-work-done for the whole device queue, then copies bytes from the
mapping. There is no per-buffer last-use wait or asynchronous mapping
callback.
This is useful, executable functionality, not yet a general memory model. In particular luv does not currently promise a WebGPU-like mapped/unmapped state machine, staging and readback policy, cache policy beyond the chosen storage, or deterministic zero initialization. Those contracts should be added only with an owner and a testable exclusion rule.
The remaining questions are local and testable #V3N7QJ
The architecture no longer needs a speculative rewrite to become clearer. Its open proofs are adjacent to working mechanisms:
- make dependency capture completeness visible when a new command kind is added;
- grow texture state into the access-hazard model demanded by real workloads;
- give host writes and reads a resource-specific exclusion protocol;
- decide what adapter selection, capabilities, device loss, and initialization guarantees luv actually wants; and
- reclaim completed records during long idle periods only if measurement says the current maintenance points retain too much.
Each item can be proved through one concrete caller and one backend path. The current seams are small enough that this is preferable to importing a generic resource manager or treating WebGPU conformance as the design goal.
(queue command-buffers &key after-commit)Commit finished Metal work and retain its dependencies to QUEUE's frontier. AFTER-COMMIT performs the drawable signal and presentation handshake before the completion event is enqueued. Presentation is the only caller of that backend-local extension; ordinary callers use SUBMIT. #T9K4RC
(queue work)Schedule some command buffers on the QUEUE. Submission is asynchronous: returning does not mean the GPU has finished the work, only that the implementation retains everything the work depends on until it completes. Use SUBMITTED-WORK-DONE to wait.
(object operation)(queue)(queue command-buffer)(command-buffers)(queue command-buffers)(ledger custodian)The maximum of compatible quantities.
Moppe first supplied the motivation: its Metal renderer proves mesh shaders, MetalFX, and a finished post stack on the same Apple hardware, while MoltenVK cannot expose every native capability. That source evidence remains on the Moppe legacy page. The implementation claim now belongs here. The current backend is…
Intent: preserve logical frame-boundary publication while removing the conservative queue stall from Metal resource destruction. Evidence: every currently supported Metal encode relationship captures its buffers, textures, views, samplers, bind groups, and pipelines in the general encoder. finish transfers that set…
Both live backends now represent completed work as a monotonically valued queue frontier, but the records attached to that frontier are different. submit-vulkan-command-buffers signals the queue timeline, installs a vulkan-gpu-submission record, and returns its index. Queue maintenance reads the semaphore counter and…
Device teardown closes admission under this lock. Recheck after any wait to make this the authoritative gate before maintenance and FFI.