Skip to content

Docs: async RequestReadPixels API (SKImage/SKSurface/GRContext, SKImageReadPixelsResult, SKImageRescaleGamma/Mode) #184

Description

@mattleibow

Docs needed: async RequestReadPixels API

The new async rescale-and-read-pixels API was added in mono/SkiaSharp#4545 (managed) + mono/skia#295 (native). Inline XML doc comments were intentionally not added to the source (the build ignores them), so all documentation needs to live here as mdoc XML.

This issue lists every new public API and gives writers ready-to-use content (summaries, params, returns, remarks).


Shared concepts (reuse across the members' <remarks>)

  • What it does. Asynchronously reads a (optionally rescaled) rectangle of pixels back from an SKImage or SKSurface. This is the primary way to get pixels off a GPU surface — notably for GPU backends where synchronous ReadPixels is unavailable.
  • Synchronous vs asynchronous. On a raster (CPU) image/surface the callback is invoked synchronously, before the method returns. On a GPU (Ganesh) image/surface the read is deferred: the callback only fires after the work is submitted (GRContext.Submit) and GRContext.CheckAsyncWorkCompletion is pumped.
  • Callback lifetime. The SKImageReadPixelsResult handed to the callback — and the pixel memory it points at — are valid only for the duration of the callback. The result is disposed automatically when the callback returns; every member then throws ObjectDisposedException. To keep the pixels, copy them out with ToImage, ToBitmap, ToArray, or CopyPlaneTo (do not stash the ReadOnlySpan<byte> from GetPlaneData).
  • Failure. On failure (for example a srcRect not contained by the source, or an unsupported configuration) the callback is invoked with a null result.
  • No state argument. There is no context/state parameter — capture any state you need in the callback closure.
  • Rescaling. The source srcRect is rescaled to the dimensions of info and converted to its color type/alpha/color space. rescaleGamma/rescaleMode control the rescale and are no-ops when srcRect is the same size as info (no scaling occurs).
  • Planes. A standard (interleaved RGBA) read has PlaneCount == 1. The multi-plane accessors are forward-looking for future YUV reads (which would report 3 or 4 planes).

enum SKImageRescaleGamma

Controls the gamma space in which rescaling happens (only relevant when actually scaling).

  • Src — rescale in the source's stored (usually non-linear / sRGB) gamma; no gamma conversion. Cheapest.
  • Linear — convert to linear light before rescaling, then convert back. More physically-correct downscaling (avoids dark halos) at the cost of two extra gamma conversions.

enum SKImageRescaleMode

The sampling technique used when rescaling (irrelevant at 1:1).

  • Nearest — nearest-neighbour; no filtering. Cheapest; at 1:1 an exact copy.
  • Linear — a single bilinear step.
  • RepeatedLinear — repeated bilinear halving passes; good quality for large downscales.
  • RepeatedCubic — repeated bicubic passes; highest quality, most expensive.

SKImage.RequestReadPixels (2 overloads) and SKSurface.RequestReadPixels (2 overloads)

public void RequestReadPixels (SKImageInfo info, SKRectI srcRect, Action<SKImageReadPixelsResult> callback);
public void RequestReadPixels (SKImageInfo info, SKRectI srcRect, SKImageRescaleGamma rescaleGamma, SKImageRescaleMode rescaleMode, Action<SKImageReadPixelsResult> callback);
  • summary — "Asynchronously reads (and optionally rescales) a rectangle of pixels from this image/surface, delivering the result to callback."
  • param info — "Describes the destination pixels: the dimensions the source is rescaled to, and the color type, alpha type and color space it is converted to."
  • param srcRect — "The sub-rectangle of the source to read. Reading fails (callback receives null) if it is not contained within the source bounds."
  • param rescaleGamma — "The gamma space to rescale in. See SKImageRescaleGamma." (4-arg overload only)
  • param rescaleMode — "The rescaling technique. See SKImageRescaleMode." (4-arg overload only)
  • param callback — "Receives the pixel data, or null on failure. Only valid for the duration of the call — see remarks. Throws ArgumentNullException if null."
  • remarks — Pull in the Shared concepts above (sync-vs-async, callback lifetime, failure→null, no-state, rescaling). For the 3-arg overload, note the defaults: SKImageRescaleGamma.Src + SKImageRescaleMode.Nearest (the least-transform choice; this matches what Skia itself uses when no rescale is required).

GRContext.CheckAsyncWorkCompletion

public void CheckAsyncWorkCompletion ();
  • summary — "Checks whether outstanding asynchronous work on this context has completed and invokes any ready callbacks."
  • remarks — "Used to drive deferred asynchronous reads (see SKImage.RequestReadPixels / SKSurface.RequestReadPixels) to completion on GPU contexts. A Submit must also occur to guarantee the work is sent to the GPU; typically callers Submit then call this in a loop until the read's callback has fired. Has no effect on backends without outstanding async work."

SKImageReadPixelsResult (new sealed class, IDisposable)

Type summary — "The pixel data delivered to the callback of SKImage.RequestReadPixels / SKSurface.RequestReadPixels."
Type remarks — "A non-owning view that is valid only for the duration of the callback it is delivered to; it is disposed automatically when the callback returns, after which every member throws ObjectDisposedException. Use ToImage, ToBitmap, ToArray or CopyPlaneTo to obtain a copy that outlives the callback."

Member Summary Notes for writers
int PlaneCount { get; } The number of planes in the result. 1 for a standard RGBA read.
int GetPlaneRowBytes (int planeIndex) The stride (bytes per row) of the given plane. May exceed width×bytesPerPixel due to row padding. Throws ArgumentOutOfRangeException for an invalid index.
ReadOnlySpan<byte> GetPlaneData (int planeIndex) A raw, zero-copy view over the plane's bytes. Valid only during the callback; may include row padding — use GetPlaneRowBytes as the stride. For a stable/packed copy use CopyPlaneTo/ToArray. Throws ArgumentOutOfRangeException for an invalid index.
void CopyPlaneTo (int planeIndex, Span<byte> destination) Copies the plane into destination as tightly-packed pixels (row padding stripped). Throws ArgumentException if destination is smaller than the packed size; ArgumentOutOfRangeException for an invalid index.
byte[] ToArray (int planeIndex = 0) Returns a new, tightly-packed copy of the plane that outlives the callback.
SKImage ToImage () Materializes the result into a new SKImage that outlives the callback. Only valid for a single-plane (interleaved) result; throws InvalidOperationException otherwise.
SKBitmap ToBitmap () Materializes the result into a new SKBitmap that outlives the callback. Same single-plane constraint as ToImage.
void Dispose () Invalidates this view. Called automatically when the callback returns; safe to call again (idempotent).

Suggested example (for the SKSurface.RequestReadPixels / SKImageReadPixelsResult page)

// GPU surface -> owned SKImage on the CPU
SKImage snapshot = null;
surface.RequestReadPixels(info, new SKRectI(0, 0, info.Width, info.Height), result =>
{
    if (result != null)
        snapshot = result.ToImage(); // owned copy that survives past the callback
});

// Drive the deferred GPU read to completion.
grContext.Submit(synchronous: true);
grContext.CheckAsyncWorkCompletion();
// `snapshot` is now populated (on a raster surface the callback would already have run synchronously).

Ping me (or see PR #4545) if any signature or behavior needs clarifying.


Graphite backend consumers (added in mono/SkiaSharp#3968 + mono/skia#236)

The Graphite backend exposes the same async read-pixels API on its GPU context, reusing the neutral SKImageReadPixelsResult result type and the SKImageRescaleGamma / SKImageRescaleMode enums documented above (there are no SKGraphite*-prefixed result/enum types — the earlier SKGraphiteAsyncReadResult / SKGraphiteRescaleGamma / SKGraphiteRescaleMode were removed in favour of the neutral ones). Writers should document these members consistently with the SKImage / SKSurface versions and cross-link them.

Why it matters for Graphite specifically: Graphite-backed surfaces do not support the synchronous SKSurface.ReadPixels in shipping builds (Skia gates it on GPU_TEST_UTILS), so this async path is the only way to read pixels back from a Graphite surface.

SKGraphiteContext.RequestReadPixels (2 overloads)

public void RequestReadPixels (SKSurface surface, SKImageInfo dstInfo, SKRectI srcRect, Action<SKImageReadPixelsResult> callback);
public void RequestReadPixels (SKSurface surface, SKImageInfo dstInfo, SKRectI srcRect, SKImageRescaleGamma rescaleGamma, SKImageRescaleMode rescaleMode, Action<SKImageReadPixelsResult> callback);
  • summary — "Asynchronously reads (and optionally rescales) a rectangle of pixels back from a Graphite-backed surface, delivering the result to callback."
  • param surface — "The Graphite-backed surface to read from. Throws ArgumentNullException if null."
  • param dstInfo — "Describes the destination pixels: the dimensions the source is rescaled to, and the color type, alpha type and color space it is converted to."
  • param srcRect — "The sub-rectangle of the surface to read. Reading fails (callback receives null) if it is not contained within the surface bounds."
  • param rescaleGamma — "The gamma space to rescale in. See SKImageRescaleGamma." (5-arg overload only)
  • param rescaleMode — "The rescaling technique. See SKImageRescaleMode." (5-arg overload only)
  • param callback — "Receives the pixel data, or null on failure. Only valid for the duration of the call — see the shared remarks above. Throws ArgumentNullException if null."
  • remarks — Reuse the Shared concepts (callback lifetime, failure→null, no-state, rescaling). The read is always deferred: drive it to completion with SKGraphiteContext.Submit (with Sync = true) followed by pumping SKGraphiteContext.CheckAsyncWorkCompletion until the callback has fired. For the 4-arg overload note the defaults: SKImageRescaleGamma.Src + SKImageRescaleMode.Nearest (matching SKImage/SKSurface).

SKGraphiteContext.CheckAsyncWorkCompletion

public void CheckAsyncWorkCompletion ();
  • summary — "Checks whether outstanding asynchronous work on this Graphite context has completed and invokes any ready callbacks."
  • remarks — "The Graphite analogue of GRContext.CheckAsyncWorkCompletion. Used to drive deferred SKGraphiteContext.RequestReadPixels reads to completion; a Submit must also occur to guarantee the work is sent to the GPU. Typically callers Submit then call this in a loop until the read's callback has fired."

Suggested example (Graphite readback)

// Graphite GPU surface -> owned SKImage on the CPU (sync ReadPixels is unavailable on Graphite).
SKImage snapshot = null;
graphiteContext.RequestReadPixels(surface, info, new SKRectI(0, 0, info.Width, info.Height), result =>
{
    if (result != null)
        snapshot = result.ToImage(); // owned copy that survives past the callback
});

// Drive the deferred GPU read to completion.
graphiteContext.Submit(new SKGraphiteSubmitInfo { Sync = true });
while (snapshot is null)
    graphiteContext.CheckAsyncWorkCompletion();
// `snapshot` is now populated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    approved-for-contextThese issues are approved for the Agentic Workflows to consume when writing API docs.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions