1
0
mirror of https://github.com/sehugg/8bitworkshop.git synced 2026-04-20 15:16:38 +00:00
Files
8bitworkshop/gen/machine/gb.js
T

1560 lines
58 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameBoyMachine = void 0;
const SM83_1 = require("../common/cpu/SM83");
const devices_1 = require("../common/devices");
const emu_1 = require("../common/emu");
const util_1 = require("../common/util");
// Game Boy DMG palette: 4 shades of green (darkest to lightest)
const DMG_PALETTE = [
0xFFd0d884, // Lightest (Warm Mint)
0xFF87a84c, // Light Mid
0xFF526b34, // Dark Mid
0xFF2d3122, // Darkest (Deep Olive)
];
var GB_KEYCODE_MAP = (0, emu_1.makeKeycodeMap)([
// D-pad
[emu_1.Keys.RIGHT, 0, 0x01],
[emu_1.Keys.LEFT, 0, 0x02],
[emu_1.Keys.UP, 0, 0x04],
[emu_1.Keys.DOWN, 0, 0x08],
// Buttons
[emu_1.Keys.A, 0, 0x10],
[emu_1.Keys.B, 0, 0x20],
[emu_1.Keys.GP_A, 0, 0x10],
[emu_1.Keys.GP_B, 0, 0x20],
[emu_1.Keys.SELECT, 0, 0x40],
[emu_1.Keys.START, 0, 0x80],
[emu_1.Keys.VK_ENTER, 0, 0x80], // Start
[emu_1.Keys.VK_SHIFT, 0, 0x40], // Select
]);
// Duty cycle waveforms for square wave channels (8 steps each)
const DUTY_TABLE = [
[0, 0, 0, 0, 0, 0, 0, 1], // 12.5%
[1, 0, 0, 0, 0, 0, 0, 1], // 25%
[1, 0, 0, 0, 0, 1, 1, 1], // 50%
[0, 1, 1, 1, 1, 1, 1, 0], // 75%
];
// Read masks for NR registers (bits that always read as 1)
const NR_READ_MASKS = new Uint8Array(0x30);
// Set up read masks per Pan Docs
(() => {
NR_READ_MASKS[0x10 - 0x10] = 0x80; // NR10
NR_READ_MASKS[0x11 - 0x10] = 0x3F; // NR11: duty readable, length not
NR_READ_MASKS[0x12 - 0x10] = 0x00; // NR12
NR_READ_MASKS[0x13 - 0x10] = 0xFF; // NR13: write-only
NR_READ_MASKS[0x14 - 0x10] = 0xBF; // NR14: bit 6 readable
NR_READ_MASKS[0x15 - 0x10] = 0xFF; // NR15: unused
NR_READ_MASKS[0x16 - 0x10] = 0x3F; // NR21
NR_READ_MASKS[0x17 - 0x10] = 0x00; // NR22
NR_READ_MASKS[0x18 - 0x10] = 0xFF; // NR23: write-only
NR_READ_MASKS[0x19 - 0x10] = 0xBF; // NR24
NR_READ_MASKS[0x1A - 0x10] = 0x7F; // NR30
NR_READ_MASKS[0x1B - 0x10] = 0xFF; // NR31: write-only
NR_READ_MASKS[0x1C - 0x10] = 0x9F; // NR32
NR_READ_MASKS[0x1D - 0x10] = 0xFF; // NR33: write-only
NR_READ_MASKS[0x1E - 0x10] = 0xBF; // NR34
NR_READ_MASKS[0x1F - 0x10] = 0xFF; // unused
NR_READ_MASKS[0x20 - 0x10] = 0xFF; // NR41: write-only
NR_READ_MASKS[0x21 - 0x10] = 0x00; // NR42
NR_READ_MASKS[0x22 - 0x10] = 0x00; // NR43
NR_READ_MASKS[0x23 - 0x10] = 0xBF; // NR44
NR_READ_MASKS[0x24 - 0x10] = 0x00; // NR50
NR_READ_MASKS[0x25 - 0x10] = 0x00; // NR51
NR_READ_MASKS[0x26 - 0x10] = 0x70; // NR52: bits 4-6 unused
})();
class GameBoyAPU {
constructor() {
// Master control
this.enabled = false;
this.nr50 = 0; // master volume / VIN
this.nr51 = 0; // panning
// Channel 1: Square with sweep
this.ch1_enabled = false;
this.ch1_dacEnabled = false;
this.ch1_duty = 0;
this.ch1_dutyPos = 0;
this.ch1_lengthCounter = 0;
this.ch1_lengthEnabled = false;
this.ch1_volume = 0;
this.ch1_envInitVol = 0;
this.ch1_envDir = 0; // 0=down, 1=up
this.ch1_envPeriod = 0;
this.ch1_envTimer = 0;
this.ch1_freq = 0;
this.ch1_freqTimer = 0;
this.ch1_sweepPeriod = 0;
this.ch1_sweepDir = 0; // 0=add, 1=subtract
this.ch1_sweepShift = 0;
this.ch1_sweepTimer = 0;
this.ch1_sweepEnabled = false;
this.ch1_sweepShadow = 0;
// Channel 2: Square (no sweep)
this.ch2_enabled = false;
this.ch2_dacEnabled = false;
this.ch2_duty = 0;
this.ch2_dutyPos = 0;
this.ch2_lengthCounter = 0;
this.ch2_lengthEnabled = false;
this.ch2_volume = 0;
this.ch2_envInitVol = 0;
this.ch2_envDir = 0;
this.ch2_envPeriod = 0;
this.ch2_envTimer = 0;
this.ch2_freq = 0;
this.ch2_freqTimer = 0;
// Channel 3: Wave
this.ch3_enabled = false;
this.ch3_dacEnabled = false;
this.ch3_lengthCounter = 0;
this.ch3_lengthEnabled = false;
this.ch3_volume = 0; // 0=mute, 1=100%, 2=50%, 3=25%
this.ch3_freq = 0;
this.ch3_freqTimer = 0;
this.ch3_samplePos = 0;
this.ch3_sampleBuffer = 0;
this.waveRAM = new Uint8Array(16);
// Channel 4: Noise
this.ch4_enabled = false;
this.ch4_dacEnabled = false;
this.ch4_lengthCounter = 0;
this.ch4_lengthEnabled = false;
this.ch4_volume = 0;
this.ch4_envInitVol = 0;
this.ch4_envDir = 0;
this.ch4_envPeriod = 0;
this.ch4_envTimer = 0;
this.ch4_clockShift = 0;
this.ch4_widthMode = 0; // 0=15-bit, 1=7-bit
this.ch4_divisor = 0;
this.ch4_freqTimer = 0;
this.ch4_lfsr = 0x7FFF;
// Frame sequencer
this.frameSeqTimer = 0;
this.frameSeqStep = 0;
// Raw register values for read-back
this.regs = new Uint8Array(0x30);
}
reset() {
this.enabled = false;
this.nr50 = 0;
this.nr51 = 0;
this.ch1_enabled = false;
this.ch1_dacEnabled = false;
this.ch1_duty = 0;
this.ch1_dutyPos = 0;
this.ch1_lengthCounter = 0;
this.ch1_lengthEnabled = false;
this.ch1_volume = 0;
this.ch1_envInitVol = 0;
this.ch1_envDir = 0;
this.ch1_envPeriod = 0;
this.ch1_envTimer = 0;
this.ch1_freq = 0;
this.ch1_freqTimer = 0;
this.ch1_sweepPeriod = 0;
this.ch1_sweepDir = 0;
this.ch1_sweepShift = 0;
this.ch1_sweepTimer = 0;
this.ch1_sweepEnabled = false;
this.ch1_sweepShadow = 0;
this.ch2_enabled = false;
this.ch2_dacEnabled = false;
this.ch2_duty = 0;
this.ch2_dutyPos = 0;
this.ch2_lengthCounter = 0;
this.ch2_lengthEnabled = false;
this.ch2_volume = 0;
this.ch2_envInitVol = 0;
this.ch2_envDir = 0;
this.ch2_envPeriod = 0;
this.ch2_envTimer = 0;
this.ch2_freq = 0;
this.ch2_freqTimer = 0;
this.ch3_enabled = false;
this.ch3_dacEnabled = false;
this.ch3_lengthCounter = 0;
this.ch3_lengthEnabled = false;
this.ch3_volume = 0;
this.ch3_freq = 0;
this.ch3_freqTimer = 0;
this.ch3_samplePos = 0;
this.ch3_sampleBuffer = 0;
this.waveRAM.fill(0);
this.ch4_enabled = false;
this.ch4_dacEnabled = false;
this.ch4_lengthCounter = 0;
this.ch4_lengthEnabled = false;
this.ch4_volume = 0;
this.ch4_envInitVol = 0;
this.ch4_envDir = 0;
this.ch4_envPeriod = 0;
this.ch4_envTimer = 0;
this.ch4_clockShift = 0;
this.ch4_widthMode = 0;
this.ch4_divisor = 0;
this.ch4_freqTimer = 0;
this.ch4_lfsr = 0x7FFF;
this.frameSeqTimer = 0;
this.frameSeqStep = 0;
this.regs.fill(0);
}
readRegister(addr) {
// addr is 0x10-0x3F range
if (addr >= 0x30 && addr <= 0x3F) {
// Wave RAM - if ch3 is reading, return the current sample byte
if (this.ch3_enabled) {
return this.waveRAM[this.ch3_samplePos >> 1];
}
return this.waveRAM[addr - 0x30];
}
if (addr === 0x26) {
// NR52: bit 7 = master enable, bits 0-3 = channel status
var val = (this.enabled ? 0x80 : 0) |
(this.ch1_enabled ? 0x01 : 0) |
(this.ch2_enabled ? 0x02 : 0) |
(this.ch3_enabled ? 0x04 : 0) |
(this.ch4_enabled ? 0x08 : 0);
return val | 0x70; // bits 4-6 always 1
}
if (!this.enabled)
return 0xFF;
var regIndex = addr - 0x10;
return this.regs[regIndex] | NR_READ_MASKS[regIndex];
}
writeRegister(addr, val) {
// Wave RAM is always writable
if (addr >= 0x30 && addr <= 0x3F) {
this.waveRAM[addr - 0x30] = val;
return;
}
// NR52 special handling
if (addr === 0x26) {
var wasEnabled = this.enabled;
this.enabled = !!(val & 0x80);
if (wasEnabled && !this.enabled) {
// Turning off: clear all registers
for (var i = 0x10; i <= 0x25; i++) {
this.writeRegister(i, 0);
}
this.ch1_enabled = false;
this.ch2_enabled = false;
this.ch3_enabled = false;
this.ch4_enabled = false;
}
if (!wasEnabled && this.enabled) {
this.frameSeqTimer = 0;
this.frameSeqStep = 0;
}
return;
}
if (!this.enabled)
return; // writes ignored when APU off (except NR52 and wave RAM)
var regIndex = addr - 0x10;
this.regs[regIndex] = val;
switch (addr) {
// Channel 1: NR10-NR14
case 0x10: // NR10: Sweep
this.ch1_sweepPeriod = (val >> 4) & 0x07;
this.ch1_sweepDir = (val >> 3) & 0x01;
this.ch1_sweepShift = val & 0x07;
break;
case 0x11: // NR11: Duty & length
this.ch1_duty = (val >> 6) & 0x03;
this.ch1_lengthCounter = 64 - (val & 0x3F);
break;
case 0x12: // NR12: Volume envelope
this.ch1_envInitVol = (val >> 4) & 0x0F;
this.ch1_envDir = (val >> 3) & 0x01;
this.ch1_envPeriod = val & 0x07;
this.ch1_dacEnabled = (val & 0xF8) !== 0;
if (!this.ch1_dacEnabled)
this.ch1_enabled = false;
break;
case 0x13: // NR13: Frequency low
this.ch1_freq = (this.ch1_freq & 0x700) | val;
break;
case 0x14: // NR14: Trigger & frequency high
this.ch1_freq = (this.ch1_freq & 0xFF) | ((val & 0x07) << 8);
this.ch1_lengthEnabled = !!(val & 0x40);
if (val & 0x80)
this.triggerChannel1();
break;
// Channel 2: NR21-NR24
case 0x16: // NR21: Duty & length
this.ch2_duty = (val >> 6) & 0x03;
this.ch2_lengthCounter = 64 - (val & 0x3F);
break;
case 0x17: // NR22: Volume envelope
this.ch2_envInitVol = (val >> 4) & 0x0F;
this.ch2_envDir = (val >> 3) & 0x01;
this.ch2_envPeriod = val & 0x07;
this.ch2_dacEnabled = (val & 0xF8) !== 0;
if (!this.ch2_dacEnabled)
this.ch2_enabled = false;
break;
case 0x18: // NR23: Frequency low
this.ch2_freq = (this.ch2_freq & 0x700) | val;
break;
case 0x19: // NR24: Trigger & frequency high
this.ch2_freq = (this.ch2_freq & 0xFF) | ((val & 0x07) << 8);
this.ch2_lengthEnabled = !!(val & 0x40);
if (val & 0x80)
this.triggerChannel2();
break;
// Channel 3: NR30-NR34
case 0x1A: // NR30: DAC enable
this.ch3_dacEnabled = !!(val & 0x80);
if (!this.ch3_dacEnabled)
this.ch3_enabled = false;
break;
case 0x1B: // NR31: Length
this.ch3_lengthCounter = 256 - val;
break;
case 0x1C: // NR32: Volume
this.ch3_volume = (val >> 5) & 0x03;
break;
case 0x1D: // NR33: Frequency low
this.ch3_freq = (this.ch3_freq & 0x700) | val;
break;
case 0x1E: // NR34: Trigger & frequency high
this.ch3_freq = (this.ch3_freq & 0xFF) | ((val & 0x07) << 8);
this.ch3_lengthEnabled = !!(val & 0x40);
if (val & 0x80)
this.triggerChannel3();
break;
// Channel 4: NR41-NR44
case 0x20: // NR41: Length
this.ch4_lengthCounter = 64 - (val & 0x3F);
break;
case 0x21: // NR42: Volume envelope
this.ch4_envInitVol = (val >> 4) & 0x0F;
this.ch4_envDir = (val >> 3) & 0x01;
this.ch4_envPeriod = val & 0x07;
this.ch4_dacEnabled = (val & 0xF8) !== 0;
if (!this.ch4_dacEnabled)
this.ch4_enabled = false;
break;
case 0x22: // NR43: Polynomial counter
this.ch4_clockShift = (val >> 4) & 0x0F;
this.ch4_widthMode = (val >> 3) & 0x01;
this.ch4_divisor = val & 0x07;
break;
case 0x23: // NR44: Trigger
this.ch4_lengthEnabled = !!(val & 0x40);
if (val & 0x80)
this.triggerChannel4();
break;
// Master controls
case 0x24: // NR50
this.nr50 = val;
break;
case 0x25: // NR51
this.nr51 = val;
break;
}
}
triggerChannel1() {
if (this.ch1_dacEnabled)
this.ch1_enabled = true;
if (this.ch1_lengthCounter === 0)
this.ch1_lengthCounter = 64;
this.ch1_freqTimer = (2048 - this.ch1_freq) * 4;
this.ch1_volume = this.ch1_envInitVol;
this.ch1_envTimer = this.ch1_envPeriod || 8;
// Sweep
this.ch1_sweepShadow = this.ch1_freq;
this.ch1_sweepTimer = this.ch1_sweepPeriod || 8;
this.ch1_sweepEnabled = this.ch1_sweepPeriod > 0 || this.ch1_sweepShift > 0;
if (this.ch1_sweepShift > 0) {
this.calcSweepFreq(); // overflow check
}
}
triggerChannel2() {
if (this.ch2_dacEnabled)
this.ch2_enabled = true;
if (this.ch2_lengthCounter === 0)
this.ch2_lengthCounter = 64;
this.ch2_freqTimer = (2048 - this.ch2_freq) * 4;
this.ch2_volume = this.ch2_envInitVol;
this.ch2_envTimer = this.ch2_envPeriod || 8;
}
triggerChannel3() {
if (this.ch3_dacEnabled)
this.ch3_enabled = true;
if (this.ch3_lengthCounter === 0)
this.ch3_lengthCounter = 256;
this.ch3_freqTimer = (2048 - this.ch3_freq) * 2;
this.ch3_samplePos = 0;
}
triggerChannel4() {
if (this.ch4_dacEnabled)
this.ch4_enabled = true;
if (this.ch4_lengthCounter === 0)
this.ch4_lengthCounter = 64;
var divisor = this.ch4_divisor === 0 ? 8 : this.ch4_divisor * 16;
this.ch4_freqTimer = divisor << this.ch4_clockShift;
this.ch4_volume = this.ch4_envInitVol;
this.ch4_envTimer = this.ch4_envPeriod || 8;
this.ch4_lfsr = 0x7FFF;
}
calcSweepFreq() {
var newFreq = this.ch1_sweepShadow >> this.ch1_sweepShift;
if (this.ch1_sweepDir) {
newFreq = this.ch1_sweepShadow - newFreq;
}
else {
newFreq = this.ch1_sweepShadow + newFreq;
}
if (newFreq > 2047) {
this.ch1_enabled = false;
}
return newFreq;
}
// Clock the APU by tCycles T-cycles
clock(tCycles) {
if (!this.enabled)
return;
for (var t = 0; t < tCycles; t++) {
// Frame sequencer: clocked at 512 Hz = every 8192 T-cycles
this.frameSeqTimer++;
if (this.frameSeqTimer >= 8192) {
this.frameSeqTimer = 0;
this.clockFrameSequencer();
}
// Channel 1 frequency timer
if (this.ch1_enabled) {
this.ch1_freqTimer--;
if (this.ch1_freqTimer <= 0) {
this.ch1_freqTimer = (2048 - this.ch1_freq) * 4;
this.ch1_dutyPos = (this.ch1_dutyPos + 1) & 7;
}
}
// Channel 2 frequency timer
if (this.ch2_enabled) {
this.ch2_freqTimer--;
if (this.ch2_freqTimer <= 0) {
this.ch2_freqTimer = (2048 - this.ch2_freq) * 4;
this.ch2_dutyPos = (this.ch2_dutyPos + 1) & 7;
}
}
// Channel 3 frequency timer (clocks at 2x rate)
if (this.ch3_enabled) {
this.ch3_freqTimer--;
if (this.ch3_freqTimer <= 0) {
this.ch3_freqTimer = (2048 - this.ch3_freq) * 2;
this.ch3_samplePos = (this.ch3_samplePos + 1) & 31;
// Read wave sample
var sampleByte = this.waveRAM[this.ch3_samplePos >> 1];
this.ch3_sampleBuffer = (this.ch3_samplePos & 1) ? (sampleByte & 0x0F) : (sampleByte >> 4);
}
}
// Channel 4 frequency timer
if (this.ch4_enabled) {
this.ch4_freqTimer--;
if (this.ch4_freqTimer <= 0) {
var divisor = this.ch4_divisor === 0 ? 8 : this.ch4_divisor * 16;
this.ch4_freqTimer = divisor << this.ch4_clockShift;
// Clock LFSR
var xor = (this.ch4_lfsr & 1) ^ ((this.ch4_lfsr >> 1) & 1);
this.ch4_lfsr = (this.ch4_lfsr >> 1) | (xor << 14);
if (this.ch4_widthMode) {
this.ch4_lfsr = (this.ch4_lfsr & ~(1 << 6)) | (xor << 6);
}
}
}
}
}
clockFrameSequencer() {
// Step 0,2,4,6 = length counter (256 Hz)
if ((this.frameSeqStep & 1) === 0) {
this.clockLengthCounters();
}
// Step 2,6 = sweep (128 Hz)
if (this.frameSeqStep === 2 || this.frameSeqStep === 6) {
this.clockSweep();
}
// Step 7 = volume envelope (64 Hz)
if (this.frameSeqStep === 7) {
this.clockEnvelopes();
}
this.frameSeqStep = (this.frameSeqStep + 1) & 7;
}
clockLengthCounters() {
if (this.ch1_lengthEnabled && this.ch1_lengthCounter > 0) {
this.ch1_lengthCounter--;
if (this.ch1_lengthCounter === 0)
this.ch1_enabled = false;
}
if (this.ch2_lengthEnabled && this.ch2_lengthCounter > 0) {
this.ch2_lengthCounter--;
if (this.ch2_lengthCounter === 0)
this.ch2_enabled = false;
}
if (this.ch3_lengthEnabled && this.ch3_lengthCounter > 0) {
this.ch3_lengthCounter--;
if (this.ch3_lengthCounter === 0)
this.ch3_enabled = false;
}
if (this.ch4_lengthEnabled && this.ch4_lengthCounter > 0) {
this.ch4_lengthCounter--;
if (this.ch4_lengthCounter === 0)
this.ch4_enabled = false;
}
}
clockSweep() {
if (!this.ch1_sweepEnabled)
return;
this.ch1_sweepTimer--;
if (this.ch1_sweepTimer <= 0) {
this.ch1_sweepTimer = this.ch1_sweepPeriod || 8;
if (this.ch1_sweepPeriod > 0) {
var newFreq = this.calcSweepFreq();
if (newFreq <= 2047 && this.ch1_sweepShift > 0) {
this.ch1_sweepShadow = newFreq;
this.ch1_freq = newFreq;
this.calcSweepFreq(); // overflow check with new freq
}
}
}
}
clockEnvelopes() {
// Channel 1
if (this.ch1_envPeriod > 0) {
this.ch1_envTimer--;
if (this.ch1_envTimer <= 0) {
this.ch1_envTimer = this.ch1_envPeriod || 8;
if (this.ch1_envDir && this.ch1_volume < 15) {
this.ch1_volume++;
}
else if (!this.ch1_envDir && this.ch1_volume > 0) {
this.ch1_volume--;
}
}
}
// Channel 2
if (this.ch2_envPeriod > 0) {
this.ch2_envTimer--;
if (this.ch2_envTimer <= 0) {
this.ch2_envTimer = this.ch2_envPeriod || 8;
if (this.ch2_envDir && this.ch2_volume < 15) {
this.ch2_volume++;
}
else if (!this.ch2_envDir && this.ch2_volume > 0) {
this.ch2_volume--;
}
}
}
// Channel 4
if (this.ch4_envPeriod > 0) {
this.ch4_envTimer--;
if (this.ch4_envTimer <= 0) {
this.ch4_envTimer = this.ch4_envPeriod || 8;
if (this.ch4_envDir && this.ch4_volume < 15) {
this.ch4_volume++;
}
else if (!this.ch4_envDir && this.ch4_volume > 0) {
this.ch4_volume--;
}
}
}
}
// Get current mixed sample as float in [-1, 1]
getSample() {
if (!this.enabled)
return 0;
// Get per-channel digital outputs (0-15 range)
var ch1out = 0, ch2out = 0, ch3out = 0, ch4out = 0;
if (this.ch1_enabled && this.ch1_dacEnabled) {
ch1out = DUTY_TABLE[this.ch1_duty][this.ch1_dutyPos] ? this.ch1_volume : 0;
}
if (this.ch2_enabled && this.ch2_dacEnabled) {
ch2out = DUTY_TABLE[this.ch2_duty][this.ch2_dutyPos] ? this.ch2_volume : 0;
}
if (this.ch3_enabled && this.ch3_dacEnabled) {
var waveSample = this.ch3_sampleBuffer;
switch (this.ch3_volume) {
case 0:
waveSample = 0;
break; // mute
case 1: break; // 100%
case 2:
waveSample >>= 1;
break; // 50%
case 3:
waveSample >>= 2;
break; // 25%
}
ch3out = waveSample;
}
if (this.ch4_enabled && this.ch4_dacEnabled) {
ch4out = (this.ch4_lfsr & 1) ? 0 : this.ch4_volume; // bit 0 inverted
}
// DAC conversion: digital 0-15 maps to analog -1..+1
// DAC output = (digital / 7.5) - 1
var dac1 = ch1out / 7.5 - 1;
var dac2 = ch2out / 7.5 - 1;
var dac3 = ch3out / 7.5 - 1;
var dac4 = ch4out / 7.5 - 1;
// Mix left and right with NR51 panning
var left = 0, right = 0;
if (this.nr51 & 0x10)
left += dac1;
if (this.nr51 & 0x20)
left += dac2;
if (this.nr51 & 0x40)
left += dac3;
if (this.nr51 & 0x80)
left += dac4;
if (this.nr51 & 0x01)
right += dac1;
if (this.nr51 & 0x02)
right += dac2;
if (this.nr51 & 0x04)
right += dac3;
if (this.nr51 & 0x08)
right += dac4;
// Master volume (NR50): 0-7 range for each side
var leftVol = ((this.nr50 >> 4) & 0x07) + 1;
var rightVol = (this.nr50 & 0x07) + 1;
left *= leftVol / 8;
right *= rightVol / 8;
// Mix to mono, scale down (4 channels max, each -1..+1)
return (left + right) / 8;
}
saveState() {
return {
enabled: this.enabled, nr50: this.nr50, nr51: this.nr51,
ch1_enabled: this.ch1_enabled, ch1_dacEnabled: this.ch1_dacEnabled,
ch1_duty: this.ch1_duty, ch1_dutyPos: this.ch1_dutyPos,
ch1_lengthCounter: this.ch1_lengthCounter, ch1_lengthEnabled: this.ch1_lengthEnabled,
ch1_volume: this.ch1_volume, ch1_envInitVol: this.ch1_envInitVol,
ch1_envDir: this.ch1_envDir, ch1_envPeriod: this.ch1_envPeriod, ch1_envTimer: this.ch1_envTimer,
ch1_freq: this.ch1_freq, ch1_freqTimer: this.ch1_freqTimer,
ch1_sweepPeriod: this.ch1_sweepPeriod, ch1_sweepDir: this.ch1_sweepDir,
ch1_sweepShift: this.ch1_sweepShift, ch1_sweepTimer: this.ch1_sweepTimer,
ch1_sweepEnabled: this.ch1_sweepEnabled, ch1_sweepShadow: this.ch1_sweepShadow,
ch2_enabled: this.ch2_enabled, ch2_dacEnabled: this.ch2_dacEnabled,
ch2_duty: this.ch2_duty, ch2_dutyPos: this.ch2_dutyPos,
ch2_lengthCounter: this.ch2_lengthCounter, ch2_lengthEnabled: this.ch2_lengthEnabled,
ch2_volume: this.ch2_volume, ch2_envInitVol: this.ch2_envInitVol,
ch2_envDir: this.ch2_envDir, ch2_envPeriod: this.ch2_envPeriod, ch2_envTimer: this.ch2_envTimer,
ch2_freq: this.ch2_freq, ch2_freqTimer: this.ch2_freqTimer,
ch3_enabled: this.ch3_enabled, ch3_dacEnabled: this.ch3_dacEnabled,
ch3_lengthCounter: this.ch3_lengthCounter, ch3_lengthEnabled: this.ch3_lengthEnabled,
ch3_volume: this.ch3_volume, ch3_freq: this.ch3_freq, ch3_freqTimer: this.ch3_freqTimer,
ch3_samplePos: this.ch3_samplePos, ch3_sampleBuffer: this.ch3_sampleBuffer,
waveRAM: this.waveRAM.slice(0),
ch4_enabled: this.ch4_enabled, ch4_dacEnabled: this.ch4_dacEnabled,
ch4_lengthCounter: this.ch4_lengthCounter, ch4_lengthEnabled: this.ch4_lengthEnabled,
ch4_volume: this.ch4_volume, ch4_envInitVol: this.ch4_envInitVol,
ch4_envDir: this.ch4_envDir, ch4_envPeriod: this.ch4_envPeriod, ch4_envTimer: this.ch4_envTimer,
ch4_clockShift: this.ch4_clockShift, ch4_widthMode: this.ch4_widthMode,
ch4_divisor: this.ch4_divisor, ch4_freqTimer: this.ch4_freqTimer, ch4_lfsr: this.ch4_lfsr,
frameSeqTimer: this.frameSeqTimer, frameSeqStep: this.frameSeqStep,
regs: this.regs.slice(0),
};
}
loadState(state) {
this.enabled = state.enabled;
this.nr50 = state.nr50;
this.nr51 = state.nr51;
this.ch1_enabled = state.ch1_enabled;
this.ch1_dacEnabled = state.ch1_dacEnabled;
this.ch1_duty = state.ch1_duty;
this.ch1_dutyPos = state.ch1_dutyPos;
this.ch1_lengthCounter = state.ch1_lengthCounter;
this.ch1_lengthEnabled = state.ch1_lengthEnabled;
this.ch1_volume = state.ch1_volume;
this.ch1_envInitVol = state.ch1_envInitVol;
this.ch1_envDir = state.ch1_envDir;
this.ch1_envPeriod = state.ch1_envPeriod;
this.ch1_envTimer = state.ch1_envTimer;
this.ch1_freq = state.ch1_freq;
this.ch1_freqTimer = state.ch1_freqTimer;
this.ch1_sweepPeriod = state.ch1_sweepPeriod;
this.ch1_sweepDir = state.ch1_sweepDir;
this.ch1_sweepShift = state.ch1_sweepShift;
this.ch1_sweepTimer = state.ch1_sweepTimer;
this.ch1_sweepEnabled = state.ch1_sweepEnabled;
this.ch1_sweepShadow = state.ch1_sweepShadow;
this.ch2_enabled = state.ch2_enabled;
this.ch2_dacEnabled = state.ch2_dacEnabled;
this.ch2_duty = state.ch2_duty;
this.ch2_dutyPos = state.ch2_dutyPos;
this.ch2_lengthCounter = state.ch2_lengthCounter;
this.ch2_lengthEnabled = state.ch2_lengthEnabled;
this.ch2_volume = state.ch2_volume;
this.ch2_envInitVol = state.ch2_envInitVol;
this.ch2_envDir = state.ch2_envDir;
this.ch2_envPeriod = state.ch2_envPeriod;
this.ch2_envTimer = state.ch2_envTimer;
this.ch2_freq = state.ch2_freq;
this.ch2_freqTimer = state.ch2_freqTimer;
this.ch3_enabled = state.ch3_enabled;
this.ch3_dacEnabled = state.ch3_dacEnabled;
this.ch3_lengthCounter = state.ch3_lengthCounter;
this.ch3_lengthEnabled = state.ch3_lengthEnabled;
this.ch3_volume = state.ch3_volume;
this.ch3_freq = state.ch3_freq;
this.ch3_freqTimer = state.ch3_freqTimer;
this.ch3_samplePos = state.ch3_samplePos;
this.ch3_sampleBuffer = state.ch3_sampleBuffer;
if (state.waveRAM)
this.waveRAM.set(state.waveRAM);
this.ch4_enabled = state.ch4_enabled;
this.ch4_dacEnabled = state.ch4_dacEnabled;
this.ch4_lengthCounter = state.ch4_lengthCounter;
this.ch4_lengthEnabled = state.ch4_lengthEnabled;
this.ch4_volume = state.ch4_volume;
this.ch4_envInitVol = state.ch4_envInitVol;
this.ch4_envDir = state.ch4_envDir;
this.ch4_envPeriod = state.ch4_envPeriod;
this.ch4_envTimer = state.ch4_envTimer;
this.ch4_clockShift = state.ch4_clockShift;
this.ch4_widthMode = state.ch4_widthMode;
this.ch4_divisor = state.ch4_divisor;
this.ch4_freqTimer = state.ch4_freqTimer;
this.ch4_lfsr = state.ch4_lfsr;
this.frameSeqTimer = state.frameSeqTimer;
this.frameSeqStep = state.frameSeqStep;
if (state.regs)
this.regs.set(state.regs);
}
}
class GameBoyMachine extends devices_1.BasicScanlineMachine {
getKeyboardMap() { return GB_KEYCODE_MAP; }
constructor() {
super();
this.cpuFrequency = 4194304; // 4.19 MHz
this.canvasWidth = 160;
this.numVisibleScanlines = 144;
this.numTotalScanlines = 154; // 144 visible + 10 vblank
this.cpuCyclesPerLine = 456; // T-cycles per scanline
this.sampleRate = 154 * 60 * 4; // ~36960 Hz: 4 audio samples per scanline
this.overscan = false;
this.defaultROMSize = 0x8000; // 32KB minimum
this.cpu = new SM83_1.SM83();
this.ram = new Uint8Array(0x2000); // Work RAM (C000-DFFF)
this.vram = new Uint8Array(0x2000); // Video RAM (8000-9FFF)
this.oam = new Uint8Array(0xA0); // OAM (FE00-FE9F)
this.hram = new Uint8Array(0x80); // High RAM (FF80-FFFE)
this.extram = new Uint8Array(0x2000); // External/cartridge RAM (A000-BFFF)
// IO registers
this.joyp = 0xCF; // FF00 - Joypad
this.sb = 0; // FF01 - Serial transfer data
this.sc = 0; // FF02 - Serial transfer control
this.divCounter = 0; // Internal 16-bit DIV counter
this.tima = 0; // FF05 - Timer counter
this.tma = 0; // FF06 - Timer modulo
this.tac = 0; // FF07 - Timer control
this.iflag = 0; // FF0F - Interrupt flag
this.ie = 0; // FFFF - Interrupt enable
// PPU registers
this.lcdc = 0x91; // FF40 - LCD control
this.stat = 0; // FF41 - LCD status
this.scy = 0; // FF42 - Scroll Y
this.scx = 0; // FF43 - Scroll X
this.ly = 0; // FF44 - LCD Y coordinate
this.lyc = 0; // FF45 - LY compare
this.dma = 0; // FF46 - DMA transfer
this.bgp = 0xFC; // FF47 - BG palette
this.obp0 = 0xFF; // FF48 - Object palette 0
this.obp1 = 0xFF; // FF49 - Object palette 1
this.wy = 0; // FF4A - Window Y
this.wx = 0; // FF4B - Window X
// Audio
this.apu = new GameBoyAPU();
this.apuCycleAccum = 0; // accumulate T-cycles for downsampling
// PPU state
this.ppuMode = 0; // 0=HBlank, 1=VBlank, 2=OAM, 3=Transfer
this.ppuDot = 0; // Dot counter within scanline
this.windowLine = 0; // Internal window line counter
// MBC1 state
this.mbcType = 0; // 0=ROM only, 1=MBC1
this.romBank = 1;
this.ramBank = 0;
this.ramEnabled = false;
this.mbcMode = 0; // 0=ROM banking, 1=RAM banking
this.romBankMask = 0x1F;
this.read = (0, emu_1.newAddressDecoder)([
[0x0000, 0x3FFF, 0x3FFF, (a) => { return this.rom ? this.rom[a] : 0xFF; }],
[0x4000, 0x7FFF, 0x3FFF, (a) => { return this.readBankedROM(a); }],
[0x8000, 0x9FFF, 0x1FFF, (a) => { return this.vram[a]; }],
[0xA000, 0xBFFF, 0x1FFF, (a) => { return this.readExtRAM(a); }],
[0xC000, 0xDFFF, 0x1FFF, (a) => { return this.ram[a]; }],
[0xE000, 0xFDFF, 0x1FFF, (a) => { return this.ram[a]; }], // Echo RAM
[0xFE00, 0xFE9F, 0xFF, (a) => { return this.oam[a]; }],
[0xFEA0, 0xFEFF, 0xFF, (a) => { return 0xFF; }], // Unusable
[0xFF00, 0xFF7F, 0x7F, (a) => { return this.readIO(a); }],
[0xFF80, 0xFFFE, 0x7F, (a) => { return this.hram[a]; }],
[0xFFFF, 0xFFFF, 0, (a) => { return this.ie; }],
]);
this.write = (0, emu_1.newAddressDecoder)([
[0x0000, 0x1FFF, 0x1FFF, (a, v) => { this.writeMBC(a, v); }],
[0x2000, 0x3FFF, 0x1FFF, (a, v) => { this.writeMBC(0x2000 + a, v); }],
[0x4000, 0x5FFF, 0x1FFF, (a, v) => { this.writeMBC(0x4000 + a, v); }],
[0x6000, 0x7FFF, 0x1FFF, (a, v) => { this.writeMBC(0x6000 + a, v); }],
[0x8000, 0x9FFF, 0x1FFF, (a, v) => { this.vram[a] = v; }],
[0xA000, 0xBFFF, 0x1FFF, (a, v) => { this.writeExtRAM(a, v); }],
[0xC000, 0xDFFF, 0x1FFF, (a, v) => { this.ram[a] = v; }],
[0xE000, 0xFDFF, 0x1FFF, (a, v) => { this.ram[a] = v; }], // Echo RAM
[0xFE00, 0xFE9F, 0xFF, (a, v) => { this.oam[a] = v; }],
[0xFEA0, 0xFEFF, 0xFF, (a, v) => { }],
[0xFF00, 0xFF7F, 0x7F, (a, v) => { this.writeIO(a, v); }],
[0xFF80, 0xFFFE, 0x7F, (a, v) => { this.hram[a] = v; }],
[0xFFFF, 0xFFFF, 0, (a, v) => { this.ie = v; }],
]);
// Timer tracking with proper sub-cycle accuracy
this.timerSubCycles = 0;
this.handler = (0, emu_1.newKeyboardHandler)(this.inputs, this.getKeyboardMap());
this.connectCPUMemoryBus(this);
}
// MBC1 ROM banking
readBankedROM(a) {
if (!this.rom)
return 0xFF;
var bank = this.romBank;
if (this.mbcType === 0)
bank = 1; // ROM only
var addr = a + (bank * 0x4000);
return addr < this.rom.length ? this.rom[addr] : 0xFF;
}
writeMBC(fullAddr, v) {
if (this.mbcType === 0)
return; // ROM-only, ignore writes
if (fullAddr < 0x2000) {
// RAM enable
this.ramEnabled = (v & 0x0F) === 0x0A;
}
else if (fullAddr < 0x4000) {
// ROM bank number (lower 5 bits)
var bank = v & 0x1F;
if (bank === 0)
bank = 1;
this.romBank = (this.romBank & 0x60) | bank;
this.romBank &= this.romBankMask;
}
else if (fullAddr < 0x6000) {
// RAM bank / upper ROM bank bits
if (this.mbcMode === 0) {
this.romBank = (this.romBank & 0x1F) | ((v & 0x03) << 5);
this.romBank &= this.romBankMask;
}
else {
this.ramBank = v & 0x03;
}
}
else {
// Banking mode select
this.mbcMode = v & 0x01;
}
}
readExtRAM(a) {
if (!this.ramEnabled && this.mbcType > 0)
return 0xFF;
var offset = a + (this.ramBank * 0x2000);
return offset < this.extram.length ? this.extram[offset] : 0xFF;
}
writeExtRAM(a, v) {
if (!this.ramEnabled && this.mbcType > 0)
return;
var offset = a + (this.ramBank * 0x2000);
if (offset < this.extram.length)
this.extram[offset] = v;
}
// IO register read
readIO(a) {
switch (a) {
case 0x00: return this.readJoypad();
case 0x01: return this.sb;
case 0x02: return this.sc;
case 0x04: return (this.divCounter >> 8) & 0xFF; // DIV
case 0x05: return this.tima;
case 0x06: return this.tma;
case 0x07: return this.tac | 0xF8;
case 0x0F: return this.iflag | 0xE0;
// Audio registers + Wave RAM
case 0x10:
case 0x11:
case 0x12:
case 0x13:
case 0x14:
case 0x15:
case 0x16:
case 0x17:
case 0x18:
case 0x19:
case 0x1A:
case 0x1B:
case 0x1C:
case 0x1D:
case 0x1E:
case 0x1F:
case 0x20:
case 0x21:
case 0x22:
case 0x23:
case 0x24:
case 0x25:
case 0x26:
case 0x30:
case 0x31:
case 0x32:
case 0x33:
case 0x34:
case 0x35:
case 0x36:
case 0x37:
case 0x38:
case 0x39:
case 0x3A:
case 0x3B:
case 0x3C:
case 0x3D:
case 0x3E:
case 0x3F:
return this.apu.readRegister(a);
// PPU registers
case 0x40: return this.lcdc;
case 0x41: return this.stat | 0x80;
case 0x42: return this.scy;
case 0x43: return this.scx;
case 0x44: return this.ly;
case 0x45: return this.lyc;
case 0x46: return this.dma;
case 0x47: return this.bgp;
case 0x48: return this.obp0;
case 0x49: return this.obp1;
case 0x4A: return this.wy;
case 0x4B: return this.wx;
default: return 0xFF;
}
}
// IO register write
writeIO(a, v) {
switch (a) {
case 0x00:
this.joyp = v;
break;
case 0x01:
this.sb = v;
break;
case 0x02:
this.sc = v;
break;
case 0x04:
this.divCounter = 0;
break; // Writing any value resets DIV
case 0x05:
this.tima = v;
break;
case 0x06:
this.tma = v;
break;
case 0x07:
this.tac = v & 0x07;
break;
case 0x0F:
this.iflag = v & 0x1F;
break;
// Audio registers + Wave RAM
case 0x10:
case 0x11:
case 0x12:
case 0x13:
case 0x14:
case 0x15:
case 0x16:
case 0x17:
case 0x18:
case 0x19:
case 0x1A:
case 0x1B:
case 0x1C:
case 0x1D:
case 0x1E:
case 0x1F:
case 0x20:
case 0x21:
case 0x22:
case 0x23:
case 0x24:
case 0x25:
case 0x26:
case 0x30:
case 0x31:
case 0x32:
case 0x33:
case 0x34:
case 0x35:
case 0x36:
case 0x37:
case 0x38:
case 0x39:
case 0x3A:
case 0x3B:
case 0x3C:
case 0x3D:
case 0x3E:
case 0x3F:
this.apu.writeRegister(a, v);
break;
// PPU registers
case 0x40:
this.lcdc = v;
if (!(v & 0x80)) {
// LCD disabled — reset PPU state
this.ly = 0;
this.ppuMode = 0;
this.ppuDot = 0;
this.stat = (this.stat & 0xFC); // mode 0
}
break;
case 0x41:
this.stat = (this.stat & 0x07) | (v & 0x78);
break; // lower 3 bits read-only
case 0x42:
this.scy = v;
break;
case 0x43:
this.scx = v;
break;
case 0x44: break; // LY is read-only
case 0x45:
this.lyc = v;
break;
case 0x46:
this.dmaTransfer(v);
break;
case 0x47:
this.bgp = v;
break;
case 0x48:
this.obp0 = v;
break;
case 0x49:
this.obp1 = v;
break;
case 0x4A:
this.wy = v;
break;
case 0x4B:
this.wx = v;
break;
}
}
// Joypad reading (FF00)
readJoypad() {
var result = this.joyp | 0x0F;
// Bits 4-5 select which button group to read
if (!(this.joyp & 0x10)) {
// Direction keys selected (active low)
result &= ~(this.inputs[0] & 0x0F);
}
if (!(this.joyp & 0x20)) {
// Button keys selected (active low)
result &= ~((this.inputs[0] >> 4) & 0x0F);
}
return result | 0xC0;
}
// OAM DMA transfer (instant copy)
dmaTransfer(v) {
this.dma = v;
var srcBase = v << 8;
for (var i = 0; i < 0xA0; i++) {
this.oam[i] = this.read(srcBase + i);
}
}
// Timer update — called per CPU cycle (M-cycle)
updateTimer(cycles) {
// DIV increments at 16384 Hz = every 256 T-cycles = 64 M-cycles
// But we count in M-cycles (4 T-cycles each)
this.divCounter = (this.divCounter + (cycles * 4)) & 0xFFFF;
if (!(this.tac & 0x04))
return; // Timer disabled
// Timer clock select
var timerPeriod;
switch (this.tac & 0x03) {
case 0:
timerPeriod = 1024;
break; // 4096 Hz
case 1:
timerPeriod = 16;
break; // 262144 Hz
case 2:
timerPeriod = 64;
break; // 65536 Hz
case 3:
timerPeriod = 256;
break; // 16384 Hz
default: timerPeriod = 1024;
}
// Simplified timer — increment TIMA per period
// In a more accurate emulator, we'd track the sub-cycle counter
// For now, we check if DIV crossed the relevant bit boundary
for (var i = 0; i < cycles; i++) {
this.tima++;
if (this.tima > 0xFF) {
this.tima = this.tma;
this.iflag |= 0x04; // Timer interrupt
}
}
}
// Sync interrupt flags from machine state to CPU
syncInterrupts() {
var pending = this.iflag & this.ie & 0x1F;
this.cpu.interruptFlags = pending;
}
// After CPU handles an interrupt, clear corresponding bit in IF
syncInterruptsBack() {
// The CPU clears bits in interruptFlags when servicing
// We need to reflect that back to IF
var handled = (this.iflag & this.ie & 0x1F) & ~this.cpu.interruptFlags;
this.iflag &= ~handled;
}
// Override advanceCPU to include timer, PPU mode tracking, and interrupt sync
advanceCPU() {
this.syncInterrupts();
var oldFlags = this.cpu.interruptFlags;
var c = this.cpu;
var n = 1;
if (this.cpu.isStable()) {
this.probe.logExecute(this.cpu.getPC(), this.cpu.getSP());
}
if (c.advanceInsn) {
n = c.advanceInsn(1);
}
var tCycles = n * 4;
this.probe.logClocks(tCycles);
// Sync back any interrupt handling
if (this.cpu.interruptFlags !== oldFlags) {
this.syncInterruptsBack();
}
// Update timer
this.updateTimerMCycles(n);
// Update PPU mode based on dot position within scanline
this.updatePPUMode(tCycles);
// Clock APU and generate audio samples
this.apu.clock(tCycles);
if (this.audio) {
// Downsample: emit a sample every ~114 T-cycles (456 / 4 samples per line)
this.apuCycleAccum += tCycles;
var samplePeriod = 114; // 456 T-cycles per line / 4 samples per line
while (this.apuCycleAccum >= samplePeriod) {
this.apuCycleAccum -= samplePeriod;
this.audio.feedSample(this.apu.getSample(), 1);
}
}
return tCycles; // return T-cycles to match cpuCyclesPerLine
}
// Update PPU mode based on dot position within the current scanline
updatePPUMode(tCycles) {
if (this.scanline >= 144)
return; // VBlank mode doesn't change
this.ppuDot += tCycles;
var oldMode = this.ppuMode;
if (this.ppuDot < 80) {
this.ppuMode = 2; // OAM scan
}
else if (this.ppuDot < 252) {
this.ppuMode = 3; // Drawing/transfer
}
else {
this.ppuMode = 0; // HBlank
}
if (this.ppuMode !== oldMode) {
this.stat = (this.stat & 0xFC) | (this.ppuMode & 0x03);
// Fire STAT interrupts on mode transitions
if (this.ppuMode === 0 && (this.stat & 0x08)) {
this.iflag |= 0x02; // HBlank STAT interrupt
}
else if (this.ppuMode === 2 && (this.stat & 0x20)) {
this.iflag |= 0x02; // OAM STAT interrupt
}
}
}
updateTimerMCycles(mCycles) {
var tCycles = mCycles * 4;
this.divCounter = (this.divCounter + tCycles) & 0xFFFF;
if (!(this.tac & 0x04))
return;
var timerPeriod;
switch (this.tac & 0x03) {
case 0:
timerPeriod = 1024;
break;
case 1:
timerPeriod = 16;
break;
case 2:
timerPeriod = 64;
break;
case 3:
timerPeriod = 256;
break;
default: timerPeriod = 1024;
}
this.timerSubCycles += tCycles;
while (this.timerSubCycles >= timerPeriod) {
this.timerSubCycles -= timerPeriod;
this.tima = (this.tima + 1) & 0xFF;
if (this.tima === 0) {
this.tima = this.tma;
this.iflag |= 0x04;
}
}
}
startScanline() {
this.ly = this.scanline;
this.ppuDot = 0;
// Update PPU mode based on scanline position
if (this.scanline < 144) {
// Visible scanline: starts in Mode 2 (OAM scan)
this.ppuMode = 2;
}
else {
// VBlank
this.ppuMode = 1;
if (this.scanline === 144) {
this.iflag |= 0x01; // VBlank interrupt
// STAT VBlank interrupt
if (this.stat & 0x10) {
this.iflag |= 0x02;
}
}
}
// LYC compare
if (this.ly === this.lyc) {
this.stat |= 0x04; // coincidence flag
if (this.stat & 0x40) {
this.iflag |= 0x02; // STAT interrupt
}
}
else {
this.stat &= ~0x04;
}
// Update STAT mode bits
this.stat = (this.stat & 0xFC) | (this.ppuMode & 0x03);
}
drawScanline() {
if (!(this.lcdc & 0x80))
return; // LCD disabled
if (this.scanline >= 144)
return; // VBlank, nothing to draw
var lineOffset = this.scanline * 160;
// Draw background
if (this.lcdc & 0x01) {
this.drawBGLine(lineOffset);
}
else {
// BG disabled — fill with color 0
for (var x = 0; x < 160; x++) {
this.pixels[lineOffset + x] = DMG_PALETTE[0];
}
}
// Draw window
if ((this.lcdc & 0x20) && this.scanline >= this.wy) {
this.drawWindowLine(lineOffset);
}
// Draw sprites
if (this.lcdc & 0x02) {
this.drawSpriteLine(lineOffset);
}
}
// Get color from palette register
getPaletteColor(palette, colorIndex) {
var shade = (palette >> (colorIndex * 2)) & 0x03;
return DMG_PALETTE[shade];
}
// Draw one line of background
drawBGLine(lineOffset) {
var tileMap = (this.lcdc & 0x08) ? 0x1C00 : 0x1800; // BG tile map select
var tileData = (this.lcdc & 0x10) ? 0x0000 : 0x0800; // BG & Window tile data select
var signed = !(this.lcdc & 0x10); // Use signed tile indices when bit 4 is 0
var y = (this.scanline + this.scy) & 0xFF;
var tileRow = (y >> 3) & 31;
var tileYOffset = y & 7;
for (var x = 0; x < 160; x++) {
var scrolledX = (x + this.scx) & 0xFF;
var tileCol = (scrolledX >> 3) & 31;
var tileXBit = 7 - (scrolledX & 7);
// Get tile index from map
var tileIndex = this.vram[tileMap + tileRow * 32 + tileCol];
var tileAddr;
if (signed) {
// Signed: tile 0 is at 0x9000 (VRAM offset 0x1000)
tileAddr = 0x1000 + (((tileIndex << 24) >> 24) * 16); // sign extend
}
else {
tileAddr = tileIndex * 16;
}
// Get pixel from tile data (2bpp)
var lo = this.vram[tileAddr + tileYOffset * 2];
var hi = this.vram[tileAddr + tileYOffset * 2 + 1];
var colorIndex = ((hi >> tileXBit) & 1) << 1 | ((lo >> tileXBit) & 1);
this.pixels[lineOffset + x] = this.getPaletteColor(this.bgp, colorIndex);
}
}
// Draw one line of window
drawWindowLine(lineOffset) {
var wxAdjusted = this.wx - 7;
if (wxAdjusted >= 160)
return;
var tileMap = (this.lcdc & 0x40) ? 0x1C00 : 0x1800; // Window tile map select
var tileData = (this.lcdc & 0x10) ? 0x0000 : 0x0800;
var signed = !(this.lcdc & 0x10);
var winY = this.windowLine;
var tileRow = (winY >> 3) & 31;
var tileYOffset = winY & 7;
var drawn = false;
for (var x = Math.max(0, wxAdjusted); x < 160; x++) {
var winX = x - wxAdjusted;
var tileCol = (winX >> 3) & 31;
var tileXBit = 7 - (winX & 7);
var tileIndex = this.vram[tileMap + tileRow * 32 + tileCol];
var tileAddr;
if (signed) {
tileAddr = 0x1000 + (((tileIndex << 24) >> 24) * 16);
}
else {
tileAddr = tileIndex * 16;
}
var lo = this.vram[tileAddr + tileYOffset * 2];
var hi = this.vram[tileAddr + tileYOffset * 2 + 1];
var colorIndex = ((hi >> tileXBit) & 1) << 1 | ((lo >> tileXBit) & 1);
this.pixels[lineOffset + x] = this.getPaletteColor(this.bgp, colorIndex);
drawn = true;
}
if (drawn)
this.windowLine++;
}
// Draw sprites for current line
drawSpriteLine(lineOffset) {
var spriteHeight = (this.lcdc & 0x04) ? 16 : 8;
var spritesOnLine = 0;
// Collect sprites on this scanline (max 10)
var sprites = [];
for (var i = 0; i < 40 && spritesOnLine < 10; i++) {
var y = this.oam[i * 4] - 16;
var x = this.oam[i * 4 + 1] - 8;
if (this.scanline >= y && this.scanline < y + spriteHeight) {
sprites.push({ x: x, index: i });
spritesOnLine++;
}
}
// Sort by X coordinate (lower X = higher priority, ties broken by OAM index)
sprites.sort((a, b) => a.x !== b.x ? a.x - b.x : a.index - b.index);
// Draw sprites in reverse order (lowest priority first, so higher priority overwrites)
for (var si = sprites.length - 1; si >= 0; si--) {
var spriteIndex = sprites[si].index;
var yPos = this.oam[spriteIndex * 4] - 16;
var xPos = this.oam[spriteIndex * 4 + 1] - 8;
var tileIndex = this.oam[spriteIndex * 4 + 2];
var flags = this.oam[spriteIndex * 4 + 3];
var palette = (flags & 0x10) ? this.obp1 : this.obp0;
var xFlip = !!(flags & 0x20);
var yFlip = !!(flags & 0x40);
var bgPriority = !!(flags & 0x80);
if (spriteHeight === 16)
tileIndex &= 0xFE; // Ignore bit 0 for 8x16 sprites
var tileY = this.scanline - yPos;
if (yFlip)
tileY = spriteHeight - 1 - tileY;
var tileAddr = tileIndex * 16 + tileY * 2;
var lo = this.vram[tileAddr];
var hi = this.vram[tileAddr + 1];
for (var px = 0; px < 8; px++) {
var screenX = xPos + px;
if (screenX < 0 || screenX >= 160)
continue;
var bit = xFlip ? px : (7 - px);
var colorIndex = ((hi >> bit) & 1) << 1 | ((lo >> bit) & 1);
if (colorIndex === 0)
continue; // Transparent
// BG priority: sprite hidden behind BG colors 1-3
if (bgPriority) {
var bgColor = this.pixels[lineOffset + screenX];
if (bgColor !== DMG_PALETTE[(this.bgp & 0x03)])
continue; // BG color 0 check
}
this.pixels[lineOffset + screenX] = this.getPaletteColor(palette, colorIndex);
}
}
}
loadROM(data, title) {
// Detect MBC type from cartridge header
if (data.length > 0x147) {
var cartType = data[0x147];
switch (cartType) {
case 0x00:
this.mbcType = 0;
break; // ROM only
case 0x01:
case 0x02:
case 0x03:
this.mbcType = 1;
break; // MBC1
default:
console.log(`Invalid cartridge type @ 0x147: ${data[0x147]}`);
break;
}
}
else
throw new emu_1.EmuHalt("ROM not long enough for header");
// Determine ROM size and bank mask
this.rom = new Uint8Array(Math.max(data.length, 0x8000));
this.rom.set(data);
var numBanks = Math.max(2, this.rom.length >> 14);
this.romBankMask = numBanks - 1;
// Determine RAM size from header
switch (data[0x149]) {
case 0x00: break; // No RAM
case 0x01:
this.extram = new Uint8Array(0x800);
break; // 2KB
case 0x02:
this.extram = new Uint8Array(0x2000);
break; // 8KB
case 0x03:
this.extram = new Uint8Array(0x8000);
break; // 32KB
case 0x04:
this.extram = new Uint8Array(0x20000);
break; // 128KB
case 0x05:
this.extram = new Uint8Array(0x10000);
break; // 64KB
default:
console.log(`Invalid RAM size code @ 0x149: ${data[0x149]}`);
break;
}
this.reset();
}
reset() {
super.reset();
this.ram.fill(0);
this.vram.fill(0);
this.oam.fill(0);
this.hram.fill(0);
this.iflag = 0;
this.ie = 0;
this.lcdc = 0x91;
this.stat = 0;
this.scy = 0;
this.scx = 0;
this.ly = 0;
this.lyc = 0;
this.bgp = 0xFC;
this.obp0 = 0xFF;
this.obp1 = 0xFF;
this.wy = 0;
this.wx = 0;
this.joyp = 0xCF;
this.divCounter = 0;
this.tima = 0;
this.tma = 0;
this.tac = 0;
this.ppuMode = 0;
this.ppuDot = 0;
this.windowLine = 0;
this.romBank = 1;
this.ramBank = 0;
this.ramEnabled = false;
this.mbcMode = 0;
this.timerSubCycles = 0;
this.apuCycleAccum = 0;
this.apu.reset();
}
preFrame() {
this.windowLine = 0;
}
readVRAMAddress(a) {
return this.vram[a & 0x1FFF];
}
// Save/load state
saveState() {
var state = super.saveState();
state['vram'] = this.vram.slice(0);
state['oam'] = this.oam.slice(0);
state['hram'] = this.hram.slice(0);
state['extram'] = this.extram.slice(0);
state['io'] = {
joyp: this.joyp, sb: this.sb, sc: this.sc,
divCounter: this.divCounter, tima: this.tima, tma: this.tma, tac: this.tac,
iflag: this.iflag, ie: this.ie,
lcdc: this.lcdc, stat: this.stat, scy: this.scy, scx: this.scx,
ly: this.ly, lyc: this.lyc, dma: this.dma,
bgp: this.bgp, obp0: this.obp0, obp1: this.obp1,
wy: this.wy, wx: this.wx,
ppuMode: this.ppuMode, ppuDot: this.ppuDot, windowLine: this.windowLine,
romBank: this.romBank, ramBank: this.ramBank,
ramEnabled: this.ramEnabled, mbcMode: this.mbcMode,
timerSubCycles: this.timerSubCycles,
};
state['apu'] = this.apu.saveState();
state['apuCycleAccum'] = this.apuCycleAccum;
return state;
}
loadState(state) {
super.loadState(state);
this.vram.set(state.vram);
this.oam.set(state.oam);
this.hram.set(state.hram);
if (state.extram)
this.extram.set(state.extram);
var io = state.io;
this.joyp = io.joyp;
this.sb = io.sb;
this.sc = io.sc;
this.divCounter = io.divCounter;
this.tima = io.tima;
this.tma = io.tma;
this.tac = io.tac;
this.iflag = io.iflag;
this.ie = io.ie;
this.lcdc = io.lcdc;
this.stat = io.stat;
this.scy = io.scy;
this.scx = io.scx;
this.ly = io.ly;
this.lyc = io.lyc;
this.dma = io.dma;
this.bgp = io.bgp;
this.obp0 = io.obp0;
this.obp1 = io.obp1;
this.wy = io.wy;
this.wx = io.wx;
this.ppuMode = io.ppuMode;
this.ppuDot = io.ppuDot;
this.windowLine = io.windowLine;
this.romBank = io.romBank;
this.ramBank = io.ramBank;
this.ramEnabled = io.ramEnabled;
this.mbcMode = io.mbcMode;
this.timerSubCycles = io.timerSubCycles;
if (state.apu)
this.apu.loadState(state.apu);
this.apuCycleAccum = state.apuCycleAccum || 0;
}
getDebugCategories() {
return ['CPU', 'Stack', 'PPU'];
}
getDebugInfo(category, state) {
switch (category) {
case 'PPU':
return "LCDC " + (0, util_1.hex)(this.lcdc, 2) + " STAT " + (0, util_1.hex)(this.stat, 2) + "\n"
+ "LY " + (0, util_1.hex)(this.ly, 2) + " LYC " + (0, util_1.hex)(this.lyc, 2) + "\n"
+ "SCX " + (0, util_1.hex)(this.scx, 2) + " SCY " + (0, util_1.hex)(this.scy, 2) + "\n"
+ "WX " + (0, util_1.hex)(this.wx, 2) + " WY " + (0, util_1.hex)(this.wy, 2) + "\n"
+ "BGP " + (0, util_1.hex)(this.bgp, 2) + " OBP0 " + (0, util_1.hex)(this.obp0, 2) + "\n"
+ "OBP1 " + (0, util_1.hex)(this.obp1, 2) + " DMA " + (0, util_1.hex)(this.dma, 2) + "\n"
+ "IF " + (0, util_1.hex)(this.iflag, 2) + " IE " + (0, util_1.hex)(this.ie, 2) + "\n"
+ "DIV " + (0, util_1.hex)((this.divCounter >> 8) & 0xFF, 2) + " TIMA " + (0, util_1.hex)(this.tima, 2) + "\n"
+ "TMA " + (0, util_1.hex)(this.tma, 2) + " TAC " + (0, util_1.hex)(this.tac, 2) + "\n"
+ "Bank " + this.romBank + "\n";
}
}
}
exports.GameBoyMachine = GameBoyMachine;
//# sourceMappingURL=gb.js.map