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:
- Open the device with
:present-tothe window system. A device opened without it cannot make surfaces. create-surfacefor the window.- Make pipelines with
(gfx:surface-format surface)among their:color-formats. - Each frame,
acquire-surface-textureand 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.