Migrate from Ganesh to Graphite
If you already render on the GPU with Ganesh — a GRContext, an SKSurface, and Flush — this page shows the equivalent Graphite calls. The concepts line up closely. Two behavior changes matter most, and they are the parts most likely to bite when you port working code:
- Reading pixels back is asynchronous. Graphite has no synchronous
SKSurface.ReadPixels; you usecontext.RequestReadPixels(...)and pumpCheckAsyncWorkCompletion(). - CPU images need an image provider. Ganesh auto-uploads a raster
SKImagewhen you draw it; Graphite does not — without a provider the draw is silently dropped.
There is also a structural shift. Neither backend is immediate-mode — both defer GPU work — but Ganesh keeps that deferred work inside a stateful GRContext that you drain with Flush/Submit, whereas Graphite is explicit and producer-based: you record into a Recorder, Snap a self-contained Recording, InsertRecording (which encodes it on the context thread), then Submit. Recording runs on CPU worker threads and can be parallelized; InsertRecording and Submit are serialized on one context thread. See What parallel recording actually means.
Note
No SkiaSharp view control drives Graphite yet. If your Ganesh code renders into a view's render target (SKGLView, SKMetalView, SKSwapChainPanel), there is no drop-in Graphite view — you drive Graphite yourself, either offscreen or by wrapping the target's texture. The view controls still use Ganesh.
Concept mapping
| Ganesh | Graphite |
|---|---|
GRContext |
SKGraphiteContext |
GRContext.CreateGl / CreateVulkan / CreateMetal / CreateDirect3D |
SKGraphiteContext.CreateVulkan / CreateMetal / CreateDawn |
GRVkBackendContext / GRMtlBackendContext |
SKGraphiteVkBackendContext / SKGraphiteMtlBackendContext / SKGraphiteDawnBackendContext |
GRSilkNetBackendContext (typed Vulkan, Silk.NET) — or legacy GRSharpVkBackendContext |
No typed Graphite wrapper — fill SKGraphiteVkBackendContext with raw handles (e.g. Silk.NET .Handle values) |
SKSurface.Create(context, budgeted, info) |
context.CreateRecorder() + SKSurface.Create(recorder, info) |
Draw on surface.Canvas |
Draw on surface.Canvas (unchanged) |
context.Flush(submit: true, synchronous: true) |
recorder.Snap() + context.InsertRecording(recording) + context.Submit(new SKGraphiteSubmitInfo { Sync = true }) |
surface.ReadPixels(...) (synchronous) |
context.RequestReadPixels(...) + context.CheckAsyncWorkCompletion() (asynchronous) |
Draw a CPU SKImage (auto-uploaded) |
Draw a CPU SKImage (needs an image provider) |
GRBackendTexture / SKSurface.Create(context, texture, ...) |
SKGraphiteBackendTexture / SKSurface.Create(recorder, backendTexture, colorType) |
SKImage.FromTexture(context, texture, ...) |
SKImage.FromTexture(recorder, backendTexture, ...) |
image.ToTextureImage(context) |
image.ToTextureImage(recorder) |
Notice the pattern: wherever Ganesh takes the context, Graphite's per-surface and per-image APIs take the recorder instead. OpenGL and Direct3D have no Graphite backend — Graphite targets Vulkan, Metal, and Dawn (WebGPU).
Before and after
These snippets compare the core offscreen render and readback flow in each backend. The Graphite version
calls the ReadPixelsFromGraphite helper from Reading pixels back;
that helper is omitted here so the migration steps stay focused on the lifecycle differences.
Ganesh
using System.Runtime.InteropServices;
var info = new SKImageInfo(512, 512, SKColorType.Rgba8888, SKAlphaType.Premul);
using var context = GRContext.CreateMetal(backendContext);
using var surface = SKSurface.Create(context, budgeted: true, info);
using var paint = new SKPaint { Color = SKColors.CornflowerBlue };
surface.Canvas.Clear(SKColors.White);
surface.Canvas.DrawCircle(256, 256, 200, paint);
context.Flush(submit: true, synchronous: true);
// synchronous readback
var pixels = new byte[info.BytesSize];
var handle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
try
{
surface.ReadPixels(info, handle.AddrOfPinnedObject(), info.RowBytes, 0, 0);
}
finally
{
handle.Free();
}
Graphite
var info = new SKImageInfo(512, 512, SKColorType.Rgba8888, SKAlphaType.Premul);
using var context = SKGraphiteContext.CreateMetal(backendContext);
using var recorder = context.CreateRecorder();
using var surface = SKSurface.Create(recorder, info);
using var paint = new SKPaint { Color = SKColors.CornflowerBlue };
surface.Canvas.Clear(SKColors.White);
surface.Canvas.DrawCircle(256, 256, 200, paint);
using (var recording = recorder.Snap())
{
context.InsertRecording(recording);
}
context.Submit(new SKGraphiteSubmitInfo { Sync = true });
// asynchronous readback — helper defined in the Graphite GPU surfaces guide
var pixels = ReadPixelsFromGraphite(context, surface, info);
The drawing calls are identical. What changes is the plumbing around them.
The changes to make
1. Replace Flush with snap + insert + submit
Ganesh flushes the context directly. Graphite splits this into three steps: recorder.Snap() packages the pending Graphite tasks and resource references into an immutable SKGraphiteRecording (not a native command buffer — nothing is encoded or executed yet), context.InsertRecording(recording) encodes those tasks into the context's current command buffer, and context.Submit(new SKGraphiteSubmitInfo { Sync = true }) sends that buffer to the GPU queue and (with Sync = true) waits.
InsertRecording returns an SKGraphiteInsertStatus that production code can inspect when it needs to recover from submission problems. Snap resets the recorder for the next frame. Recording is CPU-side and can run in parallel across worker threads; InsertRecording and Submit are serialized on one context thread. See What parallel recording actually means.
2. Replace synchronous ReadPixels with asynchronous readback
This is the most important change. Graphite surfaces do not support synchronous SKSurface.ReadPixels in shipping builds — it returns false. Replace it with RequestReadPixels, then drive the request to completion with Submit and repeated CheckAsyncWorkCompletion calls. The callback receives a backend-neutral SKImageReadPixelsResult; call ToArray(), ToBitmap(), or CopyPlaneTo(...) on it to get tightly-packed pixels (row padding is stripped for you). See Reading pixels back for the complete helper.
3. Give the recorder an image provider for CPU images
Ganesh silently uploads a raster SKImage to the GPU the first time you draw it. Graphite does not — drawing a non-Graphite image without an image provider drops the draw with no error. If your Ganesh code draws decoded/CPU images, create the recorder with an image provider (the ready-made SKGraphiteImageCache is the simplest option), or upload each image yourself with ToTextureImage first. See Drawing CPU images.
4. Pass the recorder where you used to pass the context
Per-surface and per-image creation moves from the context to the recorder:
SKSurface.Create(context, budgeted, info)→SKSurface.Create(recorder, info)SKSurface.Create(context, backendTexture, ...)→SKSurface.Create(recorder, backendTexture, colorType)SKImage.FromTexture(context, ...)→SKImage.FromTexture(recorder, ...)image.ToTextureImage(context)→image.ToTextureImage(recorder)
Watch out for
- No OpenGL or Direct3D. Graphite targets Vulkan, Metal, and Dawn. There is no Direct3D Graphite backend — on Windows, Graphite means Vulkan. If your Ganesh code uses GL or D3D, there is no direct Graphite equivalent; keep using Ganesh, or move to Vulkan/Metal/Dawn.
- Apple uses Metal, not Vulkan. On macOS/iOS/Mac Catalyst/tvOS the only Graphite backend is Metal; Vulkan Graphite is Linux/Android/Windows. See the platform matrix.
- New Vulkan code should use Silk.NET. For both Ganesh and Graphite, prefer Silk.NET or raw
libvulkanover the unmaintained SharpVk binding. Graphite has no typed wrapper, so pass raw handles toSKGraphiteVkBackendContext. - CPU images need a provider. A raster
SKImagedrawn without an image provider does not appear. See Drawing CPU images. - Browser (Dawn/WebGPU) can't submit synchronously. In a WebAssembly host,
Submit(Sync = true)throws. Submit without syncing and pumpCheckAsyncWorkCompletion. See Graphite with Dawn. - Check backend availability. Use
SKGraphiteContext.IsBackendAvailablebefore creating a context, since not every build includes every backend. - Recording is parallel; submission is serial — and that's the point. A single
SKGraphiteRecorderand its surfaces are single-owner, but Graphite is built for parallel recording: give each worker thread its own recorder, then serialize theInsertRecording/Submitcalls on one context thread. See What parallel recording actually means.