luv

Workshop wiki

application-capture.lisp

hal/application-capture.lisp

system luv · 40 definitions · on GitHub

GPU-native screenshots and films of live applications.

The application owns what a frame means. This file owns the mechanical capture transaction: one offscreen color target, one readback buffer, command submission, synchronous readback, PNG/video output, real-time film pacing, and cleanup. Nothing here reads a host window or presents a swapchain drawable.

in-package#:luv
defclassapplication-capture
application:initarg:application:readercapture-application
kind:initarg:kind:readercapture-kind
options:initarg:options:initformnil:readercapture-options
label:initarg:label:initform"application capture":readercapture-label

Established once before prepare-capture. A film deliberately keeps this exact relationship and extent for its whole frame sequence.

canvas:initformnil:accessorcapture-native-canvas
context:initformnil:accessorcapture-native-context
device:initformnil:accessorcapture-native-device
extent:initformnil:accessorcapture-extent
format:initformnil:accessorcapture-format

Shared resources remain visible to cleanup-capture while still alive. This lets an application evict target-keyed views or frame state before the target itself is invalidated.

target:initformnil:accessorcapture-target
readback-buffer:initformnil:accessorcapture-readback-buffer
frame-index:initform0:accessorcapture-frame-index

A deliberately untyped application-owned cell for saving pause state or another small piece of lifecycle information across the transaction.

client-state:initformnil:accessorcapture-client-state
:documentation

One screenshot or film transaction around an application-owned frame.

The object is control-plane state, allocated once per capture. TARGET and READBACK-BUFFER are dense GPU resources owned and destroyed by the shared capture implementation.

define-conditionapplication-capture-busy
error
application:initarg:application:readerapplication-capture-busy-application
active-capture:initarg:active-capture:readerapplication-capture-busy-active-capture
requested-capture:initarg:requested-capture:readerapplication-capture-busy-requested-capture
:report
lambda
conditionstream
let
active
application-capture-busy-active-capturecondition
requested
application-capture-busy-requested-capturecondition
formatstream"Cannot begin ~A (~(~A~)); ~A (~(~A~)) already owns ~S."
capture-labelrequested
capture-kindrequested
capture-labelactive
capture-kindactive
application-capture-busy-applicationcondition
define-conditionapplication-capture-shutting-down
error
application:initarg:application:readerapplication-capture-shutting-down-application
requested-capture:initarg:requested-capture:readerapplication-capture-shutting-down-requested-capture
:report
lambda
conditionstream
let
capture
application-capture-shutting-down-requested-capturecondition
formatstream"Cannot begin ~A (~(~A~)); ~S has begun shutting down."
capture-labelcapture
capture-kindcapture
application-capture-shutting-down-applicationcondition
define-conditionapplication-capture-shutdown-blocking-thread-error
error
application:initarg:application:readerapplication-capture-shutdown-blocking-application
active-capture:initarg:active-capture:readerapplication-capture-shutdown-blocking-active-capture
:report
lambda
conditionstream
formatstream"Cannot wait for ~A (~(~A~)) to finish on ~S's canvas thread."
capture-label
application-capture-shutdown-blocking-active-capturecondition
capture-kind
application-capture-shutdown-blocking-active-capturecondition
application-capture-shutdown-blocking-applicationcondition
defclassapplication-capture-coordinator
lock:initform
sb-thread:make-mutex:name"application capture reservation"
:readerapplication-capture-coordinator-lock
active-capture:initformnil:accessorapplication-capture-coordinator-active-capture
state:initform:open:accessorapplication-capture-coordinator-state
ready:initform
sb-thread:make-waitqueue:name"application capture quiescence"
:readerapplication-capture-coordinator-ready
:documentation

One shutdown-aware semantic reservation lane for an application's captures.

OPEN admits one capture. SHUTTING-DOWN is terminal: new captures fail and a non-canvas teardown owner may wait for the existing capture to leave.

defvar*application-capture-coordinators*
make-hash-table:test#'eq:weakness:key
"Weak application identities mapped to their capture reservation lanes."
defvar*application-capture-coordinators-lock*
sb-thread:make-mutex:name"application capture coordinators"
defunreserve-application-capture
capture

Reserve capture's application without holding a lock across its work.

let*
application
capture-applicationcapture
activenil
shutting-down-pnil
sb-thread:with-mutex
application-capture-coordinator-lockcoordinator
setfshutting-down-p
eq:shutting-down
application-capture-coordinator-statecoordinator
unlessshutting-down-p
setfactive
application-capture-coordinator-active-capturecoordinator
unlessactive
setf
application-capture-coordinator-active-capturecoordinator
capture

Signal after leaving the semantic mutex so a handler may safely inspect the capture identities or choose another application.

cond
shutting-down-p
error'application-capture-shutting-down:applicationapplication:requested-capturecapture
active
error'application-capture-busy:applicationapplication:active-captureactive:requested-capturecapture
coordinator
defunrelease-application-capture
coordinatorcapture

Release capture's exact reservation without disturbing a later owner.

sb-thread:with-mutex
application-capture-coordinator-lockcoordinator
when
eqcapture
application-capture-coordinator-active-capturecoordinator
setf
application-capture-coordinator-active-capturecoordinator
nil
sb-thread:condition-broadcast
application-capture-coordinator-readycoordinator
values
defuncall-with-application-capture-reserved
functioncapture
let
coordinator
unwind-protect
funcallfunction
release-application-capturecoordinatorcapture
defunapplication-capture-shutdown-p
application

Return true once application's capture admission has closed terminally.

let
sb-thread:with-mutex
application-capture-coordinator-lockcoordinator
eq:shutting-down
application-capture-coordinator-statecoordinator
defunrequest-application-capture-shutdown
application

Close application's capture admission without waiting for active work.

This is safe on a canvas thread. Return true when this call closed admission, or NIL when shutdown had already been requested.

let
began-pnil
sb-thread:with-mutex
application-capture-coordinator-lockcoordinator
case
application-capture-coordinator-statecoordinator
:open
setf
application-capture-coordinator-statecoordinator
:shutting-down
began-pt
:shutting-down
otherwise
error"Invalid application capture coordinator state ~S."
application-capture-coordinator-statecoordinator
whenbegan-p
sb-thread:condition-broadcast
application-capture-coordinator-readycoordinator
began-p
defuncall-if-application-captures-open
applicationfunction

Atomically call small publication function only while admission is open.

function runs under application's short capture gate lock and must not block or reenter the capture gate. Return true when it ran, NIL after shutdown began.

let
sb-thread:with-mutex
application-capture-coordinator-lockcoordinator
when
eq:open
application-capture-coordinator-statecoordinator
funcallfunction
t
defunproper-capture-options-p
options
loopfortail=optionsthen
cddrtail
whiletailalways
and
consptail
consp
cdrtail
defmethodinitialize-instance:after
unless
member
capture-kindcapture
'
:screenshot:film
error"Unsupported application capture kind ~S."
capture-kindcapture
unless
proper-capture-options-p
capture-optionscapture
error"Application capture options must be a proper property list: ~S."
capture-optionscapture
unless
stringp
capture-labelcapture
error'type-error:datum
capture-labelcapture
:expected-type'string
defuncapture-option
captureindicator&optionaldefault

Return indicator's value in capture's application-owned option plist.

let
missing
consnilnil
let
value
getf
capture-optionscapture
indicatormissing
if
eqvaluemissing
defaultvalue
defgenericcapture-canvas
application
:documentation

Return application's open luv canvas for a native GPU capture.

The shared implementation derives the presentation context and device through canvas-context and context-device; applications do not repeat those hooks.

defunquiesce-application-captures
application

Close application's capture gate and wait for its active capture to leave.

The caller must run beside the canvas thread while a capture is active. The active capture may still need that thread for its final frame and cache cleanup; waiting on the canvas thread would deadlock it and is rejected explicitly.

let*
sb-thread:with-mutex
application-capture-coordinator-lockcoordinator
loopforactive=
application-capture-coordinator-active-capturecoordinator
whileactivedo
whenblocking-thread-p
error'application-capture-shutdown-blocking-thread-error:applicationapplication:active-captureactive
sb-thread:condition-wait
application-capture-coordinator-readycoordinator
application-capture-coordinator-lockcoordinator
values
defgenericprepare-capture
applicationcapture
:documentation

Prepare application for capture on the calling thread.

This is the place to wait for application-specific readiness and to quiesce a normal frame loop. capture's canvas, context, device, extent, and format are already established, but its shared GPU resources do not exist yet. cleanup-capture is called even when this method exits by error.

defmethodprepare-capture
applicationt
declare
ignoreapplicationcapture
values
defgenericadvance-capture-frame
applicationcaptureframe-index
:documentation

Advance application-owned work before offscreen film frame frame-index.

This runs on the film caller after BEFORE-FRAME and before GPU encoding. A streaming application can publish completed products here without putting its world model or simulation policy into the shared capture loop.

defmethodadvance-capture-frame
applicationt
frame-index
declare
ignoreapplicationcaptureframe-index
values
defgenericencode-capture-frame
applicationcaptureencodertargetextent
:documentation

Encode application's complete visible frame into offscreen target.

This method runs on the canvas's native frame thread. encoder is open and owned by the shared implementation; extent is the fixed two-dimensional capture extent. The method must not finish or submit encoder and must not destroy target. Its primary value is optional application metadata returned by capture-application-screenshot.

defgenericcleanup-capture
applicationcapture
:documentation

Undo application-owned capture preparation on the calling thread.

When shared GPU resources were created they are still alive and visible through CAPTURE-TARGET and CAPTURE-READBACK-BUFFER during this method. Applications with target-keyed GPU caches should synchronously evict those entries on their canvas thread here. Shared resources are destroyed after this method returns. The method must not destroy the shared target or readback buffer.

defmethodcleanup-capture
applicationt
declare
ignoreapplicationcapture
values
defuncapture-color-format-p
format
memberformat'
:rgba8-unorm:rgba8-unorm-srgb:bgra8-unorm:bgra8-unorm-srgb
defunnormalize-capture-extent
extent
unless
and
typepextent'sequence
=2
lengthextent
every
lambda
typepdimension'
integer1
extent
error"Application capture requires a positive 2D extent, got ~S."extent
coerceextent'list
defunestablish-capture-relationship
capture
let*
application
capture-applicationcapture
unless
error"CAPTURE-CANVAS returned ~S, not a LUV canvas."canvas
unless
error'canvas-state-error:canvascanvas:operation:capture:reason:invalid-state:state:expected-state:open
let
unless
error"Capture canvas ~S has no configured LUV context."canvas
let
unless
typepdevice'gpu-device
error"Capture context ~S has no configured LUV device."context
unless
error

Application capture requires an RGBA8 or BGRA8 color ~ format, got ~S.

format
setf
capture-native-canvascapture
canvas
capture-native-contextcapture
context
capture-native-devicecapture
device
capture-extentcapture
extent
capture-formatcapture
format
capture
defunensure-capture-relationship-current
capture

Reject a resize or context replacement before it becomes a GPU mismatch.

let*
canvas
capture-native-canvascapture
unless
eqcontext
capture-native-contextcapture
error"The canvas context changed during ~A."
capture-labelcapture
unless
eq
capture-native-devicecapture
error"The canvas device changed during ~A."
capture-labelcapture
unless
eq
capture-formatcapture
error"The canvas format changed during ~A: ~S became ~S."
capture-labelcapture
capture-formatcapture
let
unless
equalextent
capture-extentcapture
error"The canvas extent changed during ~A: ~S became ~S."
capture-labelcapture
capture-extentcapture
extent
capture
defuncapture-resource-label
capturenoun
formatnil"~A ~A"
capture-labelcapture
noun
defunmake-capture-target
capture
create
capture-native-devicecapture
make-texture-descriptor:label
capture-resource-labelcapture"target"
:size
capture-extentcapture
:dimensions:2d:format
capture-formatcapture

Luvcraft copies its resolved presentation image into TARGET, while luft renders directly through a view. The shared readback needs COPY-SRC in either case.

:usage'
:render-attachment:copy-src:copy-dst
defunmake-capture-readback-buffer
capture
destructuring-bind
capture-extentcapture
create
capture-native-devicecapture
make-buffer-descriptor:label
capture-resource-labelcapture"readback"
:size:usage'
:copy-dst
defunrender-capture-frame
capture

Render and synchronously read one frame from an established capture.

let*
application
capture-applicationcapture
canvas
capture-native-canvascapture
device
capture-native-devicecapture
metadatanil
request-canvas-framecanvas
lambda
timestamp
declare
ignoretimestamp
let
encodernil
commandsnil
unwind-protect-releasing
progn
setfencoder
createdevice
make-command-encoder-descriptor:label
capture-resource-labelcapture"frame encoder"
setfmetadata
encode-capture-frameapplicationcaptureencoder
capture-targetcapture
capture-extentcapture
encodeencoder
make-gpu-copy-texture-to-buffer-command:source
capture-targetcapture
:destination
capture-readback-buffercapture
setfcommands
finishencoder
submitcommands
releasing:capture-frame-command-buffer
whencommands
destroycommands
releasing:capture-frame-encoder
whenencoder
destroyencoder
values
read-buffer
capture-readback-buffercapture
metadata
defuncall-with-capture-target
functioncapture

Call function with capture while owning its offscreen GPU resources.

call-with-application-capture-reserved
lambda
let
cleanup-pnil
unwind-protect-releasing
progn

cleanup-capture also runs for a partially completed preparation.

setfcleanup-pt
prepare-capture
capture-applicationcapture
capture
setf
capture-targetcapture
setf
capture-readback-buffercapture
funcallfunctioncapture

Application cleanup runs first because target-keyed views must not outlive the target. Every shared object still gets an attempt, and a body failure stays primary if any of these steps also fails.

releasing:capture-application-cleanup
whencleanup-p
cleanup-capture
capture-applicationcapture
capture
releasing:capture-readback-buffer
when
capture-readback-buffercapture
destroy
capture-readback-buffercapture
setf
capture-readback-buffercapture
nil
releasing:capture-target
when
capture-targetcapture
destroy
capture-targetcapture
setf
capture-targetcapture
nil
capture
defuncapture-application-screenshot
applicationpathname&keyoptions
label"application screenshot"

Render application offscreen and write its native-resolution PNG.

options is an application-owned property list interpreted by the capture protocol methods. Return pathname, packed pixels, width, height, color format, and the optional metadata value from encode-capture-frame.

let
capture
make-instance'application-capture:applicationapplication:kind:screenshot:optionsoptions:labellabel
call-with-capture-target
lambda
capture
multiple-value-bind
pixelsmetadata
destructuring-bind
capture-extentcapture
ensure-directories-existpathname
write-rgba-pngpathnamepixelswidthheight
capture-formatcapture
valuespathnamepixelswidthheight
capture-formatcapture
metadata
capture
defuncapture-clock-seconds
/
get-internal-real-time
floatinternal-time-units-per-second1.0d0
defuncapture-frame-wait-seconds
start-secondsframe-indexframe-ratenow-seconds

Return the wall-clock wait before frame-index's successor deadline.

-
+start-seconds
/
1+frame-index
floatframe-rate1.0d0
now-seconds
defuncall-with-capture-film-frames
writercaptureframe-countframe-rate&keybefore-frameprogress-function
clock-function#'capture-clock-seconds
sleep-function#'sleep

Write frame-count real-time-paced offscreen frames through writer.

check-typewriterfunction
check-typeframe-count
integer1
check-typeframe-rate
integer1
whenbefore-frame
check-typebefore-framefunction
whenprogress-function
check-typeprogress-functionfunction
check-typeclock-functionfunction
check-typesleep-functionfunction
let
start
funcallclock-function
application
capture-applicationcapture
dotimes
frameframe-count

A stop owner has closed admission and is waiting beside the canvas thread. Finish at most the current frame, then let cleanup release the reservation promptly. A film always emits its first frame so ffmpeg receives a valid stream even when shutdown races its preparation.

setf
capture-frame-indexcapture
frame
whenbefore-frame
funcallbefore-frameframe
advance-capture-frameapplicationcaptureframe
funcallwriter
whenprogress-function
funcallprogress-functionframeframe-count

Absolute deadlines prevent encoding variance from accumulating drift.

let
wait
capture-frame-wait-secondsstartframeframe-rate
funcallclock-function
when
funcallsleep-functionwait
values
defuncapture-application-film
applicationpathname&key
seconds8
frame-rate30
before-frameprogress-functionoptions
label"application film"

Film application offscreen into a real-time-paced H.264 MP4.

The application chooses readiness, advancement, frame meaning, and cleanup; the shared implementation owns the fixed-size target, readback, ffmpeg writer, and absolute wall-clock pacing. Return pathname and the encoded frame count.

check-typeseconds
real0
check-typeframe-rate
integer1
let*
frame-count
max1
round
*secondsframe-rate
capture
make-instance'application-capture:applicationapplication:kind:film:optionsoptions:labellabel
call-with-capture-target
lambda
capture
destructuring-bind
capture-extentcapture
call-with-video-encoder
lambda
writer
call-with-capture-film-frameswritercaptureframe-countframe-rate:before-framebefore-frame:progress-functionprogress-function
pathnamewidthheight:frame-rateframe-rate:format
capture-formatcapture
capture