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().
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.
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.
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.