luv

Workshop wiki

render.lisp

luvcraft/render.lisp

system luvcraft/core · 85 definitions · on GitHub

Frame encoding, input handling, and the luvcraft session lifecycle.

The canvas frame callback is the ownership boundary for all GPU replacement: shader refresh, mesh publication, and uniform updates all happen here, on the thread that owns the swapchain. This file also owns canvas event handling and the START/STOP pair that creates and releases the renderer and the other explicitly owned application components.

in-package#:luvcraft
defclassluvcraft-frame-state
uniform-buffer:initarg:uniform-buffer:readerluvcraft-frame-uniform-buffer

write-buffer writes mapped host-visible memory directly. Animated streams therefore belong to the presentation slot: reacquiring this slot is the proof that the GPU has finished reading its previous data.

particle-vertex-buffer:initarg:particle-vertex-buffer:readerluvcraft-frame-particle-vertex-buffer
critter-vertex-buffer:initarg:critter-vertex-buffer:readerluvcraft-frame-critter-vertex-buffer
physics-vertex-buffer:initarg:physics-vertex-buffer:readerluvcraft-frame-physics-vertex-buffer
physics-instance-buffer:initarg:physics-instance-buffer:readerluvcraft-frame-physics-instance-buffer
body-vertex-buffer:initarg:body-vertex-buffer:readerluvcraft-frame-body-vertex-buffer
scene-bind-group:initarg:scene-bind-group:readerluvcraft-frame-scene-bind-group
shadow-bind-group:initarg:shadow-bind-group:readerluvcraft-frame-shadow-bind-group
post-uniform-buffer:initarg:post-uniform-buffer:readerluvcraft-frame-post-uniform-buffer
post-bind-group:initarg:post-bind-group:readerluvcraft-frame-post-bind-group

The lens chain reads three different sources through one layout: the scene once, then each reduced attachment in turn.

bloom-scene-bind-group:initarg:bloom-scene-bind-group:initformnil:readerluvcraft-frame-bloom-scene-bind-group
bloom-primary-bind-group:initarg:bloom-primary-bind-group:initformnil:readerluvcraft-frame-bloom-primary-bind-group
bloom-secondary-bind-group:initarg:bloom-secondary-bind-group:initformnil:readerluvcraft-frame-bloom-secondary-bind-group
world-text-bind-groups:initarg:world-text-bind-groups:initform
:readerluvcraft-frame-world-text-bind-groups
defunluvcraft-frame-state-resources
state

Return the unique GPU resources retained by one drawable frame state.

remove-duplicates
list*
luvcraft-frame-scene-bind-groupstate
luvcraft-frame-shadow-bind-groupstate
luvcraft-frame-post-bind-groupstate
luvcraft-frame-bloom-scene-bind-groupstate
luvcraft-frame-bloom-primary-bind-groupstate
luvcraft-frame-bloom-secondary-bind-groupstate
luvcraft-frame-post-uniform-bufferstate
luvcraft-frame-particle-vertex-bufferstate
luvcraft-frame-critter-vertex-bufferstate
luvcraft-frame-physics-vertex-bufferstate
luvcraft-frame-physics-instance-bufferstate
luvcraft-frame-body-vertex-bufferstate
luvcraft-frame-uniform-bufferstate
coerce
luvcraft-frame-world-text-bind-groupsstate
'list
:test#'eq
defconstant+luvcraft-cursor-scale+1.8"Framebuffer pixels per unit of the cursor shader's design grid."
defconstant+luvcraft-cursor-margin+5.0"Design-grid slack around the arrow for its shadow and antialiased edge."
luv.arithmetic:define-quantity-constant+luvcraft-maximum-frame-duration+0.1d0:typedouble-float:quantity
:quantity:frame-duration:unit:second
luv.arithmetic:define-quantity-constant+luvcraft-shadow-half-extent+64.0:typesingle-float:quantity
:quantity:world-distance:unit:cell
luv.arithmetic:define-quantity-constant+luvcraft-shadow-depth-radius+96.0:typesingle-float:quantity
:quantity:world-distance:unit:cell

The shadow's biases and filter radii are knobs: read at frame-pack time into the shadow control lanes, so a turn shows on the next frame.

defparameter*luvcraft-shadow-base-bias*0.00405"Constant depth bias applied to every shadow comparison."
defparameter*luvcraft-shadow-slope-bias*0.002"Depth bias scaled by the receiver's slope to the light."
defparameter*luvcraft-shadow-minimum-filter-radius*8.0"PCF radius in shadow texels when the sun is a point."
defparameter*luvcraft-shadow-maximum-filter-radius*24.0"PCF radius in shadow texels at the sun's widest."
define-knobshadow-base-bias
:group:shadows:label"base bias":quantity
:quantity:shadow-depth:unit:one:character:difference
:minimum0.0:maximum0.005:step0.00005
*luvcraft-shadow-base-bias*
define-knobshadow-slope-bias
:group:shadows:label"slope bias":quantity
:quantity:shadow-depth:unit:one:character:difference
:minimum0.0:maximum0.01:step0.0001
*luvcraft-shadow-slope-bias*
define-knobshadow-minimum-filter-radius
:group:shadows:label"softness, sun small":quantity
:quantity:shadow-filter-radius:unit:one
:unit-label" px":minimum0.0:maximum8.0:step0.5
*luvcraft-shadow-minimum-filter-radius*
define-knobshadow-maximum-filter-radius
:group:shadows:label"softness, sun wide":quantity
:quantity:shadow-filter-radius:unit:one
:unit-label" px":minimum1.0:maximum24.0:step0.5
*luvcraft-shadow-maximum-filter-radius*
defunensure-block-atlas-sample-transfer
format

Check the host texture format against the block shader's decoded result.

let*
resource
find'luvcraft.shaders::block-atlas
luv.shader:shader-specification-resources
:key#'luv.shader:shader-object-name
declared
andresource
luv.shader:shader-resource-sample-transferresource
unless
eqexpecteddeclared
error"Block atlas format ~S implies ~S sampling, but the shader declares ~S."formatexpecteddeclared
format
defunmake-block-world-crosshair-vertices
widthheight&optionalxy

Make an outlined pixel-sized crosshair at screen X,Y, or at the centre.

let
vertices
make-array0:element-type'single-float:adjustablet:fill-pointer0
origin-x
ifx0.0

The Vulkan viewport already turns clip Y into downward-growing screen coordinates, so pointer Y keeps its sign here.

origin-y
ify0.0
labels
clip-x
pixels
/
*2.0pixels
width
clip-y
pixels
/
*2.0pixels
height
vertex
xycolor
dolist
component
list
clip-x
+origin-xx
clip-y
+origin-yy
0.0
firstcolor
secondcolor
thirdcolor
vector-push-extend
coercecomponent'single-float
vertices
rectangle
lefttoprightbottomcolor
dolist
corner
list
listlefttop
listrighttop
listrightbottom
listlefttop
listrightbottom
listleftbottom
vertex
firstcorner
secondcorner
color

Charcoal establishes a crisp edge on both snow and foliage; the smaller white pair is emitted afterward and paints over it.

rectangle-2.25-11.02.2511.0'
0.080.090.10
rectangle-11.0-2.2511.02.25'
0.080.090.10
rectangle-0.75-8.00.758.0'
0.960.981.0
rectangle-8.0-0.758.00.75'
0.960.981.0
defunmake-luvcraft-cursor-vertices

Make the quad the cursor shader draws its arrow inside, tip at screen X,Y.

Only the hotspot and the scale live here: each vertex carries its offset from the tip in the shader's design grid, and the fragment stage turns that into the outline, the fill, and the shadow.

let
vertices
make-array0:element-type'single-float:adjustablet:fill-pointer0
origin-x
origin-y
labels
vertex
local-xlocal-y
dolist
component
list0.0local-xlocal-y
vector-push-extend
coercecomponent'single-float
vertices

The arrow starts at the design grid's origin and runs to the extent its own outline reaches; the margin leaves room for the antialiased edge and the shadow cast down and to the right.

destructuring-bind
arrow-widtharrow-height
let*
vertexlefttop
vertexrighttop
vertexrightbottom
vertexlefttop
vertexrightbottom
vertexleftbottom
defunmake-block-world-sky-vertices

Make the one fullscreen triangle in normalized clip coordinates.

ensure-vertex-product-contract
make-array9:element-type'single-float:initial-contents'
-1.0-1.00.53.0-1.00.5-1.03.00.5
:sky-vertices3
defunshadow-frame-rows
camerasky&optionalanchor

Pack a texel-stable orthographic light-space transform as four vec4 rows.

Returns the rows and the anchor to hand back next frame. The anchor is the world point the light-space texel lattice is built around: it follows the camera in whole texels of the current light basis, so camera translation never moves the lattice by a fraction of a texel, and the sun's rotation turns the lattice about the camera rather than about the world origin. Without an anchor the camera position itself starts one.

let*
center
forward
vec3-scale
vec3-normalize
sky-frame-parameters-sun-directionsky
-1.0

Texel snapping stabilizes the map against translation but can do nothing about rotation, so the free choice of roll about the light axis is worth making well. World up is a continuous reference -- the tilted orbit never reaches the Y pole -- but it is a badly conditioned one: the sun's angle to it changes all day, the transverse component collapses toward the orbit tilt as the sun climbs, and the roll rate consequently peaks at noon, spinning the whole texel grid under the world exactly when the sun is highest. The sun's own axis of revolution keeps a constant angle to the sun at every hour, so the basis it induces simply turns with the day. #0604PY measures both the gain at noon and what it costs at dusk.

right
vec3-normalize
vec3-crossreferenceforward
up
vec3-crossforwardright
world-units-per-texel

Rotation still slides the lattice under the world, by the angle times the distance from the pivot. Snapping the camera to a lattice through the world origin put that pivot at the origin, so a player standing a few hundred units out watched every shadow edge vibrate a texel many times a second however slow the day. The pivot is instead a persistent anchor that walks toward the camera in whole texels of this frame's basis: the lattice never shifts under translation, and it rotates about a point within a texel of the eye. The anchor is kept in double precision so that the walk does not itself wobble the lattice. #QWTQ6R

anchor
let*
start
oranchorcenter
delta
make-vec3
-
vec3-xcenter
vec3-xstart
-
vec3-ycenter
vec3-ystart
-
vec3-zcenter
vec3-zstart
along-right
*
round
/
vec3-dotdeltaright
world-units-per-texel
world-units-per-texel
along-up
*
round
/
vec3-dotdeltaup
world-units-per-texel
world-units-per-texel
along-forward
vec3-dotdeltaforward
flet
walk
coerce
+
*along-right
*along-forward
'double-float
make-vec3
walk:x
walk:y
walk:z
center-right
vec3-dotanchorright
center-up
vec3-dotanchorup
flet
lane
axisscaleoffset
list
coerceoffset'single-float
values
append
laneright
/extent
-
/center-rightextent
laneup
/extent
-
/center-upextent
laneforward
/
*2.0depth-radius
-0.5
/
vec3-dotanchorforward
*2.0depth-radius
'
0.00.00.01.0
anchor
defunframe-uniform-data
sessionwidthheight&keycamera-lanes

Pack the frame environment: camera lanes plus the evaluated sky.

Lane order must match *FRAME-UNIFORM-MEMBERS* exactly; the construction-time check in block-world-camera-uniform-size keeps the two honest. camera-lanes may replace the session camera's own five lanes with a camera expressed in some other space; the environment lanes are packed the same either way.

let*
camera-lanes
orcamera-lanes
camera-uniform-data
luvcraft-session-camerasession
widthheight
sky
sky-frame-parameters
luvcraft-session-sky-clocksession
luvcraft-session-sky-profilesession
data
make-array
+
lengthcamera-lanes
56
:element-type'single-float
index
lengthcamera-lanes
replacedatacamera-lanes
flet
emit
&restvalues
dolist
valuevalues
setf
arefdataindex
coercevalue'single-float
incfindex
color
vector
coercevector'list
let
sun
sky-frame-parameters-sun-directionsky

The sky's cloud deck drifts, so the frame environment carries a bounded elapsed time alongside the fog band it shares a lane with.

emit
sky-frame-parameters-fog-nearsky
sky-frame-parameters-fog-farsky
mod
or
luvcraft-session-last-frame-timesession
0d0
3600d0
sky-frame-parameters-cloudinesssky
emit
vec3-xsun
vec3-ysun
vec3-zsun
sky-frame-parameters-day-factorsky
apply#'emit
append
color
sky-frame-parameters-sun-colorsky
list
sky-frame-parameters-sun-angular-widthsky

The zenith and horizon lanes' spare w carry the target's height and width in pixels: what a vertex stage needs to size a pixel.

apply#'emit
append
color
sky-frame-parameters-zenith-colorsky
listheight
apply#'emit
append
color
sky-frame-parameters-horizon-colorsky
listwidth
apply#'emit
append
color
sky-frame-parameters-ambient-colorsky
list
sky-frame-parameters-exposuresky
apply#'emit
append
color
sky-frame-parameters-fog-colorsky
list
if
luvcraft-session-shadow-diagnostic-psession
1.00.0

The texture resource owns atlas extent. Meshes retain only a tile offset under the mapping which painted that resource, so replacing it with a wider atlas needs no remesh.

let
renderer
luvcraft-session-renderersession
emit
if
slot-boundprenderer'atlas-texture
first
gpu-texture-size
luvcraft-renderer-atlas-texturerenderer
0.00.00.0
multiple-value-bind
rowsanchor
shadow-frame-rows
luvcraft-session-camerasession
sky
luvcraft-session-shadow-anchorsession
setf
luvcraft-session-shadow-anchorsession
anchor
apply#'emitrows
unless
=index
lengthdata
error"Frame uniform packing emitted ~D of ~D lanes."index
lengthdata
data

Grading is art direction, not architecture: these are the live knobs the presentation stack reads every frame, so a SLY eval can retune the whole look of the running game without rebuilding a pipeline.

defparameter*luvcraft-exposure*0.45"Overall scene exposure multiplied into the sky profile's own exposure."
defparameter*luvcraft-crosshair-p*t"Draw the crosshair. A film shot is not aiming at anything."
defparameter*luvcraft-focus-blur-p*nil

Engage the focus-plane background blur without a modal focus.

The focus plane is the depth at the centre of the frame, so a film shot that wants a subject sharp against a blurred background aims the centre ray at ground or blocks standing at the subject's distance. A global rather than a binding because the presentation uniform is packed on the canvas thread, not the thread directing the film.

defparameter*luvcraft-bloom-gain*0.22

How much of the blurred bright-pass image is added back in linear light.

The chain's kernel is wide and runs twice, so this is a glow spread over a sixth of the frame rather than a halo: past about a third it stops reading as light around the sun and starts reading as fog over everything.

defparameter*luvcraft-shaft-gain*0.30"How strongly sunlight scattered around the solar disc streaks the frame."
defparameter*luvcraft-vignette*0.60"Corner falloff of the presented frame, as a fraction of full brightness."
defparameter*luvcraft-bloom-threshold*1.5"Luminance at which a fragment starts contributing to the bloom chain."
defparameter*luvcraft-shaft-decay*0.955"Per-tap attenuation along a light shaft; nearer one reaches further."
define-knobexposure
:group:grading:quantity
:quantity:exposure:unit:one
:unit-label"×":minimum0.2:maximum3.0:step0.05
*luvcraft-exposure*
define-knobbloom-threshold
:group:grading:quantity
:quantity:luminance-threshold:unit:one
:minimum0.2:maximum6.0:step0.1
*luvcraft-bloom-threshold*
define-knobbloom-gain
:group:grading:quantity
:quantity:lens-gain:unit:one
:unit-label"×":minimum0.0:maximum2.0:step0.05
*luvcraft-bloom-gain*
define-knobshaft-gain
:group:grading:label"light shafts":quantity
:quantity:lens-gain:unit:one
:unit-label"×":minimum0.0:maximum2.0:step0.05
*luvcraft-shaft-gain*
define-knobshaft-decay
:group:grading:label"shaft reach":quantity
:quantity:shaft-decay:unit:one
:minimum0.85:maximum0.999:step0.005
*luvcraft-shaft-decay*
define-knobvignette
:group:grading:quantity
:quantity:vignette-strength:unit:one
:minimum0.0:maximum0.6:step0.02
*luvcraft-vignette*
defunluvcraft-sun-screen-position
cameraskywidthheight

The sun's presentation UV and how strongly it counts as on screen.

The weight fades the solar lens effects out as the disc leaves the frame or falls behind the camera, so a turn of the head does not pop the shafts.

multiple-value-bind
rightupforward
let*
sun
sky-frame-parameters-sun-directionsky
view-z
vec3-dotforwardsun
if
<=view-z0.02
values0.50.50.0
let*
focal
aspect
/
coercewidth'single-float
height
clip-x
/
*
vec3-dotrightsun
/focalaspect
view-z
clip-y
-
/
*focal
view-z
edge
max
absclip-x
absclip-y
weight
*
sky-smoothstep1.450.80edge
sky-smoothstep0.020.30view-z
sky-frame-parameters-day-factorsky
values
coerce
*0.5
+clip-x1.0
'single-float
coerce
*0.5
+clip-y1.0
'single-float
coerceweight'single-float
defunluvcraft-post-uniform-data
sessionwidthheight&optional
presentation-widthwidth
presentation-heightheight

Pack the presentation environment: texel sizes, lens gains, and the sun.

Lane order must match *POST-UNIFORM-MEMBERS* exactly; the construction-time check in luvcraft-post-uniform-size keeps the two honest.

let*
camera
luvcraft-session-camerasession
sky
sky-frame-parameters
luvcraft-session-sky-clocksession
luvcraft-session-sky-profilesession
multiple-value-bind
sun-usun-vsun-weight
let
elapsed
or
luvcraft-session-last-frame-timesession
0d0
make-array20:element-type'single-float:initial-contents
mapcar
lambda
value
coercevalue'single-float
list
if
or
luvcraft-session-modal-focussession
*luvcraft-focus-blur-p*
1.00.0
**luvcraft-exposure*
sky-frame-parameters-exposuresky
*luvcraft-bloom-gain**luvcraft-vignette**luvcraft-bloom-threshold*sun-usun-vsun-weight
if
luvcraft-session-shadow-diagnostic-psession
1.00.0
/1.0
firstbloom-extent
/1.0
secondbloom-extent
*luvcraft-shaft-decay*
modelapsed3600d0
/1.0presentation-width
/1.0presentation-height
0.00.0
defunluvcraft-post-uniform-size
session

The presentation buffer byte size derived from the shader-visible block.

let*
unless
=sizebytes
error

Presentation uniform ABI mismatch: the shader block occupies ~ ~D bytes but the host packs ~D.

sizebytes
size
defunframe-shader-uniform-product-layout
block

Flatten block's byte-offset members into frame-buffer float positions.

This is deliberately luvcraft's fixed 32-bit-lane ABI adapter, not a claim about general uniform-block packing. The shader owns offsets and member quantities; the host independently owns the product it writes.

let
unless
zerop
modbytes4
error"Frame shader uniform size ~D is not a whole float lane count."bytes
dolist
member
shader:shader-uniform-block-membersblock
let*
byte-offset
shader:shader-uniform-member-offsetmember
width
shader:shader-type-component-count
unless
andwidth
zerop
modbyte-offset4
error"Frame shader member ~S is not a 32-bit scalar-lane value."
let
base
/byte-offset4
whenwhole
pushprojections
whenlayout
unless
=width
luv.arithmetic:quantity-layout-extentlayout
error"Frame shader member ~S has width ~D but layout ~D."width
luv.arithmetic:quantity-layout-extentlayout
dolist
projection
luv.arithmetic:quantity-layout-projectionslayout
push
luv.arithmetic:make-quantity-projection
mapcar
luv.arithmetic:quantity-projection-positionsprojection
luv.arithmetic:quantity-projection-specificationprojection
projections
luv.arithmetic:make-quantity-layout
/bytes4
nreverseprojections
defunblock-world-camera-uniform-size
session

The frame buffer byte size derived from the shader-visible block layout.

Checked against the host's packed frame data at construction, so growing the frame uniform cannot silently diverge between shader and host.

let*
unless
=sizebytes
error

Frame uniform ABI mismatch: the shader block occupies ~D ~ bytes but the host packs ~D.

sizebytes
unless
luv.arithmetic:quantity-layout=host-layoutshader-layout
error"Frame uniform semantic ABI mismatch between ~S and shader ~S."
mapcar#'shader:shader-object-name
shader:shader-uniform-block-membersblock
size
defunremember-luvcraft-renderer-resource
rendererresource

Adopt resource into renderer's single release inventory.

pushresource
luvcraft-renderer-resourcesrenderer
resource
defunforget-luvcraft-renderer-resource
rendererresource

Drop resource from renderer's inventory for immediate release.

setf
luvcraft-renderer-resourcesrenderer
deleteresource
luvcraft-renderer-resourcesrenderer
:test#'eq
resource
defunrelease-luvcraft-renderer-resource
rendererresource
whenresource

A failed destruction must leave the only retry handle with its owner.

destroyresource
values
defunmake-luvcraft-bloom-bind-group
sessionuniform-bufferviewlabel

Bind one lens-chain source: a sampled texture, linear filter, uniforms.

create
luvcraft-session-devicesession
make-bind-group-descriptor:labellabel:layout:entries`
:binding0:resource,view
:binding1:resource,
:binding2:resource,uniform-buffer
defunluvcraft-bloom-extent
extent

The reduced attachment size the lens chain runs at, at least one texel.

Everything below is sized to the frame rather than to the world, which means a window resize invalidates all of it at once. Keeping the whole set behind one constructor is what lets start-luvcraft and a live resize agree on formats and usages without either one drifting.

defunmake-luvcraft-frame-attachments
devicecontextrender-extentpresentation-extent

Create the scene and presentation attachments as one GPU-object cohort.

The scene is drawn at render-extent into a linear HDR colour attachment with its own depth buffer and reduced lens chain. World-space application panels draw analytically into a transparent colour/depth pair at presentation-extent; the presentation shader depth-composes that pair over the scaled scene before native-density HUD and the complete surface copy.

let
bloom-extent
madenil
completed-pnil
flet
texture
labelsizeformatusage
let
texture
createdevice
make-texture-descriptor:labellabel:sizesize:dimensions:2d:formatformat:usageusage
pushtexturemade
let
view
createdevice
make-texture-view-descriptor:texturetexture
pushviewmade
valuestextureview
unwind-protect
multiple-value-bind
color-texturecolor-view
texture"block world color"render-extent+luvcraft-scene-color-format+'
:render-attachment:texture-binding:copy-src
multiple-value-bind
depth-texturedepth-view
texture"block world depth"render-extent:depth32-float'
:render-attachment:texture-binding
multiple-value-bind
bloom-primary-texturebloom-primary-view
texture"block world bloom primary"bloom-extent+luvcraft-bloom-color-format+'
:render-attachment:texture-binding
multiple-value-bind
bloom-secondary-texturebloom-secondary-view
texture"block world bloom secondary"bloom-extent+luvcraft-bloom-color-format+'
:render-attachment:texture-binding
multiple-value-bind
world-panel-color-textureworld-panel-color-view
texture"world application panel color"presentation-extent+luvcraft-scene-color-format+'
:render-attachment:texture-binding
multiple-value-bind
world-panel-depth-textureworld-panel-depth-view
texture"world application panel depth"presentation-extent:depth32-float'
:render-attachment:texture-binding
multiple-value-bind
presentation-texturepresentation-view
texture"block world presentation color"presentation-extent'
:render-attachment:copy-src
setfcompleted-pt
list:render-extentrender-extent:presentation-extentpresentation-extent:color-texturecolor-texture:color-viewcolor-view:depth-texturedepth-texture:depth-viewdepth-view:world-panel-color-textureworld-panel-color-texture:world-panel-color-viewworld-panel-color-view:world-panel-depth-textureworld-panel-depth-texture:world-panel-depth-viewworld-panel-depth-view:bloom-primary-texturebloom-primary-texture:bloom-primary-viewbloom-primary-view:bloom-secondary-texturebloom-secondary-texture:bloom-secondary-viewbloom-secondary-view:presentation-texturepresentation-texture:presentation-viewpresentation-view
unlesscompleted-p

Preserve the originating creation failure and make a best effort to retire every partial candidate, even if one destroy also errs.

with-release-warnings
dolist
resourcemade
releasing:frame-attachment-candidate
destroyresource
defuninstall-luvcraft-frame-attachments
rendererattachments

Adopt and atomically publish attachments as one frame-sized cohort.

let*
inventory
appendresources
luvcraft-renderer-resourcesrenderer

Everything that may allocate is complete before either owner slot is changed. FRAME-ATTACHMENTS is the sole reader-visible publication.

setf
luvcraft-renderer-resourcesrenderer
inventory
luvcraft-renderer-frame-attachmentsrenderer
attachments
renderer
defunluvcraft-renderer-frame-attachment-resources

Return renderer's currently published extent-sized GPU objects.

luvcraft-frame-attachment-resources
luvcraft-renderer-frame-attachmentsrenderer
defunrelease-luvcraft-frame-attachment-resources
rendererresources

Release an unpublished or superseded attachment cohort from renderer.

with-release-report
dolist
resourceresources
values
defunrelease-luvcraft-frame-attachments

Release renderer's currently published frame-sized image cohort.

setf
luvcraft-renderer-frame-attachmentsrenderer
nil
values
defunluvcraft-frame-attachment-resources
attachments

Return the GPU objects in an attachment candidate, excluding its extents.

loopfor
keyvalue
onattachmentsby#'cddrunless
memberkey'
:render-extent:presentation-extent
collectvalue
defunmake-luvcraft-lens-pipeline
devicelayoutrolelabel

One fullscreen lens-chain stage, named by the fragment method it runs.

make-live-shader-pipeline:rolerole:vertex-role:focus-post:labellabel:devicedevice:layoutlayout:vertex-buffers'
:array-stride12:attributes
:shader-location0:offset0:format:float32x3
:target-format+luvcraft-bloom-color-format+:primitive'
:topology:triangle-list
:depth-stencilnil

--------------------------------------------------------------------- The physics sphere drawer: the live pipeline and shared quad behind the scene pass's instanced body draw. It registers through the overlay protocol -- the renderer's own pipeline inventory is a closed slot set -- so refresh-luvcraft-shaders rebuilds its shader methods and session teardown releases it, but its draw is not an overlay's: the bodies are encoded with the rest of the world geometry, where the cubes drew.

defclassphysics-sphere-drawer
pipeline:initarg:pipeline:accessorphysics-sphere-drawer-pipeline
quad-buffer:initarg:quad-buffer:accessorphysics-sphere-drawer-quad-buffer
:documentation

Owns the sphere pipeline and the one six-vertex quad every body shares.

defmethodluvcraft-overlay-live-shader-pipelines
list
physics-sphere-drawer-pipelinedrawer
defmethodencode-luvcraft-overlay
sessionpasssurface-texture

Deliberately nothing: the bodies are drawn from the scene pass proper, before the player's arms and the particles, where the cubes once drew.

declare
ignoresessionpasssurface-texture
nil
defmethodrelease-luvcraft-overlay
when
physics-sphere-drawer-pipelinedrawer
release-live-shader-pipeline
physics-sphere-drawer-pipelinedrawer
setf
physics-sphere-drawer-pipelinedrawer
nil
when
physics-sphere-drawer-quad-bufferdrawer
destroy
physics-sphere-drawer-quad-bufferdrawer
setf
physics-sphere-drawer-quad-bufferdrawer
nil
values
defunluvcraft-session-physics-sphere-drawer
session
find-if
lambda
overlay
luvcraft-session-overlayssession
defunattach-physics-sphere-drawer
session

Create the sphere pipeline and quad for session and register the drawer.

let*
device
luvcraft-session-devicesession
quad-buffernil
pipelinenil
completed-pnil
unwind-protect
progn
setfquad-buffer
createdevice
make-buffer-descriptor:label"physics sphere proxy quad":size
*4
lengthvertex-data
:usage'
:vertex:copy-dst
pipeline
make-live-shader-pipeline:role:physics-sphere:vertex-role:physics-sphere:label"physics body sphere pipeline":devicedevice:layout:vertex-buffers'
:array-stride12:attributes
:shader-location0:offset0:format:float32x3
:array-stride64:step-mode:instance:attributes
:shader-location1:offset0:format:float32x4
:shader-location2:offset16:format:float32x4
:shader-location3:offset32:format:float32x4
:shader-location4:offset48:format:float32x4
:target-format+luvcraft-scene-color-format+:target-blend:premultiplied-alpha:primitive'
:topology:triangle-list

The lowering has no fragment-depth output, so the quads test the terrain's depth without writing their own.

:depth-stencil'
:format:depth32-float:depth-write-enablednil:depth-compare:less
write-bufferquad-buffervertex-data
let
drawer
make-instance'physics-sphere-drawer:pipelinepipeline:quad-bufferquad-buffer
setfcompleted-pt
drawer
unlesscompleted-p
whenpipeline
ignore-errors
whenquad-buffer
ignore-errors
destroyquad-buffer
defunluvcraft-frame-state
sessionsurface-texture
let
key
canvas-frame-resource-key
luvcraft-session-contextsession
surface-texture
or
let
buffernil
particle-vertex-buffernil
critter-vertex-buffernil
physics-vertex-buffernil
physics-instance-buffernil
body-vertex-buffernil
scene-bind-groupnil
shadow-bind-groupnil
post-uniform-buffernil
post-bind-groupnil
bloom-scene-bind-groupnil
bloom-primary-bind-groupnil
bloom-secondary-bind-groupnil
world-text-bind-groups
completed-pnil
unwind-protect
progn
setfbuffer
create
luvcraft-session-devicesession
make-buffer-descriptor:label"block world camera uniform":size:usage'
:uniform
particle-vertex-buffer
create
luvcraft-session-devicesession
make-buffer-descriptor:label"block smash particle vertices":size+block-particle-buffer-size+:usage'
:vertex
critter-vertex-buffer
create
luvcraft-session-devicesession
make-buffer-descriptor:label"critter model vertices":size+critter-buffer-size+:usage'
:vertex
physics-vertex-buffer
create
luvcraft-session-devicesession
make-buffer-descriptor:label"physics body shadow cube vertices":size+physics-shadow-vertex-buffer-size+:usage'
:vertex
physics-instance-buffer
create
luvcraft-session-devicesession
make-buffer-descriptor:label"physics body sphere instances":size+physics-instance-buffer-size+:usage'
:vertex
body-vertex-buffer
create
luvcraft-session-devicesession
make-buffer-descriptor:label"player body vertices":size+player-body-buffer-size+:usage'
:vertex
scene-bind-group
create
luvcraft-session-devicesession
make-bind-group-descriptor:label"block world scene bindings":layout:entries`
:binding0:resource,
:binding1:resource,
:binding2:resource,buffer
:binding3:resource,
:binding4:resource,
:binding6:resource,
shadow-bind-group
create
luvcraft-session-devicesession
make-bind-group-descriptor:label"block world shadow-pass bindings":layout:entries`
:binding2:resource,buffer
post-uniform-buffer
create
luvcraft-session-devicesession
make-buffer-descriptor:label"block world focus post uniform":size:usage'
:uniform
post-bind-group
create
luvcraft-session-devicesession
make-bind-group-descriptor:label"block world focus post bindings":layout:entries`
:binding0:resource,
:binding1:resource,
:binding2:resource,
:binding3:resource,post-uniform-buffer
:binding4:resource,
:binding5:resource,
:binding6:resource,
:binding7:resource,
:binding8:resource,
bloom-scene-bind-group
make-luvcraft-bloom-bind-groupsessionpost-uniform-buffer"block world bloom scene bindings"
bloom-primary-bind-group
make-luvcraft-bloom-bind-groupsessionpost-uniform-buffer"block world bloom primary bindings"
bloom-secondary-bind-group
make-luvcraft-bloom-bind-groupsessionpost-uniform-buffer"block world bloom secondary bindings"
world-text-bind-groups
if
luvcraft-session-world-textsession
make-world-text-frame-bind-groups
luvcraft-session-world-textsession
luvcraft-session-devicesession
buffer
let
state
make-instance'luvcraft-frame-state:uniform-bufferbuffer:particle-vertex-bufferparticle-vertex-buffer:critter-vertex-buffercritter-vertex-buffer:physics-vertex-bufferphysics-vertex-buffer:physics-instance-bufferphysics-instance-buffer:body-vertex-bufferbody-vertex-buffer:scene-bind-groupscene-bind-group:shadow-bind-groupshadow-bind-group:post-uniform-bufferpost-uniform-buffer:post-bind-grouppost-bind-group:bloom-scene-bind-groupbloom-scene-bind-group:bloom-primary-bind-groupbloom-primary-bind-group:bloom-secondary-bind-groupbloom-secondary-bind-group:world-text-bind-groupsworld-text-bind-groups
dolist
remember-luvcraft-renderer-resource
luvcraft-session-renderersession
resource
setfstatecompleted-pt
state
unlesscompleted-p
dolist
group
remove-duplicates
coerceworld-text-bind-groups'list
:test#'eq
destroygroup
whenshadow-bind-group
destroyshadow-bind-group
whenscene-bind-group
destroyscene-bind-group
whenbloom-scene-bind-group
destroybloom-scene-bind-group
whenbloom-primary-bind-group
destroybloom-primary-bind-group
whenbloom-secondary-bind-group
destroybloom-secondary-bind-group
whenpost-bind-group
destroypost-bind-group
whenpost-uniform-buffer
destroypost-uniform-buffer
whenbody-vertex-buffer
destroybody-vertex-buffer
whenphysics-instance-buffer
destroyphysics-instance-buffer
whenphysics-vertex-buffer
destroyphysics-vertex-buffer
whencritter-vertex-buffer
destroycritter-vertex-buffer
whenparticle-vertex-buffer
destroyparticle-vertex-buffer
whenbuffer
destroybuffer
defundiscard-luvcraft-frame-states

Forget every cached per-drawable binding, which names images that are gone.

The bind groups are keyed by drawable, not by size, so nothing else would notice that their scene, depth, and lens-chain views belong to the previous window. Dropping them here makes the next frame rebuild them against the attachments the renderer actually holds.

let
states
luvcraft-renderer-frame-statesrenderer
with-release-report
maphash
lambda
keystate
declare
ignorekey
states
clrhashstates
values
defmethodrelease-luvcraft-component

Release renderer's pipelines and GPU resources exactly once.

with-release-report
dolist
when
and
slot-boundprendererslot
slot-valuerendererslot
releasing:renderer-pipeline

A failed pipeline stays named so a second release retries it.

slot-makunboundrendererslot
let
retainednil
dolist
resource
remove-duplicates
luvcraft-renderer-resourcesrenderer
:test#'eq
let
released-pnil
releasing:renderer-resource
destroyresource
setfreleased-pt
unlessreleased-p
pushresourceretained

Likewise, failed resources remain in the one owner inventory.

setf
luvcraft-renderer-resourcesrenderer
nreverseretained
when
and
null
luvcraft-renderer-resourcesrenderer
setf
luvcraft-renderer-frame-attachmentsrenderer
nil
clrhash
luvcraft-renderer-frame-statesrenderer
values
defmethodresize-luvcraft-component

Transactionally replace every extent-sized image owned by renderer.

check-typeextentsluvcraft-frame-extents
let*
render-extent
luvcraft-frame-extents-renderextents
presentation-extent
luvcraft-frame-extents-presentationextents
attachments
make-luvcraft-frame-attachments
luvcraft-renderer-devicerenderer
luvcraft-renderer-contextrenderer
render-extentpresentation-extent
installed-pnil
unwind-protect
progn

The new cohort exists before any old object is released. If creation fails, make-luvcraft-frame-attachments destroys its partial candidate and the installed renderer remains coherent. Vertex writes also precede publication so a failed write leaves the old extent installed and makes the next frame retry.

write-buffer
luvcraft-renderer-crosshair-vertex-bufferrenderer
make-block-world-crosshair-vertices
firstrender-extent
secondrender-extent
write-buffer
luvcraft-renderer-cursor-vertex-bufferrenderer
make-luvcraft-cursor-vertices
firstrender-extent
secondrender-extent
/
firstrender-extent
2.0
/
secondrender-extent
2.0
setfinstalled-pt

Publication precedes retirement. Even if a backend reports a destruction failure, every reader now observes the complete new cohort instead of a half-cleared renderer.

unlessinstalled-p
with-release-warnings
dolist
releasing:frame-attachment-candidate
destroyresource
renderer
defunensure-luvcraft-frame-extent
session

Rebuild session's frame-sized images when either frame extent changes.

A Retina canvas has two deliberately different extents: the game scene renders once per logical point, while presentation and direct application UI render once per physical drawable pixel. This runs at the top of a frame, after the backend has synchronized the drawable and inside the callback which owns GPU replacement. The outgoing images stay alive until their last submission completes.

let*
canvas
luvcraft-session-canvassession
render-extent
multiple-value-list
presentation-extent
canvas-extent
luvcraft-session-contextsession
unless
and
equalrender-extent
equalpresentation-extent
log-event:luvcraft"reframing scene ~{~D~^x~}, presentation ~{~D~^x~}"render-extentpresentation-extent

Pointer intent belongs to the session. The normal frame-boundary update replaces the renderer's centred candidate and retains DIRTY-P if its buffer write fails.

setf
luvcraft-session-pointer-dirty-psession
t
resize-luvcraft-component
luvcraft-session-renderersession
make-luvcraft-frame-extentsrender-extentpresentation-extent
valuesrender-extentpresentation-extent
defunluvcraft-world-panels-back-to-front
session

Return session's native-density world panels in painter order.

stable-sort
loopforoverlayin
luvcraft-session-overlayssession
when
eq:world-panel
collectoverlay
#'>:key
lambda
overlay
zdefun
encode-luvcraft-frame:zone:luvcraft/encode-frame
sessionsurface-textureencoder&keyreadback-buffersample
include-hud-pt
include-viewmodel-pt

The canvas callback is the ownership boundary for all GPU replacement. MOP notifications from SLY workers have only marked these artifacts dirty.

with-luvcraft-frame-timing
sampleluvcraft-frame-sample-shader-refresh-seconds:luvcraft/shader-refresh

The film's clock runs on the world's frames, so it advances here, before anything is encoded: the upload is an ordinary queue write, not part of this frame's command stream.

when
luvcraft-session-video-screensession
place-video-screen-listener
luvcraft-session-video-screensession
luvcraft-session-camerasession
advance-video-screen
luvcraft-session-video-screensession
luvcraft-session-devicesession
let
overlays
luvcraft-session-overlayssession
zone
:luvcraft/refresh-overlays:value
lengthoverlays
dolist
overlayoverlays
guarding-luvcraft-overlay
sessionoverlay:overlay-refresh
let*
products
with-luvcraft-frame-timing
sampleluvcraft-frame-sample-mesh-publication-seconds:luvcraft/mesh-publication
presentation-extent
frame
luvcraft-frame-statesessionsurface-texture
particle-vertices
block-particle-vertices
luvcraft-session-particle-systemsession
particle-vertex-count
critter-vertices
critter-vertices
luvcraft-session-critterssession
luvcraft-session-worldsession
body-vertices
body-vertex-count
physics-body-count
physics-shadow-vertex-count
when
let
mesh-vertices0
mesh-draws0
dolist
productproducts
let
vertices
block-mesh-vertex-count
luvcraft-chunk-product-meshproduct
when
pluspvertices
incfmesh-draws
incfmesh-verticesvertices
let*
resident-chunks
length
resident-world-chunks
luvcraft-session-worldsession
pending-production
production-system-pending-count
luvcraft-session-production-systemsession
staged-chunks
hash-table-count
luvcraft-session-staged-chunk-productssession
chunks
lengthproducts
text-glyph-count
if
luvcraft-session-world-textsession
length
world-text-run-glyphs
luvcraft-session-world-textsession
0
draws
+2
if
plusptext-glyph-count
10
*2mesh-draws
if
pluspparticle-vertex-count
10

The animals are drawn twice: once into the shadow map and once into the scene.

if
pluspcritter-vertex-count
20

The bodies too: shadow cubes, then spheres.

if
pluspphysics-body-count
20
if
pluspbody-vertex-count
10
vertices
++block-world-crosshair-vertex-count+3
*6text-glyph-count
particle-vertex-countbody-vertex-count
*2critter-vertex-count
physics-shadow-vertex-count
*6physics-body-count
*2mesh-vertices
whensample
setf
luvcraft-frame-sample-resident-chunk-countsample
resident-chunks
luvcraft-frame-sample-pending-production-countsample
pending-production
luvcraft-frame-sample-staged-chunk-countsample
staged-chunks
luvcraft-frame-sample-chunk-countsample
chunks
luvcraft-frame-sample-draw-countsample
draws
luvcraft-frame-sample-vertex-countsample
vertices

The same counts the benchmark records per sample, drawn against the live timeline so a frame-time spike can be read against the world that produced it.

tracy-plot"resident chunks"resident-chunks
tracy-plot"pending production"pending-production
tracy-plot"staged chunks"staged-chunks
tracy-plot"drawable chunks"chunks
tracy-plot"draws"draws
tracy-plot"vertices"vertices
let
center
luvcraft-session-residency-centersession
whencenter
tracy-plot"player chunk x"
firstcenter
tracy-plot"player chunk z"
secondcenter
with-luvcraft-frame-timing
sampleluvcraft-frame-sample-uniform-seconds:luvcraft/uniform-update
write-buffer
luvcraft-frame-uniform-bufferframe
frame-uniform-datasession
firstextent
secondextent
write-buffer
luvcraft-frame-post-uniform-bufferframe
luvcraft-post-uniform-datasession
firstextent
secondextent
firstpresentation-extent
secondpresentation-extent
when
pluspparticle-vertex-count
write-buffer
luvcraft-frame-particle-vertex-bufferframe
particle-vertices
when
pluspcritter-vertex-count
write-buffer
luvcraft-frame-critter-vertex-bufferframe
critter-vertices
when
pluspbody-vertex-count
write-buffer
luvcraft-frame-body-vertex-bufferframe
body-vertices
when
pluspphysics-body-count
write-buffer
luvcraft-frame-physics-vertex-bufferframe
write-buffer
luvcraft-frame-physics-instance-bufferframe
with-luvcraft-frame-timing
sampleluvcraft-frame-sample-shadow-encode-seconds:luvcraft/shadow-pass
let
pass
begin-render-passencoder
make-render-pass-descriptor:color-attachmentsnil:depth-stencil-attachment`
:view,:depth-load-op:clear:depth-store-op:store:depth-clear-value1.0
set-bind-grouppass0
luvcraft-frame-shadow-bind-groupframe
dolist
productproducts
let
mesh
luvcraft-chunk-product-meshproduct
when
plusp
block-mesh-vertex-countmesh
set-vertex-bufferpass0
luvcraft-chunk-product-vertex-bufferproduct
drawpass
block-mesh-vertex-countmesh

An animal standing in the sun casts a shadow like anything else solid; a tumbling smash fragment deliberately does not.

when
pluspcritter-vertex-count
set-vertex-bufferpass0
luvcraft-frame-critter-vertex-bufferframe
drawpasscritter-vertex-count

So do the balls, drops, and gobbets: things with weight. The scene draws them as true spheres; a depth map is happy with the little turning cubes.

when
pluspphysics-body-count
set-vertex-bufferpass0
luvcraft-frame-physics-vertex-bufferframe
drawpassphysics-shadow-vertex-count
with-luvcraft-frame-timing
sampleluvcraft-frame-sample-scene-encode-seconds:luvcraft/scene-pass
let
pass
begin-render-passencoder
make-render-pass-descriptor:color-attachments`
:view,:load-op:clear:store-op:store:clear-value
0.430.680.921.0
:depth-stencil-attachment`
:view,:depth-load-op:clear:depth-store-op:store:depth-clear-value1.0

The sky triangle fills the frame before block geometry, with depth writes disabled; the clear value remains only a safe fallback.

set-bind-grouppass0
luvcraft-frame-scene-bind-groupframe
drawpass3
dolist
productproducts
let
mesh
luvcraft-chunk-product-meshproduct
when
plusp
block-mesh-vertex-countmesh
set-vertex-bufferpass0
luvcraft-chunk-product-vertex-bufferproduct
drawpass
block-mesh-vertex-countmesh
when
pluspcritter-vertex-count
set-vertex-bufferpass0
luvcraft-frame-critter-vertex-bufferframe
drawpasscritter-vertex-count

The physics bodies, as ray-traced spheres: one instanced draw of camera-facing quads, records already ordered farthest first because the sphere pipeline blends without writing depth. The block pipeline and its bindings are restored afterwards for the player body and the particles, which expect them.

when
pluspphysics-body-count
let
whendrawer
set-pipelinepass
live-shader-pipeline-native-pipeline
physics-sphere-drawer-pipelinedrawer
set-vertex-bufferpass0
physics-sphere-drawer-quad-bufferdrawer
set-vertex-bufferpass1
luvcraft-frame-physics-instance-bufferframe
set-bind-grouppass0
luvcraft-frame-scene-bind-groupframe
drawpass6physics-body-count
set-bind-grouppass0
luvcraft-frame-scene-bind-groupframe
when
pluspparticle-vertex-count
set-vertex-bufferpass0
luvcraft-frame-particle-vertex-bufferframe
drawpassparticle-vertex-count

Before the text, so a caption drawn over the screen wins.

when
luvcraft-session-video-screensession
let*
screen
luvcraft-session-video-screensession
group
refresh-video-screen-bind-groupscreen
luvcraft-session-devicesession
luvcraft-frame-uniform-bufferframe
set-vertex-bufferpass0
video-screen-vertex-bufferscreen
set-vertex-bufferpass1
video-screen-instance-bufferscreen
set-bind-grouppass0group
drawpass61
when
luvcraft-session-world-textsession
let
text
luvcraft-session-world-textsession
set-vertex-bufferpass0
world-text-run-vertex-buffertext
set-vertex-bufferpass1
world-text-run-instance-buffertext
set-bind-grouppass0
aref
luvcraft-frame-world-text-bind-groupsframe
0
drawpass6
length
world-text-run-glyphstext
dolist
overlay
reverse
luvcraft-session-overlayssession
when
eq:scene
guarding-luvcraft-overlay
sessionoverlay:overlay-encode
encode-luvcraft-overlayoverlaysessionpasssurface-texture

First-person geometry is above every world participant regardless of overlay attachment order. It still lives in the scene texture (and therefore the lens/grade chain), unlike the later HUD pass.

wheninclude-viewmodel-p
dolist
overlay
reverse
luvcraft-session-overlayssession
when
eq:viewmodel
guarding-luvcraft-overlay
sessionoverlay:overlay-encode
encode-luvcraft-overlayoverlaysessionpasssurface-texture

A held item's own geometry follows the analytic hands so glass, buttons, and live displays remain legible in the grip; neither body nor item enters the shadow map.

when
andinclude-viewmodel-p
pluspbody-vertex-count
set-bind-grouppass0
luvcraft-frame-scene-bind-groupframe
set-vertex-bufferpass0
luvcraft-frame-body-vertex-bufferframe
drawpassbody-vertex-count
prepare-textureencoder:texture-binding
prepare-textureencoder:texture-binding

The lens chain: bright pass, separable blur, radial sun sweep. Each stage is one fullscreen triangle over the reduced attachment pair, leaving the blurred bloom in the primary and the shafts in the secondary for presentation to add back.

flet
lens-stage
pipelinebind-groupviewtexture
let
pass
begin-render-passencoder
make-render-pass-descriptor:color-attachments`
:view,view:load-op:clear:store-op:store:clear-value
0.00.00.01.0
:depth-stencil-attachmentnil
set-pipelinepass
live-shader-pipeline-native-pipelinepipeline
set-bind-grouppass0bind-group
drawpass3
prepare-textureencodertexture:texture-binding
let
lens-stage
luvcraft-frame-bloom-scene-bind-groupframe
primary-viewprimary

The separable pair runs twice. Convolving the thirteen-tap kernel with itself widens the glow by the square root of two without a second pair of attachments or a second pipeline, and the second pass is what turns a visible halo into light.

dotimes
iteration2
lens-stage
luvcraft-frame-bloom-primary-bind-groupframe
secondary-viewsecondary
lens-stage
luvcraft-frame-bloom-secondary-bind-groupframe
primary-viewprimary
lens-stage
luvcraft-frame-bloom-primary-bind-groupframe
secondary-viewsecondary

Retained application panels stay world-projected, but they are not another low-resolution scene material. Draw their analytic command streams at the physical presentation extent into a transparent HDR layer. Far-to-near ordering gives each panel ordinary painter semantics; its depth writes use :ALWAYS because independently generated coplanar analytic vertices are not bit-identical enough for an equality depth test. The final native depth is then compared with the logical scene depth in the post shader.

zone
:luvcraft/world-panel-pass
let
pass
begin-render-passencoder
make-render-pass-descriptor:color-attachments`
:view,:load-op:clear:store-op:store:clear-value
0.00.00.00.0
:depth-stencil-attachment`
:view,:depth-load-op:clear:depth-store-op:store:depth-clear-value1.0
dolist
guarding-luvcraft-overlay
sessionoverlay:overlay-encode
encode-luvcraft-overlayoverlaysessionpasssurface-texture
let
pass
begin-render-passencoder
make-render-pass-descriptor:color-attachments`
:view,:load-op:clear:store-op:store:clear-value
0.00.00.01.0
:depth-stencil-attachmentnil
set-bind-grouppass0
luvcraft-frame-post-bind-groupframe
drawpass3
wheninclude-hud-p
dolist
overlay
reverse
luvcraft-session-overlayssession
when
guarding-luvcraft-overlay
sessionoverlay:overlay-encode
encode-luvcraft-overlayoverlaysessionpasssurface-texture

KMSDRM has no native cursor plane. A focused CLIM view still gets the same absolute pointer events as any other backend, so draw the crosshair geometry at their last position after the HUD itself.

when
andinclude-hud-p
luvcraft-session-software-cursor-psession
luvcraft-session-modal-focussession

Motion events publish only the newest pointer state. Consume it once at the frame boundary, no matter how many reports SDL drained.

when
luvcraft-session-pointer-dirty-psession
write-buffer
make-luvcraft-cursor-vertices
firstextent
secondextent
or
luvcraft-session-pointer-xsession
/
firstextent
2.0
or
luvcraft-session-pointer-ysession
/
secondextent
2.0
setf
luvcraft-session-pointer-dirty-psession
nil
set-bind-grouppass0
luvcraft-frame-scene-bind-groupframe
with-luvcraft-frame-timing
sampleluvcraft-frame-sample-surface-copy-encode-seconds:luvcraft/surface-copy
whenreadback-buffer
encodeencoder
make-gpu-copy-texture-to-buffer-command:source:destinationreadback-buffer
encodeencoder
make-gpu-copy-texture-command:source:destinationsurface-texture
let
mirror
luvcraft-session-frame-mirrorsession
whenmirror
encodeencoder
make-gpu-copy-texture-command:source:destinationmirror
defunluvcraft-session-neighborhood-center
session

Return the world X and Z the session's neighbourhood is centred on.

let
player
luvcraft-session-playersession
camera
luvcraft-session-camerasession
ifplayer
values
player-xplayer
player-zplayer
values
camera-xcamera
camera-zcamera
defunadvance-luvcraft-critters
sessionseconds

Advance session's animals and keep its neighbourhood populated with them.

let
critters
luvcraft-session-critterssession
world
luvcraft-session-worldsession
advance-critterscrittersworldseconds
multiple-value-bind
xz
defunadvance-luvcraft-session-to
sessiontimestamp

Advance session to the time its acquired frame is expected to be visible.

let*
last
luvcraft-session-last-frame-timesession
seconds
setf
luvcraft-session-last-frame-timesession
timestamp
advance-sky-clock
luvcraft-session-sky-clocksession
seconds
advance-block-particles
luvcraft-session-particle-systemsession
seconds

Scene participants own their behavior while core owns the frame clock. An embodied agent therefore advances here without making core depend on the agent system which specializes this open protocol.

dolist
overlay
reverse
luvcraft-session-overlayssession
guarding-luvcraft-overlay
sessionoverlay:overlay-advance
advance-luvcraft-overlayoverlaysessionseconds

A moving interaction takes its turn after the world it moves in and before the player controller, which stands down while it carries them.

let
focus
luvcraft-session-modal-focussession
intent
luvcraft-session-movement-intentsession
advance-luvcraft-focusfocussessionseconds
when
setf
luvcraft-session-physics-accumulatorsession
0d0
movement-intent-jump-requested-pintent
nil
let
player
luvcraft-session-playersession
intent
luvcraft-session-movement-intentsession
focus
luvcraft-session-modal-focussession
when

Held controls have immediate authority over the player's legs. Move To is still a useful player primitive, but touching movement cancels it instead of fighting a hidden autopilot.

when
cancel-body-movementplayer"cancelled by manual player input"
incf
luvcraft-session-physics-accumulatorsession
seconds
zone
:simulation/player-steps:value
floor
luvcraft-session-physics-accumulatorsession
+player-physics-step+
loopwhile
>=
luvcraft-session-physics-accumulatorsession
+player-physics-step+
do
if
progn
advance-body-movementplayer
luvcraft-session-worldsession
+player-physics-step+
unless
sync-camera-to-player
luvcraft-session-camerasession
player
step-block-world-playerplayer
luvcraft-session-worldsession
luvcraft-session-camerasession
intent+player-physics-step+:jump-p
movement-intent-jump-requested-pintent
:sync-camera-p
setf
movement-intent-jump-requested-pintent
nil
decf
luvcraft-session-physics-accumulatorsession
+player-physics-step+
advance-player-body
luvcraft-session-bodysession
sessionseconds
defunrender-luvcraft-frame
sessiontimestamp&optionalsample
declare
ignoretimestamp
when
luvcraft-session-running-psession
let
tracy-frame-start
and
get-internal-real-time
simulation-before
andsample
luvcraft-frame-sample-simulation-secondssample
streaming-before
andsample
luvcraft-frame-sample-streaming-secondssample
with-luvcraft-frame-timing
sampleluvcraft-frame-sample-frame-seconds:luvcraft/frame
with-luvcraft-frame-timing
sampleluvcraft-frame-sample-presentation-seconds:luvcraft/presentation
present-canvas-frame
luvcraft-session-contextsession
lambda
surface-textureencoderpresentation-time

Acquisition is the timing boundary: simulation and every visual clock now describe the beat this particular drawable is predicted to reach, not when the outer timer happened to ask for work.

with-luvcraft-frame-timing
sampleluvcraft-frame-sample-simulation-seconds:luvcraft/simulation
advance-luvcraft-session-tosessionpresentation-time
with-luvcraft-frame-timing
sampleluvcraft-frame-sample-streaming-seconds:luvcraft/streaming
encode-luvcraft-framesessionsurface-textureencoder:samplesample

PRESENTATION encloses acquisition and submission now, so remove the explicitly measured application phases nested inside it. This preserves the sample's old meaning while letting acquisition choose the frame's target time.

whensample
decf
luvcraft-frame-sample-presentation-secondssample
+
-
luvcraft-frame-sample-simulation-secondssample
simulation-before
-
luvcraft-frame-sample-streaming-secondssample
streaming-before
whentracy-frame-start
tracy-plot"frame CPU ms"
*1000d0
/
-
get-internal-real-time
tracy-frame-start
coerceinternal-time-units-per-second'double-float
tracy-plot"60 Hz budget ms"
/1000d060d0

The frame mark closes Tracy's frame outside every zone above, which is what lets the viewer draw a frame-time graph and say which frame a zone belongs to.

defmethodhandle-canvas-event

Offer a scroll to whatever owns focus, then to the overlays.

The player's own view does not scroll -- the wheel is not a camera control here -- so an unconsumed wheel event is simply the end of the matter.

nil
defunnote-luvcraft-pointer-position
sessionevent
setf
luvcraft-session-pointer-xsession
canvas-pointer-event-xevent
luvcraft-session-pointer-ysession
canvas-pointer-event-yevent
luvcraft-session-pointer-dirty-psession
t
defmethodhandle-canvas-event
when
and
not
luvcraft-session-pointer-captured-psession
return-fromhandle-canvas-eventnil
let
button
canvas-pointer-event-buttonevent
cond
not
luvcraft-session-pointer-captured-psession
when
eqbutton:left
setf
luvcraft-session-pointer-captured-psession
t
let
item
player-body-hand-item
luvcraft-session-bodysession
anditem
hand-item-useitem
luvcraft-session-bodysession
sessionbutton
eqbutton:left
edit-luvcraft-blocksession:remove
eqbutton:right
edit-luvcraft-blocksession:place
eqbutton:middle
nil
defmethodhandle-canvas-event
cond
luvcraft-session-pointer-captured-psession
let
camera
luvcraft-session-camerasession
incf
*
canvas-pointer-event-delta-xevent
camera-sensitivitycamera
setf
max-1.5
min1.5
-
*
canvas-pointer-event-delta-yevent
camera-sensitivitycamera
nil
defmethodhandle-canvas-event

The window losing the pointer outright ends any capture owed back to a modal interaction: coming back to the game is a click either way.

setf
luvcraft-session-pointer-capture-suspended-psession
nil
nil
defmethodhandle-canvas-event
declare
ignorecanvasevent
let
defer-close-p

The window is going; the film's sound must not outlive it, playing on from a session nobody can see until something releases it.

alexandria:when-let
screen
luvcraft-session-video-screensession
whendefer-close-p:defer-canvas-close
defun%refresh-block-atlas
session

Republish both block atlases into session without invalidating its meshes.

If *BLOCK-ATLAS-TILE-CAPACITY* changed, replace the renderer-owned textures and discard binding caches which name their old views. Vertices carry only tile-local coordinates and mapping-scoped offsets; the next frame publishes the replacement texture's width through the frame uniform.

let*
device
luvcraft-session-devicesession
layout
make-texture-data-layout:bytes-per-row:rows-per-imageheight
resize-p
not
equal
gpu-texture-sizeold-atlas
atlas
and
notresize-p
old-atlas
normal
and
notresize-p
old-normal
atlas-viewnil
normal-viewnil
published-pnil
unwind-protect
progn
whenresize-p
setfatlas
createdevice
make-texture-descriptor:label"block world texture atlas":size:dimensions:2d:format:usage'
:copy-dst:texture-binding
normal
createdevice
make-texture-descriptor:label"block world normal atlas":size:dimensions:2d:format+block-normal-atlas-texture-format+:usage'
:copy-dst:texture-binding
atlas-view
createdevice
make-texture-view-descriptor:textureatlas
normal-view
createdevice
make-texture-view-descriptor:texturenormal
whenresize-p
let
renderer
luvcraft-session-renderersession
old-atlas-view
new-resources
listatlasnormalatlas-viewnormal-view

Prepare the complete owner inventory before publishing any of the four mutually dependent atlas handles.

setf
luvcraft-renderer-resourcesrenderer
appendnew-resources
luvcraft-renderer-resourcesrenderer

The candidate is now authoritative. A failure while retiring the predecessor must neither roll it back nor destroy the newly published textures during unwind.

setfpublished-pt
with-release-report
dolist
resource
listold-atlas-viewold-normal-viewold-atlasold-normal
when
andresize-p
notpublished-p
with-release-warnings
dolist
resource
removenil
listatlas-viewnormal-viewatlasnormal
releasing:block-atlas-candidate
release-luvcraft-renderer-resource
luvcraft-session-renderersession
resource
session
defunrefresh-block-atlas
session

Republish session's block atlases while its canvas cannot render them.

call-with-canvas-frames-held
list
luvcraft-session-canvassession
defunstart-luvcraft
&key
title"luv little block world — click, look, walk"

NIL means "as much of this display as comfortably fits"; a capture asks for the exact frame it intends to write out. A KMSDRM console can only present at a real display mode, so the environment may pin the canvas to the panel's native size.

width
let
value
uiop:getenv"LUVCRAFT_WIDTH"
andvalue
parse-integervalue
height
let
value
uiop:getenv"LUVCRAFT_HEIGHT"
andvalue
parse-integervalue
frames-per-second60
visible-pt
fullscreen-pnil
high-pixel-density-pt
mesher
make-instance'exposed-face-mesher
camera
make-instance'fly-camera
player
selected-block*stone-block*
quit-function

A hidden capture wanting animals in frame hands in a population it has already placed; the ordinary game grows its own around the player as it plays.

critters
make-instance'critter-population
checkpoint-writer
sky-clock
make-instance'sky-clock
shadow-diagnostic-pnil

The world-text banner is a proof of the Slug path, not scenery: a caller that wants one asks for it, and the ordinary game sky stays empty.

world-text-stringnil
world-text-font-pathname
cl-dejavu:font-pathname"DejaVuSans.ttf"
world-text-distance8.0
world-text-lift3.0
world-text-units-per-em0.55
video-pathnamenil
video-distance13.0
video-lift4.5
video-height5.0

Open a little CPU-meshed block world.

Click to capture the pointer, look with the mouse, walk with WASD, and jump with Space. Once captured, left click removes the block at the centre of view and right click places the selected block. Number keys select materials, middle click picks the targeted material, Shift sprints, and Escape releases the pointer. In the complete game, I opens the player inventory.

Everything also works without a pointer: the arrow keys look around, E places the selected block, X removes the targeted block, C picks its material, and M toggles pointer capture for machines whose pointing device has no buttons. Control-Q quits from anywhere, saving the world on the way out.

Pass :provider to select the Vulkan or Metal relationship without changing world, simulation, streaming, or frame orchestration. Pass :visible-p NIL to keep the SDL window hidden while still exercising the real presentation path. Pass :frames-per-second NIL for a capture-only demand clock. On KMSDRM any non-NIL value selects display-paced animation because FIFO scanout, rather than an independent host timer, owns the available frame rate. Pass :fullscreen-p T to open on the whole display, and leave :width and :height NIL to let the display choose a comfortable window. A high-pixel-density drawable is requested by default; the scene remains at logical resolution and the final application UI uses every physical pixel.

let*
canvas
make-sdl-canvas:titletitle:widthwidth:heightheight:fullscreen-pfullscreen-p:high-pixel-density-phigh-pixel-density-p

Keep the native window hidden until its first complete terrain frame has been presented. Showing it here would expose black initialization and sky-only streaming states.

:visible-pnil:presentation-api
player
camera
devicenil
resourcesnil
pipelinesnil
world-text-glyph-cachenil
sessionnil
production-systemnil
completed-pnil

FFmpeg has to be dlopened before the canvas exists. Film is now a live terminal-wall mode, so waiting until the user opens its browser would attempt the first dlopen under Cocoa's running canvas and hang. Preload the libraries here; no decoder or file is opened until Film is chosen.

unwind-protect
progn
setfdevice
request-gpu-deviceprovider
make-device-descriptor:labeltitle
context
make-canvas-contextcanvasprovider
make-canvas-configuration:devicedevice
setfproduction-system
make-single-worker-production-system:name"luvcraft chunk producer"
flet
keep
resource
pushresourceresources
resource
let*
lighting-state
render-extent
multiple-value-list
presentation-extent

Both extent classes come from one constructor, which a live window or drawable resize calls again as one cohort.

attachments
let
attachments
make-luvcraft-frame-attachmentsdevicecontextrender-extentpresentation-extent
attachments
shadow-depth-texture
keep
createdevice
make-texture-descriptor:label"block world shadow depth":size:dimensions:2d:format:depth32-float:usage'
:render-attachment:texture-binding
shadow-depth-view
keep
createdevice
make-texture-view-descriptor:textureshadow-depth-texture
shadow-depth-sampler
keep
createdevice
make-sampler-descriptor:label"block world shadow sampler":mag-filter:nearest:min-filter:nearest:mipmap-filter:nearest
shadow-comparison-sampler
keep
createdevice
make-sampler-descriptor:label"block world shadow comparison sampler":mag-filter:linear:min-filter:linear:mipmap-filter:nearest:compare:less-or-equal
normal-atlas-data
atlas-texture
keep
createdevice
make-texture-descriptor:label"block world texture atlas":size
listatlas-widthatlas-height
:dimensions:2d:formatatlas-format:usage'
:copy-dst:texture-binding
atlas-view
keep
createdevice
make-texture-view-descriptor:textureatlas-texture
normal-atlas-texture
keep
createdevice
make-texture-descriptor:label"block world normal atlas":size
listatlas-widthatlas-height
:dimensions:2d:format+block-normal-atlas-texture-format+:usage'
:copy-dst:texture-binding
normal-atlas-view
keep
createdevice
make-texture-view-descriptor:texturenormal-atlas-texture
atlas-sampler
keep
createdevice
make-sampler-descriptor:label"block world nearest sampler":mag-filter:nearest:min-filter:nearest:mipmap-filter:nearest

Presentation and the lens chain read continuous images rather than a texel grid; the five-sample gaussian below is only a nine-tap kernel because it filters linearly.

linear-sampler
keep
createdevice
make-sampler-descriptor:label"block world linear sampler":mag-filter:linear:min-filter:linear:mipmap-filter:nearest
sky-vertex-buffer
keep
createdevice
make-buffer-descriptor:label"block world sky corners":size
*4
lengthsky-vertices
:usage'
:vertex
crosshair-vertices
make-block-world-crosshair-vertices
firstrender-extent
secondrender-extent
cursor-vertices
make-luvcraft-cursor-vertices
firstrender-extent
secondrender-extent
/
firstrender-extent
2.0
/
secondrender-extent
2.0
crosshair-vertex-buffer
keep
createdevice
make-buffer-descriptor:label"block world crosshair vertices":size
*4
lengthcrosshair-vertices
:usage'
:vertex
cursor-vertex-buffer
keep
createdevice
make-buffer-descriptor:label"luvcraft software cursor vertices":size
*4
lengthcursor-vertices
:usage'
:vertex
layout
keep
createdevice
make-bind-group-layout-descriptor:label"block world layout":entries'
:binding0:type:texture
:binding1:type:sampler
:binding2:type:uniform-buffer
:binding3:type:texture
:binding4:type:sampler
:binding5:type:sampler
:binding6:type:texture
shadow-layout
keep
createdevice
make-bind-group-layout-descriptor:label"block world shadow-pass layout":entries'
:binding2:type:uniform-buffer
post-layout
keep
createdevice
make-bind-group-layout-descriptor:label"block world focus post layout":entries'
:binding0:type:texture
:binding1:type:sampler
:binding2:type:texture
:binding3:type:uniform-buffer
:binding4:type:texture
:binding5:type:texture
:binding6:type:sampler
:binding7:type:texture
:binding8:type:texture
bloom-layout
keep
createdevice
make-bind-group-layout-descriptor:label"block world lens chain layout":entries'
:binding0:type:texture
:binding1:type:sampler
:binding2:type:uniform-buffer
pipeline
let
artifact
make-live-shader-pipeline:role:block-surface:vertex-role:block-surface:label"block world pipeline":devicedevice:layoutlayout:vertex-buffers'
:array-stride56:attributes
:shader-location0:offset0:format:float32x3
:shader-location1:offset12:format:float32x3
:shader-location2:offset24:format:float32x3
:shader-location3:offset36:format:float32x3
:shader-location4:offset48:format:float32x2
:target-format+luvcraft-scene-color-format+:primitive'
:topology:triangle-list
:depth-stencil'
:format:depth32-float:depth-write-enabledt:depth-compare:less
pushartifactpipelines
artifact
shadow-pipeline
let
artifact
make-live-shader-pipeline:role:block-shadow:stage:vertex:label"block world shadow pipeline":devicedevice:layoutshadow-layout:vertex-buffers'
:array-stride56:attributes
:shader-location0:offset0:format:float32x3
:shader-location1:offset12:format:float32x3
:shader-location2:offset24:format:float32x3
:shader-location3:offset36:format:float32x3
:shader-location4:offset48:format:float32x2
:target-formatnil:primitive'
:topology:triangle-list
:depth-stencil'
:format:depth32-float:depth-write-enabledt:depth-compare:less:depth-store-op:store
pushartifactpipelines
artifact
sky-pipeline
let
artifact
make-live-shader-pipeline:role:sky:vertex-role:sky:label"block world sky pipeline":devicedevice:layoutlayout:vertex-buffers'
:array-stride12:attributes
:shader-location0:offset0:format:float32x3
:target-format+luvcraft-scene-color-format+:primitive'
:topology:triangle-list
:depth-stencil'
:format:depth32-float:depth-write-enablednil:depth-compare:always
pushartifactpipelines
artifact
crosshair-pipeline
let
artifact
make-live-shader-pipeline:role:block-crosshair:vertex-role:block-crosshair:label"block world crosshair pipeline":devicedevice:layoutlayout:vertex-buffers'
:array-stride24:attributes
:shader-location0:offset0:format:float32x3
:shader-location1:offset12:format:float32x3
:target-format+luvcraft-scene-color-format+:primitive'
:topology:triangle-list
:depth-stencil'
:format:depth32-float:depth-write-enablednil:depth-compare:always
pushartifactpipelines
artifact
cursor-pipeline
let
artifact
make-live-shader-pipeline:role:cursor:vertex-role:cursor:label"luvcraft software cursor pipeline":devicedevice:layoutlayout:vertex-buffers'
:array-stride20:attributes
:shader-location0:offset0:format:float32x3
:shader-location1:offset12:format:float32x2
:target-format:target-blend:premultiplied-alpha:primitive'
:topology:triangle-list
:depth-stencilnil
pushartifactpipelines
artifact
post-pipeline
let
artifact
make-live-shader-pipeline:role:focus-post:vertex-role:focus-post:label"block world focus post pipeline":devicedevice:layoutpost-layout:vertex-buffers'
:array-stride12:attributes
:shader-location0:offset0:format:float32x3
:target-format:primitive'
:topology:triangle-list
:depth-stencilnil
pushartifactpipelines
artifact

The four lens stages differ only in which mathematical fragment method they name; everything else about them is the same fullscreen triangle over the same layout.

bloom-bright-pipeline
let
artifact
make-luvcraft-lens-pipelinedevicebloom-layout:bloom-bright"block world bloom bright pipeline"
pushartifactpipelines
artifact
bloom-horizontal-pipeline
let
artifact
make-luvcraft-lens-pipelinedevicebloom-layout:bloom-horizontal"block world bloom horizontal pipeline"
pushartifactpipelines
artifact
bloom-vertical-pipeline
let
artifact
make-luvcraft-lens-pipelinedevicebloom-layout:bloom-vertical"block world bloom vertical pipeline"
pushartifactpipelines
artifact
sun-shaft-pipeline
let
artifact
make-luvcraft-lens-pipelinedevicebloom-layout:sun-shafts"block world sun shaft pipeline"
pushartifactpipelines
artifact
text-glyph-cache
whenworld-text-string
setfworld-text-glyph-cache
text-run
whentext-glyph-cache
setfworld-text-run
make-world-text-rundevicetext-glyph-cachecamera+luvcraft-scene-color-format+world-text-stringworld-text-font-pathname:distanceworld-text-distance:liftworld-text-lift:world-units-per-emworld-text-units-per-em
screen
whenvideo-pathname
setfvideo-screen
make-video-screendevicecameravideo-pathname+luvcraft-scene-color-format+:distancevideo-distance:liftvideo-lift:heightvideo-height
renderer-owner
setfrenderer
make-instance'luvcraft-renderer:devicedevice:contextcontext:atlas-textureatlas-texture:atlas-viewatlas-view:atlas-sampleratlas-sampler:normal-atlas-texturenormal-atlas-texture:normal-atlas-viewnormal-atlas-view:frame-attachmentsattachments:shadow-depth-textureshadow-depth-texture:shadow-depth-viewshadow-depth-view:shadow-depth-samplershadow-depth-sampler:shadow-comparison-samplershadow-comparison-sampler:layoutlayout:shadow-layoutshadow-layout:post-layoutpost-layout:bloom-layoutbloom-layout:linear-samplerlinear-sampler:bloom-bright-pipelinebloom-bright-pipeline:bloom-horizontal-pipelinebloom-horizontal-pipeline:bloom-vertical-pipelinebloom-vertical-pipeline:sun-shaft-pipelinesun-shaft-pipeline:block-pipelinepipeline:shadow-pipelineshadow-pipeline:sky-vertex-buffersky-vertex-buffer:sky-pipelinesky-pipeline:crosshair-vertex-buffercrosshair-vertex-buffer:cursor-vertex-buffercursor-vertex-buffer:crosshair-pipelinecrosshair-pipeline:cursor-pipelinecursor-pipeline:post-pipelinepost-pipeline:resourcesresources
new-session
make-instance'luvcraft-session:rendererrenderer-owner:video-screenscreen:canvascanvas:devicedevice:contextcontext:worldworld:meshermesher:checkpoint-writercheckpoint-writer:production-systemproduction-system:cameracamera:playerplayer:quit-functionquit-function:stop-controller
make-canvas-stop-controllercanvas:name"luvcraft session"
:selected-blockselected-block:inventoryinventory:lighting-statelighting-state:sky-clocksky-clock:sky-profilesky-profile:shadow-diagnostic-pshadow-diagnostic-p:residency-radiusresidency-radius:publication-limitpublication-limit:load-schedule-limitload-schedule-limit:mesh-capture-limitmesh-capture-limit:title-basetitle:software-cursor-p
string-equal
or
uiop:getenv"SDL_VIDEODRIVER"
""
"kmsdrm"
:critterscritters:world-texttext-run:world-text-glyph-cachetext-glyph-cache
write-buffersky-vertex-buffersky-vertices
write-buffercrosshair-vertex-buffercrosshair-vertices
write-buffercursor-vertex-buffercursor-vertices
write-texture
make-texture-copy:textureatlas-texture
atlas-data
make-texture-data-layout:bytes-per-row
*atlas-width4
:rows-per-imageatlas-height
listatlas-widthatlas-height
write-texture
make-texture-copy:texturenormal-atlas-texture
normal-atlas-data
make-texture-data-layout:bytes-per-row
*atlas-width4
:rows-per-imageatlas-height
listatlas-widthatlas-height
setfsessionnew-session
whenvisible-p

Preserve asynchronous residency after startup, but do not publish the window until the nearest immutable mesh is ready.

The window is mapped before that first frame rather than after it. A Wayland surface that has never been mapped receives no frame callbacks, and a FIFO present waits for one, so presenting first and showing afterwards is a wait for a frame that cannot arrive until the wait ends. Nothing can break that tie from outside either: show-canvas runs on the canvas thread, which is the very thread the present is holding. Waiting for the mesh is what keeps the window from opening on an empty world; the frame merely has to come second.

setf
ifframes-per-second
let
frame-function
lambda
native-canvastimestamp
declare
ignorenative-canvas
render-luvcraft-framesessiontimestamp
if
make-cadence-clockframe-function:frames-per-secondframes-per-second
completed-pt
session
unlesscompleted-p
whenproduction-system
ignore-errors
stop-production-systemproduction-system

Startup failed and its condition is already on its way out, so trouble releasing the half-built session is warned about rather than signalled -- signalling here would replace the error that actually explains why the game did not open.

with-release-warnings
whensession
dolist
overlay
copy-list
luvcraft-session-overlayssession
releasing:overlay
ifrenderer
progn
dolist
pipelinepipelines
dolist
resourceresources
releasing:resource
destroyresource
whenworld-text-glyph-cache
releasing:glyph-cache
luv.slug:release-slug-glyph-cacheworld-text-glyph-cache
whendevice
releasing:device
destroydevice
defmethodperform-luvcraft-stop

Release session after its stop controller granted this caller ownership.

The stop controller guarantees that this owner is beside the canvas thread. Leave that thread alive until the capture transaction has encoded and evicted its target-keyed frame state.

setf
luvcraft-session-running-psession
nil
with-release-report
let
canvas
luvcraft-session-canvassession

First stop future frames, then cross the native frame boundary. This method only runs beside the canvas thread: the controller rejects a synchronous owner or waiter on that thread.

when
when
luvcraft-session-pointer-captured-psession
releasing:pointer-capture
setf
luvcraft-session-pointer-captured-psession
nil
releasing:canvas-quiescence

A synchronous no-op after changing the clock is the frame-boundary barrier: no encoder can still be borrowing application resources.

request-canvas-framecanvas
lambda
timestamp
declare
ignoretimestamp

Stop CPU publication before releasing any render-owned destination.

releasing:production-system
stop-production-system
luvcraft-session-production-systemsession
dolist
overlay
copy-list
luvcraft-session-overlayssession
releasing:overlay

The session coordinates one renderer owner; it no longer reproduces the renderer's pipeline and resource inventories during teardown.

releasing:renderer
release-luvcraft-component
luvcraft-session-renderersession
when
luvcraft-session-video-screensession
let
screen
luvcraft-session-video-screensession
releasing:video-screen
when
setf
luvcraft-session-video-screensession
nil
when
luvcraft-session-world-textsession
releasing:world-text
release-world-text-run
luvcraft-session-world-textsession
when
luvcraft-session-world-text-glyph-cachesession
releasing:glyph-cache
luv.slug:release-slug-glyph-cache
luvcraft-session-world-text-glyph-cachesession

The window and the device are last and are never skipped: they are the two handles whose loss would leave something on the desktop that nothing can close.

releasing:device
destroy
luvcraft-session-devicesession
values
defunstop-luvcraft
session

Stop session exactly once and publish its result to every caller.

The sole owner attempts every named release step and closes the canvas and device last. Concurrent and later callers observe the same values or release-error without releasing a native handle twice.

setf
luvcraft-session-running-psession
nil
call-with-stop-controller
luvcraft-session-stop-controllersession