Skip to content

Dev/graphite backend#3968

Draft
ramezgerges wants to merge 69 commits into
mono:mainfrom
ramezgerges:dev/graphite-backend
Draft

Dev/graphite backend#3968
ramezgerges wants to merge 69 commits into
mono:mainfrom
ramezgerges:dev/graphite-backend

Conversation

@ramezgerges

@ramezgerges ramezgerges commented May 12, 2026

Copy link
Copy Markdown
Contributor

Description of Change

Bugs Fixed

  • Fixes #

API Changes

Graphite backend support

Wraps Skia's new Graphite GPU backend (Vulkan / Metal / Dawn-WebGPU) in SkiaSharp. Depends on the C-shim PR in the skia submodule: mono/skia#236.

Graphite is Skia's next-generation GPU backend. Unlike Ganesh's auto-flushing draw stream, it uses an explicit record → snap → submit model and a per-thread Recorder. The public surface mirrors that shape.

What's new on the managed side

9 new types under binding/SkiaSharp/Gpu/Graphite/:

Type Wraps
SKGraphiteContext skgpu::graphite::Context (+ static CreateVulkan/Metal/Dawn factories, IsBackendAvailable(...))
SKGraphiteRecorder skgpu::graphite::Recorder
SKGraphiteRecording skgpu::graphite::Recording
SKGraphiteBackendTexture skgpu::graphite::BackendTexture
SKGraphiteTextureInfo skgpu::graphite::TextureInfo (+ per-backend factories)
SKGraphiteImageProvider callback-based hook into ContextOptions::fImageProvider
SKGraphiteVkBackendContext VulkanBackendContext (instance / device / queue / extensions / vkGetProcAddr)
SKGraphiteMtlBackendContext MtlBackendContext (device + queue)
SKGraphiteDawnBackendContext DawnBackendContext (instance / device / queue)

6 enums via codegen: SKGraphiteBackend, SKGraphiteInsertStatus, SKGraphiteMarkFrameBoundary, SKGraphiteRescaleGamma, SKGraphiteRescaleMode, SKGraphiteSyncToCpu.

Factory overloads on existing types (additive only; no signature changes — ABI is preserved):

  • SKImage.FromTexture(SKGraphiteRecorder, SKGraphiteBackendTexture, …)
  • SKImage.ToTextureImage(SKGraphiteRecorder, …)
  • SKSurface.Create(SKGraphiteRecorder, SKImageInfo, …)
  • SKSurface.Create(SKGraphiteRecorder, SKGraphiteBackendTexture, …)

SkiaApi.generated.cs is regenerated against the new C headers — no hand-edits to generated files.

Canonical render loop

using var ctx = SKGraphiteContext.CreateVulkan(backendCtx);
using var recorder = ctx.CreateRecorder();

using var surface = SKSurface.Create(recorder, new SKImageInfo(W, H, ...));
surface.Canvas.DrawRoundRect(...);

using var recording = recorder.Snap();
ctx.InsertRecording(recording);
ctx.Submit(); // optionally with SyncToCpu = true

// surface.ReadPixels(...) or surface.AsyncRescaleAndReadPixels(...)

Build-flag plumbing

SUPPORT_GRAPHITE=true is now the default on every platform: native/{linux,windows,macos,ios,tvos,android,wasm}/build.cake. Each cake file passes the appropriate flags into the GN invocation (skia_enable_graphite, plus per-backend skia_use_dawn / skia_use_vulkan / skia_use_metal). The WASM build additionally flips is_canvaskit=true and skia_use_webgpu=true.

After this PR merges, dotnet cake --target=externals-{platform} produces a libSkiaSharp with Graphite linked in. Pre-built natives downloaded via externals-download will include Graphite once the next CI run publishes them.

Tests

Three layers, all run in CI:

  1. Native smokes (tests/native/Graphite/):
    • cpp_smoke/ — drives upstream Skia directly. Proves Graphite builds + runs on Lavapipe.
    • c_smoke/ — drives the new sk_graphite_* shim only. Proves the C ABI's ownership/error contracts.
  2. Managed unit tests (tests/Tests/SkiaSharp/Graphite/):
    • GraphiteAbiSmokeTests — every new P/Invoke resolves (catches EntryPointNotFoundException regressions).
    • GraphiteSmokeTests — full managed render-and-readback.
    • GraphiteDisposalTests — disposal ordering (Context before Recorder/Recording).
    • GraphiteCoexistenceTests — Ganesh vs. Graphite produce visibly equivalent pixels for the same scene (mean per-channel Δ < 1%).
    • GraphiteBackendTextureTests — wrap a caller-allocated VkImage as a Graphite surface.
  3. Visual regression matrix (tests/Tests/SkiaSharp/Visual/) — opt-in via SKIASHARP_VISUAL_TESTS=1, runs every renderer × scene combination across raster / Ganesh / Graphite × GL / Vulkan / Metal / WebGPU on Linux / macOS / WASM / Android / iOS. Wired into Azure Pipelines as a new tests_visual stage with two jobs (Linux + macOS); failure artifacts (actual.png + diff.png) are published per cell. See documentation/dev/visual-tests.md.

Headless development

Skia's Graphite always requires a real GPU API context — there's no software fallback at the Graphite layer. For headless CI / WSL2 / GPU-less dev machines we install Mesa Lavapipe as a software Vulkan ICD: apt install mesa-vulkan-drivers vulkan-tools. WASM development uses Chromium's WebGPU (--headless=new) via Playwright. Full runbook in documentation/dev/graphite-headless.md.

Docs

  • documentation/dev/graphite.md — public-surface walkthrough + canonical render loop.
  • documentation/dev/graphite-headless.md — Lavapipe setup.
  • documentation/dev/graphite-wasm-webgpu.md — Dawn-on-WebGPU bring-up.
  • documentation/dev/visual-tests.md — visual matrix workflow.

Out of scope (follow-up PRs)

These are layered features that nothing in this PR blocks; tracking them for v2:

  • PrecompileContext / precompile/* — pipeline-cache warming.
  • PersistentPipelineStorage — client-supplied cache blob so the GPU pipeline cache survives process restarts.
  • BackendSemaphore — cross-queue / cross-context sync primitives.
  • YUVABackendTextures — multi-plane Y'UV image wrapping.
  • asyncRescaleAndReadPixels on SKImage (only SKSurface is wired in this PR).
  • InsertRecordingInfo.TargetSurface retargeting.
  • Direct3D — Skia upstream doesn't ship a Graphite/D3D backend; not on the roadmap.

Coexistence and compatibility

  • All existing GR* (Ganesh) types are unchanged. Apps can mix Ganesh and Graphite contexts in the same process; the coexistence regression test asserts they produce visually equivalent output.
  • No public signatures changed. Only additive overloads / new types / new namespace. ABI stays stable for downstream consumers.
  • Pre-existing tests pass unchanged. The new tests-visual cake target is separate from tests-netcore; non-Graphite legs of CI are untouched.

Behavioral Changes

None.

Required skia PR

mono/skia#236

PR Checklist

  • Has tests (if omitted, state reason in description)
  • Rebased on top of main at time of PR
  • Merged related skia PRs
  • Changes adhere to coding standard
  • Updated documentation

@github-actions

github-actions Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

📦 Try the packages from this PR

Warning

Do not run these scripts without first reviewing the code in this PR.

Step 1 — Download the packages

bash / macOS / Linux:

curl -fsSL https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.sh | bash -s -- 3968

PowerShell / Windows:

iex "& { $(irm https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.ps1) } 3968"

Step 2 — Add the local NuGet source

dotnet nuget add source ~/.skiasharp/hives/pr-3968/packages --name skiasharp-pr-3968
More options
Option Description
--successful-only / -SuccessfulOnly Only use successful builds
--force / -Force Overwrite previously downloaded packages
--list / -List List available artifacts without downloading
--build-id ID / -BuildId ID Download from a specific build

Or download manually from Azure Pipelines — look for the nuget artifact on the build for this PR.

Remove the source when you're done:

dotnet nuget remove source skiasharp-pr-3968

@ramezgerges ramezgerges mentioned this pull request May 12, 2026
3 tasks
@ramezgerges ramezgerges marked this pull request as draft May 12, 2026 23:27
@mattleibow

mattleibow commented May 12, 2026

Copy link
Copy Markdown
Contributor

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@ramezgerges ramezgerges force-pushed the dev/graphite-backend branch from 0468742 to 1731d7e Compare May 12, 2026 23:51
@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

5 similar comments
@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@azure-pipelines

Copy link
Copy Markdown
No pipelines are associated with this pull request.

@ramezgerges ramezgerges force-pushed the dev/graphite-backend branch from 14a22ec to 597e082 Compare May 25, 2026 17:53
Ramez Gerges and others added 15 commits May 25, 2026 21:53
Exposes Skia's Graphite GPU backend to .NET through SkiaSharp by adding
a new sk_graphite_* C-API shim (committed alongside in externals/skia)
plus matching SKGraphite* C# wrappers that P/Invoke that shim.

Scope:
* C/C++ shim: externals/skia bump to dev/graphite-c-api-shim, which
  adds include/c/sk_graphite{,_vulkan,_metal,_dawn}.h and the matching
  src/c/sk_graphite*.cpp implementations under SK_GRAPHITE / SK_VULKAN
  / SK_METAL / SK_DAWN guards.
* C# wrappers: binding/SkiaSharp/Gpu/Graphite/SKGraphite{Context,
  Recorder,Recording,BackendTexture,TextureInfo,VkBackendContext,
  MtlBackendContext,DawnBackendContext,ImageProvider}.cs. Surface and
  SKImage gain Graphite-flavored Create / ToTextureImage overloads.
* Native build: SUPPORT_GRAPHITE plumbed through native/{linux,macos,
  windows,android}/build.cake. Linux+Android cake widened to allow
  Linux as a build host (NDK r19+ ships an official linux-x86_64
  toolchain, only CI policy gated it).
* SkiaApi.generated.cs is regenerated to include the new entry points.
* libSkiaSharp.json gains the typedef/proxy descriptors the generator
  needs for the Vulkan get-proc callback and the Graphite backend
  context init struct.

Validation strategy (see specs/002-graphite-backend-support/plan.md):
three-layer smoke proves the stack end-to-end --
1. tests/native/Graphite/cpp_smoke -- C++ against libSkiaSharp.so
2. tests/native/Graphite/c_smoke   -- C against the new shim
3. tests/Tests/SkiaSharp/Graphite/ -- xUnit against the C# wrappers
Plus tests/Tests/SkiaSharp/Visual/ for golden-image comparison.

Headless validation in WSL2 uses Mesa Lavapipe (Vulkan software ICD).
documentation/dev/graphite-headless.md walks through the setup.

ABI: additive only. No existing GR*/SK* signature changes. Stubs in
the per-backend C files keep the symbol set consistent regardless of
which backends were enabled at build time (FR-013, FR-018).

Specs, contracts, and research live under specs/002-graphite-backend-support/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls in mono/skia commit that adds sk_graphite_* anchor references to
SkiaKeeper.c. Required for the macOS build, which links libSkiaSharp.dylib
against libskia.a via -lskia and relies on the keeper to keep C-API
object files from being discarded by the linker.

Without this bump, libSkiaSharp.dylib on macOS lacks
sk_graphite_image_provider_new / sk_graphite_context_make_metal / etc.,
even though they exist in libskia.a, and managed P/Invoke throws
EntryPointNotFoundException at runtime.
Adds a managed wrapper for the new sk_graphite_dawn_backend_texture_new
C entry point (skia submodule sibling commit). Used by the WASM
WebGPU renderer to wrap a canvas swap-chain texture as a Graphite
BackendTexture every frame:

    var textureHandle = jsBridge.AcquireFrameTexture(canvas);
    using var bt = SKGraphiteBackendTexture.CreateDawn(textureHandle);
    using var s = SKSurface.Create(recorder, bt, SKColorType.Rgba8888);

The wrapper does not retain or release the WGPUTexture -- callers
keep it alive for the BackendTexture's lifetime. Surfaces/Images
that wrap the BackendTexture retain it for their own lifetime.

The HarfBuzzApi.generated.cs / Resources / SceneGraph / Skottie diffs
are pure regeneration side effects (pwsh utils/generate.ps1 picked
up some new HarfBuzz typedefs hb_draw_funcs_t / hb_paint_funcs_t
and a const correctness fix on hb_buffer_append that had landed
upstream since the last regen).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plumbs an opt-in Graphite-on-WebGPU path through the WASM build:
consumers set <SkiaSharpEnableWebGpu>True</SkiaSharpEnableWebGpu>
on their WASM head csproj and get -sUSE_WEBGPU=1 + an in-runtime
JS bridge at globalThis.skiaSharpWebGpu wired into the final emcc
link. Default is False so existing GL consumers see no change.

Native build (native/wasm/build.cake):
- New SUPPORT_GRAPHITE / SUPPORT_DAWN / SUPPORT_WEBGPU env-driven
  flags. When SUPPORT_GRAPHITE=true, the GN invocation flips on
  is_canvaskit=true (so :graphite's Dawn-backed sources resolve
  <webgpu/webgpu_cpp.h> via Emscripten's bundled headers rather
  than the native-Dawn-generated webgpu_cpp.h that doesn't exist
  on Emscripten) and skia_enable_graphite / skia_use_dawn /
  skia_use_webgpu.
- The cake archive-merge step previously globbed only *.a.wasm;
  is_canvaskit=true makes Skia's GN toolchain switch static-lib
  output extensions to plain *.a, so the merge silently produced
  empty archives. Glob widened to both patterns, with libSkiaSharp.a
  (self) and libHarfBuzzSharp.* (separate consumer-facing archive)
  excluded.

Consumer-side plumbing (SkiaSharp.NativeAssets.WebAssembly.targets):
- Adds -s USE_WEBGPU=1 to EmccExtraLDFlags when SkiaSharpEnableWebGpu
  is True.
- Adds --js-library skia_wgpu_bridge.js (the new JS file packed via
  PackageFile) which installs globalThis.skiaSharpWebGpu at module
  init. The bridge exposes initAsync / acquireFrameTexture /
  releaseTexture / setSize and gives consumers access to the
  Emscripten $WebGPU mgr* tables without needing to reach the live
  Module instance (Blazor / .NET 10 WASM encapsulates it inside an
  IIFE).
- Adds -s DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=['$SkiaSharpWebGpuBridge']
  to force-include the bridge anchor (the $-prefixed library symbol
  is otherwise dead-stripped because no C code references it). The
  %24 escape is needed because MSBuild does not understand $$ as a
  literal $ in this position.
- Adds WebGPU to EmccExportedRuntimeMethod for diagnostic / devtools
  inspection (not required by skia_wgpu_bridge.js itself, which
  uses the local $WebGPU binding directly).
- POSIX path separators on the NativeFileReference glob:
  $(SkiaSharpStaticLibraryPath)/3.1.56/$(_SkiaSharpNativeBinaryType)
  /*.a. MSBuild does NOT normalize backslashes on Linux, so the
  previous Windows-style \\ separators silently produced empty
  NativeFileReference items on Linux consumers (Uno SamplesApp on
  Linux dev boxes). HarfBuzzSharp.targets gets the same fix.

skia_wgpu_bridge.js (new):
  - Async initAsync(canvasId) -> {instanceId, deviceId, queueId,
    textureId, format}: requestAdapter + requestDevice, configures
    canvas's webgpu context, registers device/queue with $WebGPU's
    mgrDevice/mgrQueue tables, returns numeric handles that Skia's
    Graphite-Dawn shim treats as raw WGPU* on Emscripten.
  - Emscripten 3.1.56's $WebGPU has no mgrInstance (newer versions
    added it); the bridge falls back to wgpuCreateInstance(0) or a
    sentinel value 1 (Skia's DawnBackendContext.fInstance is stored
    opaquely; any non-null value works).
  - acquireFrameTexture(canvasId, prevTextureId) is the per-frame
    hot path -- releases the previous frame's texture handle then
    grabs the current swap-chain texture.

externals/skia bump pulls in the sibling sk_graphite_dawn_backend_texture_new
shim + Emscripten guards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds documentation/dev/graphite-wasm-webgpu.md covering the
SkiaSharp-side path that the earlier three commits delivered:

* Consumer setup -- one-line opt-in via SkiaSharpEnableWebGpu,
  example Blazor + Uno flows.
* What globalThis.skiaSharpWebGpu exposes (initAsync /
  acquireFrameTexture / releaseTexture / setSize) and why we
  ship it as a JS library rather than expecting consumers to
  reach Module.WebGPU directly (.NET 10 encapsulates Module
  inside an IIFE).
* Non-yielding mode trade-offs and the Context-destruction
  warning that follows from disposing under it.
* SKGraphiteImageProvider plumbing -- without it, every
  DrawImage of a raster SkImage is silently dropped.
* Build internals for maintainers: is_canvaskit GN switch,
  archive-merge glob, emscripten version pinning.
* Headless test recipe with Chromium + SwiftShader Vulkan.
* Troubleshooting table cross-referencing every gotcha we hit
  during bring-up (wasm-ld undefined symbols, MSBuild backslash
  globs on Linux, $-escape for DEFAULT_LIBRARY_FUNCS_TO_INCLUDE,
  canvas-context contention).

Indexed under a new "Graphite Backend" section in
documentation/dev/README.md alongside the existing graphite.md
and graphite-headless.md (which were not previously linked).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EmccExportedRuntimeMethod=WebGPU was added speculatively for devtools
inspection but the skia_wgpu_bridge.js library uses the local \$WebGPU
binding directly. Dropping it shrinks the build's exports table.
Commit 608a3bc regenerated HarfBuzz bindings as a side effect of
the SKGraphiteBackendTexture.CreateDawn work. The regen emitted 14
new partial-method declarations for hb_paint_*_func_t proxies whose
implementation halves were never authored, leaving the build broken
(CS8795).

Wiring full hb_paint_funcs_t support — including delegate types and
a PaintFunctions class — is unrelated to the Graphite work that
commit was actually doing, and was never intended to land. Revert
HarfBuzzApi.generated.cs to main's state; a future generator run
+ proper paint_funcs implementation can land independently.
Splits the test logic from the transport: scenes describe WHAT to
draw (deterministic Action<SKCanvas> identified by name), renderers
describe HOW to reach a backend (raster, Vulkan, Metal, …). One
xUnit theory now fans across every compatible (renderer × scene)
combination at discovery time, scaling with neither N×M boilerplate
nor per-platform divergence.

Shape:

  tests/Tests/SkiaSharp/Visual/
    Scenes/        ISkiaScene + SceneCatalog + 3 sample scenes
    Renderers/     IRenderer + RendererCatalog + Raster/GaneshVk/
                    GraphiteVk/GraphiteMetal renderers
    Tests/         VisualMatrixTests (the one matrix theory)
    Goldens/
      _shared/     canonical golden per scene (used by default)
      raster/      per-renderer override (raster diverges from GPU)

Golden lookup: {renderer.Name}/{scene}.png → _shared/{scene}.png
fallback. Update mode (SKIASHARP_UPDATE_GOLDENS=1) writes to _shared/
by default; SKIASHARP_GOLDEN_SCOPE=renderer pins a per-renderer
divergence. Three new shared goldens + two raster overrides replace
the previous nine per-setup PNGs.

GraphiteDisposalTests and GraphiteCoexistenceTests switch from the
deleted Setup classes to VulkanLoader.Shared / the new renderers
directly. Behavior unchanged.
Mirror of the Vulkan path for GL: dlopen libEGL.so.1, enumerate EGL
devices via EGL_EXT_platform_device, prefer the Mesa software device
(EGL_MESA_device_software), bring up a surfaceless GL context via
EGL_KHR_surfaceless_context. No display server contacted; pixels
land deterministically through Mesa llvmpipe.

GL contexts are thread-affine, so the renderer owns a dedicated
worker thread that calls eglMakeCurrent once and pumps a work queue
from RenderAsync. Per-render GRContext + SKSurface allocation
matches the Vulkan/Metal lifetime model and keeps the assembly-end
GarbageCleanupFixture clean.

Matrix coverage on Linux is now: raster, ganesh-vulkan, graphite-vulkan,
and ganesh-gl — 11/11 cells green against _shared/ goldens within
the MaxChannelDelta=2 tolerance.
Two new cells on Linux CI: wasm-raster (CPU SkiaSharp inside the
WASM runtime) and wasm-graphite-dawn (Skia Graphite on WebGPU via
Emscripten Dawn). Both go through the same browser session — one
Chromium tab, three in-page renderers dispatched by name.

Pieces:

* native/wasm/skia_wgpu_bridge.js — extended with three offscreen
  helpers (createOffscreenTexture, readTextureRgbaAsync,
  releaseOffscreenTexture) and a canvas-less initOffscreenAsync.
  WebGPU device + queue are acquired without a canvas; renders go
  into a GPUTexture we own; readback is JS-side
  copyTextureToBuffer + mapAsync, NOT Skia's ctx.ReadPixels.
  That last bit is critical: in Skia's non-yielding mode (we don't
  link -sASYNCIFY) ctx.ReadPixels deadlocks waiting on a mapAsync
  that needs the JS event loop to tick, which can't happen while
  C# is on a synchronous P/Invoke stack.

* tests/Hosts/RenderHost.Wasm/ — barebones Microsoft.NET.Sdk.
  WebAssembly project (no Blazor, no Uno). Program.Main blocks
  forever so the runtime stays alive servicing the
  [JSExport] RenderSceneAsync entry point. Scene .cs files are
  Compile-Included from tests/Tests/SkiaSharp/Visual/Scenes/, so
  a scene change updates the in-process AND browser-side cells
  in one edit. SKGraphiteContext + Recorder are kept alive for the
  WASM process lifetime — disposing them in non-yielding mode
  fatally asserts "all GPU work must be finished".

* tests/Tests/SkiaSharp/Visual/Renderers/WasmHostSession.cs +
  WasmRenderers.cs — Microsoft.Playwright-driven transport with a
  process-singleton browser + page. WasmRasterRenderer and
  WasmGraphiteDawnRenderer are IRenderer proxies; the renderer
  name is the dispatch key on the JS side. WasmGaneshGlEsRenderer
  is sketched but not wired (Ganesh-on-WebGL2 needs a separate
  JS-library shim for emscripten_webgl_create_context, deferred).

* Goldens: wasm-raster's CPU output diverges from the GPU
  _shared baseline the same way desktop raster does — recorded
  as per-renderer overrides (5-44 channel delta vs MaxChannelDelta=2).
  wasm-graphite-dawn matches _shared bit-exactly on
  RedRoundedRectOnWhite; DiagonalLines and GpuOnly_FilledCircle
  drift by 5 channels (104-1764 pixels) under SwiftShader-WebGPU
  vs Lavapipe-Vulkan — recorded as overrides until the tolerance
  policy is revisited.

Matrix on Linux is now 16/16 active cells green, 3 graphite-metal
cells skip with a clean message.
Sibling JS-library shim skia_gl_bridge.js bootstraps an
OffscreenCanvas + WebGL2 context and registers it with emscripten's
$GL runtime. Living in tests/Hosts/RenderHost.Wasm/ because
offscreen GL is test-only — production Uno/Blazor consumers target
a real canvas through the regular emscripten WebGL plumbing.

WebGL is synchronous on the readback path (glReadPixels blocks
until queued work completes), so unlike the WebGPU cell we don't
need the bridge to do an async copy + mapAsync. Per-render
GRContext matches the Linux ganesh-gl model and keeps things tidy.

Matrix on Linux is now 19/19 active cells green:
  raster ×2, ganesh-gl ×3, ganesh-vulkan ×3, graphite-vulkan ×3,
  wasm-raster ×2, wasm-ganesh-gles ×3, wasm-graphite-dawn ×3
Plus 3 graphite-metal cells that skip cleanly (no Apple host).

Two scenes through wasm-ganesh-gles match the _shared/ GPU golden
exactly within MaxChannelDelta=2; DiagonalLines drifts by 6
channels (28 pixels) under SwiftShader vs Lavapipe and gets a
per-renderer override.
Three things:

1. WglLoader.cs — Windows equivalent of EglLoader. Bootstraps a
   HWND_MESSAGE window with the built-in STATIC class, gets a DC,
   sets a minimal RGBA8/D24/S8 pixel format, creates a dummy
   wglCreateContext, queries wglCreateContextAttribsARB through it,
   then creates a real GL 3.3 core context. The window is never
   shown; Skia renders into its own FBO. GetProc falls back from
   wglGetProcAddress (returns sentinels for legacy entry points) to
   GetProcAddress on opengl32.dll so glClear / glBegin / etc.
   resolve correctly.

2. GaneshGlRenderer dispatches the platform loader: EglLoader on
   Linux, WglLoader on Windows. One renderer name (`ganesh-gl`);
   exactly one loader is available per host, so the catalog stays
   non-duplicate and the cell joins the matrix on both platforms
   without forking the goldens.

3. VulkanLoader resolver — `[DllImport("vulkan")]` on Windows would
   look for vulkan.dll which doesn't exist; the real Vulkan loader
   ships as vulkan-1.dll. Install a SetDllImportResolver in the
   static cctor that maps the name (Windows-only — Linux's loader
   handles libvulkan.so → libvulkan.so.1 natively).

Plus tests/Tests/SkiaSharp/Visual/README.md documenting the matrix,
how to run, prerequisites per platform, the SKIASHARP_UPDATE_GOLDENS
+ SKIASHARP_GOLDEN_SCOPE conventions, and the rationale for each
per-renderer override that exists today.

Tested on Linux (19/19 active cells stay green after the refactor;
3 graphite-metal cells skip cleanly). Windows-specific paths need
real-machine verification.
Flips the default from "false" to "true" in the per-platform cake
files that have the switch (linux, windows, macos, wasm, android),
and adds skia_enable_graphite=true unconditionally to the two that
hardcode their GN args (ios, tvos — maccatalyst delegates to ios).
The env-var / cake-argument escape hatch stays in place: set
SUPPORT_GRAPHITE=false to opt out of the new default.

Vulkan was already on-by-default on the targets that support it; Dawn
stays off everywhere except WASM (where it follows SUPPORT_GRAPHITE
because it's the only Graphite backend the WebAssembly build ships).
Metal-platform builds (ios/tvos/maccatalyst, and macos via Metal)
pick up Graphite-on-Metal automatically.

Documentation: the three graphite-*.md files dropped their
'SUPPORT_GRAPHITE=true \\' lines from the bring-up snippets — those
are now no-ops — and graphite.md's index table notes the new defaults.

The actual libSkiaSharp binaries don't change shape from this commit;
the next time someone runs dotnet cake --target=externals-{platform}
they'll get Graphite + Vulkan built in without needing the env var.
Windows-only side effect of the test project's multi-target: net48
also tries to compile WasmHostSession.cs + WasmRenderers.cs, but
Microsoft.Playwright + ValueTask + IAsyncDisposable aren't available
there. Exclude the two files from the net48 build — the matrix
theory still runs against the same renderer/scene set on net48, just
without the wasm-* cells.
Replace OperatingSystem.IsX() (.NET 5+) with
RuntimeInformation.IsOSPlatform(OSPlatform.X) — same intent,
available on every TFM the test assembly targets including net48.

VulkanLoader's SetDllImportResolver path is gated behind
#if NET5_0_OR_GREATER. The resolver is purely a Windows-vulkan-1.dll
mapping; net48-on-Windows users hitting that cell would see an
unresolved-DLL failure, which matches the pre-matrix baseline.

WglLoader's opengl32.dll-fallback path swaps NativeLibrary for plain
kernel32 LoadLibraryW + GetProcAddress P/Invoke. Works on every TFM,
no functional change on net10.0+.

Linux net10.0 matrix re-verified — 19/19 cells still green.
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

13 similar comments
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@mattleibow mattleibow added the partner/unoplatform Issues and PRs that are/should being looked at or worked on by the Uno Platform partners. label Jun 15, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

10 similar comments
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community ✨ partner/unoplatform Issues and PRs that are/should being looked at or worked on by the Uno Platform partners.

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants