1 Textures and Uploading
dxtr edited this page 2026-10-11 17:49:00 +02:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Textures and Uploading

A renderer keeps two GPU textures for a store and brings them up to date each frame, before it draws.

The textures

Texture Format Width Height
Curves 4 × 16-bit float (:rgba16-float, VK_FORMAT_R16G16B16A16_SFLOAT, GL_RGBA16F) +texture-width+, 4096 store-curve-rows or more
Bands 2 × 16-bit unsigned integer (:rg16-uint, VK_FORMAT_R16G16_UINT, GL_RG16UI) 4096 store-band-rows or more

Both are read with texel fetches, by integer coordinates, so their samplers' filters do not matter and no mipmaps are needed. In GLSL the band texture is a usampler2D. Both must take uploads (:copy-dst in cl-gfx) and be sampled (:sampled).

Each frame

  1. Let the frame's drawing code add the shapes it needs.
  2. For each texture, if it has fewer rows than the store needs, make a new one, taller, and upload every row into it. Otherwise upload the rows that changed.
  3. Draw.

With cl-gfx:

(defun grow (device texture rows format copy store)
  "Return TEXTURE if it has ROWS rows, or else a new texture of FORMAT twice
as tall, holding every row of STORE, which COPY reads."
  (if (and texture (>= (gfx:texture-height texture) rows))
      texture
      (let* ((height (max 16 rows (if texture (* 2 (gfx:texture-height texture)) 0)))
             (new (gfx:create-texture device :width cl-slug:+texture-width+ :height height
                                             :format format :usage '(:sampled :copy-dst))))
        (when (plusp rows)
          (gfx:write-texture new (funcall copy store 0 rows)
                             :width cl-slug:+texture-width+ :height rows))
        (when texture
          (gfx:destroy-texture texture))
        new)))

(defun upload-changes (texture store take copy)
  "Upload the rows of STORE that TAKE says changed into TEXTURE."
  (multiple-value-bind (row count) (funcall take store)
    (when (and row (plusp count))
      (gfx:write-texture texture (funcall copy store row count)
                         :y row :width cl-slug:+texture-width+ :height count))))

(defun prepare (device store)
  (let ((curves (grow device *curves* (cl-slug:store-curve-rows store) :rgba16-float
                      #'cl-slug:copy-curve-rows store))
        (bands (grow device *bands* (cl-slug:store-band-rows store) :rg16-uint
                     #'cl-slug:copy-band-rows store)))
    ;; A new texture holds every row already: just forget the changes.
    (if (eq curves *curves*)
        (upload-changes curves store #'cl-slug:take-curve-changes #'cl-slug:copy-curve-rows)
        (cl-slug:take-curve-changes store))
    (if (eq bands *bands*)
        (upload-changes bands store #'cl-slug:take-band-changes #'cl-slug:copy-band-rows)
        (cl-slug:take-band-changes store))
    (unless (and (eq curves *curves*) (eq bands *bands*))
      ;; Bind groups that held the old textures must be made anew.
      (setf *curves* curves
            *bands* bands)
      (remake-bind-groups))))

Pyssel's Slug painter, in pyssel/2d/slug.lisp, does exactly this.

Why textures grow by remaking them

A shape's data is found by texel coordinates, not by coordinates from 0 to 1, so a taller texture holding the same rows needs nothing recorded about any shape to change. Only the bind groups that held the old texture must be made anew. Doubling the height keeps the number of remakes small.

What it costs

Rows are uploaded only when shapes are added, and a shape is added once. A frame that adds no shapes uploads nothing, and the functions above allocate nothing then: take-curve-changes returns nil without consing. copy-curve-rows returns a fresh vector, so an upload allocates, but uploads happen only as new shapes arrive.

Several stores

A program may keep several stores, for example one per font, each with its own pair of textures. A draw call then reads one pair: shapes of one store can be drawn together, shapes of different stores cannot. One store for everything batches best.