2320 lines
72 KiB
Plaintext
2320 lines
72 KiB
Plaintext
cpu = require 'z80'
|
|
cpu.gameboy = true
|
|
setmetatable(_ENV, cpu)
|
|
|
|
gb = {
|
|
ROM0 = 0x0000,
|
|
ROMX = 0x4000,
|
|
VRAM = 0x8000,
|
|
SRAM = 0xa000,
|
|
WRAM = 0xc000,
|
|
OAM = 0xfe00,
|
|
HRAM = 0xff80,
|
|
JOYP = 0xff00,
|
|
SB = 0xff01,
|
|
SC = 0xff02,
|
|
DIV = 0xff04,
|
|
TIMA = 0xff05,
|
|
TMA = 0xff06,
|
|
TAC = 0xff07,
|
|
IF = 0xff0f,
|
|
NR10 = 0xff10,
|
|
NR11 = 0xff11,
|
|
NR12 = 0xff12,
|
|
NR13 = 0xff13,
|
|
NR14 = 0xff14,
|
|
NR21 = 0xff16,
|
|
NR22 = 0xff17,
|
|
NR23 = 0xff18,
|
|
NR24 = 0xff19,
|
|
NR30 = 0xff1a,
|
|
NR31 = 0xff1b,
|
|
NR32 = 0xff1c,
|
|
NR33 = 0xff1d,
|
|
NR34 = 0xff1e,
|
|
NR41 = 0xff20,
|
|
NR42 = 0xff21,
|
|
NR43 = 0xff22,
|
|
NR44 = 0xff23,
|
|
NR50 = 0xff24,
|
|
NR51 = 0xff25,
|
|
NR52 = 0xff26,
|
|
LCDC = 0xff40,
|
|
STAT = 0xff41,
|
|
SCY = 0xff42,
|
|
SCX = 0xff43,
|
|
LY = 0xff44,
|
|
LYC = 0xff45,
|
|
DMA = 0xff46,
|
|
BGP = 0xff47,
|
|
OBP0 = 0xff48,
|
|
OBP1 = 0xff49,
|
|
WY = 0xff4a,
|
|
WX = 0xff4b,
|
|
KEY1 = 0xff4d,
|
|
KEY1_PREPARE = 0x01,
|
|
KEY1_DOUBLE_SPEED = 0x80,
|
|
VBK = 0xff4f,
|
|
HDMA1 = 0xff51,
|
|
HDMA2 = 0xff52,
|
|
HDMA3 = 0xff53,
|
|
HDMA4 = 0xff54,
|
|
HDMA5 = 0xff55,
|
|
RP = 0xff56,
|
|
RPF_EMIT = 0x01,
|
|
RPF_IDLE = 0x02,
|
|
RPF_READ_ENABLE = 0xc0,
|
|
BCPS = 0xff68,
|
|
BCPD = 0xff69,
|
|
OCPS = 0xff6a,
|
|
OCPD = 0xff6b,
|
|
SVBK = 0xff70,
|
|
IE = 0xffff,
|
|
MBC_RAM_ENABLE = 0x0000,
|
|
MBC_ROM_BANK = 0x2000,
|
|
MBC5_ROM_BANK_HIGH = 0x3000,
|
|
MBC5_RUMBLE = 0x08,
|
|
MBC_RAM_BANK = 0x4000,
|
|
MBC_RTC_REGISTER = 0x4000,
|
|
MBC_MODE = 0x6000,
|
|
MBC_RTC_LATCH = 0x6000,
|
|
|
|
RTC_SECONDS = 0x08,
|
|
RTC_MINUTES = 0x09,
|
|
RTC_HOURS = 0x0a,
|
|
RTC_DAY_LOW = 0x0b,
|
|
RTC_DAY_HIGH = 0x0c,
|
|
RTCF_DAY_8 = 0x01,
|
|
RTCF_HALT = 0x40,
|
|
RTCF_CARRY = 0x80,
|
|
|
|
HARDWARE_DMG = 0x00,
|
|
HARDWARE_CGB = 0x01,
|
|
HARDWARE_GBA = 0x02,
|
|
HARDWARE_GBC = 0x01,
|
|
HARDWARE_AGB = 0x02,
|
|
|
|
TACF_4096_HZ = 0x00,
|
|
TACF_262144_HZ = 0x01,
|
|
TACF_65536_HZ = 0x02,
|
|
TACF_16384_HZ = 0x03,
|
|
TACF_ENABLE = 0x04,
|
|
|
|
SCF_CLOCK_INTERNAL = 0x01,
|
|
SCF_CLOCK_FAST = 0x02,
|
|
SCF_START = 0x80,
|
|
|
|
PAD_A = 0x01,
|
|
PAD_B = 0x02,
|
|
PAD_SELECT = 0x04,
|
|
PAD_START = 0x08,
|
|
PAD_RIGHT = 0x10,
|
|
PAD_LEFT = 0x20,
|
|
PAD_UP = 0x40,
|
|
PAD_DOWN = 0x80,
|
|
|
|
IEF_VBLANK = 0x01,
|
|
IEF_LCD = 0x02,
|
|
IEF_TIMER = 0x04,
|
|
IEF_SERIAL = 0x08,
|
|
IEF_JOYPAD = 0x10,
|
|
|
|
OAMF_PRIORITY = 0x80,
|
|
OAMF_YFLIP = 0x40,
|
|
OAMF_XFLIP = 0x20,
|
|
OAMF_PALETTE = 0x10,
|
|
OAMF_BANK1 = 0x08,
|
|
}
|
|
|
|
-- Names and values follow gbdev/hardware.inc v5.3.0
|
|
local hardware_constants = {
|
|
P1 = 0xff00,
|
|
AUD1SWEEP = 0xff10, AUD1LEN = 0xff11, AUD1ENV = 0xff12,
|
|
AUD1LOW = 0xff13, AUD1HIGH = 0xff14,
|
|
AUD2LEN = 0xff16, AUD2ENV = 0xff17, AUD2LOW = 0xff18,
|
|
AUD2HIGH = 0xff19,
|
|
AUD3ENA = 0xff1a, AUD3LEN = 0xff1b, AUD3LEVEL = 0xff1c,
|
|
AUD3LOW = 0xff1d, AUD3HIGH = 0xff1e,
|
|
AUD4LEN = 0xff20, AUD4ENV = 0xff21, AUD4POLY = 0xff22,
|
|
AUD4GO = 0xff23, AUDVOL = 0xff24, AUDTERM = 0xff25,
|
|
AUDENA = 0xff26,
|
|
AUD3WAVE_0 = 0xff30, AUD3WAVE_1 = 0xff31,
|
|
AUD3WAVE_2 = 0xff32, AUD3WAVE_3 = 0xff33,
|
|
AUD3WAVE_4 = 0xff34, AUD3WAVE_5 = 0xff35,
|
|
AUD3WAVE_6 = 0xff36, AUD3WAVE_7 = 0xff37,
|
|
AUD3WAVE_8 = 0xff38, AUD3WAVE_9 = 0xff39,
|
|
AUD3WAVE_A = 0xff3a, AUD3WAVE_B = 0xff3b,
|
|
AUD3WAVE_C = 0xff3c, AUD3WAVE_D = 0xff3d,
|
|
AUD3WAVE_E = 0xff3e, AUD3WAVE_F = 0xff3f,
|
|
KEY0 = 0xff4c, SYS = 0xff4c, SPD = 0xff4d, BANK = 0xff50,
|
|
VDMA_SRC_HIGH = 0xff51, VDMA_SRC_LOW = 0xff52,
|
|
VDMA_DEST_HIGH = 0xff53, VDMA_DEST_LOW = 0xff54,
|
|
VDMA_LEN = 0xff55,
|
|
BGPI = 0xff68, BGPD = 0xff69, OBPI = 0xff6a, OBPD = 0xff6b,
|
|
OPRI = 0xff6c, WBK = 0xff70, PCM12 = 0xff76, PCM34 = 0xff77,
|
|
|
|
B_JOYP_GET_BUTTONS = 5, B_JOYP_GET_CTRL_PAD = 4,
|
|
JOYP_GET = 0x30, JOYP_GET_BUTTONS = 0x10,
|
|
JOYP_GET_CTRL_PAD = 0x20, JOYP_GET_NONE = 0x30,
|
|
B_JOYP_START = 3, B_JOYP_SELECT = 2, B_JOYP_B = 1,
|
|
B_JOYP_A = 0, B_JOYP_DOWN = 3, B_JOYP_UP = 2,
|
|
B_JOYP_LEFT = 1, B_JOYP_RIGHT = 0,
|
|
JOYP_INPUTS = 0x0f, JOYP_START = 0x08, JOYP_SELECT = 0x04,
|
|
JOYP_B = 0x02, JOYP_A = 0x01, JOYP_DOWN = 0x08,
|
|
JOYP_UP = 0x04, JOYP_LEFT = 0x02, JOYP_RIGHT = 0x01,
|
|
B_JOYP_SGB_ONE = 5, B_JOYP_SGB_ZERO = 4,
|
|
JOYP_SGB_START = 0x00, JOYP_SGB_ONE = 0x10,
|
|
JOYP_SGB_ZERO = 0x20, JOYP_SGB_FINISH = 0x30,
|
|
|
|
B_PAD_DOWN = 7, B_PAD_UP = 6, B_PAD_LEFT = 5, B_PAD_RIGHT = 4,
|
|
B_PAD_START = 3, B_PAD_SELECT = 2, B_PAD_B = 1, B_PAD_A = 0,
|
|
PAD_CTRL_PAD = 0xf0, PAD_BUTTONS = 0x0f,
|
|
PAD_DOWN = 0x80, PAD_UP = 0x40, PAD_LEFT = 0x20, PAD_RIGHT = 0x10,
|
|
PAD_START = 0x08, PAD_SELECT = 0x04, PAD_B = 0x02, PAD_A = 0x01,
|
|
B_SC_START = 7, B_SC_SPEED = 1, B_SC_SOURCE = 0,
|
|
SC_START = 0x80, SC_SPEED = 0x02, SC_SLOW = 0x00,
|
|
SC_FAST = 0x02, SC_SOURCE = 0x01, SC_EXTERNAL = 0x00,
|
|
SC_INTERNAL = 0x01,
|
|
|
|
B_TAC_START = 2, TAC_STOP = 0x00, TAC_START = 0x04,
|
|
TAC_CLOCK = 0x03, TAC_4KHZ = 0x00, TAC_262KHZ = 0x01,
|
|
TAC_65KHZ = 0x02, TAC_16KHZ = 0x03,
|
|
|
|
B_IF_JOYPAD = 4, B_IF_SERIAL = 3, B_IF_TIMER = 2,
|
|
B_IF_STAT = 1, B_IF_VBLANK = 0,
|
|
IF_JOYPAD = 0x10, IF_SERIAL = 0x08, IF_TIMER = 0x04,
|
|
IF_STAT = 0x02, IF_VBLANK = 0x01,
|
|
B_IE_JOYPAD = 4, B_IE_SERIAL = 3, B_IE_TIMER = 2,
|
|
B_IE_STAT = 1, B_IE_VBLANK = 0,
|
|
IE_JOYPAD = 0x10, IE_SERIAL = 0x08, IE_TIMER = 0x04,
|
|
IE_STAT = 0x02, IE_VBLANK = 0x01,
|
|
|
|
AUD1SWEEP_TIME = 0x70, B_AUD1SWEEP_DIR = 3,
|
|
AUD1SWEEP_DIR = 0x08, AUD1SWEEP_UP = 0x00,
|
|
AUD1SWEEP_DOWN = 0x08, AUD1SWEEP_SHIFT = 0x07,
|
|
AUD1LEN_DUTY = 0xc0, AUD1LEN_DUTY_12_5 = 0x00,
|
|
AUD1LEN_DUTY_25 = 0x40, AUD1LEN_DUTY_50 = 0x80,
|
|
AUD1LEN_DUTY_75 = 0xc0, AUD1LEN_TIMER = 0x3f,
|
|
AUD1ENV_INIT_VOLUME = 0xf0, B_AUD1ENV_DIR = 3,
|
|
AUD1ENV_DIR = 0x08, AUD1ENV_DOWN = 0x00, AUD1ENV_UP = 0x08,
|
|
AUD1ENV_PACE = 0x07,
|
|
B_AUD1HIGH_RESTART = 7, B_AUD1HIGH_LEN_ENABLE = 6,
|
|
AUD1HIGH_RESTART = 0x80, AUD1HIGH_LENGTH_OFF = 0x00,
|
|
AUD1HIGH_LENGTH_ON = 0x40, AUD1HIGH_PERIOD_HIGH = 0x07,
|
|
|
|
AUD2LEN_DUTY = 0xc0, AUD2LEN_DUTY_12_5 = 0x00,
|
|
AUD2LEN_DUTY_25 = 0x40, AUD2LEN_DUTY_50 = 0x80,
|
|
AUD2LEN_DUTY_75 = 0xc0, AUD2LEN_TIMER = 0x3f,
|
|
AUD2ENV_INIT_VOLUME = 0xf0, B_AUD2ENV_DIR = 3,
|
|
AUD2ENV_DIR = 0x08, AUD2ENV_DOWN = 0x00, AUD2ENV_UP = 0x08,
|
|
AUD2ENV_PACE = 0x07,
|
|
B_AUD2HIGH_RESTART = 7, B_AUD2HIGH_LEN_ENABLE = 6,
|
|
AUD2HIGH_RESTART = 0x80, AUD2HIGH_LENGTH_OFF = 0x00,
|
|
AUD2HIGH_LENGTH_ON = 0x40, AUD2HIGH_PERIOD_HIGH = 0x07,
|
|
|
|
B_AUD3ENA_ENABLE = 7, AUD3ENA_OFF = 0x00, AUD3ENA_ON = 0x80,
|
|
AUD3LEVEL_VOLUME = 0x60, AUD3LEVEL_MUTE = 0x00,
|
|
AUD3LEVEL_100 = 0x20, AUD3LEVEL_50 = 0x40,
|
|
AUD3LEVEL_25 = 0x60,
|
|
B_AUD3HIGH_RESTART = 7, B_AUD3HIGH_LEN_ENABLE = 6,
|
|
AUD3HIGH_RESTART = 0x80, AUD3HIGH_LENGTH_OFF = 0x00,
|
|
AUD3HIGH_LENGTH_ON = 0x40, AUD3HIGH_PERIOD_HIGH = 0x07,
|
|
|
|
AUD4LEN_TIMER = 0x3f, AUD4ENV_INIT_VOLUME = 0xf0,
|
|
B_AUD4ENV_DIR = 3, AUD4ENV_DIR = 0x08, AUD4ENV_DOWN = 0x00,
|
|
AUD4ENV_UP = 0x08, AUD4ENV_PACE = 0x07,
|
|
AUD4POLY_SHIFT = 0xf0, B_AUD4POLY_WIDTH = 3,
|
|
AUD4POLY_15STEP = 0x00, AUD4POLY_7STEP = 0x08,
|
|
AUD4POLY_DIV = 0x07,
|
|
B_AUD4GO_RESTART = 7, B_AUD4GO_LEN_ENABLE = 6,
|
|
AUD4GO_RESTART = 0x80, AUD4GO_LENGTH_OFF = 0x00,
|
|
AUD4GO_LENGTH_ON = 0x40,
|
|
|
|
B_AUDVOL_VIN_LEFT = 7, AUDVOL_VIN_LEFT = 0x80,
|
|
AUDVOL_LEFT = 0x70, B_AUDVOL_VIN_RIGHT = 3,
|
|
AUDVOL_VIN_RIGHT = 0x08, AUDVOL_RIGHT = 0x07,
|
|
B_AUDTERM_4_LEFT = 7, B_AUDTERM_3_LEFT = 6,
|
|
B_AUDTERM_2_LEFT = 5, B_AUDTERM_1_LEFT = 4,
|
|
B_AUDTERM_4_RIGHT = 3, B_AUDTERM_3_RIGHT = 2,
|
|
B_AUDTERM_2_RIGHT = 1, B_AUDTERM_1_RIGHT = 0,
|
|
AUDTERM_4_LEFT = 0x80, AUDTERM_3_LEFT = 0x40,
|
|
AUDTERM_2_LEFT = 0x20, AUDTERM_1_LEFT = 0x10,
|
|
AUDTERM_4_RIGHT = 0x08, AUDTERM_3_RIGHT = 0x04,
|
|
AUDTERM_2_RIGHT = 0x02, AUDTERM_1_RIGHT = 0x01,
|
|
B_AUDENA_ENABLE = 7, B_AUDENA_ENABLE_CH4 = 3,
|
|
B_AUDENA_ENABLE_CH3 = 2, B_AUDENA_ENABLE_CH2 = 1,
|
|
B_AUDENA_ENABLE_CH1 = 0, AUDENA_OFF = 0x00, AUDENA_ON = 0x80,
|
|
AUDENA_CH4_OFF = 0x00, AUDENA_CH4_ON = 0x08,
|
|
AUDENA_CH3_OFF = 0x00, AUDENA_CH3_ON = 0x04,
|
|
AUDENA_CH2_OFF = 0x00, AUDENA_CH2_ON = 0x02,
|
|
AUDENA_CH1_OFF = 0x00, AUDENA_CH1_ON = 0x01,
|
|
|
|
B_LCDC_ENABLE = 7, B_LCDC_WIN_MAP = 6, B_LCDC_WINDOW = 5,
|
|
B_LCDC_BLOCKS = 4, B_LCDC_BG_MAP = 3, B_LCDC_OBJ_SIZE = 2,
|
|
B_LCDC_OBJS = 1, B_LCDC_BG = 0, B_LCDC_PRIO = 0,
|
|
LCDC_ENABLE = 0x80, LCDC_OFF = 0x00, LCDC_ON = 0x80,
|
|
LCDC_WIN_MAP = 0x40, LCDC_WIN_9800 = 0x00, LCDC_WIN_9C00 = 0x40,
|
|
LCDC_WINDOW = 0x20, LCDC_WIN_OFF = 0x00, LCDC_WIN_ON = 0x20,
|
|
LCDC_BLOCKS = 0x10, LCDC_BLOCK21 = 0x00, LCDC_BLOCK01 = 0x10,
|
|
LCDC_BG_MAP = 0x08, LCDC_BG_9800 = 0x00, LCDC_BG_9C00 = 0x08,
|
|
LCDC_OBJ_SIZE = 0x04, LCDC_OBJ_8 = 0x00, LCDC_OBJ_16 = 0x04,
|
|
LCDC_OBJS = 0x02, LCDC_OBJ_OFF = 0x00, LCDC_OBJ_ON = 0x02,
|
|
LCDC_BG = 0x01, LCDC_BG_OFF = 0x00, LCDC_BG_ON = 0x01,
|
|
LCDC_PRIO = 0x01, LCDC_PRIO_OFF = 0x00, LCDC_PRIO_ON = 0x01,
|
|
LCDC_DEFAULT = 0x91,
|
|
|
|
B_STAT_LYC = 6, B_STAT_MODE_2 = 5, B_STAT_MODE_1 = 4,
|
|
B_STAT_MODE_0 = 3, B_STAT_LYCF = 2, B_STAT_BUSY = 1,
|
|
STAT_LYC = 0x40, STAT_MODE_2 = 0x20, STAT_MODE_1 = 0x10,
|
|
STAT_MODE_0 = 0x08, STAT_LYCF = 0x04, STAT_BUSY = 0x02,
|
|
STAT_MODE = 0x03, STAT_HBLANK = 0x00, STAT_VBLANK = 0x01,
|
|
STAT_OAM = 0x02, STAT_LCD = 0x03, LY_VBLANK = 144,
|
|
BGP_SGB_TRANSFER = 0xe4, WX_OFS = 7,
|
|
|
|
SYS_MODE = 0x0c, SYS_CGB = 0x00, SYS_DMG = 0x04,
|
|
SYS_PGB1 = 0x08, SYS_PGB2 = 0x0c,
|
|
B_SPD_DOUBLE = 7, B_SPD_PREPARE = 0,
|
|
SPD_SINGLE = 0x00, SPD_DOUBLE = 0x80, SPD_PREPARE = 0x01,
|
|
VBK_BANK = 0x01, B_BANK_ON = 0, BANK_ON = 0x00, BANK_OFF = 0x01,
|
|
B_VDMA_LEN_MODE = 7, VDMA_LEN_MODE = 0x80,
|
|
VDMA_LEN_MODE_GENERAL = 0x00, VDMA_LEN_MODE_HBLANK = 0x80,
|
|
B_VDMA_LEN_BUSY = 7, VDMA_LEN_BUSY = 0x80,
|
|
VDMA_LEN_NO = 0x00, VDMA_LEN_YES = 0x80, VDMA_LEN_SIZE = 0x7f,
|
|
RP_READ = 0xc0, RP_DISABLE = 0x00, RP_ENABLE = 0xc0,
|
|
B_RP_DATA_IN = 1, B_RP_LED_ON = 0, RP_DATA_IN = 0x02,
|
|
RP_LED_ON = 0x01, RP_WRITE_LOW = 0x00, RP_WRITE_HIGH = 0x01,
|
|
B_BGPI_AUTOINC = 7, BGPI_AUTOINC = 0x80, BGPI_INDEX = 0x3f,
|
|
B_OBPI_AUTOINC = 7, OBPI_AUTOINC = 0x80, OBPI_INDEX = 0x3f,
|
|
B_OPRI_PRIORITY = 0, OPRI_PRIORITY = 0x01,
|
|
OPRI_OAM = 0x00, OPRI_COORD = 0x01, WBK_BANK = 0x07,
|
|
PCM12_CH2 = 0xf0, PCM12_CH1 = 0x0f,
|
|
PCM34_CH4 = 0xf0, PCM34_CH3 = 0x0f,
|
|
|
|
RAMG = 0x0000, ROMB = 0x2000, RAMB = 0x4000,
|
|
BMODE = 0x6000, ROM2B = 0x2100, RTCLATCH = 0x6000,
|
|
RTCREG = 0xa000, ROMB0 = 0x2000, ROMB1 = 0x3000,
|
|
RAMG_SRAM_DISABLE = 0x00, RAMG_SRAM_ENABLE = 0x0a,
|
|
RAMB_RTC_S = 0x08, RAMB_RTC_M = 0x09, RAMB_RTC_H = 0x0a,
|
|
RAMB_RTC_DL = 0x0b, RAMB_RTC_DH = 0x0c,
|
|
B_RAMB_RTC_DH_CARRY = 7, B_RAMB_RTC_DH_HALT = 6,
|
|
B_RAMB_RTC_DH_HIGH = 0, RAMB_RTC_DH_CARRY = 0x80,
|
|
RAMB_RTC_DH_HALT = 0x40, RAMB_RTC_DH_HIGH = 0x01,
|
|
B_RAMB_RUMBLE = 3, RAMB_RUMBLE = 0x08,
|
|
RAMB_RUMBLE_OFF = 0x00, RAMB_RUMBLE_ON = 0x08,
|
|
BMODE_SIMPLE = 0x00, BMODE_ADVANCED = 0x01,
|
|
RTCLATCH_START = 0x00, RTCLATCH_FINISH = 0x01,
|
|
|
|
SCREEN_WIDTH_PX = 160, SCREEN_HEIGHT_PX = 144,
|
|
SCREEN_WIDTH = 20, SCREEN_HEIGHT = 18, SCREEN_AREA = 360,
|
|
VRAM_SIZE = 0x2000, DMG_PALETTE_IDENTITY = 0xe4,
|
|
TILEMAP_WIDTH_PX = 256, TILEMAP_HEIGHT_PX = 256,
|
|
TILEMAP_WIDTH = 32, TILEMAP_HEIGHT = 32, TILEMAP_AREA = 1024,
|
|
TILE_WIDTH = 8, TILE_HEIGHT = 8, TILE_SIZE = 16,
|
|
COLOR_SIZE = 2, PAL_COLORS = 4, PAL_SIZE = 8,
|
|
COLOR_CH_WIDTH = 5, COLOR_CH_MAX = 31,
|
|
B_COLOR_RED = 0, B_COLOR_GREEN = 5, B_COLOR_BLUE = 10,
|
|
COLOR_RED = 0x1f, COLOR_GREEN_LOW = 0xe0,
|
|
COLOR_GREEN_HIGH = 0x03, COLOR_BLUE = 0x7c,
|
|
SHADE_WHITE = 0, SHADE_LIGHT = 1, SHADE_DARK = 2, SHADE_BLACK = 3,
|
|
TILEMAP0 = 0x9800, TILEMAP1 = 0x9c00,
|
|
B_BG_PRIO = 7, B_BG_YFLIP = 6, B_BG_XFLIP = 5, B_BG_BANK1 = 3,
|
|
BG_PALETTE = 0x07, BG_PRIO = 0x80, BG_YFLIP = 0x40,
|
|
BG_XFLIP = 0x20, BG_BANK0 = 0x00, BG_BANK1 = 0x08,
|
|
|
|
OAMA_Y = 0, OAMA_X = 1, OAMA_TILEID = 2, OAMA_FLAGS = 3,
|
|
OAM_Y_OFS = 16, OAM_X_OFS = 8,
|
|
B_OAM_PRIO = 7, B_OAM_YFLIP = 6, B_OAM_XFLIP = 5,
|
|
B_OAM_PAL1 = 4, B_OAM_BANK1 = 3, OAM_PALETTE = 0x07,
|
|
OAM_PRIO = 0x80, OAM_YFLIP = 0x40, OAM_XFLIP = 0x20,
|
|
OAM_PAL0 = 0x00, OAM_PAL1 = 0x10,
|
|
OAM_BANK0 = 0x00, OAM_BANK1 = 0x08,
|
|
OBJ_SIZE = 4, OAM_COUNT = 40, OAM_SIZE = 160,
|
|
|
|
AUD1RAM = 0xff10, AUD2RAM = 0xff15,
|
|
AUD3RAM = 0xff1a, AUD4RAM = 0xff1f, AUDRAM_SIZE = 5,
|
|
AUD3WAVERAM = 0xff30, AUD3WAVE_SIZE = 16,
|
|
INT_HANDLER_VBLANK = 0x0040, INT_HANDLER_STAT = 0x0048,
|
|
INT_HANDLER_TIMER = 0x0050, INT_HANDLER_SERIAL = 0x0058,
|
|
INT_HANDLER_JOYPAD = 0x0060,
|
|
|
|
BOOTUP_A_DMG = 0x01, BOOTUP_A_CGB = 0x11, BOOTUP_A_MGB = 0xff,
|
|
BOOTUP_A_SGB = 0x01, BOOTUP_A_SGB2 = 0xff, B_BOOTUP_B_AGB = 0,
|
|
BOOTUP_B_CGB = 0x00, BOOTUP_B_AGB = 0x01,
|
|
BOOTUP_C_DMG = 0x13, BOOTUP_C_SGB = 0x14, BOOTUP_C_CGB = 0x00,
|
|
BOOTUP_D_MONO = 0x00, BOOTUP_D_COLOR = 0xff,
|
|
BOOTUP_E_DMG0 = 0xc1, BOOTUP_E_DMG = 0xc8,
|
|
BOOTUP_E_SGB = 0x00, BOOTUP_E_CGB_DMGMODE = 0x08,
|
|
BOOTUP_E_CGB = 0x56,
|
|
}
|
|
for name, value in pairs(hardware_constants) do
|
|
local previous = gb[name]
|
|
assert(previous == nil or previous == value,
|
|
name .. " disagrees with hardware.inc v5.3.0")
|
|
gb[name] = value
|
|
end
|
|
|
|
-- Public numeric constants must remain assembler symbols so delayed operands
|
|
-- can resolve them, but only actual hardware registers belong in debugger
|
|
-- symbol files.
|
|
local function is_debugger_register(name, value)
|
|
return value >= 0xff00 and value <= 0xff7f
|
|
or name == "IE"
|
|
end
|
|
|
|
do
|
|
local symbols = cpu.symbols
|
|
for k, v in pairs(gb) do symbols[k] = v end
|
|
end
|
|
|
|
-- RGBDS-compatible .sym files are understood by the usual Game Boy
|
|
-- debuggers. ROM labels include their physical bank; non-ROM symbols use
|
|
-- their unbanked CPU address.
|
|
local function gb_symfile()
|
|
local fmt = string.format
|
|
local s = cpu.getsym(function(addr, name, label_name)
|
|
local gb_value = gb[label_name]
|
|
if type(gb_value) == "number"
|
|
and not is_debugger_register(label_name, gb_value) then
|
|
return
|
|
end
|
|
local physical = cpu.symbolsorg[label_name]
|
|
if physical and physical >= 0 then
|
|
local bank = math.floor(physical / 0x4000)
|
|
return fmt("%02x:%04x %s", bank, addr & 0xffff, name)
|
|
elseif addr >= 0 and addr <= 0xffff then
|
|
return fmt("%04x %s", addr, name)
|
|
end
|
|
end)
|
|
return table.concat(s, '\n')
|
|
end
|
|
|
|
cpu.getsym_as.rgbds = gb_symfile
|
|
cpu.getsym_as.bgb = gb_symfile
|
|
cpu.getsym_as.mesen = gb_symfile
|
|
cpu.getsym_as.sameboy = gb_symfile
|
|
cpu.getsym_as.mgba = gb_symfile
|
|
cpu.getsym_as.emulicious = gb_symfile
|
|
|
|
-- Game Boy debugfile v1 support
|
|
-- https://github.com/aaaaaa123456789/gb-debugfiles/blob/master/debugfile.md
|
|
--
|
|
-- Debug annotations are zero-sized markers. Their addresses are resolved only
|
|
-- when the debugfile is written, after the linker has placed their sections.
|
|
local debugfile_entries = {}
|
|
|
|
local function debug_identifier(name, variable)
|
|
assert(type(name) == "string" and name:match("^[A-Za-z_][A-Za-z0-9$#.@_]*$"),
|
|
"invalid debugfile identifier: " .. tostring(name))
|
|
if variable then
|
|
assert(name:sub(1, 1) == "_",
|
|
"debugfile variable names must begin with an underscore")
|
|
end
|
|
return name
|
|
end
|
|
|
|
local function debug_plain_string(value)
|
|
assert(type(value) == "string", "debugfile string must be a string")
|
|
assert(not value:find('"', 1, true),
|
|
"plain debugfile strings cannot contain a quote")
|
|
for i = 1, #value do
|
|
assert(value:byte(i) >= 32,
|
|
"plain debugfile strings cannot contain control characters")
|
|
end
|
|
return '"' .. value .. '"'
|
|
end
|
|
|
|
local function debug_message_string(value)
|
|
assert(type(value) == "string", "debugfile message must be a string")
|
|
value = value:gsub("\r\n", "{:n}")
|
|
:gsub("\r", "{:n}")
|
|
:gsub("\n", "{:n}")
|
|
:gsub("\t", "{:t}")
|
|
:gsub('"', "{:q}")
|
|
for i = 1, #value do
|
|
assert(value:byte(i) >= 32,
|
|
"debugfile messages cannot contain control characters")
|
|
end
|
|
return '"' .. value .. '"'
|
|
end
|
|
|
|
local function debug_expression(value)
|
|
if type(value) == "number" then
|
|
assert(value % 1 == 0, "debugfile numeric expressions must be integers")
|
|
return value < 0 and tostring(value) or string.format("$%X", value)
|
|
elseif type(value) == "boolean" then
|
|
return value and "1" or "0"
|
|
end
|
|
assert(type(value) == "string" and value ~= "",
|
|
"debugfile expression must be a string, integer, or boolean")
|
|
assert(not value:find("[\r\n]"),
|
|
"debugfile expressions must fit on one line")
|
|
return value
|
|
end
|
|
|
|
local function debug_flags(flags)
|
|
flags = flags or "x"
|
|
assert(type(flags) == "string", "debugfile flags must be a string")
|
|
flags = string.lower(flags)
|
|
assert(flags ~= "" and not flags:find("[^rwxsdmb]"),
|
|
"invalid debugfile flags: " .. flags)
|
|
|
|
local seen, operation, index = {}, false, 1
|
|
local doubled = { w = true, x = true, s = true, b = true }
|
|
while index <= #flags do
|
|
local family = flags:sub(index, index)
|
|
local token = family
|
|
if doubled[family] and flags:sub(index + 1, index + 1) == family then
|
|
token = family .. family
|
|
index = index + 1
|
|
end
|
|
assert(not seen[family], "duplicate or conflicting debugfile flag: " .. token)
|
|
seen[family] = token
|
|
operation = operation or family == "r" or family == "w" or family == "x"
|
|
index = index + 1
|
|
end
|
|
assert(operation, "debugfile actions require an r, w, ww, x, or xx flag")
|
|
return flags
|
|
end
|
|
|
|
local function debug_commands(commands)
|
|
if type(commands) == "table" then
|
|
commands = table.concat(commands, "; ")
|
|
end
|
|
assert(type(commands) == "string" and commands:match("%S"),
|
|
"debugfile action requires at least one command")
|
|
assert(not commands:find("[\r\n]"),
|
|
"debugfile commands must fit on one line")
|
|
assert(not commands:match(";%s*$"),
|
|
"the final debugfile command must not end with a semicolon")
|
|
return commands
|
|
end
|
|
|
|
local function debug_marker()
|
|
local section = cpu.section_current
|
|
assert(section, "dbg_action requires a current section")
|
|
|
|
local marker = { type = "debug_marker", section = section }
|
|
marker.size = function()
|
|
marker.offset = section.size
|
|
marker.size = 0
|
|
return 0
|
|
end
|
|
table.insert(section.instructions, marker)
|
|
section.refcount = (section.refcount or 0) + 1
|
|
return marker
|
|
end
|
|
|
|
local function banked_debug_address(physical, address)
|
|
assert(type(physical) == "number" and type(address) == "number",
|
|
"unresolved debugfile address")
|
|
return string.format("$%02X:$%04X", physical // 0x4000, address & 0xffff)
|
|
end
|
|
|
|
local function marker_debug_address(marker)
|
|
local section = marker.section
|
|
assert(type(section.org) == "number" and type(marker.offset) == "number",
|
|
"unresolved debugfile marker")
|
|
local physical = section.org + marker.offset
|
|
return banked_debug_address(physical, section.location.rorg(physical))
|
|
end
|
|
|
|
local function debug_address(address)
|
|
if type(address) == "function" then return debug_address(address()) end
|
|
if type(address) == "table" then
|
|
if address.label then return debug_address(address.label) end
|
|
local values = {}
|
|
for i, value in ipairs(address) do values[i] = debug_address(value) end
|
|
assert(#values > 0, "empty debugfile address list")
|
|
return table.concat(values, ",")
|
|
end
|
|
if type(address) == "number" then
|
|
assert(address % 1 == 0, "debugfile address must be an integer")
|
|
return string.format("$%04X", address & 0xffff)
|
|
end
|
|
assert(type(address) == "string" and address ~= "",
|
|
"invalid debugfile address")
|
|
|
|
local resolved = rawget(cpu.symbols, address)
|
|
if type(resolved) == "number" then
|
|
local physical = cpu.symbolsorg[address]
|
|
if type(physical) == "number" then
|
|
return banked_debug_address(physical, resolved)
|
|
end
|
|
return string.format("$%04X", resolved & 0xffff)
|
|
end
|
|
assert(not address:find("%s"),
|
|
"debugfile address fields cannot contain whitespace")
|
|
return address
|
|
end
|
|
|
|
local function add_debug_action(address, flags, commands, condition)
|
|
local resolved_condition
|
|
if condition ~= nil then resolved_condition = debug_expression(condition) end
|
|
debugfile_entries[#debugfile_entries + 1] = {
|
|
type = "action",
|
|
address = address,
|
|
flags = debug_flags(flags),
|
|
commands = debug_commands(commands),
|
|
condition = resolved_condition,
|
|
}
|
|
end
|
|
|
|
-- Add any single raw directive, private-use line, or action. The mandatory
|
|
-- @debugfile directive is generated automatically by writedebug(). A function
|
|
-- may be supplied when the line needs link-time resolution.
|
|
function gb.dbg_line(line)
|
|
assert(type(line) == "string" or type(line) == "function",
|
|
"dbg_line expects a string or function")
|
|
debugfile_entries[#debugfile_entries + 1] = { type = "raw", line = line }
|
|
end
|
|
|
|
function gb.dbg_var(name, initial)
|
|
name = debug_identifier(name, true)
|
|
initial = debug_expression(initial == nil and 0 or initial)
|
|
dbg_line("@var " .. name .. " " .. initial)
|
|
end
|
|
|
|
function gb.dbg_group(name, display_name)
|
|
name = debug_identifier(name)
|
|
local line = "@group " .. name
|
|
if display_name ~= nil then line = line .. " " .. debug_plain_string(display_name) end
|
|
dbg_line(line)
|
|
end
|
|
|
|
function gb.end_dbg_group()
|
|
dbg_line("@endgroup")
|
|
end
|
|
|
|
-- Add an execute action at the current assembly position. This emits no ROM
|
|
-- bytes. flags defaults to "x".
|
|
function gb.dbg_action(commands, condition, flags)
|
|
add_debug_action(debug_marker(), flags or "x", commands, condition)
|
|
end
|
|
|
|
-- Add an action for an explicit address, symbol, range expression, or list of
|
|
-- address specifications. This supports read/write watches as well as code.
|
|
function gb.dbg_watch(address, flags, commands, condition)
|
|
add_debug_action(address, flags, commands, condition)
|
|
end
|
|
|
|
function gb.dbg_break(condition, flags)
|
|
dbg_action("break", condition, flags)
|
|
end
|
|
|
|
function gb.dbg_log(message, condition, flags)
|
|
dbg_action("message " .. debug_message_string(message), condition, flags)
|
|
end
|
|
|
|
function gb.dbg_alert(message, condition, flags)
|
|
dbg_action("alert " .. debug_message_string(message), condition, flags)
|
|
end
|
|
|
|
function gb.runtime_assert(condition, message)
|
|
condition = debug_expression(condition)
|
|
dbg_alert(message or "assert failure", "!(" .. condition .. ")")
|
|
end
|
|
|
|
function gb.unreachable(message)
|
|
dbg_alert(message or "unreachable code reached!")
|
|
end
|
|
|
|
-- Return a complete version-1 debugfile after resolving all link-time marker
|
|
-- addresses. opt may be a symfile path or a table with version and symfile.
|
|
function gb.getdebugfile(opt)
|
|
if type(opt) == "string" then opt = { symfile = opt } end
|
|
opt = opt or {}
|
|
assert(type(opt) == "table", "debugfile options must be a table or symfile path")
|
|
cpu.resolve()
|
|
|
|
local version = tostring(opt.version or "1")
|
|
local version_parts = {}
|
|
for part in version:gmatch("[^.]+") do version_parts[#version_parts + 1] = part end
|
|
assert(#version_parts >= 1 and #version_parts <= 3
|
|
and table.concat(version_parts, ".") == version,
|
|
"invalid debugfile version")
|
|
for _, part in ipairs(version_parts) do
|
|
assert(part:match("^%d+$") and (part == "0" or part:sub(1, 1) ~= "0"),
|
|
"invalid debugfile version")
|
|
end
|
|
local lines = { "@debugfile " .. version }
|
|
if opt.symfile then
|
|
lines[#lines + 1] = "@symfile " .. debug_plain_string(opt.symfile)
|
|
end
|
|
|
|
for _, entry in ipairs(debugfile_entries) do
|
|
if entry.type == "raw" then
|
|
local line = type(entry.line) == "function" and entry.line() or entry.line
|
|
assert(type(line) == "string" and not line:find("[\r\n]"),
|
|
"dbg_line must resolve to exactly one line")
|
|
lines[#lines + 1] = line
|
|
else
|
|
local address = type(entry.address) == "table"
|
|
and entry.address.type == "debug_marker"
|
|
and marker_debug_address(entry.address)
|
|
or debug_address(entry.address)
|
|
local condition = entry.condition and " " .. entry.condition or ""
|
|
lines[#lines + 1] = string.format("%s %s%s: %s",
|
|
address, entry.flags, condition, entry.commands)
|
|
end
|
|
end
|
|
return table.concat(lines, "\n") .. "\n"
|
|
end
|
|
|
|
function gb.writedebug(filename, opt)
|
|
assert(type(filename) == "string" and filename ~= "",
|
|
"writedebug requires a filename")
|
|
local file = assert(io.open(filename, "wb"),
|
|
"failed to open " .. filename .. " for writing")
|
|
file:write(getdebugfile(opt))
|
|
file:close()
|
|
end
|
|
|
|
rom0 = location{ 0x0000, 0x3fff, name = "rom0" }
|
|
rom1 = location{ 0x4000, 0x7fff, rorg = 0x4000, name = "rom1" }
|
|
rom = rom0
|
|
romx = rom1
|
|
location(rom0)
|
|
|
|
local function contiguous_rom()
|
|
if rom0 == rom1 then return rom0 end
|
|
|
|
assert(#rom0.sections == 0 and #rom1.sections == 0,
|
|
"32 KiB mapper must be declared before ROM sections")
|
|
|
|
local fixed, switchable = rom0, rom1
|
|
for i = #cpu.locations, 1, -1 do
|
|
local loc = cpu.locations[i]
|
|
if loc == fixed or loc == switchable then
|
|
table.remove(cpu.locations, i)
|
|
end
|
|
end
|
|
|
|
rom = location{ 0x0000, 0x7fff, name = "rom" }
|
|
rom0 = rom
|
|
rom1 = rom
|
|
romx = rom
|
|
return rom
|
|
end
|
|
|
|
function gb.romx_bank(bank)
|
|
assert(bank >= 1, "ROMX bank index must be at least 1")
|
|
local name = "rom" .. bank
|
|
local loc = rawget(_ENV, name)
|
|
if not loc then
|
|
local offset = bank * 0x4000
|
|
loc = location{ offset, offset + 0x3fff, rorg = 0x4000, name = name }
|
|
rawset(_ENV, name, loc)
|
|
else
|
|
location(loc)
|
|
end
|
|
return loc
|
|
end
|
|
|
|
function gb.rom_banks(count)
|
|
assert(count >= 2, "Game Boy ROM must have at least 2 banks")
|
|
assert(count <= 512, "Game Boy ROM can have at most 512 banks")
|
|
if count == 2 then
|
|
location(contiguous_rom())
|
|
return rom
|
|
end
|
|
for bank = 1, count - 1 do gb.romx_bank(bank) end
|
|
location(rom0)
|
|
return rom0
|
|
end
|
|
|
|
function gb.romx_offset(bank, org)
|
|
org = org or 0x4000
|
|
assert(org >= 0x4000 and org <= 0x7fff, "ROMX CPU address must be in 0x4000-0x7fff")
|
|
return bank * 0x4000 + (org - 0x4000)
|
|
end
|
|
|
|
function gb.romx_section(name, bank, org)
|
|
location(gb.romx_bank(bank))
|
|
return section{ name, org = gb.romx_offset(bank, org) }
|
|
end
|
|
|
|
if rawget(_ENV, "stack_size") == nil then rawset(_ENV, "stack_size", 0x20) end
|
|
if rawget(_ENV, "stack_address") == nil then rawset(_ENV, "stack_address", 0xfffe) end
|
|
|
|
-- stack_size reserves bytes below stack_address for the downward-growing
|
|
-- stack. Their defaults are 0x20 and 0xfffe. Set them after the mapper and
|
|
-- before allocating RAM; the WRAM and HRAM allocators stop before the stack.
|
|
local function stack_config()
|
|
local size = stack_size
|
|
assert(type(size) == "number" and size >= 0 and size % 1 == 0,
|
|
"stack_size must be a non-negative integer")
|
|
local top = stack_address
|
|
assert(type(top) == "number" and top % 1 == 0,
|
|
"stack_address must be an integer")
|
|
|
|
local region, region_start
|
|
if top >= WRAM and top <= 0xdfff then
|
|
region, region_start = "wram", WRAM
|
|
elseif top >= HRAM and top <= 0xfffe then
|
|
region, region_start = "hram", HRAM
|
|
else
|
|
error("stack_address must be in WRAM or HRAM")
|
|
end
|
|
local bottom = top - size
|
|
assert(bottom >= region_start, "stack does not fit in its RAM region")
|
|
return size, bottom, region
|
|
end
|
|
|
|
local function allocate_ram(name, size, addr)
|
|
assert(type(name) == "string", "RAM symbol name must be a string")
|
|
assert(type(size) == "number" and size >= 1 and size % 1 == 0,
|
|
"RAM allocation size must be a positive integer")
|
|
assert(rawget(_ENV, name) == nil and cpu.symbols[name] == nil,
|
|
"duplicate symbol: " .. name)
|
|
cpu.symbols[name] = addr
|
|
rawset(_ENV, name, addr)
|
|
return addr
|
|
end
|
|
|
|
gb.wram_next = WRAM
|
|
function gb.wram(name, size, alignment)
|
|
size = size or 1
|
|
alignment = alignment or 1
|
|
assert(type(alignment) == "number" and alignment >= 1
|
|
and alignment % 1 == 0, "WRAM alignment must be a positive integer")
|
|
local stack_bytes, stack_bottom, stack_region = stack_config()
|
|
local addr = math.floor((gb.wram_next + alignment - 1) / alignment) * alignment
|
|
local limit = stack_bytes > 0 and stack_region == "wram"
|
|
and stack_bottom - 1 or 0xdfff
|
|
assert(addr + size - 1 <= limit,
|
|
"WRAM allocation overlaps the reserved stack")
|
|
gb.wram_next = addr + size
|
|
return allocate_ram(name, size, addr)
|
|
end
|
|
|
|
gb.hram_next = HRAM
|
|
function gb.hram(name, size)
|
|
size = size or 1
|
|
local stack_bytes, stack_bottom, stack_region = stack_config()
|
|
local addr = gb.hram_next
|
|
local limit = stack_bytes > 0 and stack_region == "hram"
|
|
and stack_bottom - 1 or 0xfffe
|
|
assert(addr + size - 1 <= limit,
|
|
"HRAM allocation overlaps the reserved stack")
|
|
gb.hram_next = addr + size
|
|
return allocate_ram(name, size, addr)
|
|
end
|
|
|
|
cpu.before_link[#cpu.before_link + 1] = function()
|
|
stack_config()
|
|
end
|
|
|
|
gb.logo = {
|
|
0xce,0xed,0x66,0x66,0xcc,0x0d,0x00,0x0b,0x03,0x73,0x00,0x83,
|
|
0x00,0x0c,0x00,0x0d,0x00,0x08,0x11,0x1f,0x88,0x89,0x00,0x0e,
|
|
0xdc,0xcc,0x6e,0xe6,0xdd,0xdd,0xd9,0x99,0xbb,0xbb,0x67,0x63,
|
|
0x6e,0x0e,0xec,0xcc,0xdd,0xdc,0x99,0x9f,0xbb,0xb9,0x33,0x3e,
|
|
}
|
|
|
|
local fixed_string = function(s, n)
|
|
local bytes = { string.byte(s or "", 1, #(s or "")) }
|
|
assert(#bytes <= n, "string is too long for Game Boy ROM header")
|
|
for i = #bytes + 1, n do bytes[i] = 0 end
|
|
return bytes
|
|
end
|
|
|
|
gb.rom_size_codes = {
|
|
[2] = 0x00, [4] = 0x01, [8] = 0x02, [16] = 0x03,
|
|
[32] = 0x04, [64] = 0x05, [128] = 0x06, [256] = 0x07,
|
|
[512] = 0x08, [72] = 0x52, [80] = 0x53, [96] = 0x54,
|
|
}
|
|
|
|
gb.ram_size_codes = {
|
|
[0] = 0x00, [2] = 0x01, [8] = 0x02, [32] = 0x03,
|
|
[128] = 0x04, [64] = 0x05,
|
|
}
|
|
|
|
function gb.rom_size_code(banks)
|
|
local code = gb.rom_size_codes[banks]
|
|
assert(code, "unsupported Game Boy ROM bank count: " .. tostring(banks))
|
|
return code
|
|
end
|
|
|
|
function gb.ram_size_code(kib)
|
|
local code = gb.ram_size_codes[kib]
|
|
assert(code, "unsupported Game Boy RAM size in KiB: " .. tostring(kib))
|
|
return code
|
|
end
|
|
|
|
function gb.cartridge_type(t)
|
|
t = t or {}
|
|
local mapper = (t.mapper or t.mbc or "rom")
|
|
if type(mapper) == "string" then mapper = mapper:lower() end
|
|
local ram = t.ram or t.ram_banks or t.ram_kib or (t.ramsize and t.ramsize ~= 0)
|
|
local battery = not not t.battery
|
|
local timer = not not (t.timer or t.rtc)
|
|
local rumble = not not t.rumble
|
|
|
|
if mapper == "rom" or mapper == "none" or mapper == 0 then
|
|
if battery then return 0x09 end
|
|
if ram then return 0x08 end
|
|
return 0x00
|
|
elseif mapper == "mbc1" or mapper == 1 then
|
|
assert(ram or not battery, "MBC1 battery cartridge type requires RAM")
|
|
return ram and (battery and 0x03 or 0x02) or 0x01
|
|
elseif mapper == "mbc2" or mapper == 2 then
|
|
return battery and 0x06 or 0x05
|
|
elseif mapper == "mbc3" or mapper == 3 then
|
|
if timer then return ram and 0x10 or 0x0f end
|
|
return ram and (battery and 0x13 or 0x12) or 0x11
|
|
elseif mapper == "mbc5" or mapper == 5 then
|
|
if rumble then return ram and (battery and 0x1e or 0x1d) or 0x1c end
|
|
return ram and (battery and 0x1b or 0x1a) or 0x19
|
|
end
|
|
error("unsupported Game Boy mapper: " .. tostring(mapper))
|
|
end
|
|
|
|
gb.header = function(t)
|
|
t = t or {}
|
|
local entry = t.entry or "main"
|
|
location(rom0)
|
|
section{ "gb_entry", org = 0x0100 }
|
|
nop
|
|
jp !entry
|
|
|
|
section{ "gb_logo", org = 0x0104 }
|
|
dc.b !gb.logo
|
|
|
|
local cgb_only = t.cgb_only or t.gbc_only or t.color_only
|
|
local cgb = t.cgb or t.gbc or t.color or t.gameboy_color or cgb_only
|
|
local header = fixed_string(t.title or "LZ80", cgb and 15 or 16)
|
|
if cgb then
|
|
header[#header + 1] = cgb_only and 0xc0 or 0x80
|
|
end
|
|
local licensee = fixed_string(t.licensee or "00", 2)
|
|
header[#header + 1] = licensee[1]
|
|
header[#header + 1] = licensee[2]
|
|
header[#header + 1] = t.sgb or 0
|
|
header[#header + 1] = t.carttype or t.cartridge_type or gb.cartridge_type(t)
|
|
local ram_kib = t.ram_kib or (t.ram_banks and t.ram_banks * 8)
|
|
header[#header + 1] = t.romsize or (t.rom_banks and gb.rom_size_code(t.rom_banks)) or 0
|
|
header[#header + 1] = t.ramsize or (ram_kib and gb.ram_size_code(ram_kib)) or 0
|
|
header[#header + 1] = t.destination or 1
|
|
header[#header + 1] = t.old_licensee or 0x33
|
|
header[#header + 1] = t.version or 0
|
|
|
|
local checksum = 0
|
|
for _, v in ipairs(header) do checksum = (checksum - v - 1) & 0xff end
|
|
|
|
section{ "gb_header", org = 0x0134 }
|
|
dc.b !header -- header content
|
|
dc.b !checksum -- header checksum
|
|
dc.b 0, 0 -- ROM checksum, set by writebin
|
|
end
|
|
|
|
mappers = mappers or {}
|
|
|
|
local function mapper_options(t, mapper, default_rom_banks, max_rom_banks)
|
|
t = t or {}
|
|
t.mapper = mapper
|
|
if not t.rom_banks then t.rom_banks = default_rom_banks or 2 end
|
|
assert(t.rom_banks >= 2, "Game Boy ROM must have at least 2 banks")
|
|
assert(t.rom_banks <= max_rom_banks, mapper .. " ROM can have at most " .. max_rom_banks .. " banks")
|
|
return t
|
|
end
|
|
|
|
local function mapper_ram_kib(t)
|
|
return t.ram_kib or (t.ram_banks and t.ram_banks * 8) or 0
|
|
end
|
|
|
|
local function mapper_header(t)
|
|
gb.rom_banks(t.rom_banks)
|
|
gb.header(t)
|
|
mappers.header = t
|
|
end
|
|
|
|
mappers.ROM = function(t)
|
|
t = mapper_options(t, "rom", 2, 2)
|
|
assert(mapper_ram_kib(t) == 0, "ROM-only cartridges do not support external RAM")
|
|
mapper_header(t)
|
|
end
|
|
|
|
mappers.MBC1 = function(t)
|
|
t = mapper_options(t, "mbc1", 2, 128)
|
|
assert(mapper_ram_kib(t) <= 32, "MBC1 RAM size must be at most 32 KiB")
|
|
mapper_header(t)
|
|
end
|
|
|
|
mappers.MBC2 = function(t)
|
|
t = mapper_options(t, "mbc2", 2, 16)
|
|
assert(mapper_ram_kib(t) == 0, "MBC2 uses built-in RAM; do not set external RAM")
|
|
mapper_header(t)
|
|
end
|
|
|
|
mappers.MBC3 = function(t)
|
|
t = mapper_options(t, "mbc3", 2, 128)
|
|
assert(mapper_ram_kib(t) <= 32, "MBC3 RAM size must be at most 32 KiB")
|
|
if t.rtc then t.timer = true end
|
|
if t.timer then t.battery = true end
|
|
mapper_header(t)
|
|
end
|
|
|
|
mappers.MBC5 = function(t)
|
|
t = mapper_options(t, "mbc5", 2, 512)
|
|
assert(mapper_ram_kib(t) <= 128, "MBC5 RAM size must be at most 128 KiB")
|
|
assert(not t.rumble or mapper_ram_kib(t) <= 64,
|
|
"MBC5 rumble cartridges can address at most 64 KiB of RAM")
|
|
mapper_header(t)
|
|
end
|
|
|
|
mappers.rom = mappers.ROM
|
|
mappers.mbc1 = mappers.MBC1
|
|
mappers.mbc2 = mappers.MBC2
|
|
mappers.mbc3 = mappers.MBC3
|
|
mappers.mbc5 = mappers.MBC5
|
|
|
|
function gb.fix_global_checksum(bin)
|
|
assert(bin and #bin >= 0x150, "Game Boy ROM is too small for a header")
|
|
local checksum = 0
|
|
for i = 1, 0x14e do
|
|
checksum = (checksum + bin[i]) & 0xffff
|
|
end
|
|
for i = 0x151, #bin do
|
|
checksum = (checksum + bin[i]) & 0xffff
|
|
end
|
|
bin[0x14f] = (checksum >> 8) & 0xff
|
|
bin[0x150] = checksum & 0xff
|
|
return checksum
|
|
end
|
|
|
|
cpu.bin_filler = 0xff
|
|
cpu.bin_finalizer = gb.fix_global_checksum
|
|
|
|
function ldh_a(addr) ldh a,!addr end
|
|
function ldh_addr(addr) ldh !addr,a end
|
|
|
|
-- Dispatch A through the word table immediately following the calling RST.
|
|
-- The caller chooses the section and RST vector where this code is emitted.
|
|
function gb.rst_dispatch()
|
|
add a,a
|
|
ld e,a ld d,#0
|
|
pop hl add hl,de
|
|
ld e,(hl) inc hl ld d,(hl)
|
|
ld h,d ld l,e
|
|
jp (hl)
|
|
end
|
|
|
|
-- Emit a crash handler, intended for the RST 0x38 vector reached by 0xFF padding.
|
|
function gb.crash_handler()
|
|
di
|
|
local loop = label()
|
|
halt
|
|
jr !loop
|
|
end
|
|
|
|
-- Reset the APU control registers to zero. Wave RAM is left untouched.
|
|
-- The APU must be enabled for these writes to take effect. Clobbers AF, B, HL.
|
|
function gb.sound_reset()
|
|
ld hl,#!NR10
|
|
ld b,#!(NR52 - NR10)
|
|
xor a
|
|
local loop = label()
|
|
ldi (hl),a
|
|
dec b
|
|
jr nz,!loop
|
|
end
|
|
|
|
-- Power on the APU, then initialize its control registers to a silent state.
|
|
function gb.sound_enable()
|
|
ld a,#!AUDENA_ON
|
|
ldh !NR52,a
|
|
gb.sound_reset()
|
|
end
|
|
|
|
-- Power off the APU. This also clears its control registers in hardware.
|
|
function gb.sound_disable()
|
|
xor a
|
|
ldh !NR52,a
|
|
end
|
|
|
|
-- Wait for the beginning of VBlank by polling LY. Clobbers AF.
|
|
function gb.vblank_waitbegin()
|
|
local loop = label()
|
|
a := [LY]
|
|
cp #!LY_VBLANK
|
|
jr.c !loop
|
|
end
|
|
|
|
-- Wait until VBlank has ended by polling LY. Clobbers AF.
|
|
function gb.vblank_waitend()
|
|
local loop = label()
|
|
a := [LY]
|
|
cp #!LY_VBLANK
|
|
jr.nc !loop
|
|
end
|
|
|
|
-- Wait for a complete mode 3, then return at the beginning of an HBlank.
|
|
-- This gives the caller the full following mode 0 VRAM/OAM access window.
|
|
-- Clobbers AF.
|
|
function gb.hblank_waitbegin()
|
|
local mode3 = label()
|
|
a := [STAT]
|
|
bit B_STAT_BUSY,a
|
|
jr.z !mode3
|
|
|
|
local hblank = label()
|
|
a := [STAT]
|
|
bit B_STAT_BUSY,a
|
|
jr.nz !hblank
|
|
end
|
|
|
|
-- Disable the LCD during VBlank, as required by the hardware. If it is
|
|
-- already disabled, return immediately. Clobbers AF.
|
|
function gb.lcd_disable()
|
|
local done = "lcd_disable_done_" .. id()
|
|
a := [LCDC]
|
|
bit B_LCDC_ENABLE,a
|
|
jr.z !done
|
|
|
|
vblank_waitbegin()
|
|
xor a
|
|
[LCDC] := a
|
|
|
|
label(done)
|
|
end
|
|
|
|
-- Enable the LCD with the supplied LCDC value (LCDC_DEFAULT by default).
|
|
-- Clobbers AF.
|
|
function gb.lcd_enable(value)
|
|
a := !(value or LCDC_DEFAULT)
|
|
[LCDC] := a
|
|
end
|
|
|
|
local function assert_cgb_header()
|
|
local header = mappers.header
|
|
assert(header and (header.cgb or header.gbc or header.color
|
|
or header.gameboy_color or header.cgb_only or header.gbc_only
|
|
or header.color_only),
|
|
"CGB hardware access requires a CGB cartridge header")
|
|
end
|
|
|
|
local function cgb_boot_b_value()
|
|
local header = mappers.header
|
|
assert(header, "hardware detection requires a mapper declaration")
|
|
local cgb_only = header.cgb_only or header.gbc_only or header.color_only
|
|
local cgb = header.cgb or header.gbc or header.color
|
|
or header.gameboy_color or cgb_only
|
|
if cgb then return 0 end
|
|
|
|
local old_licensee = header.old_licensee or 0x33
|
|
local licensee = header.licensee or "00"
|
|
local nintendo = old_licensee == 0x01
|
|
or (old_licensee == 0x33 and licensee == "01")
|
|
if not nintendo then return 0 end
|
|
|
|
local sum = 0
|
|
for _, byte in ipairs(fixed_string(header.title or "LZ80", 16)) do
|
|
sum = (sum + byte) & 0xff
|
|
end
|
|
return sum
|
|
end
|
|
|
|
-- Detect the hardware from the boot ROM's untouched A and B registers.
|
|
-- This must be emitted at the cartridge entry point before either register is
|
|
-- changed. Returns HARDWARE_DMG, HARDWARE_CGB, or HARDWARE_GBA in A; DMG also
|
|
-- covers Pocket and Super Game Boy because boot registers cannot distinguish
|
|
-- them reliably. Clobbers AF; preserves B and all other registers.
|
|
function gb.hardware_detect()
|
|
local dmg = "hardware_detect_dmg_" .. id()
|
|
local gba = "hardware_detect_gba_" .. id()
|
|
local done = "hardware_detect_done_" .. id()
|
|
local gba_b = (cgb_boot_b_value() + 1) & 0xff
|
|
|
|
cp #!BOOTUP_A_CGB
|
|
jr.nz !dmg
|
|
a := b
|
|
a ^= gba_b
|
|
jr.z !gba
|
|
a := HARDWARE_CGB
|
|
jr !done
|
|
|
|
label(gba)
|
|
a := HARDWARE_GBA
|
|
jr !done
|
|
|
|
label(dmg)
|
|
a := HARDWARE_DMG
|
|
|
|
label(done)
|
|
end
|
|
|
|
gb.detect_hardware = gb.hardware_detect
|
|
gb.hardware_type = gb.hardware_detect
|
|
|
|
-- Switch between CGB normal and double speed. With no argument, always
|
|
-- toggle. Pass true to ensure double speed or false to ensure normal speed.
|
|
-- Preserves AF, IE, and JOYP. A dual-mode ROM must first detect CGB hardware.
|
|
function gb.speed_switch(double_speed)
|
|
assert_cgb_header()
|
|
local done = "speed_switch_done_" .. id()
|
|
|
|
push af
|
|
if double_speed ~= nil then
|
|
a := [KEY1]
|
|
bit B_SPD_DOUBLE,a
|
|
if double_speed then jr.nz !done else jr.z !done end
|
|
end
|
|
|
|
a := [IE]
|
|
push af
|
|
a := [JOYP]
|
|
push af
|
|
|
|
xor a
|
|
[IE] := a
|
|
a := JOYP_GET_NONE
|
|
[JOYP] := a
|
|
a := KEY1_PREPARE
|
|
[KEY1] := a
|
|
stop
|
|
|
|
pop af
|
|
[JOYP] := a
|
|
pop af
|
|
[IE] := a
|
|
|
|
label(done)
|
|
pop af
|
|
end
|
|
|
|
gb.speed_toggle = gb.speed_switch
|
|
gb.cgb_speed_switch = gb.speed_switch
|
|
|
|
-- Enable the CGB infrared receiver with its emitter off. The caller must
|
|
-- runtime-detect actual CGB hardware in a dual-mode ROM; GBA has no IR port.
|
|
-- Clobbers A; preserves F.
|
|
function gb.infrared_enable()
|
|
assert_cgb_header()
|
|
a := RP_ENABLE
|
|
[RP] := a
|
|
end
|
|
|
|
-- Reset RP after sending or receiving to reduce power consumption. Clobbers
|
|
-- AF. Call infrared_enable() again before polling the receiver.
|
|
function gb.infrared_disable()
|
|
assert_cgb_header()
|
|
xor a
|
|
[RP] := a
|
|
end
|
|
|
|
-- Turn the CGB infrared LED on by default, or turn it off with false while
|
|
-- leaving the receiver enabled. Clobbers A; preserves F.
|
|
function gb.infrared_emit(enabled)
|
|
assert_cgb_header()
|
|
local control = RP_ENABLE
|
|
| (enabled == false and RP_WRITE_LOW or RP_WRITE_HIGH)
|
|
a := control
|
|
[RP] := a
|
|
end
|
|
|
|
-- Read RP into A and test its active-low receiver bit. Z means an IR signal
|
|
-- is being received; NZ means idle. The receiver must already be enabled.
|
|
function gb.infrared_poll()
|
|
assert_cgb_header()
|
|
a := [RP]
|
|
bit B_RP_DATA_IN,a
|
|
end
|
|
|
|
gb.ir_enable = gb.infrared_enable
|
|
gb.ir_disable = gb.infrared_disable
|
|
gb.ir_emit = gb.infrared_emit
|
|
gb.ir_poll = gb.infrared_poll
|
|
gb.infrared_read = gb.infrared_poll
|
|
|
|
-- Return the current divider value in A. Preserves F.
|
|
function gb.divider_read()
|
|
a := [DIV]
|
|
end
|
|
|
|
-- Reset the divider. Any value written to DIV resets it, so AF is preserved.
|
|
function gb.divider_reset()
|
|
[DIV] := a
|
|
end
|
|
|
|
-- Return TIMA in A. Preserves F.
|
|
function gb.timer_read()
|
|
a := [TIMA]
|
|
end
|
|
|
|
-- Set TMA and TIMA. counter defaults to modulo. Clobbers A; preserves F.
|
|
function gb.timer_reload(modulo, counter)
|
|
modulo = modulo or 0
|
|
counter = counter == nil and modulo or counter
|
|
a := modulo
|
|
[TMA] := a
|
|
a := counter
|
|
[TIMA] := a
|
|
end
|
|
|
|
-- Disable TIMA while preserving the selected TAC frequency. Clobbers AF.
|
|
function gb.timer_stop()
|
|
a := [TAC]
|
|
a &= TAC_CLOCK
|
|
[TAC] := a
|
|
end
|
|
|
|
-- Enable TIMA with the current TAC frequency. Clobbers AF.
|
|
function gb.timer_resume()
|
|
a := [TAC]
|
|
a |= TAC_START
|
|
[TAC] := a
|
|
end
|
|
|
|
-- Configure and start TIMA. frequency is one of TACF_*_HZ; modulo defaults
|
|
-- to zero and counter defaults to modulo. Clobbers AF.
|
|
function gb.timer_start(frequency, modulo, counter)
|
|
frequency = frequency or TACF_4096_HZ
|
|
assert(type(frequency) == "number" and frequency % 1 == 0
|
|
and frequency >= 0 and frequency <= 3,
|
|
"timer frequency must be a TACF_*_HZ value")
|
|
timer_stop()
|
|
timer_reload(modulo, counter)
|
|
a := !(frequency | TAC_START)
|
|
[TAC] := a
|
|
end
|
|
|
|
-- Start a transfer using the byte already in SB. Internal clock is the
|
|
-- default; pass false for external clock. fast=true requires CGB mode.
|
|
-- Clobbers A; preserves F.
|
|
function gb.serial_start(internal_clock, fast)
|
|
if fast then
|
|
assert(internal_clock ~= false,
|
|
"fast serial mode requires the internal clock")
|
|
assert_cgb_header()
|
|
end
|
|
local control = SC_START
|
|
| (internal_clock == false and SC_EXTERNAL or SC_INTERNAL)
|
|
| (fast and SC_FAST or SC_SLOW)
|
|
a := !control
|
|
[SC] := a
|
|
end
|
|
|
|
-- Store A in SB and start a simultaneous send/receive transfer. Clobbers A;
|
|
-- preserves F. Arguments match serial_start().
|
|
function gb.serial_transfer(internal_clock, fast)
|
|
[SB] := a
|
|
serial_start(internal_clock, fast)
|
|
end
|
|
|
|
gb.serial_send = gb.serial_transfer
|
|
|
|
-- Return SC in A and test its transfer bit. NZ means busy; Z means complete.
|
|
function gb.serial_poll()
|
|
a := [SC]
|
|
bit B_SC_START,a
|
|
end
|
|
|
|
-- Poll until a serial transfer completes, then return the received byte in A.
|
|
-- External-clock transfers can wait forever; use an application timeout when
|
|
-- the peer may be absent. Clobbers AF.
|
|
function gb.serial_wait()
|
|
local loop = label()
|
|
serial_poll()
|
|
jr.nz !loop
|
|
a := [SB]
|
|
end
|
|
|
|
-- Cancel a requested/in-progress transfer by clearing SC bit 7. Clobbers AF.
|
|
function gb.serial_cancel()
|
|
a := [SC]
|
|
a &= 0xff ~ SC_START
|
|
[SC] := a
|
|
end
|
|
|
|
-- Clear tile data and both tile maps in VRAM bank 0. The LCD must be off.
|
|
-- Clobbers AF, BC, HL.
|
|
function gb.vram_clear()
|
|
fill(VRAM, 0, VRAM_SIZE)
|
|
end
|
|
|
|
-- Clear all 40 hardware OAM entries. The LCD must be off, or OAM must be
|
|
-- accessible. Clobbers AF, BC, HL.
|
|
function gb.oam_clear()
|
|
fill(OAM, 0, OAM_SIZE)
|
|
end
|
|
|
|
-- Emit reset-entry initialization inline. This is not a callable routine: it
|
|
-- sets SP and deliberately leaves the LCD, interrupts, and sound disabled.
|
|
-- It clears VRAM bank 0 and OAM, resets scrolling/window coordinates, and sets
|
|
-- identity DMG palettes. It does not clear WRAM or HRAM.
|
|
function gb.init()
|
|
di
|
|
sp := stack_address
|
|
|
|
xor a
|
|
[IE] := a
|
|
[IF] := a
|
|
|
|
lcd_disable()
|
|
sound_disable()
|
|
vram_clear()
|
|
oam_clear()
|
|
|
|
xor a
|
|
[SCX] := a
|
|
[SCY] := a
|
|
[WX] := a
|
|
[WY] := a
|
|
|
|
a := DMG_PALETTE_IDENTITY
|
|
[BGP] := a
|
|
[OBP0] := a
|
|
[OBP1] := a
|
|
|
|
a := JOYP_GET_NONE
|
|
[JOYP] := a
|
|
end
|
|
|
|
-- Read both joypad rows and return pressed buttons as 1 bits in A, using the
|
|
-- conventional hardware.inc PAD_* order (directions high, buttons low).
|
|
-- The optional destination may be in HRAM or WRAM;
|
|
-- operator syntax selects LDH or LD after delayed resolution. Clobbers F, B.
|
|
function gb.read_joy(destination)
|
|
a := JOYP_GET_CTRL_PAD
|
|
[JOYP] := a
|
|
a := [JOYP]
|
|
a := [JOYP]
|
|
cpl
|
|
a &= JOYP_INPUTS
|
|
b := a
|
|
|
|
a := JOYP_GET_BUTTONS
|
|
[JOYP] := a
|
|
a := [JOYP]
|
|
a := [JOYP]
|
|
cpl
|
|
a &= JOYP_INPUTS
|
|
swap a
|
|
a |= b
|
|
b := a
|
|
|
|
a := JOYP_GET_NONE
|
|
[JOYP] := a
|
|
a := b
|
|
swap a
|
|
if destination ~= nil then [destination] := a end
|
|
end
|
|
|
|
-- Convert a sprite description to the four bytes stored by GB OAM: Y, X,
|
|
-- tile, flags. x and y are screen coordinates; set raw=true to supply the
|
|
-- hardware coordinates directly. If flags is omitted, the named flag fields
|
|
-- and CGB palette are combined.
|
|
function gb.oam_bytes(t)
|
|
assert(type(t) == "table", "OAM entry must be a table")
|
|
local function enabled(value) return value == true or value == 1 end
|
|
local x = t.x or 0
|
|
local y = t.y or 0
|
|
if not t.raw then x, y = x + 8, y + 16 end
|
|
local flags = t.flags
|
|
if flags == nil then
|
|
flags = ((enabled(t.behind) or enabled(t.priority)) and OAMF_PRIORITY or 0)
|
|
| ((enabled(t.flipy) or enabled(t.yflip)) and OAMF_YFLIP or 0)
|
|
| ((enabled(t.flipx) or enabled(t.xflip)) and OAMF_XFLIP or 0)
|
|
| (enabled(t.palette) and OAMF_PALETTE or 0)
|
|
| (enabled(t.bank1) and OAMF_BANK1 or 0)
|
|
| ((t.cgb_palette or 0) & 0x07)
|
|
end
|
|
return { y & 0xff, x & 0xff, (t.tile or 0) & 0xff, flags & 0xff }
|
|
end
|
|
|
|
local function oam_entry_address(base, t)
|
|
local index = assert(t.index or t[1],
|
|
"OAM entry requires index 1.." .. OAM_COUNT)
|
|
assert(type(index) == "number" and index % 1 == 0
|
|
and index >= 1 and index <= OAM_COUNT,
|
|
"OAM index must be in 1.." .. OAM_COUNT)
|
|
return base + (index - 1) * OBJ_SIZE
|
|
end
|
|
|
|
local function emit_oam_entry(base, t)
|
|
local bytes = oam_bytes(t)
|
|
hl := !oam_entry_address(base, t)
|
|
for i = 1, 4 do
|
|
a := !bytes[i]
|
|
[hl+] := a
|
|
end
|
|
end
|
|
|
|
-- Write one entry directly to hardware OAM. The LCD must be off, or OAM must
|
|
-- be accessible. Clobbers AF, HL.
|
|
function gb.oam_set(t)
|
|
emit_oam_entry(OAM, t)
|
|
end
|
|
|
|
local function oamcache_address()
|
|
local address = rawget(_ENV, "oamcache") or cpu.symbols.oamcache
|
|
if address == nil then
|
|
address = wram("oamcache", OAM_SIZE, 0x100)
|
|
end
|
|
assert(type(address) == "number" and address >= WRAM
|
|
and address + OAM_SIZE - 1 <= 0xdfff, "oamcache must fit in WRAM")
|
|
assert(address & 0xff == 0, "oamcache must be 0x100-byte aligned")
|
|
rawset(_ENV, "oamcache", address)
|
|
cpu.symbols.oamcache = address
|
|
return address
|
|
end
|
|
|
|
-- Clear the lazy 160-byte WRAM OAM cache. Clobbers AF, BC, HL.
|
|
function gb.oamcache_clear()
|
|
hl := !oamcache_address()
|
|
bc := OAM_SIZE
|
|
xor a
|
|
memset()
|
|
end
|
|
|
|
-- Write one entry to the lazy WRAM OAM cache. Clobbers AF, HL.
|
|
function gb.oamcache_set(t)
|
|
emit_oam_entry(oamcache_address(), t)
|
|
end
|
|
|
|
gb.oam_dma_bytes = { 0xe0, 0x46, 0x3e, OAM_COUNT, 0x3d, 0x20, 0xfd, 0xc9 }
|
|
|
|
local function oam_dma_address_value()
|
|
local address = rawget(_ENV, "oam_dma_address")
|
|
or cpu.symbols.oam_dma_address
|
|
if address == nil then
|
|
address = hram("oam_dma_address", #gb.oam_dma_bytes)
|
|
end
|
|
assert(type(address) == "number" and address >= HRAM
|
|
and address + #gb.oam_dma_bytes - 1 <= 0xfffe,
|
|
"oam_dma_address must fit in HRAM")
|
|
rawset(_ENV, "oam_dma_address", address)
|
|
cpu.symbols.oam_dma_address = address
|
|
return address
|
|
end
|
|
|
|
-- Copy the OAM DMA routine to HRAM. Emit this once during initialization,
|
|
-- before oamcache_flush is called. Falls through and clobbers AF, BC, DE, HL.
|
|
function gb.oam_dma_install()
|
|
local source = "oam_dma_source_" .. id()
|
|
local done = "oam_dma_install_done_" .. id()
|
|
hl := !oam_dma_address_value()
|
|
de := !source
|
|
bc := !#gb.oam_dma_bytes
|
|
memcpy()
|
|
jr !done
|
|
|
|
label(source)
|
|
dc.b !gb.oam_dma_bytes
|
|
label(done)
|
|
end
|
|
|
|
-- DMA the lazy WRAM OAM cache to hardware OAM through the installed HRAM
|
|
-- routine. Call during VBlank with interrupts disabled. Clobbers AF.
|
|
function gb.oamcache_flush()
|
|
local source = oamcache_address()
|
|
a := !(source >> 8)
|
|
call !oam_dma_address_value()
|
|
end
|
|
|
|
-- Copy BC bytes from DE to HL. Clobbers AF and BC; advances HL and DE.
|
|
-- A count of zero wraps and copies 65536 bytes.
|
|
-- Falls through without RET, so it can be used inline or followed by RET in
|
|
-- a section chosen by the caller.
|
|
function gb.memcpy()
|
|
bc--
|
|
b++
|
|
c++
|
|
|
|
local loop = label()
|
|
a := [de]
|
|
de++
|
|
[hl+] := a
|
|
c--
|
|
jr.nz !loop
|
|
b--
|
|
jr.nz !loop
|
|
end
|
|
|
|
-- Copy BC bytes from DE to HL, treating a zero count as a no-op.
|
|
-- Clobbers AF always; clobbers BC for a non-zero count; advances HL and DE.
|
|
-- Falls through without RET.
|
|
function gb.memcpy0()
|
|
local done = "memcpy0_done_" .. id()
|
|
|
|
a := b
|
|
a |= c
|
|
jr.z !done
|
|
|
|
memcpy()
|
|
|
|
label(done)
|
|
end
|
|
|
|
-- Copy C bytes from DE to HL. C=0 means 256. Clobbers AF and C; advances HL
|
|
-- and DE. Falls through without RET.
|
|
function gb.memcpy_c()
|
|
local loop = label()
|
|
a := [de]
|
|
de++
|
|
[hl+] := a
|
|
c--
|
|
jr.nz !loop
|
|
end
|
|
|
|
-- Copy exactly 16 bytes from DE to HL. Clobbers A; advances HL and DE.
|
|
-- Falls through without RET.
|
|
function gb.memcpy16()
|
|
for i = 1, 16 do
|
|
a := [de]
|
|
de++
|
|
[hl+] := a
|
|
end
|
|
end
|
|
|
|
-- Fill BC bytes at HL with A. Preserves A; clobbers F and BC; advances HL.
|
|
-- A count of zero wraps and writes 65536 bytes.
|
|
-- Falls through without RET, so it can be used inline or followed by RET in
|
|
-- a section chosen by the caller.
|
|
function gb.memset()
|
|
bc--
|
|
b++
|
|
c++
|
|
|
|
local loop = label()
|
|
[hl+] := a
|
|
c--
|
|
jr.nz !loop
|
|
b--
|
|
jr.nz !loop
|
|
end
|
|
|
|
-- Fill BC bytes at HL with A, treating a zero count as a no-op.
|
|
-- Preserves A. Clobbers F always; clobbers BC for a non-zero count;
|
|
-- advances HL for a non-zero count. Falls through without RET.
|
|
function gb.memset0()
|
|
local loop = "memset0_loop_" .. id()
|
|
local done = "memset0_done_" .. id()
|
|
|
|
bc--
|
|
b++
|
|
c++
|
|
|
|
-- If C is nonzero after the transform, original BC was nonzero.
|
|
jr.nz !loop
|
|
|
|
-- C is zero: either original BC was zero, or original count was a multiple of 256.
|
|
-- Transformed B is zero only for original BC = 0.
|
|
b++
|
|
b--
|
|
jr.z !done
|
|
|
|
label(loop)
|
|
[hl+] := a
|
|
c--
|
|
jr.nz !loop
|
|
b--
|
|
jr.nz !loop
|
|
|
|
label(done)
|
|
end
|
|
|
|
-- Fill C bytes at HL with A. C=0 means 256. Preserves A; clobbers F and C;
|
|
-- advances HL. Falls through without RET.
|
|
function gb.memset_c()
|
|
local loop = label()
|
|
[hl+] := a
|
|
c--
|
|
jr.nz !loop
|
|
end
|
|
|
|
-- Fill exactly 16 bytes at HL with A. Preserves AF; advances HL.
|
|
-- Falls through without RET.
|
|
function gb.memset16()
|
|
for i = 1, 16 do
|
|
[hl+] := a
|
|
end
|
|
end
|
|
|
|
-- Pseudorandom number generator
|
|
--
|
|
-- Copyright 2018, 2020 Damian Yerrick
|
|
--
|
|
-- This software is provided 'as-is', without any express or implied
|
|
-- warranty. In no event will the authors be held liable for any damages
|
|
-- arising from the use of this software.
|
|
--
|
|
-- Permission is granted to anyone to use this software for any purpose,
|
|
-- including commercial applications, and to alter it and redistribute it
|
|
-- freely, subject to the following restrictions:
|
|
--
|
|
-- 1. The origin of this software must not be misrepresented; you must not
|
|
-- claim that you wrote the original software. If you use this software
|
|
-- in a product, an acknowledgment in the product documentation would be
|
|
-- appreciated but is not required.
|
|
-- 2. Altered source versions must be plainly marked as such, and must not be
|
|
-- misrepresented as being the original software.
|
|
-- 3. This notice may not be removed or altered from any source distribution.
|
|
|
|
local randstate_address
|
|
local function rand_state()
|
|
if randstate_address == nil then
|
|
randstate_address = rawget(_ENV, "randstate")
|
|
or cpu.symbols.randstate
|
|
or wram("randstate", 4)
|
|
end
|
|
return randstate_address
|
|
end
|
|
|
|
-- The formula is:
|
|
-- x[i + 1] = (x[i] + 0xb3) * 0x01010101
|
|
-- or equivalently:
|
|
-- x[i + 1] = x[i] * 0x01010101 + 0xb3b3b3b3.
|
|
-- Before cc65 commit 3994fee595, the additive constant was 0x31415927.
|
|
|
|
-- Generate a pseudorandom 16-bit integer in BC using the current cc65 rand()
|
|
-- LCG. Returns A=B=state bits 31-24 and C=state bits 23-16; clobbers H and L.
|
|
-- The four-byte state defaults to an automatically allocated WRAM symbol named
|
|
-- randstate. Set randstate before emitting this routine to relocate it.
|
|
-- Emits RET, so place it in a caller-chosen section and invoke it with CALL.
|
|
function gb.rand()
|
|
local state = rand_state()
|
|
|
|
hl := state
|
|
a := [hl]
|
|
a += 0xb3
|
|
[hl+] := a
|
|
adc a,(hl)
|
|
[hl+] := a
|
|
adc a,(hl)
|
|
[hl+] := a
|
|
c := a
|
|
adc a,(hl)
|
|
[hl] := a
|
|
b := a
|
|
ret
|
|
end
|
|
|
|
-- Seed rand() from BC. C is the low byte and B is the high byte; startup code
|
|
-- should behave as though srand(1) had been called. Clobbers A, H, and L.
|
|
-- Emits RET, so place it in a caller-chosen section and invoke it with CALL.
|
|
function gb.srand()
|
|
local state = rand_state()
|
|
|
|
hl := state + 3
|
|
a ^= a
|
|
[hl-] := a
|
|
[hl-] := a
|
|
a := b
|
|
[hl-] := a
|
|
[hl] := c
|
|
ret
|
|
end
|
|
|
|
-- PB8 decompression for Game Boy
|
|
-- Copyright 2019 Damian Yerrick
|
|
-- The decoder below is adapted from the version used by gb-starter-kit. It
|
|
-- emits the original byte stream; unlike the crash-handler specialization,
|
|
-- it does not duplicate each decoded 1bpp byte into both Game Boy bitplanes.
|
|
-- The zlib license notice is retained in pb.lua.
|
|
--
|
|
-- A clear control bit loads a literal and a set bit repeats the previous byte.
|
|
|
|
-- Unpack one PB8 packet from DE to HL, producing eight bytes. A holds the
|
|
-- previous byte and must initially be zero; it is preserved between packets.
|
|
-- Clobbers F and B; advances DE and HL. Falls through without RET.
|
|
function gb.pb8_unpack_packet()
|
|
local loop = "pb8_packet_loop_" .. id()
|
|
local repeat_byte = "pb8_repeat_byte_" .. id()
|
|
|
|
push af
|
|
a := [de]
|
|
de++
|
|
b := a
|
|
pop af
|
|
scf
|
|
rl b
|
|
|
|
label(loop)
|
|
jr.c !repeat_byte
|
|
a := [de]
|
|
de++
|
|
|
|
label(repeat_byte)
|
|
[hl+] := a
|
|
sla b
|
|
jr.nz !loop
|
|
end
|
|
|
|
-- Unpack C PB8 packets from DE to HL, producing 8*C bytes. C=0 means 256
|
|
-- packets, or 2048 bytes. The packet routine defaults to pb8_unpack_packet.
|
|
-- Clobbers AF, BC, DE, and HL. Falls through without RET.
|
|
function gb.pb8_unpack_block(packet)
|
|
local loop = "pb8_block_loop_" .. id()
|
|
packet = packet or "pb8_unpack_packet"
|
|
|
|
xor a
|
|
|
|
label(loop)
|
|
call !packet
|
|
c--
|
|
jr.nz !loop
|
|
end
|
|
|
|
local pb8_runtime
|
|
|
|
-- Emit callable pb8_unpack_packet and pb8_unpack_block sections at the
|
|
-- caller's current ROM location. Nothing is emitted until this is called.
|
|
function gb.pb8_unpacker()
|
|
if pb8_runtime then
|
|
return pb8_runtime.packet, pb8_runtime.block
|
|
end
|
|
|
|
local previous = cpu.section_current
|
|
assert(previous, "pb8_unpacker requires a current section")
|
|
|
|
local packet = section("pb8_unpack_packet")
|
|
gb.pb8_unpack_packet()
|
|
ret
|
|
|
|
local block = section("pb8_unpack_block")
|
|
gb.pb8_unpack_block(packet)
|
|
ret
|
|
|
|
section(previous)
|
|
pb8_runtime = { packet = packet, block = block }
|
|
return packet, block
|
|
end
|
|
|
|
-- PB16 decompression for Game Boy
|
|
-- Copyright 2018 Damian Yerrick
|
|
-- Modified by Eldred Habert (ISSOtm), then ported to lz80. This is an altered
|
|
-- source version. The zlib license notice is retained in pb.lua.
|
|
--
|
|
-- The PB16 format encodes two interleaved byte streams. A clear control bit
|
|
-- loads a literal and a set bit repeats the byte from two positions earlier.
|
|
|
|
local pb16_byte0_address
|
|
local function pb16_temp_byte()
|
|
if pb16_byte0_address == nil then
|
|
pb16_byte0_address = rawget(_ENV, "pb16_byte0")
|
|
or cpu.symbols.pb16_byte0
|
|
or hram("pb16_byte0")
|
|
end
|
|
return pb16_byte0_address
|
|
end
|
|
|
|
-- Unpack one PB16 packet from DE to HL, producing eight bytes. The previous
|
|
-- even byte is stored in pb16_byte0 (allocated in HRAM by default); C holds
|
|
-- the previous odd byte. Clobbers AF and B; advances DE and HL. Falls through.
|
|
function gb.pb16_unpack_packet()
|
|
local temp = pb16_temp_byte()
|
|
local loop = "pb16_packet_loop_" .. id()
|
|
local p0_literal = "pb16_p0_literal_" .. id()
|
|
local have_p0 = "pb16_have_p0_" .. id()
|
|
local have_p1 = "pb16_have_p1_" .. id()
|
|
|
|
a := [de]
|
|
de++
|
|
scf
|
|
rla
|
|
b := a
|
|
|
|
label(loop)
|
|
jr.nc !p0_literal
|
|
a := [temp]
|
|
jr !have_p0
|
|
|
|
label(p0_literal)
|
|
a := [de]
|
|
de++
|
|
[temp] := a
|
|
|
|
label(have_p0)
|
|
[hl+] := a
|
|
|
|
a := c
|
|
sla b
|
|
jr.c !have_p1
|
|
a := [de]
|
|
de++
|
|
c := a
|
|
|
|
label(have_p1)
|
|
[hl+] := a
|
|
|
|
sla b
|
|
jr.nz !loop
|
|
end
|
|
|
|
-- Unpack 2*B PB16 packets from DE to HL, producing 16*B bytes. B=0 means
|
|
-- 4096 bytes. The packet routine defaults to the pb16_unpack_packet symbol.
|
|
-- Clobbers AF, BC, DE, and HL. Falls through.
|
|
function gb.pb16_unpack_block(packet)
|
|
local temp = pb16_temp_byte()
|
|
local loop = "pb16_block_loop_" .. id()
|
|
packet = packet or "pb16_unpack_packet"
|
|
|
|
xor a
|
|
[temp] := a
|
|
c := a
|
|
|
|
label(loop)
|
|
push bc
|
|
call !packet
|
|
call !packet
|
|
a := c
|
|
pop bc
|
|
c := a
|
|
b--
|
|
jr.nz !loop
|
|
end
|
|
|
|
local pb16_runtime
|
|
|
|
-- Emit callable pb16_unpack_packet and pb16_unpack_block sections at the
|
|
-- caller's current ROM location. Nothing is emitted until this is called.
|
|
function gb.pb16_unpacker()
|
|
if pb16_runtime then
|
|
return pb16_runtime.packet, pb16_runtime.block
|
|
end
|
|
|
|
local previous = cpu.section_current
|
|
assert(previous, "pb16_unpacker requires a current section")
|
|
|
|
local packet = section("pb16_unpack_packet")
|
|
gb.pb16_unpack_packet()
|
|
ret
|
|
|
|
local block = section("pb16_unpack_block")
|
|
gb.pb16_unpack_block(packet)
|
|
ret
|
|
|
|
section(previous)
|
|
pb16_runtime = { packet = packet, block = block }
|
|
return packet, block
|
|
end
|
|
|
|
-- Client scripts may set emit_runtime_memops to select runtime calls for all
|
|
-- BC-based memory operations, then override individual choices with
|
|
-- emit_memcpy, emit_memcpy0, emit_memset, or emit_memset0. Options may be set
|
|
-- before or after require'gb', as long as they precede the relevant helper.
|
|
-- With no option set, auto mode keeps the operation inline.
|
|
function gb.should_call_memop(name)
|
|
local option = "emit_" .. name
|
|
local specific = rawget(_ENV, option)
|
|
if specific == nil then specific = rawget(gb, option) end
|
|
if specific ~= nil then
|
|
assert(type(specific) == "boolean", option .. " must be boolean")
|
|
return specific
|
|
end
|
|
|
|
local generic = rawget(_ENV, "emit_runtime_memops")
|
|
if generic == nil then generic = rawget(gb, "emit_runtime_memops") end
|
|
if generic ~= nil then
|
|
assert(type(generic) == "boolean",
|
|
"emit_runtime_memops must be boolean")
|
|
end
|
|
return generic == true
|
|
end
|
|
|
|
local emitted_memops = {}
|
|
local function ensure_memop(name, emit)
|
|
local symbol = "gb_" .. name
|
|
if emitted_memops[name] or cpu.symbols[symbol] ~= nil then
|
|
emitted_memops[name] = true
|
|
return symbol
|
|
end
|
|
|
|
local previous_section = cpu.section_current
|
|
assert(previous_section, "runtime memory operation requires a current section")
|
|
emitted_memops[name] = true
|
|
|
|
location(rom0)
|
|
section(symbol)
|
|
emit()
|
|
ret
|
|
section(previous_section)
|
|
return symbol
|
|
end
|
|
|
|
|
|
function gb.ensure_memcpy()
|
|
return ensure_memop("memcpy", gb.memcpy)
|
|
end
|
|
|
|
function gb.ensure_memcpy0()
|
|
return ensure_memop("memcpy0", gb.memcpy0)
|
|
end
|
|
|
|
function gb.ensure_memset()
|
|
return ensure_memop("memset", gb.memset)
|
|
end
|
|
|
|
function gb.ensure_memset0()
|
|
return ensure_memop("memset0", gb.memset0)
|
|
end
|
|
|
|
local function memop_mode(opt)
|
|
local mode = opt or "auto"
|
|
assert(mode == "auto" or mode == "inline" or mode == "call",
|
|
"memory operation mode must be 'auto', 'inline', or 'call'")
|
|
return mode
|
|
end
|
|
|
|
local function emit_bc_memop(name, mode)
|
|
if mode == "call" or (mode == "auto" and gb.should_call_memop(name)) then
|
|
local ensure = assert(gb["ensure_" .. name])
|
|
local symbol = ensure()
|
|
call !symbol
|
|
else
|
|
gb[name]()
|
|
end
|
|
end
|
|
|
|
-- Resolve a transfer size early when possible, without requiring link-time
|
|
-- symbols to be available yet. An unresolved size selects the safe zero-aware
|
|
-- BC implementation below.
|
|
local function transfer_size_value(size)
|
|
assert(size ~= nil, "transfer size is required")
|
|
|
|
local ok, value
|
|
if type(size) == "function" then
|
|
ok, value = pcall(size, 0, op_resolve)
|
|
else
|
|
ok, value = pcall(op_resolve, size)
|
|
end
|
|
if not ok or type(value) ~= "number" then return nil end
|
|
|
|
assert(value >= 0 and value <= 0xffff and value % 1 == 0,
|
|
"transfer size must be an integer in 0..65535")
|
|
return value
|
|
end
|
|
|
|
-- Copy size bytes from source to destination. HL is loaded with destination
|
|
-- and DE with source. A resolved size selects the most specific specialized
|
|
-- implementation; an unresolved size uses memcpy0 so zero remains a no-op.
|
|
-- opt is "auto" by default; "inline" and "call" force that behavior for the
|
|
-- BC variants. The C and 16-byte variants are always inline.
|
|
-- Clobbers AF, BC, DE, and HL as required by the selected implementation.
|
|
function gb.copy(source, destination, size, opt)
|
|
assert(source ~= nil, "copy source is required")
|
|
assert(destination ~= nil, "copy destination is required")
|
|
local resolved_size = transfer_size_value(size)
|
|
local mode = memop_mode(opt)
|
|
|
|
hl := destination
|
|
de := source
|
|
|
|
if resolved_size == 16 then
|
|
memcpy16()
|
|
elseif resolved_size ~= nil and resolved_size > 0
|
|
and resolved_size <= 256 then
|
|
local size8 = resolved_size & 0xff
|
|
c := size8
|
|
memcpy_c()
|
|
elseif resolved_size ~= nil and resolved_size > 0 then
|
|
bc := resolved_size
|
|
emit_bc_memop("memcpy", mode)
|
|
else
|
|
bc := !size
|
|
emit_bc_memop("memcpy0", mode)
|
|
end
|
|
end
|
|
|
|
-- Fill size bytes at destination with value. HL is loaded with destination
|
|
-- and A with value. A resolved size selects the most specific specialized
|
|
-- implementation; an unresolved size uses memset0 so zero remains a no-op.
|
|
-- opt is "auto" by default; "inline" and "call" force that behavior for the
|
|
-- BC variants. The C and 16-byte variants are always inline.
|
|
-- Clobbers AF, BC, and HL as required by the selected implementation.
|
|
function gb.fill(destination, value, size, opt)
|
|
assert(destination ~= nil, "fill destination is required")
|
|
assert(value ~= nil, "fill value is required")
|
|
local resolved_size = transfer_size_value(size)
|
|
local mode = memop_mode(opt)
|
|
|
|
hl := destination
|
|
a := value
|
|
|
|
if resolved_size == 16 then
|
|
memset16()
|
|
elseif resolved_size ~= nil and resolved_size > 0
|
|
and resolved_size <= 256 then
|
|
local size8 = resolved_size & 0xff
|
|
c := size8
|
|
memset_c()
|
|
elseif resolved_size ~= nil and resolved_size > 0 then
|
|
bc := resolved_size
|
|
emit_bc_memop("memset", mode)
|
|
else
|
|
bc := !size
|
|
emit_bc_memop("memset0", mode)
|
|
end
|
|
end
|
|
|
|
local function vblank_wait_flag()
|
|
if waiting_for_vblank == nil then
|
|
waiting_for_vblank = rawget(_ENV, "hWaitingForVBlank")
|
|
or cpu.symbols.hWaitingForVBlank
|
|
or hram("hWaitingForVBlank")
|
|
end
|
|
return waiting_for_vblank
|
|
end
|
|
|
|
-- Emit the body of a callable wait-for-next-VBlank routine.
|
|
-- IE must contain the interrupts to process; IME is restored on return.
|
|
-- Set waiting_for_vblank before emitting this routine and the ISR to choose
|
|
-- its address. It defaults to hWaitingForVBlank in HRAM. Operator assignments
|
|
-- select LDH for an address in 0xff00-0xffff, or LD for any other address.
|
|
-- Example: waiting_for_vblank = wram("wWaitingForVBlank")
|
|
function gb.wait_vblank()
|
|
local flag = vblank_wait_flag()
|
|
di
|
|
ld a,#1
|
|
[flag] := a
|
|
|
|
local loop = label()
|
|
a := [flag]
|
|
ana a
|
|
local done = "wait_vblank_done_" .. id()
|
|
jr z,!done
|
|
|
|
halt
|
|
nop
|
|
ei
|
|
nop
|
|
di
|
|
jr !loop
|
|
|
|
label(done)
|
|
ei
|
|
ret
|
|
end
|
|
|
|
-- Install the VBlank vector and a relocatable ISR in ROM0.
|
|
-- callback is emitted after clearing hWaitingForVBlank and before pop AF.
|
|
function gb.vblank_isr(callback)
|
|
assert(callback == nil or type(callback) == "function",
|
|
"VBlank ISR callback must be a function")
|
|
local flag = vblank_wait_flag()
|
|
local previous_section = cpu.section_current
|
|
|
|
location(rom0)
|
|
local handler = section{ "vblank_isr" }
|
|
section{ "vblank_vector", org = INT_HANDLER_VBLANK }
|
|
jp !handler.label
|
|
|
|
section(handler)
|
|
push af
|
|
xor a
|
|
[flag] := a
|
|
if callback then callback() end
|
|
pop af
|
|
reti
|
|
|
|
section(previous_section)
|
|
end
|
|
|
|
function gb.ram_enable(enabled)
|
|
ld a,#!(enabled == false and RAMG_SRAM_DISABLE or RAMG_SRAM_ENABLE)
|
|
ld !gb.MBC_RAM_ENABLE,a
|
|
end
|
|
|
|
function gb.ram_disable()
|
|
gb.ram_enable(false)
|
|
end
|
|
|
|
gb.mbc1_ram_enable = gb.ram_enable
|
|
gb.mbc2_ram_enable = gb.ram_enable
|
|
gb.mbc3_ram_enable = gb.ram_enable
|
|
gb.mbc5_ram_enable = gb.ram_enable
|
|
|
|
function gb.mbc1_select_rom_bank(bank)
|
|
if type(bank) == "number" then
|
|
bank = bank & 0x1f
|
|
if bank == 0 then bank = 1 end
|
|
end
|
|
ld a,#!bank
|
|
ld !gb.MBC_ROM_BANK,a
|
|
end
|
|
|
|
function gb.mbc1_select_ram_bank(bank)
|
|
ld a,#!bank
|
|
ld !gb.MBC_RAM_BANK,a
|
|
end
|
|
|
|
function gb.mbc1_select_upper_rom_bits(bits)
|
|
gb.mbc1_select_ram_bank(bits)
|
|
end
|
|
|
|
function gb.mbc1_select_mode(mode)
|
|
ld a,#!mode
|
|
ld !gb.MBC_MODE,a
|
|
end
|
|
|
|
function gb.mbc2_select_rom_bank(bank)
|
|
if type(bank) == "number" then
|
|
bank = bank & 0x0f
|
|
if bank == 0 then bank = 1 end
|
|
end
|
|
ld a,#!bank
|
|
ld !gb.ROM2B,a
|
|
end
|
|
|
|
function gb.mbc3_select_rom_bank(bank)
|
|
if type(bank) == "number" then
|
|
bank = bank & 0x7f
|
|
if bank == 0 then bank = 1 end
|
|
end
|
|
ld a,#!bank
|
|
ld !gb.MBC_ROM_BANK,a
|
|
end
|
|
|
|
function gb.mbc3_select_ram_bank(bank)
|
|
ld a,#!bank
|
|
ld !gb.MBC_RAM_BANK,a
|
|
end
|
|
|
|
function gb.mbc3_select_rtc_register(reg)
|
|
gb.mbc3_select_ram_bank(reg)
|
|
end
|
|
|
|
function gb.mbc3_latch_clock()
|
|
xor a
|
|
ld !gb.MBC_RTC_LATCH,a
|
|
inc a
|
|
ld !gb.MBC_RTC_LATCH,a
|
|
end
|
|
|
|
local function rtc_register(register)
|
|
local header = mappers.header
|
|
assert(header and header.mapper == "mbc3" and (header.timer or header.rtc),
|
|
"RTC requires an MBC3 cartridge declared with rtc=true")
|
|
assert(type(register) == "number" and register % 1 == 0
|
|
and register >= RTC_SECONDS and register <= RTC_DAY_HIGH,
|
|
"RTC register must be RTC_SECONDS through RTC_DAY_HIGH")
|
|
return register
|
|
end
|
|
|
|
-- Latch a stable snapshot of the running MBC3 clock. RAM/RTC access must have
|
|
-- been enabled by the caller with ram_enable(). Clobbers AF.
|
|
function gb.rtc_latch()
|
|
rtc_register(RTC_SECONDS)
|
|
gb.mbc3_latch_clock()
|
|
end
|
|
|
|
-- Select an RTC register and return its value in A. Call rtc_latch() first
|
|
-- when reading multiple fields from one coherent snapshot. Preserves F.
|
|
function gb.rtc_read(register)
|
|
gb.mbc3_select_rtc_register(rtc_register(register))
|
|
a := [SRAM]
|
|
end
|
|
|
|
-- Write A to an RTC register while preserving AF. Halt the clock before
|
|
-- changing counter registers, as required by the hardware.
|
|
function gb.rtc_write(register)
|
|
push af
|
|
gb.mbc3_select_rtc_register(rtc_register(register))
|
|
pop af
|
|
[SRAM] := a
|
|
end
|
|
|
|
-- Latch and copy seconds, minutes, hours, day-low, and day-high to [HL+].
|
|
-- RAM/RTC access must already be enabled. Clobbers AF and advances HL by 5.
|
|
function gb.rtc_read_all()
|
|
rtc_latch()
|
|
for register = RTC_SECONDS, RTC_DAY_HIGH do
|
|
rtc_read(register)
|
|
[hl+] := a
|
|
end
|
|
end
|
|
|
|
local function rtc_access_delay()
|
|
for i = 1, 4 do nop end
|
|
end
|
|
|
|
local function rtc_update_day_high(clear_mask, set_mask)
|
|
rtc_latch()
|
|
rtc_read(RTC_DAY_HIGH)
|
|
a &= clear_mask
|
|
if set_mask ~= 0 then a |= set_mask end
|
|
rtc_access_delay()
|
|
[SRAM] := a
|
|
end
|
|
|
|
-- Stop the RTC while preserving the day counter and carry flag. Clobbers AF.
|
|
function gb.rtc_halt()
|
|
rtc_update_day_high(0xff, RTCF_HALT)
|
|
end
|
|
|
|
-- Resume the RTC while preserving the day counter and carry flag. Clobbers AF.
|
|
function gb.rtc_resume()
|
|
rtc_update_day_high(0xff ~ RTCF_HALT, 0)
|
|
end
|
|
|
|
-- Clear the sticky day-counter overflow flag without changing halt/day bit 8.
|
|
-- The clock must already be halted. Clobbers AF.
|
|
function gb.rtc_clear_carry()
|
|
rtc_update_day_high(0xff ~ RTCF_CARRY, 0)
|
|
end
|
|
|
|
function gb.mbc5_select_rom_bank(bank)
|
|
if type(bank) == "number" then
|
|
local lo = bank & 0xff
|
|
local hi = (bank >> 8) & 0x01
|
|
ld a,#!lo
|
|
ld !gb.MBC_ROM_BANK,a
|
|
ld a,#!hi
|
|
ld !gb.MBC5_ROM_BANK_HIGH,a
|
|
else
|
|
ld a,#!bank
|
|
ld !gb.MBC_ROM_BANK,a
|
|
xor a
|
|
ld !gb.MBC5_ROM_BANK_HIGH,a
|
|
end
|
|
end
|
|
|
|
local mbc5_ram_bank = 0
|
|
local mbc5_rumble_enabled = false
|
|
|
|
local function mbc5_write_ram_control()
|
|
local header = mappers.header
|
|
local rumble_cart = header and header.mapper == "mbc5" and header.rumble
|
|
if rumble_cart and type(mbc5_ram_bank) == "number" then
|
|
local value = (mbc5_ram_bank & 0x07)
|
|
| (mbc5_rumble_enabled and MBC5_RUMBLE or 0)
|
|
a := value
|
|
else
|
|
a := mbc5_ram_bank
|
|
if rumble_cart then
|
|
a &= 0x07
|
|
if mbc5_rumble_enabled then a |= MBC5_RUMBLE end
|
|
end
|
|
end
|
|
[MBC_RAM_BANK] := a
|
|
end
|
|
|
|
function gb.mbc5_select_ram_bank(bank)
|
|
if type(bank) == "number" then
|
|
local max_bank = mappers.header and mappers.header.rumble and 7 or 15
|
|
assert(bank >= 0 and bank <= max_bank and bank % 1 == 0,
|
|
"MBC5 RAM bank must be an integer in 0.." .. max_bank)
|
|
end
|
|
mbc5_ram_bank = bank
|
|
mbc5_write_ram_control()
|
|
end
|
|
|
|
-- Enable cartridge rumble by default, or disable it with false. The selected
|
|
-- MBC5 RAM bank is preserved. The mapper declaration must use rumble=true.
|
|
function gb.rumble(enabled)
|
|
local header = mappers.header
|
|
assert(header and header.mapper == "mbc5" and header.rumble,
|
|
"rumble requires an MBC5 cartridge declared with rumble=true")
|
|
mbc5_rumble_enabled = enabled ~= false
|
|
mbc5_write_ram_control()
|
|
end
|
|
|
|
gb.mbc5_rumble = gb.rumble
|
|
|
|
-- Match the NES module API: helper functions are directly available in the
|
|
-- assembler environment. The gb table remains available to the module itself.
|
|
for name, value in pairs(gb) do
|
|
if type(value) == "function" then rawset(_ENV, name, value) end
|
|
end
|