Using the Capture Service

The Capture Service records a bounded sequence of screen frames into memory and lets you fetch them afterwards with per-frame metadata. Where overwatch.screenshot answers “what is on screen now”, this answers “what happened over the last few dozen frames” — an instant replay you can step through after the fact.

How it works

Recording is a keep-last-N ring. capture.start arms it and frames are written continuously, overwriting the oldest once the ring is full. capture.stop freezes it, so what you fetch is the N frames leading up to the moment you stopped — you do not have to predict when the interesting thing will happen, only to stop soon after it does.

The flow mirrors the profiling trace service: configure → record → fetch.

capture.start 8 256 2      Arm the ring: 8 frames, downsampled to 256 px, every 2nd rendered frame
capture.stop               Freeze it
capture.manifest           What is held, frame by frame
capture.frame 0            Fetch one frame as base64 RGBA
capture.fetchall           Fetch every held frame in one response

The service is enabled by default. Set ServiceCaptureEnabled = 0 in the configuration file to disable it, and CaptureMaxBytes to change the RAM ceiling for the ring, which defaults to 128 MB. See Configuring Kanzi Monitor.

CaptureMaxBytes must be greater than 0. There is no unlimited setting: the ceiling is what keeps a long capture at full surface resolution from exhausting memory, so a value of 0 or a negative one is rejected and the default is used instead, with a warning naming the value in the log. To raise the ceiling, raise the number — values above 2 GiB are accepted. A value the configuration parser cannot read as a number, which includes one too large to be one, also falls back to the default.

Commands

capture.start <frames> [maxDim] [everyNth] [outputIndex]
                           Arm frame-sequence recording (keep-last-N ring).
capture.stop               Freeze the ring so the last N frames can be fetched.
capture.status             Capture state, frames held and capacity, and framerate-impact figures.
capture.manifest           Per-frame metadata for the held sequence, oldest to newest.
capture.frame <index> [maxDim]
                           Fetch one frame as base64 RGBA.
capture.fetchall [maxDim]  Fetch all held frames in one response, size-guarded.

capture.start reports what it actually armed, which is not always what you asked for:

capture.start 8 256 2
→ {"state":"recording","requestedFrames":8,"capacity":8,"clamped":false,"everyNth":2,
   "spansRenderedFrames":16,"width":256,"height":192,"maxDim":256,"perFrameBytes":196608,
   "ringBytes":1572864,"byteBudget":134217728,"overflowPolicy":"overwrite-oldest"}

capacity is the frame count the ring really holds and clamped says whether the byte budget cut your request down. spansRenderedFrames is capacity × everyNth — how many rendered frames the ring covers end to end.

Watching the cost

capture.status doubles as the instrumentation for what recording costs:

{"state":"recording","framesHeld":8,"capacity":8,"totalCaptured":33,"everyNth":2,
 "renderedFrames":66,"width":256,"height":192,"perFrameBytes":196608,"peakBytes":1572864,
 "readbackMaxMs":30.5835,"readbackAvgMs":21.694}

readbackAvgMs and readbackMaxMs are the honest numbers to look at before trusting a capture: framebuffer readback is synchronous and expensive. On the example application at 256 px it costs roughly 20–30 ms per captured frame, which is more than a frame’s budget at 60 fps. A capture therefore changes the framerate of what it is recording, and the more it records the more it changes it.

Two levers reduce that:

  • maxDimdoes not make the readback cheaper. captureFramebuffer is always called at the graphics output’s full width and height; maxDim changes only what is stored, and the width/height in capture.status are that stored geometry rather than what was read back. What it does cut is perFrameBytes, so the ring holds more frames within the byte budget — and with maxDim set the downscale runs at capture time, not at fetch, so it adds hot-path work rather than removing it.

  • everyNth — capture one rendered frame in every N. This is the only lever the current implementation has against the byte budget, and it reduces framerate impact rather than adding to it: the skip is decided before the readback, so a skipped frame costs a counter increment. Recording every 2nd frame at the same capacity covers twice the wall-clock span at half the cost.

peakBytes is the high-water mark of the ring, to compare against CaptureMaxBytes.

What this costs in practice

Measured on the example application at 640x480, Windows, Release, vsync at 60 fps, with SuspendWhenIdle disabled and a continuous 30-frame capture running (KZMON-656):

Frame rate (averaged)

Frame interval (one sample)

No capture

59.37 fps

15.18 ms

During continuous capture

59.91 fps

15.63 ms

Read the frame-rate column, not the interval column. fps comes from the engine’s frame-time queue and is averaged over a window; frameTimeMs is MainLoopScheduler::getLastFrameDuration(), a single sample of the gap between two frame starts. The two are not comparable and in this sample they disagree — 15.18 ms would be 65.9 fps, not 59.37 — which is what one noisy frame looks like next to an average. The interval column is reported here because overwatch.perf reports it, not because it supports the conclusion.

No sustained frame rate drop, on the averaged figures, and the difference between them is within noise. The readback still cost 6.2 to 6.4 ms per captured frame on average and 8.3 ms at worst, and with continuous capture every rendered frame pays it — the ring holding 30 of a 746-frame run is what it kept, not what it captured.

Those two facts sit together because of vsync, not because the readback is cheap. At 60 fps the budget per frame is 16.7 ms. This application’s own render work leaves enough of that unused that adding 6.2 ms still lands inside the budget, so the frame is finished before the swap deadline and the rate does not move. Spend more than the slack and frames start missing the deadline immediately — the effect is a cliff, not a gradient.

So the useful question is not “how expensive is the readback” but “how much slack does this application have”, and the answer is specific to the application, the surface size and the hardware. An application already near its budget has nowhere to put 6 ms and will drop frames steadily rather than occasionally.

The figures above are one application on one machine in a Release build — the numbers in the capture.status example earlier in this page came from a different configuration and are several times larger. Read your own readbackAvgMs rather than either set — and note that it brackets the readback only. The t0/t1 pair closes before the RGBA copy and the downscale, so with maxDim set the capture-time downscale is additional cost that appears in no reported figure.

The reason that example is larger at a smaller maxDim is the bullet above: the readback is always at full output resolution. “20-30 ms at 256 px” describes reading back a much bigger framebuffer that was merely stored at 256x192 — so the two figures are not in contradiction, and neither is a function of maxDim.

A hitch is visible in the data you already have: interFrameDeltaNs in capture.manifest shows it as roughly twice the vsync interval.

Note

The allocation per captured frame is not what this costs. The hot path does heap-allocate one image per frame rather than reading back into the pre-sized ring slot, and that was measured inside the same 6 to 8 ms: an allocation is nanoseconds against a synchronous readback. Making the ring slots pre-allocated would not move the number that matters. If the frame rate cost ever needs to come down, the target is the readback itself — asynchronous or fenced transfer, or buffer-object double buffering — not the allocator.

Reading the manifest

capture.manifest describes the held sequence without transferring any pixels, which is how you decide what to fetch:

{"framesHeld":8,"capacity":8,"frames":[
  {"index":0,"frameNumber":27,"sourceFrame":54,"timestampNs":300382986123100,
   "interFrameDeltaNs":100347100,"readbackNs":19066000,"width":256,"height":192,
   "format":"rgba","byteSize":196608}, … ]}

index is the fetch index, 0 being the oldest frame held. frameNumber counts captured frames and sourceFrame counts rendered ones, so the two differ by everyNth. interFrameDeltaNs is the wall-clock gap to the previous captured frame — uneven gaps are the readback cost showing up in the application’s own frame pacing.

Fetching frames

capture.frame <index> returns one frame as base64 RGBA, in the same shape as overwatch.screenshot. capture.fetchall returns every held frame in one response and is size-guarded, so a large ring at a large maxDim is refused rather than truncated silently.

Like every image command, the pixel data is bottom-up relative to screen orientation — flip it vertically before display. See image orientation in Using the Overwatch service.

Current limits

The service is an MVP, and the boundaries are deliberate rather than accidental:

  • Frames live in RAM only. There is no streaming and no write-to-disk; the ring is bounded by CaptureMaxBytes and lost when the application exits.

  • Screen only. The source is the framebuffer of one graphics output, selected with outputIndex. Individual render passes are not capturable — use overwatch.renderpasspreview for a single composition target.

  • Bounded by frame count, not by time. You ask for N frames, not N seconds; everyNth is how you trade frame density for span.

  • The capture perturbs what it records, as described above, and measurably: no sustained frame rate drop on an application with headroom, but occasional dropped frames on an application with slack to spare, steady drops on one without (see What this costs in practice). Treat captured timings as indicative, and use the profiling trace service when you need timing that is not disturbed by the measurement.

In the Web UI

The Capture tab in the Monitor Web UI drives the same commands: arm a recording, stop it, then step or play back the held frames and download them. See Using the Monitor Web UI.