7 Painters
dxtr edited this page 2026-10-20 10:09:08 +02:00

Painters

Shapes are drawn by painters, each with its own shaders and instance format. A renderer has three: quad-painter for shapes, textures and raster text, slug-painter and sdf-painter. A canvas records instances of any of them in the order they are drawn; neighbouring instances of one painter that read the same texture are one draw call.

A painter of your own subclasses painter, names its clsl program, whose vertex shader's inputs are an instance's, and says how many floats an instance has; drawing functions find it with canvas-painter and record instances with add-instance. In a package that uses CL and CLSL, with p2d for pyssel/2d:

(define-shader solid-vertex :vertex ((rect :vec4) (color :vec4))
  (let* ((index (vertex-index))
         (corner (vec2 (float (mod index 2)) (float (floor index 2)))))
    (values (p2d:clip-position (mix (swizzle rect :xy) (swizzle rect :zw) corner))
            color)))

(define-shader solid-fragment :fragment ((color :vec4 :interpolation :flat))
  (vec4 (* (swizzle color :xyz) (w color)) (w color)))

(define-program solid-program
  :vertex solid-vertex :fragment solid-fragment)

(defclass solid-painter (p2d:painter) ()
  (:default-initargs :program 'solid-program :instance-floats 8))

(defun fill-solid (canvas x0 y0 x1 y1 color)
  (let ((painter (p2d:canvas-painter canvas 'solid-painter)))
    (multiple-value-bind (instances start) (p2d:add-instance canvas painter nil :linear)
      (multiple-value-bind (ax ay) (p2d:transform-point canvas x0 y0)
        (multiple-value-bind (bx by) (p2d:transform-point canvas x1 y1)
          (replace instances (list ax ay bx by) :start1 start)
          (replace instances color :start1 (+ start 4)))))))

(defun panel (canvas)
  (fill-solid canvas 20 20 140 80 '(0.2 0.6 1.0 1.0)))

;; (p2d:make-canvas renderer 'panel)

(p2d:canvas-painter canvas 'solid-painter) gives the painter of that class of the canvas's renderer, made the first time the renderer is asked for one, and the same painter after. So fill-solid keeps no painter in a variable and draws in any engine, as a node's draw method draws both in the editor's viewport and in a play, each with a renderer of its own. renderer-painter asks a renderer, and the built-in classes, quad-painter, polygon-painter, slug-painter and sdf-painter, give the renderer's own. The renderer makes a painter with make-instance and no initargs, so its class gives :program and :instance-floats in its :default-initargs.

Asking never waits for the render thread, so a draw function may ask: draw functions run on the engine's job pool while the render thread waits for them, and a painter asked for first is made at once, on the thread that asks, and its pipeline on the render thread before the frame draws what was recorded with it. Until then it is not among renderer-painters. If a painter's pipeline cannot be made, as when its shaders do not compile, the canvases that drew with it keep what they drew before, as when a draw function fails: canvas-error says why. The renderer keeps the failure: asking for the class again signals the same error at once, without compiling anything, so a canvas warns of it once. Once a clsl definition is redefined, or the class gives another :program or :instance-floats, asking makes the painter anew, so a fix takes at once.

Each instance is drawn as a triangle strip of :vertex-count vertices, 4 by default. Its floats are the vertex shader's inputs, side by side, as clsl:program-vertex-buffers lays them out, and a painter whose :instance-floats differ from what its shader reads is refused when its pipeline is made. The vertex shader places its vertices with p2d:clip-position, which takes a point of the units a canvas records in to clip space, through the transform the canvas is drawn through, so the painter's shapes move, zoom, pan and turn with the others; p2d:pixel-vector gives the pixels a vector of those units covers, as anti-aliasing over a pixel needs. The shaders may read *target-size*, *canvas-transform*, a :mat3x2 from those units to the target's pixels, which each canvas has its own of, worked out in double-floats so that it holds numbers near the view, and *linear-target*, pyssel/2d's uniforms of the :frame group, and give colours of premultiplied alpha. Uniforms of a painter's own belong in the :draw group or one of their own: the :frame group holds 256 bytes at most, and a painter whose shaders read more of it is refused as its pipeline is made. A painter whose batches read textures gives their :draw group from a method on painter-bind-group, updates its GPU data each frame in a method on prepare-painter, and what the batches drawn anew read in one on prepare-batch, and releases it in one on release-painter. prepare-painter returns true if it destroyed bind groups it gave before, and, as a second value, true if its data changed what its instances draw, as a uniform that follows the time does: a render target that shows them is then drawn again, where one whose canvases changed nothing keeps its picture.

(defmethod p2d:prepare-painter ((painter wave-painter))
  ;; WRITE-TIME-UNIFORM, the painter's own, writes the time its shader waves by.
  (write-time-uniform painter (pyssel:engine-time
                               (p2d:renderer-engine (p2d:painter-renderer painter))))
  (values nil t))                       ; what it draws changes in every frame

A painter says where an instance lies in a method on instance-parallelogram, from the floats recorded for it: the parallelogram its drawing lies within, as a corner and two sides in the units the canvas records in, six single-floats. canvas-bounds finds the box around a canvas from them, and the editor picks what is drawn by that box. Without a method of its own, a painter's instances count for nothing there, and its renderer warns with painter-without-bounds as it makes the painter; a method that returns nil, for a painter whose drawing has no place, says so on purpose and silences it. solid-painter's instances begin with their rectangle's corners:

(defmethod p2d:instance-parallelogram ((painter solid-painter) instances start)
  (let ((x0 (aref instances start)) (y0 (aref instances (+ start 1)))
        (x1 (aref instances (+ start 2))) (y1 (aref instances (+ start 3))))
    (values x0 y0 (- x1 x0) 0.0 0.0 (- y1 y0))))

A renderer is released when its engine stops, the textures it made for fonts and all; destroy-renderer drops one sooner, while the engine goes on.

Draw functions run on the engine's job pool, several at once, so a draw function draws only on its own canvas. If one signals an error, its canvas keeps what it drew before, the engine warns with canvas-not-redrawn unless the canvas failed with that same error last, and canvas-error returns the error until a redraw succeeds. A renderer's renderer-error-version moves on whenever a canvas's error comes, goes or changes, so what shows which canvases failed notices without looking at each. A frame in which no canvas changes allocates nothing, and neither does redrawing a canvas once its storage has grown to fit.