Skip to content

Instantly share code, notes, and snippets.

@peters
Forked from jclapis/skiasharp_arm64.md
Created September 4, 2020 17:29
Show Gist options
  • Save peters/716940e676a49dd4d8fd4d02e317c9b3 to your computer and use it in GitHub Desktop.
Save peters/716940e676a49dd4d8fd4d02e317c9b3 to your computer and use it in GitHub Desktop.
Building SkiaSharp on a Raspberry Pi 4B

Recently, I've been playing around with a Raspberry Pi 4B that I just picked up. It's an awesome little device for the price; I'm really impressed with what it can do so far. I'm planning to use it as a driver for a DIY observatory I'm working on. As part of that, I'm writing an application that can remote control my camera and a custom motorized mount for my telescope. I decided to use .NET Core as my framework of choice; I'm a big fan of C# and its ecosystem, and the fact that .NET Core 3.1 works on ARM64 systems (which is what the Pi 4B runs) makes it a no-brainer.

Unfortunately, .NET Core doesn't come with any cross-platform UI frameworks out of the box. I use WPF a lot for Windows projects, but one of its biggest weaknesses is that it isn't supported on anything else. The WPF repo already has a very long thread about this very topic, so it's definitely one that's on a lot of peoples' minds. That being said, that thread makes a lot of references to the Avalonia project. Avalonia is basically a cross-platform reimagination of WPF with .NET Core support, which makes it a huge win for me and my Pi!

I tried out the latest version (0.9.2 as of this writing), and got very close to making it work... but it had one little problem. One of the dependencies, a graphics library called SkiaSharp, wasn't loading properly. The main culprit is that Avalonia provides a native runtime library for SkiaSharp on ARM32, but not one for ARM64. After doing a little digging, I wasn't the only one that hit this problem.

If you're reading this, then you probably ran into this issue as well. My solution was to build the native runtime library for ARM64 myself. Here's my process for doing that.

My Setup

My Pi is running Xubuntu 19.10 (I'm a fan of xfce). Bluesabre has a nice walkthrough of how to install it here. The USB problem has been solved with v19.10.1, so you can use the full 64-bit ARM image instead of the Hard-Float one now. I'm using the 4 GB RAM model with a 64 GB SD card. I built the whole library on the Pi because apparently cross-compiling for ARM64 on an x64 machine isn't supported.

Installing Dependencies

My Pi already had a lot of stuff on it since it was already in use as a dev box. I don't know exactly what SkiaSharp needs, but at the very least I was missing the headers for fontconfig. You're also going to need ninja. You can install them both with apt:

$ sudo apt install libfontconfig1-dev ninja-build

There might be some other stuff you'll need that I missed here, but it should be fairly easy to figure them out and install them as you go.

Next, you're going to need to build gn from source. SkiaSharp uses it to generate its ninja build files. It includes a binary, but unfortunately the binary is x64, so we can't use it on a Pi.

$ git clone https://gn.googlesource.com/gn
$ cd gn
$ python build/gen.py

Before you build it, by default gn wants to use clang++ as its compiler. If you have it installed already, go for it. I just opted to use g++ since it comes with Ubuntu. If you want to use g++ too, open out/build.ninja with your favorite editor, and change the top lines to this:

xx = g++
ar = ar
ld = g++

Now you can build it:

$ ninja -C out

This will take a while, but it should build successfully. Once it's done, the binary will be gn/out/gn.

Preparing SkiaSharp

First, clone Skia and grab its other dependencies:

$ cd ..
$ git clone https://github.com/mono/skia.git
$ cd skia
$ python tools/git-sync-deps

Once that's done, open the file gn/BUILDCONFIG.gn. Go to line 92, and change it to this:

is_clang = exec_script("is_clang.py",

Basically, just modify the path of is_clang.py by removing the leading gn/ directory. Without this, it will end up looking in the wrong directory during the ninja file creation process.

Now, replace the included gn binary with the one you just built:

$ mv bin/gn bin/gn_old
$ cp ../gn/out/gn bin

Now you need to kick gn off. It has a lot of arguments, so I preferred to make a build script for it. Create a new file here called build.sh with this as the contents:

./bin/gn gen 'out/linux/arm64' --args='
    is_official_build=true skia_enable_tools=false
    target_os="linux" target_cpu="arm64"
    skia_use_icu=false skia_use_sfntly=false skia_use_piex=true
    skia_use_system_expat=false skia_use_system_freetype2=false skia_use_system_libjpeg_turbo=false skia_use_system_libpng=false skia_use_system_libwebp=false skia_use_system_zlib=false
    skia_enable_gpu=true
    extra_cflags=[ "-DSKIA_C_DLL" ]
    linux_soname_version="68.1.1"'

The last line will just append a version number to the libSkiaSharp.so file that gets produced at the end, so you can probably take it out if you want but I left it in.

Make this file executable, then kick it off:

$ chmod +x build.sh
$ ./build.sh

Building SkiaSharp

Before building the library, you have to make one small adjustment to the source. The details are best captured by solabc16 in this issue on the SkiaSharp repo. Make the changes that he specifies in his patch (which I've copied below for convenience):

--- ./skiasharp/externals/skia/src/opts/SkRasterPipeline_opts.h-orig	2019-03-26 21:10:09.615000000 +0000
+++ ./skiasharp/externals/skia/src/opts/SkRasterPipeline_opts.h	2019-03-26 21:13:28.961000000 +0000
@@ -653,10 +653,7 @@
 }
 
 SI F from_half(U16 h) {
-#if defined(__aarch64__) && !defined(SK_BUILD_FOR_GOOGLE3)  // Temporary workaround for some Google3 builds.
-    return vcvt_f32_f16(h);
-
-#elif defined(JUMPER_IS_HSW) || defined(JUMPER_IS_AVX512)
+#if defined(JUMPER_IS_HSW) || defined(JUMPER_IS_AVX512)
     return _mm256_cvtph_ps(h);
 
 #else
@@ -673,10 +670,7 @@
 }
 
 SI U16 to_half(F f) {
-#if defined(__aarch64__) && !defined(SK_BUILD_FOR_GOOGLE3)  // Temporary workaround for some Google3 builds.
-    return vcvt_f16_f32(f);
-
-#elif defined(JUMPER_IS_HSW) || defined(JUMPER_IS_AVX512)
+#if defined(JUMPER_IS_HSW) || defined(JUMPER_IS_AVX512)
     return _mm256_cvtps_ph(f, _MM_FROUND_CUR_DIRECTION);
 
 #else

Once that's done, you should be able to build it!

$ ninja 'SkiaSharp' -C 'out/linux/arm64'

This will take a while to complete, but when it's done, your new library should be in out/linux/arm64/libSkiaSharp.so.68.1.1.

Using the Library in an Avalonia App on the Pi

To get your Avalonia app to work, all you need to do is rename this library to libSkiaSharp.so, and copy it into the output directory of your app. You want it to live in the same directory as SkiaSharp.dll and the DLL for your project. You should be able to kick your app off with dotnet now, and everything should work. Enjoy Avalonia on your Pi!

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