1 Uploading Pages
dxtr edited this page 2026-10-14 04:36:31 +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.

Uploading Pages

cl-atlas keeps every page in memory as bytes. A renderer gives each page a texture, and before drawing uploads what changed in it. Each page remembers the rectangle that changed since it was last asked, so only new images are uploaded, and a frame in which nothing was packed uploads nothing.

Each frame

Before drawing from an atlas, for each page:

  1. Ask take-page-changes. It returns the column, row, width and height of the rectangle that changed, and forgets it; or nil four times when nothing changed. It allocates nothing when nothing changed.
  2. If the page has no texture yet, make one of page-size by page-size texels, of a format of page-channels bytes, and upload the whole page into it. A new texture's texels are undefined until written, and filtering reads the padding around each image, which no change covers: the whole page brings its padding, empty, along.
  3. Otherwise, if something changed, read the rectangle's texels with copy-page-region and upload them to the same place in the texture.

Ask for the changes before copying: an image packed meanwhile, on another thread, is then in the copy or in the next frame's changes, never lost.

(atlas:take-page-changes page) => x, y, width, height   ; or NIL, NIL, NIL, NIL
(atlas:copy-page-region page x y width height &optional into) => octets

copy-page-region returns the texels row by row from the top, without padding between rows, page-channels bytes each: what a texture upload takes. With into, a vector of octets long enough, it copies into that and returns it, allocating nothing; the vector may then be longer than the texels, so pass their length, width × height × page-channels, on to the upload. Without into, it returns a new vector of exactly the texels.

Then draw each region with its page's texture and region-uv.

With cl-gfx

cl-gfx makes the textures and uploads the changes with gfx:write-texture:

(asdf:load-system :cl-atlas)
(asdf:load-system :cl-gfx/vulkan)

(defpackage #:example
  (:use #:cl)
  (:local-nicknames (#:atlas #:cl-atlas)
                    (#:gfx #:cl-gfx)))
(in-package #:example)

(defun upload-atlas (device atlas textures format)
  "Bring TEXTURES, an adjustable vector of the textures of ATLAS's pages by
page index, up to date: make a texture of FORMAT on DEVICE for each new
page, and upload what changed in each page since the last call.  Call on
the render thread, before drawing from ATLAS."
  (dotimes (index (atlas:atlas-page-count atlas))
    (let ((page (atlas:atlas-page atlas index)))
      (multiple-value-bind (x y width height) (atlas:take-page-changes page)
        (cond ((= index (length textures))
               ;; A new texture's texels are undefined: upload the whole
               ;; page, with the empty padding that filtering reads.
               (let ((size (atlas:page-size page)))
                 (vector-push-extend (gfx:create-texture device :width size :height size
                                                                :format format
                                                                :usage '(:sampled :copy-dst))
                                     textures)
                 (gfx:write-texture (aref textures index)
                                    (atlas:copy-page-region page 0 0 size size))))
              (x
               (gfx:write-texture (aref textures index)
                                  (atlas:copy-page-region page x y width height)
                                  :x x :y y :width width :height height)))))))

(gfx:with-device (device :backend :vulkan)
  (let ((atlas (atlas:make-atlas :page-size 256))
        (textures (make-array 0 :adjustable t :fill-pointer 0)))
    (atlas:atlas-add atlas (make-array '(8 8) :element-type '(unsigned-byte 8)
                                              :initial-element 255))
    (upload-atlas device atlas textures :r8-unorm)   ; once a frame
    (prog1 (length textures)
      (map nil #'gfx:destroy-texture textures))))
;; => 1
  • The format follows atlas-channels: :r8-unorm for one byte, :rg8-unorm for two, :rgba8-unorm or :rgba8-srgb for four. cl-gfx has no format of three bytes a texel, as WebGPU has none, so give colour atlases four channels.
  • Allocation. gfx:write-texture takes a vector of exactly the rectangle's texels, which copy-page-region without into makes. That allocates only in a frame where images were packed; in steady state, upload-atlas allocates nothing.
  • Threads. Images may be packed on other threads while the render thread uploads: copy-page-region reads a page under the atlas's lock, so it never sees half an image. See Threads.

Drawing a region

region-uv gives a region's corners as texture coordinates of its page. Draw a quad of region-width by region-height texels, scaled as you need, with those coordinates, from the texture of (atlas:page-index (atlas:region-page region)). An empty image's region has no page: draw nothing for it.

After clearing

atlas-clear forgets the pages, and the pages after it count from 0 again. Destroy the textures of the old pages and empty the vector, so that each new page gets a texture of its own, uploaded whole, and forget the regions kept from before the clear.