Palix: tiles
A tilemap is rectangular grid of tiles, where each tile is a square made of pixels. The square is either 16 × 16 ("jumbo" tiles typically used for game levels) or 8 × 8 (typically used by fonts). The tile pictures come from a tileset, which is a collection of numbered tile images. For example, a video game forest might use a tile 7 for each bush, a tile 13 for a vertical segment of a dirt road, etc. As another example, the Hybrix text console is a tilemap where each tile is a text character (letter or number or punctuation mark or graphical glyph). In this case, the tileset represents a typeface font.
Note: The Hybrix framework defines two "tilemap" classes
tilemapandio_tilemap. Thetilemapclass is used for designer art resources, whereasio_tilemapdescribes the hardware interface for rendering. Thetile_layerclass preparesio_tilemapusing data fromtilemapandtileset.
Screen dimensions
Here's a handy reference for measuring screen dimensions as tiles:
| Screen size | Units |
|---|---|
| 320 × 224 | pixels |
| 40 × 28 | regular tiles (8 × 8) |
| 20 × 14 | jumbo tiles (16 × 16) |
Rendering
A tilemap is a two dimensional grid of cells, arranged in left-to-right rows, starting from the top row. The rows increase downwards like the Y axis, and columns increase rightwards like the X axis. Each cell contains a 16-bit pair called a tile code, whose binary bits encode three pieces of information for that cell:
| Tile code bits | Values | Encoded field |
|---|---|---|
| 0…9 | 0 – 1023 | tile index: indicates which tile to show from the tileset |
| 10…14 | 0 – 31 | theme index: the color palette theme used in this cell |
| 15 | 0 or 1 | matte coloring: if 1, then the io::matte_color is applied in this cell |
If enabled, matte coloring causes the tile's transparent pixels to be painted using the globally specified io::matte_color. The HASCII {mat} mode makes use of this feature to add a solid background for text fonts. Matte coloring is guaranteed to eliminate all transparency; if io::matte_color specifies a transparent or invalid palette index, then the matte color will be black (system palette index #1).
The io_tilemap.x and io_tilemap.y fields position the entire tilemap on the screen. The coordinate origin is in the upper-left corner of the screen, with the Y axis pointing downwards and the X axis pointing to the right.
The tileset data is an array of up to 1,024 tile images. io::tileset_a_addresses defines the tileset for io::tileset_a and so forth.
Each array element of io::tileset_a_addresses stores the memory address of that tile's bitmap pixels, one byte per pixel, in left-to-right rows, starting from the top row. For regular 8 × 8 tiles, there will be 64 pixels. For "jumbo" 16 × 16 tiles, there will be 256 pixels. The first byte is the upper-left pixel of that tile. (The byte is a theme color index, whose color is determined by that tile code's theme index.)
Tilemap A always appears in front of Tilemap B, which always appears in front of Tilemap C. See display priority for sprite interaction with the tilemap.
Palix-sized tilemaps
The io_tilemap.col_count and io_tilemap.row_count locations must have one of these dimensions:
16, 32, 64, 128, or 256
If any other number is used, it is considered an error—Palix will not display that tilemap at all.
The Hybrix designer warns you if a tilemap's dimensions are not Palix-sized. Why allow non-Palix sizes at all? This can be useful for tilemaps that will not be rendered directly by io_tilemap. The idea is to configure io_tilemap with a separate tilemap buffer that is Palix-sized, and then the non-Palix tilemaps get copied into this buffer. For example, the framework's console sets up a kernel::console_grid buffer for font text. You could make a tilemap that is a dialog box with a non-Palix size, and then at runtime your program could copy it onto the screen. Another common example is an infinite scrolling plane (io_tilemap.edge_mode = 0), where the program generates the tiles interactively as they scroll onto the screen.
Tile pitch
By default, the io::tileset_a_addresses elements point to individual tile images. In the tile image bytes, the address of a pixel is calculated as:
...where tile_width is 8 for regular tiles, or 16 for jumbo tiles.
Suppose we want to plot pixels at arbitrary (x,y) points on the screen. We could fill the screen with tiles whose memory is in RAM, and then plot pixels into their individual arrays. This requires calculating the tile column and row first, then calculating the pixel index within the target tile:
func draw_pixel(x: int, y: int, color: byte)
# Read the tile code
var col: int, row: int, tile_code: pair
col <- x / 16 # (jumbo tile)
row <- y / 16
tile_code <- example::tile_codes[row * 32 + col] # 32=col_count
# Extract the tile index from the tile code
var tile_index: int
tile_index <- math::bit_and(tile_code, $3ff) # bits 0..9
# Find the pixel array and calculate its index:
var pixels: byte[], index: int
pixels <- example::tiles[tile_index].pixels
index <- (y % 16) * 16 + (x % 16)
pixels[index] <- color
end func
For an inner loop, the above implementation can be quite expensive. It would be much cheaper to have a linear frame buffer, where we render into a large rectangle called a raster, like this:
func draw_pixel_fast(x: int, y: int, color: byte)
var index: int
index <- y * example::raster_width + x
example::raster[index] <- color
end func
The io_tilemap.tile_pitch makes this possible. When set to a nonzero number between 1 and 512, the above equation changes to:
For example, suppose we create a 320 × 224 raster to fill the entire screen with jumbo tiles:
module example
var raster: byte[size 71680] # 320 x 224 pixels
. . .
end module
Then we set tile_pitch to 320, and then point each tileset_a_addresses[tile_index] entry to its corresponding upper-left corner within the raster, and then fill the tilemap with consecutive tile indexes. After this initial setup, draw_pixel_fast() "just works" without needing to consider the tileset geometry at all. The Mandelbrot fractal sample program illustrates this in detail.
Why do we need tiles at all?
Classic graphics chips sometimes feature a "bitmap mode" that can directly render a pixel array. Why does Palix require the raster to be constructed from tiles? One answer is simplicity:
tile_pitchis a cheap extension to the existing digital electronics, avoiding the complexity of an entirely new rendering mode. But a more important reason is memory: a full screen of bitmap bytes would consume of the addressable RAM. By contrast,tile_pitchenables a smaller raster buffer to be mapped to an arbitrary region of the screen. Regular ROM tiles can even coexist in the sameio_tilemaplayer (assuming their pitch is adjusted likemain::brick_pixelsin the Mandelbrot sample).
I/O definitions
class io_tilemap # size 16
# Screen coordinates of upper-left corner of tilemap
var x: pair, y: pair
# Possible values: 16, 32, 64, 128, 256
var col_count: pair, row_count: pair
# A pointer to a grid of tile codes arranged in left-to-right rows,
# starting from the top row. the array index is row*col_count+col.
# Each tile code is a 16-bit pair:
# bits 0..9 encode the tile index (0..1023) from the tileset
# bits 10..14 encode the theme index (0..31) into io::themes
# bit 15 if set, io::matte_color replaces any clear pixels in this tile
var tile_codes_address: int
# 0 = wrap: the map wraps around, repeating infinitely in every direction
# 1 = clip: cells outside the map bounds are transparent
var edge_mode: byte
# ------------------
# Tileset properties
# 0 = tiles are 8 x 8 pixels
# 1 = tiles are 16 x 16 pixels
var jumbo: byte
# Normally tileset_X_addresses entries point to contiguous tiles where:
# tile_pixel(x,y) = address + y*tile_width + x
# ...with tile_width being 8 for regular tiles or 16 for jumbo tiles.
# If tile_pitch is in the range 1..512, then this formula changes to:
# tile_pixel(x,y) = address + y*tile_pitch + x
# With tile_pitch>tile_width, the gaps enable a block of tilemap cells to
# be mapped to a contiguous linear frame buffer for advanced rasterizers.
var tile_pitch: pair # 0 = default behavior (use tile_width)
end class
module io
. . .
# Palix video system
var background_color: byte located at $d0_0300
var matte_color: byte located at $d0_0301
. . .
inset tilemap_a: io_tilemap located at $d0_0310 # ..$d0_031f
inset tilemap_b: io_tilemap located at $d0_0320 # ..$d0_032f
inset tilemap_c: io_tilemap located at $d0_0330 # ..$d0_033f
. . .
inset tileset_a_addresses: int[size 1024] located at $d0_1000 # ..$d0_1fff
inset tileset_b_addresses: int[size 1024] located at $d0_2000 # ..$d0_2fff
inset tileset_c_addresses: int[size 1024] located at $d0_3000 # ..$d0_3fff
end module