@soapbox.pub/wasmboy
Version:
Soapbox fork of Wasmboy.
478 lines (422 loc) • 20.1 kB
text/typescript
// Functions for rendering the background
import { FRAME_LOCATION } from '../constants';
import { Cpu } from '../cpu/index';
import { Config } from '../config';
import { Graphics, loadFromVramBank, setPixelOnFrame, getRgbPixelStart } from './graphics';
import { getColorizedGbHexColorFromPalette, getRgbColorFromPalette, getColorComponentFromRgb } from './palette';
import { getRedFromHexColor, getGreenFromHexColor, getBlueFromHexColor } from './colors';
import { addPriorityforPixel, getPriorityforPixel } from './priority';
import { TileCache, drawPixelsFromLineOfTile, getTileDataAddress } from './tiles';
// Assembly script really not feeling the reexport
// using Skip Traps, because LCD has unrestricted access
// http://gbdev.gg8.se/wiki/articles/Video_Display#LCD_OAM_DMA_Transfers
import { eightBitLoadFromGBMemory } from '../memory/load';
import { checkBitOnByte, resetBitOnByte } from '../helpers/index';
import { i32Portable } from '../portable/portable';
// NOTE: i32Portable wraps modulo here as somehow it gets converted to a double:
// https://github.com/torch2424/wasmboy/issues/216
// Inlined because closure compiler inlines
export function renderBackground(scanlineRegister: i32, tileDataMemoryLocation: i32, tileMapMemoryLocation: i32): void {
// NOTE: Camera is reffering to what you can see inside the 160x144 viewport of the entire rendered 256x256 map.
// Get our scrollX and scrollY (u16 to play nice with assemblyscript)
// let scrollX: i32 = eightBitLoadFromGBMemory(Graphics.memoryLocationScrollX);
// let scrollY: i32 = eightBitLoadFromGBMemory(Graphics.memoryLocationScrollY);
let scrollX: i32 = Graphics.scrollX;
let scrollY: i32 = Graphics.scrollY;
// Get our current pixel y positon on the 160x144 camera (Row that the scanline draws across)
// this is done by getting the current scroll Y position,
// and adding it do what Y Value the scanline is drawing on the camera.
let pixelYPositionInMap: i32 = scanlineRegister + scrollY;
// Gameboy camera will "wrap" around the background map,
// meaning that if the pixelValue is 350, then we need to subtract 256 (decimal) to get it's actual value
// pixel values (scrollX and scrollY) range from 0x00 - 0xFF
pixelYPositionInMap &= 0x100 - 1;
// Draw the Background scanline
drawBackgroundWindowScanline(scanlineRegister, tileDataMemoryLocation, tileMapMemoryLocation, pixelYPositionInMap, 0, scrollX);
}
// Inlined because closure compiler inlines
export function renderWindow(scanlineRegister: i32, tileDataMemoryLocation: i32, tileMapMemoryLocation: i32): void {
// Get our windowX and windowY
// let windowX: i32 = eightBitLoadFromGBMemory(Graphics.memoryLocationWindowX);
// let windowY: i32 = eightBitLoadFromGBMemory(Graphics.memoryLocationWindowY);
let windowX: i32 = Graphics.windowX;
let windowY: i32 = Graphics.windowY;
// NOTE: Camera is reffering to what you can see inside the 160x144 viewport of the entire rendered 256x256 map.
// First ensure that the scanline is greater than our window
if (scanlineRegister < windowY) {
// Window is not within the current camera view
return;
}
// WindowX is offset by 7
windowX -= 7;
// Get our current pixel y positon on the 160x144 camera (Row that the scanline draws across)
let pixelYPositionInMap = scanlineRegister - windowY;
// xOffset is simply a neagative window x
// NOTE: This can become negative zero?
// https://github.com/torch2424/wasmboy/issues/216
let xOffset = i32Portable(-windowX);
// Draw the Background scanline
drawBackgroundWindowScanline(scanlineRegister, tileDataMemoryLocation, tileMapMemoryLocation, pixelYPositionInMap, windowX, xOffset);
}
// Function frankenstein'd together to allow background and window to share the same draw scanline function
function drawBackgroundWindowScanline(
scanlineRegister: i32,
tileDataMemoryLocation: i32,
tileMapMemoryLocation: i32,
pixelYPositionInMap: i32,
iStart: i32,
xOffset: i32
): void {
// Get our tile Y position in the map
let tileYPositionInMap = pixelYPositionInMap >> 3;
// Loop through x to draw the line like a CRT
for (let i = iStart; i < 160; ++i) {
// Get our Current X position of our pixel on the on the 160x144 camera
// this is done by getting the current scroll X position,
// and adding it do what X Value the scanline is drawing on the camera.
let pixelXPositionInMap = i + xOffset;
// This is to compensate wrapping, same as pixelY
if (pixelXPositionInMap >= 0x100) {
pixelXPositionInMap -= 0x100;
}
// Divide our pixel position by 8 to get our tile.
// Since, there are 256x256 pixels, and 32x32 tiles.
// 256 / 8 = 32.
// Also, bitshifting by 3, do do a division by 8
// Need to use u16s, as they will be used to compute an address, which will cause weird errors and overflows
let tileXPositionInMap = pixelXPositionInMap >> 3;
// Get our tile address on the tileMap
// NOTE: (tileMap represents where each tile is displayed on the screen)
// NOTE: (tile map represents the entire map, now just what is within the "camera")
// For instance, if we have y pixel 144. 144 / 8 = 18. 18 * 32 = line address in map memory.
// And we have x pixel 160. 160 / 8 = 20.
// * 32, because remember, this is NOT only for the camera, the actual map is 32x32. Therefore, the next tile line of the map, is 32 byte offset.
// Think like indexing a 2d array, as a 1d array and it make sense :)
let tileMapAddress = tileMapMemoryLocation + (tileYPositionInMap << 5) + tileXPositionInMap;
// Get the tile Id on the Tile Map
let tileIdFromTileMap: i32 = loadFromVramBank(tileMapAddress, 0);
// Now that we have our Tile Id, let's check our Tile Cache
let usedTileCache = false;
if (Config.tileCaching) {
let pixelsDrawn: i32 = drawLineOfTileFromTileCache(
i,
scanlineRegister,
pixelXPositionInMap,
pixelYPositionInMap,
tileMapAddress,
tileDataMemoryLocation,
tileIdFromTileMap
);
// Increment i by 7, not 8 because i will be incremented at end of for loop
if (pixelsDrawn > 0) {
i += pixelsDrawn - 1;
usedTileCache = true;
}
}
if (Config.tileRendering && !usedTileCache) {
let pixelsDrawn: i32 = drawLineOfTileFromTileId(
i,
scanlineRegister,
pixelXPositionInMap,
pixelYPositionInMap,
tileMapAddress,
tileDataMemoryLocation,
tileIdFromTileMap
);
// A line of a tile is 8 pixels wide, therefore increase i by (pixelsDrawn - 1), and then the for loop will increment by 1
// For a net increment for 8
if (pixelsDrawn > 0) {
i += pixelsDrawn - 1;
}
} else if (!usedTileCache) {
if (Cpu.GBCEnabled) {
// Draw the individual pixel
drawColorPixelFromTileId(
i,
scanlineRegister,
pixelXPositionInMap,
pixelYPositionInMap,
tileMapAddress,
tileDataMemoryLocation,
tileIdFromTileMap
);
} else {
// Draw the individual pixel
drawMonochromePixelFromTileId(
i,
scanlineRegister,
pixelXPositionInMap,
pixelYPositionInMap,
tileDataMemoryLocation,
tileIdFromTileMap
);
}
}
}
}
// Function to draw a pixel for the standard GB
// Inlined because closure compiler inlines
function drawMonochromePixelFromTileId(
xPixel: i32,
yPixel: i32,
pixelXPositionInMap: i32,
pixelYPositionInMap: i32,
tileDataMemoryLocation: i32,
tileIdFromTileMap: i32
): void {
// Now we can process the the individual bytes that represent the pixel on a tile
// Now get our tileDataAddress for the corresponding tileID we found in the map
// Read the comments in _getTileDataAddress() to see what's going on.
// tl;dr if we had the tile map of "a b c d", and wanted tileId 2.
// This funcitons returns the start of memory locaiton for the tile 'c'.
let tileDataAddress: i32 = getTileDataAddress(tileDataMemoryLocation, tileIdFromTileMap);
// Get the y pixel of the 8 by 8 tile.
// Simply modulo the scanline.
// For instance, let's say we are printing the first line of pixels on our camera,
// And the first line of pixels on our tile.
// yPixel = 1. 1 % 8 = 1.
// And for the last line
// yPixel = 144. 144 % 8 = 0.
// 0 Represents last line of pixels in a tile, 1 represents first. 1 2 3 4 5 6 7 0.
// Because remember, we are counting lines on the display NOT including zero
let pixelYInTile = i32Portable(pixelYPositionInMap & 7);
// Remember to represent a single line of 8 pixels on a tile, we need two bytes.
// Therefore, we need to times our modulo by 2, to get the correct line of pixels on the tile.
// Again, think like you had to map a 2d array as a 1d.
let byteOneForLineOfTilePixels: i32 = loadFromVramBank(tileDataAddress + pixelYInTile * 2, 0);
let byteTwoForLineOfTilePixels: i32 = loadFromVramBank(tileDataAddress + pixelYInTile * 2 + 1, 0);
// Same logic as pixelYInTile.
// However, We need to reverse our byte,
// As pixel 0 is on byte 7, and pixel 1 is on byte 6, etc...
// Therefore, is pixelX was 2, then really is need to be 5
// So 2 - 7 = -5, * 1 = 5
// Or to simplify, 7 - 2 = 5 haha!
let pixelXInTile = i32Portable(pixelXPositionInMap & 7);
pixelXInTile = 7 - pixelXInTile;
// Now we can get the color for that pixel
// Colors are represented by getting X position of ByteTwo, and X positon of Byte One
// To Get the color Id.
// For example, the result of the color id is 0000 00[xPixelByteTwo][xPixelByteOne]
// See: How to draw a tile/sprite from memory: http://www.codeslinger.co.uk/pages/projects/gameboy/graphics.html
let paletteColorId: u8 = 0;
if (checkBitOnByte(pixelXInTile, byteTwoForLineOfTilePixels)) {
// Byte one represents the second bit in our color id, so bit shift
paletteColorId += 1;
paletteColorId = paletteColorId << 1;
}
if (checkBitOnByte(pixelXInTile, byteOneForLineOfTilePixels)) {
paletteColorId += 1;
}
// Not checking u8 Portability overflow here, since it can't be greater than i32 over :p
// Now get the colorId from the pallete, to get our final color
// Developers could change colorIds to represents different colors
// in their palette, thus we need to grab the color from there
//let pixelColorInTileFromPalette: u8 = getColorFromPalette(paletteColorId, Graphics.memoryLocationBackgroundPalette);
// Moved below for perofrmance
// FINALLY, RENDER THAT PIXEL!
// Only rendering camera for now, so coordinates are for the camera.
// Get the rgb value for the color Id, will be repeated into R, G, B. if not colorized
let hexColor: i32 = getColorizedGbHexColorFromPalette(paletteColorId, Graphics.memoryLocationBackgroundPalette);
setPixelOnFrame(xPixel, yPixel, 0, getRedFromHexColor(hexColor));
setPixelOnFrame(xPixel, yPixel, 1, getGreenFromHexColor(hexColor));
setPixelOnFrame(xPixel, yPixel, 2, getBlueFromHexColor(hexColor));
// Lastly, add the pixel to our background priority map
// https://github.com/torch2424/wasmBoy/issues/51
// Bits 0 & 1 will represent the color Id drawn by the BG/Window
// Bit 2 will represent if the Bg/Window has GBC priority.
addPriorityforPixel(xPixel, yPixel, paletteColorId);
}
// Function to draw a pixel from a tile in C O L O R
// See above for more context on some variables
// Inlined because closure compiler inlines
function drawColorPixelFromTileId(
xPixel: i32,
yPixel: i32,
pixelXPositionInMap: i32,
pixelYPositionInMap: i32,
tileMapAddress: i32,
tileDataMemoryLocation: i32,
tileIdFromTileMap: i32
): void {
// Now get our tileDataAddress for the corresponding tileID we found in the map
// Read the comments in _getTileDataAddress() to see what's going on.
// tl;dr if we had the tile map of "a b c d", and wanted tileId 2.
// This funcitons returns the start of memory locaiton for the tile 'c'.
let tileDataAddress: i32 = getTileDataAddress(tileDataMemoryLocation, tileIdFromTileMap);
// Get the GB Map Attributes
// Bit 0-2 Background Palette number (BGP0-7)
// Bit 3 Tile VRAM Bank number (0=Bank 0, 1=Bank 1)
// Bit 4 Not used
// Bit 5 Horizontal Flip (0=Normal, 1=Mirror horizontally)
// Bit 6 Vertical Flip (0=Normal, 1=Mirror vertically)
// Bit 7 BG-to-OAM Priority (0=Use OAM priority bit, 1=BG Priority)
let bgMapAttributes: i32 = loadFromVramBank(tileMapAddress, 1);
// See above for explanation
let pixelYInTile = i32Portable(pixelYPositionInMap & 7);
if (checkBitOnByte(6, bgMapAttributes)) {
// We are mirroring the tile, therefore, we need to opposite byte
// So if our pixel was 0 our of 8, it wild become 7 :)
pixelYInTile = 7 - pixelYInTile;
}
// Remember to represent a single line of 8 pixels on a tile, we need two bytes.
// Therefore, we need to times our modulo by 2, to get the correct line of pixels on the tile.
// But we need to load the time from a specific Vram bank
let vramBankId = i32Portable(<i32>checkBitOnByte(3, bgMapAttributes));
let byteOneForLineOfTilePixels: i32 = loadFromVramBank(tileDataAddress + pixelYInTile * 2, vramBankId);
let byteTwoForLineOfTilePixels: i32 = loadFromVramBank(tileDataAddress + pixelYInTile * 2 + 1, vramBankId);
// Get our X pixel. Need to NOT reverse it if it was flipped.
// See above, you have to reverse this normally
let pixelXInTile = i32Portable(pixelXPositionInMap & 7);
if (!checkBitOnByte(5, bgMapAttributes)) {
pixelXInTile = 7 - pixelXInTile;
}
// Now we can get the color for that pixel
// Colors are represented by getting X position of ByteTwo, and X positon of Byte One
// To Get the color Id.
// For example, the result of the color id is 0000 00[xPixelByteTwo][xPixelByteOne]
// See: How to draw a tile/sprite from memory: http://www.codeslinger.co.uk/pages/projects/gameboy/graphics.html
let paletteColorId = 0;
if (checkBitOnByte(pixelXInTile, byteTwoForLineOfTilePixels)) {
// Byte one represents the second bit in our color id, so bit shift
paletteColorId += 1;
paletteColorId = paletteColorId << 1;
}
if (checkBitOnByte(pixelXInTile, byteOneForLineOfTilePixels)) {
paletteColorId += 1;
}
// Finally lets add some, C O L O R
// Want the botom 3 bits
let bgPalette = bgMapAttributes & 0x07;
// Call the helper function to grab the correct color from the palette
let rgbColorPalette = getRgbColorFromPalette(bgPalette, paletteColorId, false);
// Split off into red green and blue
let red = getColorComponentFromRgb(0, rgbColorPalette);
let green = getColorComponentFromRgb(1, rgbColorPalette);
let blue = getColorComponentFromRgb(2, rgbColorPalette);
// Finally Place our colors on the things
setPixelOnFrame(xPixel, yPixel, 0, red);
setPixelOnFrame(xPixel, yPixel, 1, green);
setPixelOnFrame(xPixel, yPixel, 2, blue);
// Lastly, add the pixel to our background priority map
// https://github.com/torch2424/wasmBoy/issues/51
// Bits 0 & 1 will represent the color Id drawn by the BG/Window
// Bit 2 will represent if the Bg/Window has GBC priority.
addPriorityforPixel(xPixel, yPixel, paletteColorId, checkBitOnByte(7, bgMapAttributes));
}
// Function to attempt to draw the tile from the tile cache
// Inlined because closure compiler inlines
function drawLineOfTileFromTileCache(
xPixel: i32,
yPixel: i32,
pixelXPositionInMap: i32,
pixelYPositionInMap: i32,
tileMapAddress: i32,
tileDataMemoryLocation: i32,
tileIdFromTileMap: i32
): i32 {
// First, initialize how many pixels we have drawn
let pixelsDrawn: i32 = 0;
// Check if the current tile matches our tileId
// TODO: Allow the first line to use the tile cache, for some odd reason it doesn't work when scanline is 0
let nextXIndexToPerformCacheCheck = TileCache.nextXIndexToPerformCacheCheck;
if (yPixel > 0 && xPixel > 8 && <i32>tileIdFromTileMap === TileCache.tileId && xPixel === nextXIndexToPerformCacheCheck) {
// Was last tile flipped
let wasLastTileHorizontallyFlipped = checkBitOnByte(5, eightBitLoadFromGBMemory(tileMapAddress - 1));
let isCurrentTileHorizontallyFlipped = checkBitOnByte(5, eightBitLoadFromGBMemory(tileMapAddress));
// Simply copy the last 8 pixels from memory to copy the line from the tile
for (let tileCacheIndex = 0; tileCacheIndex < 8; ++tileCacheIndex) {
// Check if we need to render backwards for flipping
if (wasLastTileHorizontallyFlipped !== isCurrentTileHorizontallyFlipped) {
tileCacheIndex = 7 - tileCacheIndex;
}
let xPos = xPixel + tileCacheIndex;
// First check for overflow
if (xPos <= 160) {
// Get the pixel location in memory of the tile
let previousXPixel = xPixel - (8 - tileCacheIndex);
let previousTilePixelLocation = FRAME_LOCATION + getRgbPixelStart(xPos, yPixel);
// Cycle through the RGB
// for (let tileCacheRgb = 0; tileCacheRgb < 3; ++tileCacheRgb) {
// setPixelOnFrame(xPixel + tileCacheIndex, yPixel, tileCacheRgb, load<u8>(previousTilePixelLocation + tileCacheRgb));
// }
// unroll
setPixelOnFrame(xPos, yPixel, 0, load<u8>(previousTilePixelLocation, 0));
setPixelOnFrame(xPos, yPixel, 1, load<u8>(previousTilePixelLocation, 1));
setPixelOnFrame(xPos, yPixel, 2, load<u8>(previousTilePixelLocation, 2));
// Copy the priority for the pixel
let pixelPriority: i32 = getPriorityforPixel(previousXPixel, yPixel);
addPriorityforPixel(xPos, yPixel, resetBitOnByte(2, pixelPriority), checkBitOnByte(2, pixelPriority));
pixelsDrawn++;
}
}
} else {
// Save our current tile Id, and the next x value we should check the x index
TileCache.tileId = tileIdFromTileMap;
}
// Calculate when we should do the tileCache calculation again
if (xPixel >= nextXIndexToPerformCacheCheck) {
nextXIndexToPerformCacheCheck = xPixel + 8;
let xOffsetTileWidthRemainder = i32Portable(pixelXPositionInMap & 7);
if (xPixel < xOffsetTileWidthRemainder) {
nextXIndexToPerformCacheCheck += xOffsetTileWidthRemainder;
}
}
TileCache.nextXIndexToPerformCacheCheck = nextXIndexToPerformCacheCheck;
return pixelsDrawn;
}
// Function to draw a line of a tile in Color
// This is for tile rendering shortcuts
// Inlined because closure compiler inlines
function drawLineOfTileFromTileId(
xPixel: i32,
yPixel: i32,
pixelXPositionInMap: i32,
pixelYPositionInMap: i32,
tileMapAddress: i32,
tileDataMemoryLocation: i32,
tileIdFromTileMap: i32
): i32 {
// Get the which line of the tile we are rendering
let tileLineY: i32 = i32Portable(pixelYPositionInMap & 7);
// Now lets find our tileX start and end
// This is for the case where i = 0, but scroll X was 3.
// Or i is 157, and our camera is only 160 pixels wide
let tileXStart = 0;
if (xPixel == 0) {
tileXStart = pixelXPositionInMap - ((pixelXPositionInMap >> 3) << 3);
}
let tileXEnd = 7;
if (xPixel + 8 > 160) {
tileXEnd = 160 - xPixel;
}
// initialize some variables for GBC
let bgMapAttributes = -1;
let vramBankId = 0;
if (Cpu.GBCEnabled) {
// Get Our GBC properties
bgMapAttributes = loadFromVramBank(tileMapAddress, 1);
vramBankId = i32Portable(<i32>checkBitOnByte(3, <u8>bgMapAttributes));
if (checkBitOnByte(6, bgMapAttributes)) {
// We are mirroring the tile, therefore, we need to opposite byte
// So if our pixel was 0 our of 8, it wild become 7 :)
tileLineY = 7 - tileLineY;
}
}
// Return the number of pixels drawn
return drawPixelsFromLineOfTile(
tileIdFromTileMap,
tileDataMemoryLocation,
vramBankId,
tileXStart,
tileXEnd,
tileLineY,
xPixel,
yPixel,
160,
FRAME_LOCATION,
false,
0,
bgMapAttributes,
-1
);
}