@soapbox.pub/wasmboy
Version:
Soapbox fork of Wasmboy.
315 lines (270 loc) • 11.5 kB
text/typescript
import { getSaveStateMemoryOffset } from '../core';
import { Cpu } from '../cpu/index';
import {
eightBitLoadFromGBMemory,
eightBitStoreIntoGBMemory,
loadBooleanDirectlyFromWasmMemory,
storeBooleanDirectlyToWasmMemory
} from '../memory/index';
import { requestTimerInterrupt } from '../interrupts/index';
import { checkBitOnByte } from '../helpers/index';
export class Timers {
// Current cycles
// This will be used for batch processing
static currentCycles: i32 = 0;
// Number of cycles to run in each batch process
static batchProcessCycles(): i32 {
return 256;
}
// Divider Register = DIV
// Divider Register is 16 bits.
// Divider Register when read is just the upper 8 bits
// But internally is used as the full 16
// Essentially dividerRegister is an always counting clock
// DIV Drives everything, it is the heart of the timer.
// All other timing registers base them selves relative to the DIV register
// Think of the div register as like a cycle counter :)
// DIV will increment TIMA, whenever there is a falling edge, see below for that.
static readonly memoryLocationDividerRegister: i32 = 0xff04; // DIV
static dividerRegister: i32 = 0;
static updateDividerRegister(): void {
let oldDividerRegister = Timers.dividerRegister;
Timers.dividerRegister = 0;
eightBitStoreIntoGBMemory(Timers.memoryLocationDividerRegister, 0);
if (Timers.timerEnabled && _checkDividerRegisterFallingEdgeDetector(oldDividerRegister, 0)) {
_incrementTimerCounter();
}
}
// timerCounter = TIMA
// TIMA is the actual counter.
// Whenever the DIV gets the falling edge, and other obscure cases,
// This is incremented. When this overflows, we need to fire an interrupt.
static readonly memoryLocationTimerCounter: i32 = 0xff05;
static timerCounter: i32 = 0;
static timerCounterOverflowDelay: boolean = false;
static timerCounterWasReset: boolean = false;
static timerCounterMask: i32 = 0;
static updateTimerCounter(value: i32): void {
if (Timers.timerEnabled) {
// From binjgb, dont write TIMA if we were just reset
if (Timers.timerCounterWasReset) {
return;
}
// Mooneye Test, tima_write_reloading
// Writing in this strange delay cycle, will cancel
// Both the interrupt and the TMA reload
if (Timers.timerCounterOverflowDelay) {
Timers.timerCounterOverflowDelay = false;
}
}
Timers.timerCounter = value;
}
// Timer Modulo = TMA
// TMA is what TIMA (Notice the I :p) is counting from, and TIMA will load
// Whenever TIMA overflow.
// For instance, we count like 1,2,3,4,5,6,7,8,9, and then overflow to 10.
// TMA would be like "Hey, start counting from 5 whenever we reset"
// Then we would be like 5,6,7,8,9...5,6,7,8,9...etc...
static readonly memoryLocationTimerModulo: i32 = 0xff06;
static timerModulo: i32 = 0;
static updateTimerModulo(value: i32): void {
Timers.timerModulo = value;
// Mooneye Test, tma_write_reloading
// Don't update if we were reloading
if (Timers.timerEnabled && Timers.timerCounterWasReset) {
Timers.timerCounter = value;
Timers.timerCounterWasReset = false;
}
}
// Timer Control = TAC
// TAC Says how fast we are counting.
// TAC controls which bit we are watching for the falling edge on the DIV register
// And whenever the bit has the falling edge, we increment TIMA (The thing counting).
// Therefore, depending on the value, we will either count faster or slower.
static readonly memoryLocationTimerControl: i32 = 0xff07;
// Bit 2 - Timer Stop (0=Stop, 1=Start)
// Bits 1-0 - Input Clock Select
// 00: 4096 Hz (~4194 Hz SGB) (1024 cycles)
// 01: 262144 Hz (~268400 Hz SGB) (16 cycles)
// 10: 65536 Hz (~67110 Hz SGB) (64 cycles)
// 11: 16384 Hz (~16780 Hz SGB) (256 cycles)
static timerEnabled: boolean = false;
static timerInputClock: i32 = 0;
static updateTimerControl(value: i32): void {
// Get some initial values
let oldTimerEnabled = Timers.timerEnabled;
Timers.timerEnabled = checkBitOnByte(2, value);
let newTimerInputClock = value & 0x03;
// Do some obscure behavior for if we should increment TIMA
// This does the timer increments from rapid_toggle mooneye tests
if (!oldTimerEnabled) {
let oldTimerCounterMaskBit = _getTimerCounterMaskBit(Timers.timerInputClock);
let newTimerCounterMaskBit = _getTimerCounterMaskBit(newTimerInputClock);
let shouldIncrementTimerCounter = false;
let dividerRegister = Timers.dividerRegister;
if (Timers.timerEnabled) {
shouldIncrementTimerCounter = checkBitOnByte(oldTimerCounterMaskBit, dividerRegister);
} else {
shouldIncrementTimerCounter =
checkBitOnByte(oldTimerCounterMaskBit, dividerRegister) && checkBitOnByte(newTimerCounterMaskBit, dividerRegister);
}
if (shouldIncrementTimerCounter) {
_incrementTimerCounter();
}
}
Timers.timerInputClock = newTimerInputClock;
}
// Save States
static readonly saveStateSlot: i32 = 5;
// Function to save the state of the class
// TODO: Save state for new properties on Timers
static saveState(): void {
// Batch Processing
store<i32>(getSaveStateMemoryOffset(0x00, Timers.saveStateSlot), Timers.currentCycles);
// Divider Register
store<i32>(getSaveStateMemoryOffset(0x04, Timers.saveStateSlot), Timers.dividerRegister);
// Timer Counter
store<i32>(getSaveStateMemoryOffset(0x08, Timers.saveStateSlot), Timers.timerCounter);
storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x0c, Timers.saveStateSlot), Timers.timerCounterOverflowDelay);
storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x0d, Timers.saveStateSlot), Timers.timerCounterWasReset);
store<i32>(getSaveStateMemoryOffset(0x0e, Timers.saveStateSlot), Timers.timerCounterMask);
// Timer Modulo
store<i32>(getSaveStateMemoryOffset(0x12, Timers.saveStateSlot), Timers.timerModulo);
// Timer Control
storeBooleanDirectlyToWasmMemory(getSaveStateMemoryOffset(0x16, Timers.saveStateSlot), Timers.timerEnabled);
store<i32>(getSaveStateMemoryOffset(0x17, Timers.saveStateSlot), Timers.timerInputClock);
}
// Function to load the save state from memory
static loadState(): void {
// Batch Processing
Timers.currentCycles = load<i32>(getSaveStateMemoryOffset(0x00, Timers.saveStateSlot));
// Divider Register
Timers.dividerRegister = load<i32>(getSaveStateMemoryOffset(0x04, Timers.saveStateSlot));
// Timer Counter
Timers.timerCounter = load<i32>(getSaveStateMemoryOffset(0x08, Timers.saveStateSlot));
Timers.timerCounterOverflowDelay = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x0c, Timers.saveStateSlot));
Timers.timerCounterWasReset = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x0d, Timers.saveStateSlot));
Timers.timerCounterMask = load<i32>(getSaveStateMemoryOffset(0x0e, Timers.saveStateSlot));
// Timer Modulo
Timers.timerModulo = load<i32>(getSaveStateMemoryOffset(0x12, Timers.saveStateSlot));
// Timer Control
Timers.timerEnabled = loadBooleanDirectlyFromWasmMemory(getSaveStateMemoryOffset(0x16, Timers.saveStateSlot));
Timers.timerInputClock = load<i32>(getSaveStateMemoryOffset(0x17, Timers.saveStateSlot));
}
}
// Inlined because closure compiler inlines
export function initializeTimers(): void {
// Reset stateful Variables
Timers.currentCycles = 0;
Timers.dividerRegister = 0;
Timers.timerCounter = 0;
Timers.timerModulo = 0;
Timers.timerEnabled = false;
Timers.timerInputClock = 0;
Timers.timerCounterOverflowDelay = false;
Timers.timerCounterWasReset = false;
if (Cpu.GBCEnabled) {
// DIV
eightBitStoreIntoGBMemory(0xff04, 0x1e);
Timers.dividerRegister = 0x1ea0;
// 0xFF05 -> 0xFF06 = 0x00
// TAC
eightBitStoreIntoGBMemory(0xff07, 0xf8);
Timers.timerInputClock = 0xf8;
} else {
// DIV
eightBitStoreIntoGBMemory(0xff04, 0xab);
Timers.dividerRegister = 0xabcc;
// 0xFF05 -> 0xFF06 = 0x00
// TAC
eightBitStoreIntoGBMemory(0xff07, 0xf8);
Timers.timerInputClock = 0xf8;
}
// Override/reset some variables if the boot ROM is enabled
if (Cpu.BootROMEnabled) {
if (Cpu.GBCEnabled) {
// GBC
} else {
// GB
// DIV
eightBitStoreIntoGBMemory(0xff04, 0x00);
Timers.dividerRegister = 0x0004;
}
}
}
// Batch Process Timers
// Only checked on writes
// Function to batch process our Timers after we skipped so many cycles
export function batchProcessTimers(): void {
// TODO: Did a timer rewrite, make a proper batch processing
// For timers
updateTimers(Timers.currentCycles);
Timers.currentCycles = 0;
}
export function updateTimers(numberOfCycles: i32): void {
// Want to increment 4 cycles at a time like an actual GB would
let cyclesIncreased = 0;
while (cyclesIncreased < numberOfCycles) {
let oldDividerRegister = Timers.dividerRegister;
let curDividerRegister = oldDividerRegister;
cyclesIncreased += 4;
curDividerRegister += 4;
curDividerRegister &= 0xffff;
Timers.dividerRegister = curDividerRegister;
if (Timers.timerEnabled) {
let timerCounterWasReset = Timers.timerCounterWasReset;
if (Timers.timerCounterOverflowDelay) {
Timers.timerCounter = Timers.timerModulo;
// Fire off timer interrupt
requestTimerInterrupt();
Timers.timerCounterOverflowDelay = false;
Timers.timerCounterWasReset = true;
} else if (timerCounterWasReset) {
Timers.timerCounterWasReset = false;
}
if (_checkDividerRegisterFallingEdgeDetector(oldDividerRegister, curDividerRegister)) {
_incrementTimerCounter();
}
}
}
}
// Function to increment our Timer Counter
// This fires off interrupts once we overflow
function _incrementTimerCounter(): void {
var counter = Timers.timerCounter;
if (++counter > 255) {
// Whenever the timer overflows, there is a slight delay (4 cycles)
// Of when TIMA gets TMA's value, and the interrupt is fired.
// Thus we will set the delay, which can be handled in the update timer or write trap
Timers.timerCounterOverflowDelay = true;
counter = 0;
}
Timers.timerCounter = counter;
}
// Function to act as our falling edge detector
// Whenever we have a falling edge, we need to increment TIMA
// http://gbdev.gg8.se/wiki/articles/Timer_Obscure_Behaviour
// https://github.com/binji/binjgb/blob/master/src/emulator.c#L1944
function _checkDividerRegisterFallingEdgeDetector(oldDividerRegister: i32, newDividerRegister: i32): boolean {
// Get our mask
let timerCounterMaskBit = _getTimerCounterMaskBit(Timers.timerInputClock);
// If the old register's watched bit was zero,
// but after adding the new registers wastch bit is now 1
return checkBitOnByte(timerCounterMaskBit, oldDividerRegister) && !checkBitOnByte(timerCounterMaskBit, newDividerRegister);
}
// Function to get our current tima mask bit
// used for our falling edge detector
// See The docs linked above, or TCAGB for this bit mapping
function _getTimerCounterMaskBit(timerInputClock: i32): i32 {
switch (timerInputClock) {
case 0x00:
return 9;
case 0x01:
return 3;
case 0x02:
return 5;
case 0x03:
return 7;
}
return 0;
}