Skip to content

Dev/graphite backend#236

Open
ramezgerges wants to merge 14 commits into
mono:skiasharpfrom
ramezgerges:dev/graphite-backend
Open

Dev/graphite backend#236
ramezgerges wants to merge 14 commits into
mono:skiasharpfrom
ramezgerges:dev/graphite-backend

Conversation

@ramezgerges

@ramezgerges ramezgerges commented May 12, 2026

Copy link
Copy Markdown

Description of Change

SkiaSharp Issue

Related to https://github.com/mono/SkiaSharp/issues/

API Changes

Expose skgpu::graphite::* through the C shim

Adds a C ABI for Skia's Graphite backend so that downstream language bindings (in our case mono/SkiaSharp#3968) can wrap it without touching the C++ types directly.

What's in

Four new headers + their .cpp implementations under include/c/ and src/c/:

Header Public entry points Scope
sk_graphite.h 44 Context, Recorder, Recording, BackendTexture, TextureInfo, ImageProvider, Surface::RenderTarget/WrapBackendTexture, Image::WrapTexture/TextureFromImage, asyncRescaleAndReadPixels on surfaces, context budgets/cleanup
sk_graphite_vulkan.h 5 VulkanBackendContext, ContextFactory::MakeVulkan, VulkanTextureInfo, VulkanBackendTexture factories
sk_graphite_metal.h 4 MtlBackendContext, ContextFactory::MakeMetal, MtlBackendTexture
sk_graphite_dawn.h 4 DawnBackendContext, ContextFactory::MakeDawn, DawnBackendTexture

57 total SK_C_API symbols. The core file (sk_graphite.{h,cpp}) is guarded by SK_GRAPHITE; per-backend files are additionally guarded by SK_VULKAN / SK_METAL / SK_DAWN so platforms with one backend disabled still link clean.

Conventions

Matches the rest of the C shim in include/c/:

  • Naming: sk_<type>_<action> (sk_graphite_recorder_snap, sk_graphite_context_make_recorder, etc.).
  • Ownership: factories return owning handles, *_delete releases them, raw _t* parameters are non-owning. Same rules as sk_canvas_* / sk_image_*.
  • Errors: factories return nullptr on failure; boolean operations return false. No exceptions across the ABI.
  • Each backend's Make* factory returns a sk_graphite_context_t* so callers can stay backend-agnostic once the context is up.

Test coverage

End-to-end smokes live in the consuming PR (mono/SkiaSharp#3968) — both a standalone C++ test that drives the upstream Skia API directly (proves Skia's Graphite works on the build agent) and a C-only test that drives this shim (proves the ownership/error contracts above hold without any managed code in the picture). Lavapipe is the canonical software ICD for headless CI.

Out of scope (deferred to a follow-up)

The Graphite surface is large; this PR covers the parts SkiaSharp ships in v1. Not included:

  • PrecompileContext / precompile/* — pipeline-cache warming.
  • PersistentPipelineStorage — client-supplied cache blob.
  • BackendSemaphore — cross-queue / cross-context sync.
  • YUVABackendTextures — multi-plane Y'UV wrapping.
  • asyncRescaleAndReadPixels{,YUV420,YUVA420} on SkImage (only the SkSurface variant is wired).
  • InsertRecordingInfo::fTargetSurface retargeting.

These can be added incrementally — none of them are blocked by anything in this PR.

Behavioral Changes

None.

Required SkiaSharp PR

Requires mono/SkiaSharp#3968

PR Checklist

  • Rebased on top of skiasharp at time of PR
  • Changes adhere to coding standard
  • Updated documentation

Ramez Gerges and others added 3 commits May 12, 2026 23:47
Adds an sk_graphite_* C ABI living inside :core so SkiaSharp's P/Invoke
layer can drive Graphite without binding directly to C++. Mirrors the
existing sk_gr_context_* / gr_* shim shape:

  include/c/sk_graphite.h             -- core: context, recorder, recording,
                                          surface, image, backend texture,
                                          texture info, async readback,
                                          ImageProvider hook.
  include/c/sk_graphite_vulkan.h      -- Vulkan-only entry points
  include/c/sk_graphite_metal.h       -- Metal-only entry points
  include/c/sk_graphite_dawn.h        -- Dawn-only entry points
  src/c/sk_graphite{,_vulkan,_metal,_dawn}.cpp

The implementations are wrapped in `#if defined(SK_GRAPHITE)` etc., with
no-op stubs in the else branch so the C ABI surface is consistent
whether or not the backend is built.

BUILD.gn / gn/core.gni: the :graphite target's all_dependent_configs
apply SK_GRAPHITE/SK_DAWN to dependents but not to :core itself. The
new shim cpp files live in :core, so add those defines explicitly when
the respective skia_enable_* args are set; otherwise the bodies
collapse to the stubs and never link the real symbols.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
macOS builds libSkiaSharp.dylib in two steps -- GN/ninja produces
libskia.a, then xcodebuild compiles src/xamarin/*.cpp and links the
dylib against libskia.a via -lskia. The macOS linker pulls object
files from libskia.a lazily: only those that resolve undefined symbol
references in the linkage roots get included.

SkiaKeeper.c is the existing anchor file -- it holds one void*
reference per C-API module so the linker keeps the whole object file
alive in the final dylib. The new sk_graphite_*.cpp shim files weren't
on the anchor list, so libskia.a contained them but the dylib didn't
export their symbols.

Add one anchor per shim cpp. The per-backend cpps have stub bodies in
their !SK_GRAPHITE / !SK_VULKAN / !SK_METAL / !SK_DAWN branches, so
anchoring is safe when those backends are disabled at build time.
Threads Skia's Graphite/Dawn backend through Emscripten's bundled
WebGPU shim (-sUSE_WEBGPU=1) so it links cleanly into libSkiaSharp.a
under is_canvaskit=true. Five surgical edits:

* include/c/sk_graphite_dawn.h + src/c/sk_graphite_dawn.cpp -- new
  sk_graphite_dawn_backend_texture_new C entry point that wraps a
  caller-supplied WGPUTexture (e.g. canvas.getContext('webgpu')
  .getCurrentTexture()) via BackendTextures::MakeDawn. The wrapper
  does not retain or release the WGPUTexture; any SkSurface/SkImage
  that wraps it will retain for its own lifetime.

* src/gpu/graphite/dawn/DawnCaps.cpp -- guard YCbCr/biplanar texture
  formats (R8BG8Biplanar420Unorm, R10X6BG10X6Biplanar420Unorm,
  R8BG8A8Triplanar420Unorm, OpaqueYCbCrAndroid, DualSourceBlending,
  TextureFormatsTier1) under #if !defined(__EMSCRIPTEN__). These
  wgpu::TextureFormat enum values do not exist in Emscripten's
  bundled webgpu_cpp.h and would fail to compile.

* src/gpu/graphite/dawn/DawnBuffer.cpp -- replace SKGPU_LOG(...) macro
  with explicit SKGPU_LOG_D / SKGPU_LOG_E; Emscripten's preprocessor
  rejects the macro form in this position.

* third_party/dawn/BUILD.gn -- skip the native Dawn CMake build AND
  the libdawn_combined.a link step when is_canvaskit=true. On
  Emscripten the wgpu_* symbols are provided by -sUSE_WEBGPU=1 at
  the consumer's final link step; building native Dawn for WASM is
  both unnecessary and breaks (the Dawn CMake build assumes desktop
  toolchains).

Build prerequisite (out-of-tree): externals/dawn requires one extra
#include in third_party/externals/dawn/include/tint/tint.h --
'src/tint/api/common/bindings.h'. Dawn isn't a git submodule of skia
(it is checked out via DEPS), so we cannot land that patch here.
Apply it separately if a fresh DEPS sync is needed and Dawn pre-m146.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ramezgerges ramezgerges force-pushed the dev/graphite-backend branch from 2e90471 to bf5e889 Compare May 12, 2026 23:51
ramezgerges and others added 11 commits May 13, 2026 15:14
The five Graphite C-API types (Context, Recorder, Recording,
BackendTexture, TextureInfo) were hand-rolled with three or four
overload-incomplete static-inline reinterpret_cast helpers per type
in sk_graphite.cpp. Move them into sk_types_priv.h under the same
DEF_MAP_WITH_NS pattern that already handles GrDirectContext /
VulkanYcbcrConversionInfo / etc.

Each invocation generates 8 const/non-const ref+ptr As/To overloads,
gated by SK_GRAPHITE so non-Graphite builds skip both the includes
and the helpers. The per-backend cpps (sk_graphite_vulkan/metal/dawn)
now use the generated ToGraphiteContext / ToGraphiteBackendTexture /
ToGraphiteTextureInfo instead of raw reinterpret_cast.

Net: -10 source lines, consistent convention with the rest of the
C-API map declarations.
Replace the misleading comment ("SKGPU_LOG unavailable on Emscripten")
with the actual reason: SKGPU_LOG expands via SKIA_LOG to
`if constexpr (priority <= ...)`, which requires a compile-time
constant. The call site passes a runtime variable set by the switch
above, so the original code fails to compile.

Only surfaces in WASM because the function is gated by
__EMSCRIPTEN__; upstream doesn't build Graphite-on-WASM, so the bug
sits there without anyone noticing.
* DawnCaps.cpp + third_party/dawn/BUILD.gn — hoist explanatory
  comments above the preprocessor gate and prefix each with
  "mono/skia:" so grep surfaces every local upstream patch.
* SkiaKeeper.c — drop the "mono/skia:" prefix from the Graphite
  comments. The whole src/xamarin/ directory is mono-only territory,
  so the marker adds nothing.
The previous block explained what the anchors are doing, but the
file-level convention (one anchor per .cpp to keep the linker from
pruning archive members) already covers that. The Graphite includes
and four anchors below speak for themselves.
Three related fixes to remove silent fallback returns from the
sk_graphite_* C shim:

* Closed-enum exhaustiveness. ToInsertStatus and the sampleCount()
  getter on sk_graphite_texture_info handled every Skia value but
  still ended in a `return X;` fallback. Drop the unreachable
  default values; end the switches with SkUNREACHABLE so the
  compiler can enforce exhaustiveness via -Wswitch-enum and any
  future Skia milestone that grows the enum produces a compile
  error here instead of a runtime mis-mapping.

* Open-enum honesty for BackendApi. skgpu::BackendApi has kMock and
  kUnsupported values we don't expose on the public C ABI. The four
  *_get_backend functions used to silently map them to kVulkan,
  which is a lie. Add UNKNOWN_SK_GRAPHITE_BACKEND = -1 to the C
  enum, list kMock/kUnsupported explicitly, drop the `default:`
  blanket case, and end with SkUNREACHABLE. Null-handle early
  returns also use UNKNOWN now rather than guessing Vulkan.

* Bool-returning sample-count validator. ToGraphiteSampleCount used
  to silently clamp invalid input to k1, which means the caller had
  no way to know they had passed a bad value. Switch to a
  bool-returning out-parameter form and propagate the failure up
  through sk_graphite_make_context_options (also bool-returning out
  param) and the per-backend ContextFactory factories
  (sk_graphite_context_make_{vulkan,metal,dawn}), which now return
  nullptr when the C options struct is invalid. The managed binding
  validates options up front and throws ArgumentException with a
  clear message; the null path is a defense-in-depth backstop.
The shim was double-checking input that the managed binding already
controls: null handles, null info pointers, negative budgets, zero
or negative widths, out-of-range plane indices. Move that work to
managed where the failure mode is a proper ArgumentException with a
diagnostic message, and let the C shim be a straight 1-to-1 mapping
to skgpu::graphite::*.

Kept in the shim:
  * if (!opts) return true; in sk_graphite_make_context_options — a
    documented "null = use Skia defaults" semantic.
  * fGpuBudgetInBytes >= 0 and recorderBudgetBytes >= 0 sentinels —
    documented "negative = use Skia default" behaviour mirroring
    the C struct comment.
  * if (!bt.isValid()) return nullptr; — validates Skia's own return
    value, not a defensive null check.
  * Vulkan memory-allocator fallback in sk_graphite_context_make_vulkan
    — real setup logic, not a guard.
  * FfiImageProvider's findOrCreate boundary nulls — Skia is calling
    into us through a C++ vtable; we can't trust the inputs.
The plain make_recorder entry was a forwarder to the _with_image_provider
variant with a null third argument. Two C entry points, two C# P/Invokes,
two SkiaApi declarations, two stub branches in the !SK_GRAPHITE block —
all for what's a single C++ call. Collapse to one entry that takes the
optional image-provider directly; callers that don't want one pass null.
…lags

Five sites in the Graphite C ABI were carrying a binary flag as
int32_t with a "0 = no, 1 = yes" comment instead of the C bool type
used elsewhere in the same headers (e.g. sk_graphite_backend_is_-
available, sk_graphite_context_is_device_lost, fProtectedContext):

  * sk_graphite_image_make_texture(... mipmapped)
  * sk_graphite_surface_make_render_target(... mipmapped, ...)
  * sk_graphite_image_provider_proc_t callback (mipmapped)
  * sk_graphite_texture_info_get_mipmapped return
  * sk_graphite_vk_texture_info_t::fMipmapped
  * sk_graphite_dawn_backend_context_init_t::fNonYielding

Switch them all to bool to match the rest of the surface and drop
the "0 = no, 1 = yes" comments. Managed side gains a slightly nicer
shape (no `? 1 : 0` / `!= 0` adapters around every call), and the
binary nature of each flag is now self-documenting from the type.

The two-state SubmitInfo enums (fSync, fMarkBoundary) are kept as
distinct enums on purpose — the named YES_/NO_ constants read more
clearly at call sites than bare bools and mirror Skia's enum class.
Both sk_graphite_sync_to_cpu_t and sk_graphite_mark_frame_boundary_t
were two-value enums shaped exactly like a bool. Mirroring Skia's
`enum class SyncToCpu : bool` / `MarkFrameBoundary : bool` shape in
the C ABI never paid off — at call sites the YES_/NO_ constants are
just ceremony around a true/false. Drop both enums and make the
fields in sk_graphite_submit_info_t plain `bool`. Same translation
to Skia's enum class happens in sk_graphite_context_submit, only now
the input is a clean boolean instead of an enum-equals-named-constant
expression.
…ints

Drop the opaque sk_graphite_{vk,mtl,dawn}_backend_context_t handles plus
their paired _new / _delete functions. sk_graphite_context_make_* now take
const _init_t* directly. The CFRetain (Metal) and AddRef (Dawn) work that
the wrappers used to do moves inline into _make_*, bracketed by the
ContextFactory::Make* call so locals release on scope exit whether Skia
took its own refs or not. The Vulkan dispatch lambda was already
capturing fGetProc + fGetProcUserData by value, so the wrapper was
purely cosmetic on that path.

Net: 6 fewer exported symbols, ~60 lines removed, one consistent
managed lifecycle across all three backends.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The DEF_MAP_WITH_NS helpers added in 70150e1 reference the C-side
typedefs sk_graphite_context_t/recorder_t/recording_t/backend_texture_t
/texture_info_t. They live in include/c/sk_graphite.h, but
sk_types_priv.h only pulled in the C++ skgpu::graphite::* headers — so
any translation unit that includes sk_types_priv.h without first
including sk_graphite.h fails to compile when SK_GRAPHITE is defined.

Xamarin TUs (sk_managedtracememorydump.cpp, sk_compatpaint.cpp, etc.)
are the affected callers and broke the WASM build (the first target
that defines SK_GRAPHITE while compiling them). Native builds masked
the issue because they don't currently set skia_enable_graphite=true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ramezgerges ramezgerges force-pushed the dev/graphite-backend branch from e1cf751 to ff92039 Compare May 25, 2026 23:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant