Screenshots and recordings of the app's rendered output.
Record the window framebuffer as a PNG sequence, GIF, or WebM. Recorded
frames use the window dimensions. Export a Draw.RenderTexture for a still
image at another size.
Every path here is relative to the output directory set with
App.default.with_output_dir, and one that would escape it -- absolute,
containing .., or passing through a symbolic link -- is refused rather
than rewritten. Captures are the app's own output, so they need no declared
permission; being confined to the output directory is what makes that
safe.
start! and stop! control recording. screenshot! writes a presented
frame, and screenshot_texture! writes an offscreen render texture.
Recording state is sampled into input.capture each cycle.
pixel_at! and read_region! return pixels from the last presented frame
or a render texture without writing a file. Both are refused in render!.
Legal in init!, update!, and tasks; refused in render!.
This is the cheap read. A point needs no allocation and no delivery slot,
which a one-pixel read_region! would still take for the same four
bytes, so a colour picker should ask for a point.
picked = match Capture.pixel_at!(Screen, { x: 40, y: 24 }) {
Ok(color) => color
Err(_) => Color.black
}
Reading Screen costs one full framebuffer readback per frame for as
long as the app keeps reading: the host snapshots the frame it presents
so that a later update! has defined pixels to look at, and stops
snapshotting once a frame goes by with no read. Nothing is snapshotted
before the first read, so the first Screen read of a run -- and every
Screen read from init!, which runs before any frame -- is
Unavailable, and the next cycle's read is not.
Reading Target costs one readback of the whole target per call, however
small the point, because that is what the graphics API will give. Read a
region once rather than a point many times.
A headless run has no pixels of any kind and returns Unavailable, so an
app that reads pixels has to say what it does without them before it can
run under --host-headless.
Read a rectangle of a source as packed RGBA8 bytes.
Legal in init!, update!, and tasks; refused in render!.
The bytes are row-major and top-down: four bytes per pixel in red,
green, blue, alpha order, width pixels per row, the source's topmost
requested row first. A Screen region is always opaque, because reading
the framebuffer forces alpha; a Target region keeps the alpha the app
drew.
The list is handed over rather than copied, so a large region costs no
second buffer -- and, like every other handed-over byte list, it occupies
one of a bounded number of delivery slots until the app drops it. A read
with no slot free is Busy.
The bound is Capture.max_readback_bytes: a region above it is
RegionOutOfBounds rather than something a later frame could take. The
per-call and per-frame costs are the ones pixel_at! describes; this is
not a per-frame operation on a whole window.
Most RGBA bytes one read_region! may deliver: 128 mebibytes, which is
8192 by 4096 pixels.
screenshot_texture! draws on the same budget, for the same reason: each
holds one whole RGBA image in host memory while it works, so the two
cannot both be given the whole of it.
Png writes a numbered still per captured frame. Gif and WebM each
write a single animated file, encoded incrementally as frames arrive --
so memory stays bounded by one frame and the length of a recording is
limited only by max_frames and by disk space.
CaptureFormat in the signature is the module-private nominal this
aliases; Capture.Format is the name to write.
How far each captured frame is downscaled from the framebuffer.
Scaling happens after rendering, so it shrinks the output file without
changing the window size or what the app draws. A ratio that would round
an axis to zero is clamped to one pixel.
Whether simulation time follows the wall clock or advances in exact steps.
Reading back the framebuffer stalls the GPU, so a RealTime recording
bakes that stutter into the output and differs between runs. FixedStep
reports 1/fps as the frame delta regardless of how long the frame
actually took, which is smooth and reproducible.
How hard the encoder works to choose colours for each frame.
This setting affects Gif and is ignored by Png and WebM.
Best searches the full colour depth. Balanced is the default and
trades at most 16/255 channel error for faster encoding on flat-colour
frames. Fast uses a coarser palette and may show visible banding.
PathInvalid, PathEscapesOutputDir, AlreadyRecording, and
BudgetExceeded reject a start request before anything is written. The
remaining reasons may stop an active recording.
PathInvalid is a path the sandbox cannot even resolve: empty, absolute,
containing .., or holding a NUL byte. PathEscapesOutputDir is the
resolvable path that lands outside the output directory. Both are the
app's own string to fix.
Busy is the host at its limit across every capture request at once, so
nothing was captured or written. It is about right now: the same
screenshot offered on a later frame may well be taken, which is what
separates it from Unavailable. AlreadyPending is narrower still --
this app's own previous screenshot is still waiting for its frame.
Unavailable is the host having no capture facility to use at all --
the app is shutting down and the wait was cancelled before the frame
ended. It is not what a call from the wrong callback gets: that is a
programmer error and stops the app. See screenshot!.
TargetUnavailable is a render target that no longer resolves to a host
resource -- a released one, or the Draw.RenderTexture.stub a pure test
holds. BudgetExceeded is an image too large for the host to hold at all,
so retrying will not help; Busy is one that would fit if other exports
were not still running, so a later frame may take it. Unavailable is the app
shutting down before the write started.
Screen is the last frame the host presented, which is the frame on
screen while update! runs, at the framebuffer's own size. Target is
an offscreen Draw.RenderTexture, holding whatever the last completed
render! drew into it, at the target's own size.
RegionOutOfBounds is a point or rectangle that is not entirely inside
the source, or a rectangle larger than one read may deliver; neither
becomes possible on a later frame. TargetUnavailable is a render target
that no longer resolves to a host resource -- a released one, or the
Draw.RenderTexture.stub a pure test holds. Busy is the readback budget
being committed elsewhere -- to still exports that are running, or to the
delivery slots that pass byte lists to this app -- or, for a render
target bigger than max_readback_bytes on its own, committed for good.
ReadbackFailed is the graphics driver declining to return the pixels. Unavailable is there being nothing to read at all -- a headless
run, or a Screen read before the host has a presented frame to read.
The framebuffer is read back at the end of the frame that asked -- after
the draw batch is flushed and before the buffers are swapped, so the
pixels are the ones just drawn -- and the PNG is encoded and written off
the frame thread. This call waits for that write, so it parks the task
until the file exists and returns the write's own outcome.
Legal only in a task, where it parks the task; refused in init!,
update!, and render!. Every other waiting effect also works in
init!, where it blocks; a screenshot cannot, because what it waits for
is the end of a frame and init! runs before the frame loop has drawn
one. Spawn a task from update! instead -- on the first cycle if the
shot is meant to be of the first frame:
if input.time.cycle_count == 0 {
Task.spawn!(input, || Shot(io.capture().screenshot!("frame0.png")))
}
A headless run has no framebuffer at all and returns Ok({}) without
writing, so a screenshotting app still runs under --host-headless.
Only one screenshot can be pending: asking for a second one while the
first is still waiting for its frame returns AlreadyPending.
Write one PNG of what a render target holds, at the target's own size.
This is how an app exports an image larger than its window: draw the
composition into a Draw.RenderTexture of the size the output needs and
export the target rather than the frame. Transparency survives, unlike a
screenshot!, whose framebuffer readback is always opaque.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!. Nothing here waits on the
frame loop, which is why init! is allowed where a screenshot! cannot
be -- but drawing into a target is only possible during render!, so a
target no render! has drawn into holds undefined pixels.
The pixels are the ones the last completed render! left in the target,
so an app that draws its composition every frame exports what it last
showed. The path is resolved under the output directory exactly as
screenshot! resolves one.
A headless run has no pixels to read and returns Ok({}) without writing,
so an exporting app still runs under --host-headless.
Legal in init!, update!, and tasks; refused in render!.
Frames accumulate until the recording hits its frame cap, Capture.Writer.stop!
is called, or the app exits -- all three finalize the file.
A rejected start appears as Failed in input.capture on the next cycle;
the call itself reports nothing, so the recording's outcome is observed
the same way whichever phase started it.