Back to Skiasharp

Known Gotchas & Troubleshooting

.agents/skills/update-skia/references/known-gotchas.md

4.150.014.3 KB
Original Source

Known Gotchas & Troubleshooting

Hard-won findings from past Skia milestone updates. Check these proactively — they will save hours of debugging.

C++ ↔ C API Layer

1. DEF_STRUCT_MAP vs Type Aliases

When upstream changes a C++ type from a struct to a using alias (e.g., GrVkYcbcrConversionInfousing VulkanYcbcrConversionInfo), the DEF_STRUCT_MAP macro in sk_types_priv.h forward-declares struct X, which conflicts with the alias. Fix by switching to DEF_MAP_WITH_NS(namespace, ActualType, CType) and wrapping in the appropriate platform guard (e.g., #if SK_VULKAN).

2. Struct Size static_assert Failures

sk_structs.cpp asserts sizeof(our_c_type) == sizeof(CppType) for every struct mapped via reinterpret_cast. When upstream adds fields (e.g., SkPngEncoder::Options gaining gainmap pointers in m133), the assert fires. During analysis, proactively check every asserted struct against the target milestone — don't wait for the build to catch it.

3. Custom Patches May Partially Survive Merges

SkiaSharp adds custom methods to upstream headers (e.g., SkTypeface::RefDefault()). After merge, implementations in .cpp files may survive but header declarations can be silently removed by upstream changes.

When header declarations are lost:

  1. Check if upstream provides a replacement API — if so, update the C API to use it
  2. If no replacement exists, re-add the declaration. But consider moving custom methods out of upstream headers and into our C API layer (src/c/) to avoid this recurring conflict
  3. Never leave a mismatched header/implementation — it compiles but crashes at runtime on some platforms

4. Fontconfig #ifdef Guards

Platform font manager calls (e.g., SkFontMgr_New_FontConfig) must be guarded by feature-availability macros (SK_FONTMGR_FONTCONFIG_AVAILABLE), not platform macros (SK_BUILD_FOR_UNIX). When skia_use_fontconfig=false, platform macros leave the symbol unresolved. The -Wl,--no-undefined linker flag catches this at link time.

Build System

5. git-sync-deps emsdk Failure

Upstream m121+ added an activate-emsdk call in tools/git-sync-deps. Since SkiaSharp comments out emsdk in DEPS (the fork builds WASM against an external, pre-activated emsdk via skia_emsdk_dir), an unguarded activate-emsdk would try to install/activate an in-tree emsdk that does not exist. The fix is in the submodule: bin/activate-emsdk returns early when third_party/externals/emsdk is absent, making the call a no-op for every caller (cake builds, the review skill, direct tools/git-sync-deps runs). No GIT_SYNC_DEPS_SKIP_EMSDK env var is needed.

6. BUILD.gn Legacy Flags

Upstream progressively deprecates legacy APIs behind flags (e.g., SK_DEFAULT_TYPEFACE_IS_EMPTY, SK_DISABLE_LEGACY_DEFAULT_TYPEFACE). When these flags break SkiaSharp's C API:

  1. Check what the flag removes — read the upstream commit that added it
  2. If the C API uses the removed behavior, update the C API to use the replacement API. This is the real fix — upstream is signaling that the old API will be removed entirely in a future milestone.
  3. Only as a short-term bridge (with a TODO comment and tracking issue), you may comment out the flag to unblock the build while you work on the proper fix. Never leave a commented-out flag without a plan to address it.

Also watch for renamed/removed GN flags between milestones — obsolete flags cause Unknown GN flag errors. Always diff the target BUILD.gn against the current one.

7. .gitmodules Branch Name

When the mono/skia target branch name changes, .gitmodules must be updated to track the new branch. Easy to forget; causes silent submodule tracking failures.

19. Fork-Only GN declare_args Cleanup

When removing fork-only GN args, check ALL native build scripts (native/*/build.cake) for references. Platform scripts may pass these args to GN, causing Unknown GN flag warnings.

20. GN-to-Bazel Define Migration

Upstream progressively moves defines from GN to Bazel-only. Check that all #if defined(X) guards in Skia source that SkiaSharp depends on still have their defines set in the GN build. Symptoms: feature code silently compiled out, returning nullptr/fallback.

21. Backend Defines Not Reaching :core

The C API shims (src/c/gr_context.cpp etc.) compile as part of :core, but backend defines (SK_VULKAN, SK_DIRECT3D, SK_METAL) may only live on :gpu's all_dependent_configs. Verify these reach :core's own compile — if not, macros like SK_ONLY_VULKAN(expr, fallback) expand to the fallback. Add the backend defines directly to :core in BUILD.gn.

22. Modules Made Optional

Upstream may move previously-core modules into separate optional targets. If the C API exposes functions from that module, add it as an explicit dependency of the SkiaSharp target in BUILD.gn rather than merging sources into core.

23. Required New Upstream GN Arg → build.cake (not a one-off flag)

Upstream sometimes introduces a build dependency our fork deliberately does not vendor. The fix is a durable gn arg in build.cake — not a CLI flag, and not a hack to silence the error.

Worked example — skia_use_partition_alloc. Upstream's gn/skia.gni defaults skia_use_partition_alloc = is_clang (true on clang). When true, BUILD.gn imports third_party/externals/partition_alloc/partition_alloc.gni — but our fork's DEPS keeps that entry commented out, so the folder is never fetched and the build aborts. Skia ships an official zero-overhead noop raw_ptr for exactly this case ("because the partition_alloc dependency is missing"), gated on SK_USE_PARTITION_ALLOC, so building with skia_use_partition_alloc=false is the supported embedder path, not a workaround — it disables Chromium's MiraclePtr/BackupRefPtr hardening (irrelevant to SkiaSharp's trusted-app threat model) with no runtime regression. Vendoring the allocator instead would add a large Chromium dependency across the whole platform matrix for marginal benefit.

Resolution:

  • Add the gn arg to every affected clang platform's native/**/build.cake gn-args list, next to the existing skia_use_* toggles — that file is the single source of truth. Do not rely on a one-off dotnet cake … --gnArgs flag (non-durable, and easily lost).
  • Sequencing: a gn arg added upstream after a milestone's branch point does not exist in that milestone. Adding it to build.cake before the submodule actually carries it fails the build with Unknown build argument. Only add it in the same change that advances the submodule to a tree that has the arg (e.g. a tip/main sync, or the milestone bump that introduces it).
  • This is only for a genuinely required arg. Never add a gn arg — or change compiler/linker flags — merely to silence a build error on one host; that is a missing-dependency problem (the host toolchain/packages), not a build-config one.

Dependencies & Bindings

8. DEPS: Fork-Customized Dependencies

SkiaSharp's fork often has newer dependency versions than upstream. When resolving DEPS conflicts, do NOT blindly take upstream's hashes — you may downgrade and break the build.

bash
git log --oneline skiasharp | grep -i "update\|bump\|libpng\|zlib\|expat\|brotli\|webp\|harfbuzz\|vulkan"

Keep fork's hash for customized deps. Common ones: libwebp, brotli, expat, libpng, zlib, vulkanmemoryallocator, harfbuzz.

9. HarfBuzz — ALWAYS Separate

HarfBuzz updates require hand-written C# delegate proxies and must be done via the native-dependency-update skill. During a milestone update, ALWAYS:

  1. Keep the fork's harfbuzz hash in DEPS
  2. Revert any generated HarfBuzz binding changes: git checkout HEAD -- binding/HarfBuzzSharp/HarfBuzzApi.generated.cs

10. Enum Value Renumbering

When upstream inserts new enum values mid-sequence, ALL subsequent values shift. This affects sk_enums.cpp, Definitions.cs, EnumMappings.cs, and any test hardcoding enum integers. Always regenerate bindings — never hand-edit enum values.

Diff Reading Traps

11. Deleted Files ≠ Deleted Functionality

Skia relocates files, it rarely removes them. Example: src/utils/SkJSON.hmodules/jsonreader/SkJSONReader.h in m133. Always search the target branch for where content moved before removing references:

bash
git ls-tree -r upstream/chrome/m{TARGET} --name-only | grep -i "FILENAME_STEM"

12. Reordered Fields ≠ Removed Fields

A symbol on a diff - line may have been moved within the same file, not removed. Always confirm on the target branch:

bash
git show upstream/chrome/m{TARGET}:FILEPATH | grep "SYMBOL"

Merge Strategy

13. Genuine Merge Required

Never use a tree-override merge (git merge -s ours, git read-tree --reset). This destroys git blame attribution for all C API files. Always resolve each conflict individually. Use git merge --no-commit for manual control.

14. Conflict Resolution by File Category

File CategoryStrategy
BUILD.gnCombine both — most complex; keep upstream structure AND SkiaSharp's platform flags/targets
DEPSCombine — keep our dependency pins, accept upstream structure
RELEASE_NOTES.md, infra/Take upstream
C API headers (include/c/)Keep SkiaSharp — these don't exist upstream
C API source (src/c/)Keep SkiaSharp + adapt — fix includes and API calls in post-merge commits
Other upstream source (src/, include/)Check history first — see gotcha #15

15. Verify-Upstream-or-Reapply — Never Blanket --theirs/--ours

Failure mode (two directions):

  • A conflict in an upstream file (outside src/c/ / include/c/) is resolved with git checkout --theirs, silently dropping an intentional SkiaSharp fork patch.
  • Or our side is blindly kept with --ours, freezing a stale form of a patch that upstream has since adopted and refined (e.g. our [M150] Turn off LCD in SDF slugs used getMaxScale(); upstream relanded it as sk_ieee_float_divide(1.f, getMinScale()) — keeping ours would have shipped the worse formulation).

A fork patch in a conflicted file is in exactly one of two states. You MUST determine which before resolving, and resolve accordingly. A blanket --theirs/--ours is never acceptable.

bash
# 1. Identify the fork patch(es) touching this file
git log --oneline {SKIA_BASE_BRANCH} -- <conflicted-file>      # e.g. skiasharp
# 2. For EACH fork patch, check whether upstream already contains the same change
git log --oneline upstream/{UPSTREAM_REF} --grep "<key phrase from the patch subject>"
git log -S "<distinctive line of code from the patch>" --oneline upstream/{UPSTREAM_REF} -- <conflicted-file>
Patch stateResolution
Upstreamed — upstream now contains an equivalent (or refined) formTake upstream's form. Our patch is redundant; convergence is correct (upstream may have improved it). Record "<subject>" upstreamed as <sha>.
Not upstreamed — only our fork carries itRe-apply our change on top of upstream's edits (absorb upstream's harmless additions AND keep our patch). Never drop it. Record "<subject>" re-applied.
  • Never git checkout --theirs/--ours as a shortcut for a file you have not classified.
  • Never "resolve what you reasonably can and move on" — that is how a fork patch gets lost.

Mandatory audit — no fork patch silently dropped. Snapshot the fork patches before merging so you can cross-reference every conflict against them:

bash
# before the merge — list our fork's commits on top of the merge base (the patches at risk)
MB=$(git merge-base {SKIA_BASE_BRANCH} upstream/{UPSTREAM_REF})
git log --oneline "$MB..{SKIA_BASE_BRANCH}" > /tmp/fork-patches-before.txt

For every conflicted file, find which fork patch(es) from that list touch it and classify each as upstreamed or re-applied (above). Every such patch must appear in the mono/skia PR's "Conflicts resolved" table with its disposition. A fork patch on a conflicted file that is neither upstreamed nor re-applied is a lost patch — STOP and fix it before committing the merge. (Fork patches whose files did not conflict merge cleanly and need no listing.)

Key signal words in commit messages that indicate intentional fork patches: Restore, patch, fix for, platform, workaround, SkiaSharp, iOS, Tizen, [M1xx]

Testing

16. Version Compatibility Errors

InvalidOperationException: The version of the native libSkiaSharp library (X) is incompatible means VERSIONS.txt wasn't fully updated. Fix the root cause — do NOT work around it.

17. Pixel Value Precision

Upstream periodically improves color conversion precision, shifting expected pixel values by ±1. When pixel-exact test assertions break, check if upstream changed the conversion and update expected values.

18. Test Runner

Tests use Skip.If() for unsupported platforms. Run dotnet test tests/SkiaSharp.Tests.Console.sln for the full suite. Backend-specific tests self-skip when hardware isn't available.


Troubleshooting

ErrorCauseFix
EntryPointNotFoundExceptionNative lib not rebuilt after C API changedotnet cake --target=externals-{platform}
error CS0246 missing typeBinding not regeneratedpwsh ./utils/generate.ps1
static_assert sizeof failureUpstream struct gained/lost fieldsUpdate C API struct in sk_types.h
#include file not foundUpstream moved file to new pathSearch target branch, update path
LNK2001 unresolved externalC function name mismatch or missing libVerify names; check system library linkage
Unknown GN flag errorObsolete build flagRemove flag; diff target BUILD.gn; check native/*/build.cake
git blame all from merge commitTree-override merge was usedRedo as genuine conflict-resolved merge
Merge conflict in DEPSBoth forks updated depsKeep our pins, accept upstream structure
Enum values don't matchMid-sequence insertionRegenerate bindings — never hand-edit
Pixel mismatch by ±1Upstream precision changeUpdate expected test values
GPU context C API returns nullptrBackend defines not reaching :coreAdd defines to :core in BUILD.gn
Vulkan GRContext returns nullVMA fallback compiled outCheck SK_USE_VMA is defined in GN build