2018-11-20 20:57:43 +11:00
|
|
|
# Tile Data
|
|
|
|
|
|
|
|
When using the GBA's hardware graphics, if you want to let the hardware do most
|
|
|
|
of the work you have to use Modes 0, 1 or 2. However, to do that we first have
|
|
|
|
to learn about how tile data works inside of the GBA.
|
|
|
|
|
|
|
|
## Tiles
|
|
|
|
|
|
|
|
Fundamentally, a tile is an 8x8 image. If you want anything bigger than 8x8 you
|
|
|
|
need to arrange several tiles so that it looks like whatever you're trying to
|
|
|
|
draw.
|
|
|
|
|
|
|
|
As was already mentioned, the GBA supports two different color modes: 4 bits per
|
2018-11-20 21:18:41 +11:00
|
|
|
pixel and 8 bits per pixel. This means that we have two types of tile that we
|
|
|
|
need to model. The pixel bits always represent an index into the PALRAM.
|
2018-11-20 20:57:43 +11:00
|
|
|
|
|
|
|
* With 4 bits per pixel, the PALRAM is imagined to be 16 **palbank** sections of
|
|
|
|
16 palette entries each. The image data selects the index within the palbank,
|
|
|
|
and an external configuration selects which palbank is used.
|
|
|
|
* With 8 bits per pixel, the PALRAM is imagined to be a single 256 entry array
|
|
|
|
and the index just directly picks which of the 256 colors is used.
|
|
|
|
|
2018-11-20 21:18:41 +11:00
|
|
|
Knowing this, we can write the following definitions:
|
2018-11-20 20:57:43 +11:00
|
|
|
|
|
|
|
```rust
|
|
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
|
|
#[repr(transparent)]
|
|
|
|
pub struct Tile4bpp {
|
|
|
|
data: [u32; 8]
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, Default)]
|
|
|
|
#[repr(transparent)]
|
|
|
|
pub struct Tile8bpp {
|
|
|
|
data: [u32; 16]
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
I hope this makes sense so far. At 4bpp, we have 4 bits per pixel, times 8
|
|
|
|
pixels per line, times 8 lines: 256 bits required. Similarly, at 8 bits per
|
|
|
|
pixel we'll need 512 bits. Why are we defining them as arrays of `u32` values?
|
|
|
|
Because when it comes time to do bulk copies the fastest way to it will be to go
|
|
|
|
one whole machine word at a time. If we make the data inside the type be an
|
|
|
|
array of `u32` then it'll already be aligned for fast `u32` bulk copies.
|
|
|
|
|
|
|
|
Keeping track of the current color depth is naturally the _programmer's_
|
|
|
|
problem. If you get it wrong you'll see a whole ton of garbage pixels all over
|
|
|
|
the screen, and you'll probably be able to guess why. You know, unless you did
|
|
|
|
one of the other things that can make a bunch of garbage pixels show up all over
|
|
|
|
the screen. Graphics programming is fun like that.
|
|
|
|
|
|
|
|
## Charblocks
|
|
|
|
|
2018-11-21 17:44:58 +11:00
|
|
|
Tiles don't just sit on their own, they get grouped into **charblocks**. Long
|
|
|
|
ago in the distant past, video games were built with hardware that was also used
|
|
|
|
to make text terminals. So tile image data was called "character data". In fact
|
|
|
|
some guides will even call the regular mode for the background layers "text
|
|
|
|
mode", despite the fact that you obviously don't have to show text at all.
|
2018-11-20 20:57:43 +11:00
|
|
|
|
|
|
|
A charblock is 16kb long (`0x4000` bytes), which means that the number of tiles
|
|
|
|
that fit into a charblock depends on your color depth. With 4bpp you get 512
|
|
|
|
tiles, and with 8bpp there's 256 tiles. So they'd be something like this:
|
|
|
|
|
|
|
|
```rust
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
#[repr(transparent)]
|
|
|
|
pub struct Charblock4bpp {
|
|
|
|
data: [Tile4bpp; 512],
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
#[repr(transparent)]
|
|
|
|
pub struct Charblock8bpp {
|
|
|
|
data: [Tile8bpp; 256],
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
You'll note that we can't even derive `Debug` or `Default` any more because the
|
|
|
|
arrays are so big. Rust supports Clone and Copy for arrays of any size, but the
|
|
|
|
rest is still size 32 or less. We won't generally be making up an entire
|
|
|
|
Charblock on the fly though, so it's not a big deal. If we _absolutely_ had to,
|
|
|
|
we could call `core::mem::zeroed()`, but we really don't want to be trying to
|
|
|
|
build a whole charblock at runtime. We'll usually want to define our tile data
|
2018-11-21 17:44:58 +11:00
|
|
|
as `const` charblock values (or even parts of charblock values) that we then
|
|
|
|
load out of the game pak ROM at runtime.
|
2018-11-20 20:57:43 +11:00
|
|
|
|
|
|
|
Anyway, with 16k per charblock and only 96k total in VRAM, it's easy math to see
|
|
|
|
that there's 6 different charblocks in VRAM when in a tiled mode. The first four
|
|
|
|
of these are for backgrounds, and the other two are for objects. There's rules
|
|
|
|
for how a tile ID on a background or object selects a tile within a charblock,
|
|
|
|
but since they're different between backgrounds and objects we'll cover that on
|
|
|
|
their own pages.
|
|
|
|
|
|
|
|
## Image Editing
|
|
|
|
|
|
|
|
It's very important to note that if you use a normal image editor you'll get
|
|
|
|
very bad results if you translate that directly into GBA memory.
|
|
|
|
|
|
|
|
Imagine you have part of an image that's 16 by 16 pixels, aka 2 tiles by 2
|
|
|
|
tiles. The data for that bitmap is the 1st row of the 1st tile, then the 1st row
|
|
|
|
of the 2nd tile. However, when we translate that into the GBA, the first 8
|
|
|
|
pixels will indeed be the first 8 tile pixels, but then the next 8 pixels in
|
|
|
|
memory will be used as the _2nd row of the first tile_, not the 1st row of the
|
|
|
|
2nd tile.
|
|
|
|
|
|
|
|
So, how do we fix this?
|
|
|
|
|
|
|
|
Well, the simple but annoying way is to edit your tile image as being an 8 pixel
|
|
|
|
wide image and then have the image get super tall as you add more and more
|
|
|
|
tiles. It can work, but it's really impractical if you have any multi-tile
|
|
|
|
things that you're trying to do.
|
|
|
|
|
|
|
|
Instead, there are some image conversion tools that devkitpro provides in their
|
|
|
|
gba-dev section. They let you take normal images and then repackage them and
|
|
|
|
export it in various formats that you can then compile into your project.
|
|
|
|
|
2018-11-21 17:44:58 +11:00
|
|
|
Ketsuban uses the [grit](http://www.coranac.com/projects/grit/) tool, with the
|
|
|
|
following suggestions:
|
|
|
|
|
|
|
|
1) Include an actual resource file and a file describing it somewhere in your
|
|
|
|
project (see [the grit
|
|
|
|
manual](http://www.coranac.com/man/grit/html/index.htm) for all details
|
|
|
|
involved here).
|
|
|
|
2) In a `build.rs` you run `grit` on each resource+description pair, such as in
|
|
|
|
this [old gist
|
|
|
|
example](https://gist.github.com/ketsuban/526fa55fbef0a3ccd4c7cd6204f29f94)
|
|
|
|
3) Then within your rust code you use the
|
|
|
|
[include_bytes!](https://doc.rust-lang.org/core/macro.include_bytes.html)
|
|
|
|
macro to have the formatted resource be available as a const value you can
|
|
|
|
load at runtime.
|