Graphics - TypeDefinition/NautiBuoy GitHub Wiki

The Rendering Process

Pixel processing unit (PPU)

  • The Gameboy possess a PPU, it is like a very primitive GPU, help makes graphics.
  • Has 12 registers used for graphics, registers at $FF40 - $FF4B.
  • PPU is connected to VRAM and OAM RAM.

How it's drawn

  • Draw the screen each line from left to right. The lines are then drawn line by line from top to bottom.

What happens when a line is being drawn

  • When a line is being drawn, it is in the pixel transfer mode of PPU, it usually takes 43 clocks per line. And this is done for 144 lines. (resolution of Gameboy is 160p x 144p)
  • But before the 43 clocks for the Pixel transfer, there is an additional 20 clocks for OAM search and after the 43 clocks for the pixel transfer, there are another 51 clocks for the H-Blank.
  • So in total when a line is being drawn it looks like this: 20 clocks of OAM search -> 43 clocks of Pixel transfer -> 51 clocks for H-blank

OAM search

  • 20 cycles at the start of each line.
  • PPU needs to decide which sprites are visible in the line it is drawing.
  • Need to find the lines that are visible.
  • Do not do 16-bit calculation of data from $FE00 to $FEFF (the OAM RAM location), it will destroy OAM RAM.

H-blank (Horizontal blanking)

  • H-blank is the little time period after a line on the screen is finish being drawn horizontally.
  • Thus, it is the time period from a line on the screen finish being drawn to the start of the next line being drawn.
  • It takes about 51 clocks
  • In H-blank mode, the PPU is idling, does not do anything.

V-blank (vertical blanking)

  • The screen waits for a little till the next frame goes back to the top of the screen and draw line by line downwards, the time interval is vertical blanking, V-blank.
  • PPU also does not do anything here.
  • About 1140 clocks.

Accessing VRAM and OAM

  • CPU must make sure PPU is in the right mode, or else it will not be able to access (read and write) VRAM or OAM RAM.
  • During pixel transfer cannot access VRAM, but VBLANK, HBLANK and OAM search can access.
  • To access OAM RAM, can only do it during HBLANK and VBLANK.
  • If you want to do anything related to graphics, moving background map, moving camera, update OAM data, updating sprite screen pos eg., best to do it during VBlank since you'll have the most uninterrupted time.

Rendering Layers

  • There are 3 rendering layers, the window layer, background layer and sprite layer.
  • Window layer: The position of the window is fixed regardless of where you move your screen viewport. This is perfect for UI. You can set the window position.
  • Background layer: 32 tiles x 32 tiles map. That's 256 pixels x 256 pixels. Since the viewport is only 160 pixels x 144 pixels, we can easily move the viewport around to see the other parts of the map by changing some registers. ScrollX ($FF43) and scrollY ($FF42) register.
  • Sprite layer: We want to put our sprites here. Sprites can be position freely.

Screen Effects

More info and references on rendering and graphics

How data is stored in VRAM

VRAM

  • VRAM is 8kb, $8000 - $9FFF
  • Holds data on tiles and sprites patterns that are used to be constructed for the graphics shown on screen.
  • Also holds data to tell the Gameboy how and where to draw the tiles on the background (draw the map eg.)
  • Can only be accessed (written and read) during VBLANK or HBLANK.
  • 4kb for Sprite tiles, 4kb for background tiles (overlaps with the sprite tiles), 1kb for background map ($9800-$9BFF), 1kb for window map ($9C00-$9FFF)

Tiles

  • One tile is 8 pixels x 8 pixels, 16 bytes.
  • Since there are 4 colours, each pixel needs 2 bits to denote which colour the pixel is.
  • Which is also why Gameboy graphics are also called 2 bits per pixel (2bpp).
  • The tile data can be easily converted to the right format for it to be read using RGBDS. rgbgfx -o nameOfFile.2bpp nameOfFile.png
  • More on how the graphics is formatted and read:

VRAM Tile Data Table

  • Tile data table is stored in VRAM, location $8000-$97FF. Since each tile is 16 bytes, it can store a total of 384 tiles.
  • The tile data table contains the different tiles to be displayed.
  • When the Gameboy wants to render images on screen, it goes to the tile data table, find the tiles needed, and then place them correctly onto the screen. For example, we want to display a house in the background, the Gameboy would go to the tile data table, look for the tiles that make up the house, and piece them together to display them in the game. (this is a very simplified explanation)
  • In the VRAM tile data table, part of the tiles are used for sprites, and the other part is used for background and window. There is some overlap between them.
  • Reason for the overlap is because of the tile ID. On the Gameboy, 1 register (1 byte) is used to denote the tile ID (to know which tile to render), thus, the tile ID can only be an 8-bit number so from 0 - 255 or -128 - 127.
  • For sprites, the tiles are at $8000-$8FFF. So you can have 256 sprite tiles.
  • For the background and window, the tiles are located at either $8000-$8FFF or $8800-$97FF. This can be set through the LCDF register flag.
  • $8800-$8FFF can be accessed for sprites, background and window.
  • More on how it is stored. Although the video says it is for NES games, the logic still applies

Sprites

Rendering sprites

  • Can either be set as 1 sprite is 8 pixels x 8 pixels or 8 pixels x 16 pixels. Once you set it, it is global to all sprites.
  • For which version to use, it can be set through the LCDF register flag.
  • We can only have 40 sprites on screen at once, and 10 sprites per line. Due to the OAM limitations.
  • For 8x16 sprites, if you have some extremely small sprites for example bullets which are only 2 pixels, in the Sprite tile table, make sure the sprite tile after it is blank. Or else, it'll render the second half of the sprite with the sprite that has the tile ID after it in the sprite tile table.
  • For example, the sprite tile for the bullet is at tile ID 8, for the second half of the 8x16 sprite, it will take the tile at tile ID 9.
  • Normally when you generate the binary layout of the sprite, it'll save the sprite by 8x8 from left to right.
  • To make it save the sprite by 8x8 from top to bottom instead, add a -h flag in rgbgfx generation. This might help for 8x16 sprites. rgbgfx -h -o nameOfFile.2bpp nameOfFile.png
  • More details about 8-bit sprites
  • An example on how it works
  • Another example

Meta sprites

  • If you wish for sprites that are bigger than 8 pixels x 8 pixels or 8 pixels x 16 pixels, you can use meta-sprites.
  • When you combine multiple smaller sprites together to form 1 big sprite, that big sprite is known as a meta-sprite.

Sprite draw priority

  • The draw priority of the sprites are fixed. It cannot be changed with the sprite priority flag.
  • Sprites will draw on top of another sprite if it has a smaller x pos.
  • If sprite both same x pos, then it depends on who is drawn earlier.
  • The sprite draw priority flag, on the other hand, allows you to prioritise whether for the background or window colours to draw over the sprites. This allows for more depth effects.
  • If priority bit = 1, then the sprite will be hidden behind the colours 1,2 and 3 of the background & window layer.

Drawing sprites on screen

  • Please note that assigning your sprite to screen coordinate (0,0), it will not render on screen.
  • The very left of the screen which you can see, is actually x pos = 0x08 since the sprites are 8 pixels wide. This is done to allow the sprites to be scrolled in from outside the screen.
  • The very top of the screen which you can see is actually y pos = 0x10 (16).

Sprite attribute table (OAM)

  • OAM RAM is located at $FE00-FE9F
  • It stores sprite attributes for each sprite.
  • Each sprite takes up 4 bytes in the OAM.
  • Thus, because of this, we can only have a maximum of 40 sprites on screen at once.
  • The attributes: Y position on screen, X position on screen, tile ID, and the flags.
  • The flags: Draw priority for background and window colors (bit 7), Y flip (bit 6), X flip (bit 5), and the palette number (bit 4).
  • Please note that the X and Y position for the OAM should not be the same as the sprite's game world position as the pos in the OAM is more about the position on the screen. Screen position and world position are different.
  • More on OAM

Direct Memory Access (DMA transfer)

  • We cannot access OAM RAM unless it is VBlank or Hblank.
  • That means the only time we can read or write to it is during VBlank or Hblank.
  • We cannot update the y and x coordinates of the sprites or update the sprite ID (for animation or changing the direction of the sprite) on screen when it is not during VBlank or Hblank.
  • Thus, what we'll do is keep a 'shadow' copy of the OAM variables in RAM so we can update the variables, and then during VBlank or Hblank, we transfer the copy of the OAM variables to the actual OAM RAM.
  • "Shadow X" means "a copy of X in RAM that has to be copied to the real thing"
  • The process to transfer data from ROM/RAM to OAM is known as Direct Memory Access transfer (DMA transfer).
  • To start a DMA transfer, write to register $FF46.
  • However, during a DMA transfer, the CPU can only access HRAM at $FF80 - $FFFE.
  • Thus, we need to copy codes from ROM to HRAM and then run the codes for the transfer and wait for the transfer to finish from HRAM to update the OAM variables.
  • More on the process
  • Another link on the process