Module Rig_metal_abi
What compiled code needs from a Metal device.
Compiled code drives a Metal device through two things its driver provides. The record t makes indirect command buffers, gives the address of split and states the device's argument alignment. A fill is a C function of the compiled code that the driver's submit calls to encode the code's work. Making an indirect command buffer calls Metal on the device's objects, which only the driver holds.
This library holds only that agreement. The driver makes the record when it opens the device and declares it (declare), compiled code finds it under key, and neither links the other.
Fills
A fill is work for the device's queue given as a C function, int fill(void *queue, void *arg, uint64_t v). The driver calls it with:
queue, a pointer to a word that holds the open compute command encoder, anid<MTLComputeCommandEncoder>the driver made before the call.queueis valid only during the call;arg, the argument the work was given;v, the value the work completes on the device's timeline.
The encoder runs its commands in order, each after the one before has completed (MTLDispatchTypeSerial), and after the device's earlier work. Every allocation of the device is resident while they run. The encoder and its command buffer may already hold commands of earlier work, and later work may add to them, so a fill must set every state its commands read, such as a dispatch's pipeline, buffers and threadgroup memory lengths.
A fill must encode into that encoder only, for instance with executeCommandsInBuffer:withRange:. It must not end the encoder or make another encoder or command buffer. The driver orders the fill's work after earlier work and signals v, so a fill does neither. To start a new command buffer, a fill calls split, as often as it needs. It must stop at the first split that fails and return that failure, and return 0 otherwise.
The driver ends encoders and commits command buffers when it chooses, and a split is the only boundary a fill sets. The driver commits a fill's commands no later than when the command buffers committed before them have completed. v is therefore reached once every command buffer holding the fill's commands has completed, without a later submission. The driver loses the device if a fill returns a failure, or, with Metal's reason, if a command buffer holding the fill's commands fails.
Indirect command buffers
An indirect command buffer is a sequence of dispatches recorded once and run by every submit of the submission that holds it. Its handle, commands and their pipelines live while its value is reachable, even after the pipelines' image is unloaded. Once the value is collected, the driver releases them, even if it has stopped the device.
type dispatch = {- #
pipeline : int;(*Its
*)MTLComputePipelineState: thecodeof an entry of an image loaded on the device. - #
offset : int;(*Where its arguments start in the argument buffer, which it binds as its kernel buffer
*)0, in bytes from theMTLBuffer's first byte: for a rig buffera,Rig.Buffer.offset aplus the offset ina. It must be a multiple ofalign, whichicbchecks, and of the alignment of the kernel's argument structure, that of its largest member, which nothing checks, such as8for a structure that holds device pointers. - #
groups : int * int * int;(*Its threadgroups per grid, in x, y and z, each at least
*)1. - #
threads : int * int * int;(*Its threads per threadgroup, in x, y and z, each at least
*)1.
}The type for the dispatches an indirect command buffer records.
type icb = private {- #
handle : nativeint;(*The
*)MTLIndirectCommandBuffer. - #
commands : nativeint array;(*The
*)MTLIndirectComputeCommandof each dispatch, in order. A fill may change a command, such as its threadgroups per grid withconcurrentDispatchThreadgroups:threadsPerThreadgroup:, before its first run ofhandle, and only while no earlier work that runshandleis in flight.
}The type for indirect command buffers.
Warning. A submission whose work runs handle must keep the value (Rig.Submission.make ~keep:icb), or the buffer may be released while its work runs. A buffer collected between two of the submission's submits is released with its pipelines, and the next submit's fill encodes the released buffer.
icb ~handle ~commands ~collect is the value of a native indirect command buffer that a driver made. The driver owns handle, commands and their pipelines until this call returns. From then on collect, which runs once the value is collected, releases them. A driver must pass each native buffer at most once.
collect must keep no reference to the value, must neither block nor raise, and must leave inherited Metal objects untouched in a child of fork.
The record
type t = {- #
align : int;(*
*)alignis the device's minimum constant buffer offset alignment, in bytes, at least1. Metal requires a buffer a kernel reads as constant data to start at a multiple of it. - #
icb : nativeint -> dispatch array -> (icb, string) Stdlib.result;(*icb buffer dsisOk bwithban indirect command buffer that records one dispatch per element ofds, in order, each run after the one before it has completed.bufferis the argument buffer, anMTLBufferof the device, such as a rig buffer'sRig.Buffer.handle.dsmay be empty. Any domain may callicbat any time, andRig_edge.Driver.stopwaits for a call oficbin flight.Warning.
bdoes not keepbuffer. The caller keeps it until the work that runsbis done: for a rig bufferargs, by keepingargswith that work's submission (Rig.Submission.make ~keep:(b, args)).The result is
Errorwith the reason if a dispatch asks for more threads per threadgroup than its pipeline allows, if Metal cannot make the indirect command buffer, or once the driver has begun to stop the device.Raises
Invalid_argumentif:bufferis not a live region of the device;- a pipeline belongs to no image loaded on the device;
- a dispatch's offset is negative, lies outside
bufferor is not a multiple ofalign; - one of a dispatch's sizes is less than
1.
A call from a callback inside a lock (
*)Rig's domains section) raisesInvalid_argument, having changed nothing. - #
split : nativeint;(*splitis the address ofint split(void *queue, uint64_t *start, uint64_t *end), which ends the open command buffer and starts a new one. It ends the open encoder, commits its command buffer, and stores atqueuethe encoder of a new command buffer, which runs as the first did. While the device's queue holds as many command buffers as it can,splitwaits until one of them completes, so a fill may make more command buffers than the queue holds. Only a fill, during its call, may callsplit, and the address is valid while the device is open.Unless
startisNULL,splitwrites atstartthe time the committed command buffer started on the GPU, and unlessendisNULL, the time it ended atend. The command buffer may hold commands of earlier work before the fill's, and its times cover them too. Both are written beforevis reached. Times are nanoseconds of the host clock (CLOCK_UPTIME_RAW, the clock of Metal'sGPUStartTime), as unsigned 64-bit integers in the host's byte order.
*)splitreturns0, or a nonzero failure if Metal cannot make a new command buffer (commandBufferreturnsnil) or an encoder for it (computeCommandEncoderWithDispatchType:returnsnil).
}The type for what compiled code needs from a Metal device.
key is the key of a Metal device's record. Compiled code finds the record of a device d with Rig.capability d key, which is None if d is not a Metal device.
val declare : t -> Rig_edge.capabilitydeclare c is c under key, as the facts of a Metal device carry it (Rig_edge.facts).
References
- Apple's Metal framework headers (macOS 26 SDK):
MTLIndirectCommandBuffer.h,MTLIndirectCommandEncoder.h(setBarrier),MTLComputeCommandEncoder.h(executeCommandsInBuffer:withRange:,useResources:count:usage:),MTLCommandBuffer.h(GPUStartTime,GPUEndTime,MTLDispatchType,computeCommandEncoderWithDispatchType:). MTLCommandQueue.h(commandBuffer,maxCommandBufferCount) and makeCommandBuffer(): a full queue blocks until the GPU finishes a command buffer.- gpuStartTime: host time relative to mach time;
clock_gettime(3):CLOCK_UPTIME_RAWis mach time. - Encoding indirect command buffers on the CPU: the buffers that the commands of an indirect command buffer use must be declared resident.
- Metal feature set tables, Resources: minimum constant buffer offset alignment.