Skip to content

Bump skia to milestone 147#3702

Merged
mattleibow merged 59 commits intomono:mainfrom
ramezgerges:dev/update-skia-147
Apr 28, 2026
Merged

Bump skia to milestone 147#3702
mattleibow merged 59 commits intomono:mainfrom
ramezgerges:dev/update-skia-147

Conversation

@ramezgerges
Copy link
Copy Markdown
Contributor

@ramezgerges ramezgerges commented Apr 20, 2026

Description of Change

Update the Skia graphics library from milestone 133 to milestone 147, a 14-milestone jump covering upstream changes from Chrome 133 through Chrome 147.

Bugs Fixed

  • Fixes #

Changes

  • SKPath is now immutable — construction methods moved to SKPathBuilder
  • Added SKColorType.R16Unorm
  • Updated versions to 3.147.0
  • Regenerated bindings and updated all tests
  • Added tests for SKFont.GetTextPath with per-glyph positions
  • Removed ICC profile fields from encoder option structs
  • Fixed WASM build for .a.wasm extension
  • Fixed macOS/iOS Xcode projects (C++20, removed iOS -all_load)
  • Updated update-skia skill with reusable learnings

Behavioral Changes

Change Migration
SKPath mutation methods removed Use SKPathBuilder + Detach()
SKPath.Rewind() removed Use SKPathBuilder.Reset()
SKPaint.GetFillPath dst parameter Change to SKPathBuilder
SKPathMeasure.GetSegment dst parameter Change to SKPathBuilder
SKColorType enum values shifted Use named members
Encoder option ICC profile fields removed Remove from constructors

Required skia PR

mono/skia#184

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
Copy link
Copy Markdown

github-actions Bot commented Apr 20, 2026

📦 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 -- 3702

PowerShell / Windows:

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

Step 2 — Add the local NuGet source

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

@ramezgerges ramezgerges mentioned this pull request Apr 20, 2026
3 tasks
@ramezgerges ramezgerges force-pushed the dev/update-skia-147 branch 2 times, most recently from 4b55317 to 29d4349 Compare April 20, 2026 21:14
@mattleibow
Copy link
Copy Markdown
Contributor

ABI Breaking Changes in SKPath

This PR removes ~30 public mutation methods from SKPath (MoveTo, LineTo, AddRect, etc.) without deprecation, and changes GetFillPath/GetSegment signatures from SKPath dst to SKPathBuilder dst. This will break every downstream consumer that builds paths.

SkiaSharp's stated policy is "deprecate, don't remove." We should provide a migration runway.

Background

Upstream Skia made SkPath geometry-immutable starting at m143 (guarded by SK_HIDE_PATH_EDIT_METHODS), with the flag and all mutation code permanently removed in m145 (commit 98c01ea504 — "Remove dead code related to SkPathRef"). The internal backing store changed from SkPathRef (COW, mutable) to SkPathData (single immutable allocation). There is no going back upstream.

However, SkPath retains operator=, reset(), setFillType(), and swap() in m147 — it's a value type that can be replaced wholesale. The C API already exploits this pattern: sk_path_transform does makeTransform() → *AsPath(cpath) = std::move(result).

Migration Timeline

Version key:

  • vPR — this PR / the major release containing this Skia bump (includes docs update)
  • vNext — the next major release after vPR (compile errors, ABI still present)
  • vFuture — the major release after vNext (optional: full removal of compat shims)
Release Action Consumer impact
vPR Add SKPathBuilder + restore [Obsolete] mutation methods on SKPath + docs/migration guide ⚠️ Warnings only, zero source breaks
vNext Change to [Obsolete("...", error: true)] — ABI still exists and works for already-compiled apps, but recompilation produces errors ❌ Compile errors with clear migration message; existing binaries unaffected
vFuture (optional) Remove compat methods entirely, delete shim code Clean immutable SKPath

Proposal: Two Options (+ last-resort alternative)

All options restore the same [Obsolete] C# API surface on SKPath. They differ only in where the builder-based mutation shim lives. Benchmarking is needed to determine which performs best in practice.

Option A: C# Shims (simplest, highest overhead)

Add 1 new C API function:

void sk_path_set_from_builder(sk_path_t* dst, sk_pathbuilder_t* src) {
    *AsPath(dst) = AsPathBuilder(src)->detach();
}

All mutation methods are pure C# wrappers:

[Obsolete("Use SKPathBuilder. SKPath will be immutable in a future version of SkiaSharp.")]
public void LineTo(float x, float y)
{
    using var b = new SKPathBuilder(this);
    b.LineTo(x, y);
    SkiaApi.sk_path_set_from_builder(Handle, b.Handle);
}

Pros:

  • Minimal native changes (1 function, 2 lines of C++)
  • No new opaque types, no new C++ classes, no build file changes
  • Compat code lives entirely in C# — trivially removable in vFuture
  • Easiest to review and maintain

Cons:

  • Highest per-call overhead: managed SKPathBuilder allocation + 3 P/Invoke round-trips per mutation (new_from_path, mutation, set_from_builder)
  • O(N) per call, O(N²) for incremental path building of N segments

Option B: C API Shims (moderate complexity, lower overhead)

Restore the old sk_path_* mutation functions directly in sk_path.cpp, using the same builder-detach-assign pattern that sk_path_transform already uses:

void sk_path_move_to(sk_path_t* cpath, float x, float y) {
    SkPathBuilder b(*AsPath(cpath));
    b.moveTo(x, y);
    *AsPath(cpath) = b.detach();
}

void sk_path_line_to(sk_path_t* cpath, float x, float y) {
    SkPathBuilder b(*AsPath(cpath));
    b.lineTo(x, y);
    *AsPath(cpath) = b.detach();
}
// ... ~30 functions

C# just calls through like the old API:

[Obsolete("Use SKPathBuilder. SKPath will be immutable in a future version of SkiaSharp.")]
public void LineTo(float x, float y) =>
    SkiaApi.sk_path_line_to(Handle, x, y);

Pros:

  • No managed allocations per mutation — everything happens in a single native call
  • 1 P/Invoke per mutation instead of 3
  • No new types — uses existing sk_path_t*
  • C# compat code is trivial one-liners (just like the old code)
  • Same pattern as existing sk_path_transform

Cons:

  • ~30 new functions in sk_path.cpp / sk_path.h (the old declarations come back)
  • Still O(N) per call, O(N²) for incremental building (same fundamental cost)
  • Mutation functions on sk_path_* blur the line between real SkPath API and compat shims
  • Removing them in vFuture means another C API change

Option C: SkCompatPath with Lazy Batching (last resort — has critical issues)

⚠️ This option is documented for analysis only. It has a fundamental type-safety issue (see below) that makes it unsuitable unless significant additional infrastructure is added. Options A and B are the recommended paths.

Follow the SkCompatPaint pattern — create SkCompatPath : public SkPath in src/xamarin/ with an internal SkPathBuilder that batches mutations and only flushes on read:

class SkCompatPath : public SkPath {
    SkPathBuilder* fBuilder = nullptr;

    void ensureBuilder() {
        if (!fBuilder) fBuilder = new SkPathBuilder(*this);
    }

    void flush() {
        if (!fBuilder) return;
        SkPath::operator=(fBuilder->detach());
        delete fBuilder;
        fBuilder = nullptr;
    }

public:
    void lineTo(float x, float y) {
        ensureBuilder();
        fBuilder->lineTo(x, y);
    }

    // Flush before any read
    const SkPath& asPath() { flush(); return *this; }

    ~SkCompatPath() { delete fBuilder; }
};

C# switches to sk_compatpath_new() / sk_compatpath_delete():

public SKPath()
    : this(SkiaApi.sk_compatpath_new(), true) { }

[Obsolete("Use SKPathBuilder. SKPath will be immutable in a future version of SkiaSharp.")]
public void LineTo(float x, float y) =>
    SkiaApi.sk_compatpath_line_to(Handle, x, y);

Pros:

  • Best potential performance: mutations are O(1) each, batched into the builder
  • Flush happens once (O(N)) when the path is read/drawn — total O(N) for building N segments
  • Pointer IS-A SkPath* — works with all existing native APIs after flush
  • Same proven pattern as SkCompatPaint
  • Clear type separation: sk_path_* = real API, sk_compatpath_* = compat

Cons:

  • Most code: new C++ class, new C API header/impl, build file changes (pbxproj, GN)
  • New opaque type sk_compatpath_t adds complexity
  • Flush must be called before any read — every sk_path_* query function and every drawing function that takes a path needs to call AsCompatPath(cpath)->flush() first
  • ~30 flush sites across the C API must be patched (query functions, drawing, clipping, path effects, path measure, regions, pathops). Example of what every read site looks like:
    // Before (current):
    void sk_path_get_bounds(const sk_path_t* cpath, sk_rect_t* crect) {
        *crect = ToRect(AsPath(cpath)->getBounds());
    }
    
    // After (with compat flush):
    void sk_path_get_bounds(const sk_path_t* cpath, sk_rect_t* crect) {
        const_cast<SkCompatPath*>(static_cast<const SkCompatPath*>(AsPath(cpath)))->flush();
        *crect = ToRect(AsPath(cpath)->getBounds());
    }
  • Functions we don't control also need flush. Any C API function that passes sk_path_t* through to Skia for reading must flush first — including canvas draw, clip, path effects, etc.:
    // sk_canvas.cpp — we'd need to modify this:
    void sk_canvas_draw_path(sk_canvas_t* ccanvas, const sk_path_t* cpath, const sk_paint_t* cpaint) {
        // Must flush before Skia reads the path!
        const_cast<SkCompatPath*>(static_cast<const SkCompatPath*>(AsPath(cpath)))->flush();
        AsCanvas(ccanvas)->drawPath(*AsPath(cpath), *AsPaint(cpaint));
    }
    
    // sk_region.cpp — same:
    bool sk_region_set_path(sk_region_t* r, const sk_path_t* t, const sk_region_t* clip) {
        const_cast<SkCompatPath*>(static_cast<const SkCompatPath*>(AsPath(cpath)))->flush();
        return AsRegion(r)->setPath(*AsPath(t), *AsRegion(clip));
    }
    
    // ... and ~28 more sites
  • Risk of stale reads if a flush site is missed — silent wrong behavior (draws old path data), not a crash. Hard to diagnose.
  • Every new C API function added in future bumps that takes a path must also remember to flush
  • More native code to maintain in the skia fork across future bumps
  • Harder to remove in vFuture (C++/C API/build files vs just deleting C# methods)

🛑 Critical Issue: Not all sk_path_t* pointers are SkCompatPath.
Several C API functions return plain sk_path_t* that are NOT backed by SkCompatPath:

  • sk_pathbuilder_detach_path() / sk_pathbuilder_snapshot_path() — return plain SkPath
  • sk_path_clone() — clones as plain SkPath
  • Pathop results (sk_pathop_op, sk_pathop_simplify) — write into plain SkPath
  • Font glyph paths (sk_font_get_path) — return plain SkPath

Calling flush() via static_cast<SkCompatPath*> on a plain SkPath* is undefined behavior — it reads memory past the object boundary. Since SkPath has no virtual methods, there's no RTTI to distinguish the types at runtime.

This means either:

  1. Every sk_path_t* entering the system must be wrapped in SkCompatPath (complex, allocation overhead), OR
  2. A tag/discriminator must be added to the opaque type system (invasive change), OR
  3. Flush calls must only happen on paths known to be SkCompatPath (fragile, requires tracking)

None of these are clean. This issue makes Option C impractical without significant additional infrastructure.

Performance Comparison

Operation Option A (C#) Option B (C shim) Option C (lazy)
Single mutation 3 P/Invoke + managed alloc + O(N) 1 P/Invoke + O(N) 1 P/Invoke + O(1)
Build N segments O(N²) + N managed allocs O(N²) O(N)
Read after build O(1) O(1) O(N) flush
Total build+read O(N²) O(N²) O(N)

Note: The O(N²) in Options A/B only matters for large paths built incrementally via the obsolete API. Users who heed the [Obsolete] warning and switch to SKPathBuilder get O(N) regardless. Benchmarking is needed to determine if the constant-factor differences matter for typical path sizes (10-100 segments).

Regardless of Option: Also needed

  1. sk_pathbuilder_add_path_reverseAddPathReverse has no public SkPathBuilder equivalent. Implement via SkPathPriv::ReverseAddPath() (exists at SkPathPriv.h:366, fully implemented at SkPathBuilder.cpp:934). This is needed for all options.

  2. GetFillPath / GetSegment dual overloads — keep both SKPathBuilder dst (new preferred) and SKPath dst ([Obsolete] shim) overloads.

  3. Rewind()[Obsolete] alias to Reset() — upstream removed rewind() in m145; the C API already maps it to reset(). The old distinction (rewind kept memory, reset freed it) no longer applies since SkPathData is immutable/ref-counted.

  4. Generated files — Skottie, SceneGraph, Resources bindings need regeneration (missing sk_pathbuilder_t alias).

Recommendation

Start with Option B (C shims) for vPR. It has the best balance of simplicity and performance — no managed overhead, no new types, and the C# layer is trivial one-liners that look identical to the old code. The C API functions follow the exact same pattern as the existing sk_path_transform. If benchmarking reveals that the constant-factor differences between A and B don't matter for typical path sizes, Option A (C# shims) is also viable and has the advantage of zero native changes beyond one helper function.

@mattleibow
Copy link
Copy Markdown
Contributor

SKColorType Enum Renumbering — Unnecessary ABI Break

The PR inserts R16Unorm = 24 in the middle of the SKColorType enum, shifting three existing values:

Member Before After
R16Unorm (new) 24
Rgba10x6 24 25 ⚠️
Bgra10101010XR 25 26 ⚠️
RgbF16F16F16x 26 27 ⚠️

This is a binary/ABI break — any code that serialized these enum values as integers will silently get the wrong color type.

This shouldn't be necessary

SkiaSharp has a two-level enum architecture specifically designed to prevent this:

PUBLIC enum (SKColorType)           ← Stable values, consumer-facing
        ↕  EnumMappings.cs          ← Translates between the two
NATIVE enum (SKColorTypeNative)     ← Generated, mirrors C header exactly
        ↕  P/Invoke
C enum (sk_colortype_t)             ← Skia native

The public SKColorType values are intentionally different from the native sk_colortype_t values. For example, today:

Public Value Native Value
SKColorType.Gray8 9 SKColorTypeNative.Gray8 14
SKColorType.RgbaF16 10 SKColorTypeNative.RgbaF16 16
SKColorType.Alpha16 16 SKColorTypeNative.A16Unorm 22

The mapping in EnumMappings.cs handles the translation. This is the whole point — Skia can reorder its internal enum freely, and as long as we update the mapping, the public API is stable.

Suggested fix

Add R16Unorm at the end of the public enum (value 27) instead of inserting it at 24. Keep existing values stable:

public enum SKColorType
{
    // ... values 0-23 unchanged ...
    R8Unorm = 23,
    Rgba10x6 = 24,         // ← unchanged
    Bgra10101010XR = 25,   // ← unchanged
    RgbF16F16F16x = 26,    // ← unchanged
    R16Unorm = 27,          // ← new, appended at end
}

Then update EnumMappings.cs to map SKColorType.R16Unorm (27)SKColorTypeNative.R16Unorm (23). The mapping layer already handles mismatched values — that's what it's for.

@ramezgerges ramezgerges force-pushed the dev/update-skia-147 branch from fd3dfc3 to 5a52301 Compare April 22, 2026 18:30
@mattleibow
Copy link
Copy Markdown
Contributor

mattleibow commented Apr 23, 2026

/azp run

@azure-pipelines
Copy link
Copy Markdown

No pipelines are associated with this pull request.

@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/update-skia-147 branch from c23ce99 to 72921ab Compare April 26, 2026 13:53
@azure-pipelines
Copy link
Copy Markdown

No pipelines are associated with this pull request.

@ramezgerges ramezgerges force-pushed the dev/update-skia-147 branch from d19c29a to 6a38a03 Compare April 26, 2026 17:57
@azure-pipelines
Copy link
Copy Markdown

No pipelines are associated with this pull request.

@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 and others added 10 commits April 26, 2026 19:33
- VERSIONS.txt: bump all versions to 3.147.0
- cgmanifest.json: update commit hash and chrome_milestone to 147
- SkiaApi.generated.cs: add sk_pathbuilder_t bindings, remove old sk_path
  mutation bindings, update enum/struct changes
- SKPathBuilder.cs: new class with all path mutation methods (MoveTo, LineTo,
  QuadTo, ConicTo, CubicTo, ArcTo, Close, AddRect, AddOval, etc.)
- SKPath.cs: remove mutation methods (now immutable, matching upstream SkPath)
- Definitions.cs: remove fICCProfile/fICCProfileDescription from encoder options
- SKFont.cs: use SKPathBuilder for text-on-path morphing
- native/linux/build.cake: preserve retpoline flag (Clang-only)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All tests that used SKPath mutation methods now use SKPathBuilder.
Build + Detach pattern replaces direct path mutation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SKColorType: add R16Unorm (new in Skia m144), renumber subsequent values
- EnumMappings.cs: add R16Unorm <-> R16Unorm mapping
- Definitions.cs: R16Unorm is 2 bytes per pixel
- externals/skia: fix PDF document jpeg callbacks, pathbuilder symbol keeper

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add R16Unorm to GetBitShiftPerPixel and GetAlphaType switch expressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SKPathMeasure.GetSegment: changed dst from SKPath to SKPathBuilder
  (was crashing by passing SKPath handle as SKPathBuilder to native)
- SKPaint.GetFillPath: changed dst from SKPath to SKPathBuilder
- SKRegion.getBoundaryPath: fix use-after-move bug
- SKPathTest: fix MeasuringSegementsWorks to use SKPathBuilder,
  update TightBounds precision for m147
- SKPaintTest: update GetFillPath tests to use return-value overloads

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Upstream int->size_t change in textToGlyphs converts -1 error code
to ~0ULL, crashing on unpaired surrogates. C API now validates UTF
before calling into Skia.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[mono/skia fork patches] Three bugs in SkPixmap::getColor():
- kRGBA_1010102: lost conditional R/B swap (always used BGRA order)
- kSRGBA_8888, kR16G16B16A16_unorm: wrong SkColorSetARGB arg order
- kRGBA_10x6: missing *255 scale (float [0,1] passed as uint8)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- sk_text_utils_get_pos_path: re-implemented in src/c/sk_font.cpp
  using SkFont::textToGlyphs + getPaths (was a fork addition on
  SkTextUtils that got removed during m147 merge)
- SkTextUtils.h: removed stale struct SkPoint forward declaration
  left over from the GetPosPath removal

Without this fix, SKFont.GetTextPath(text, positions) would crash
at runtime with EntryPointNotFoundException.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verifies that the re-implemented sk_text_utils_get_pos_path produces
correct results:
- GetTextPathWithPositionsProducesNonEmptyPath: multi-glyph text at
  spaced positions produces a path with points and non-empty bounds
- GetTextPathWithPositionsMatchesExpectedBounds: a glyph positioned at
  (100, 50) produces a path offset from the origin path

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pulls in the mono/skia change that replaces the dpi-only
sk_document_create_xps_from_stream with an additive
sk_document_create_xps_from_stream_with_options entry point mirroring
SkXPS::Options, and regenerates the P/Invoke bindings so the new
SKDocumentXpsOptions struct and companion import are callable from
the managed layer. The dpi-only C function is kept as a forwarder,
preserving ABI for existing consumers. No managed wrapper surface
yet; that's a follow-up on top of SKDocument.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the libgcc + libstdc++ static-link toolchain with a pure
clang/LLVM stack on the default (debian-cross) Linux variant:

  -stdlib=libc++        libc++ instead of libstdc++
  -rtlib=compiler-rt    compiler-rt builtins instead of libgcc (provides
                        __mulodi4 for 32-bit dng_sdk natively)
  -unwindlib=libunwind  LLVM libunwind instead of libgcc_s
  -static-libstdc++     statically link libc++ into the .so
  -static-libgcc        in clang's driver this also flips libunwind to
                        its static archive (-l:libunwind.a)

Dockerfiles for debian/11 and debian/13 now install the LLVM runtime
libraries (libclang-rt-N-dev, libc++-N-dev, libc++abi-N-dev,
libunwind-N-dev) and target glibc/fontconfig via Debian multiarch
instead of the gcc-*-cross packages. This drops the libstdc++/libgcc
"current" symlink and the manual fontconfig .deb extraction; both
were workarounds for the cross-package layout.

Net effect for the x86 .so verified locally:
  libSkiaSharp.so.147.0.0 -> libfontconfig.so.1, libm.so.6, libc.so.6
  libHarfBuzzSharp.so.0.60831.0 -> libm.so.6, libc.so.6
No libstdc++/libgcc_s/libunwind/libc++ runtime deps.

Reverts the per-arch -L/usr/lib/gcc-cross/<triple>/<ver> workaround
from 794d55d — it was a stop-gap; compiler-rt is the real fix.

Alpine and bionic variants keep their existing toolchains.
- Bump skia submodule to ddd59e7a19 (mono/skia: add default PNG image
  proc to SkPicture serialize/deserialize). m147's default SerialProcs
  drop raster images on serialize and produce null on deserialize; the
  fork patch restores the historical PNG fallback at the C API boundary.
  Fixes SKPictureTest.EncodesImageIntoPicture.

- GRDefinitions.ToGlSizedFormat: add SKColorType.R16Unorm => R16. The
  enum value was added in m144 but the GL format switch wasn't updated,
  so any GR/GL code path that hit an R16Unorm-backed image threw
  ArgumentOutOfRangeException.

- SKFontTest.GetTextPathWithPositions{ProducesNonEmptyPath,Matches-
  ExpectedBounds}: assign SKTypeface.Default explicitly. m132 changed
  SkFont(null) to eagerly use MakeEmpty() instead of resolving a
  default typeface, so `new SKFont()` has no glyphs and the previously-
  passing tests stopped producing real paths.

- native/linux/build.cake: drop the variant gating on the clang/LLVM
  runtime flags. All Linux variants (debian-cross, alpine, bionic-NDK)
  now build with -stdlib=libc++ -rtlib=compiler-rt -unwindlib=libunwind.
  On bionic the flags are NDK defaults so this is a no-op; on alpine
  the matching apk packages are now installed in the cross sysroot.

- scripts/Docker/alpine/Dockerfile: replace `build-base` (gcc/libstdc++
  /libgcc) with the LLVM runtime apk set: libc++-dev, libc++-static,
  compiler-rt, compiler-rt-static, llvm-libunwind-dev,
  llvm-libunwind-static, plus musl-dev for libc/crt files.
`new SKFont()` once again seeds the typeface with `SKTypeface.Default`
rather than letting the C++ side fall through to MakeEmpty(). Avoiding
the auto-resolve was a workaround for the m132 SIGSEGV in
`sk_typeface_ref_default()` (Android API 36, null deref in upstream
C++); the managed default path used now goes through
`sk_fontmgr_legacy_create_typeface` instead, which already handles a
null return by falling back to the empty singleton — so the original
crash no longer applies. Restoring the auto-resolve keeps the historical
SkiaSharp contract (`new SKFont()` produces a font that can actually
draw text) without bringing the crash back.

Tests for GetTextPath with per-glyph positions can drop their explicit
`Typeface = SKTypeface.Default` assignment now that the default
constructor does it for them.
@azure-pipelines
Copy link
Copy Markdown

No pipelines are associated with this pull request.

Bullseye main has no riscv64 binary archive, so the LLVM-multiarch path
used by the other Debian-cross archs doesn't apply. Bullseye does ship
gcc-N-cross packages built from cross-toolchain-base, which install a
riscv64 sysroot under /usr/riscv64-linux-gnu/ at glibc 2.31. Use those
for riscv64 only — the other archs keep the libc++/compiler-rt/
libunwind multiarch path. libfontconfig comes from the Trixie pool
(suite-agnostic /pool/main/f/fontconfig path) since Bullseye's archive
has no riscv64 .deb to pull from main.

Build flag-side: native/linux/build.cake gates the LLVM runtime flags
(`-stdlib=libc++ -rtlib=compiler-rt -unwindlib=libunwind`) on
arch != "riscv64". riscv64 stays on the historical
`-static-libstdc++ -static-libgcc` GCC stack — 64-bit
__builtin_smul_overflow doesn't lower to __mulodi4 so the post-m133
dng_sdk link issue that motivated the LLVM switch on x86/arm doesn't
apply here.

Restore the libstdc++ "current" symlink and libc.so / libpthread.so
linker-script sed for riscv64; native/linux-clang-cross/build.cake
references those paths in its cross-compile cflags/ldflags. The other
archs still don't need the symlink (libc++ is found via /usr/include/
c++/v1 and clang's -stdlib=libc++).

CI matrix now routes riscv64 to debian/11 with --verifyGlibcMax=2.31;
loongarch64 is the only arch left on debian/13 (LoongArch port wasn't
release-supported until Trixie either, and there's no equivalent
gcc-loong64-cross story in Bullseye).
Alpine's compiler-rt apk doesn't ship a separate -static subpackage —
the static `libclang_rt.builtins-<arch>.a` (and the corresponding
clang_rt.crtbegin/crtend.o files) live inside the regular
`compiler-rt` package. Asking apk for `compiler-rt-static` produces
"no such package" and aborts the install.

Verified by simulating the full apk add against Alpine v3.17/main+
community for armv7/aarch64/x86/x86_64, v3.20 for riscv64, and v3.21
for loongarch64 — all six arch+suite combinations resolve, and
`/usr/lib/clang/15.0.7/lib/linux/libclang_rt.builtins-armhf.a`,
`/usr/lib/libc++.a`, `/usr/lib/libunwind.a` all land where the build
flags expect them.
Alpine ships its libc++abi.a built without -fPIC — the static archive
uses local-exec TLS relocations (R_ARM_TLS_LE32 et al) and lld refuses
to link it into a -shared output. Alpine's libc++.a is also separate
from libc++abi.a (Ubuntu/Debian merges them), so a static libc++ link
without explicit libc++abi leaves __cxa_throw / __gxx_personality_v0 /
vtable for std::length_error unresolved. There's no clean way to
static-link the LLVM C++ stack into libSkiaSharp.so on alpine without
either rebuilding libc++abi from source with -fPIC or accepting a
runtime libc++.so dep that the historical alpine package never had.

Treat alpine like the riscv64 carve-out: keep libstdc++/libgcc on the
old `-static-libstdc++ -static-libgcc` GCC stack, gate the LLVM runtime
flags off when VARIANT starts with "alpine", and revert the apk add
list back to `build-base` (which transitively pulls in gcc/g++/
libstdc++/libgcc/musl-dev). Alpine's libgcc has the helpers dng_sdk's
__builtin_smul_overflow lowers to, so the post-m133 dng_sdk link issue
that motivated the LLVM switch on debian-cross x86/arm doesn't apply.

Verified locally: rebuilt the alpine arm sysroot (apk + build-base),
ran `dotnet cake --target=externals-linux-clang-cross --buildarch=arm
--variant=alpine` against it. libSkiaSharp.so.147.0.0 (ELF32 ARM,
musl) ships with NEEDED = [libfontconfig.so.1, libc.musl-armv7.so.1]
— no libstdc++.so / libgcc_s.so runtime dep.
@azure-pipelines
Copy link
Copy Markdown

No pipelines are associated with this pull request.

LoongArch is still a debian-ports architecture, not a Trixie release
arch, so multiarch installs from Trixie main fail with "Unable to
locate package" for libc6-dev:loong64, libclang-rt-19-dev:loong64,
libc++-19-dev:loong64, libfontconfig1-dev:loong64, etc. — Trixie's
binary index doesn't have loong64 entries even though dpkg --add-
architecture loong64 succeeds.

Trixie does ship the gcc-14-cross series for loong64 (built from
cross-toolchain-base, packaged as amd64 .debs containing a loong64
sysroot under /usr/loongarch64-linux-gnu/), so this is the same
shape as the riscv64 carve-out on Bullseye:

  * scripts/Docker/debian/13/Dockerfile rewritten to use libc6-dev-
    loong64-cross + libstdc++-14-dev-loong64-cross + libgcc-14-dev-
    loong64-cross + binutils-loongarch64-linux-gnu, restore the
    libstdc++ "current" symlink and the libc.so / libpthread.so.0
    linker-script sed, and pull libfontconfig from the Trixie pool
    URL (suite-agnostic, serves loong64 .debs even though Trixie's
    main binary lists don't include them).
  * native/linux/build.cake: expand the GCC carve-out gate from
    `arch == "riscv64"` to `arch == "riscv64" || arch == "loongarch64"`,
    so both archs link libstdc++/libgcc statically and skip the
    LLVM runtime flags.

After the riscv64 carve-out moved its routing to debian/11, debian/13
is a single-arch image (loong64 only). Drop the multi-arch case
statement and fail fast if anything else is passed in BUILD_ARCH.

Verified locally: installed libstdc++-14-dev-loong64-cross + libgcc-
14-dev-loong64-cross + binutils-loongarch64-linux-gnu, staged the
Trixie pool fontconfig into /usr/loongarch64-linux-gnu/, ran
`dotnet cake --target=externals-linux-clang-cross --buildarch=
loongarch64`. libSkiaSharp.so.147.0.0 (LoongArch ELF64) ships with
NEEDED = [libfontconfig.so.1, libm.so.6, libc.so.6, ld-linux-loongarch
-lp64d.so.1] and glibc symbols capped at 2.38.
The clang/LLVM runtime stack (-stdlib=libc++ -rtlib=compiler-rt
-unwindlib=libunwind -static-libstdc++ -static-libgcc) cannot be
delivered cleanly on Bullseye + clang-13 cross-compile. CI surfaced
this as a sequence of cascading docker_build failures:

  * docker_build_3.txt: alpine apk install fails — compiler-rt-static
    isn't a separate package on Alpine.
  * after fixing that: alpine link fails with R_ARM_TLS_LE32 against
    __cxxabiv1::eh_globals because Alpine's libc++abi.a is built
    without -fPIC, plus libc++ and libc++abi static archives aren't
    merged the way Ubuntu/Debian's libc++.a is.
  * docker_build_4.txt: Debian 13 multiarch install fails with "Unable
    to locate package libc6-dev:loong64" — Trixie has no loong64
    binary archive (still a debian-ports arch).
  * linux_x86_3.txt: Debian 11 multiarch install fails with "Unable
    to locate package libclang-rt-13-dev:i386" — Bullseye main has
    no libclang-rt-13-dev at all (the package wasn't split out from
    libclang-common-N-dev until LLVM 14).

Each carve-out (riscv64, loongarch64, alpine) was already on
gcc-cross / libstdc++ / libgcc anyway, so the unification was only
nominally applying to arm/arm64/x86/x64 — and it now turns out it
doesn't apply there either without resorting to apt.llvm.org or a
base-image bump that would raise the glibc floor above 2.31.

Revert to the pre-`64b71c51a` toolchain shape:

  * scripts/Docker/debian/11/Dockerfile drops the multiarch path,
    drops the per-arch branching for fontconfig staging, and uses
    libstdc++-10-dev-<arch>-cross + libgcc-10-dev-<arch>-cross +
    binutils-<arch> for every supported arch (arm/arm64/x86/x64/
    riscv64). The riscv64-only Trixie-pool fontconfig fallback stays
    (Bullseye main has no riscv64 .debs); other archs get
    Bullseye-era fontconfig as before.
  * native/linux/build.cake drops the LLVM runtime triplet and the
    `useLlvmRuntime` gate. Restores the
    `-L/usr/lib/gcc-cross/<triple>/10` workaround for 32-bit
    `__mulodi4` from 794d55d — clang-13's auto-detect of the
    cross-libgcc dir doesn't reliably feed -static-libgcc on a
    --start-group/--end-group link with --no-undefined.
  * libHarfBuzzSharp's flag block goes back to the simple
    -static-libstdc++ -static-libgcc form.

Net effect: every Linux variant now builds with libstdc++/libgcc
statically linked into the .so, glibc symbols capped at the build
image's glibc (2.31 for Bullseye archs, 2.38 for Trixie loong64).
The result matches the pre-unification CI-passing state, with the
two carve-outs (riscv64 in debian/11, loongarch64 in debian/13)
preserved.
After the revert in 979bd00, native/linux/build.cake no longer has
a per-arch LLVM-flag gate, so the comment on the loongarch64 carve-
out in this file referencing it was misleading. Trim the rationale
to just the gcc-N-cross story (which is unchanged).
@azure-pipelines
Copy link
Copy Markdown

No pipelines are associated with this pull request.

x64 docker build: `binutils-x86_64-linux-gnu` doesn't exist on Bullseye
— the package is `binutils-x86-64-linux-gnu` (with hyphens, matching
Debian's gcc-cross alias spelling). My LLVM-unification cleanup in
64b71c5 swapped the case statement to set `TOOLCHAIN_ARCH=x86_64-
linux-gnu` directly so I could drop the sed line that normalized
`x86-64` -> `x86_64` for filesystem paths, but that broke the apt
package name lookup for x64. Restore the hyphenated form in the case
statement and run the sed in-line wherever a `/usr/<triple>/` path is
used.

x86 + arm link: `__mulodi4` is undefined despite `-static-libgcc`
finding the cross-libgcc dir via the `-L` from 794d55d. Inspecting
Bullseye's libgcc-10-dev-i386-cross.deb directly reveals libgcc.a
contains __muldi3 / __mulvdi3 / __mulsc3 / __multf3 / __mulxc3 etc.
but NO __mulodi4 — the `-L` workaround was based on the wrong premise.
The helper actually lives in compiler-rt's libclang_rt.builtins-
<arch>.a, which only exists for i386 (bundled in the libclang-common-
13-dev host package) but not for armhf in Bullseye main. Submodule
bump to ddd59e7a19's successor b5666199c3 adds a local copy of
compiler-rt's mulodi4.c into src/xamarin/sk_mulodi4.c (gated on
__i386__ || __arm__) so libSkiaSharp.so defines `__mulodi4` itself
on every 32-bit target, no toolchain extras required.
Tizen 32-bit (armel + i586 on the mobile-6.0 rootstrap, gcc-9.2 /
glibc 2.30 era) is no longer supported. Only deployment targets that
ever shipped 32-bit Tizen are years past EOL, and the rootstrap's
header set has accumulated enough drift from current Skia that the
build needs distinct fork carries (sysroot-relative freetype `-I=`,
`-std=c++2a` for clang-10) just to compile.

Removed from:
  * scripts/azure-templates-stages-native-windows.yml
  * scripts/azure-templates-stages-native-macos.yml
  * scripts/azure-templates-stages-native-merge.yml (artifact list)
  * native/tizen/build.cake (Build() invocations for armel / i586)
  * binding/IncludeNativeAssets.{SkiaSharp,HarfBuzzSharp}.targets
    (TizenTpkFiles entries for tizen-armel / tizen-x86)
  * binding/{SkiaSharp,HarfBuzzSharp}.NativeAssets.Tizen/*.csproj
    (PackageFile entries for armel / i586)

Tizen x64 (x86_64-emulator64) and Tizen arm64 (aarch64-device64) on
the tizen-8.0 rootstrap remain.
794d55d added `-L/usr/lib/gcc-cross/{arm-linux-gnueabihf,i686-linux-
gnu}/10` to extra_ldflags so `-static-libgcc` could find `libgcc.a`
"with __mulodi4 in it". Inspecting Bullseye's libgcc-10-dev-{i386,
armhf}-cross.deb directly shows libgcc.a contains __muldi3, __mulvdi3,
__mulsc3, __multf3, __mulxc3, __mulvsi3 — but no __mulodi4, and no
_mulodi4.o. The helper only ever ships in compiler-rt's libclang_rt.
builtins-<arch>.a; the -L paths were dead.

Now that b5666199c3 (mono/skia: provide __mulodi4 locally for 32-bit
Linux cross-compile) drops a self-contained copy of __mulodi4 into
libSkiaSharp.so itself, the linker has no other unresolved libgcc
helper to chase, so the -L flag has no effect on any 32-bit link
either. Drop it and the misleading comment so future readers don't
re-introduce the same wrong fix.
@azure-pipelines
Copy link
Copy Markdown

No pipelines are associated with this pull request.

…havior

c0c12b1 restored the parameterless `new SKFont()` constructor to
seed the typeface with `SKTypeface.Default` (instead of letting the
C++ side fall through to MakeEmpty()). The test
`DefaultFontTypefaceIsEmpty` was added in 02712b7 (mono#3730) to
validate the m132 empty-default behaviour and was directly
invalidated by that revert. Rename to `DefaultFontTypefaceIsDefault`
and assert the new contract: typeface is non-null, non-empty, and
the same instance as `SKTypeface.Default`.

The other tests in this section (`FontWithNullTypefaceIsEmpty`,
`FontTypefaceSetNullReturnsEmpty`, `FontTypefaceSetNullDoesNotCrash-
OnMeasure`, `FontWithDefaultCanMeasure`) still validate the explicit
`null` and `SKTypeface.Default` paths which weren't changed, so they
keep passing as-is.
@azure-pipelines
Copy link
Copy Markdown

No pipelines are associated with this pull request.

ramezgerges and others added 2 commits April 27, 2026 22:39
Picks up the submodule fix where the C-API shim was passing nullptr
instead of converting the supplied GrRecordingContext to an SkRecorder.
Restores SKImageTest.TextureImageIsValidOnContext (the only macOS
CoreCLR regression remaining in this PR's CI) — IsValid(grContext) now
correctly returns true for GPU-backed images.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up the second SkRecorder shim fix in this family — same shape as
the isValid one. SKImage.Subset(grContext, rect) was silently returning
null for GPU-backed images because the shim threw away the context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@azure-pipelines
Copy link
Copy Markdown

No pipelines are associated with this pull request.

mattleibow added a commit that referenced this pull request Apr 28, 2026
The Skia m133 bump (PR #3702) is still open — incorrectly added it
to 4.133.0.md by inferring from the version number. Now the workflow
only mentions Skia bumps when a 'Bump skia' PR appears in the merged
PR list.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Point submodule at 6f8139adf7 (Merge upstream chrome/m147 (mono#184)),
the merged tip of mono/skia's skiasharp branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow mattleibow merged commit e8f124f into mono:main Apr 28, 2026
3 of 4 checks passed
github-actions Bot added a commit that referenced this pull request Apr 28, 2026
Add PR #3702 (Bump skia to milestone 147) to upcoming release notes:
- Update Highlights to reflect m147 engine leap (14 milestones)
- Add breaking changes: SKPath immutability, SKPathBuilder migration
- Add SKColorType.R16Unorm new feature
- Reorder Engine section to lead with m147
- Update Platform Support table

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants