Project Ember

The Plan: Design and implement a fully functional “retro” video game console, with a custom CPU, GPU, Audio Processor, and development architecture from scratch. Engineering background, while helpful, is not required to follow along. Please join us!

The Flame GPU — Initial Design Part 2: Tilesheets, Tilemaps, and Graphics Registers

Tom Gambill
Project Ember
Published in
9 min readMar 24, 2025

--

AI-Generated image licensed from FreePik
Retro Arcade — AI-Generated by FreePik

Now that we have a basic understanding of the Flame GPU requirements, we can consider the fundamental structures needed to implement our first display mode. For the initial development and testing of the Ember design, we need a very simple text mode that allows us to get something on the screen. This mode can be used while implementing the initial firmware or operating system for the Ember console. To do this, we need the ability to define a font, use that font to draw characters on the screen, and a way for code on the CPU to set up and configure the required GPU settings.

GPU Registers

The Flame GPU and the Ember CPU will communicate using a set of registers mapped directly into a shared range of system memory addresses. This is typically called memory-mapped IO. These GPU registers, working much like the registers in the CPU, determine various graphics and display settings and allow the CPU to configure the active display mode, colors, memory addresses, and settings of various structures like tilemaps, tilesheets, sprites, and palettes.

To define our first text display mode, we will need at least one tilemap to hold the letters to display on the screen, one font tilesheet to define the various letter and number shapes, a foreground and background color, and a few other registers to hold values like screen resolution.

Tilemap

The first structure we will look at is the tilemap. On some systems, like NES or C64, this is also called a nametable. A tilemap is a block of memory that represents what is displayed at each location on the screen. For graphics display modes, each location in memory contains a reference to a rectangular graphics image, perhaps 8x8 or 16x16 pixels in size, stored elsewhere in memory. In our text display mode, each location contains a reference to an image of a letter or character in the font.

Let’s consider a text mode capable of displaying 40x30 characters on the screen. If each location in the tilemap is one 8-bit byte, the entire 2-dimensional array would need 1,200 bytes to hold all the characters that could be displayed on the screen at once. Higher-resolution display modes will require more bytes and, thus, more memory to display. It is also common to use more than one byte at each location; this can be used to store additional information for each character, like character color or highlighting, but we will stick to one byte for this mode.

Tilemap, Bitmap Font, and Memory Layout for proposed Text Mode 0

Tilesheet

Each of the 8-bit bytes in the tilemap can reference up to 256 different tiles, each with a different image. In the case of characters, we don’t need all 256 values for letters and numbers; however, other characters, like punctuation, are needed, as well as control characters like carriage return or space. Most systems also fill in the additional space with graphical characters like horizontal and vertical lines, which can be used to draw outlines or blocky images around the text.

Colors

Our simple text mode will use only two colors: the background color, and the font color, which we will call the foreground color. Graphics display modes for games can often display a different color for each pixel in the tileset using what is called a palette. This is another level of indirection: The tilemap indexes into the tilesheet, which in turn is an index into the palette.

For our simple case, we have only two colors, so the palette has only two entries. For this, we only need one bit for each pixel on the screen: 0 means use the background color; 1 means use the foreground color. Now, we can define a system palette, which we will implement with two GPU registers to set the two colors.

At one bit per display pixel, a 16x16 pixel font needs 32 8-bit bytes to hold each character. That means a 256-character font of that size would need 8k of memory. But what if we don’t really need all 256 characters? We might only define 128 characters; then we can half that at 4k of memory required. We just need to be careful not to set any of the tilemap locations to a value greater than 127, or that character will be undefined.

Display Resolution

We have now defined the memory structures needed to display text. Next, we need to describe how that information will be displayed on the monitor.

With CRT displays, due to overscan, or the cutoff edges we discussed previously, we typically have one larger resolution of the actual display itself, and then a smaller resolution of the desired image or set of text. On an LCD display, we don’t typically inset the application display from the edges of the screen, but it’s possible and even desirable sometimes. This is called letterboxing, or when the inset is on the sides rather than the top or bottom, it is called pillarboxing (or pillboxing).

Letterboxing can be useful in games for a cinematic effect, or pillarboxing when emulating an arcade game vertical aspect on a widescreen monitor. Think arcade Pac-Man on a PC monitor with black bars on each side of the game display.

For this, we will need two resolution settings: the display or monitor resolution, and the tile resolution. We can also add a start location to indicate where the inset tilemap resolution starts on the display.

Playfield

There is one more concept we will briefly touch upon, that is the concept of playfields. Many computers and consoles of the time had the ability to display more than one tilemap at once, overlaying and blending between them. We will call these playfields. A playfield is a superset of a tilemap, adding additional settings. The Flame design will reserve the ability to display up to 6 of these, depending on the display mode, though most will allow fewer than this. In fact, our initial Text Mode 0 will only support one playfield. We can refer to the first playfield as the Background Playfield, with additional playfields appearing in front of the others, with some blending method between them, which we will define later.

Playfields can also add additional functionality and effects to the tilemap, such as scrolling or rotation. They can even define a tilemap that is larger than the displayable region with an offset to the start location and even a rotation amount. The tilemap will then be clipped to the visible rectangle on the screen using the Tile Count and Tilemap Start registers.

Memory map of Ember Flame GPU registers
Flame GPU Memory-Mapped Registers

Zero-Page GPU Registers

In the Ember memory map, GPU registers are assigned a 32k window in the second half of the Ember Zero Page. Much like the 256-byte zero page in an 8-bit CPU like the 6502, that window is 64k in size for a 32-bit address space. This is done for performance reasons, as accessing memory in this region only requires 16 bits. One example is that only half as many LDA instructions are needed to load constants into registers referencing zero-page memory addresses. Another advantage is that the 64K zero page memory could fit entirely within the Block RAM on an FPGA implementation such as a MiSTer core on a Cyclone V, making it many times faster to access than static RAM.

The graphic above shows the various memory-mapped registers as defined so far. There are quite a few reserved sections, which we will fill in over time as we add additional features, including movable sprites, which we will cover in a future article as we add support for graphical display modes.

In addition, as we write the initial Ember firmware, we will cover the GPU startup sequence, including all the flags and settings needed to configure the GPU and the readable status and error code flags. For now, the following is a high-level overview of the various registers and settings.

Configuration Registers

Starting at address 0x00008000 in hex, we have the basic configuration registers for the GPU, followed by about 32K of fast VRAM that can be used as a scratch pad for graphics, sprite tables, etc. (Going forward, at least in this article, when we reference addresses in zero page, we will only use the last four hex digits, or 16 bits, since the other four high-order digits will always be zero, by definition. Thus, the 32-bit address 0x00008000 can be written as 0x8000).

The first 16 bytes contain the global GPU Flags, display resolution, tile resolution, and offsets. The next 16 bytes, starting at address 0x8010, are read-only and contain the GPU status flags and other information about the GPU state, including errors or exception conditions.

The active display mode settings, including the display mode index and default system colors, start at address 0x8020. The default foreground and background colors are set using 8-bit per channel BGRA (standard 2.2 gamma logarithmic) colors. Notice the ordering of the color channels in memory; since this is a little-endian machine, these elements reverse when stored as a double word (e.g. AARRGGBB). Also, be aware that some implementations of Ember, especially those that are FPGA-based, might quantize these to fewer bits of actual display color depending on the available hardware.

Sprite Registers

We will not define these here, but the next section of registers at address 0x8030 is reserved for sprite configuration, such as a pointer to the address of various sprite tables and settings in memory.

Playfield Registers

Starting at address 0x8040, there are 6 32-byte descriptors, each defining a playfield. Depending on the display mode, not all of these will be active, but the specification allows up to 6 simultaneous playfields overlaid on the screen, with blending between them to be defined per mode.

Each playfield descriptor starts with a pointer to the address of the start of the tilemap in memory, followed by the size and scroll offset of the timemap within the defined display tilemap area. Future settings may be added, such as rotation or skew, and only allowable settings will be honored depending on the features of the configured display mode.

As mentioned, the tilemap can be larger than the displayable area and will be clipped to the defined area on the display. Any visible pixels within the display area but outside the bounds of the playfield tilemap will be drawn using the system background color. This will happen if the scrolling values are too far to the left, for example, and the tilemap doesn’t extend all the way to the right side of the displayable screen area.

Video Memory

One other concept that we will touch upon is VRAM, or Video RAM, which is a range of system memory dedicated to storing Tilemaps and other graphics data that is also needed by the GPU in order to build the actual display output. VROM, or video ROM, is also a possibility in cartridge systems. If an Ember implementation were to utilize cartridges, they would also be mapped into a similar range of addresses. In the Ember design, we will designate the address starting at 0x11000000 as the start of this memory range.

VRAM

In many systems of the time, video RAM, in particular, was dual-port, meaning that it could be accessed simultaneously by the CPU and the GPU. In other systems, in order to share this memory, the CPU and GPU had to take turns reading and writing to this memory, typically using interrupts or some other bus-mastering signal to determine which was allowed to access any particular location at a time. Regardless, for our design at this point, the details are not important. Just be aware that a range of memory will be reserved for this purpose.

VROM

Video ROM might not be necessary for an Ember emulator, especially if we’re loading game images from disk, but a hardware implementation might load virtual ROMs, or cartridge images, that the application is not meant to modify, so we might designate a range of addresses within the 0x11000000 range for ROM as well.

Graphics Display Modes and Sprites

In future articles, we will look at sprites and how a simple graphics mode might work on the Flame GPU for simple 2D games like Pac-Man or Galaga. We will also need to write the Ember startup code needed to initialize and manage the GPU while it is operating.

Next post in this series:

The Flame GPU — Initial Design Part 3: Sprites [Coming Soon!]

Ember Design Series

Back to the beginning of Project Ember:

Going Old-School: Designing A Custom Homebrew Retro Video Game Console From Scratch

https://buymeacoffee.com/emberproject
https://buymeacoffee.com/emberproject

Enjoying my content? Support me by buying me a coffee, clapping for this post, and following my page. Thanks so much, and stay safe!

--

--

Project Ember
Project Ember

Published in Project Ember

The Plan: Design and implement a fully functional “retro” video game console, with a custom CPU, GPU, Audio Processor, and development architecture from scratch. Engineering background, while helpful, is not required to follow along. Please join us!

Tom Gambill
Tom Gambill

Written by Tom Gambill

Software Engineer, Retro Hacker, World Traveler. And also: sailboats, fish tanks, nature, family, startups…

No responses yet