Main Window Code Behind
In this code, Context.Create
initializes an ILGPU context, providing an environment for GPU computations and
configuring the devices available to the program. When creating this context with builder => builder.Cuda()
,
the program specifies that it should use CUDA-compatible devices, such as NVIDIA GPUs, allowing ILGPU to leverage CUDA’s
processing capabilities. Once the context is set, context.GetPreferredDevice(preferCPU: false)
selects the
most suitable device within the context for GPU computations, prioritizing a GPU over the CPU, which is typically faster
for parallel operations like those needed for rendering the Mandelbrot set. This selected device is used to create an
Accelerator, which will handle all GPU-specific tasks.
Next, accelerator.LoadAutoGroupedStreamKernel
loads the ComputeMandelbrotFrame
function as a
kernel, or parallel processing function, on the GPU. Using LoadAutoGroupedStreamKernel
allows ILGPU to
automatically group and distribute this kernel’s workload across the GPU’s cores, optimizing its parallel processing power.
The kernel itself is designed to compute Mandelbrot values in parallel, with each thread handling the calculations for one
pixel. To store the results, accelerator.Allocate1D(width * height)
allocates a 1D buffer on the GPU
with a size equal to the number of pixels in the image. Each int in this buffer represents the iteration count or colour
index for a specific pixel in the Mandelbrot set.
To ensure that all GPU computations finish before the CPU proceeds, accelerator.Synchronize()
is called.
Synchronization is crucial here to avoid retrieving incomplete data from the GPU. Once synchronized,
buffer.GetAsArray1D()
converts the GPU buffer into a standard 1D array of int values, bringing the results
back to the CPU. Each value in this array corresponds to the iteration count of a pixel, which is then mapped to a colour
using a colour-mapping function. This colour data is used to create a Bitmap in C#, where each pixel is assigned a colour
based on its iteration count. This approach produces a full-colour Mandelbrot image, efficiently computed by leveraging
the GPU.
MainWindows.xaml.cs
// Fast Mandelbrot Rendering with GPU in C#.
// Guy Fernando - i4cy (2024)
using System.Globalization;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using ILGPU;
using ILGPU.Runtime;
using ILGPU.Runtime.Cuda;
using Mandelbrot;
namespace Mandelbrot;
public static class MandelbrotConstants
{
public const short MaxIterations = 1000;
}
public partial class MainWindow : Window
{
private short width;
private short height;
private double centerX = -0.74;
private double centerY = 0.15;
private double scale = 2.5;
private bool isPanning = false;
private bool isZooming = false;
private Point startPanPoint;
private Context context;
private Accelerator accelerator;
private Action
<Index1D, ArrayView1D<int, Stride1D.Dense>,
double, double, double, short, short> kernel;
public MainWindow()
{
InitializeComponent();
// Initialize ILGPU context and accelerator.
context = Context.Create(builder => builder.Cuda());
accelerator =
context.GetPreferredDevice(preferCPU: false).
CreateAccelerator(context);
// Load the kernel once during initialization.
kernel =
accelerator.LoadAutoGroupedStreamKernel
<Index1D, ArrayView1D<int, Stride1D.Dense>,
double, double, double, short, short>
(MandelbrotKernel.ComputeMandelbrotFrame);
// Add event handlers.
this.KeyDown += MainWindow_KeyDown;
this.SizeChanged += MainWindow_SizeChanged;
// Generate the initial Mandelbrot set.
GenerateMandelbrotFrame();
}
private void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e)
{
// Update the width and height based on the new window size.
width = (short)e.NewSize.Width;
height = (short)e.NewSize.Height;
// Regenerate the Mandelbrot set with the updated dimensions.
GenerateMandelbrotFrame();
}
private void MainWindow_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Space)
{
if (!isZooming)
{
isZooming = true;
StartAutoZoom();
}
else
{
isZooming = false;
}
}
}
private async void StartAutoZoom()
{
// Set fixed coordinates for auto-zoom, near a point of interest.
centerX = -0.74335165531181;
centerY = +0.13138323820835;
// Zoom speed multiplier.
const double zoomFactorIncrement = 0.95;
// Stop when zoom factor is extremely high.
while (isZooming && scale > 1e-13)
{
// Reduce the zoom scale.
scale *= zoomFactorIncrement;
// Render the Mandelbrot set at the new zoom level.
GenerateMandelbrotFrame();
// Small delay ensuring UI responsiveness.
await Task.Delay(1);
}
}
private void UpdateStatusBar()
{
CenterXText.Text = $"Center X: {centerX:F14}";
CenterYText.Text = $"Center Y: {centerY:F14}";
// Display zoom factor in engineering format
string zoomFormatted =
(1 / scale).ToString("F1", CultureInfo.InvariantCulture);
ZoomFactorText.Text = $"Zoom: {zoomFormatted}";
}
protected override void OnClosed(EventArgs e)
{
// Cleanup resources on window close.
base.OnClosed(e);
accelerator.Dispose();
context.Dispose();
}
private void GenerateMandelbrotFrame()
{
if (width <= 0 || height <= 0)
return; // Skip rendering if dimensions are invalid.
// Update Status Bar.
UpdateStatusBar();
// Calculate the aspect ratio
double aspectRatio = (double)width / height;
// Determine the scaling factors to maintain aspect ratio.
double adjustedScaleX, adjustedScaleY;
if (aspectRatio >= 1.0)
{
adjustedScaleX = scale * aspectRatio;
adjustedScaleY = scale;
}
else
{
adjustedScaleX = scale;
adjustedScaleY = scale / aspectRatio;
}
// Allocate memory on the GPU.
using var buffer = accelerator.Allocate1D<int>(width * height);
// Execute the GPU kernel with the current parameters.
kernel(
(int)(width * height), buffer.View,
centerX, centerY, scale, width, height);
// Wait for all GPU kernel processes to complete.
accelerator.Synchronize();
// Retrieve the results from GPU
int[] result = buffer.GetAsArray1D();
// Set the Image control source to display the Mandelbrot set.
MandelbrotImage.Source = CreateFrameBitmap(result);
}
private WriteableBitmap CreateFrameBitmap(int[] pixels)
{
// Create a WriteableBitmap filled with the Mandelbrot set image.
WriteableBitmap bitmap =
new WriteableBitmap(
width, height, 96, 96, PixelFormats.Bgra32, null);
bitmap.Lock();
unsafe
{
IntPtr pBackBuffer = bitmap.BackBuffer;
for (short y = 0; y < height; y++)
{
for (short x = 0; x < width; x++)
{
Color color = GetPixelColor(pixels[y * width + x]);
*((uint*)pBackBuffer + y * width + x) = (uint)(
(color.A << 24) |
(color.R << 16) |
(color.G << 8) |
(color.B << 0) );
}
}
}
bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
bitmap.Unlock();
return bitmap;
}
private static Color GetPixelColor(int iterations)
{
if (iterations >= MandelbrotConstants.MaxIterations)
{
return Colors.Black;
}
else
{
// Convert HSV to RGB for a more colour pleasing image.
return ColorFromHSV(
((double)(iterations)) /
MandelbrotConstants.MaxIterations * 360.0,
1.0,
1.0
);
}
}
public static Color ColorFromHSV(
double hue, double saturation, double value)
{
sbyte hi = Convert.ToSByte(Math.Floor(hue / 60) % 6);
double f = hue / 60 - Math.Floor(hue / 60);
value = value * 255;
byte v = Convert.ToByte(value);
byte p = Convert.ToByte(value * (1 - saturation));
byte q = Convert.ToByte(value * (1 - f * saturation));
byte t = Convert.ToByte(value * (1 - (1 - f) * saturation));
if (hi == 0)
return Color.FromArgb(255, v, t, p);
else if (hi == 1)
return Color.FromArgb(255, q, v, p);
else if (hi == 2)
return Color.FromArgb(255, p, v, t);
else if (hi == 3)
return Color.FromArgb(255, p, q, v);
else if (hi == 4)
return Color.FromArgb(255, t, p, v);
else
return Color.FromArgb(255, v, p, q);
}
}