8 Backend Protocol
dxtr edited this page 2026-10-18 15:59:12 +02:00

Backend Protocol

This page is for writing a backend, such as the coming OpenGL one. Programs that only use cl-gfx don't need it.

How the layers fit

program
   │  public functions: check arguments, signal clear errors,
   │  keep the object registry and the frame state
cl-gfx  (src/api.lisp, src/commands.lisp)
   │  backend- generic functions of cl-gfx/backend, called with checked arguments
backend (cl-gfx/vulkan)
   │
graphics API (cl-vk → Vulkan)
  • Every object is a CLOS object. A backend subclasses device, surface, buffer, texture, sampler, shader-module, bind-group-layout, bind-group, render-pipeline, compute-pipeline and frame, and puts its handles in its own slots.
  • The public functions check every argument. A backend- method may assume its arguments are valid.
  • Recording is done by cl-gfx into command lists. The backend translates a frame's list in backend-end-frame.
  • No type of the graphics API may appear in the cl-gfx package.

The package

The protocol is in the package cl-gfx/backend, apart from cl-gfx, which holds only what programs use. A backend's package uses it, and takes the public API through a nickname:

(defpackage #:cl-gfx/opengl
  (:use #:cl #:cl-gfx/backend)
  (:local-nicknames (#:gfx #:cl-gfx)))
(in-package #:cl-gfx/opengl)

(define-backend :opengl "OpenGL 4.5.")

(defmethod backend-open-device ((backend (eql :opengl)) &key present-to validation
                                validation-checks frames-in-flight application-name)
  ...)

define-backend adds the backend to the list open-device chooses from. open-device opens a device through :vulkan unless asked for another.

Generic functions

Each destroy method must defer the release until no frame in flight uses the object. cl-gfx marks the object destroyed and unregisters it itself.

Devices

Generic function Must
(backend-open-device backend &key present-to validation validation-checks frames-in-flight application-name) Return a device subclass instance with :backend, :name, :frames-in-flight and :features set. Signal unsupported-error if no GPU fits.
(backend-close-device device) Wait for the GPU and release everything. The device's objects are destroyed already.
(backend-take-validation-messages device) Return and forget (severity . text) messages, oldest first: what validation says of the program's use of the API, not the graphics API's notes on the machine.

Surfaces

Generic function Must
(backend-create-surface device target &key present-mode srgb) Return a surface, of an sRGB format if srgb and one is available.
(backend-destroy-surface surface) Release it at once, waiting first for the GPU to finish what it may still do with it, so that its target may go right after. Called between frames.
(backend-surface-format surface) Return its texture-format.
(backend-surface-size surface) Return the width and height of its textures.
(backend-acquire-surface-texture surface frame) Return the texture to draw into, to present when frame ends, or nil. Follow the target's pending size, and call surface-target-adopt-size just before presenting the first frame at a new size.

Resources

Generic function Must
(backend-create-buffer device size usage memory) Return a buffer.
(backend-destroy-buffer buffer)
(backend-write-buffer buffer offset data size frame) Copy size bytes of data (a foreign pointer or a specialised vector, as write-buffer takes it) to offset. frame is the frame being recorded, whose passes must see the write, or nil between frames. data may be reused on return. Must not allocate during a frame in steady state.
(backend-read-buffer buffer offset size pointer) Copy bytes to pointer, waiting for the GPU.
(backend-create-texture device &key width height depth array-layers dimension mip-levels samples format usage) Return a texture. mip-levels is a count.
(backend-destroy-texture texture)
(backend-write-texture texture x y z width height depth level data size frame) Copy the region's texels, layers or slices in turn, rows top to bottom without padding. frame as for backend-write-buffer.
(backend-read-texture texture level pointer) Copy a level's texels to pointer, waiting for the GPU.
(backend-generate-mipmaps texture frame) Fill each level from the one before.
(backend-create-sampler device &key filter address-mode mipmap-filter compare max-anisotropy) Return a sampler.
(backend-destroy-sampler sampler)

Bindings and pipelines

Generic function Must
(backend-create-bind-group-layout device entries) Return a bind-group-layout. entries are (binding kind visibility) by increasing binding.
(backend-destroy-bind-group-layout layout)
(backend-create-bind-group device layout entries textures) Return a bind-group. entries are in the layout's order, as bind-group-entries describes them; textures is a simple vector of its textures.
(backend-destroy-bind-group group)
(backend-create-shader-module device pointer size) Return a shader-module from size bytes of SPIR-V at pointer.
(backend-destroy-shader-module module)
(backend-create-render-pipeline device &key stages vertex-buffers topology patch-size color-formats depth-format depth-compare depth-write samples cull-mode front-face blend bind-group-layouts) Return a render-pipeline. stages is a list of (stage module entry) in the order they run.
(backend-destroy-render-pipeline pipeline)
(backend-create-compute-pipeline device module entry bind-group-layouts) Return a compute-pipeline.
(backend-destroy-compute-pipeline pipeline)

Frames

Generic function Must
(backend-begin-frame device) Wait until the next slot is free, release what was retired while it was in flight, and return its frame. Make one frame per slot, a subclass of gfx:frame made with :slot, which frame-slot returns. The core numbers the frame.
(backend-end-frame frame) Translate (frame-commands frame), submit it, and present the surface textures acquired.

Command lists

A frame's commands, and a render bundle's, are a command-list: a flat vector of opcodes, each followed by its arguments.

(let ((list (frame-commands frame))
      (index 0))
  (loop while (< index (command-list-count list))
        do (let ((opcode (command-list-ref list index)))
             (translate opcode list (1+ index))
             (incf index (1+ (command-arity opcode))))))
Opcode Arguments
:color-attachment texture, load-op (:clear or :load), red, green, blue, alpha, level, layer, resolve texture or nil
:depth-attachment texture, load-op, depth, level, layer
:begin-render-pass (follows its attachments)
:end-render-pass
:set-pipeline render pipeline
:set-bind-group index, bind group
:set-vertex-buffer slot, buffer, offset
:set-index-buffer buffer, format, offset
:set-viewport x, y, width, height, min-depth, max-depth (single-floats)
:set-scissor x, y, width, height
:draw vertex-count, instance-count, first-vertex, first-instance
:draw-indexed index-count, instance-count, first-index, base-vertex, first-instance
:execute-render-bundle render bundle; read its list with render-bundle-commands
:begin-compute-pass
:end-compute-pass
:set-compute-pipeline compute pipeline
:dispatch x, y, z

The backend is responsible for:

  • Barriers. Passes run in recorded order and each must see what earlier passes wrote. Track the last write and reads of every buffer and texture, and emit the barriers each pass needs before it begins. cl-gfx refuses the hazards inside a pass that cannot be synchronised.
  • Coordinates. They are Vulkan's: clip space has y down and depth 0 to 1, pixel (0, 0) is the top left, and the tessellation domain's origin is the top left. The Vulkan backend needs nothing for it. A GL backend sets glClipControl(GL_LOWER_LEFT, GL_ZERO_TO_ONE), under which GL puts clip space's y = -1 in row 0 of a render target as Vulkan does; renders each frame into a texture and flips it into the window when presenting, as a window shows its row 0 at the bottom; swaps the front face, as GL's polygon area has the opposite sign of Vulkan's, unless tessellation is the last stage before rasterization, whose lower-left domain turns its triangles the other way already; and sets the point sprite origin to match.
  • Bundles. A bundle executes with nothing set, and nothing it sets lasts after it: reapply the pass's state afterwards.
  • No allocation while translating a frame in steady state.

Helpers for backends

All exported from cl-gfx/backend:

Symbol Use
define-backend Register a backend.
signal-gfx-error, signal-unsupported-error Signal gfx-error or unsupported-error with a message, a format control and its arguments, as the core does.
mark-destroyed Mark an object the backend made itself, such as a surface texture, as destroyed.
command-list, command-list-count, command-list-ref, command-arity Read command lists. Only the core records into them: command-list-count has no writer, and command-arity signals a type-error, listing the opcodes, for a keyword that is none.
frame-commands, frame-device A frame's command list and device.
render-bundle-commands A bundle's command list.
bind-group-entries What each binding of a group holds: (binding buffer offset size), (binding buffer offset size format), (binding texture sampler) or (binding texture).

Implementation-specific code

cl-gfx has none of its own: what it needs beyond standard Common Lisp, cl-ffi provides. cl-ffi:with-foreign-float-environment masks the floating-point traps around calls into the driver, as C code expects. Call the driver only inside it.

Surface targets on a backend

A backend may need more from a surface target than the size protocol. The Vulkan backend defines, in cl-gfx/vulkan:

(target-vulkan-instance-extensions present-to) ; => list of extension names
(target-vulkan-surface target instance)        ; => cl-vk:surface

A new backend defines its own, such as a function returning a GL context for a target, and cl-gfx/windowing implements them for cl-windowing.