Skip to main content

Mandelbrot fractal

Click to see it run!

This sample program illustrates how to use tile_pitch to render the Mandelbrot set fractal.

Code listing

# Display the Mandelbrot set fractal by plotting pixels into a
# linear frame buffer, which illustrates how to use io_tilemap.tile_pitch.

# s20.12 fixed point (4096 represents the real number 1.0)
type fixed is int

module main
# The complex number at the center of the view using s20.12 fixed point.
# For example, -1243 / 2^12 = -0.3035
const center_x is -1243 # -0.3035 (s20.12 fixed point)
const center_y is -3017 # -0.7366 (s20.12 fixed point)

# The width of one pixel using s20.12 fixed point.
# Smaller values zoom in, too small and the math precision breaks down.
const pixel_size is 15

# Increasing this number shows finer detail but draws more slowly
const max_escape_steps is 48

const tile_size is 16 # jumbo tiles are 16 x 16 pixels
const map_cols is 16 # tilemap width (16, 32, 64, 128 or 256)
const raster_cols is 12 # raster block width, measured in tiles
const raster_rows is 8 # raster block height, measured in tiles

const raster_width is main::raster_cols * main::tile_size
const raster_height is main::raster_rows * main::tile_size

# We add 1 on each side for the border made from brick tiles
const grid_cols is main::raster_cols + 2
const grid_rows is main::raster_rows + 2

# The frame buffer array size
const buffer_size is main::raster_width * main::raster_height

# These are stored as module variables because referencing them from
# MMIO won't prevent the garbage collector from freeing them.
var _raster: byte[]
var _grid: pair[]

# These two ROM arrays are initialized by "data" blocks
var iteration_colors: byte[]
var brick_pixels: byte[]

# Set up the rendering surface
func init(io_tilemap: io_tilemap, io_tileset_addresses: int[size 1024])
var raster: byte[]
raster <- new byte[](main::buffer_size)
main::_raster <- raster

var raster_address: int

raster_address <- to_address(raster) + 3 # +3 skip the array size field

var brick_address: int
brick_address <- to_address(main::brick_pixels) + 3

# Tile 0 is clear (the zeroed memory to the right of Tile 1)
io_tileset_addresses[0] <- brick_address + main::tile_size

# Tile 1 is the brick artwork from main::brick_pixels
io_tileset_addresses[1] <- brick_address

# Tiles 2 through 97 are mapped into _raster memory in RAM
var grid: pair[]
grid <- new pair[](main::map_cols * main::map_cols)
main::_grid <- grid

var row: int, col: int, tile: int
tile <- 2
row <- 0
loop
if row >= main::grid_rows
do drop

col <- 0
loop
if col >= main::grid_cols
do drop

if row = 0 or row = main::grid_rows - 1
| or col = 0 or col = main::grid_cols - 1 then
# Put bricks (tile #1) around the border
grid[row * main::map_cols + col] <- 1
else
# Use consecutive tile indexes for the raster tiles
grid[row * main::map_cols + col] <- to_pair(tile)

# Compute the address of the tile's top left pixel inside _raster.
io_tileset_addresses[tile] <- raster_address
| + (row - 1) * main::tile_size * main::raster_width
| + (col - 1) * main::tile_size

tile <- tile + 1
end if

col <- col + 1
end loop

row <- row + 1
end loop

# Configure the tilemap, centered on the 320 x 224 screen
io_tilemap.x <- (320 - main::grid_cols * main::tile_size) / 2
io_tilemap.y <- (224 - main::grid_rows * main::tile_size) / 2
io_tilemap.col_count <- main::map_cols
io_tilemap.row_count <- main::map_cols
io_tilemap.tile_codes_address <- to_address(grid) + 3
io_tilemap.edge_mode <- 1 # clip
io_tilemap.jumbo <- 1

# Palix reads a tile's pixel (x,y) from address + y*tile_pitch + x, so
# using the image width as the pitch makes each tile a 16 x 16 window
# onto the raster.
io_tilemap.tile_pitch <- main::raster_width
end func

# Render the Mandelbrot image into the linear frame buffer.
func render()
var raster: byte[], colors: byte[]
raster <- main::_raster
colors <- main::iteration_colors

var left: fixed, cx: fixed, cy: fixed
left <- main::center_x - (main::raster_width / 2) * main::pixel_size
cy <- main::center_y + (main::raster_height / 2) * main::pixel_size

var x: int, y: int, index: int, count: int
index <- 0
y <- 0
loop
if y >= main::raster_height
do drop

cx <- left
x <- 0
loop
if x >= main::raster_width
do drop

count <- main::_measure_escape_steps(cx, cy)
if count >= main::max_escape_steps then
# The point did not escape, i.e. belongs to the Mandelbrot set
raster[index] <- 1 # 1=black
else
raster[index] <- colors[math::bit_and(count, 15)]
end if

# The linear frame buffer enables a simple "index" to walk the raster.
# Without tile_pitch, we would instead have to calculate a tile index
# and a corresponding pixel offset within that tile.
index <- index + 1
cx <- cx + main::pixel_size
x <- x + 1
end loop

cy <- cy - main::pixel_size
y <- y + 1
end loop
end func

# The inner loop that determines the pixel color for a given (cx, cy), where
# cx is the real part of c, and cy is the imaginary part. It iterates the
# equation z <- z^2 + c starting from z = 0, counting how many steps it took
# for |z| to escape past 2.0. The number of steps is returned for points
# that escaped, or max_escape_steps for points that belong to the fractal.
func _measure_escape_steps(cx: fixed, cy: fixed): int
var x: fixed, y: fixed, xx: fixed, yy: fixed
var count: int

x <- 0
y <- 0
count <- 0

loop
xx <- math::shift_right_signed(x * x, 12)
yy <- math::shift_right_signed(y * y, 12)

if xx + yy > 16384 # |z| squared > 4.0
do drop

# Shifting by 11 instead of 12 calculates 2*x*y
y <- math::shift_right_signed(x * y, 11) + cy
x <- xx - yy + cx

count <- count + 1
if count >= main::max_escape_steps
do drop
end loop

return count
end func

func start()
io::background_color <- 4 # 4=tinman gray
io::paint_mode <- 0 # display the output as it is being rendered

main::init(io::tilemap_b, io::tileset_b_addresses)
main::render()

io::irq_wake_mask <- 0 # sleep forever
kernel::sleep()
end func
end module

# Palix theme color indexes used to show how fast the fractal escapes
data main::iteration_colors
[32, 23, 31, 30, 22, 37, 29, 20, 28, 35, 27, 19, 11, 26, 17, 16]
end data

# The 16x16 border brick tile
data main::brick_pixels
[
# Appending 176 zeros to each line gives us tile_pitch=192
2, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 2, repeat 176 of 0,
43, 42, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 43, repeat 176 of 0,
43, 42, 42, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 43, repeat 176 of 0,
43, 42, 42, 42, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 43, repeat 176 of 0,
43, 42, 42, 42, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 43, repeat 176 of 0,
43, 43, 42, 42, 42, 41, 41, 41, 41, 42, 42, 41, 41, 41, 41, 43, repeat 176 of 0,
43, 42, 43, 42, 42, 42, 42, 42, 42, 42, 42, 42, 41, 41, 41, 43, repeat 176 of 0,
43, 43, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 41, 41, 41, 43, repeat 176 of 0,
43, 42, 43, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 41, 41, 43, repeat 176 of 0,
43, 43, 42, 43, 42, 42, 42, 43, 42, 43, 42, 42, 42, 41, 41, 43, repeat 176 of 0,
43, 42, 43, 42, 42, 42, 43, 42, 43, 42, 43, 42, 42, 41, 41, 43, repeat 176 of 0,
43, 43, 42, 43, 42, 43, 42, 43, 42, 43, 42, 42, 42, 41, 41, 43, repeat 176 of 0,
43, 42, 43, 42, 43, 42, 43, 42, 43, 42, 43, 42, 42, 42, 41, 43, repeat 176 of 0,
43, 43, 42, 43, 42, 43, 42, 43, 42, 43, 42, 43, 42, 42, 42, 43, repeat 176 of 0,
43, 42, 43, 42, 43, 42, 43, 42, 43, 42, 43, 42, 43, 42, 42, 43, repeat 176 of 0,
2, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 2, repeat 176 of 0
]
end data