Sixty fractals per second

Originally posted to Shawn Hargreaves Blog on MSDN, Monday, December 11, 2006

The Xbox GPU is a shading monster!

I've written several Mandelbrot viewers over the years, but this is the first time I've ever been able to move around this at a rock solid 60 frames per second:

The trick to making this fast is to do all the heavy lifting on the GPU. I'm computing the fractal entirely inside my pixel shader, using this Mandelbrot.fx effect file:

    #define Iterations 128

float2 Pan;
float Zoom;
float Aspect;

float4 PixelShader(float2 texCoord : TEXCOORD0) : COLOR0
{
float2 c = (texCoord - 0.5) * Zoom * float2(1, Aspect) - Pan;
float2 v = 0;

for (int n = 0; n < Iterations; n++)
{
v = float2(v.x * v.x - v.y * v.y, v.x * v.y * 2) + c;
}

return (dot(v, v) > 1) ? 1 : 0;
}

technique
{
pass
{
PixelShader = compile ps_3_0 PixelShader();
}
}

Since the GPU is doing all the work, my C# code is pretty simple. Starting with the default Xbox 360 Game project, you need to add a few fields:

    Effect mandelbrot;
SpriteBatch spriteBatch;
Texture2D dummyTexture;

Vector2 pan = new Vector2(0.25f, 0);
float zoom = 3;

Add this to the LoadGraphicsContent method:

    mandelbrot = content.Load<Effect>("Mandelbrot");

spriteBatch = new SpriteBatch(graphics.GraphicsDevice);

int w = graphics.GraphicsDevice.Viewport.Width;
int h = graphics.GraphicsDevice.Viewport.Height;

dummyTexture = new Texture2D(graphics.GraphicsDevice, w, h, 1,
ResourceUsage.None, SurfaceFormat.Color);

I'm using a bit of a trick here. To render my fractal, I want to draw a fullscreen quad using my custom pixel shader. SpriteBatch provides an easy way to draw fullscreen quads, but it expects to be given a source texture. I don't need any source texture for my fractal, but I'm creating the dummyTexture as a trick to keep SpriteBatch happy. Yes, that's a nasty hack, and I apologise for it :-)

My Update method uses the gamepad to zoom and pan the display:

    GamePadState pad = GamePad.GetState(PlayerIndex.One);

if (pad.Buttons.A == ButtonState.Pressed)
zoom /= 1.05f;

if (pad.Buttons.B == ButtonState.Pressed)
zoom *= 1.05f;

float panSensitivity = 0.01f * (float)Math.Log(zoom + 1);

pan += new Vector2(pad.ThumbSticks.Left.X, -pad.ThumbSticks.Left.Y) * panSensitivity;

And finally, my Draw method issues a single SpriteBatch call to draw a fullscreen quad, using my Mandelbrot effect to apply all that massively parallel Xbox GPU goodness to every pixel of the screen:

    GraphicsDevice device = graphics.GraphicsDevice;

float aspectRatio = (float)device.Viewport.Height / (float)device.Viewport.Width;

mandelbrot.Parameters["Pan"].SetValue(pan);
mandelbrot.Parameters["Zoom"].SetValue(zoom);
mandelbrot.Parameters["Aspect"].SetValue(aspectRatio);

spriteBatch.Begin(SpriteBlendMode.None, SpriteSortMode.Immediate, SaveStateMode.None);
mandelbrot.Begin();
mandelbrot.CurrentTechnique.Passes[0].Begin();

spriteBatch.Draw(dummyTexture, Vector2.Zero, Color.White);

spriteBatch.End();
mandelbrot.CurrentTechnique.Passes[0].End();
mandelbrot.End();

This is not only the fastest Mandelbrot renderer I've ever written, but probably also the least code!

Blog index   -   Back to my homepage