6 Surfaces and Windows
dxtr edited this page 2026-10-16 21:41:23 +02:00

Surfaces and Windows

A surface presents what a device draws to a surface target, such as a window. cl-gfx knows nothing about windows: cl-gfx/windowing makes cl-windowing's windows into surface targets.

Drawing into a window

(asdf:load-system :cl-gfx/windowing)
(asdf:load-system :cl-windowing/wayland)   ; :cl-windowing/win32 on Windows

(win:with-window-system (window-system)
  (win:with-window (window window-system :title "Triangle" :width 640 :height 480)
    (gfx:with-device (device :present-to window-system)
      (let* ((surface (gfx:create-surface device window))
             (pipeline (gfx:create-render-pipeline
                        device
                        :vertex (gfx:create-shader-module device (shader "triangle.vert"))
                        :fragment (gfx:create-shader-module device (shader "triangle.frag"))
                        :vertex-buffers '((:stride 24 :attributes ((0 :float32x3 0)
                                                                   (1 :float32x3 12))))
                        :color-formats (list (gfx:surface-format surface))))
             (vertices (gfx:create-buffer device :size 72 :usage '(:vertex))))
        (gfx:write-buffer vertices (make-array 18 :element-type 'single-float
                                                  :initial-contents '( 0.0 -0.5 0.0  1.0 0.0 0.0
                                                                      -0.5  0.5 0.0  0.0 1.0 0.0
                                                                       0.5  0.5 0.0  0.0 0.0 1.0)))
        (loop repeat 60
              do (win:process-events window-system)
                 (gfx:with-frame (frame device)
                   (let ((texture (gfx:acquire-surface-texture surface frame)))
                     (when texture
                       (gfx:with-render-pass (pass frame :color ((texture :clear '(0 0 0 1))))
                         (gfx:set-pipeline pass pipeline)
                         (gfx:set-vertex-buffer pass 0 vertices)
                         (gfx:draw pass 3))))))
        (gfx:surface-size surface)))))
;; => 640, 480

The steps:

  1. Open the device with :present-to the window system. A device opened without it cannot make surfaces.
  2. create-surface for the window.
  3. Make pipelines with (gfx:surface-format surface) among their :color-formats.
  4. Each frame, acquire-surface-texture and draw into the texture. The frame presents it when it ends.

A real program loops until cl-windowing reports a :close event; see Devices and Frames.

create-surface

(gfx:create-surface device target &key (present-mode :fifo) (srgb t))

Present modes

Mode Behaviour
:fifo The default, and always supported. Waits for vertical blank: no tearing, paced to the display.
:fifo-relaxed Like :fifo, but a late frame is shown at once, and may tear.
:mailbox Doesn't wait; the newest finished frame is shown at the next vertical blank. No tearing.
:immediate Doesn't wait; frames are shown at once and may tear.

A mode the target doesn't support signals unsupported-error, listing the modes it does.

sRGB or not

With :srgb t, the default, the surface prefers an sRGB format such as :bgra8-srgb. Shaders write linear colours, which are encoded to sRGB, and blending happens in linear light. This is what physically based lighting wants.

With :srgb nil, the surface prefers a plain format such as :bgra8-unorm. It stores what shaders write and blends that, as image editors and browsers do. Use it for UIs whose colours are given in sRGB already.

surface-format returns the format the surface got, and srgb-format-p tells you which kind it is. Use the surface format for the pipelines that draw into the surface.

Surface textures

(gfx:acquire-surface-texture surface frame)  ; => texture or NIL

Returns the texture to draw the frame into, which the frame presents when it ends. It returns nil while there is nothing to draw into: until the window system has given the window its first size, when nothing may be drawn in it yet, and while the window is minimised. Skip drawing, and end the frame as usual. So a program need not wait for the window before its first frame.

The texture belongs to the surface and is valid until the frame ends. Don't destroy it. Its usage is :render-target. A frame may present to up to 8 surfaces.

Resizing

The surface follows its window's size in pixels: its size times its scale. When the window system proposes a size or a scale, the next texture acquire-surface-texture returns has it, and the window adopts the size just before that frame presents. You don't need to do anything, except to size your own textures, such as a depth buffer, to match:

(let ((texture (gfx:acquire-surface-texture surface frame)))
  (when texture
    (unless (and (= (gfx:texture-width texture) (gfx:texture-width depth))
                 (= (gfx:texture-height texture) (gfx:texture-height depth)))
      (gfx:destroy-texture depth)
      (setf depth (gfx:create-texture device :width (gfx:texture-width texture)
                                             :height (gfx:texture-height texture)
                                             :format :depth32-float
                                             :usage '(:render-target))))
    (gfx:with-render-pass (pass frame :color ((texture :clear '(0 0 0 1)))
                                      :depth (depth :clear 1.0))
      ...)))

Destroying the old depth texture during the frame is safe: it is released once no frame in flight uses it.

surface-size returns the width and height of the surface's textures now.

Destroying

Destroy a surface with destroy-surface before its window. It waits for the GPU to finish the frames in flight, at most, as making a swapchain anew for a new size does, and destroys the surface at once, so the window may be destroyed right after it, as when a popup closes in the middle of an editor's frames. Since it waits for the GPU, call it between frames: calling it while a frame is recorded signals gfx-error.

with-device destroys surfaces still alive when it closes the device; order the with-window and with-device forms so the device closes first, as above.

Threads

The surface belongs to the render thread, like its device. The window system may process events on another thread.

Your own surface targets

Anything can be a surface target. Implement the surface target protocol for it, and the backend's part:

Generic function Package Returns
surface-target-size cl-gfx The width and height in pixels that frames have now, or 0 and 0 while nothing may be drawn into the target.
surface-target-pending-size cl-gfx The width and height the platform has proposed and not yet adopted, or nil and nil.
surface-target-adopt-size cl-gfx Takes the proposed size and returns the width and height. Called just before the first frame at that size presents.
target-vulkan-instance-extensions cl-gfx/vulkan Specialised on the :present-to object: the Vulkan instance extensions its surfaces need.
target-vulkan-surface cl-gfx/vulkan Specialised on the target: creates a cl-vk:surface from a cl-vk:instance. cl-gfx then owns it.

cl-gfx/windowing is the example: its whole source is these five methods.

(defmethod gfx:surface-target-size ((window win:window))
  (win:window-pixel-size window))

(defmethod gfx:surface-target-pending-size ((window win:window))
  (win:window-pending-pixel-size window))

(defmethod gfx:surface-target-adopt-size ((window win:window))
  (win:adopt-window-size window)
  (win:window-pixel-size window))

(defmethod cl-gfx/vulkan:target-vulkan-instance-extensions ((window-system win:window-system))
  (cl-windowing/vulkan:vulkan-instance-extensions window-system))

(defmethod cl-gfx/vulkan:target-vulkan-surface ((window win:window) instance)
  (cl-windowing/vulkan:create-vulkan-surface window instance))

create-surface signals gfx-error if nothing implements the protocol for a target, and tells you to load cl-gfx/windowing for cl-windowing's windows.