diff --git a/documentation/docfx/guides/TOC.yml b/documentation/docfx/guides/TOC.yml
index 2a81edbdc096..fe9bbb20897a 100644
--- a/documentation/docfx/guides/TOC.yml
+++ b/documentation/docfx/guides/TOC.yml
@@ -1,4 +1,4 @@
-- name: Overview
+- name: SkiaSharp guides
href: index.md
- name: Drawing Basics
href: basics/index.md
@@ -115,3 +115,58 @@
href: effects/image-filters.md
- name: Color Filters
href: effects/color-filters.md
+- name: Drawing destinations
+ href: surfaces/index.md
+ items:
+ - name: Raster surfaces
+ href: surfaces/raster/index.md
+ - name: Ganesh GPU surfaces
+ href: surfaces/ganesh/index.md
+ items:
+ - name: OpenGL
+ href: surfaces/ganesh/opengl.md
+ - name: Vulkan
+ href: surfaces/ganesh/vulkan.md
+ - name: Metal
+ href: surfaces/ganesh/metal.md
+ - name: Direct3D
+ href: surfaces/ganesh/direct3d.md
+ - name: Graphite GPU surfaces
+ href: surfaces/graphite/index.md
+ items:
+ - name: Vulkan
+ href: surfaces/graphite/vulkan.md
+ - name: Metal
+ href: surfaces/graphite/metal.md
+ - name: Dawn and WebGPU
+ href: surfaces/graphite/dawn.md
+ - name: Migrate from Ganesh
+ href: surfaces/graphite/migrate-from-ganesh.md
+ - name: Documents
+ href: surfaces/documents/index.md
+ items:
+ - name: Create a PDF document
+ href: surfaces/documents/pdf.md
+ - name: Create an SVG document
+ href: surfaces/documents/svg.md
+ - name: Create an XPS document
+ href: surfaces/documents/xps.md
+ - name: Choose a SkiaSharp view
+ href: surfaces/views/index.md
+ items:
+ - name: .NET MAUI
+ href: surfaces/views/maui.md
+ - name: Android
+ href: surfaces/views/android.md
+ - name: Apple platforms
+ href: surfaces/views/apple.md
+ - name: Windows
+ href: surfaces/views/windows.md
+ - name: Linux
+ href: surfaces/views/linux.md
+ - name: Tizen
+ href: surfaces/views/tizen.md
+ - name: Uno Platform
+ href: surfaces/views/uno.md
+ - name: Blazor WebAssembly
+ href: surfaces/views/blazor.md
diff --git a/documentation/docfx/guides/index.md b/documentation/docfx/guides/index.md
index 064ba9306866..175a8fcd55c1 100644
--- a/documentation/docfx/guides/index.md
+++ b/documentation/docfx/guides/index.md
@@ -1,27 +1,29 @@
---
-title: "Overview"
-description: "SkiaSharp is a 2D graphics system for .NET and C# powered by the open-source Skia graphics engine that is used extensively in Google products such as Google Chrome, ChromeOS, and Android, in Chromium-based products like Microsoft Edge, in applications like LibreOffice, and in .NET UI frameworks like Uno Platform. This guide explains how to use SkiaSharp for 2D graphics in your .NET MAUI applications."
+title: "SkiaSharp guides"
+description: "Learn how to use SkiaSharp across .NET applications, from drawing basics to surfaces, document output, and platform views."
---
-# SkiaSharp Graphics in .NET MAUI
+# SkiaSharp guides
-_Use SkiaSharp for 2D graphics in your .NET MAUI applications_
+_Use SkiaSharp for 2D graphics in .NET applications_
-SkiaSharp is a 2D graphics system for .NET and C# powered by the open-source Skia graphics engine that is used extensively in Google products such as Google Chrome, ChromeOS, and Android, in Chromium-based products like Microsoft Edge, in applications like LibreOffice, and in .NET UI frameworks like Uno Platform. You can use SkiaSharp in your .NET MAUI applications to draw 2D vector graphics, bitmaps, and text.
+SkiaSharp is a 2D graphics system for .NET and C# powered by the open-source Skia graphics engine that is used extensively in Google products such as Google Chrome, ChromeOS, and Android, in Chromium-based products like Microsoft Edge, in applications like LibreOffice, and in .NET UI frameworks like Uno Platform. You can use SkiaSharp to draw 2D vector graphics, bitmaps, and text in UI, offscreen, document, and headless workloads.
+
+The introductory drawing articles use .NET MAUI views for their examples, but the underlying `SKCanvas` drawing APIs apply across SkiaSharp integrations. The [drawing destinations](surfaces/index.md) section covers manually managed surfaces, document output, and platform-specific views.
+
+## Get started with .NET MAUI
> [!IMPORTANT]
> In .NET MAUI, you must initialize SkiaSharp by calling `UseSkiaSharp()` on the `MauiAppBuilder` in your `MauiProgram.cs` file. This requires adding a `using` directive for the `SkiaSharp.Views.Maui.Controls.Hosting` namespace.
-This guide assumes that you are familiar with .NET MAUI programming.
-
-## SkiaSharp Preliminaries
+The introductory articles assume that you are familiar with .NET MAUI programming.
-SkiaSharp for .NET MAUI is packaged as a NuGet package. After you've created a .NET MAUI solution in Visual Studio or Visual Studio for Mac, you can use the NuGet package manager to search for the **SkiaSharp.Views.Maui.Controls** package and add it to your solution. If you check the **References** section of each project after adding SkiaSharp, you can see that various **SkiaSharp** libraries have been added to each of the projects in the solution.
+After creating a .NET MAUI project, add the **SkiaSharp.Views.Maui.Controls** NuGet package. The package brings in the SkiaSharp libraries required by the target platforms.
In any C# page that uses SkiaSharp you'll want to include a `using` directive for the [`SkiaSharp`](xref:SkiaSharp) namespace, which encompasses all the SkiaSharp classes, structures, and enumerations that you'll use in your graphics programming. You'll also want a `using` directive for the [`SkiaSharp.Views.Maui.Controls`](xref:SkiaSharp.Views.Maui.Controls) namespace for the classes specific to .NET MAUI. This is a much smaller namespace, with the most important class being [`SKCanvasView`](xref:SkiaSharp.Views.Maui.Controls.SKCanvasView). This class derives from the .NET MAUI `View` class and hosts your SkiaSharp graphics output.
> [!IMPORTANT]
-> The `SkiaSharp.Views.Maui.Controls` namespace also contains an `SKGLView` class that derives from `View` but uses OpenGL for rendering graphics. For purposes of simplicity, this guide restricts itself to `SKCanvasView`, but using `SKGLView` instead is quite similar.
+> The `SkiaSharp.Views.Maui.Controls` namespace also contains an `SKGLView` class that derives from `View` and uses a GPU backend selected by the platform handler. These introductory articles use `SKCanvasView`; see [Render with SkiaSharp in .NET MAUI](surfaces/views/maui.md) before choosing `SKGLView`.
## [SkiaSharp Drawing Basics](basics/index.md)
@@ -47,6 +49,10 @@ Bitmaps are rectangular arrays of bits corresponding to the pixels of a display
Effects are properties that alter the normal display of graphics, including linear and circular gradients, bitmap tiling, blend modes, blur, and others.
+## [Drawing destinations](surfaces/index.md)
+
+Choose a manually managed raster, Ganesh, or Graphite surface; draw into PDF, SVG, or XPS documents; or let a platform view manage its surface and presentation.
+
## Related Links
- [SkiaSharp APIs](https://learn.microsoft.com/dotnet/api/skiasharp)
diff --git a/documentation/docfx/guides/surfaces/documents/index.md b/documentation/docfx/guides/surfaces/documents/index.md
new file mode 100644
index 000000000000..7896b8fbb2cb
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/documents/index.md
@@ -0,0 +1,38 @@
+---
+title: "Documents"
+description: "Choose PDF, SVG, or XPS output and learn how each SkiaSharp document API supplies a canvas and finalizes its stream safely."
+---
+
+# Documents
+
+Use a SkiaSharp document canvas when drawing should be serialized to a file or stream instead of rendered into an [`SKSurface`](xref:SkiaSharp.SKSurface). You still draw with an [`SKCanvas`](xref:SkiaSharp.SKCanvas), but the document canvas encodes those draw calls as PDF, SVG, or XPS output in a caller-supplied stream.
+
+Start with PDF for multi-page output unless the consumer specifically requires XPS. Choose SVG when you need one scalable vector graphic represented as XML.
+
+## Choose a format
+
+| Format | Use when | Canvas lifecycle | Main constraint |
+| --- | --- | --- | --- |
+| PDF | You need portable, multi-page document output | Create an `SKDocument`, call `BeginPage` and `EndPage` for each page, then call `Close` | Page dimensions use points; close the document before reading the completed output |
+| SVG | You need one scalable vector graphic or SVG markup | Create an `SKCanvas` with `SKSvgCanvas.Create`, draw your content, then dispose the canvas | There is no page lifecycle; the SVG is not complete until the canvas is disposed |
+| XPS | A Windows workflow specifically requires XPS output | Use the same `SKDocument` page lifecycle as PDF | Requires the Windows XPS Object Model and COM; creation returns `null` where unavailable |
+
+PDF and XPS page sizes use point units, where 72 points equal one inch. Their raster DPI options control how drawing operations without a native document representation are rasterized; DPI does not change the page coordinate system.
+
+## How document canvases differ from surfaces
+
+An `SKSurface` exposes pixels in CPU memory or a GPU resource. You can snapshot it, read its pixels, or present it through a view. A document canvas instead serializes draw calls to an output stream. The public document workflow has no surface to snapshot or read back.
+
+This distinction lets you reuse drawing code. Put the drawing itself in a method that accepts `SKCanvas`, then call it with a surface canvas, a document page canvas, or an SVG canvas as appropriate. Keep destination-specific setup and finalization outside that method.
+
+## In this section
+
+- [Create a PDF document](pdf.md) - write and finalize a multi-page PDF, set metadata, and handle incomplete output.
+- [Create an SVG document](svg.md) - draw one bounded SVG graphic and complete its XML by disposing the canvas.
+- [Create an XPS document](xps.md) - initialize COM and write a multi-page XPS file on supported Windows systems.
+
+## Related output paths
+
+- To produce PNG, JPEG, WebP, or another raster image, [render and encode bitmap pixels](../../bitmaps/saving.md).
+- To record Skia drawing commands for replay or Skia-specific serialization, use [`SKPictureRecorder`](xref:SkiaSharp.SKPictureRecorder) and [`SKPicture`](xref:SkiaSharp.SKPicture). An `SKPicture` is not a standard document format.
+- To render into pixels rather than a document stream, [choose a SkiaSharp surface](../index.md).
diff --git a/documentation/docfx/guides/surfaces/documents/pdf.md b/documentation/docfx/guides/surfaces/documents/pdf.md
new file mode 100644
index 000000000000..c68d8641a031
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/documents/pdf.md
@@ -0,0 +1,87 @@
+---
+title: "Create a PDF document with SkiaSharp"
+description: "Create and finalize a multi-page PDF with SKDocument, point-based page sizes, metadata, and safe stream ownership."
+---
+
+# Create a PDF document with SkiaSharp
+
+Use [`SKDocument`](xref:SkiaSharp.SKDocument) to draw a multi-page PDF into a file or writable stream. Create the document, begin and end each page, and call `Close` after the final page. PDF page dimensions use points, where 72 points equal one inch.
+
+## Create and finalize the pages
+
+The following complete example writes a two-page US Letter PDF. The page canvas is valid only until `EndPage` or `Close` is called.
+
+```csharp
+using System.IO;
+using SkiaSharp;
+
+const string outputPath = "sample.pdf";
+
+using (var output = File.Create(outputPath))
+{
+ WritePdf(output);
+}
+
+static void WritePdf(Stream output)
+{
+ const float pageWidth = 612; // 8.5 inches * 72 points
+ const float pageHeight = 792; // 11 inches * 72 points
+
+ var metadata = SKDocumentPdfMetadata.Default;
+ metadata.Title = "SkiaSharp PDF example";
+ metadata.Author = "Example application";
+
+ using var document = SKDocument.CreatePdf(output, metadata);
+ using var paint = new SKPaint
+ {
+ IsAntialias = true,
+ Color = SKColors.CornflowerBlue,
+ };
+
+ for (var pageNumber = 1; pageNumber <= 2; pageNumber++)
+ {
+ var canvas = document.BeginPage(pageWidth, pageHeight);
+ canvas.Clear(SKColors.White);
+ canvas.DrawCircle(
+ pageWidth / 2,
+ 220 + pageNumber * 120,
+ 100,
+ paint);
+
+ document.EndPage();
+ }
+
+ document.Close();
+}
+```
+
+`SKDocument` owns the native page canvas and invalidates it when the page ends. Scope the managed canvas wrapper to one page and do not use it after `EndPage`.
+
+The `Stream` overload keeps the caller's .NET stream open. `Close` finalizes the PDF into that stream; disposing the document then releases its internal stream wrapper. The caller remains responsible for disposing the .NET stream.
+
+## Configure metadata and raster fallback
+
+Start with `SKDocumentPdfMetadata.Default`, then change the fields you need. A newly zero-initialized `SKDocumentPdfMetadata` does not contain the documented raster DPI and encoding-quality defaults.
+
+The most relevant options are:
+
+- `Title`, `Author`, `Subject`, `Keywords`, `Creator`, `Producer`, `Creation`, and `Modified` set PDF metadata.
+- `RasterDpi` controls the resolution used when a draw operation must be rasterized because PDF has no native representation for it. It does not change page dimensions.
+- `EncodingQuality` is `101` by default, which selects lossless image encoding. Values of `100` or less allow opaque images to use JPEG at that quality.
+- `PdfA` requests the additional metadata and output intent needed by Skia's PDF/A-2b path. Validate the resulting file against any conformance rules your application must meet.
+
+## Handle incomplete output
+
+Call `Abort` if page generation cannot complete. After an abort, discard the output stream contents; they are not a valid document. The example uses a `finally` block so the original failure still propagates while the document is abandoned.
+
+Call `Close` before reading or publishing the result. Disposing a PDF document also closes it when needed, but an explicit `Close` makes the successful completion point clear and ensures final bytes have reached the stream.
+
+## Verify the result
+
+The resulting file should be non-empty, begin with the PDF header `%PDF-`, and open as a two-page US Letter document. Verify text, images, effects, and links in the PDF viewer used by your target workflow.
+
+## Related links
+
+- [Documents](index.md)
+- [Create an XPS document](xps.md)
+- [Save SkiaSharp bitmaps to files](../../bitmaps/saving.md)
diff --git a/documentation/docfx/guides/surfaces/documents/svg.md b/documentation/docfx/guides/surfaces/documents/svg.md
new file mode 100644
index 000000000000..3e60b7160948
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/documents/svg.md
@@ -0,0 +1,60 @@
+---
+title: "Create an SVG document with SkiaSharp"
+description: "Create a single SVG document with SKSvgCanvas, draw through SKCanvas, and finalize valid XML without closing the caller-owned stream."
+---
+
+# Create an SVG document with SkiaSharp
+
+Use [`SKSvgCanvas`](xref:SkiaSharp.SKSvgCanvas) to translate `SKCanvas` draw calls into a Scalable Vector Graphics (SVG) stream. Unlike PDF and XPS, SVG creation returns the canvas directly and has no page lifecycle. Dispose the canvas to complete the XML.
+
+## Create and complete the SVG
+
+The bounds passed to `SKSvgCanvas.Create` define the initial SVG viewport. This complete example writes a 640 by 480 SVG:
+
+```csharp
+using System.IO;
+using SkiaSharp;
+
+const string outputPath = "drawing.svg";
+var bounds = SKRect.Create(640, 480);
+
+using (var output = File.Create(outputPath))
+using (var canvas = SKSvgCanvas.Create(bounds, output))
+using (var backgroundPaint = new SKPaint { Color = SKColors.White })
+using (var paint = new SKPaint
+{
+ IsAntialias = true,
+ Color = SKColors.CornflowerBlue,
+})
+{
+ canvas.DrawRect(bounds, backgroundPaint);
+ canvas.DrawCircle(bounds.MidX, bounds.MidY, 140, paint);
+}
+```
+
+The SVG canvas may buffer output. Its closing XML is not guaranteed to be present until the canvas is disposed. Keep the output stream alive for the full canvas lifetime and dispose the canvas before reading, sending, or closing the stream.
+
+The `Stream` overload does not dispose the caller's .NET stream. In the example, the nested `using` statements dispose the canvas first and the file stream second.
+
+## Reuse drawing code
+
+`SKSvgCanvas.Create` returns an ordinary `SKCanvas`, so drawing helpers that accept `SKCanvas` can target SVG without an `SKSurface`. Keep the SVG bounds and stream lifecycle in the caller:
+
+```csharp
+static void DrawBadge(SKCanvas canvas, SKRect bounds, SKPaint paint)
+{
+ canvas.DrawCircle(bounds.MidX, bounds.MidY, 64, paint);
+}
+```
+
+The SVG output represents drawing commands rather than a pixel snapshot. Open the result in the browsers or SVG renderers your application supports. If you require exact raster pixels instead, [render to a raster surface](../raster/index.md) and encode the resulting image.
+
+## Verify the result
+
+The file should parse as XML with an `svg` root element whose width and height are `640` and `480`. It should display a blue circle on a white background.
+
+## Related links
+
+- [Documents](index.md)
+- [Create a PDF document](pdf.md)
+- [SVG path data in SkiaSharp](../../curves/path-data.md)
diff --git a/documentation/docfx/guides/surfaces/documents/xps.md b/documentation/docfx/guides/surfaces/documents/xps.md
new file mode 100644
index 000000000000..19f31c34cb87
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/documents/xps.md
@@ -0,0 +1,77 @@
+---
+title: "Create an XPS document with SkiaSharp"
+description: "Create and finalize a multi-page XPS document on supported Windows systems, including COM setup and point-based pages."
+---
+
+# Create an XPS document with SkiaSharp
+
+Use [`SKDocument.CreateXps`](xref:SkiaSharp.SKDocument.CreateXps*) when a Windows workflow specifically requires XML Paper Specification (XPS) output. XPS uses the same `BeginPage`, `EndPage`, and `Close` lifecycle as PDF, but the native factory requires the Windows XPS Object Model and an initialized Component Object Model (COM) apartment.
+
+XPS document creation is supported on desktop and server Windows where the XPS Object Model is available. It is not supported on non-Windows systems or Nano Server. `CreateXps` returns `null` when the native XPS factory is unavailable.
+
+## Initialize COM and create the document
+
+Keep [`SKAutoCoInitialize`](xref:SkiaSharp.SKAutoCoInitialize) alive for the complete XPS document lifetime. The following complete example writes a two-page US Letter XPS file:
+
+```csharp
+using System.IO;
+using SkiaSharp;
+
+const string outputPath = "sample.xps";
+
+using (var com = new SKAutoCoInitialize())
+{
+ using var output = File.Create(outputPath);
+ using var document = SKDocument.CreateXps(output);
+
+ const float pageWidth = 612; // 8.5 inches * 72 points
+ const float pageHeight = 792; // 11 inches * 72 points
+
+ using var paint = new SKPaint
+ {
+ IsAntialias = true,
+ Color = SKColors.CornflowerBlue,
+ };
+
+ for (var pageNumber = 1; pageNumber <= 2; pageNumber++)
+ {
+ var canvas = document.BeginPage(pageWidth, pageHeight);
+ canvas.Clear(SKColors.White);
+ canvas.DrawRect(
+ SKRect.Create(96, 96 + pageNumber * 80, 420, 180),
+ paint);
+
+ document.EndPage();
+ }
+
+ document.Close();
+}
+```
+
+The `using` order keeps COM and the output stream alive while the XPS document uses them. The page canvas is invalid after `EndPage`.
+
+## Set the raster DPI
+
+XPS page dimensions use points, where 72 points equal one inch. Within the Windows and COM scope shown in the complete example, the optional `dpi` argument to `CreateXps` controls the resolution used when document content must be rasterized; it does not change the page coordinate system. Keep the writable `Stream output` alive for the document lifetime:
+
+```csharp
+using var document = SKDocument.CreateXps(output, dpi: 144);
+```
+
+The default raster DPI is `SKDocument.DefaultRasterDpi`, which is 72. Increase it only when rasterized content needs more detail and the additional document size and processing cost are acceptable.
+
+## Handle unavailable or incomplete output
+
+Production code can check the nullable result from `CreateXps` when it needs to handle systems where the XPS Object Model factory is unavailable.
+
+If generation fails after document creation, call `Abort` and discard the stream contents. Call `Close` after the final page before reading or publishing the file.
+
+## Verify the result
+
+The resulting file should be non-empty and open as a two-page XPS document in the viewer or print workflow your application targets. Run this verification on the Windows versions you support.
+
+## Related links
+
+- [Documents](index.md)
+- [Create a PDF document](pdf.md)
+- [Create an SVG document](svg.md)
diff --git a/documentation/docfx/guides/surfaces/ganesh/direct3d.md b/documentation/docfx/guides/surfaces/ganesh/direct3d.md
new file mode 100644
index 000000000000..ecfc73b64060
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/ganesh/direct3d.md
@@ -0,0 +1,34 @@
+---
+title: "Ganesh with Direct3D"
+description: "Create a Ganesh Direct3D context from D3D12 objects and describe existing Direct3D resources for SkiaSharp GPU surfaces."
+---
+
+# Use Ganesh with Direct3D
+
+Use the Direct3D backend on Windows when your host owns a DXGI adapter, D3D12 device, and command queue. Graphite has no direct Direct3D backend, so Ganesh is the SkiaSharp path when D3D12 is a requirement.
+
+## Create the Ganesh context
+
+Build a [`GRD3DBackendContext`](xref:SkiaSharp.GRD3DBackendContext) from the native handles:
+
+```csharp
+using var backendContext = new GRD3DBackendContext
+{
+ Adapter = adapterHandle,
+ Device = d3d12DeviceHandle,
+ Queue = commandQueueHandle,
+};
+
+using var context = GRContext.CreateDirect3D(backendContext);
+```
+
+## Describe Direct3D resources
+
+Use `GRD3DTextureResourceInfo` when constructing a [`GRBackendRenderTarget`](xref:SkiaSharp.GRBackendRenderTarget) or [`GRBackendTexture`](xref:SkiaSharp.GRBackendTexture) for an existing D3D12 resource. Your host remains responsible for allocation, resource-state transitions, synchronization, presentation, and final release.
+
+After constructing the backend target or texture, return to [wrapping an existing Ganesh resource](index.md#wrapping-an-existing-render-target) for the shared `SKSurface.Create` calls.
+
+## Related links
+
+- [Ganesh GPU surfaces](index.md)
+- [Choose a SkiaSharp view](../views/index.md)
diff --git a/documentation/docfx/guides/surfaces/ganesh/index.md b/documentation/docfx/guides/surfaces/ganesh/index.md
new file mode 100644
index 000000000000..bf9c003a4b04
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/ganesh/index.md
@@ -0,0 +1,101 @@
+---
+title: "Ganesh GPU surfaces"
+description: "Create Ganesh GPU surfaces with OpenGL, Vulkan, Metal, or Direct3D, then render offscreen or wrap a render target or texture."
+---
+
+# Ganesh GPU surfaces
+
+*Ganesh* is Skia's classic GPU backend. To draw on the GPU with Ganesh you create a [`GRContext`](xref:SkiaSharp.GRContext) — a handle to a live graphics API context — and then create an [`SKSurface`](xref:SkiaSharp.SKSurface) from it. There are two shapes of GPU surface:
+
+- **Offscreen** surfaces, where Skia allocates and owns the GPU texture. This is the GPU equivalent of a [raster surface](../raster/index.md) and is the easiest way to render on the GPU.
+- **Wrapped** surfaces, where you hand Skia an existing render target (a window's framebuffer) or texture that some other code created. This is how you draw SkiaSharp content into a swap-chain image you present to the screen.
+
+Ganesh supports four graphics APIs: [OpenGL](opengl.md), [Vulkan](vulkan.md), [Metal](metal.md), and [Direct3D](direct3d.md). Context and backend-resource setup differ per API; everything after that — creating the surface, drawing, flushing, and reading back — is shared.
+
+A `GRContext` and the resources created from it are not thread-safe, so use them from one thread at a time. OpenGL has an additional requirement: the GL context used to create the `GRContext` must be current whenever Skia makes GL calls, including during resource cleanup.
+
+## Choose and create a backend
+
+Start with the page for the graphics API your host already owns:
+
+- [OpenGL](opengl.md) — make a WGL, GLX, EGL, CGL, or OpenGL ES context current, then let Ganesh resolve its functions.
+- [Vulkan](vulkan.md) — provide the Vulkan instance, physical device, device, graphics queue, and function resolver; a typed Silk.NET adapter is available.
+- [Metal](metal.md) — provide an `MTLDevice` and `MTLCommandQueue` on Apple platforms.
+- [Direct3D](direct3d.md) — provide a DXGI adapter, D3D12 device, and command queue on Windows.
+
+Each page creates the same [`GRContext`](xref:SkiaSharp.GRContext) abstraction. Return here after context creation for the shared surface, drawing, flushing, readback, and wrapping flow.
+
+## Rendering offscreen
+
+The simplest GPU surface is an offscreen one: describe the image with an [`SKImageInfo`](xref:SkiaSharp.SKImageInfo) and let Skia allocate the backing GPU texture. Pass `budgeted: true` so the texture counts against the context's resource budget and can be recycled.
+
+```csharp
+var info = new SKImageInfo(512, 512, SKColorType.Rgba8888, SKAlphaType.Premul);
+
+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);
+
+// push the recorded work to the GPU and wait for it to finish
+context.Flush(submit: true, synchronous: true);
+```
+
+After flushing, you can read the pixels back synchronously with the same `ReadPixels` call used in the [raster case](../raster/index.md#getting-the-result-out):
+
+```csharp
+using System.Runtime.InteropServices;
+
+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();
+}
+```
+
+## Wrapping an existing render target
+
+To draw SkiaSharp content into a render target that already exists — most often a window's framebuffer or a swap-chain image — describe it to Skia with a [`GRBackendRenderTarget`](xref:SkiaSharp.GRBackendRenderTarget) and wrap it with [`SKSurface.Create`](xref:SkiaSharp.SKSurface.Create*).
+
+Construct the `GRBackendRenderTarget` from the API-specific descriptor shown on the [OpenGL](opengl.md#wrap-the-current-framebuffer), [Vulkan](vulkan.md#describe-vulkan-images), [Metal](metal.md#describe-metal-textures), or [Direct3D](direct3d.md#describe-direct3d-resources) page. Once you have it, the wrapping call is shared:
+
+```csharp
+using var surface = SKSurface.Create(
+ context, renderTarget, GRSurfaceOrigin.BottomLeft, colorType);
+
+surface.Canvas.Clear(SKColors.White);
+// ... draw the frame ...
+
+context.Flush();
+// then present/swap buffers with your windowing code
+```
+
+## Wrapping an existing texture
+
+If instead of a render target you have a GPU **texture**, describe it with a [`GRBackendTexture`](xref:SkiaSharp.GRBackendTexture) and create a surface that renders into it:
+
+```csharp
+using var surface = SKSurface.Create(
+ context, backendTexture, GRSurfaceOrigin.TopLeft, sampleCount: 0, colorType: colorType);
+```
+
+You can also wrap a texture as a *sampling* [`SKImage`](xref:SkiaSharp.SKImage) with [`SKImage.FromTexture`](xref:SkiaSharp.SKImage.FromTexture*) when you want to draw an existing GPU texture *onto* a surface rather than *into* it.
+
+## Cleaning up
+
+Dispose your surfaces and the `GRContext` when you are done. If you use OpenGL, keep the GL context current while disposing the SkiaSharp objects that use it. Disposing the `GRContext` frees the GPU resources Skia allocated through it.
+
+## Related links
+
+- [SkiaSharp APIs](xref:SkiaSharp)
+- [Surface overview](../index.md)
+- [Raster surfaces](../raster/index.md)
+- [Choose a SkiaSharp view](../views/index.md)
+- [Graphite GPU surfaces](../graphite/index.md)
+- [Skia canvas creation, GPU backend (skia.org)](https://skia.org/docs/user/api/skcanvas_creation/)
diff --git a/documentation/docfx/guides/surfaces/ganesh/metal.md b/documentation/docfx/guides/surfaces/ganesh/metal.md
new file mode 100644
index 000000000000..96b3c1839d3e
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/ganesh/metal.md
@@ -0,0 +1,34 @@
+---
+title: "Ganesh with Metal"
+description: "Create a Ganesh Metal context from an Apple Metal device and command queue, then wrap Metal textures as SkiaSharp surfaces."
+---
+
+# Use Ganesh with Metal
+
+Use Metal for GPU rendering on Apple platforms. Build a [`GRMtlBackendContext`](xref:SkiaSharp.GRMtlBackendContext) from an `MTLDevice` and an `MTLCommandQueue`.
+
+## Create the Ganesh context
+
+On Apple target frameworks you can assign typed `IMTLDevice` and `IMTLCommandQueue` objects. From other targets, assign their native handles:
+
+```csharp
+using var backendContext = new GRMtlBackendContext
+{
+ DeviceHandle = mtlDeviceHandle,
+ QueueHandle = mtlCommandQueueHandle,
+};
+
+using var context = GRContext.CreateMetal(backendContext);
+```
+
+## Describe Metal textures
+
+Use `GRMtlTextureInfo` when constructing a [`GRBackendRenderTarget`](xref:SkiaSharp.GRBackendRenderTarget) or [`GRBackendTexture`](xref:SkiaSharp.GRBackendTexture) for an existing `MTLTexture`. Your Metal host remains responsible for allocating and eventually releasing that texture.
+
+After constructing the backend target or texture, return to [wrapping an existing Ganesh resource](index.md#wrapping-an-existing-render-target) for the shared `SKSurface.Create` calls.
+
+## Related links
+
+- [Ganesh GPU surfaces](index.md)
+- [Graphite with Metal](../graphite/metal.md)
+- [Choose a SkiaSharp view](../views/index.md)
diff --git a/documentation/docfx/guides/surfaces/ganesh/opengl.md b/documentation/docfx/guides/surfaces/ganesh/opengl.md
new file mode 100644
index 000000000000..d3b3cbc4eb1e
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/ganesh/opengl.md
@@ -0,0 +1,45 @@
+---
+title: "Ganesh with OpenGL"
+description: "Create a Ganesh context from a current OpenGL context and wrap an existing framebuffer as a SkiaSharp GPU surface."
+---
+
+# Use Ganesh with OpenGL
+
+Use OpenGL when your host already owns a WGL, GLX, EGL, CGL, or OpenGL ES context. SkiaSharp does not create that platform context for you, and it must be current on the calling thread whenever Ganesh makes GL calls.
+
+## Create the Ganesh context
+
+Once the platform GL context is current, create a [`GRGlInterface`](xref:SkiaSharp.GRGlInterface) to resolve its entry points and pass it to [`GRContext.CreateGl`](xref:SkiaSharp.GRContext.CreateGl*):
+
+```csharp
+// A platform GL context is already current on this thread.
+using var glInterface = GRGlInterface.Create();
+using var context = GRContext.CreateGl(glInterface);
+```
+
+`GRContext.CreateGl()` also has a parameterless overload that assembles the interface from the current context.
+
+## Wrap the current framebuffer
+
+To draw into a window framebuffer, query its identifier, stencil bits, and sample count from OpenGL, then describe it with [`GRGlFramebufferInfo`](xref:SkiaSharp.GRGlFramebufferInfo):
+
+```csharp
+var glInfo = new GRGlFramebufferInfo((uint)framebuffer, colorType.ToGlSizedFormat());
+using var renderTarget = new GRBackendRenderTarget(
+ width, height, sampleCount, stencilBits, glInfo);
+
+using var surface = SKSurface.Create(
+ context, renderTarget, GRSurfaceOrigin.BottomLeft, colorType);
+```
+
+Draw and flush the surface as described in the [shared Ganesh surface flow](index.md#rendering-offscreen), then present or swap buffers with your windowing API.
+
+## Keep OpenGL current during cleanup
+
+A `GRContext` and its resources are not thread-safe. Keep the same platform GL context current while disposing surfaces, backend targets, and the `GRContext`; cleanup can make GL calls.
+
+## Related links
+
+- [Ganesh GPU surfaces](index.md)
+- [Choose a SkiaSharp view](../views/index.md)
+- [Skia canvas creation, GPU backend (skia.org)](https://skia.org/docs/user/api/skcanvas_creation/)
diff --git a/documentation/docfx/guides/surfaces/ganesh/vulkan.md b/documentation/docfx/guides/surfaces/ganesh/vulkan.md
new file mode 100644
index 000000000000..0d13f1a5213d
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/ganesh/vulkan.md
@@ -0,0 +1,78 @@
+---
+title: "Ganesh with Vulkan"
+description: "Create a Ganesh Vulkan context from raw handles or Silk.NET objects and describe Vulkan images for SkiaSharp GPU surfaces."
+---
+
+# Use Ganesh with Vulkan
+
+Use Vulkan when your host owns the Vulkan instance, physical device, logical device, and graphics queue. Ganesh can consume raw handles through [`GRVkBackendContext`](xref:SkiaSharp.GRVkBackendContext) or typed Silk.NET objects through `GRSilkNetBackendContext`.
+
+## Create a context from raw handles
+
+Supply the queue-family index and a delegate that resolves instance and device functions:
+
+```csharp
+GRVkGetProcedureAddressDelegate getProc = (name, instance, device) =>
+ System.IntPtr.Zero; // TODO: Forward to vkGetInstanceProcAddr or vkGetDeviceProcAddr.
+
+using var extensions = new GRVkExtensions();
+extensions.Initialize(getProc, instanceHandle, physicalDeviceHandle);
+
+using var backendContext = new GRVkBackendContext
+{
+ VkInstance = instanceHandle,
+ VkPhysicalDevice = physicalDeviceHandle,
+ VkDevice = deviceHandle,
+ VkQueue = graphicsQueueHandle,
+ GraphicsQueueIndex = graphicsFamilyIndex,
+ MaxAPIVersion = apiVersion,
+ Extensions = extensions,
+ GetProcedureAddress = getProc,
+};
+
+using var context = GRContext.CreateVulkan(backendContext);
+```
+
+Replace the zero-returning placeholder with your `vkGetInstanceProcAddr` and `vkGetDeviceProcAddr` integration.
+
+Initialize `GRVkExtensions` with the same function resolver before creating the context. If `Extensions` is `null`, Skia uses an empty extension set and cannot detect extension-dependent capabilities. `MaxAPIVersion` should describe the maximum Vulkan API version the host enabled; if it remains `0`, Skia falls back to the instance version it queries.
+
+## Use the Silk.NET adapter
+
+For new managed Vulkan code, [Silk.NET](https://www.nuget.org/packages/Silk.NET.Vulkan) is the recommended binding. The **SkiaSharp.Vulkan.Silk.NET** package provides a typed adapter:
+
+```csharp
+using Silk.NET.Vulkan;
+
+using var extensions = new GRVkExtensions();
+extensions.Initialize(getProc, instance, physicalDevice);
+
+using var backendContext = new GRSilkNetBackendContext
+{
+ VkInstance = instance,
+ VkPhysicalDevice = physicalDevice,
+ VkDevice = device,
+ VkQueue = graphicsQueue,
+ GraphicsQueueIndex = graphicsFamily,
+ MaxAPIVersion = apiVersion,
+ Extensions = extensions,
+ GetProcedureAddress = getProc,
+ VkPhysicalDeviceFeatures = features,
+};
+
+using var context = GRContext.CreateVulkan(backendContext);
+```
+
+The legacy **SkiaSharp.Vulkan.SharpVk** package still exposes `GRSharpVkBackendContext`, but SharpVk is unmaintained and should not be a new dependency.
+
+## Describe Vulkan images
+
+Use `GRVkImageInfo` when constructing a [`GRBackendRenderTarget`](xref:SkiaSharp.GRBackendRenderTarget) or [`GRBackendTexture`](xref:SkiaSharp.GRBackendTexture) for an existing Vulkan image. Your Vulkan host remains responsible for allocating the image, synchronizing access, and presenting or releasing it.
+
+After constructing the backend target or texture, return to [wrapping an existing Ganesh resource](index.md#wrapping-an-existing-render-target) for the shared `SKSurface.Create` calls.
+
+## Related links
+
+- [Ganesh GPU surfaces](index.md)
+- [Graphite with Vulkan](../graphite/vulkan.md)
+- [Skia canvas creation, GPU backend (skia.org)](https://skia.org/docs/user/api/skcanvas_creation/)
diff --git a/documentation/docfx/guides/surfaces/graphite/dawn.md b/documentation/docfx/guides/surfaces/graphite/dawn.md
new file mode 100644
index 000000000000..5ac424b6c475
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/graphite/dawn.md
@@ -0,0 +1,54 @@
+---
+title: "Graphite with Dawn"
+description: "Create a Graphite Dawn context for WebAssembly, submit without blocking, and drive WebGPU completion from the browser loop."
+---
+
+# Use Graphite with Dawn
+
+Graphite Dawn is the WebGPU backend used by SkiaSharp in browser/WebAssembly hosts. `SKGraphiteContext.IsBackendAvailable(SKGraphiteBackend.Dawn)` reports whether the Dawn factory was compiled into the current native library.
+
+## Create the Graphite context
+
+Supply the WebGPU instance, device, and queue handles:
+
+```csharp
+using var backendContext = new SKGraphiteDawnBackendContext
+{
+ WgpuInstance = instanceHandle,
+ WgpuDevice = deviceHandle,
+ WgpuQueue = queueHandle,
+};
+
+using var context = SKGraphiteContext.CreateDawn(backendContext);
+```
+
+With the emdawnwebgpu port, create a real `WGPUInstance` through `wgpuCreateInstance`. Register the device and queue under that instance as their event-source parent. A placeholder or mismatched instance can cause `SKGraphiteContext.CreateDawn` to wait indefinitely.
+
+## Submit from the browser loop
+
+The browser event loop cannot be pumped from inside a managed call, so synchronous submission is not allowed. Calling `Submit` with `Sync = true` throws `InvalidOperationException`.
+
+Submit without blocking, then drive completion from the host render or event loop:
+
+```csharp
+context.InsertRecording(recording);
+context.Submit(new SKGraphiteSubmitInfo { Sync = false });
+
+// Later, from the host loop:
+context.CheckAsyncWorkCompletion();
+```
+
+The same rule applies to readback: request the pixels, submit asynchronously, and keep pumping `CheckAsyncWorkCompletion` until the callback runs.
+
+## Wrap Dawn textures
+
+Use `SKGraphiteBackendTexture.CreateDawn(wgpuTexture)` to describe an existing WebGPU texture, then pass it to the shared `SKSurface.Create(recorder, backendTexture, colorType)` overload. The native WebGPU texture remains owned by the code that created it; release it only after any supplied Graphite release callback fires.
+
+## Next steps
+
+Continue with the shared [Graphite recording, submission, readback, texture, and resource flow](index.md).
+
+## Related links
+
+- [Graphite GPU surfaces](index.md)
+- [Migrate from Ganesh to Graphite](migrate-from-ganesh.md)
diff --git a/documentation/docfx/guides/surfaces/graphite/index.md b/documentation/docfx/guides/surfaces/graphite/index.md
new file mode 100644
index 000000000000..5535a5542b9e
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/graphite/index.md
@@ -0,0 +1,340 @@
+---
+title: "Graphite GPU surfaces"
+description: "Create Graphite GPU surfaces for Vulkan, Metal, or WebGPU, submit recordings, wrap textures, and read pixels asynchronously."
+---
+
+# Graphite GPU surfaces
+
+*Graphite* is Skia's newer GPU backend, built on modern explicit graphics APIs. You create a context, record drawing into a surface, submit that recording to the GPU, and read the result back yourself. None of the SkiaSharp [view controls](../views/index.md) drive Graphite yet, so you drive it directly today — either fully offscreen, or by wrapping an onscreen render target or texture yourself, the same way you can with Ganesh.
+
+Both backends **defer** GPU work rather than executing it as you draw — neither is immediate-mode. The difference is *where* that deferred work lives. [Ganesh](../ganesh/index.md) accumulates it inside a stateful, context-centric [`GRContext`](xref:SkiaSharp.GRContext) and drains it when you call `Flush`/`Submit` (and it may flush internally on its own when it needs to). Graphite instead splits the pipeline into independent producer objects: a **recorder** records drawing and hands you a self-contained **recording**, which you later transfer to one **context** thread to be encoded and submitted. That split is deliberate — it maps cleanly onto modern explicit APIs (Vulkan, Metal, and WebGPU) and lets an application record drawing on several CPU threads in parallel, then funnel the recordings through one shared context.
+
+Graphite differs from [Ganesh](../ganesh/index.md) in two important ways:
+
+- **Drawing is recorded, then submitted separately.** You draw onto a surface's canvas as usual, but instead of flushing a context you *snap* a **recording** from a **recorder** and *insert* that recording into the context, then *submit* it.
+- **Reading pixels back is asynchronous.** Graphite surfaces do not support the synchronous `SKSurface.ReadPixels` you use with raster and Ganesh surfaces. You request a readback and drive it to completion. This is the single most important thing to get right — see [Reading pixels back](#reading-pixels-back).
+
+Graphite supports three backends: **Vulkan**, **Metal**, and **Dawn** (WebGPU).
+
+**Threading model:** `SKGraphiteRecorder` and `SKGraphiteContext` are single-owner objects — never call into one from two threads at once — but they are *not* permanently pinned to the thread that created them. Separate recorders may run concurrently on different CPU workers, provided each recorder (and the surfaces made from it) is touched by one worker at a time and handed off with proper synchronization. Every `SKGraphiteContext` operation — `InsertRecording`, `Submit`, `CheckAsyncWorkCompletion` — must be serialized onto one designated context/submission thread.
+
+## What parallel recording actually means
+
+Graphite's split between recording and submission is what makes it worth the extra plumbing — but it is easy to misread. The parallelism is on the **CPU**: many threads can build recordings at once. Submission stays **serial**: one context encodes those recordings, in order, into one command buffer and hands them to **one** backend queue. That is not a bottleneck that makes Graphite pointless — encoding is cheap CPU work, and the GPU itself is where the massive parallelism happens.
+
+```mermaid
+flowchart TB
+ subgraph CPU["Parallel CPU recording — many worker threads"]
+ direction LR
+ drawA["Draw calls (root A)"] --> recA["SKGraphiteRecorder A"] --> snapA["Snap()"] --> rgA["Recording A
(logical tasks + resource refs + callbacks)"]
+ drawB["Draw calls (root B)"] --> recB["SKGraphiteRecorder B"] --> snapB["Snap()"] --> rgB["Recording B
(logical tasks + resource refs + callbacks)"]
+ end
+ subgraph CTX["Serialized Context calls / command encoding — one context thread"]
+ direction TB
+ insert["InsertRecording A, then B
encode tasks into the one current native command buffer"] --> submit["Submit()"]
+ end
+ subgraph QUEUE["One ordered backend queue — ordered submissions"]
+ queue["MTLCommandQueue / VkQueue / WGPUQueue"]
+ end
+ subgraph GPU["Massively parallel GPU execution — hardware/driver decides overlap"]
+ cp["Command processor"] --> lanes["Thousands of shader lanes"]
+ cp --> raster["Raster units"]
+ cp --> samplers["Texture samplers"]
+ end
+ rgA --> insert
+ rgB --> insert
+ submit --> queue --> cp
+```
+
+Read the zones from top to bottom:
+
+- **Parallel CPU recording.** Each worker owns a recorder, issues draw calls, and calls `Snap()` to get an immutable recording. Recorders never touch each other, so this scales across cores.
+- **Serialized Context calls / command encoding.** `InsertRecording` walks each recording's tasks and encodes them into the context's single current native command buffer; `Submit` closes and submits it. These calls run one at a time on the context thread. No GPU work has executed yet.
+- **Ordered submissions.** The command buffer goes to one backend queue (`MTLCommandQueue`, `VkQueue`, or `WGPUQueue`). Submissions execute in the order you made them.
+- **Massively parallel GPU execution.** The GPU's command processor feeds thousands of shader lanes, raster units, and samplers. *This* is where a single draw is parallelized across pixels and vertices.
+
+Two things people get wrong: **one queue is not one GPU thread** — a single queue already drives the whole GPU's parallel hardware — and **using several recorders does not create several GPU queues** or guarantee that their work overlaps on the GPU. Recorders buy you parallel *recording*; the driver and hardware decide any parallel *execution*.
+
+### Cross-frame pipelining
+
+Because `Submit` is asynchronous and returns without waiting for the GPU, the CPU does not sit idle while a frame renders. While the GPU executes frame N, your workers can already be recording frame N+1:
+
+```mermaid
+flowchart LR
+ recN["CPU records frame N"] --> subN["Submit N — async, returns immediately"]
+ subN --> gpuN["GPU executes frame N"]
+ subN --> recN1["CPU records frame N+1 — overlaps GPU work on N"]
+ recN1 --> subN1["Submit N+1"]
+```
+
+Keep the ordered-draw caveat in mind: this overlaps *CPU recording of the next frame* with *GPU execution of the current one*. It does not reorder or parallelize the ordered draws within a frame — the hardware and driver decide whatever internal overlap is safe.
+
+### A compositor-style example
+
+A UI framework's compositor is the classic fit for parallel recording. (This describes a *pattern* you could build; the stock SkiaSharp view controls and stock Uno controls do **not** render through Graphite today.)
+
+1. **Invalidation identifies independent dirty raster roots** — self-contained subtrees such as scrolling tiles, separate windows, popups, a chart, or a heavy custom control that changed this frame.
+2. **Worker threads record roots in parallel** — one recorder per worker turns each dirty root into a recording, while unchanged roots keep their cached GPU content.
+3. **The context thread inserts recordings in dependency order** — parents after the children they composite, back-to-front where blending requires it — then submits once.
+4. **The compositor assembles and presents** — it combines the freshly recorded roots with the cached ones and presents the frame.
+
+This pays off only when the roots are genuinely independent and each is substantial. Watch for:
+
+- **Partitioning overhead** — splitting the scene, allocating recorders, and synchronizing handoff all cost CPU; too fine a split loses more than it gains.
+- **Dependencies** — a root that reads another's output cannot be recorded in isolation; encode order still has to respect it.
+- **Shared caches** — text/glyph atlases and image caches are shared state; coordinate access rather than racing on them.
+- **Tiny jobs** — recording a trivial root on its own worker is usually slower than just recording it inline.
+
+## Backend platform support
+
+Which Graphite backend you use is determined by the platform:
+
+| Backend | Platforms |
+| --- | --- |
+| [**Metal**](metal.md) | macOS, iOS (including the iOS Simulator on Apple Silicon, with one caveat), Mac Catalyst, tvOS |
+| [**Vulkan**](vulkan.md) | Linux, Android, Windows |
+| [**Dawn**](dawn.md) (WebGPU) | WebAssembly / browser only |
+
+A few consequences worth calling out:
+
+- **Apple platforms use Metal, not Vulkan.** The native Skia build for Apple is not compiled with Vulkan, so on macOS/iOS/Mac Catalyst/tvOS the only Graphite backend is Metal.
+- **On Windows, Graphite means Vulkan.** There is **no Direct3D Graphite backend** in SkiaSharp — a D3D path would only exist as Graphite→Dawn→D3D12, which is not exposed. If you need D3D specifically, use [Ganesh with Direct3D](../ganesh/direct3d.md).
+- **Dawn is browser-only.** It is the WebAssembly path and cannot submit synchronously; see [Graphite with Dawn](dawn.md).
+
+## Checking a backend is available
+
+A given build of SkiaSharp may not include every Graphite backend. Before creating a context, you can check whether a backend is compiled in with `SKGraphiteContext.IsBackendAvailable`:
+
+```csharp
+if (SKGraphiteContext.IsBackendAvailable(SKGraphiteBackend.Metal))
+{
+ // The Metal factory is compiled in. Validate the device before creating a context.
+}
+```
+
+The check does not validate native devices, queues, or required capabilities. Most factory failures return `null`, but the current Metal backend terminates the process if its `MTLDevice` reports none of the GPU families Skia supports. Follow each backend page's preflight requirements before calling its factory.
+
+## Choose and create a backend
+
+Create the context from the native device objects owned by your host:
+
+- [Vulkan](vulkan.md) — raw Vulkan handles on Linux, Android, or Windows, including the render-target usage flags and release ordering required for wrapped images.
+- [Metal](metal.md) — an `MTLDevice` and `MTLCommandQueue` on Apple platforms, including iOS Simulator caveats.
+- [Dawn](dawn.md) — WebGPU handles in a browser/WebAssembly host, including its asynchronous submission constraint.
+
+Each backend page returns the same `SKGraphiteContext`. Return here after context creation for the shared recorder, surface, submission, readback, texture, image-provider, and resource-management flow. Every factory also has an overload that takes [`SKGraphiteContextOptions`](#context-options).
+
+## The render loop
+
+Once you have a context, the Graphite drawing loop is: create a **recorder**, create a surface from it, draw, **snap** a recording, **insert** it, and **submit**.
+
+```csharp
+var info = new SKImageInfo(512, 512, SKColorType.Rgba8888, SKAlphaType.Premul);
+
+using var recorder = context.CreateRecorder();
+using var surface = SKSurface.Create(recorder, info);
+using var paint = new SKPaint { Color = SKColors.CornflowerBlue };
+
+// draw exactly as you would on any other surface
+surface.Canvas.Clear(SKColors.White);
+surface.Canvas.DrawCircle(256, 256, 200, paint);
+
+// capture everything recorded so far
+using var recording = recorder.Snap();
+
+// hand the recording to the context and submit it to the GPU
+context.InsertRecording(recording);
+context.Submit(new SKGraphiteSubmitInfo { Sync = true });
+```
+
+A few things to note:
+
+- `CreateRecorder` returns an `SKGraphiteRecorder`. A recorder is a reusable unit of work capture; you create the surface from it, not from the context directly. If you draw raster (CPU-backed) `SKImage`s, create the recorder with an image provider instead — see [Drawing CPU images](#drawing-cpu-images-the-image-provider).
+- `Snap` produces an `SKGraphiteRecording` — an immutable package of prepared Graphite *tasks*, the resource references they need, and completion callbacks. It is **not** a native command buffer: no Metal, Vulkan, or WebGPU commands have been encoded yet, and no GPU work has run. Snapping resets the recorder so it can record the next frame. It can return **`null`** if the recording could not be built, for example if the driver could not compile a pipeline for something you drew; see [Pipeline compilation](#pipeline-compilation).
+- `InsertRecording` walks the recording's tasks and encodes them into the context's current native command buffer, on the context thread. This is CPU work — it still does not execute anything on the GPU. It returns an [`SKGraphiteInsertStatus`](#status-and-enums) that production code can inspect when it needs to recover from submission problems.
+- `Submit(new SKGraphiteSubmitInfo { Sync = true })` sends that command buffer to the backend queue — this is where the GPU actually starts working — and, with `Sync = true`, waits for it to finish. It returns `false` if submission failed.
+
+## Reading pixels back
+
+> [!IMPORTANT]
+> Graphite surfaces do **not** support the synchronous [`SKSurface.ReadPixels`](xref:SkiaSharp.SKSurface.ReadPixels*) used with [raster](../raster/index.md) and [Ganesh](../ganesh/index.md) surfaces — it returns `false`. To get pixels off a Graphite surface you must use the **asynchronous** readback path. This is the number-one thing to get right when porting existing code.
+
+Call `RequestReadPixels` with the surface, the destination `SKImageInfo`, the source rectangle, and a callback. Then drive the request to completion by submitting and repeatedly calling `CheckAsyncWorkCompletion` until the callback fires. This helper is for **native hosts** only — it submits with `Sync = true`, which a browser/WebGPU host cannot do because it must yield to the event loop (see [Graphite with Dawn](dawn.md)). It bounds the pump, checks the `Submit` result, and handles a failed read:
+
+```csharp
+static byte[] ReadPixelsFromGraphite(
+ SKGraphiteContext context,
+ SKSurface surface,
+ SKImageInfo dstInfo,
+ int maxPumps = 10_000)
+{
+ byte[] pixels = null;
+ var done = false;
+
+ context.RequestReadPixels(
+ surface,
+ dstInfo,
+ new SKRectI(0, 0, dstInfo.Width, dstInfo.Height),
+ result =>
+ {
+ done = true;
+ // The read can fail: the result is null when Graphite could not satisfy it.
+ if (result is null)
+ return;
+ // ToArray copies the plane into a tightly-packed byte[] that outlives the
+ // callback, stripping any per-row transfer padding for you.
+ pixels = result.ToArray();
+ });
+
+ // Flush the queued readback. Submit reports failure by returning false.
+ if (!context.Submit(new SKGraphiteSubmitInfo { Sync = true }))
+ throw new InvalidOperationException("Graphite Submit failed during read-back.");
+
+ // Bounded pump so a callback that never fires can't spin forever.
+ for (var pump = 0; !done && pump < maxPumps; pump++)
+ context.CheckAsyncWorkCompletion();
+
+ if (!done)
+ throw new TimeoutException("Graphite read-back did not complete within the pump budget.");
+ if (pixels is null)
+ throw new InvalidOperationException("Graphite read-back failed.");
+
+ return pixels;
+}
+
+var dstInfo = new SKImageInfo(info.Width, info.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
+var pixels = ReadPixelsFromGraphite(context, surface, dstInfo);
+```
+
+The helper drives the callback to completion within a bounded budget so the sequence is easy to see. In a production renderer, pump completion from the host's render or event loop and apply the timeout or cancellation policy appropriate for the application. Browser hosts cannot use `Sync = true` and must never block the event loop; submit without syncing and pump `CheckAsyncWorkCompletion` from the loop instead — see [Graphite with Dawn](dawn.md).
+
+The callback receives an [`SKImageReadPixelsResult`](#status-and-enums) — the backend-neutral async-read result type shared by the [`SKImage`](xref:SkiaSharp.SKImage), [`SKSurface`](xref:SkiaSharp.SKSurface), and `SKGraphiteContext` read paths. SkiaSharp disposes it automatically when the callback returns, so copy what you need out before returning; using its accessors afterwards throws `ObjectDisposedException`. Keep the context and surface undisposed until the callback completes.
+
+It offers a few ways to extract pixels:
+
+- `ToArray(planeIndex = 0)` — a tightly-packed `byte[]` copy (padding stripped), as above.
+- `ToBitmap()` / `ToImage()` — an owned [`SKBitmap`](xref:SkiaSharp.SKBitmap) or [`SKImage`](xref:SkiaSharp.SKImage) for single-plane (interleaved) results.
+- `CopyPlaneTo(planeIndex, destination)` — copies one plane into a `Span` you own, stripping row padding.
+- `GetPlaneData(planeIndex)` / `GetPlaneRowBytes(planeIndex)` — the raw `ReadOnlySpan` and its stride, if you want to handle padding yourself.
+
+A shorter `RequestReadPixels` overload uses default rescaling; a longer overload lets you pass an [`SKImageRescaleGamma`](#status-and-enums) and [`SKImageRescaleMode`](#status-and-enums) when you want the read to also rescale the image. The default is `(SKImageRescaleGamma.Src, SKImageRescaleMode.Nearest)`.
+
+## Wrapping an external GPU texture
+
+Instead of letting Skia allocate the surface's texture, you can render into a GPU texture your own code created. Build an `SKGraphiteBackendTexture` as shown on the [Vulkan](vulkan.md#wrap-and-release-vulkan-images), [Metal](metal.md#wrap-metal-textures), or [Dawn](dawn.md#wrap-dawn-textures) page, then create a surface that wraps it:
+
+```csharp
+using var surface = SKSurface.Create(
+ recorder, backendTexture, SKColorType.Rgba8888);
+
+surface.Canvas.Clear(SKColors.White);
+// ... draw, then Snap / InsertRecording / Submit as above ...
+```
+
+### Releasing a wrapped texture
+
+When Skia is done with a wrapped backend texture it can notify you through the parameterless `SKGraphiteReleaseDelegate` accepted by the wrap overloads. The callback means Skia no longer needs the texture; it does not automatically delete a texture allocated through `CreateBackendTexture`.
+
+The callback fires after the wrapping surface or image is disposed and pending GPU work has drained. Disposing the wrapper alone is not enough. Delete a Skia-allocated backend texture only after the callback fires, and release an externally allocated texture through the API that created it. The [Vulkan page](vulkan.md#release-a-wrapped-texture) contains the complete wrapper-dispose, GPU-drain, callback, delete, and failure sequence.
+
+`SKImage.FromTexture` has the same release-callback overload and fires after image disposal and GPU drain.
+
+## Using textures as images
+
+You can also move between GPU textures and [`SKImage`](xref:SkiaSharp.SKImage) objects on a recorder:
+
+- [`SKImage.FromTexture`](xref:SkiaSharp.SKImage.FromTexture*) wraps a backend texture as a sampling image you can draw onto a surface:
+
+ ```csharp
+ using var image = SKImage.FromTexture(
+ recorder, backendTexture, SKColorType.Rgba8888, SKAlphaType.Premul);
+ ```
+
+ A longer overload also takes a color space and a parameterless `SKGraphiteReleaseDelegate` that fires once when Skia releases the wrapped texture.
+
+- [`ToTextureImage`](xref:SkiaSharp.SKImage.ToTextureImage*) uploads an existing image (for example, one decoded on the CPU) into a GPU-backed image on the recorder:
+
+ ```csharp
+ using var gpuImage = cpuImage.ToTextureImage(recorder);
+ ```
+
+## Drawing CPU images: the image provider
+
+> [!IMPORTANT]
+> Unlike Ganesh, Graphite does **not** automatically upload a non-Graphite `SKImage` to the GPU. If you draw a raster/CPU-backed `SKImage` — for example one you decoded with `SKImage.FromEncodedData` — onto a Graphite surface **without an image provider, the draw is silently dropped**: nothing appears and no error is raised.
+
+There are two ways to handle this. You can upload each image yourself with [`ToTextureImage`](#using-textures-as-images) and draw the GPU-backed result. Or you can give the recorder an *image provider* callback that uploads CPU images on demand when ordinary `DrawImage` calls need them.
+
+Pass the callback to the `CreateRecorder` overload that accepts one. SkiaSharp ships a ready-made `SKGraphiteImageCache` whose `FindOrCreate` method implements the callback (uploading via `ToTextureImage`) and caches the results — an LRU cache (capped at 256 entries, keyed on the image's unique id and mipmap flag) so repeated draws of the same image don't re-upload every frame:
+
+```csharp
+static SKGraphiteRecorder CreateRecorderWithImageCache(SKGraphiteContext context)
+{
+ var imageCache = new SKGraphiteImageCache();
+ return context.CreateRecorder(
+ recorderBudgetBytes: -1, // -1 = use Skia's default budget
+ findOrCreate: imageCache.FindOrCreate, // uploads + caches CPU images on demand
+ findOrCreateDispose: imageCache.Dispose); // released with the recorder
+}
+
+using var recorder = CreateRecorderWithImageCache(context);
+
+using var surface = SKSurface.Create(recorder, info);
+surface.Canvas.DrawImage(cpuImage, 0, 0); // now uploaded through the provider
+```
+
+The callback has the signature `SKImage SKGraphiteFindOrCreateImageDelegate(SKGraphiteRecorder recorder, SKImage image, bool mipmapped)`, and returning `null` drops that image's draw. `SKGraphiteImageCache` is `IDisposable`; pass its `Dispose` as `findOrCreateDispose` so its cached GPU images are released while the recorder is still alive. Provide your own delegate if you want custom upload or caching behavior; otherwise `SKGraphiteImageCache` is the simplest default.
+
+## Context options
+
+The `Create*` factories accept an optional `SKGraphiteContextOptions`. The most commonly useful field is `InternalMultisampleCount` (the internal MSAA sample count), which must be `0` (use Skia's default) or one of `1`, `2`, `4`, `8`, or `16`; other values are rejected. Other options include a GPU byte budget (`GpuBudgetInBytes`) and driver-workaround toggles.
+
+```csharp
+var options = new SKGraphiteContextOptions
+{
+ InternalMultisampleCount = 4,
+ GpuBudgetInBytes = -1, // preserve Skia's default 256 MB resource budget
+};
+using var context = SKGraphiteContext.CreateMetal(backendContext, options);
+```
+
+The factory overloads that **don't** take options use Skia's defaults, including its default GPU resource budget (256 MB). If you build an `SKGraphiteContextOptions` yourself and want that same default budget, set `GpuBudgetInBytes = -1` (the "use Skia's default" sentinel). A literal `0` creates a zero-byte resource cache.
+
+## Managing resources
+
+An `SKGraphiteContext` exposes a few properties and methods for inspecting and managing GPU resources:
+
+- `Backend`, `IsDeviceLost`, `MaxTextureSize`, and `SupportsProtectedContent` report the state of the underlying device.
+- `MaxBudgetedBytes` gets or sets the GPU memory budget (defaulting to Skia's 256 MB); `CurrentBudgetedBytes` reports current usage.
+- `FreeGpuResources()` releases cached GPU resources; `PerformDeferredCleanup(TimeSpan)` purges resources unused for longer than the given duration.
+
+Dispose recordings, surfaces, recorders, and the context when you are done. The context manages its cached GPU resources. Backend textures created with `CreateBackendTexture` remain caller-owned and must be deleted after their wrappers and pending GPU work are gone, as shown in [Releasing a wrapped texture](#releasing-a-wrapped-texture).
+
+## Pipeline compilation
+
+Graphite renders by building a GPU **pipeline** (a compiled shader program) for each distinct combination of draw operation, paint effects, blend mode, and target surface format. Each pipeline is compiled the **first time** that combination is drawn, and then cached on the context for reuse.
+
+Two practical consequences follow:
+
+- **The first frame that uses a new combination can be slower**, because the pipeline is compiled on demand (during `Snap`/`InsertRecording`). Subsequent frames can reuse the cached pipeline and avoid that first-use compilation cost.
+- **If the driver cannot compile the pipeline, `recorder.Snap()` returns `null`** for that frame. This is exactly the [iOS Simulator gradient limitation](metal.md#simulator-caveats) — the simulator's Metal compiler rejects the pipeline Graphite emits for gradient shaders. Always null-check `Snap()`.
+
+Skia itself supports *pipeline precompilation* — warming the pipeline cache before the first frame so there is no first-use hitch — but that is not yet surfaced in SkiaSharp. The first use of a new draw/paint combination therefore pays a one-time compilation cost.
+
+## Status and enums
+
+Graphite uses a handful of enums and one shared result type:
+
+- `SKGraphiteBackend` — `Dawn`, `Metal`, `Vulkan`, or `Unknown`.
+- `SKGraphiteInsertStatus` — the result of `InsertRecording`; `Success` plus failure reasons such as `InvalidRecording`, `AddCommandsFailed`, and `OutOfOrderRecording`.
+- `SKImageRescaleGamma` — `Src` or `Linear`, for the optional readback rescale. Backend-neutral (shared with the Ganesh async-read path), not Graphite-specific.
+- `SKImageRescaleMode` — `Nearest`, `Linear`, `RepeatedLinear`, or `RepeatedCubic`, for the optional readback rescale. Also backend-neutral.
+- `SKImageReadPixelsResult` — the backend-neutral result handed to the `RequestReadPixels` callback (see [Reading pixels back](#reading-pixels-back)). `IDisposable` and valid only for the duration of the callback.
+
+## Related links
+
+- [SkiaSharp APIs](xref:SkiaSharp)
+- [Surface overview](../index.md)
+- [Ganesh GPU surfaces](../ganesh/index.md)
+- [Migrate from Ganesh to Graphite](migrate-from-ganesh.md)
+- [Skia GPU documentation (skia.org)](https://skia.org/docs/user/api/skcanvas_creation/)
diff --git a/documentation/docfx/guides/surfaces/graphite/metal.md b/documentation/docfx/guides/surfaces/graphite/metal.md
new file mode 100644
index 000000000000..c9a204e166d3
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/graphite/metal.md
@@ -0,0 +1,54 @@
+---
+title: "Graphite with Metal"
+description: "Create a Graphite Metal context, wrap Metal textures, and account for iOS and tvOS Simulator pipeline limitations."
+---
+
+# Use Graphite with Metal
+
+Graphite Metal is available on macOS, iOS, Mac Catalyst, and tvOS, including Apple Silicon simulators. `SKGraphiteContext.IsBackendAvailable(SKGraphiteBackend.Metal)` reports whether the Metal factory was compiled into the current native library; it does not validate the selected `MTLDevice`.
+
+## Create the Graphite context
+
+> [!WARNING]
+> Before creating the command queue or calling `CreateMetal`, use the platform binding for `supportsFamily:` to confirm that the device reports a GPU family accepted by the current Skia build. Current macOS builds check Apple 7 through 9 and Mac 2; iOS-family builds also check Apple 2 through 6. If none of those families is reported, Skia calls `SK_ABORT` instead of returning `null`, which terminates the process.
+
+Supply an `MTLDevice` and `MTLCommandQueue`. On Apple target frameworks you can assign typed `IMTLDevice` and `IMTLCommandQueue` objects; from other targets, assign their native handles:
+
+```csharp
+using var backendContext = new SKGraphiteMtlBackendContext
+{
+ MtlDevice = mtlDeviceHandle,
+ MtlQueue = mtlCommandQueueHandle,
+};
+
+using var context = SKGraphiteContext.CreateMetal(backendContext);
+```
+
+## Wrap Metal textures
+
+Describe an existing `MTLTexture` with `SKGraphiteBackendTexture.CreateMetal`, then use the shared surface-wrapping call:
+
+```csharp
+using var backendTexture = SKGraphiteBackendTexture.CreateMetal(
+ width, height, mtlTextureHandle);
+using var surface = SKSurface.Create(
+ recorder, backendTexture, SKColorType.Rgba8888);
+```
+
+The native `MTLTexture` remains owned by the code that created it. If you supply a release callback, release the native allocation only after the callback fires.
+
+## Simulator caveats
+
+Graphite Metal works on the iOS and tvOS Simulator on Apple Silicon because it uses the host GPU. The simulator's `MTLDevice` can under-report its family, so detect the Apple Simulator explicitly and allow it as an exception to the family probe. Do not apply that exception to other devices or virtualized Metal environments.
+
+The simulator's Metal shader compiler cannot build some Graphite pipelines, including some gradient shaders. In that case, `recorder.Snap()` returns `null` for the frame. The same content renders with Graphite Metal on macOS and physical iOS hardware, and with Ganesh Metal on the simulator. See [mono/SkiaSharp#4555](https://github.com/mono/SkiaSharp/issues/4555), and always check the result of `Snap()`.
+
+## Next steps
+
+Continue with the shared [Graphite recording, submission, readback, texture, and resource flow](index.md).
+
+## Related links
+
+- [Graphite GPU surfaces](index.md)
+- [Ganesh with Metal](../ganesh/metal.md)
+- [Migrate from Ganesh to Graphite](migrate-from-ganesh.md)
diff --git a/documentation/docfx/guides/surfaces/graphite/migrate-from-ganesh.md b/documentation/docfx/guides/surfaces/graphite/migrate-from-ganesh.md
new file mode 100644
index 000000000000..8c99ebd8c027
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/graphite/migrate-from-ganesh.md
@@ -0,0 +1,136 @@
+---
+title: "Migrate from Ganesh to Graphite"
+description: "Migrate Ganesh GPU code to Graphite by replacing context flushing, synchronous readback, and automatic CPU-image uploads."
+---
+
+# Migrate from Ganesh to Graphite
+
+If you already render on the GPU with [Ganesh](../ganesh/index.md) — a [`GRContext`](xref:SkiaSharp.GRContext), an `SKSurface`, and `Flush` — this page shows the equivalent [Graphite](index.md) 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:
+
+1. **Reading pixels back is asynchronous.** Graphite has no synchronous `SKSurface.ReadPixels`; you use `context.RequestReadPixels(...)` and pump `CheckAsyncWorkCompletion()`.
+2. **CPU images need an image provider.** Ganesh auto-uploads a raster `SKImage` when 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](index.md#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](index.md#drawing-cpu-images-the-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](index.md#reading-pixels-back);
+that helper is omitted here so the migration steps stay focused on the lifecycle differences.
+
+### Ganesh
+
+```csharp
+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
+
+```csharp
+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](index.md#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`](xref:SkiaSharp.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](index.md#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](index.md#drawing-cpu-images-the-image-provider).
+
+### 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](index.md#backend-platform-support).
+- **New Vulkan code should use Silk.NET.** For both Ganesh and Graphite, prefer Silk.NET or raw `libvulkan` over the unmaintained SharpVk binding. Graphite has no typed wrapper, so pass raw handles to `SKGraphiteVkBackendContext`.
+- **CPU images need a provider.** A raster `SKImage` drawn without an image provider does not appear. See [Drawing CPU images](index.md#drawing-cpu-images-the-image-provider).
+- **Browser (Dawn/WebGPU) can't submit synchronously.** In a WebAssembly host, `Submit(Sync = true)` throws. Submit without syncing and pump `CheckAsyncWorkCompletion`. See [Graphite with Dawn](dawn.md#submit-from-the-browser-loop).
+- **Check backend availability.** Use `SKGraphiteContext.IsBackendAvailable` before creating a context, since not every build includes every backend.
+- **Recording is parallel; submission is serial — and that's the point.** A single `SKGraphiteRecorder` and its surfaces are single-owner, but Graphite is built for parallel recording: give each worker thread its own recorder, then serialize the `InsertRecording`/`Submit` calls on one context thread. See [What parallel recording actually means](index.md#what-parallel-recording-actually-means).
+
+## Related links
+
+- [SkiaSharp APIs](xref:SkiaSharp)
+- [Ganesh GPU surfaces](../ganesh/index.md)
+- [Graphite GPU surfaces](index.md)
diff --git a/documentation/docfx/guides/surfaces/graphite/vulkan.md b/documentation/docfx/guides/surfaces/graphite/vulkan.md
new file mode 100644
index 000000000000..9a25914a54f7
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/graphite/vulkan.md
@@ -0,0 +1,116 @@
+---
+title: "Graphite with Vulkan"
+description: "Create a Graphite Vulkan context, wrap Vulkan images with the required usage flags, and release backend textures safely."
+---
+
+# Use Graphite with Vulkan
+
+Graphite Vulkan is available on Linux, Android, and Windows. Apple builds use [Metal](metal.md), not Vulkan. `SKGraphiteContext.IsBackendAvailable(SKGraphiteBackend.Vulkan)` reports whether the Vulkan factory was compiled into the current native library.
+
+## Create the Graphite context
+
+Fill `SKGraphiteVkBackendContext` with the Vulkan objects owned by your host:
+
+```csharp
+using var backendContext = new SKGraphiteVkBackendContext
+{
+ VkInstance = instanceHandle,
+ VkPhysicalDevice = physicalDeviceHandle,
+ VkDevice = deviceHandle,
+ VkQueue = graphicsQueueHandle,
+ GraphicsQueueIndex = graphicsFamilyIndex,
+ MaxApiVersion = apiVersion,
+ GetProcedureAddress = (name, instance, device) =>
+ System.IntPtr.Zero, // TODO: Forward to vkGetInstanceProcAddr or vkGetDeviceProcAddr.
+};
+
+using var context = SKGraphiteContext.CreateVulkan(backendContext);
+```
+
+Replace the zero-returning placeholder with your `vkGetInstanceProcAddr` and `vkGetDeviceProcAddr` integration.
+
+There is no typed Graphite-specific Vulkan adapter. With [Silk.NET](https://www.nuget.org/packages/Silk.NET.Vulkan), pass each object's `.Handle` value:
+
+```csharp
+using Silk.NET.Vulkan;
+
+using var backendContext = new SKGraphiteVkBackendContext
+{
+ VkInstance = instance.Handle,
+ VkPhysicalDevice = physicalDevice.Handle,
+ VkDevice = device.Handle,
+ VkQueue = graphicsQueue.Handle,
+ GraphicsQueueIndex = graphicsFamily,
+ MaxApiVersion = apiVersion,
+ GetProcedureAddress = getProc,
+};
+
+using var context = SKGraphiteContext.CreateVulkan(backendContext);
+```
+
+Use Silk.NET or raw `libvulkan` P/Invoke for new code. The legacy SharpVk binding is unmaintained.
+
+## Wrap and release Vulkan images
+
+Use `SKGraphiteBackendTexture.CreateVulkan` to describe an existing `VkImage`. A Vulkan image wrapped as a **renderable surface** must include both `VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT` (`0x10`) and `VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT` (`0x80`). Without them, `SKSurface.Create` returns `null`.
+
+A typical renderable mask is `TRANSFER_SRC | TRANSFER_DST | SAMPLED | COLOR_ATTACHMENT | INPUT_ATTACHMENT` (`0x97`). An image used only for sampling needs `SAMPLED`, not `INPUT_ATTACHMENT`.
+
+When Skia is done with a wrapped texture, the parameterless `SKGraphiteReleaseDelegate` fires after the wrapper is disposed and pending GPU work has drained. Disposing the wrapper alone does not mean the allocation is safe to delete.
+
+### Release a wrapped texture
+
+Starting with an initialized Vulkan `backendContext`, this successful-path sequence allocates a backend texture through the recorder, wraps it, submits work, waits for the release callback, and deletes the allocation while the recorder is still alive:
+
+```csharp
+const int width = 256;
+const int height = 256;
+
+using var context = SKGraphiteContext.CreateVulkan(backendContext);
+using var recorder = context.CreateRecorder();
+
+var vkInfo = new SKGraphiteVkTextureInfo
+{
+ Format = 37, // VK_FORMAT_R8G8B8A8_UNORM
+ ImageTiling = 0, // VK_IMAGE_TILING_OPTIMAL
+ SampleCount = 1,
+ AspectMask = 1, // VK_IMAGE_ASPECT_COLOR_BIT
+ SharingMode = 0, // VK_SHARING_MODE_EXCLUSIVE
+ ImageUsageFlags = 0x1 | 0x2 | 0x4 | 0x10 | 0x80, // = 0x97
+};
+using var textureInfo = SKGraphiteTextureInfo.CreateVulkan(vkInfo);
+using var backendTexture = recorder.CreateBackendTexture(width, height, textureInfo);
+
+var released = false;
+using (var surface = SKSurface.Create(
+ recorder, backendTexture, SKColorType.Rgba8888,
+ colorSpace: null, props: null,
+ releaseProc: () => released = true))
+{
+ surface.Canvas.Clear(SKColors.Red);
+ using var recording = recorder.Snap();
+ context.InsertRecording(recording);
+ context.Submit(new SKGraphiteSubmitInfo { Sync = true });
+}
+
+context.Submit(new SKGraphiteSubmitInfo { Sync = true });
+while (!released)
+ context.CheckAsyncWorkCompletion();
+
+context.FreeGpuResources();
+recorder.DeleteBackendTexture(backendTexture);
+```
+
+The sample assumes wrapping and submission succeed so the release ordering is easy to see. Production code should bound the callback wait. If insertion or submission fails, do not delete an allocation while Skia might still reference it; tear down and recreate the owning Graphite context and native Vulkan device instead.
+
+For a `VkImage` allocated by your own Vulkan code, construct the wrapper with `SKGraphiteBackendTexture.CreateVulkan` and release the native image through Vulkan after the callback. `SKImage.FromTexture` uses the same callback timing.
+
+## Next steps
+
+Continue with the shared [Graphite recording, submission, readback, texture, and resource flow](index.md).
+
+## Related links
+
+- [Graphite GPU surfaces](index.md)
+- [Ganesh with Vulkan](../ganesh/vulkan.md)
+- [Migrate from Ganesh to Graphite](migrate-from-ganesh.md)
diff --git a/documentation/docfx/guides/surfaces/index.md b/documentation/docfx/guides/surfaces/index.md
new file mode 100644
index 000000000000..602087b5cca7
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/index.md
@@ -0,0 +1,71 @@
+---
+title: "Choose a SkiaSharp drawing destination"
+description: "Choose a manually managed surface, document canvas, or platform view for SkiaSharp drawing."
+---
+
+# Choose a SkiaSharp drawing destination
+
+You draw with an [`SKCanvas`](xref:SkiaSharp.SKCanvas), but the API that provides the canvas depends on where the result should go:
+
+| Destination | Who manages it | Use when |
+| --- | --- | --- |
+| [`SKSurface`](xref:SkiaSharp.SKSurface) | Your code | You need offscreen pixels, custom GPU hosting, image processing, or headless rendering |
+| [Document canvas](documents/index.md) | `SKDocument` or `SKSvgCanvas` | Drawing should be serialized as PDF, SVG, or XPS |
+| [Platform view](views/index.md) | A SkiaSharp view control | Drawing belongs in an app UI and the control should manage presentation |
+
+Shared drawing helpers can accept `SKCanvas` and work with any of these destinations. Keep destination-specific creation, callbacks, finalization, and disposal in the caller.
+
+## Create and manage a surface
+
+An `SKSurface` manages a drawing destination and exposes its canvas. The surface decides whether pixels live in system memory or in a GPU resource. SkiaSharp exposes one CPU surface family and two GPU surface families:
+
+- **Raster surfaces** live in CPU memory. They are always available and need no GPU. Use them for image generation, thumbnails, raster assets for document or print pipelines, unit tests, and headless workloads. See [Raster surfaces](raster/index.md).
+
+- **Ganesh GPU surfaces** are backed by a GPU texture through the classic Skia GPU backend, *Ganesh*. Create a [`GRContext`](xref:SkiaSharp.GRContext) for [OpenGL](ganesh/opengl.md), [Vulkan](ganesh/vulkan.md), [Metal](ganesh/metal.md), or [Direct3D](ganesh/direct3d.md), then create an offscreen surface or wrap an existing render target. See [Ganesh GPU surfaces](ganesh/index.md).
+
+- **Graphite GPU surfaces** use Skia's newer GPU backend, *Graphite*, built on [Vulkan](graphite/vulkan.md), [Metal](graphite/metal.md), and [Dawn/WebGPU](graphite/dawn.md). Graphite records drawing into a recorder, snaps it into a recording, and submits that recording to the GPU. See [Graphite GPU surfaces](graphite/index.md).
+
+For app UI, usually let a [SkiaSharp view](views/index.md) create and manage the surface. For document output, use a [document canvas](documents/index.md) instead of rendering an intermediate surface unless the document needs a raster asset.
+
+## Ganesh or Graphite?
+
+Both Ganesh and Graphite are GPU backends that render into an `SKSurface`, but the programming models differ:
+
+| | Ganesh | Graphite |
+| --- | --- | --- |
+| Context | [`GRContext`](xref:SkiaSharp.GRContext) | `SKGraphiteContext` |
+| Backends | OpenGL, Vulkan, Metal, Direct3D | Vulkan, Metal, Dawn (WebGPU) |
+| Platform fit | Broad desktop and mobile support | Metal on Apple; Vulkan elsewhere; Dawn in WebAssembly |
+| Drawing | Draw, then flush or submit | Record, insert, then submit |
+| Readback | Synchronous | Asynchronous |
+| SkiaSharp Views | Ganesh-backed views available | Not available; offscreen only |
+
+Ganesh is mature and is what the Views use today. It supports synchronous `SKSurface.ReadPixels`; Graphite uses `SKGraphiteContext.RequestReadPixels`. Graphite is the direction Skia is moving in, and in SkiaSharp it is currently an **offscreen** path — you create the surface, draw, submit, and read back the result yourself.
+
+If you already have Ganesh code and want to understand the equivalent Graphite calls, see [Migrating from Ganesh to Graphite](graphite/migrate-from-ganesh.md).
+
+## In this section
+
+### [Raster surfaces](raster/index.md)
+
+Create CPU-backed surfaces with `SKSurface.Create`, draw into memory you own with a raster-direct surface, and read the result back.
+
+### [Ganesh GPU surfaces](ganesh/index.md)
+
+Create a `GRContext` for OpenGL, Vulkan, Metal, or Direct3D, then make an offscreen surface or wrap an existing render target or texture.
+
+### [Graphite GPU surfaces](graphite/index.md)
+
+Create an `SKGraphiteContext`, record and submit drawing, wrap external GPU textures, and read pixels back through the asynchronous readback path. If you are porting existing code, [map the Ganesh lifecycle to Graphite](graphite/migrate-from-ganesh.md).
+
+### [Documents](documents/index.md)
+
+Create and finalize PDF, SVG, or XPS output through a canvas backed by a document stream.
+
+### [Choose a SkiaSharp view](views/index.md)
+
+Choose a raster or Ganesh-backed control for .NET MAUI, native platforms, Uno Platform, or Blazor WebAssembly.
+
+## Related links
+
+- [SkiaSharp APIs](xref:SkiaSharp)
diff --git a/documentation/docfx/guides/surfaces/raster/index.md b/documentation/docfx/guides/surfaces/raster/index.md
new file mode 100644
index 000000000000..f817dfb3f865
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/raster/index.md
@@ -0,0 +1,127 @@
+---
+title: "Raster surfaces"
+description: "Create CPU-backed SKSurface objects for offscreen and headless rendering, draw into caller-owned memory, and save or read pixels."
+---
+
+# Raster surfaces
+
+A **raster** surface keeps its pixels in system (CPU) memory. It needs no GPU and is available on every platform SkiaSharp supports. Use a raster surface when you want a portable CPU rendering path for images, thumbnails, raster assets for document or print pipelines, server workloads, or tests.
+
+## Creating a raster surface
+
+The most common way to create a raster surface is to describe the image you want with an [`SKImageInfo`](xref:SkiaSharp.SKImageInfo) and let SkiaSharp allocate the pixel buffer for you:
+
+```csharp
+var info = new SKImageInfo(256, 256, SKColorType.Rgba8888, SKAlphaType.Premul);
+
+using var surface = SKSurface.Create(info);
+using var paint = new SKPaint { Color = SKColors.CornflowerBlue };
+var canvas = surface.Canvas;
+
+canvas.Clear(SKColors.White);
+canvas.DrawCircle(128, 128, 100, paint);
+```
+
+`SKImageInfo` describes the width, height, color type, and alpha type of the surface. `SKColorType.Rgba8888` with `SKAlphaType.Premul` is a common, portable choice, but you can pick whatever format your pipeline needs.
+
+The [`SKSurface.Create(SKImageInfo)`](xref:SkiaSharp.SKSurface.Create(SkiaSharp.SKImageInfo)) overload returns `null` if the surface could not be created, for example when dimensions are invalid. The snippets in this guide use valid inputs and focus on the successful rendering flow.
+
+## Getting the result out
+
+Once you've finished drawing, there are two common ways to get the pixels back.
+
+The simplest is to take an immutable snapshot as an [`SKImage`](xref:SkiaSharp.SKImage), which you can then encode to PNG, JPEG, or another format:
+
+```csharp
+using System.IO;
+
+using var image = surface.Snapshot();
+using var data = image.Encode(SKEncodedImageFormat.Png, 100);
+
+using var stream = File.OpenWrite("output.png");
+data.SaveTo(stream);
+```
+
+If you need the raw pixels rather than an encoded image, read them back into a buffer with [`ReadPixels`](xref:SkiaSharp.SKSurface.ReadPixels*). Because a raster surface already lives in CPU memory, this read is synchronous and does not cross a GPU boundary:
+
+```csharp
+using System.Runtime.InteropServices;
+
+var info = new SKImageInfo(256, 256, SKColorType.Rgba8888, SKAlphaType.Premul);
+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();
+}
+```
+
+> [!NOTE]
+> Synchronous `ReadPixels` is a raster and Ganesh convenience. Graphite surfaces do **not** support it — see [Graphite GPU surfaces](../graphite/index.md#reading-pixels-back).
+
+## Raster-direct: drawing into memory you own
+
+The `SKSurface.Create(SKImageInfo)` overload lets Skia allocate the pixel buffer. Sometimes you already have a buffer — a `byte[]`, a native allocation, or the pixels of an [`SKBitmap`](xref:SkiaSharp.SKBitmap) — and you want Skia to draw *directly* into it with no extra copy. That is a **raster-direct** surface.
+
+Pass the address of your buffer along with the info and row stride:
+
+```csharp
+using System.Runtime.InteropServices;
+
+var info = new SKImageInfo(256, 256, SKColorType.Rgba8888, SKAlphaType.Premul);
+var pixels = new byte[info.BytesSize];
+var handle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
+try
+{
+ using var surface = SKSurface.Create(info, handle.AddrOfPinnedObject(), info.RowBytes);
+ using var paint = new SKPaint { Color = SKColors.Red };
+
+ // every draw call writes straight into `pixels`
+ surface.Canvas.Clear(SKColors.White);
+ surface.Canvas.DrawCircle(128, 128, 100, paint);
+}
+finally
+{
+ handle.Free();
+}
+```
+
+> [!IMPORTANT]
+> The memory you pass must stay alive and pinned until the `SKSurface` and every image or pixmap that still shares the buffer have been disposed. Flushing a raster canvas does not release the pointer. Freeing a `GCHandle` earlier lets the garbage collector move the buffer while Skia can still access it.
+
+You can also wrap an [`SKPixmap`](xref:SkiaSharp.SKPixmap) — which already bundles an `SKImageInfo` with a pixel pointer — using the [`SKSurface.Create(SKPixmap)`](xref:SkiaSharp.SKSurface.Create(SkiaSharp.SKPixmap)) overload:
+
+```csharp
+using var bitmap = new SKBitmap(info);
+using var pixmap = bitmap.PeekPixels();
+using var surface = SKSurface.Create(pixmap);
+
+surface.Canvas.Clear(SKColors.White);
+// ... draw ...
+// the pixels are now visible directly in `bitmap`
+```
+
+This is a convenient way to draw straight into an `SKBitmap` you already have.
+
+## When to use a raster surface
+
+Reach for a raster surface when:
+
+- You are rendering **offscreen** or **headless** — no window, no GPU context.
+- You want a portable CPU rendering path without a graphics API or driver dependency.
+- You are producing images to save, stream, or process further (thumbnails, tiles, reports).
+- You need to draw directly into a buffer you already own (raster-direct).
+
+If you need GPU acceleration — because you are rendering many frames per second, compositing with other GPU content, or drawing very large scenes — use a GPU-backed surface instead. See [Ganesh GPU surfaces](../ganesh/index.md) and [Graphite GPU surfaces](../graphite/index.md).
+
+## Related links
+
+- [SkiaSharp APIs](xref:SkiaSharp)
+- [Surface overview](../index.md)
+- [Creating and drawing on bitmaps](../../bitmaps/drawing.md)
+- [Skia canvas creation, Raster backend (skia.org)](https://skia.org/docs/user/api/skcanvas_creation/)
diff --git a/documentation/docfx/guides/surfaces/views/android.md b/documentation/docfx/guides/surfaces/views/android.md
new file mode 100644
index 000000000000..12fa48a3f5f6
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/views/android.md
@@ -0,0 +1,44 @@
+---
+title: "Render with SkiaSharp views on Android"
+description: "Choose a native Android raster, GLSurfaceView, or GLTextureView control and draw through its paint event."
+---
+
+# Render with SkiaSharp views on Android
+
+The `SkiaSharp.Views` package includes native Android controls in the `SkiaSharp.Views.Android` namespace:
+
+| Control | Android base | Rendering path | Paint event |
+| --- | --- | --- | --- |
+| `SKCanvasView` | `View` | CPU raster | `SKPaintSurfaceEventArgs` |
+| `SKGLSurfaceView` | `GLSurfaceView` | Ganesh over OpenGL ES | `SKPaintGLSurfaceEventArgs` |
+| `SKGLTextureView` | `TextureView` through SkiaSharp's `GLTextureView` | Ganesh over OpenGL ES | `SKPaintGLSurfaceEventArgs` |
+
+Use `SKCanvasView` for the default raster path. The two GPU controls expose the same SkiaSharp drawing model but inherit from different Android view primitives. Choose between surface-view and texture-view behavior based on the composition needs of the Android layout.
+
+## Create a view in code
+
+This example installs a raster view as an activity's content:
+
+```csharp
+using SkiaSharp.Views.Android;
+
+var skiaView = new SKCanvasView(this);
+skiaView.PaintSurface += OnPaintSurface;
+SetContentView(skiaView);
+```
+
+The view owns the surface passed to `OnPaintSurface`. Do not dispose the surface or retain its canvas after the callback.
+
+For GPU rendering, create `SKGLSurfaceView` or `SKGLTextureView` and handle `SKPaintGLSurfaceEventArgs` instead. Both controls expose their managed `GRContext` while the GL context is valid.
+
+## Request another frame
+
+Call `Invalidate()` on `SKCanvasView` after state changes. The GL controls use their inherited render-request and render-mode APIs. Avoid running a continuous render loop when the scene is unchanged.
+
+Android screen density can make the raw pixel size differ from the logical control size. Check `Info`, `RawInfo`, and the control's pixel-scaling option before mixing Skia coordinates with Android layout coordinates.
+
+## Related links
+
+- [Choose a SkiaSharp view](index.md)
+- [.NET MAUI views](maui.md)
+- [Ganesh with OpenGL](../ganesh/opengl.md)
diff --git a/documentation/docfx/guides/surfaces/views/apple.md b/documentation/docfx/guides/surfaces/views/apple.md
new file mode 100644
index 000000000000..c7db69c054a6
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/views/apple.md
@@ -0,0 +1,54 @@
+---
+title: "Render with SkiaSharp views on Apple platforms"
+description: "Choose raster, OpenGL, or Metal SkiaSharp views on iOS, tvOS, macOS, and Mac Catalyst."
+---
+
+# Render with SkiaSharp views on Apple platforms
+
+The `SkiaSharp.Views` package supplies native controls for iOS, tvOS, macOS, and Mac Catalyst. The namespace depends on the target:
+
+- iOS and Mac Catalyst: `SkiaSharp.Views.iOS`
+- tvOS: `SkiaSharp.Views.tvOS`
+- macOS: `SkiaSharp.Views.Mac`
+
+## Choose a control
+
+| Target | Raster | OpenGL | Metal |
+| --- | --- | --- | --- |
+| iOS | `SKCanvasView` | `SKGLView` | `SKMetalView` |
+| tvOS | `SKCanvasView` | `SKGLView` | `SKMetalView` |
+| macOS | `SKCanvasView` | `SKGLView` | `SKMetalView` |
+| Mac Catalyst | `SKCanvasView` | Not available | `SKMetalView` |
+
+Raster views raise `SKPaintSurfaceEventArgs`. OpenGL views raise `SKPaintGLSurfaceEventArgs`, and `SKMetalView` raises `SKPaintMetalSurfaceEventArgs`.
+
+Use the same canvas drawing code for each event type:
+
+```csharp
+void OnPaintMetalSurface(object? sender, SKPaintMetalSurfaceEventArgs e)
+{
+ var canvas = e.Surface.Canvas;
+ canvas.Clear(SKColors.White);
+ // Draw the frame without retaining the surface or canvas.
+}
+```
+
+## Prefer Metal for new GPU paths
+
+`SKGLView` on iOS and tvOS is marked obsolete starting with operating-system version 12. Use `SKMetalView` for new native GPU rendering on those targets.
+
+Mac Catalyst compiles the raster and Apple Metal controls but excludes `SKGLView`. On macOS, both OpenGL and Metal controls remain available; choose Metal for a modern GPU path and verify behavior on the macOS versions your app supports.
+
+`SKMetalView` creates a Ganesh Metal context. It does not use Graphite.
+
+## Redraw safely
+
+Use the platform view invalidation mechanism rather than drawing outside the paint callback. For on-demand Metal rendering, configure the `MTKView` to pause its loop and request display when state changes. For animation, use the control's render-loop behavior and stop it when the view is no longer visible.
+
+The view owns its render target and the surface supplied to the callback. Dispose only resources your drawing code creates, such as paints, paths, images, and shaders.
+
+## Related links
+
+- [Choose a SkiaSharp view](index.md)
+- [.NET MAUI views](maui.md)
+- [Ganesh with Metal](../ganesh/metal.md)
diff --git a/documentation/docfx/guides/surfaces/views/blazor.md b/documentation/docfx/guides/surfaces/views/blazor.md
new file mode 100644
index 000000000000..442b27677478
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/views/blazor.md
@@ -0,0 +1,50 @@
+---
+title: "Render with SkiaSharp views in Blazor WebAssembly"
+description: "Use SkiaSharp.Views.Blazor raster HTML canvas and Ganesh WebGL components in a Blazor WebAssembly app."
+---
+
+# Render with SkiaSharp views in Blazor WebAssembly
+
+The `SkiaSharp.Views.Blazor` package provides two Razor components for browser WebAssembly:
+
+| Component | Rendering path | Callback |
+| --- | --- | --- |
+| `SKCanvasView` | CPU raster copied to an HTML 2D canvas | `OnPaintSurface` with `SKPaintSurfaceEventArgs` |
+| `SKGLView` | Ganesh over WebGL | `OnPaintSurface` with `SKPaintGLSurfaceEventArgs` |
+
+The package is browser-only. Its JavaScript interop rejects non-WebAssembly hosting.
+
+## Add a raster component
+
+```razor
+@using SkiaSharp
+@using SkiaSharp.Views.Blazor
+
+
+
+@code {
+ private void OnPaintSurface(SKPaintSurfaceEventArgs e)
+ {
+ var canvas = e.Surface.Canvas;
+ canvas.Clear(SKColors.White);
+ }
+}
+```
+
+Use `SKGLView` when the browser and workload benefit from WebGL. The drawing callback still receives an `SKSurface`; only the event argument type and backing target change.
+
+## Control redraws
+
+Call `Invalidate()` after application state changes. Set `EnableRenderLoop` only for animation, because it requests browser animation frames continuously.
+
+`IgnorePixelScaling` changes whether the callback's logical size or device-pixel size is used for drawing. Compare `Info` and `RawInfo` when coordinating Skia drawing with CSS layout or pointer coordinates.
+
+Both components own their event surfaces and JavaScript interop objects. The raster component also owns its pinned pixel buffer; the GPU component owns its WebGL-backed Ganesh context. Do not retain the event surface after the callback.
+
+## Related links
+
+- [Choose a SkiaSharp view](index.md)
+- [Uno Platform views](uno.md)
+- [Raster surfaces](../raster/index.md)
+- [Ganesh with OpenGL](../ganesh/opengl.md)
diff --git a/documentation/docfx/guides/surfaces/views/index.md b/documentation/docfx/guides/surfaces/views/index.md
new file mode 100644
index 000000000000..b5d2d7ca85d0
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/views/index.md
@@ -0,0 +1,66 @@
+---
+title: "Choose a SkiaSharp view"
+description: "Choose a raster or Ganesh-backed SkiaSharp view for .NET MAUI, native platforms, Uno Platform, or Blazor WebAssembly."
+---
+
+# Choose a SkiaSharp view
+
+SkiaSharp view controls create the drawing surface, size it to the control, and provide its canvas through a paint callback. Use a view when drawing belongs in an app UI and you do not need to create, wrap, present, or dispose the [`SKSurface`](xref:SkiaSharp.SKSurface) yourself.
+
+View controls use one of two rendering paths:
+
+| View family | Typical controls | Rendering path | Use when |
+| --- | --- | --- | --- |
+| Raster | `SKCanvasView`, `SKXamlCanvas`, `SKElement`, `SKControl`, `SKDrawingArea` | CPU [raster surface](../raster/index.md), then platform presentation | You want the broadest support or do not need continuous GPU rendering |
+| GPU | `SKGLView`, `SKMetalView`, `SKGLSurfaceView`, `SKGLTextureView`, `SKSwapChainPanel`, `SKGLElement`, `SKGLControl` | [Ganesh](../ganesh/index.md) over OpenGL, OpenGL ES, Metal, ANGLE, or WebGL | You render continuously or have a workload that benefits from a GPU-backed target |
+
+> [!NOTE]
+> The shipped view controls use raster surfaces or Ganesh. They do not drive [Graphite](../graphite/index.md). Graphite remains a manually managed rendering path.
+>
+> `SkiaSharp.Views.Tizen.NUI.SKGLSurfaceView` is an exception to the control naming pattern: it raises raster `SKPaintSurfaceEventArgs`, not Ganesh-backed `SKPaintGLSurfaceEventArgs`. See [Tizen views](tizen.md).
+
+## Draw in the paint callback
+
+Raster controls raise or invoke a callback with `SKPaintSurfaceEventArgs`. Draw on the provided surface and do not retain its canvas after the callback returns:
+
+```csharp
+void OnPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
+{
+ var canvas = e.Surface.Canvas;
+ using var paint = new SKPaint
+ {
+ IsAntialias = true,
+ Color = SKColors.CornflowerBlue,
+ };
+
+ canvas.Clear(SKColors.White);
+ canvas.DrawCircle(e.Info.Width / 2f, e.Info.Height / 2f, 100, paint);
+}
+```
+
+GL-, ANGLE-, and WebGL-backed controls use `SKPaintGLSurfaceEventArgs`. Native Apple Metal controls use `SKPaintMetalSurfaceEventArgs`. All of these event arguments expose an `SKSurface`; the drawing code still starts with `e.Surface.Canvas`.
+
+The .NET MAUI `SKGLView` always exposes the MAUI `SKPaintGLSurfaceEventArgs`, including on Mac Catalyst where its handler uses a native Metal view.
+
+## Choose a platform or integration
+
+- [.NET MAUI](maui.md) - use cross-platform `SKCanvasView` and `SKGLView` controls and register their handlers.
+- [Android](android.md) - use native raster, GL surface, or GL texture views.
+- [Apple platforms](apple.md) - choose raster, OpenGL, or Metal controls on iOS, tvOS, macOS, and Mac Catalyst.
+- [Windows](windows.md) - use WPF, Windows Forms, or WinUI controls.
+- [Linux](linux.md) - use the GTK 3 or GTK 4 raster drawing area.
+- [Tizen](tizen.md) - choose the ElmSharp or NUI control family.
+- [Uno Platform](uno.md) - use WinUI-shaped raster and GPU controls with target-specific support.
+- [Blazor WebAssembly](blazor.md) - use raster HTML canvas or WebGL Razor components.
+
+## Choose raster or GPU
+
+Start with the raster control for your UI framework. Move to a GPU control when profiling shows that raster presentation is the bottleneck, or when the app renders a continuously changing scene that benefits from GPU acceleration.
+
+Changing the control does not require changing shared drawing helpers that accept `SKCanvas`. It does change the platform APIs, event argument type, invalidation model, and GPU availability, so verify the chosen control on every target your app ships.
+
+## Related links
+
+- [Choose a SkiaSharp drawing destination](../index.md)
+- [Ganesh GPU surfaces](../ganesh/index.md)
+- [Documents](../documents/index.md)
diff --git a/documentation/docfx/guides/surfaces/views/linux.md b/documentation/docfx/guides/surfaces/views/linux.md
new file mode 100644
index 000000000000..5b14816863a1
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/views/linux.md
@@ -0,0 +1,36 @@
+---
+title: "Render with SkiaSharp views on Linux"
+description: "Use the GTK 3 or GTK 4 SKDrawingArea control for raster SkiaSharp rendering on Linux."
+---
+
+# Render with SkiaSharp views on Linux
+
+SkiaSharp provides one raster control for each supported GTK generation:
+
+| GTK version | Package | Control | Paint event |
+| --- | --- | --- | --- |
+| GTK 3 | `SkiaSharp.Views.Gtk3` | `SkiaSharp.Views.Gtk.SKDrawingArea` | `SKPaintSurfaceEventArgs` |
+| GTK 4 | `SkiaSharp.Views.Gtk4` | `SkiaSharp.Views.Gtk.SKDrawingArea` | `SKPaintSurfaceEventArgs` |
+
+Both controls create a memory-backed `SKSurface`, invoke `PaintSurface`, and present the result through Cairo. The packages do not include a GTK GPU view.
+
+## Add the drawing area
+
+Create the control, subscribe to `PaintSurface`, and add it with the normal GTK container API:
+
+```csharp
+using SkiaSharp.Views.Gtk;
+
+var skiaView = new SKDrawingArea();
+skiaView.PaintSurface += OnPaintSurface;
+```
+
+Draw only during the event. The control owns the surface and its Cairo backing storage. Use GTK's `QueueDraw()` mechanism when application state changes.
+
+If a Linux application needs a GPU-backed view, it must host an appropriate graphics context and wrap its render target manually. The packaged GTK controls do not provide that integration.
+
+## Related links
+
+- [Choose a SkiaSharp view](index.md)
+- [Raster surfaces](../raster/index.md)
+- [Ganesh GPU surfaces](../ganesh/index.md)
diff --git a/documentation/docfx/guides/surfaces/views/maui.md b/documentation/docfx/guides/surfaces/views/maui.md
new file mode 100644
index 000000000000..b9ef1aa55bb2
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/views/maui.md
@@ -0,0 +1,72 @@
+---
+title: "Render with SkiaSharp in .NET MAUI"
+description: "Register and use SkiaSharp.Views.Maui.Controls raster and GPU views across Android, iOS, Mac Catalyst, and Windows."
+---
+
+# Render with SkiaSharp in .NET MAUI
+
+The `SkiaSharp.Views.Maui.Controls` package provides two cross-platform controls:
+
+| Control | Surface | Paint event |
+| --- | --- | --- |
+| `SKCanvasView` | CPU raster | `PaintSurface` with `SKPaintSurfaceEventArgs` |
+| `SKGLView` | Ganesh GPU | `PaintSurface` with `SKPaintGLSurfaceEventArgs` |
+
+Start with `SKCanvasView`. Choose `SKGLView` only after checking the backend and support level on every target your app ships.
+
+## Register the handlers
+
+Call `UseSkiaSharp()` when creating the MAUI app. Without this call, MAUI does not register the handlers for the controls:
+
+```csharp
+using SkiaSharp.Views.Maui.Controls.Hosting;
+
+public static MauiApp CreateMauiApp() =>
+ MauiApp
+ .CreateBuilder()
+ .UseMauiApp()
+ .UseSkiaSharp()
+ .Build();
+```
+
+## Add a view
+
+Declare the controls from the `SkiaSharp.Views.Maui.Controls` namespace:
+
+```xml
+
+
+
+```
+
+The handler receives `SkiaSharp.Views.Maui.SKPaintSurfaceEventArgs`. Draw on `e.Surface.Canvas`, then let the control present the result.
+
+Use `InvalidateSurface()` when application state changes and the control needs another frame. For `SKGLView`, set `HasRenderLoop` only while continuous rendering is required; otherwise invalidate on demand.
+
+## Check the platform backend
+
+The public MAUI controls remain the same, but their handlers use different native views:
+
+| Target | `SKCanvasView` handler | `SKGLView` handler |
+| --- | --- | --- |
+| Android | Native raster `SKCanvasView` | `SKGLTextureView` with OpenGL ES |
+| iOS | Native raster `SKCanvasView` | Native `SKGLView` with OpenGL ES |
+| Mac Catalyst | Native raster `SKCanvasView` | Native `SKMetalView` with Metal |
+| Windows | `SKXamlCanvas` | `SKSwapChainPanel` with ANGLE/OpenGL ES |
+
+The current MAUI projects target Android, iOS, Mac Catalyst, and Windows. They do not target tvOS, macOS, or Tizen.
+
+On iOS, the `SKGLView` handler is marked obsolete starting with iOS 12 because it uses OpenGL ES. The MAUI package does not expose a separate `SKMetalView` control for iOS. Use `SKCanvasView` or provide a native/custom Metal integration when an iOS GPU path is required.
+
+On Mac Catalyst, the MAUI `SKGLView` handler uses Metal internally, but the callback type is still the MAUI `SKPaintGLSurfaceEventArgs`.
+
+## Related links
+
+- [Choose a SkiaSharp view](index.md)
+- [Android views](android.md)
+- [Apple platform views](apple.md)
+- [Windows views](windows.md)
+- [Integrating with .NET MAUI](../../basics/integration.md)
diff --git a/documentation/docfx/guides/surfaces/views/tizen.md b/documentation/docfx/guides/surfaces/views/tizen.md
new file mode 100644
index 000000000000..90ff2116bd13
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/views/tizen.md
@@ -0,0 +1,37 @@
+---
+title: "Render with SkiaSharp views on Tizen"
+description: "Choose the ElmSharp or NUI SkiaSharp view controls and understand which Tizen path is GPU-backed."
+---
+
+# Render with SkiaSharp views on Tizen
+
+The `SkiaSharp.Views` package contains separate control families for ElmSharp and Tizen NUI.
+
+## ElmSharp controls
+
+The `SkiaSharp.Views.Tizen` namespace provides:
+
+| Control | Rendering path | Paint event |
+| --- | --- | --- |
+| `SKCanvasView` | CPU raster | `SKPaintSurfaceEventArgs` |
+| `SKGLSurfaceView` | Ganesh over OpenGL ES | `SKPaintGLSurfaceEventArgs` |
+
+The GPU control creates an Evas GL context, tries OpenGL ES 3 before falling back to OpenGL ES 2 or the platform default, and exposes its `GRContext`.
+
+## NUI controls
+
+The `SkiaSharp.Views.Tizen.NUI` namespace also provides `SKCanvasView` and `SKGLSurfaceView`. Both NUI controls expose `SKPaintSurfaceEventArgs` and draw through memory-backed `SKSurface` instances before presenting through NUI. The NUI `SKGLSurfaceView` is therefore not the same Ganesh API as the ElmSharp control with the same class name.
+
+Choose the namespace that matches the application's Tizen UI framework, and use the event argument type supplied by that control rather than assuming it from the class name.
+
+## Invalidate and dispose
+
+Create Tizen controls on the main thread. Call `Invalidate()` to request a new frame after state changes. The control owns its surface and native presentation resources; drawing code owns and disposes the paints, paths, images, and other objects it creates.
+
+The current `SkiaSharp.Views.Maui.Controls` package does not target Tizen. Use the native Tizen controls directly.
+
+## Related links
+
+- [Choose a SkiaSharp view](index.md)
+- [Raster surfaces](../raster/index.md)
+- [Ganesh with OpenGL](../ganesh/opengl.md)
diff --git a/documentation/docfx/guides/surfaces/views/uno.md b/documentation/docfx/guides/surfaces/views/uno.md
new file mode 100644
index 000000000000..1a9f1b1e1e16
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/views/uno.md
@@ -0,0 +1,56 @@
+---
+title: "Render with SkiaSharp views in Uno Platform"
+description: "Use SKXamlCanvas and SKSwapChainPanel in Uno Platform while accounting for target-specific GPU support."
+---
+
+# Render with SkiaSharp views in Uno Platform
+
+The `SkiaSharp.Views.Uno.WinUI` package provides WinUI-shaped controls in the `SkiaSharp.Views.Windows` namespace:
+
+| Control | Rendering path | Paint event |
+| --- | --- | --- |
+| `SKXamlCanvas` | CPU raster | `SKPaintSurfaceEventArgs` |
+| `SKSwapChainPanel` | Ganesh where a GL or WebGL implementation is available | `SKPaintGLSurfaceEventArgs` |
+
+Use `SKXamlCanvas` as the portable default. `SKSwapChainPanel` support depends on the Uno runtime, so do not select it solely because the type is present.
+
+## Add a control
+
+```xml
+
+
+
+```
+
+Call `Invalidate()` when the scene changes. `SKSwapChainPanel` also has an `EnableRenderLoop` property for continuous rendering.
+
+## Handle runtime differences
+
+| Target | `SKSwapChainPanel` implementation |
+| --- | --- |
+| Windows | The ANGLE-backed control from `SkiaSharp.Views.WinUI` |
+| WebAssembly | Ganesh over WebGL |
+| Android | Ganesh over OpenGL ES in an `SKGLTextureView` |
+| iOS | Ganesh over OpenGL ES in an `SKGLView` |
+| macOS | Ganesh over OpenGL in an `SKGLView` |
+| Mac Catalyst | Unsupported |
+| Uno Skia renderer | Unsupported |
+
+The iOS implementation uses the native `SKGLView` compatibility path, which Apple obsoleted starting with iOS 12. Prefer `SKXamlCanvas` on iOS unless your application specifically needs that OpenGL ES path.
+
+On Mac Catalyst and the Uno Skia-renderer runtime, the default `RaiseOnUnsupported` value causes construction or use of `SKSwapChainPanel` to throw `NotSupportedException`.
+
+Setting `RaiseOnUnsupported` to `false` suppresses the exception but does not create a working GPU renderer. Choose `SKXamlCanvas` or another runtime-native drawing integration as the fallback.
+
+Verify the GPU control on every Uno target. A control that works in the browser or on Android is not evidence that it works on another runtime.
+
+## Related links
+
+- [Choose a SkiaSharp view](index.md)
+- [Windows views](windows.md)
+- [Apple platform views](apple.md)
+- [Blazor WebAssembly views](blazor.md)
+- [Ganesh with OpenGL](../ganesh/opengl.md)
diff --git a/documentation/docfx/guides/surfaces/views/windows.md b/documentation/docfx/guides/surfaces/views/windows.md
new file mode 100644
index 000000000000..a59dc0e232eb
--- /dev/null
+++ b/documentation/docfx/guides/surfaces/views/windows.md
@@ -0,0 +1,68 @@
+---
+title: "Render with SkiaSharp views on Windows"
+description: "Choose SkiaSharp raster or GPU controls for WPF, Windows Forms, and WinUI 3."
+---
+
+# Render with SkiaSharp views on Windows
+
+SkiaSharp ships separate view packages for the major Windows UI frameworks:
+
+| UI framework | Package | Raster control | GPU control |
+| --- | --- | --- | --- |
+| WPF | `SkiaSharp.Views.WPF` | `SKElement` | `SKGLElement` |
+| Windows Forms | `SkiaSharp.Views.WindowsForms` | `SKControl` | `SKGLControl` |
+| WinUI 3 | `SkiaSharp.Views.WinUI` | `SKXamlCanvas` | `SKSwapChainPanel` |
+
+The raster controls raise `SKPaintSurfaceEventArgs`. The GPU controls raise `SKPaintGLSurfaceEventArgs`.
+
+## WPF
+
+Add the WPF namespace and choose `SKElement` or `SKGLElement`:
+
+```xml
+
+
+
+```
+
+`SKElement` renders through a `WriteableBitmap`. `SKGLElement` creates a Ganesh OpenGL context through OpenTK. Use `InvalidateVisual()` to request another WPF frame.
+
+## Windows Forms
+
+Create `SKControl` for raster drawing or `SKGLControl` for Ganesh OpenGL:
+
+```csharp
+using System.Windows.Forms;
+using SkiaSharp.Views.Desktop;
+
+var skiaControl = new SKControl { Dock = DockStyle.Fill };
+skiaControl.PaintSurface += OnPaintSurface;
+Controls.Add(skiaControl);
+```
+
+Use the normal Windows Forms invalidation and lifetime APIs. The control owns the per-frame surface; application code owns any drawing resources it creates.
+
+## WinUI 3
+
+The WinUI controls use the `SkiaSharp.Views.Windows` namespace:
+
+```xml
+
+
+
+```
+
+`SKXamlCanvas` presents raster pixels through a `WriteableBitmap`. `SKSwapChainPanel` uses Ganesh over ANGLE/OpenGL ES. Call the control's `Invalidate()` method for on-demand rendering; enable its render loop only for continuous animation.
+
+## Related links
+
+- [Choose a SkiaSharp view](index.md)
+- [.NET MAUI views](maui.md)
+- [Uno Platform views](uno.md)
+- [Ganesh with OpenGL](../ganesh/opengl.md)