mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-24 08:02:27 +00:00
gb: double speed on GBC
This commit is contained in:
1 parent
20e9827210
commit
28e268ea05
3 files changed
+124
-45
No files matched your search
+7
-19
@@ -5,8 +5,7 @@ A CGB has 8 background and 8 sprite palettes of 4 colors each, chosen
|
||||
from a 15-bit RGB palette, plus a double-speed CPU mode. This demo checks
|
||||
_cpu to see which machine it is running on. On a CGB it:
|
||||
|
||||
- pulses one background palette entry smoothly with
|
||||
set_bkg_palette_entry(),
|
||||
- animates a background palette entry with set_bkg_palette_entry(),
|
||||
- toggles between vivid color and the DMG-compatible grayscale palettes
|
||||
with cgb_compatibility() (press A),
|
||||
- switches the CPU between normal and double speed with cpu_slow() and
|
||||
@@ -27,7 +26,7 @@ On a monochrome Game Boy it falls back to the four-shade DMG palettes.
|
||||
#include "gb/cgb.h"
|
||||
#include "gbtext.h"
|
||||
|
||||
// 2x2 checkerboard tile (colors 1 and 2)
|
||||
// checkerboard tile (colors 1 and 2)
|
||||
static const uint8_t checker_tile[] = {
|
||||
/*;;{w:8,h:8,bpp:1,count:1,brev:1,np:2,pofs:1,sl:2};;*/
|
||||
0xCC,0xCC,0x33,0x33,0xCC,0xCC,0x33,0x33,
|
||||
@@ -35,22 +34,11 @@ static const uint8_t checker_tile[] = {
|
||||
/*;;*/
|
||||
};
|
||||
|
||||
// background palette. color 1 is replaced each frame by pulse().
|
||||
// vivid background palette, used for CGB palette 0
|
||||
palette_color_t colors[4] = {
|
||||
RGB_BLACK, RGB_RED, RGB_DARKBLUE, RGB_WHITE
|
||||
RGB_BLACK, RGB_RED, RGB_BLUE, RGB_CYAN
|
||||
};
|
||||
|
||||
// smooth pastel rainbow: three triangle waves 120 degrees apart
|
||||
static uint16_t pulse(uint8_t t) {
|
||||
uint8_t r = (t + 0) & 63;
|
||||
uint8_t g = (t + 21) & 63;
|
||||
uint8_t b = (t + 42) & 63;
|
||||
if (r >= 32) r = 63 - r;
|
||||
if (g >= 32) g = 63 - g;
|
||||
if (b >= 32) b = 63 - b;
|
||||
return RGB(r, g, b);
|
||||
}
|
||||
|
||||
volatile uint8_t vbl_flag;
|
||||
void vbl_handler(void) NONBANKED { vbl_flag++; }
|
||||
|
||||
@@ -126,9 +114,9 @@ void main(void) {
|
||||
}
|
||||
put_str(7, 7, buf);
|
||||
|
||||
// pulse palette entry 1 (CGB only)
|
||||
if (cgb_mode && color_mode)
|
||||
set_bkg_palette_entry(0, 1, pulse(frame));
|
||||
// pulse one palette entry slowly (CGB only)
|
||||
if (cgb_mode && color_mode && (frame & 7) == 0)
|
||||
set_bkg_palette_entry(0, 1, colors[(frame >> 3) & 3]);
|
||||
|
||||
frame++;
|
||||
}
|
||||
|
||||
+28
-26
@@ -1172,31 +1172,22 @@ export class GameBoyMachine extends BasicScanlineMachine {
|
||||
|
||||
// CGB speed switch (executed during STOP instruction)
|
||||
//
|
||||
// TODO: double speed is only partially implemented. performSpeedSwitch()
|
||||
// toggles the flag, but nothing calls it and advanceCPU() does not account
|
||||
// for the faster CPU. Plan for fixing:
|
||||
// 1. Hook STOP: in advanceCPU(), snapshot the PC before advanceInsn() and
|
||||
// call performSpeedSwitch() when the executed opcode is 0x10.
|
||||
// 2. Decouple CPU from hardware cycles: at double speed one CPU T-cycle is
|
||||
// half a hardware T-cycle. Accumulate the fractional remainder (the CPU
|
||||
// clock is 8 MHz vs the 4 MHz PPU/timer/APU) and pass only the hardware
|
||||
// T-cycles to updatePPUMode(), the timer and the APU, while returning
|
||||
// those hardware cycles so advanceFrame() fits ~2x instructions per
|
||||
// scanline. Keep the full tCycles for probe.logClocks().
|
||||
// 3. The timer/APU paths currently take M-cycles; convert them to a
|
||||
// T-cycle entry point (or pass the halved value) so they stay at 4 MHz.
|
||||
// 4. Once done, presets/gb/gbc.c's WORK counter should roughly double when
|
||||
// SPEED is DOUBLE, and DIV/timer rates should be unchanged.
|
||||
// Double speed runs the CPU at 8 MHz while the PPU/timer/APU stay at
|
||||
// 4 MHz. SM83 returns whole M-cycles per instruction, so an instruction
|
||||
// that takes n M-cycles consumes n*4 CPU T-cycles normally, but only
|
||||
// n*2 hardware T-cycles in double speed. Both are integers, so no
|
||||
// fractional accumulator is required: advanceCPU() returns the hardware
|
||||
// (PPU) T-cycles to the scanline loop while clocking the PPU/timer/APU
|
||||
// with that same hardware count. The frame loop then fits ~2x the
|
||||
// instructions per scanline.
|
||||
performSpeedSwitch(): void {
|
||||
if (this.cgbMode && this.speedSwitchArmed) {
|
||||
this.doubleSpeed = !this.doubleSpeed;
|
||||
this.speedSwitchArmed = false;
|
||||
// In double speed, CPU runs at 8 MHz but PPU stays at normal speed
|
||||
// We handle this by doubling T-cycles returned from advanceCPU
|
||||
}
|
||||
}
|
||||
|
||||
// Timer update — called per CPU cycle (M-cycle)
|
||||
// Timer update — hardware T-cycles
|
||||
updateTimer(cycles: number): void {
|
||||
// DIV increments at 16384 Hz = every 256 T-cycles = 64 M-cycles
|
||||
// But we count in M-cycles (4 T-cycles each)
|
||||
@@ -1246,16 +1237,28 @@ export class GameBoyMachine extends BasicScanlineMachine {
|
||||
var oldFlags = this.cpu.interruptFlags;
|
||||
var c = this.cpu as any;
|
||||
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);
|
||||
var isStop = false;
|
||||
if (this.cpu.isStable()) {
|
||||
this.probe.logExecute(this.cpu.getPC(), this.cpu.getSP());
|
||||
// STOP (0x10) toggles speed if the switch was armed via KEY1 (FF4D)
|
||||
if (this.cgbMode && this.speedSwitchArmed && !this.cpu.halted &&
|
||||
this.read(this.cpu.getPC()) === 0x10) {
|
||||
isStop = true;
|
||||
}
|
||||
}
|
||||
n = c.advanceInsn(1);
|
||||
var cpuCycles = n * 4;
|
||||
this.probe.logClocks(cpuCycles);
|
||||
// Hardware T-cycles: half of the CPU count in double speed. n is a
|
||||
// whole M-cycle count, so n*2 stays an integer (no accumulator).
|
||||
var tCycles = this.doubleSpeed ? n * 2 : cpuCycles;
|
||||
if (isStop) this.performSpeedSwitch();
|
||||
// Sync back any interrupt handling
|
||||
if (this.cpu.interruptFlags !== oldFlags) {
|
||||
this.syncInterruptsBack();
|
||||
}
|
||||
// Update timer
|
||||
this.updateTimerMCycles(n);
|
||||
this.updateTimerCycles(tCycles);
|
||||
// Update PPU mode based on dot position within scanline
|
||||
this.updatePPUMode(tCycles);
|
||||
// Clock APU and generate audio samples
|
||||
@@ -1269,7 +1272,7 @@ export class GameBoyMachine extends BasicScanlineMachine {
|
||||
this.audio.feedSample(this.apu.getSample(), 1);
|
||||
}
|
||||
}
|
||||
return tCycles; // return T-cycles to match cpuCyclesPerLine
|
||||
return tCycles; // hardware T-cycles to match cpuCyclesPerLine
|
||||
}
|
||||
|
||||
// Update PPU mode based on dot position within the current scanline
|
||||
@@ -1300,8 +1303,7 @@ export class GameBoyMachine extends BasicScanlineMachine {
|
||||
// Timer tracking with proper sub-cycle accuracy
|
||||
private timerSubCycles: number = 0;
|
||||
|
||||
updateTimerMCycles(mCycles: number): void {
|
||||
var tCycles = mCycles * 4;
|
||||
updateTimerCycles(tCycles: number): void {
|
||||
this.divCounter = (this.divCounter + tCycles) & 0xFFFF;
|
||||
|
||||
if (!(this.tac & 0x04)) return;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import assert from "assert";
|
||||
import { describe, it } from "mocha";
|
||||
import { GameBoyMachine } from "../../src/machine/gb";
|
||||
|
||||
// CGB ROM that arms the speed switch (KEY1 = FF4D), executes STOP, then
|
||||
// spins in a 3-M-cycle JR loop:
|
||||
// 0100: 3E 01 LD A,$01
|
||||
// 0102: E0 4D LDH ($4D),A ; arm speed switch
|
||||
// 0104: 10 00 STOP 0
|
||||
// 0106: 18 FE JR -2
|
||||
function makeCGBMachine(): GameBoyMachine {
|
||||
const rom = new Uint8Array(0x8000);
|
||||
rom[0x143] = 0x80; // CGB flag
|
||||
rom[0x147] = 0x00; // ROM only
|
||||
rom[0x100] = 0x3e; rom[0x101] = 0x01;
|
||||
rom[0x102] = 0xe0; rom[0x103] = 0x4d;
|
||||
rom[0x104] = 0x10; rom[0x105] = 0x00;
|
||||
rom[0x106] = 0x18; rom[0x107] = 0xfe;
|
||||
const m = new GameBoyMachine();
|
||||
m.loadROM(rom);
|
||||
m.connectVideo(new Uint32Array(160 * 144));
|
||||
m.cpu.PC = 0x0100;
|
||||
return m;
|
||||
}
|
||||
|
||||
// Run the prologue so the CPU sits in the idle loop with the requested speed.
|
||||
function makeLoopingMachine(doubleSpeed: boolean): GameBoyMachine {
|
||||
const m = makeCGBMachine();
|
||||
for (let i = 0; i < 3; i++) m.advanceCPU(); // LD, LDH, STOP
|
||||
assert.strictEqual(m.cpu.PC, 0x0106, "should be in the JR loop");
|
||||
m.doubleSpeed = doubleSpeed;
|
||||
return m;
|
||||
}
|
||||
|
||||
describe('Game Boy CGB double speed', function () {
|
||||
|
||||
it('arms and toggles speed on STOP', function () {
|
||||
const m = makeCGBMachine();
|
||||
assert.strictEqual(m.doubleSpeed, false);
|
||||
m.advanceCPU(); // LD A,$01
|
||||
m.advanceCPU(); // LDH ($4D),A
|
||||
assert.strictEqual(m.speedSwitchArmed, true);
|
||||
m.advanceCPU(); // STOP
|
||||
assert.strictEqual(m.doubleSpeed, true, "STOP should enter double speed");
|
||||
assert.strictEqual(m.speedSwitchArmed, false, "arm bit should clear");
|
||||
m.cpu.PC = 0x0106;
|
||||
m.write(0xFF4D, 0x01); // re-arm
|
||||
m.cpu.PC = 0x0104; // STOP again
|
||||
m.advanceCPU();
|
||||
assert.strictEqual(m.doubleSpeed, false, "second STOP should return to normal");
|
||||
});
|
||||
|
||||
it('does not switch without a preceding KEY1 write', function () {
|
||||
const m = makeCGBMachine();
|
||||
m.cpu.PC = 0x0104; // jump straight to STOP
|
||||
m.advanceCPU();
|
||||
assert.strictEqual(m.doubleSpeed, false);
|
||||
});
|
||||
|
||||
it('halves the hardware T-cycles per instruction', function () {
|
||||
const normal = makeLoopingMachine(false);
|
||||
assert.strictEqual(normal.advanceCPU(), 12, "JR takes 3 M-cycles = 12 T-cycles");
|
||||
const fast = makeLoopingMachine(true);
|
||||
assert.strictEqual(fast.advanceCPU(), 6, "double speed halves hardware T-cycles");
|
||||
});
|
||||
|
||||
it('fits roughly twice the instructions into a frame', function () {
|
||||
const normal = makeLoopingMachine(false);
|
||||
const fast = makeLoopingMachine(true);
|
||||
const normalSteps = normal.advanceFrame(null);
|
||||
const fastSteps = fast.advanceFrame(null);
|
||||
assert.ok(fastSteps > normalSteps * 1.9 && fastSteps < normalSteps * 2.1,
|
||||
`expected ~2x steps, got ${normalSteps} vs ${fastSteps}`);
|
||||
});
|
||||
|
||||
it('keeps DIV/timer at hardware rate', function () {
|
||||
const run = (doubleSpeed: boolean) => {
|
||||
const m = makeLoopingMachine(doubleSpeed);
|
||||
m.tac = 0x05; // enable timer, 16 T-cycle period
|
||||
m.advanceFrame(null);
|
||||
return { div: m.divCounter, tima: m.tima };
|
||||
};
|
||||
const normal = run(false);
|
||||
const fast = run(true);
|
||||
// Same number of scanlines elapsed, so DIV should advance identically.
|
||||
assert.strictEqual(fast.div, normal.div, "DIV must run at hardware rate");
|
||||
assert.strictEqual(fast.tima, normal.tima, "TIMA must run at hardware rate");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user