Skip to main content

Capture Pipeline

Kanvas capture is not a single hook. It is a coordinated pipeline that has to identify the active frame, identify the correct Skia canvas, mirror supported drawing state, and only then hand the completed frame to takeover/presentation.

The central implementation is ComposeCaptureHooks.java.

Arming conditions​

Takeover is not enabled immediately when the agent starts.

Kanvas waits until both of these are ready:

canvas transformer ready
+
frame transformer ready
│
▼
TAKEOVER_ARMED

That is enforced by armTakeoverIfReady().

Why both hooks must be ready

A frame boundary without canvas interception would identify frames but not capture their drawing. Canvas interception without a reliable frame boundary would capture calls without knowing which calls belong to the final presented frame.

Kanvas only arms takeover after it has both sides of that contract.

Per-thread frame state​

Capture state is stored in thread-local fields:

FRAME_CAPTURE_ACTIVE
FRAME_CANVAS
CANVAS_CALL_ALLOWED
MIRROR_ORIGINAL_CANVAS

This prevents one rendering thread's state from leaking into another thread.

The frame lifecycle is:

frameStart(...)
│
├─ mark frame active
├─ clear previous canvas routing state
├─ notify LiveGpuTakeover
└─ optionally write FRAME_BEGIN

draw calls...
│
├─ identify canvas
├─ validate whether it belongs to the frame
└─ mirror supported operations

frameEnd()
│
├─ clear routing state
├─ present through LiveGpuTakeover
├─ optionally write FRAME_END
└─ release thread-local frame state

Canvas identity​

A Java Canvas wrapper is not always a reliable identity for the underlying native SkCanvas.

On some Skiko paths, multiple JVM wrappers can refer to the same native canvas during one frame.

Kanvas therefore tries to identify a canvas using a native pointer field or getter:

_ptr
ptr
nativePtr
getPtr()
get_ptr()

and falls back to Java object identity only when no native pointer is available.

Why this exists

Without stable native identity, two wrappers around the same native SkCanvas could be mistaken for two different canvases. The second wrapper would then be rejected as off-frame work even though it belongs to the real frame.

Source: canvasIdentity.

Recording mode vs direct-draw mode​

Kanvas supports two broad capture shapes.

Direct-draw mode​

One primary canvas is selected for the frame and subsequent calls must resolve to the same underlying canvas identity.

Modern recording mode​

Modern Skiko can use nested PictureRecorder canvases while building the final picture.

In this path, MIRROR_ORIGINAL_CANVAS allows all participating canvases to be mirrored while the frame is active.

This distinction is documented directly in the source because treating all recording canvases as "offscreen" would drop legitimate Compose content.

Coordinate sizing​

frameStartLayer derives physical frame size from:

layer width × contentScale
layer height × contentScale

Invalid/non-positive scale values fall back to 1.0.

This is important because the rendering surface is pixel-sized while Compose/Skiko may expose logical dimensions separately from content scale.

Supported state mirroring​

Capture hooks currently include operations for:

  • clear / clear rect
  • rectangles and rounded rectangles
  • save / restore / restoreToCount
  • saveLayer
  • translate / scale / rotate / skew / matrix concatenation
  • clipping
  • paths
  • text/paragraph-related hooks
  • shaders and filters
  • images/textures
  • paint/blend state

The agent routes these into LiveGpuTakeover and can simultaneously emit a capture log.

Strict mode changes failure behavior

Some unsupported paint/filter/clip combinations that may be tolerated during compatibility testing become explicit failures in strict takeover mode.

That is intentional: strict mode is for exposing unsupported renderer semantics, not hiding them.

Source​