SkiaSharp

GitHub

SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google's Skia Graphics Library. It provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images.

RAW Rules

AGENTS.md

# SkiaSharp

SkiaSharp is a cross-platform 2D graphics API for .NET wrapping Google's Skia library.

**Architecture:** `C# Wrapper` -> `P/Invoke` -> `C API` -> `C++ Skia`
**Principle:** C# validates parameters, C API trusts and passes through.

---

## Critical Rules (Read First)

These rules are **non-negotiable**. Violating them causes broken builds, crashes, or downstream breakage.

### 1. Bootstrap First

Before C# code can build, native binaries must exist in `output/native/`. **How** you produce them depends on what you're changing:

| You are changing… | Bootstrap with |
|---|---|
| **Only C# code** (no files under `externals/skia/`, no `DEPS`, no submodule bump) | `dotnet cake --target=externals-download` (downloads pre-built natives from the **current** milestone) |
| **Native code, C API, `DEPS`, or the Skia submodule** (incl. milestone updates) | `dotnet cake --target=externals-{platform} --arch={arch}` β€” build from source. |

> **πŸ›‘ If you are doing a Skia milestone update, a C API change, or anything under `externals/skia/`, STOP. Do not run `externals-download` β€” ever. The downloaded binaries are from the OLD milestone and do not contain your changes; using them produces silently-wrong builds and `EntryPointNotFoundException` at runtime.** When source builds fail (missing `gn`, network errors, etc.), debug the source build β€” do not fall back to download.

### 2. Never `externals-download` After Native Changes

If you have modified **any** of the following, `externals-download` is FORBIDDEN until your changes ship to the pre-built artifact server (which only happens after merge):

- `externals/skia/**` (including the submodule SHA)
- `externals/skia/src/c/**`, `externals/skia/include/c/**`
- `externals/skia/DEPS`
- Any milestone bump or version file (`VERSIONS.txt`, `sk_types.h SK_C_INCREMENT`)

Falling back to `externals-download` because a native build failed is the #1 way agents corrupt milestone updates. Fix the source build instead.

### 3. Never Edit Generated Files

Files matching `*.generated.cs` and `docs/` are auto-generated.

- **NEVER** manually edit these files
- **ALWAYS** regenerate after C API changes (see [Commands](#commands))

### 4. ABI Stability

SkiaSharp maintains stable ABI. Breaking changes break downstream apps.

| Allowed | Never |
|---------|-------|
| Add new overloads | Modify existing signatures |
| Add new methods | Remove public APIs |
| Add new classes | Change return types |

### 5. Tests Are Mandatory

**Building alone is NOT sufficient.** Run tests before claiming completion (see [Commands](#commands)).

### 6. Branch Protection (COMPLIANCE REQUIRED)

**Direct commits to protected branches are a policy violation.**

| Repository | Protected Branches |
|------------|-------------------|
| SkiaSharp (parent) | `main` |
| externals/skia (submodule) | `main`, `skiasharp` |

**Required workflow:**

1. **Create a feature branch FIRST** β€” Human-driven changes use `dev/issue-NNNN-description`
2. **Make all commits on the feature branch** β€” Never commit directly to protected branches
3. **Submit a Pull Request** β€” Fill in the PR template (`.github/pull_request_template.md`) completely; changes must be reviewed before merging

Repository-owned automation may use a dedicated branch convention explicitly defined by its
workflow. It may update only branches owned by that workflow and must use `--force-with-lease`
for any approved force update; an unguarded `--force` remains forbidden. This never permits
direct commits to a protected branch.

```bash
# CORRECT β€” Always create a feature branch first
git checkout -b dev/issue-1234-fix-description

# For submodule changes:
cd externals/skia
git checkout -b dev/issue-1234-add-c-api

# NEVER DO THIS β€” Policy violation
git checkout main && git commit  # FORBIDDEN
git checkout skiasharp && git commit  # FORBIDDEN (in skia submodule)
```

**This applies to BOTH repositories.** The skia submodule has its own protected branches that must be respected.

**Always use the PR template.** When opening a pull request, populate every section of the repository's `.github/pull_request_template.md` β€” do **not** open a PR with an empty or default body. Keep the ABI-critical **Changes** and **Required skia PR** sections (write `None.` instead of deleting them), tick the relevant **Areas Affected**, describe how you verified the change under **Testing**, and attach before/after screenshots for any rendering change. The `externals/skia` submodule ships its own matching template for C API PRs.

An automated workflow may use a dedicated PR body only when the workflow owns and renders the
complete body deterministically. That body must satisfy the same requirements as the repository's
contributor template.

---

## Commands

Single source of truth for all commands:

| Task | Command |
|------|---------|
| **Bootstrap (C#-only work β€” see Rule #1, FORBIDDEN for native changes)** | `dotnet cake --target=externals-download` |
| **Build Native (macOS ARM64)** | `dotnet cake --target=externals-macos --arch=arm64` |
| **Build Native (macOS Intel)** | `dotnet cake --target=externals-macos --arch=x64` |
| **Build Native (Windows x64)** | `dotnet cake --target=externals-windows --arch=x64` |
| **Build Native (Linux x64)** | `dotnet cake --target=externals-linux --arch=x64` |
| **Build Native (Linux ARM64)** | `dotnet cake --target=externals-linux --arch=arm64` |
| **Build C#** | `dotnet build binding/SkiaSharp/SkiaSharp.csproj` |
| **Test** | `dotnet test tests/SkiaSharp.Tests.Console.slnx -p:TargetFramework=net10.0 -p:TargetFrameworks=net10.0` |
| **Regenerate** | `pwsh -NoLogo -NoProfile -File ./utils/generate.ps1` |
| **Regenerate release-notes (Prepare: api diffs + facts + index)** | `.agents/skills/release-notes/scripts/prepare.sh [--force] [--min-version X --max-version Y]` |
| **Render all pages + TOC/index (offline, from committed JSON)** | `.agents/skills/release-notes/scripts/render.sh [--min-version X --max-version Y]` |

### When to Use Which Bootstrap

| What You Changed | Command Required |
|------------------|------------------|
| C# code only (`binding/SkiaSharp/*.cs`) | `externals-download` (pre-built natives) |
| C API (`externals/skia/src/c/`, `externals/skia/include/c/`) | **`externals-{platform}` (MUST rebuild natives β€” `externals-download` is FORBIDDEN)** |
| Dependencies (`externals/skia/DEPS`) | **`externals-{platform}` (MUST rebuild natives β€” `externals-download` is FORBIDDEN)** |
| Skia submodule SHA / milestone update | **`externals-{platform}` (MUST rebuild natives β€” `externals-download` is FORBIDDEN)** |

> **CRITICAL:** If you modify ANY native code (C API headers/implementations), you MUST rebuild
> the native library with `dotnet cake --target=externals-{platform}`. Using `externals-download`
> after native changes will cause `EntryPointNotFoundException` at runtime because the downloaded
> binaries don't contain your new functions.

> **Note:** For release verification, see `/release-testing` command for the full platform matrix.

**Recovery Commands:**

| Problem | Command |
|---------|---------|
| Clean rebuild (**C#-only work**) | `dotnet cake --target=clean && dotnet cake --target=externals-download` |
| Clean rebuild (**any native or milestone work**) | `dotnet cake --target=clean && dotnet cake --target=externals-{platform} --arch={arch}` |
| Reset submodule | `git submodule update --init --recursive` |

> **Native build failing?** Do **NOT** "fall back" to `externals-download`. Common causes: missing `gn`/`ninja`, missing depot_tools on PATH, missing network access to `chromium.googlesource.com`. Diagnose and fix the source build. Using `externals-download` to make the failure go away will produce a build that runs against stale binaries and silently corrupts milestone updates.

---

## Architecture & Directories

### Layer Overview

```
C# Wrapper (binding/SkiaSharp/)  ->  P/Invoke  ->  C API (externals/skia/src/c/)  ->  C++ Skia
```

### Directory Guide

| Directory | Editable? | Notes |
|-----------|-----------|-------|
| `binding/SkiaSharp/` | Yes | C# wrappers |
| `externals/skia/src/c/` | Yes | C API implementation (our shim) |
| `externals/skia/include/c/` | Yes | C API headers (our shim) |
| `externals/skia/**` (other) | Conditional | Do not modify during ordinary binding work. The `update-skia` and `native-dependency-update` workflows may resolve upstream conflicts and maintain deliberate fork patches by following their dedicated audit rules. |
| `*.generated.cs` | No | Run `pwsh -NoLogo -NoProfile -File ./utils/generate.ps1` |
| `docs/` | No | Auto-generated |
| `documentation/dev/` | Yes | Architecture guides |
| `documentation/docfx/releases/<version>.md` | No | Generated by `release-notes-render.py` β€” edit `_sources/<version>.prose.json` (or `.notes.md`), never the page |
| `documentation/docfx/releases/_sources/<version>.notes.md` | Yes | Manual additions sidecar β€” maintainer prose that survives re-render (spec Β§3.7) |

---

## Writing Code

This section covers memory management, code patterns, and error handling together β€” they're tightly coupled when writing wrappers.

### Step 1: Identify Pointer Type

```
Is it wrapped in sk_sp<T>?
+- Yes -> SkRefCnt?      -> ISKReferenceCounted
|         SkNVRefCnt<T>? -> ISKNonVirtualReferenceCounted
+- No  -> Parameter?     -> owns: false
          Otherwise      -> DisposeNative()
```

| Type | C++ | C# | Examples |
|------|-----|-----|----------|
| Raw | `T*` param | `owns: false` | Temporary refs |
| Owned | Manual delete | `DisposeNative()` | Canvas, Paint, Path |
| Ref-counted | `sk_sp<T>` | `ISKReferenceCounted` | Image, Shader, Surface |

### Step 2: Choose Pattern

**Factory method** β€” return null on failure, validate inputs:

```csharp
public static SKImage FromPixels(SKImageInfo info, SKData data, int rowBytes)
{
    if (data == null)
        throw new ArgumentNullException(nameof(data));
    var cinfo = SKImageInfoNative.FromManaged(ref info);
    return GetObject(SkiaApi.sk_image_new_raster_data(&cinfo, data.Handle, (IntPtr)rowBytes));
}
```

**Instance method** β€” validate then call:

```csharp
public void DrawRect(SKRect rect, SKPaint paint)
{
    if (paint == null)
        throw new ArgumentNullException(nameof(paint));
    SkiaApi.sk_canvas_draw_rect(Handle, &rect, paint.Handle);
}
```

**C API** β€” naming convention `sk_<type>_<action>`:

```cpp
sk_image_t* sk_image_new_from_encoded(const sk_data_t* cdata) {
    return ToImage(SkImages::DeferredFromEncodedData(sk_ref_sp(AsData(cdata))).release());
}
```

### Step 3: Error Handling

| Layer | On Failure |
|-------|------------|
| C API | Return `nullptr` or `false` |
| C# Factory | Return `null` |
| C# Constructor | Throw |

### Step 4: Same-Instance Returns

Some methods return the **same instance**. Always check before disposing:

```csharp
// CORRECT β€” always use this pattern
var source = GetImage();
var result = source.Subset(bounds);
if (result != source)
    source.Dispose();
return result;
```

**Methods that may return same instance:** `Subset()`, `ToRasterImage()`, `ToRasterImage(false)`

### API Design Rules

- **Overloads, not defaults** β€” Default parameters break ABI
- **Deprecate, don't remove** β€” Use `[Obsolete]` with migration guidance
- **Naming:** `SK` prefix, PascalCase methods, camelCase parameters

**Adding overloads (ABI-safe):**

```csharp
// Existing method (don't modify)
public void DrawText(string text, float x, float y, SKPaint paint)

// New overload (safe to add)
public void DrawText(string text, SKPoint point, SKPaint paint)
    => DrawText(text, point.X, point.Y, paint);
```

### Threading Rules

Skia is **NOT thread-safe**.

| Never share between threads | Safe to share (immutable) |
|-----------------------------|---------------------------|
| `SKCanvas`, `SKPaint`, `SKPath` | `SKImage`, `SKShader`, `SKData` |

```csharp
// Thread-safe pattern β€” each thread gets own Paint
ThreadLocal<SKPaint> paint = new(() => new SKPaint());
```

### Anti-Patterns (Never Do This)

| Anti-Pattern | Why |
|-------------|-----|
| `canvas.Dispose()` while using derived objects | Crashes |
| Sharing `SKPaint` between threads | Race conditions |
| Modifying method signatures | ABI breaking |
| Manual edits to `*.generated.cs` | Overwritten on regenerate |
| Using default parameters in public APIs | ABI breaking |
| **Skipping failing tests** | **Unacceptable β€” tests must pass** |
| **Using `externals-download` after C API changes** | **Causes `EntryPointNotFoundException`** |
| Passing `fixed` pointers to native objects that outlive the block | GC moves memory -> corruption. Use `GCHandle.Alloc(Pinned)` or `Marshal.AllocCoTaskMem` |
| Testing WASM version changes without cleaning `bin/obj/_framework` | Stale cached native `.wasm` files produce false results |

---

## Testing & Debugging

### Running Tests

```bash
dotnet test tests/SkiaSharp.Tests.Console.slnx -p:TargetFramework=net10.0 -p:TargetFrameworks=net10.0
```

The unfiltered solution is the primary test entry point and the only final validation.
Microsoft.Testing.Platform treats a solution project with zero filtered matches as a failure,
so do not apply a single-test filter to the `.slnx`. After an unfiltered solution run identifies
a failure, use that test's owning host project for filtered diagnostic iterations:

| Failing host | Diagnostic project |
|---|---|
| Core/base | `tests/SkiaSharp.Tests.Console/SkiaSharp.Tests.Console.csproj` |
| Singleton initialization | `tests/SkiaSharp.Tests.SingletonInit.Console/SkiaSharp.Tests.SingletonInit.Console.csproj` |
| Vulkan | `tests/SkiaSharp.Vulkan.Tests.Console/SkiaSharp.Vulkan.Tests.Console.csproj` |
| Direct3D | `tests/SkiaSharp.Direct3D.Tests.Console/SkiaSharp.Direct3D.Tests.Console.csproj` |

Example:

```bash
dotnet test tests/SkiaSharp.Vulkan.Tests.Console/SkiaSharp.Vulkan.Tests.Console.csproj \
  -p:TargetFramework=net10.0 -p:TargetFrameworks=net10.0 \
  -- --filter-method "*CreateVkContextIsValid*"
```

Once the focused failure passes, rerun the unfiltered `.slnx`. A project-level run never
satisfies the final test gate.

### Tests MUST Pass

> **NON-NEGOTIABLE:** Tests must PASS before claiming completion.
>
> - Do NOT skip failing tests
> - Do NOT claim completion if tests fail
> - Do NOT use `SkipException` to work around failures
>
> **A skip must always be DECLARED, never inferred from an exception.**
>
> For **GPU tests** the rule is enforced by `GpuPolicy` β€” see
> [documentation/dev/gpu-test-policy.md](documentation/dev/gpu-test-policy.md).
> A backend is *required* on every platform we build it for; "no device", "no
> driver", "no ICD" and "no display" are **failures**. Skips are owned by the
> existing platform/host policy; an agent investigating or fixing a failure must
> not add or expand a GPU skip. Never wrap a GPU bring-up in
> `try/catch { Assert.Skip }`.
>
> For **non-GPU** tests, skipping is acceptable only for a genuine capability
> gap that is checked explicitly (no system font manager, no XPS support, no
> display for GTK).

### Writing Tests

```csharp
[SkippableFact]
public void FeatureWorks()
{
    using var data = SKData.Create(Path.Combine(PathToImages, "baboon.jpg"));
    using var image = SKImage.FromEncodedData(data);
    Assert.NotNull(image);
}
```

**BaseTest helpers:** `PathToImages`, `PathToFonts`, `IsWindows/Mac/Linux`

**Philosophy:** Tests fail when wrong. GPU tests skip only when `GpuPolicy`
declares it; other tests skip only for an explicitly checked capability gap.

### Debugging Methodology

1. **Establish baseline** β€” What's the known-good state?
2. **One change at a time** β€” Verify each change before proceeding
3. **Track changes in a table** β€” Log what you changed and the result
4. **Platform differences are signals** β€” If X works and Y fails, the difference IS the answer
5. **Revert if worse** β€” Don't pile fixes on top of failures

### Failure Recognition

| Error | Likely Cause | Fix |
|-------|--------------|-----|
| `error CS0246` (missing type) | Missing binding | Run `pwsh -NoLogo -NoProfile -File ./utils/generate.ps1` |
| `LNK2001 unresolved external` | C API signature mismatch | Check C function names match |
| `AccessViolationException` | Memory management bug | Check disposal patterns |
| `NullReferenceException` | Factory returned null | Check C API return value |
| Random crashes | Threading violation | Check Canvas/Paint thread scope |
| **`EntryPointNotFoundException`** | **Native library not rebuilt after C API change** | **Run `dotnet cake --target=externals-{platform}`** |

See [documentation/dev/debugging-methodology.md](documentation/dev/debugging-methodology.md).

---

## Slash Commands

Custom slash commands are available for specialized workflows. Use these for complex tasks that benefit from structured processes.

### When to Use Commands

| Task | Command | Triggers |
|------|---------|----------|
| Triage issue | `/issue-triage` | "triage #NNNN", "classify issue", "analyze issue" |
| Reproduce bug | `/issue-repro` | "repro #NNNN", "reproduce issue", "create reproduction" |
| Fix bug | `/issue-fix` | "investigate #NNNN", "fix issue", crash, exception, segfault, "doesn't work" |
| Scan/fix memory leak | `/memory-leak-fixer` | "memory leak", "leak scan", "undisposed handle", "owns flag", "double free", "fix the leak" |
| Scan/fix performance | `/performance-fixer` | "performance", "perf scan", "optimize", "make it faster", "hot path", "reduce allocations", "P/Invoke overhead", "port to managed" |
| Bulk process issues | `/issue-bulk-process` | "triage these issues", "process issues #1 #2 #3" |
| Add new API | `/api-add-review` | "expose", "wrap method", issue requests new functionality |
| Update dependency | `/native-dependency-update` | "bump libpng", "fix CVE in zlib" |
| Write XML docs | `/api-docs` | "document", "fill in missing docs" |
| Security check | `/security-audit` | "audit CVEs", "security overview" (read-only) |
| Start release (Step 1/5) | `/release-branch` | "release now", "start release X" |
| Check release status (Step 2/5) | `/release-status` | "check release status", "how is the build", "pipeline status" |
| Test release (Step 3/5) | `/release-testing` | "test the release", "verify packages" |
| Publish release (Step 4/5) | `/release-publish` | "push to nuget", "tag release" |
| Release milestones (Step 5/5) | `/release-milestones` | "reconcile milestones", "advance milestone schedule", "close release milestone" |
| Release notes | `/release-notes` | "generate release notes", "regenerate 3.119.x", "write release notes for" |
| Skia analyst | `/skia-analyst` | "what changed", "what are we missing", "feature gap", "api diff", "scout features", "diff tags" |
| Update Skia | `/update-skia` | "update to milestone NNN", "bump Skia" |
| Review Skia update | `/review-skia-update` | "review the Skia merge PR" |
| PR commit message | `/pr-commit-message` | "write commit message for PR" |
| Validate samples | `/validate-samples` | "build samples", "test sample projects" |
| Scout GM samples | `/sample-scout` | "find demos to port", "what samples are we missing", "gallery ideas" |
| Create/improve skill | `/skill-creator` | "create a new skill", "improve skill X" |

### Issue Pipeline (3 steps)

The first three commands form a pipeline. Each can run standalone, but they work best in sequence:

| Step | Command | Produces |
|------|---------|----------|
| 1 | `/issue-triage` | `ai-triage/{n}.json` |
| 2 | `/issue-repro` | `ai-repro/{n}.json` |
| 3 | `/issue-fix` | `ai-fix/{n}.json` + PR |

See [documentation/dev/issue-pipeline.md](documentation/dev/issue-pipeline.md) for handoff contracts and feedback loop.

### Issue Classification (#NNNN)

| If Issue Contains... | Type | Command |
|---------------------|------|---------|
| "triage", "classify", "analyze issue" | Triage | `/issue-triage` |
| "repro", "reproduce", "reproduction" | Reproduction | `/issue-repro` |
| "crash", "exception", "wrong", "fails", "broken", "segfault" | Bug | `/issue-fix` |
| "memory leak", "not disposed", "handle leak", "owns flag", "double free" | Memory leak | `/memory-leak-fixer` |
| "slow", "performance", "optimize", "faster", "hot path", "reduce allocations", "interop overhead" | Performance | `/performance-fixer` |
| "add", "expose", "missing API", "feature request" | New API | `/api-add-review` |
| "docs", "documentation", "XML", "comments" | Docs | `/api-docs` |
| CVE, security, vulnerability | Security | `/security-audit` then `/native-dependency-update` |

### When NOT to Use Commands

Work directly for:
- Trivial fixes (typos, whitespace, obvious one-liners)
- Changes only to `documentation/dev/` (non-generated docs)
- Build/test-only tasks (no reported bug)
- Questions about code or architecture
- Refactoring without a reported problem
- Performance optimization when the caller already knows the exact one-line change (otherwise use `/performance-fixer`, which proves the win with a benchmark + parity test)

---

## Further Reading

| Topic | Document |
|-------|----------|
| Architecture | `documentation/dev/architecture.md` |
| Memory Management | `documentation/dev/memory-management.md` |
| Adding APIs | `documentation/dev/adding-apis.md` |
| API Design | `documentation/dev/api-design.md` |
| Error Handling | `documentation/dev/error-handling.md` |
| Debugging | `documentation/dev/debugging-methodology.md` |
| NuGet Packages | `documentation/dev/packages.md` |
| Release Notes & API Diffs | `documentation/dev/release-notes-and-api-diffs.md` |