luv

Workshop wiki

metal-backend.org

The native Metal 4 backend

The seams now meet in one Metal 4 draw #4M91DG

The native Metal backend is no longer only an architectural proposal. Luv can create a Metal GPU provider, attach it to an SDL-owned CAMetalLayer, compile two live mathematical shader methods through a device-owned MTL4Compiler, link them into MTLRenderPipelineState, bind a shared MTLBuffer by GPU address through MTL4ArgumentTable, and issue a completed draw through the ordinary GPU command vocabulary. The remaining backend threshold is the full block material: uniform, texture, sampler, depth, and mesh resources must cross the same path before a pinned capture can be compared with Vulkan.

SDL canvas and events
        |
        | SDL_MetalView / borrowed CAMetalLayer
        v
Metal canvas context ---- Objective-C declarations ---- libobjc + Metal
        |                              |
        |                              `---- native ABI help where required
        v
luv GPU protocol
        ^
        |
typed shader specification
        |\
        | `---- MSL lowering ---- Metal pipeline ---- argument table ---- draw
        `------ SPIR-V lowering - Vulkan pipeline                    WORKING

The seams remain independently understandable: SDL owns the host, the Objective-C substrate (#M2PDTP) owns the Apple object boundary, and the shader language owns target-specific lowering. They are now staged proofs of one backend rather than three reasons to postpone starting it. #5A7QDE records the executable presentation half, #58IDSR the compiler half, and #PH57K5 the remaining block-material join. #348B7B records why argument tables implement resource binding here rather than decorate a legacy encoder path. #NABRMK records why the second backend matters and #HXUGEA why Metal 4 is the right native model for it.

A native Metal backend now stands beside Vulkan #NABRMK

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 deliberately narrow but real:

The canvas now uses that same general submission contract rather than a frame-owned shortcut: it creates an encoder through create, records ordinary commands, obtains one-shot work from finish, and calls the queue's submit. Only drawable signaling and presentation remain backend-local. This is enough to make asynchronous submission, dependency ownership, and destruction a portable contract for the implemented command vocabulary. It does not claim equal backend feature surfaces or WebGPU conformance. #HXUGEA describes the explicit backend mechanisms shared semantically.

Metal 4's explicit model matches the Vulkan layer #HXUGEA

Metal 4 asks the application to own memory, synchronization, hazards, and resource lifetime explicitly. That is closer to luv's modern Vulkan layer than Metal 3's implicit model would be:

The correspondence is now represented by the Lisp objects as well as the native calls. A finished metal-gpu-command-buffer owns its allocator, native command buffer, and every wrapper captured while encoding. Submission stamps those dependencies with a monotonically increasing shared-event value and retains them in a queue record until the frontier passes. Metal supplies no safety net for a missing barrier, so this lifetime parity is not a substitute for hazard proofs. The useful standard is semantic parity with explicit backend mechanisms, not line-for-line similarity.

Argument tables realize semantic binding groups #348B7B

Apple's Metal 4 core API account says that Metal 4 encoders do not bind individual buffers, textures, and heaps. The application instead populates an argument table and makes it visible to selected pipeline stages. The Metal 4 introduction separately places residency in MTLResidencySet. These are source claims about the native contract, not reasons to expose Metal tables as luv's application model.

Luv's binding group remains the semantic relationship among a layout and its resources. Its Metal realization should own stage-appropriate argument tables: buffers become GPU addresses, textures and samplers become resource IDs, and residency remains a queue or command-buffer concern. Vertex arrays use the same native table mechanism, but set-vertex-buffer still expresses their distinct draw relationship and obtains attribute stride from the linked pipeline layout.

The first realization is intentionally smaller than that destination. A render pass creates a vertex-stage table for the installed pipeline, set-vertex-buffer writes one GPU address and stride into it, and draw snapshots it through setArgumentTable:atStages:. The device's shared residency set contains the buffer allocation. This proves the mechanism, but the per-pass table is not yet a reusable Metal bind group; uniform buffers, texture resource IDs, and sampler resource IDs are the next extension.

SDL remains the platform shell #SODRMA

The canvas protocol separates a native place from its GPU presentation relationship. sdl-canvas owns the window, event translation, main-thread dispatch, pointer mode, size, and clock; backend contexts own the presentation resources.

SDL 3 provides SDL_Metal_CreateView, SDL_Metal_DestroyView, and SDL_Metal_GetLayer. The resulting metal-canvas-context owns the view, borrows its CAMetalLayer, assigns the configured Metal device, and destroys the view before the SDL window. Those SDL calls are main-thread-only; the existing call-on-sdl-canvas-thread path already reaches Cocoa's main thread. The host selects :vulkan or :metal as an immutable presentation policy before opening, while provider-specialized make-canvas-context methods validate the relationship.

Replacing SDL with NSWindow remains a separate experiment. It was neither necessary for the clear proof nor desirable as hidden scope in the backend.

The typed shader specification is already the shared IR #LVKEYO

The mathematical shader language already owns backend-independent meaning: stages, interfaces, resources, typed expressions, quantity annotations, lexical order, and source provenance. Its current compiler immediately turns that graph into SPIR-V IDs, decorations, storage classes, instructions, and a spir-v-module. Those objects are therefore a Vulkan lowering product, not a necessary interchange format for Metal.

The smaller shape is sibling lowering:

                    /--> SPIR-V lowering --> structured SPIR-V --> words
shader specification
                    \--> MSL lowering ----> structured document --> text

Translating SPIR-V back to MSL would preserve a single machinery path at the cost of making Vulkan interface choices the architecture of the Metal backend. Direct lowering keeps expression-to-output provenance and leaves native mesh/object stages available when a real pipeline gives them semantics.

Target-specific lowering stays open and inspectable #JDLQPN

The CLOS seam is target × source construct, not one growing target switch. The intended relationship is illustrated by:

lower-shader-call'+msl-contextexpression
lower-shader-call'+spir-v-contextexpression

Target objects are justified when they carry version, capability, platform, or binding-layout policy; an EQL keyword is enough while target choice is only a closed setting. Dense instruction and text data remain data.

The MSL product should be a modest structured document of declarations, entry-point parameters, statements, and source occurrences rather than one opaque accumulated string or a general C AST. The shader lab should be able to select a Lisp expression and show the MSL it caused just as it shows SPIR-V instruction occurrences. Unsupported target operations should signal a source-located shader-language condition rather than silently approximate.

defgeneric lower-shader-specification language.lisp:3934

Lowering protocols -------------------------------------------------------

defgenericlower-shader-specification
targetspecification
:documentation

Lower specification for target without changing its source graph.

target participates in ordinary CLOS dispatch so each backend can own its structured product and source provenance. #JDLQPN

Generated MSL remains a view of semantic objects #YA4KDP

Generated source is useful to read, but it is not a new semantic store. The MSL fields, parameters, local statements, and output statements retain the declaration, resource, binding, or assignment which produced them. Rendering asks those existing #C6E3LA objects for their quantity specification or component layout and writes a short English explanation beside the ordinary Metal representation. Missing annotations remain visible rather than being inferred from a nearby use.

This establishes one more provenance link beneath #58IDSR:

shader declaration or binding
        | identity retained by
        v
structured MSL node ---- renders ----> comment + Metal declaration
        |
        `---- may later record ----> exact source range

The existing expression occurrences retain an expression and its rendered text, but not yet the final character range in the document. A useful next tooling step is therefore small: let the renderer record half-open ranges for fields, statements, comments, and expression occurrences. The shader lab can then move in either direction between a visible piece of generated MSL and the live semantic object which explains it. Type aliases may later improve the spelling of homogeneous quantities, but they do not replace this relation and cannot describe heterogeneous lane layouts.

defclass msl-field lowering.lisp:30
defclassmsl-field
type:initarg:type:readermsl-field-type
name:initarg:name:readermsl-field-name
attribute:initarg:attribute:readermsl-field-attribute
origin:initarg:origin:readermsl-field-origin
array-length:initarg:array-length:initformnil:readermsl-field-array-length
:documentation

One rendered structure field retaining its shader declaration for #YA4KDP.

The Metal 4 compiler belongs to the live backend #2X8254

The running backend should compile through MTL4Compiler rather than make xcrun metal, temporary files, or subprocess management part of live Lisp. The compiler is a device-owned object which accepts source-bearing MTL4LibraryDescriptor objects and creates library and pipeline products.

The first path should be synchronous: a candidate owns its MSL document, source, library, descriptors, and pipeline state until the complete artifact can publish at the frame boundary. The currently installed artifact remains untouched on failure. Only after that transaction works should asynchronous compiler tasks, Objective-C blocks, archives, or warm-start serializers enter the required path.

That first compiler boundary is now executable. metal-gpu-device owns one MTL4Compiler beside its queue. Creating a shader module from a descriptor whose language is :mathematical lowers the specification to an msl-document, passes its source through an in-memory MTL4LibraryDescriptor with language version 4.0, verifies the named function and stage, and retains the document, library, entry point, and function type as one inspectable module artifact. Native rejection copies NSError text into a metal-gpu-error without poisoning the compiler.

The next compiler boundary is executable too. metal-gpu-render-pipeline checks that both shader modules belong to the device and have the required stages, translates the HAL's 48-byte vertex layout into an MTLVertexDescriptor, names both library functions with MTL4LibraryFunctionDescriptor objects, and synchronously creates an MTLRenderPipelineState. Its depth comparison and write policy are retained as a draw-time MTLDepthStencilState. The pipeline product no longer retains the temporary native descriptors, while its owned native states are released explicitly by destroy.

xcrun metal remains useful as an offline diagnostic and cross-check. It is not the acceptance authority for a live pipeline.

The second backend tests apparently portable names #OV1AZO

The Metal slice is allowed to reveal target-shaped vocabulary rather than declare the existing surface neutral in advance:

Names should change only when this second implementation supplies evidence. #58IDSR is the first deliberate test.

DONE Present a Metal clear through the existing SDL canvas #5A7QDE

Intent: prove #SODRMA by adding a metal-canvas-context relationship while leaving SDL window, input, clock, and main-thread ownership intact.

Evidence:

Done when (met): the Metal provider presents a deterministic clear through the shared command vocabulary, reports Retina drawable size, tears down in ownership order, and leaves the Vulkan path unchanged.

The first live pipeline is the backend threshold #DR3V60

The backend becomes a complete rendering alternative when one unchanged shader-specification-for method can feed both target compilers and the Metal artifact preserves live publication semantics.

One implementation sequence owns that threshold: #58IDSR has proved direct MSL source and compiler acceptance; #PH57K5 now joins that product to the existing canvas and transactional live-pipeline lifecycle. Moppe remains visual and API evidence, not the owner of luv's implementation roadmap.

DONE Lower one mathematical shader directly to MSL #58IDSR

Intent: compile the existing block fragment shader-specification through a sibling target context, without translating its spir-v-module and without inventing a new shared IR.

Evidence:

Done when (met):

defmethod create gpu.lisp:1484
defmethodcreate
descriptorshader-module-descriptor

Lower a mathematical shader directly to MSL and compile it on device.

The complete MSL document remains attached to the returned module so native diagnostics and graph provenance stay inspectable. This is the device-owned compiler boundary of #58IDSR.

ensure-live-metal-objectdevice:create-shader-module
let*
source
luv.msl:msl-document-sourcedocument
entry-point
luv.msl:msl-entry-point-name
luv.msl:msl-document-entry-pointdocument
stage
luv.msl:msl-entry-point-stage
luv.msl:msl-document-entry-pointdocument
multiple-value-bind
librarydiagnostic
luv.metal:compile-metal-4-library
metal-device-compilerdevice
source:name
or
gpu-descriptor-labeldescriptor
entry-point
unlesslibrary
error'metal-gpu-error:operation:create-shader-module:reason:library-compilation-failed:details
list:diagnosticdiagnostic:documentdocument
let
completed-pnil
unwind-protect
luv.objective-c:with-autorelease-pool
let
unlessfunction
error'metal-gpu-error:operation:create-shader-module:reason:entry-point-not-found:details
list:entry-pointentry-point:documentdocument
unwind-protect
let
unless
=actual-typeexpected-type
error'metal-gpu-error:operation:create-shader-module:reason:entry-point-stage-mismatch:details
list:entry-pointentry-point:expectedexpected-type:actualactual-type
let
module
make-instance'metal-gpu-shader-module:label
gpu-descriptor-labeldescriptor
:native-objectlibrary:devicedevice:documentdocument:entry-pointentry-point:function-typeactual-type
setfcompleted-pt
module

DONE Join the proofs in one live Metal pipeline #PH57K5

Intent: preserve luvcraft's transactional shader-definition loop while the same live mathematical shader methods feed a native Metal render pipeline.

Evidence: #58IDSR now supplies a device-owned MSL library and #5A7QDE already owns presentation. The live candidate builder also passes mathematical specifications through the shared shader-module descriptor, so target lowering has moved below the HAL boundary. The Vulkan live artifact proves revision tracking, last-known-good retention, frame-boundary publication, and deferred retirement. Metal should specialize those ownership points rather than fork the application model.

The middle of that join is now proved. The vertex target lowers the semantic :world-to-light map and its project-sample operation into a named shadow_projection value: four homogeneous row products, division by w, then coordinate scale and offset. The shadow vertex stage consumes the same map before division. Both stages pass Apple's offline compiler. make metal-pipeline then cold-starts the device-owned path and reports the expected luvcraft entry points, one 48-byte four-attribute vertex buffer, MTLRenderPipelineState, and MTLDepthStencilState on the Apple M2 Pro. Shader and pipeline plausibility are no longer the open question.

The live transaction now has native Metal evidence as well. A focused test installs a real MTLRenderPipelineState behind live-shader-pipeline, replaces its fragment method with a deliberately unsupported candidate, and observes :failed while the first pipeline remains installed and undestroyed. A second valid replacement publishes revision 1 and only then destroys the old Metal pipeline. This proves synchronous last-known-good publication and recovery through the shared owner.

The first real draw now exposes and crosses the command boundary. With Metal API validation enabled, make metal-draw defines two mathematical shader methods, installs their live pipeline, writes three vertices into a shared MTLBuffer, and presents through the ordinary begin-render-pass, set-pipeline, set-vertex-buffer, draw, and end-pass vocabulary. The native trace includes setAddress:attributeStride:atIndex:, setArgumentTable:atStages:, and the five-argument drawPrimitives selector. Completion advances one MTLSharedEvent value and drains the retained command buffer and allocator back to zero. #348B7B explains the binding choice.

The complete luvcraft frame now crosses the same boundary. Metal bind groups populate MTL4ArgumentTable buffer addresses, texture resource IDs, and sampler resource IDs from the ordinary layout relationship. The real frame encodes a depth-only shadow pass followed by sky, block, and crosshair pipelines into an owned color/depth pair, then copies that color texture into the drawable. The atlas upload and screenshot readback use the existing queue commands rather than backend calls in luvcraft.

MTL_DEBUG_LAYER=1 make metal-smoke renders the deterministic little world through those paths and writes an upright sRGB capture comparable to the Vulkan smoke image. The Metal capture is 1920×1280 because the Metal SDL canvas deliberately uses the Retina drawable; Vulkan's hidden reference is 960×640. On macOS, running bare ./build/luvcraft now selects Metal 4 as the platform default, keeps the ordinary interactive world alive across continuous validated frames, and exits through its normal window-close lifecycle. --vulkan remains the explicit MoltenVK comparison path. Luvcraft selects a provider, but does not fork its world, meshing, input, frame graph, live shader, or capture orchestration.

Done when: redefining the block material compiles MSL, publishes a complete Metal pipeline at the owning frame boundary, keeps the previous pipeline after an invalid edit, recovers after a valid edit, and renders a pinned capture comparable to Vulkan. Asynchronous compiler tasks, Objective-C blocks, and mesh/object stages remain later capabilities.

DONE Retire replaced Metal resources without a frontier wait #JAM5XR

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 and the native allocator to a one-shot command buffer. Public submit stamps the command buffer and dependencies with a shared-event value and retains them in a queue submission record.

destroy now marks substantial wrappers invalid immediately. It releases native ownership at once when the resource has no incomplete use, or attaches the native teardown to the last submission which can still reference it. Normal submission and submitted-work-done reclaim the completed prefix. Tests submit general offscreen work, reject a second submit of the same command buffer, retire its captured texture and command memory at the frontier, and replace a live pipeline while its old pipeline is used by an in-flight draw. The old wrapper becomes invalid at publication and its native pipeline is released only after completion.