Skip to content

Add the SKMesh API (custom vertex-mesh drawing) - #4553

Open
ramezgerges wants to merge 8 commits into
mono:mainfrom
ramezgerges:dev/issue-3777-skmesh
Open

Add the SKMesh API (custom vertex-mesh drawing)#4553
ramezgerges wants to merge 8 commits into
mono:mainfrom
ramezgerges:dev/issue-3777-skmesh

Conversation

@ramezgerges

Copy link
Copy Markdown
Contributor

Add the SKMesh API (custom vertex-mesh drawing)

Completes #3779 and resolves #3777. Wraps Skia's SkMesh — custom vertex meshes driven
by SkSL vertex/fragment programs — as SKMesh, SKMeshSpecification, SKMeshBuilder,
SKMeshVertexBuffer, SKMeshIndexBuffer, and SKCanvas.DrawMesh, plus mesh-spec
constructors on SKRuntimeEffectUniforms/Children.

Requires the companion native C API PR mono/skia#297 (sk_mesh_*, sk_canvas_draw_mesh);
the submodule pointer in this PR references that commit.

What's in this PR

  • The original four feature commits from @mattleibow, rebased onto main with authorship
    preserved (SKMesh API, builder-pattern alignment, the deformable-grid MeshSample, and the
    CPU/raster test handling).
  • GPU render test — verifies the mesh actually rasterizes red pixels on a GPU surface, not
    just that objects construct (raster drawMesh is a no-op upstream, so construction-only tests
    passed even when nothing drew).
  • Uno 6.x browser-wasm gallery wiring — the single-project head no longer sets
    UnoRuntimeIdentifier=WebAssembly, so the native library is now linked via
    @(SkiaSharpStaticLibrary) with a dedicated st,simd binary-type property.

Two bugs fixed to make the sample actually work on WebAssembly

  1. SKMeshSpecification uniforms/children never enumerated. The uniformNames/childNames
    cache fields were initialized to Array.Empty<string>(), which defeated the ??= lazy-init
    in the Uniforms/Children properties — the enumerator was never called, so both always
    returned empty and builder.Uniforms["uImageSize"] = … always threw
    ArgumentOutOfRangeException. Fields are now nullable; added a regression test that a spec with
    a declared uniform enumerates and can be set.

  2. Custom Mesh sample OOM on WebAssembly. MeshSample is animated but didn't override
    OnUpdate. CanvasSampleBase's animation loop calls OnUpdate then schedules a Refresh
    each iteration; the default OnUpdate returns instantly, so with no override the loop spun
    with zero delay and flooded the UI task scheduler until the managed heap hit the 2 GB wasm cap
    ("GC could not allocate … for major heap section"). Now overrides OnUpdate with a ~60 FPS
    delay, like every other animated sample.

Testing

  • SKMeshTest — 28/28 pass on desktop GPU (llvmpipe via Xvfb), including the new render + uniform
    regression tests.
  • Custom Mesh sample verified headlessly (Chromium + WebGL/SwiftShader) in both the Blazor and
    Uno WebAssembly galleries: renders and stays memory-stable (previously OOM'd within ~7 s).

🤖 Generated with Claude Code

mattleibow and others added 8 commits July 25, 2026 12:45
Implements custom vertex mesh drawing with SkSL shaders, mirroring the
SKRuntimeEffect API pattern with Create/Build factories, Uniforms/Children
properties, and a builder class.

New types: SKMeshSpecification, SKMesh, SKMeshVertexBuffer, SKMeshIndexBuffer,
SKMeshBuilder, SKMeshMode, SKMeshSpecificationAttributeType/VaryingType.

C API uses a builder pattern (sk_mesh_new + setters + sk_mesh_validate)
with all P/Invoke calls ≤4 params for WASM interpreter compatibility.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- SKMeshSpecification.Build() now returns SKMeshBuilder (was SKMeshSpecification),
  matching SKRuntimeEffect.BuildShader/BuildColorFilter/BuildBlender pattern
- SKMeshBuilder.Dispose() now disposes Specification (builder owns it),
  matching SKRuntimeEffectBuilder.Dispose() which disposes Effect
- Nullable annotations on Create/ToMesh/ToMeshIndexed out parameters and
  nullable SKData/SKRuntimeEffect params — eliminates all CS8604/CS8625 warnings
- Add BuildReturnsMeshBuilder test to cover the new return type
- Update builder tests to not double-using spec (builder owns it)
- Rewrite MeshSample to use Build() factory, cache builder across frames,
  and animate with a cycling color uniform over an indexed quad mesh
- Rebase onto origin/main (skia submodule rebased onto origin/skiasharp)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the mesh sample from the Skia mesh2d demo featuring:
- NxN configurable grid with image texture (baboon.png)
- Four animated deformations: Squircle, Twirl, Wiggle, Cylinder
- Wireframe overlay toggle
- SkSL vertex/fragment shaders with UV coordinates

Also fix nullable reference type warnings introduced by rebase:
- Make GetObject(), Make(), Build(), BuildIndexed(), ToMesh(),
  ToMeshIndexed() return nullable types
- Initialize field defaults for uniformNames/childNames
- Fix redundant null-conditional on errorString

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- SkBitmapDevice::drawMesh() is a no-op in upstream Skia (GPU only)
- Updated pixel-readback tests to verify creation + no-crash on CPU
- Deferred MeshSample resource loading to first draw for WASM stability
- Used Create() instead of Build() for graceful failure handling

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The draft PR's mesh tests only asserted construction/validation and
"does not crash" on a CPU surface — where SkMesh drawing is a documented
no-op — so the feature's core purpose (rendering) was never verified.

Replace the misleadingly-named MeshRendersRedPixelWithColorShader (which
rendered nothing and asserted no pixel) with real coverage on a GPU-backed
surface:

- MeshRendersRedPixelOnGpuSurface: renders the mesh via a GL context and
  asserts the output pixels are actually red.
- DrawMeshDefaultPaintModulatesToBlack: pins the "black by default" gotcha
  (mesh colour is modulated by the paint colour; white paint is required
  for the mesh's own colour to show).
- DrawMeshOnRasterSurfaceIsNoOp: makes the CPU/raster no-op an explicit
  assertion (canvas unchanged) instead of a comment.

GPU tests skip gracefully where no GL context is available; verified here
against Mesa llvmpipe (software GL 4.5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Uno gallery's browser-wasm head could not resolve the in-tree SkiaSharp/
HarfBuzz native: IncludeNativeAssets only handled the legacy Uno WASM
(UnoRuntimeIdentifier=WebAssembly), which Uno 6.x no longer sets — it now uses
the .NET WebAssembly SDK with UnoUIRuntimeIdentifier=Skia. As a result the app
booted but threw TypeInitialization for SkiaSharp.SkiaApi (native never linked).

Uno's Skia-WASM targets link the native through the @(SkiaSharpStaticLibrary) /
@(HarfBuzzSharpStaticLibrary) items (normally set by the NativeAssets NuGet, which
the in-tree gallery excludes). Populate those items from output/native/wasm for the
IsUnoHead + browser-wasm case. A dedicated binary-type property is used (not the
shared _SkiaSharpNativeBinaryType) because Uno also sets UsingMicrosoftNETSdkWebAssembly,
whose block would otherwise clobber it to a non-SIMD variant; Uno's WASM runtime is
always SIMD.

Verified: `dotnet publish -f net10.0-browserwasm` now links libSkiaSharp.a
(3.1.56/st,simd) and the gallery boots headlessly with the Custom Mesh (SKMesh) sample.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The uniformNames/childNames cache fields were initialized to
Array.Empty<string>(), which defeated the `??=` lazy-initialization in
the Uniforms/Children properties: GetUniformNames()/GetChildrenNames()
were never called, so both properties always returned empty. As a
result, setting a declared mesh uniform by name
(builder.Uniforms["uImageSize"] = ...) always threw
ArgumentOutOfRangeException even though the native spec exposed it.

Make the fields nullable so the lazy initialization actually runs.

Adds a regression test asserting that a spec with a declared uniform
enumerates it (UniformSize/Uniforms) and can be set via the builder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MeshSample sets IsAnimated => true but did not override OnUpdate.
CanvasSampleBase's animation loop runs, for animated samples,
`while (!cancelled) { await OnUpdate(token); new Task(Refresh).Start(scheduler); }`.
The default OnUpdate returns Task.CompletedTask instantly, so with no
override the loop spun with zero delay and queued Refresh tasks to the
UI scheduler far faster than they could drain. On single-threaded
WebAssembly this floods the managed heap until it hits the 2 GB wasm
cap, aborting with "GC could not allocate ... for major heap section".

Override OnUpdate with a ~60 FPS delay, matching every other animated
sample (ShaderPlayground, ShaderCrossFade, LottiePlayer, GifPlayer).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

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 -- 4553

PowerShell / Windows:

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

Step 2 — Add the local NuGet source

dotnet nuget add source ~/.skiasharp/hives/pr-4553/packages --name skiasharp-pr-4553
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-4553

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Hey there @@ramezgerges! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@mattleibow

mattleibow commented Jul 31, 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.

2 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.

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

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[api] Add SKMesh API for custom vertex mesh drawing with SkSL shaders

2 participants