API Reference · Diagnostics

Diagnostics.FrameCapture

Namespace: Ava3D.Diagnostics

public static class Diagnostics.FrameCapture

Writes one rendered frame to a PNG — the static, process-wide way, kept for one release.

This exists because "the diagnostics say OpenGL" and "the pixels are right" are different claims, and only the second one matters. Capturing the render target itself is better evidence than a window grab: it is exactly what gets composited, at full resolution, with no window server involved — and it works headless, on a build server, and in a browser tab.

Obsolete.Ava3DView.CaptureAsync does the same for one view, returns the pixels rather than a file, queues rather than replaces, and never resizes the view for a scaled capture; and Ava3DView.ComposeAsync takes the view together with the Avalonia content over it. This facade is implemented over that path now: an arm is taken by the first view to render a frame after it — the most recently rendered view, in a process with several — and the PNG is encoded and written on the thread pool, after which FrameCapture.Captured is raised there. What it captures is the picture as shown, bloom and vignette included, which it did not use to be.

Properties

MemberDescription
static int Frame { get; set; }

Which frame to capture, counted per view from its first. Not the first: early frames are missing textures that are still streaming, and a screenshot of a half-loaded scene is evidence of nothing.

static string Path { get; set; }

Where to write. Null disables capture entirely, which is the default, and so does an empty string.

Empty counts because this is almost always an environment variable, and an environment variable that has been switched off is as likely to be present and empty as it is to be absent — a process started with the switch explicitly cleared is exactly that. It is not a path anybody can write to, so the only thing treating it as one buys is a PNG encoded and thrown away on every frame.

static Nullable<PixelSize> RenderSize { get; set; }

A fixed pixel size for the capture, for a reproducible run. Overrides FrameCapture.Scale — see CaptureRequest.Size, which this becomes. The view keeps its own size; only the capture is rendered at this one. Null for the view's size. Applies to all views in this process.

static double Scale { get; set; }

Renders the capture at this multiple of the size the control is shown at, so a capture can be larger than the window. Supersampling rather than enlargement — see CaptureRequest.Scale, which this becomes. The view itself is not resized for it any more.

Events

MemberDescription
event Action<string> Captured

Raised after a frame is written, with its path, so a harness can shut down at the right moment. Raised on a thread pool thread — the one that wrote the file — never on the render thread or the UI thread.

Methods

MemberDescription
static void Next(string path)

Points this at the next file and arms it again, so a second frame is written after the first.

One capture per process is the right default — a screenshot is one picture, and a switch that goes on writing until something stops it is a switch that fills a disk when somebody forgets it. But a sequence is the same evidence extended: a fault that only shows while something is moving cannot be argued about from a single frame, and a film recorded a frame at a time is how this demo is turned into a video without a screen recorder, a compositor or anybody's permission to capture a display.

It arms rather than records: the next frame a view renders is written to path, and nothing after that, until this is called again. So the caller owns the clock — advance the scene, arm, wait for FrameCapture.Captured, repeat — and the sequence is exactly as regular as the caller is, rather than as regular as the machine happened to be. That is the whole reason it is one frame at a time and not a "record until I say stop": the second kind samples whatever the compositor felt like doing, and a dropped frame in it is invisible.

Arming again before the last frame has landed replaces it: the earlier path is never written and nobody is told. That is the fault Ava3DView.CaptureAsync fixes, with a queue.

FrameCapture.Frame is left alone — set it to zero for a sequence, or the first several arms are spent waiting for the frame counter to reach its default.

static void Next(string path, Camera camera)

Arms a capture of this exact camera pose. Frames already in flight at an older pose are skipped. Read the camera on its owning UI thread after updating it. This makes deterministic camera replays independent of compositor timing; it does not freeze or synchronise scene edits.

See also