Mandelbrot Fractal

Mandelbrot Fractal

Task: Render the Mandelbrot fractal. [Source Code]

What is the Mandelbrot Fractal?

It's a set of repeating self-similar shapes. Consider the relation $z[n+1] = z^2[n] + c$ where $z$ is a complex number and $c$, called the 'parameter', is a complex constant. The Mandelbrot set then is the set of all values of $c$ so that the relation does not fly off to infinity. To quote an example from Wiki, putting $c=1$ makes the sequence ${0,1,2,5,26, \dots}$ to infinity, so $c=1$ is not part of the set. Setting $c=-1$ on the other hand makes the sequence ${0,-1,0,-1,0, \dots}$ which stays bounded. This article from the University of Kent offers the best explanation in my opinion.

Results

Here's the fractal rendering 256 iterations:

I've only just gotten started with the Intruments profiler. These are the results with a 800x600 resolution. I've tried two things, one was to see what takes up most of my compute - what a profiler is meant for.

Analysis of Resource Events in Xcode Instruments

We see that 11.34 of the 13.09 memory is being used by allocate_drawable_texture - the renderer. So the calculation is not the bottleneck here. The render process currently refreshes the entire screen when the view shifts and this is my best guess for why this much memory needed consistently. For a long time now, any rendering software only updates the component that needs to be updated (incremental rendering), not the whole view. Perhaps I will retry this using MTKView instead of CA::MetalLayer entirely.

The other thing I saw (and also something that Apple recommends you should test) is how long it takes to render one frame.

Analysis of frame rate in Xcode Instruments

In the above snapshot we see one frame typically takes around 13.3 milliseconds, which corresponds to a frame rate of $\frac{1000}{13.3} \approx 75$ FPS. Pretty good.

Logical Explanation

There are two components to the whole app. First, we must render the fractal. And second, we must allow the user to browse around.

MSL Code

The MSL Code is pretty straightforward. I find it's been one of the most direct translations of logic into code yet.

Our condition per the rules is to ensure that the radius of the circles never exceeds 2. So we should be checking if the root of the principal components is less than or equal to 2. Calculating the square-root is an avoidable calculation and so we just do if (dot(z, z) > 4.0f).

Lastly, we have the logic for adding colour to our fractal. For a nice gradient, I've used sinusoids, a pretty common technique.

float t = 0.1f * smooth_i;
float r = 0.5f + 0.5f * sin(t + 0.0f);
float g = 0.5f + 0.5f * sin(t + 2.094f); // 120 degrees phase shift
float b = 0.5f + 0.5f * sin(t + 4.188f); // 240 degrees phase shift

... which correspond to the RGBA values.

Understanding the Simple DirectMedia Layer (SDL)

SDL3 was released early this year and with it came an amazing set of features to better support Metal. With native Metal API integration, it's possible to use Metal in cpp without metal-cpp altogether. But I didn't do that. I've used metal-cpp anyway and used SDL alongside, mostly for window management.

Core Animation Layer

The CA::MetalLayer rendering surface that bridges Apple's Metal graphics API with windowing systems like SDL. It acts as the target for GPU-rendered content, managing drawable textures presented in your app's window. I got a very good understanding of this from the Schneide Blog.

Seasoning with attributes

I have added two optimisation directives in this code. The important one is atop this function definition:

[[nodiscard("The returned MetalLayer must be configured and used.")]]
auto initMetalLayer(WindowGUI& windowGUI) -> CA::MetalLayer*

The [[nodiscard]] attribute ensures that an object returned by this function is actually used and not forgotten. I have added it here as forgetting to use it could introduce a dangling pointer. I also added:

if (!pDrawable) [[unlikely]] { return; }

[[[unlikely]]] tells the compiler that the condition is unlikely and allows the compiler to optimize the code accordingly. I typically wouldn't bother with this but the renderFrame method (which this snippet belongs to) is called often and every little bit helps.

a LEAP towards safety ...

I've had a failed attempt in trying to incorporate smart pointers before and I've finally made it happen. I tested this code with leaks and happily, but also not surprisingly there were no leaks. I was doing two things wrong. Firstly, there isn't a direct equivalent of std::make_unique in Objective-C, so the best other option is to use NS::RetainPtr. The other thing I was doing wrong was scoping the AutoReleasePool within the MetalObjects constructor. When I initially did this, the pool would delete the members of this struct (all good). Then the smart pointer would join and find it's objects were already deleted. Bam. So to fix this, I've moved the AutoReleasePool to the main() block so it let's the smart pointers do it's thing.

Gratitude

I owe a big thanks to Jacob for writing his blog post on the same topic - it's the first time I heard about Metal and well... I forgot about it until now. Happy I could finally make it work.

Also to Luple for his idea on how to colour a fractal.