luv

Workshop wiki

capture.lisp

luvcraft/capture.lisp

system luvcraft/core · 12 definitions · on GitHub

Direct luvcraft framebuffer capture for live use, smoke tests, and CI.

in-package#:luvcraft
defmethodluv:capture-canvas
luvcraft-session-canvassession
defmethodluv:prepare-capture

A still wants a useful initial neighborhood. A film must not publish a late chunk halfway through a supposedly continuous authored shot.

ecase
luv:capture-kindcapture
:screenshot
:film
wait-for-luvcraft-productssession:minimum
hash-table-count
luvcraft-session-desired-chunkssession
session
defmethodluv:encode-capture-frame
encodertargetextent
declare
ignoreextent
let*
camera
luvcraft-session-camerasession
saved-pose
camera-pose
luv:capture-optioncapture:camera-pose
pose
if
functionpcamera-pose
funcallcamera-posesession
camera-pose
metadata-function
luv:capture-optioncapture:metadata-function
metadatanil
unwind-protect
progn
whenpose
set-camera-posecamerapose
whenmetadata-function
setfmetadata
funcallmetadata-functionsession

encode-luvcraft-frame resolves its scene into target. The shared capture transaction owns the following target-to-buffer copy.

encode-luvcraft-framesessiontargetencoder:include-hud-p
luv:capture-optioncapture:include-hud-pt
:include-viewmodel-p
luv:capture-optioncapture:include-viewmodel-pt
set-camera-posecamerasaved-pose
metadata
defunrelease-luvcraft-capture-frame-state
sessiontarget

Release per-target frame state cached while encoding target.

let*
renderer
luvcraft-session-renderersession
key
canvas-frame-resource-key
luvcraft-session-contextsession
target
state
gethashkeystates
with-release-report
dolist
overlay
copy-list
luvcraft-session-overlayssession
releasing
list:capture-overlay-frame-keyoverlay
whenstate

Detach the dying target's identity before any native release can signal, so cleanup remains idempotent under a partial driver failure.

remhashkeystates
dolist
resource
remove-duplicates:test#'eq
releasing:capture-frame-state-resource
values
defmethodluv:cleanup-capture
let
target
luv:capture-targetcapture
canvas
luvcraft-session-canvassession
when
request-canvas-framecanvas
lambda
timestamp
declare
ignoretimestamp

The target is still alive here. Never leave its identity and the bind groups that name it retained in the long-lived renderer.

values
defuncapture-luvcraft-screenshot
sessionpathname&keycamera-posemetadata-function
include-hud-pt
include-viewmodel-pt

Render session once offscreen and write its native GPU frame to PNG.

This works for both live and hidden sessions. The call returns only after GPU readback and compressed PNG writing have completed; it does not inspect the host window or depend on the window being visible. camera-pose may be a pose or a function of session evaluated on the canvas thread; the session camera is restored before the call returns. metadata-function, when supplied, is called there under the capture pose and its value is returned last.

luv:capture-application-screenshotsessionpathname:label"luvcraft screenshot":options
list:camera-posecamera-pose:metadata-functionmetadata-function:include-hud-pinclude-hud-p:include-viewmodel-pinclude-viewmodel-p
defuntemporal-derivative-rgba
currentpreviousscale&optionalprevious-previous

Return a visible rgba temporal derivative and normalized summary values.

With previous-previous, compute the absolute second temporal derivative; otherwise compute the absolute first derivative. rgb channel order is irrelevant because the output is grayscale. Values returned after the image are normalized mean magnitude, maximum magnitude, and changed-pixel fraction.

unless
=
lengthcurrent
lengthprevious
error"Temporal derivative frame sizes differ: ~D and ~D."
lengthcurrent
lengthprevious
when
andprevious-previous
/=
lengthcurrent
lengthprevious-previous
error"Second temporal derivative frame sizes differ: ~D and ~D."
lengthcurrent
lengthprevious-previous
unless
zerop
mod
lengthcurrent
4
error"Temporal derivative expects tightly packed RGBA pixels."
check-typescale
real0
let
output
make-array
lengthcurrent
:element-type'
unsigned-byte8
sum0
maximum0
changed0
pixel-count
/
lengthcurrent
4
loopforoffsetfrom0below
lengthcurrent
by4formagnitude=
round
/
loopforchannelbelow3forcurrent-value=
arefcurrent
+offsetchannel
forprevious-value=
arefprevious
+offsetchannel
sum
abs
ifprevious-previous
+current-value
-
*2previous-value
arefprevious-previous
+offsetchannel
-current-valueprevious-value
3
forvisible=
min255
round
*scalemagnitude
do
incfsummagnitude
setfmaximum
maxmaximummagnitude
when
pluspmagnitude
incfchanged
setf
arefoutputoffset
visible
arefoutput
+offset1
visible
arefoutput
+offset2
visible
arefoutput
+offset3
255
valuesoutput
/sum
*pixel-count255.0
/maximum255.0
/changed
floatpixel-count
defunwrite-temporal-derivative-summary
pathnamerows

Write temporal derivative metric rows to a small CSV file.

with-open-file
streampathname:direction:output:if-exists:supersede
formatstream"frame,d1_mean,d1_max,d1_changed,d2_mean,d2_max,d2_changed~%"
dolist
rowrows
formatstream"~D,~,8F,~,8F,~,8F,"
firstrow
thirdrow
fourthrow
when
fifthrow
formatstream"~,8F"
fifthrow
write-char#\,stream
when
sixthrow
formatstream"~,8F"
sixthrow
write-char#\,stream
when
seventhrow
formatstream"~,8F"
seventhrow
terpristream
pathname
defunhidden-luvcraft-frame-pathname
directoryindex&optional
prefix"block-world"
merge-pathnames
formatnil"~A-~3,'0D.png"prefixindex
uiop:ensure-directory-pathnamedirectory
defuncapture-hidden-luvcraft-screenshot
pathname&key
title"luv hidden block world"
mesher
make-instance'exposed-face-mesher
camera
make-instance'fly-camera
critters
make-instance'critter-population
world-text-stringnil
world-text-distance8.0
world-text-lift3.0
world-text-units-per-em0.55
sky-clock
make-instance'sky-clock:pinned-day-fraction0.5

Open a hidden SDL canvas, render one block-world frame, and save it.

The sky clock arrives pinned at noon so captures stay byte-deterministic; pass an unpinned clock to photograph another time of day. A capture never runs the frame simulation, so any animals it wants in shot arrive already placed in critters rather than growing around a player.

let
sessionnil
unwind-protect
progn
setfsession
start-luvcraft:titletitle:widthwidth:heightheight:frames-per-secondnil:visible-pnil

Authored hidden captures name output pixels explicitly. Live-window screenshots retain the session's native Retina presentation extent through capture-luvcraft-screenshot.

:high-pixel-density-pnil:providerprovider:worldworld:meshermesher:cameracamera:critterscritters:world-text-stringworld-text-string:world-text-distanceworld-text-distance:world-text-liftworld-text-lift:world-text-units-per-emworld-text-units-per-em:sky-clocksky-clock:sky-profilesky-profile
whensession
defuncapture-hidden-luvcraft-text-closeup
pathname&key

Render a native-resolution close-up of the world Slug text proof.

The wide viewport and enlarged world-space text make outline, band-selection, and coverage defects visible without scaling up a smaller raster afterward. #9G0Z19

capture-hidden-luvcraft-screenshotpathname:title"luv Slug world text close-up":width1280:height360:providerprovider:camera
make-instance'fly-camera:position
make-vec38.024.0-6.0
:yaw0.0:pitch0.02
:world-text-string"hello, world":world-text-distance6.0:world-text-lift2.2:world-text-units-per-em3.4
defuncapture-hidden-luvcraft-frames
directory&key
count6
title"luv hidden block world"
yaw-step0.35
forward-step0.0
day-start
day-step0.0
difference-scale
shadow-diagnostic-pnil
pathname-prefix"block-world"
mesher
make-instance'exposed-face-mesher
camera
make-instance'fly-camera
critters
make-instance'critter-population
sky-clock
make-instance'sky-clock:pinned-day-fraction0.5

Capture count hidden block-world frames into directory.

Each frame reuses one hidden SDL canvas on provider, advances camera's yaw by yaw-step, moves forward-step world units along its initial heading, and moves the evaluated sky by day-step day fractions. day-start can replace the clock's initial time. When difference-scale is non-NIL, also write amplified first and second frame derivatives plus a CSV of unscaled normalized metrics. shadow-diagnostic-p replaces block materials with direct-shadow visibility so the derivatives are not confounded by albedo, sky colour, or fog changes. This is a consecutive-view capture, not a set of independently restarted scenes.

check-typecount
integer1
check-typeyaw-stepreal
check-typeforward-stepreal
whenday-start
check-typeday-startreal
check-typeday-stepreal
whendifference-scale
check-typedifference-scale
real0
let
directory
uiop:ensure-directory-pathnamedirectory
sessionnil
previous-pixelsnil
previous-previous-pixelsnil
derivative-rows'
outputs'
initial-x
camera-xcamera
initial-z
camera-zcamera
initial-yaw
initial-day-fraction
ensure-directories-existdirectory
unwind-protect
progn
setfsession
start-luvcraft:titletitle:widthwidth:heightheight:frames-per-secondnil:visible-pnil:high-pixel-density-pnil:providerprovider:worldworld:meshermesher:cameracamera:critterscritters:sky-clocksky-clock:sky-profilesky-profile:shadow-diagnostic-pshadow-diagnostic-p

Temporal evidence requires a fixed scene. The ordinary capture threshold of nine products is enough for a useful screenshot but allowed later desired chunks to publish in the middle of a derivative sequence.

wait-for-luvcraft-productssession:minimum
hash-table-count
luvcraft-session-desired-chunkssession
loopforindexbelowcountforpathname=
hidden-luvcraft-frame-pathnamedirectoryindexpathname-prefix
do
setf
+initial-yaw
*yaw-stepindex
camera-xcamera
+initial-x
*forward-stepindex
sininitial-yaw
camera-zcamera
+initial-z
*forward-stepindex
cosinitial-yaw
let
day-fraction
mod
+initial-day-fraction
*day-stepindex
1.0
if
sky-clock-pinned-day-fractionsky-clock
setf
sky-clock-pinned-day-fractionsky-clock
day-fraction
setf
sky-clock-day-fractionsky-clock
day-fraction
multiple-value-bind
writtenpixelsframe-widthframe-heightformat
pushwrittenoutputs
formatt"~&capture ~A: frame ~D/~D~%"pathname-prefix
1+index
count
finish-output
when
anddifference-scaleprevious-pixels
multiple-value-bind
differencemeanmaximumchanged
temporal-derivative-rgbapixelsprevious-pixelsdifference-scale
let
difference-pathname
hidden-luvcraft-frame-pathnamedirectoryindex
formatnil"~A-d1"pathname-prefix
write-rgba-pngdifference-pathnamedifferenceframe-widthframe-heightformat
pushdifference-pathnameoutputs
let
row
listindexmeanmaximumchangednilnilnil
whenprevious-previous-pixels
multiple-value-bind
second-differencesecond-meansecond-maximumsecond-changed
temporal-derivative-rgbapixelsprevious-pixelsdifference-scaleprevious-previous-pixels
let
difference-pathname
hidden-luvcraft-frame-pathnamedirectoryindex
formatnil"~A-d2"pathname-prefix
write-rgba-pngdifference-pathnamesecond-differenceframe-widthframe-heightformat
pushdifference-pathnameoutputs
setf
fifthrow
second-mean
sixthrow
second-maximum
seventhrow
second-changed
pushrowderivative-rows
setfprevious-previous-pixelsprevious-pixelsprevious-pixelspixels
whendifference-scale
let
summary
merge-pathnames
formatnil"~A-temporal.csv"pathname-prefix
directory
write-temporal-derivative-summarysummary
nreversederivative-rows
pushsummaryoutputs
nreverseoutputs
whensession