## Thursday, May 13, 2021

### Isogeometric Analysis: Bezier Curves and Surfaces

While working on my sample IGA implementation here https://github.com/carlonluca/isogeometric-analysis, I found myself in need of defining and implementing the math structures used to define the domain and the solution space. This means that I had to implement various algorithms to define structures like Bezier, B-spline, NURBS and T-spline. So this is a quick intro to Bezier curves and surfaces with a couple of implementations.

## Power Basis

A simple math structure to handle curves and surfaces is the power basis representation. Power basis representation uses polynomials, which are fast to compute and simple to handle.
A $n^{th}$-degree power basis curve can be defined in the parametric space as:

$$\begin{eqnarray*} \boldsymbol{C}\left(\xi\right) & = & \left(x\left(\xi\right),y\left(\xi\right),z\left(\xi\right)\right)\\ & = & \sum_{i=0}^{n}\boldsymbol{a}_{i}\xi^{i}\\ & = & \left(\left[\boldsymbol{a}_{i}\right]_{i=0}^{n}\right)^{T}\left[\xi^{i}\right]_{i=0}^{n}, \end{eqnarray*}$$

where the functions $\xi^{i}$ are the basis (or blending) functions. Using the tensor product scheme we can also define a power basis surface:

$$\begin{eqnarray*} \boldsymbol{S}\left(\xi,\eta\right) & = & \left(x\left(\xi,\eta\right),y\left(\xi,\eta\right),z\left(\xi,\eta\right)\right)\\ & = & \sum_{i=0}^{n}\sum_{j=0}^{m}\boldsymbol{a}_{i,j}\xi^{i}\eta^{j}\\ & = & \left(\left[\xi^{i}\right]_{i=0}^{n}\right)^{T}\left[\boldsymbol{a}_{i,j}\right]_{i,j=0}^{i=n,j=m}\left[\eta^{j}\right]_{j=0}^{m} \end{eqnarray*}$$

where:

$$\left\{ \begin{array}{l} \boldsymbol{a}_{i,j}=\left(x_{i,j},y_{i,j},z_{i,j}\right)\\ b\leq\xi\leq c\\ d\leq\eta\leq e \end{array}\right.$$

## Bezier

Bezier curves do not increase the space of curves representable by the power basis form, but introduce the concept of control point. Control points convey a clear geometrical meaning, which is very useful during the design process. So, a $n^{th}$-degree Bezier curve is defined as:

$$\boldsymbol{C}\left(\xi\right)=\sum_{i=0}^{n}B_{i}^{n}\left(\xi\right)\boldsymbol{P}_{i},\;a\leq\xi\leq b$$

where $\boldsymbol{P}_{i}$ represents the $i^{th}$ control point and:

$$B_{i}^{n}\left(\xi\right)=\frac{n!\cdot\xi^{i}\left(1-\xi\right)^{n-i}}{i!\cdot\left(n-i\right)!}$$

is the $i^{th}$ basis function (also known as Bernstein polynomial). With the tensor product scheme, we can also define a Bezier surface with:

$$\boldsymbol{S}\left(\xi,\eta\right)=\sum_{i=0}^{n}\sum_{j=0}^{m}B_{i}^{n}\left(\xi\right)B_{j}^{m}\left(\eta\right)\boldsymbol{P}_{i,j},\;\left\{ \begin{array}{l} a\leq\xi\leq b\\ c\leq\eta\leq d \end{array}\right.$$

## Octave Implementation

In the repo https://github.com/carlonluca/isogeometric-analysis, you can find a basic implementation of Bezier curves and surfaces written for Octave (and Matlab) in: https://github.com/carlonluca/isogeometric-analysis/tree/master/3.3. The implementation is tested with a few examples. For example, given these control points:

$$\boldsymbol{P}_{0}=\left(0,0\right),\;\boldsymbol{P}_{1}=\left(1,1\right),\;\boldsymbol{P}_{2}=\left(2,0.5\right)$$ $$\boldsymbol{P}_{3}=\left(3,0.5\right),\;\boldsymbol{P}_{4}=\left(0.5,1.5\right),\;\boldsymbol{P}_{5}=\left(1.5,0\right)$$

we can get to this result by using the computeBezier.m script:

We can also define the curve in the 3D space. For example these control points:

$$\boldsymbol{P}_{0}=\left(0,0,0\right),\;\boldsymbol{P}_{1}=\left(1,1,1\right),\;\boldsymbol{P}_{2}=\left(2,0.5,0\right)$$ $$\boldsymbol{P}_{3}=\left(3,0.5,0\right),\;\boldsymbol{P}_{4}=\left(0.5,1.5,0\right),\;\boldsymbol{P}_{5}=\left(1.5,0,1\right)$$

It may also be interesting to see what happens to a curve when a new control point is added to the previous one:

Another example is provided for Bezier surfaces. From these control points:

$$\boldsymbol{P}_{0}=\left(-3,0,2\right),\;\boldsymbol{P}_{1}=\left(-2,0,6\right),\;\boldsymbol{P}_{2}=\left(-1,0,7\right),\boldsymbol{P}_{3}=\left(0,0,2\right)$$ $$\boldsymbol{P}_{4}=\left(-3,1,2\right),\;\boldsymbol{P}_{5}=\left(-2,1,4\right),\;\boldsymbol{P}_{6}=\left(-1,1,5\right),\;\boldsymbol{P}_{7}=\left(0,1,2.5\right)$$ $$\boldsymbol{P}_{8}=\left(-3,3,0\right),\;\boldsymbol{P}_{9}=\left(-2,3,2.5\right),\;\boldsymbol{P}_{10}=\left(-1,3,4.5\right),\;\boldsymbol{P}_{11}=\left(0,3,6.5\right)$$

this is what the algorithms produce:

## Typescript Implementation

What about a browser implemenation? The above algorithms can clearly be implemented in a browser, so this is an attempt written in TypeScript: https://github.com/carlonluca/isogeometric-analysis/tree/master/ts/src/bezier. The BezierCurve object can be used to draw a plot of the first examples:

The BezierSurf instead can be used to compute the surface example:

The TypeScript implementation is rendering the plots using Plotly. The demo embedded into this post can be found in this GitHub page.

## Tuesday, April 27, 2021

### Isogeometric Analysis and Finite Element Method

It's been some years since I completed my dissertation on FEM and Isogeometric Analysis, but I realised I never had the time to organise my code properly and archive it. So I archived a part of it in a new repo here: https://github.com/carlonluca/isogeometric-analysis. It may be useful for someone studying the topic.

The main topic of the dissertation is not the Finite Element Method (FEM) actually, but the first and the second chapters present it and I wrote a basic implementation that works for 1D problems here: https://github.com/carlonluca/isogeometric-analysis/blob/master/2.3/drawFEM1DExample.m. The first example uses the implementation to compute an approximation using FEM.

## Problem

In the code, the first example solves the differential equation:

$$\left\{ \begin{array}{ll} \frac{d^{2}u(x)}{dx^{2}}=10 & \forall x\in\Omega=\left(0,1\right)\\ u(x)=0 & x=0\\ u(x)=1 & x=1 \end{array}\right.$$

## Exact solution

It is possible to find an exact solution by integrating both parts twice:

$$\iint_{\Omega}u''(x)dxdx=\iint_{\Omega}10dxdx\Rightarrow u(x)=5x^{2}+c_{1}x+c_{2}$$

The solution of the system:

$$\left\{ \begin{array}{ll} u(x)=5x^{2}+c_{1}x+c_{2} & \forall x\in\Omega=\left(0,1\right)\\ u(x)=0 & x=0\\ u(x)=1 & x=1 \end{array}\right.$$

is the exact solution to the problem:

$$u(x)=x\cdot(5x-4)$$

## Weak Formulation

To calculate the weak formulation we need to first define a Dirichlet lift, as the boundary conditions are non-homogeneous:

$$u(x)=\gamma(x)+v(x)\Rightarrow(\gamma(x)+v(x))''=10$$

Multiply by a test function $\varphi\in C_{0}^{\infty}(\Omega)$:

$$(\gamma(x)+v(x))''\cdot\varphi(x)=10\cdot\varphi(x)$$

Now integrate both parts:

$$\int_{\Omega}(\gamma(x)+v(x))''\cdot\varphi(x)dx=\int_{\Omega}10\cdot\varphi(x)dx$$

Using the technique of integration by parts:

$$\int_{a}^{b}u(x)v'(x)dx=\left[u(x)v(x)\right]_{a}^{b}-\int_{a}^{b}u'(x)v(x)dx$$

we can get to:

$$\left[\varphi(x)\left(\gamma(x)+v(x)\right)'\right]_{\Omega}-\int_{\Omega}\varphi'(x)\left(\gamma(x)+v(x)\right)'dx=\int_{\Omega}10\varphi(x)dx$$

$\varphi$ is a distribution and it therefore vanishes on the boundary:

$$-\int_{\Omega}\varphi'(x)\left(\gamma(x)+v(x)\right)'dx=\int_{\Omega}10\varphi(x)dx$$

It is now possible to define the two terms:

$$a(v,\varphi)=\int_{\Omega}\varphi'(x)v'(x)dx$$ $$l(\varphi)=-\int_{\Omega}\left(\varphi'(x)\gamma'(x)+10\varphi(x)\right)dx$$

## Galerkin Method

At this point, the Galerkin method can be applied if we accept to look for an approximate solution in a space $V_{n}$ of dimension $dim(V_{n})=n$. Thus, assuming $\left\{ v_{i}\right\} _{i=0}^{n-1}$ is a basis for $V_{n}$, then we can write our approx solution:

$$\tilde{v}(x)=\sum_{i=0}^{n-1}\bar{v}_{i}\cdot v_{i}(x)$$

where $\left\{ \bar{v}_{i}\right\} _{i=0}^{n-1}$ are coefficients of the linear combination.
We can create a linear system with $n$ independent equations that can be written in matrix form as:

$$\boldsymbol{S}_{n}\cdot\boldsymbol{\Upsilon}_{n}=\boldsymbol{F}_{n}$$

where:

$$\boldsymbol{S}_{n}=\left[\int_{0}^{1}v_{i}v_{j}dx\right]_{i,j=0}^{n-1}$$ $$\boldsymbol{\Upsilon}_{n}=\left[\bar{v}_{i}\right]_{i=0}^{n-1}$$ $$\boldsymbol{F}_{n}=\left[-\int_{0}^{1}\left(v_{i}'(x)\gamma'(x)+10\gamma(x)\right)dx\right]_{i=0}^{n-1}$$

The basis functions $\left\{ v_{i}\right\} _{i=0}^{n-1}$ can be chosen according to the needs. In the example, simple roof functions are used: https://github.com/carlonluca/isogeometric-analysis/blob/master/2.3/computePhi.m and https://github.com/carlonluca/isogeometric-analysis/blob/master/2.3/computeDphi.m (note that nomenclature in the code is slightly different). Different approximations can be achieved with a different basis for the space $V_{n}$.
A possible choice for the Dirichlet lift is:

$$\gamma(x)=0\cdot v_{0}(x)+1\cdot v_{n-1}(x)$$

which is the one that is used in the example implementation.

## Result

The example script solves the problem for $n=2,...,7$. As expected with FEM, the approximation is exact at the nodes. By increasing the dimension of the space where the solution is to be found, the approximation gets closer to the exact curve.

## Lagrange Polynomials

In the previous implementation, roof functions were used. It is possible to use piecewise-polynomials of higher order. One possible implementation is Lagrange polynomials.

$$l_{Lag,i}\left(\xi\right)={\displaystyle \prod_{1\leq j\leq p_{m}+1,j\neq i}\dfrac{\left(\xi-y_{j}\right)}{\left(y_{i}-y_{j}\right)},\;i=1,2,\ldots,p_{m}+1}$$

In the repo there is a sample implementation. The demo draws Lagrange polynomials interpolating an increasing number of points:

## Blog

It's been many years now since I started to use a Pi as a version control server. I recently even started to use it more heavily for my development, using it for running my unit tests and for CI/CD. I list here the alternatives I tested and the results.

## Subversion

The first version control system that I used on the Pi was SVN. It does not probably make much sense to use SVN as a version control service in 2021, but I still have old SVN repos that I do not want to migrate to git. If this is the case for you as well, user krisdavison prepared a docker image that includes a SVN server with a web server to browse the repos: https://hub.docker.com/r/krisdavison/svn-server. Unfortunately, krisdavison is only providing amd64 images, so I created my own fork, including images for armv7, arm64, x86 and x64, which should cover Pi versions from 2 to 4: https://hub.docker.com/r/carlonluca/docker-svn.
If you prefer, Raspbian also offers the SVN server package and WebSVN for browsing.

## Gitea

At the beginning, I simply installed git in my Raspbian to be able to push to remotes stored in my pi. This was quick, worked well and I could also browse with GitWeb https://git-scm.com/book/en/v2/Git-on-the-Server-GitWeb. Raspbian includes all you need to setup git and GitWeb on Apache.

After some time though, I started to feel this was a bit restrictive. I could benefit from an issue tracker, a better browser experience with syntax highlighting etc... Therefore, I started to think about installing GitLab on my Pi. On my first Pi 2, this was difficult and, as a matter of fact, impossible, due to hardware limitations (it was taking minutes to load the first login page). 1GB of RAM, with other services running, is not enough for GitLab, so I started to look for other options and I found Gitea: https://gitea.io. Gitea is an excellent git service written in Go, it does not provide all the features GitLab offers, but is incredibly fast and requires much much less RAM. Unfortunately, the Gitea team was not providing a docker arm image for my Pi (https://hub.docker.com/r/gitea/gitea), so I started to build my own fork for x86, amd64, armv6, armv7 and armv8. For Pi 2, the armv7 image was working perfectly fine: https://hub.docker.com/r/carlonluca/gitea.

Gitea is a great self-hosted service if you are running on a Pi. It is a git service with many features, it is fast and it is lightweight.

## GitLab

Unfortunately, all good things come to an end :-( and my glorious Pi 2 died in a tragical stormy summer night. RIP. I therefore "had" to switch to a shiny new Pi 4, with 4GB of RAM. This led me to think: "hey, what about GitLab now"?!
It seems GitLab is not currently providing docker images for arm64 (or any other arm variant): https://hub.docker.com/r/gitlab/gitlab-ce. I therefore built my own image for arm64: https://hub.docker.com/r/carlonluca/gitlab. The result is awesome. GitLab is a bit heavy, but it works well. Follow the same instructions of the original image (https://docs.gitlab.com/omnibus/docker) and everything should work. It is a bit slower than Gitea, much heavier, but has more to offer. In particular, package repos and the docker registry can be very handy.
Reference repo for GitLab docker image is https://github.com/carlonluca/docker-gitlab.

## Jenkins

Another interesting topic in software development is continuous integration and deployment. As I'm used to Jenkins, this is what I looked into at first. There is a good official docker image for Jenkins: https://hub.docker.com/r/jenkins/jenkins. Unfortunately, atm, the image is only provided for amd64. I therefore, again, forked and created my arm64 image: https://hub.docker.com/r/carlonluca/jenkins. On my Pi 4, Jenkins behaves fine. You can also create other docker containers in your Pi and communicate with those containers from the Jenkins container. Everything seems to be working fine.

## GitLab Runner

After some time, I wanted to also try the CI/CD feature included in my GitLab arm64 image. I therefore created a few gitlab configurations and configured a GitLab runner on the same Pi 4. The configuration of the runner was a bit tricky, but seems to be possible. In particular I had troubles using my internal DNS service, but host networking for the runner seems to work so far. Luckily, the runner is already available for arm64: https://hub.docker.com/r/gitlab/gitlab-runner. I created a configuration for a node app and created a CI configuration using a mongo docker image for running my unit tests. Everything worked properly. So you can have your node + angular apps versioned and tested on your arm64 Pi.

## Dind

Of course, I also wanted to create a docker image for my node app. So I tried to use the dind service included in GitLab, to see if it could be used on the Pi properly. Yes, it worked :-) So you can have your app versioned on the Pi, tested on the Pi, dockerized on your Pi and uploaded to docker hub or the GitLab registry on the Pi.

## Multiarch

The image resulting from the previous step worked great, it can be installed and used properly... on arm64. Also, the images I listed above are pretty long to build and maintain. Gitea, in particular, needs a long building process on an Intel Xeon, because the simplest procedure needs emulators for cross-arch building. Also, I have a slow Internet service, which is even slower when uploading. I was wondering if it could be possible to do all this on the Pi and let it build and transfer for me, instead of keeping my main machine busy... Yes, it is technically possible :-) qemu seems to be able to emulate other arm archs and amd64 on an arm64 kernel.
To do this, I needed an image including docker and buildkit for arm64: I couldn't find one, so I created one for me: https://hub.docker.com/r/carlonluca/docker-multiarch. At this point I wrote the gitlab CI file and tested. Unfortunately, I got less encouraging results this time: I got many types of failures, probably due to several unrelated causes. I could however identify some:
• slow Internet connection: it seems that docker or buildkit have too short timeouts. Couldn't find a way to set the timeout, but reducing concurrency of builds reduced the frequency of errors (see below).
• ubuntu binfmt not properly working in some cases: I used this project this fix this issue: https://hub.docker.com/r/tonistiigi/binfmt. I use it in all my GitLab CI files.
• buildx building with high concurrency: the authors tried to make full use of the build machine by building the images concurrently. This is reasonable, but... they are not currently providing a way to reduce or remove concurrency when needed... which is less reasonable. On the Pi, concurrent complex builds are difficult/impossible. If a single build requires much RAM, 4 builds concurrently over emulators require really too much RAM. The only way I found to build sequentially is to build and push each arch separately, and then use the manifest tool to merge. Here is an example: https://github.com/carlonluca/darkmediawiki-docker/blob/master/.gitlab-ci.yml.
I'm still working on this topic, but I'm mostly able to multiarch-build on my Pi with GitLab CI/CD. All the docker images I listed above are built this way, including the docker-multiarch image, which requires itself to build itself :-) this is a handy way of keeping them up to date. The only partial exception is Gitea: the build procedure of the image also builds Gitea itself, and there seems to be an issue building for x86. The other archs seem to properly build.

This is a list of the images I'm currently using on my Pi 4 in this context:
* https://hub.docker.com/r/carlonluca/docker-svn
* https://hub.docker.com/r/carlonluca/gitea
* https://hub.docker.com/r/carlonluca/gitlab
* https://hub.docker.com/r/carlonluca/jenkins
* https://hub.docker.com/r/gitlab/gitlab-runner
* https://hub.docker.com/r/carlonluca/docker-multiarch

Have fun! ;-)

## Friday, July 17, 2020

### Serialize/Deserialize JSON Representations to Qt Objects (QObject's) in C++

Calls to HTTP servers are very frequent nowadays, and JSON is frequently used when dealing with structured data. When I write applications in Java/Kotlin, I always use retrofit2 with gson integration, or similar approaches. It is very comfortable to be able to serialize/deserialize to Java objects automatically. I tried to find something similar for Qt, but I couldn't find much. JSON support in Qt is good, but does not seem to be able to map QOject's to QJSonObject. I also could not find other C++ libraries doing this. Probably the difficulty of implementing reflection in C++ would make the implementation a bit convoluted. A good candidate I found is this: https://github.com/Loki-Astari/ThorsSerializer, which seems not to require much boilerplate code, not too verbose and no generated code.

However, Qt includes the meta object system (https://doc.qt.io/qt-5/metaobjects.html), and therefore I tried to use it to get the desired result. In a couple of hours I got to a pretty decent result: https://github.com/carlonluca/lqobjectserializer. It is far from perfect, but it seems to work. Have a look at the readme or the unit tests to know more.
The draft is free to use: you can contribute, report bugs etc...

For instance, a JSON object like this:

"items": [
{"id": "Open"},
{"id": "OpenNew", "label": "Open New"},
null,
{"id": "ZoomIn", "label": "Zoom In"},
{"id": "ZoomOut", "label": "Zoom Out"},
{"id": "OriginalView", "label": "Original View"},
null,
{"id": "Quality"},
{"id": "Pause"},
{"id": "Mute"},
null,
{"id": "Find", "label": "Find..."},
{"id": "FindAgain", "label": "Find Again"},
{"id": "Copy"},
{"id": "CopyAgain", "label": "Copy Again"},
{"id": "CopySVG", "label": "Copy SVG"},
{"id": "ViewSVG", "label": "View SVG"},
{"id": "ViewSource", "label": "View Source"},
{"id": "SaveAs", "label": "Save As"},
null,
{"id": "Help"},
]
}}

can be deserialized to a QObject simply by defining the classes (macros here are inherited by my other project https://github.com/carlonluca/lqtutils, but this is not mandatory):

L_BEGIN_CLASS(Item)
L_RW_PROP(QString, id, setId, QString())
L_RW_PROP(QString, label, setLabel, QString())
L_END_CLASS

L_END_CLASS

L_END_CLASS

and by writing these few lines:

Bye! ;-)

## Sunday, June 14, 2020

### Synthesize Qt Settings

I frequently create classes that I use as an interface to settings in my apps. Using a single class with accessors makes the code readable, safer and simple to maintain. Unfortunately, doing this requires to create getters and setters for every entry in the settings file, which is a bit bothering. Also, QSettings is not a QObject and cannot provide signals to notify changes.

My use cases are typically very simple, so I created a couple of macros that synthesise classes for me. Macros synthesise a reentrant class that can be used to access settings and a notifier, that can be used to get notifications of the changes. An example of definition of the class is:

L_DECLARE_SETTINGS(LSettingsTest, new QSettings("settings.ini", QSettings::IniFormat))
L_DEFINE_VALUE(QString, string1, QString("string1"), toString)
L_DEFINE_VALUE(QSize, size, QSize(100, 100), toSize)
L_DEFINE_VALUE(double, temperature, -1, toDouble)
L_DEFINE_VALUE(QByteArray, image, QByteArray(), toByteArray)
L_END_CLASS

L_DECLARE_SETTINGS(LSettingsTestSec1, new QSettings("settings.ini", QSettings::IniFormat), "SECTION_1")
L_DEFINE_VALUE(QString, string2, QString("string2"), toString)
L_END_CLASS

this will let you instantiate objects of class LSettingsTest and LSettingsTestSec1, and access entries with strong typed methods. Also, by calling LSettingsTest::notifier(), you can get a reference to the unique notifier. By setting an instance as a context property, you can get notifications and you can update settings from QML. You can find some more info in the repo https://github.com/carlonluca/lqtutils and an example using Qt Quick here: https://github.com/carlonluca/lqtutils/tree/master/LQtUtilsQuick.
Bye! ;-)

## Thursday, May 28, 2020

### Synthesize Qt properties

The are some utils that I use in most of my Qt projects. I'm a bit fed up of copy-pasting those every time I need something, so I created a project containing tools that are of frequent use when I write Qt apps, regardless of the type of app.

The only interesting tool I added yet is the synthesizer of Q_PROPERTY's. You can use the macros:
L_RW_PROPL_RO_PROP
to synthétise props, with getter, setter and notifications. Each macro is overloaded: with 3 params, you do not have initialisation, adding a 4th param, also initialises the variable to the provided value. Also, I frequently need QObject that are just containers of properties, to be used in QML. I added these two macros to spare some lines:
L_BEGIN_CLASS(<class_name>)L_END_CLASS
A class like:
class Fraction : public QObject{    Q_OBJECT    Q_PROPERTY(double numerator READ numerator WRITE setNumerator NOTIFY numeratorChanged)    Q_PROPERTY(double denominator READ denominator WRITE setDenominator NOTIFY denominatorChanged)public:    Fraction(QObject* parent = nullptr) : QObject(parent) {}    double numerator() const {        return m_numerator;    }    double denominator() const {        return m_denominator;    }public slots:    void setNumerator(double numerator) {        if (m_numerator == numerator)            return;        m_numerator = numerator;        emit numeratorChanged(numerator);    }    void setDenominator(double denominator) {        if (m_denominator == denominator)            return;        m_denominator = denominator;        emit denominatorChanged(denominator);    }signals:    void numeratorChanged(double numerator);    void denominatorChanged(double denominator);private:    double m_numerator;    double m_denominator;};
can be simplified to:
L_BEGIN_CLASS(Fraction)L_RW_PROP(double, numerator, setNumerator)L_RW_PROP(double, denominator, setDenominator)L_END_CLASS
I'm sure there are other similar approaches out there, but if you need it, you can simply add this repo as a submodule like I do: https://github.com/carlonluca/lqtutils.
Bye! ;-)

## Monday, September 9, 2019

### Raspbian Buster and Cross Toolchain for Linux and Mac OS 8.3.0

Raspbian Buster is out. It seems it updated gcc from version 6.3.0 (used for Stretch) to gcc version 8.3.0. This is not always needed, but I had a lot of problems in the past related to using an outdated crosscompiler, or the one provided in the raspberry repo: https://github.com/raspberrypi/tools. I therefore built my own toolchain for Stretch, which I provided here, both for Linux and Mac OS.

I did the same for Buster: I built my own toolchain and I built many projects with it, like Qt, ffmpeg etc... Just place the toolchain in /opt/rpi (probably position independent) and you should be done. This toolchain should be 100% compatible with gcc provided by Buster.