Software (CPU) presentation subsystem: cl-windowing/software #1

Open
opened 2026-10-18 04:10:38 +02:00 by dxtr · 0 comments
Owner

Goal

Present a CPU-drawn frame — a plain Lisp array of pixels — to a cl-windowing window on Windows and Wayland, with no GPU API (no OpenGL/Vulkan/DirectX). The case that asks for it is an application that is not graphically intensive and wants no Vulkan device: a text editor, a log viewer, a small tool. It also keeps the display layer honestly usable without Vulkan, and is a very different second presentation path, so it exercises the window abstraction the way a second backend did.

Design

A new optional system cl-windowing/software holds the portable part (depends on cl-windowing core only), exactly parallel to cl-windowing/vulkan:

  • A pixel-surface object bound to a window, the canonical pixel format, the public API, and the resize handshake.
  • A four-generic backend protocol declared in the core and implemented per backend, where the platform dependency already lives — native objects cross only there:
    • backend-create-pixel-surface, backend-resize-pixel-surface, backend-present-pixels, backend-destroy-pixel-surface.
  • cl-windowing/win32 implements it with GDI (via cl-win32); cl-windowing/wayland with wl_shm (via cl-wl).

Public API

(sw:with-pixel-surface (surface window)
  (loop ...
    (multiple-value-bind (pixels width height) (sw:begin-pixel-frame surface)
      (declare (type (simple-array (unsigned-byte 32) (*)) pixels))
      (fill pixels #xFF1E1E1E)            ; opaque dark grey, 0xAARRGGBB
      ;; ... blit cl-textrender glyph bitmaps into PIXELS ...
      (sw:present-pixel-surface surface))))
  • create-pixel-surface / destroy-pixel-surface / with-pixel-surface.
  • begin-pixel-frame → (values pixels width height): reallocates to the window's pending pixel size if any (so you draw at the new size), else returns the current buffer; allocates nothing in steady state.
  • present-pixel-surface &key damage: blits/commits; damage = optional (x y w h) rectangles for partial repaint; calls adopt-window-size just before the frame shows (the core's resize contract).
  • Accessors pixel-surface-window, pixel-surface-size, pixel-surface-pixels.

Pixel format

One canonical format so user code never branches on platform: a (simple-array (unsigned-byte 32) (*)) of #xAARRGGBB pixels, top row first, tightly packed. On little-endian (SBCL's targets) this is byte-identical to a top-down GDI BI_RGB 32 bpp DIB and to wl_shm XRGB8888 (bytes B,G,R,X), so the backends hand the array's memory straight to the platform. Windows are opaque (alpha byte ignored); transparency (ARGB8888 premultiplied) is a later extension.

Resize and scale

Follows the existing handshake: window-pending-pixel-size → begin-pixel-frame reallocates → present-pixel-surface calls adopt-window-size. Buffer sized to window-pixel-size; the ±1px fractional-scale rounding on Windows is absorbed by StretchDIBits to the client rect (as the Vulkan swapchain absorbs it), and on Wayland by viewporter (the path the Wayland backend already uses).

Backends

  • Win32 (GDI): a top-down CreateDIBSection DIB is the retained frame; present copies the Lisp array into it and BitBlt/StretchDIBits to the window DC; WM_PAINT repaints from the retained DIB (so uncovering/resizing needs no app redraw); WM_ERASEBKGND keeps returning 1. Needs GDI bindings → dxtr/cl-win32#1.
  • Wayland (wl_shm): a wl_shm pool + XRGB8888 buffer (double-buffered); write-shared-memory + attach/damage_buffer/commit; viewporter for fractional scale. cl-wl already exposes all the shared-memory machinery (make-shared-memory, shared-memory-fd, write-shared-memory, wl-shm-create-pool), so no cl-wl work is needed.

Capabilities & errors

  • window-system-capabilities gains :software-presentation where the backend offers it; create-pixel-surface signals unsupported-error where absent.
  • One presentation surface (Vulkan or software) per window; a second signals unsupported-error.
  • closed-error for dead window/surface; window-system-error wrapping a failed CreateDIBSection / mmap / wl_shm allocation.

Tests (cl-windowing/test)

  • Portable: format packing; begin-pixel-frame sizing, resize reallocation and adopt-at-present; steady-state frames allocate nothing.
  • Win32 (real window): present a known pattern, read back with GetDIBits, check pixels; resize follows; WM_PAINT repaints the pattern; cross-thread present test to settle the thread rule.
  • Wayland (cl-wl in-process compositor): present a pattern, have the test compositor read the attached wl_shm buffer and check pixels; resize remakes the pool; damage reported as committed.

Packaging

New cl-windowing/software system + software/ dir; /win32 and /wayland each gain a software.lisp (dependencies unchanged — they already need cl-win32 / cl-wl); the test system depends on it; README gains a "Software presentation" section with the run example.

Spec & dependencies

Full design in gdi.org in the engine workspace. Depends on GDI bindings: dxtr/cl-win32#1. No cl-wl changes needed.

Decided

  • Not part of Pyssel, the engine (Kim, 2026-10-19). This is cl-windowing's own work, not a step of the engine's plan: it stays out of engine-design.org, its Libraries table and its Build order, and that document does not link gdi.org.

Open decisions for Kim (see gdi.org)

  • Go-ahead. Its placement is settled (above): it can land whenever a CPU-drawn program wants it.
  • Retained-DIB-plus-copy vs zero-copy draw-into-DIB (recommended: retained DIB; keeps the draw buffer a plain Lisp array).
  • Present thread on Win32 (render thread vs hand to the window thread) — settle by the cross-thread test.
  • Damage API shape; transparency as a later :format :argb8888 extension.
## Goal Present a CPU-drawn frame — a plain Lisp array of pixels — to a cl-windowing window on **Windows and Wayland, with no GPU API** (no OpenGL/Vulkan/DirectX). The case that asks for it is an application that is not graphically intensive and wants no Vulkan device: a text editor, a log viewer, a small tool. It also keeps the display layer honestly usable without Vulkan, and is a very different second presentation path, so it exercises the window abstraction the way a second backend did. ## Design A new optional system **`cl-windowing/software`** holds the portable part (depends on `cl-windowing` core only), exactly parallel to `cl-windowing/vulkan`: - A `pixel-surface` object bound to a window, the canonical pixel format, the public API, and the resize handshake. - A four-generic **backend protocol** declared in the core and implemented per backend, where the platform dependency already lives — native objects cross only there: - `backend-create-pixel-surface`, `backend-resize-pixel-surface`, `backend-present-pixels`, `backend-destroy-pixel-surface`. - `cl-windowing/win32` implements it with **GDI** (via `cl-win32`); `cl-windowing/wayland` with **`wl_shm`** (via `cl-wl`). ## Public API ```lisp (sw:with-pixel-surface (surface window) (loop ... (multiple-value-bind (pixels width height) (sw:begin-pixel-frame surface) (declare (type (simple-array (unsigned-byte 32) (*)) pixels)) (fill pixels #xFF1E1E1E) ; opaque dark grey, 0xAARRGGBB ;; ... blit cl-textrender glyph bitmaps into PIXELS ... (sw:present-pixel-surface surface)))) ``` - `create-pixel-surface` / `destroy-pixel-surface` / `with-pixel-surface`. - `begin-pixel-frame` → `(values pixels width height)`: reallocates to the window's pending pixel size if any (so you draw at the new size), else returns the current buffer; allocates nothing in steady state. - `present-pixel-surface &key damage`: blits/commits; `damage` = optional `(x y w h)` rectangles for partial repaint; calls `adopt-window-size` just before the frame shows (the core's resize contract). - Accessors `pixel-surface-window`, `pixel-surface-size`, `pixel-surface-pixels`. ## Pixel format One canonical format so user code never branches on platform: a `(simple-array (unsigned-byte 32) (*))` of `#xAARRGGBB` pixels, top row first, tightly packed. On little-endian (SBCL's targets) this is **byte-identical** to a top-down GDI `BI_RGB` 32 bpp DIB and to `wl_shm` `XRGB8888` (bytes B,G,R,X), so the backends hand the array's memory straight to the platform. Windows are opaque (alpha byte ignored); transparency (`ARGB8888` premultiplied) is a later extension. ## Resize and scale Follows the existing handshake: `window-pending-pixel-size` → `begin-pixel-frame` reallocates → `present-pixel-surface` calls `adopt-window-size`. Buffer sized to `window-pixel-size`; the ±1px fractional-scale rounding on Windows is absorbed by `StretchDIBits` to the client rect (as the Vulkan swapchain absorbs it), and on Wayland by viewporter (the path the Wayland backend already uses). ## Backends - **Win32 (GDI)**: a top-down `CreateDIBSection` DIB is the retained frame; present copies the Lisp array into it and `BitBlt`/`StretchDIBits` to the window DC; `WM_PAINT` repaints from the retained DIB (so uncovering/resizing needs no app redraw); `WM_ERASEBKGND` keeps returning 1. **Needs GDI bindings → dxtr/cl-win32#1.** - **Wayland (wl_shm)**: a `wl_shm` pool + `XRGB8888` buffer (double-buffered); `write-shared-memory` + `attach`/`damage_buffer`/`commit`; viewporter for fractional scale. **`cl-wl` already exposes all the shared-memory machinery (`make-shared-memory`, `shared-memory-fd`, `write-shared-memory`, `wl-shm-create-pool`), so no cl-wl work is needed.** ## Capabilities & errors - `window-system-capabilities` gains `:software-presentation` where the backend offers it; `create-pixel-surface` signals `unsupported-error` where absent. - One presentation surface (Vulkan *or* software) per window; a second signals `unsupported-error`. - `closed-error` for dead window/surface; `window-system-error` wrapping a failed `CreateDIBSection` / `mmap` / `wl_shm` allocation. ## Tests (cl-windowing/test) - Portable: format packing; `begin-pixel-frame` sizing, resize reallocation and adopt-at-present; steady-state frames allocate nothing. - Win32 (real window): present a known pattern, read back with `GetDIBits`, check pixels; resize follows; `WM_PAINT` repaints the pattern; cross-thread present test to settle the thread rule. - Wayland (cl-wl in-process compositor): present a pattern, have the test compositor read the attached `wl_shm` buffer and check pixels; resize remakes the pool; damage reported as committed. ## Packaging New `cl-windowing/software` system + `software/` dir; `/win32` and `/wayland` each gain a `software.lisp` (dependencies unchanged — they already need `cl-win32` / `cl-wl`); the test system depends on it; README gains a "Software presentation" section with the run example. ## Spec & dependencies Full design in **`gdi.org`** in the engine workspace. **Depends on GDI bindings: dxtr/cl-win32#1.** No `cl-wl` changes needed. ### Decided - **Not part of Pyssel, the engine** (Kim, 2026-10-19). This is cl-windowing's own work, not a step of the engine's plan: it stays out of `engine-design.org`, its Libraries table and its Build order, and that document does not link `gdi.org`. ### Open decisions for Kim (see gdi.org) - **Go-ahead.** Its placement is settled (above): it can land whenever a CPU-drawn program wants it. - Retained-DIB-plus-copy vs zero-copy draw-into-DIB (recommended: retained DIB; keeps the draw buffer a plain Lisp array). - Present thread on Win32 (render thread vs hand to the window thread) — settle by the cross-thread test. - Damage API shape; transparency as a later `:format :argb8888` extension.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
dxtr/cl-windowing#1
No description provided.