Table of Contents
Engines
An engine runs a window, or an offscreen target, and the frame loop that draws into it, on threads of its own, so the REPL stays free; the triangle of Getting Started starts one.
(pyssel:start ...) opens a window and returns an engine once it runs. It
takes these options:
:title,:width,:height: the window.:display-name: a Wayland display other than the default.:offscreen t: draw into a texture of:widthby:heightinstead of a window.read-pixelsreturns what was drawn, for tests and screenshots.:setup: called once with the engine, before the first frame.:draw: called for each frame with the engine and a cl-gfx render pass over the window, cleared to:clear-color(red, green, blue and alpha, from 0 to 1).:redraw::continuous, the default, draws every frame, as a game does;:on-demanddraws only when asked, as an editor does (see below).:workers: how many threads the engine's job pool has, by default one for each processor beyond two.:scale: for an offscreen engine, how many pixels it draws to a unit, such as 2 for a picture of what a display at 200% shows. A window's scale follows its display.:frame-interval: for an offscreen engine, the seconds between its frames, 1/60 by default. A window's frames are paced by its display.:validation: check every use of Vulkan with its validation layer.:frames-in-flight: how many frames the GPU may work on at once.:bindings: a list of(symbol value)that every thread of the engine binds for its whole life, its render thread, its event thread and its job pool's workers, as a game played in the editor's image binds*session*(see Sessions). The threads also take the standard output, error output, trace output and package of the thread that callsstart, unless:bindingsbinds them, so the engine's output shows where you started it.
start declares each option's type, so a wrong one is refused as a
type-error before anything starts, and at compile time where SBCL can
tell. A clear colour is the type pyssel:clear-color: a list of red,
green, blue and alpha, each from 0 to 1, as '(0 0 0 1).
Colours are sRGB, as image editors and CSS give them: '(0.5 0.5 0.5 1)
is the grey #808080. As in Godot, they are stored and blended as the
values they are, so half-transparent white over black is that same grey,
as an image editor or a browser shows it. A window and an offscreen target
both work this way, so read-pixels returns what a window would show.
Sizes are in units: pixels of a display at scale 1. On a scaled display,
such as one at 150%, a window's frames have more pixels than units:
engine-size is the size in units, engine-scale the pixels to a unit
(3/2), and engine-pixel-size the size in pixels. When the window moves
to a display of another scale, the engine draws a frame at the new one.
(pyssel:set-engine-size engine 1280 720) asks for another size. A
window asks its window system, which may give it another size, and
engine-size follows once a frame is drawn at it; while the window
system sizes the window itself, as while it is maximised, nothing is
asked and it returns nil. An offscreen target takes the size at the
next frame.
Give setup and draw as symbols: then redefining the function changes
what the next frame does. engine-frame-number, engine-time (in seconds,
for animation) and engine-size describe a running engine;
engine-job-pool is its cl-threads
job pool. During draw, engine-frame and engine-frame-texture are the
cl-gfx frame and the texture it draws into, and engine-frame-scale the
scale it is drawn at. engine-window is the cl-windowing window it draws
into, for what the engine leaves to the window, such as the pointer's
cursor and input methods, or nil offscreen. with-engine starts an
engine around a body and stops it afterwards.
If draw signals an error, the debugger opens on the render thread with the
restarts skip-drawing, which finishes the frame without the rest of its
drawing, and stop-engine. Fix draw at the REPL, choose skip-drawing,
and the next frame uses the fix.
Drawing on demand
An engine started with :redraw :on-demand draws a frame only when
something asks for one, and otherwise sleeps:
(request-redraw engine)asks for a frame, from any thread. Call it indrawto keep animating.- A redefined shader, and a window that changes size, ask by themselves.
(add-redraw-check engine function)has the engine callfunctionat each sync point, and draw when it returns true. A sleeping engine looks every:idle-intervalseconds, an option ofstart, 1/20 by default.
Frames and input
Before each frame it draws, an engine hands the input its window and its
popups received, from the keyboard, the pointer and input methods, to the
functions given to add-input-function, one cl-windowing event at a time,
then calls those given to add-frame-function with the seconds since the
previous frame, all on its render thread, at the sync point. This is
where game logic runs; a scene tree uses both. Input asks an engine that
draws on demand for a frame. The motions of one frame arrive as one, and a
frame keeps at most +input-capacity+ events. remove-frame-function
and remove-input-function take effect at once: a function that removes
another, as a scene tree's that destroys another tree, keeps it from
being called later in the same frame.
Stopping
stop, or closing the window, stops an engine and releases what it holds:
its device and everything made from it, its render targets among them,
its threads, its popups and its window. An engine started with
:auto-accept-quit nil, as Godot's
auto_accept_quit, does not stop as its window closes, but hands the
closing to its input functions as a :close event, which a scene tree
passes to its root's close-requested: for a program that asks first,
as an editor asks whether to save. (stop engine :wait nil) asks it to
stop and returns at once, for
code that must not wait on an engine that may never stop, and
wait-for-stop returns once an engine has stopped, whatever stopped it. Code
that makes things for an engine, such as the 2D renderer, adds a function
with add-stop-function, which the engine calls on its render thread as it
stops, while the device is still open, newest first.
A system above the engine that keeps one object for each engine, as the 2D
renderer keeps the glyphs an engine's renderers share, keeps it with
(engine-cache engine key make): the object make returned the first
time, under key, a symbol of the system's own. make may return as a
second value a function that releases the object, which the engine calls
on its render thread as it stops, after the stop functions, while the
device is still open; the engine forgets what it kept then. make runs
under a lock of the engine's, so it makes no GPU objects and waits for no
other thread. Finding what was made before allocates nothing.
An engine stuck in a loop of game code never reaches its sync point, so it
does not stop when asked. break-into-engine interrupts its render thread
into the debugger, SLY's while it is connected, which shows the thread's
stack and offers its restarts: stop-engine stops the engine, and
continue lets it go on. Nothing is unwound until you choose; a restart
that unwinds leaves any foreign code on the stack above the frame loop
unfinished, holding what it held, so choose it when the stack shows Lisp
code down to the frame loop.
Threads and the sync point
An engine runs on threads of its own, so the REPL stays free. The render
thread makes every GPU call, and the event thread handles the window. The
engine's device, and everything made from it, belong to the render thread:
use them in setup and draw.
From any other thread, such as the REPL's, use call-at-sync-point. It runs
a function on the render thread at the start of the next frame and returns
its values, or signals its error, as for the triangle's engine:
(pyssel:call-at-sync-point *engine*
(lambda () (gfx:device-name (pyssel:engine-device *engine*))))
;; => "llvmpipe (LLVM 21.1.8, 256 bits)", or your GPU's name
Never wait for the sync point on a worker of the engine's own job pool,
such as in a 2D canvas's draw function: the render thread may be waiting
for that worker, so it would never reach the sync point. There
call-at-sync-point signals a pyssel-error rather than wait forever,
and so do the functions that wait for it, such as make-pipeline,
make-render-target, read-pixels and pyssel/2d's make-renderer and
destroy-canvas. A worker of another engine's pool may call it.
Errors
The errors Pyssel signals itself, in each of its systems, are of the type
pyssel:pyssel-error, whose message, pyssel-error-message, says what
went wrong and what to do about it. An argument of the wrong type is a
type-error instead.
(pyssel:signal-pyssel-error control &rest arguments) signals a
pyssel-error whose message is the format control string control
applied to arguments, as format applies it. Pyssel's systems report
their errors with it, and a game may too, to report an error in Pyssel's
terms, which a handler of pyssel-error takes as it takes the engine's
own:
(pyssel:signal-pyssel-error "The level ~S has no start." "moon")
;; signals a pyssel-error: The level "moon" has no start.
Getting started
The core
Drawing
Scenes
User interface
Games and the editor
- Playing a Game
- Projects
- The Editor
- The File System Dock
- The Script Screen
- The REPL
- The Debugger
- Lisp Text
- Code Services
- Project Manager
- Undo and Redo
- Exporting
The project