mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-24 08:02:27 +00:00
astrocade: improved wait state accuracy
This commit is contained in:
1 parent
60963cbfd2
commit
cca1b9bb14
2 files changed
+202
-21
No files matched your search
+81
-21
@@ -61,6 +61,7 @@ const audioOversample = 2;
|
||||
|
||||
const _BallyAstrocade = function(arcade:boolean) {
|
||||
|
||||
var machine : BallyAstrocade;
|
||||
var cpu : Z80;
|
||||
var ram : Uint8Array;
|
||||
var membus, iobus : Bus;
|
||||
@@ -76,6 +77,29 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
const PIXEL_ON = 0xffeeeeee;
|
||||
const PIXEL_OFF = 0xff000000;
|
||||
|
||||
/*
|
||||
Screen RAM is DRAM shared by three requesters in priority order: DRAM refresh
|
||||
(blanking only, so it never costs the CPU), screen refresh, then Z80/Magic.
|
||||
CAS# free-runs, so memory timeslots are fixed and one Z80 M clock wide:
|
||||
|
||||
- Every Z80 screen-RAM access takes at least T1,T2,Tw,T3 — "A Z80 access cycle
|
||||
to this memory always results in at least one wait state" — blanked or not.
|
||||
That's +1 over the usual 3 T-states.
|
||||
- During active video, the refresh wins the slot. Consumer mode fetches 1 byte
|
||||
(4 px) every 4 PX = every 2 M clocks → memory is unavailable half the time
|
||||
→ 50% of accesses eat a second wait state. Commercial/arcade fetches 4 bytes
|
||||
(16 px) every 8 PX = every 4 M clocks → unavailable ¼ of the time → 25% eat
|
||||
a second one.
|
||||
- The fetch only runs for the 40 (consumer) / 20 (arcade) reads per line — 160
|
||||
px of the 63.5 µs line, ≈70%. Border and hblank are free, as is ROM and the
|
||||
arcade's separate scratch RAM board.
|
||||
*/
|
||||
const REFRESH_SLOT_MASK = arcade ? 3 : 1;
|
||||
// the refresh reads 40 (consumer) or 20 (commercial) times per line, covering the
|
||||
// 160 displayed pixels of the 63.5 us line; the rest is border and hblank
|
||||
const ACTIVE_LINE_FRACTION = 0.7;
|
||||
var activecycles = 0; // = ACTIVE_LINE_FRACTION of a scanline, set by init()
|
||||
|
||||
// state variables
|
||||
var inputs = new Uint8Array(0x20);
|
||||
var magicop = 0;
|
||||
@@ -95,13 +119,27 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
var rotcount = 0;
|
||||
var intst = 0;
|
||||
var waitstates = 0;
|
||||
var slotphase = 0;
|
||||
var patboard = new Uint8Array(0x08);
|
||||
var patdest = 0;
|
||||
|
||||
// is the video refresh reading screen RAM at this point in the frame?
|
||||
function refreshing() : boolean {
|
||||
return machine.scanline < verbl && machine.getRasterX() < activecycles;
|
||||
}
|
||||
|
||||
// charge the Z80 for one screen RAM (DRAM) bus cycle
|
||||
function screenwait() {
|
||||
waitstates++; // always at least one wait state
|
||||
if (refreshing() && !(slotphase++ & REFRESH_SLOT_MASK)) {
|
||||
waitstates++; // and a second one when we collide with the screen refresh
|
||||
}
|
||||
}
|
||||
|
||||
function ramwrite(a: number, v: number) {
|
||||
// set RAM
|
||||
ram[a] = v;
|
||||
waitstates++;
|
||||
screenwait();
|
||||
// mark scanline as dirty
|
||||
dirtylines[((a & samask) / swbytes) | 0] = 1;
|
||||
// this was old behavior where we updated instantly
|
||||
@@ -216,6 +254,7 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
let curwidth: number;
|
||||
let u13ff: number = 0;
|
||||
let cycles: number = 0;
|
||||
const wsave = waitstates;
|
||||
|
||||
u13ff = 0;
|
||||
|
||||
@@ -296,9 +335,10 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
m_pattern_height--;
|
||||
}
|
||||
|
||||
// Adjust m_maincpu.icount
|
||||
// m_maincpu.adjust_icount(-cycles);
|
||||
// Replace the above line with the actual adjustment of icount.
|
||||
// the pattern board owns the bus while it runs, so its own cycles (two reads
|
||||
// and two writes per byte, as MAME counts them) replace the Z80 wait states
|
||||
// that the transfers above accrued
|
||||
waitstates = wsave + cycles;
|
||||
}
|
||||
|
||||
this.drawScanline = (sl:number) => {
|
||||
@@ -320,7 +360,9 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
this.init = (machine:BallyAstrocade, c:Z80, r:Uint8Array, inp:Uint8Array<ArrayBuffer>, psgg) => {
|
||||
this.init = (mach:BallyAstrocade, c:Z80, r:Uint8Array, inp:Uint8Array<ArrayBuffer>, psgg) => {
|
||||
machine = mach;
|
||||
activecycles = mach.cpuCyclesPerLine * ACTIVE_LINE_FRACTION;
|
||||
cpu = c;
|
||||
ram = r;
|
||||
inputs = inp;
|
||||
@@ -333,7 +375,7 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
read: newAddressDecoder([
|
||||
[0x0000, 0x1fff, 0x1fff, function(a) { return bios[a]; }],
|
||||
[0x2000, 0x3fff, 0x1fff, function(a) { return rom ? rom[a] : 0; }],
|
||||
[0x4000, 0x4fff, 0xfff, function(a) { waitstates++; return ram[a]; }],
|
||||
[0x4000, 0x4fff, 0xfff, function(a) { screenwait(); return ram[a]; }],
|
||||
]),
|
||||
write: newAddressDecoder([
|
||||
[0x4000, 0x4fff, 0xfff, ramwrite],
|
||||
@@ -341,18 +383,18 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
]),
|
||||
}
|
||||
} else {
|
||||
// arcade game (TODO: wait states 1/4 of the time)
|
||||
// arcade game
|
||||
membus = {
|
||||
read: newAddressDecoder([
|
||||
[0x4000, 0x7fff, 0x3fff, function(a) { return ram[a]; }], // screen RAM
|
||||
[0xd000, 0xdfff, 0x0fff, function(a) { return ram[a + 0x4000]; }], // static RAM
|
||||
[0x4000, 0x7fff, 0x3fff, function(a) { screenwait(); return ram[a]; }], // screen RAM
|
||||
[0xd000, 0xdfff, 0x0fff, function(a) { return ram[a + 0x4000]; }], // static RAM (own board, no wait states)
|
||||
[0x0000, 0x3fff, 0x3fff, function(a) { return rom ? rom[a] : 0; }], // ROM
|
||||
[0x8000, 0xbfff, 0x3fff, function(a) { return rom ? rom[a + 0x4000] : 0; }], // ROM
|
||||
]),
|
||||
write: newAddressDecoder([
|
||||
[0x4000, 0x7fff, 0x3fff, ramwrite],
|
||||
[0x0000, 0x3fff, 0x3fff, magicwrite],
|
||||
[0xd000, 0xdfff, 0x0fff, function(a, v) { ramwrite(a + 0x4000, v); }], // static RAM
|
||||
[0xd000, 0xdfff, 0x0fff, function(a, v) { ram[a + 0x4000] = v; }], // static RAM (not displayed, no wait states)
|
||||
]),
|
||||
}
|
||||
}
|
||||
@@ -458,8 +500,8 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
}
|
||||
}
|
||||
}
|
||||
machine.connectCPUMemoryBus(membus);
|
||||
machine.connectCPUIOBus(iobus);
|
||||
mach.connectCPUMemoryBus(membus);
|
||||
mach.connectCPUIOBus(iobus);
|
||||
this.membus = membus;
|
||||
this.iobus = iobus;
|
||||
// default palette
|
||||
@@ -468,12 +510,21 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
}
|
||||
};
|
||||
|
||||
this.resetWaitStates = function(sl) {
|
||||
var n = sl < verbl ? waitstates : 0; // only wait if video active
|
||||
// wait states accrued since the last call, charged to the CPU by advanceCPU()
|
||||
this.resetWaitStates = function() {
|
||||
var n = waitstates;
|
||||
waitstates = 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
// read for the debugger/UI, without stalling the CPU
|
||||
this.peek = function(a: number) {
|
||||
var n = waitstates;
|
||||
var v = membus.read(a);
|
||||
waitstates = n;
|
||||
return v;
|
||||
}
|
||||
|
||||
this.loadState = (state) => {
|
||||
cpu.loadState(state.c);
|
||||
ram.set(state.ram);
|
||||
@@ -491,6 +542,7 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
rotcount = state.rotcount;
|
||||
rotdata.set(state.rotdata);
|
||||
intst = state.intst;
|
||||
slotphase = state.slotphase | 0;
|
||||
inputs.set(state.inputs);
|
||||
patboard.set(state.patboard);
|
||||
patdest = state.patdest;
|
||||
@@ -516,6 +568,7 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
rotcount: rotcount,
|
||||
rotdata: rotdata.slice(0),
|
||||
intst: intst,
|
||||
slotphase: slotphase,
|
||||
patboard: patboard.slice(0),
|
||||
patdest: patdest,
|
||||
};
|
||||
@@ -565,12 +618,12 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
export class BallyAstrocade extends BasicScanlineMachine implements AcceptsPaddleInput {
|
||||
|
||||
cpuFrequency = 1789000;
|
||||
numTotalScanlines = 262;
|
||||
numTotalScanlines : number;
|
||||
numVisibleScanlines : number;
|
||||
cpuCyclesPerLine : number;
|
||||
defaultROMSize : number;
|
||||
canvasWidth : number;
|
||||
sampleRate = 60 * 262 * audioOversample;
|
||||
sampleRate : number;
|
||||
ram : Uint8Array;
|
||||
cpu : Z80;
|
||||
m; // _BallyAstrocade
|
||||
@@ -586,7 +639,14 @@ export class BallyAstrocade extends BasicScanlineMachine implements AcceptsPaddl
|
||||
this.arcade = arcade;
|
||||
this.cpu = new Z80();
|
||||
this.psg = new AstrocadeAudio(new MasterAudio());
|
||||
this.audioadapter = new TssChannelAdapter(this.psg.psg, audioOversample, this.sampleRate);
|
||||
// in consumer mode every line of screen RAM is displayed twice, so one emulated
|
||||
// scanline is two of the 262 NTSC lines (as are INLIN and VERBL, hence the >>1)
|
||||
this.numTotalScanlines = arcade ? 262 : 131;
|
||||
// one audio sample per emulated scanline, so oversample twice as much in
|
||||
// consumer mode to keep the sample rate the same
|
||||
var oversample = audioOversample * (arcade ? 1 : 2);
|
||||
this.sampleRate = 60 * this.numTotalScanlines * oversample;
|
||||
this.audioadapter = new TssChannelAdapter(this.psg.psg, oversample, this.sampleRate);
|
||||
this.handler = newKeyboardHandler(this.inputs, ASTROCADE_KEYCODE_MAP);
|
||||
this.defaultROMSize = arcade ? 0xb000 : 0x2000;
|
||||
this.ram = new Uint8Array(arcade ? 0x5000 : 0x1000);
|
||||
@@ -606,7 +666,7 @@ export class BallyAstrocade extends BasicScanlineMachine implements AcceptsPaddl
|
||||
}
|
||||
|
||||
read(a:number) : number {
|
||||
return this.m.membus.read(a);
|
||||
return this.m.peek(a);
|
||||
}
|
||||
write(a:number, v:number) : void {
|
||||
this.m.membus.write(a,v);
|
||||
@@ -619,7 +679,7 @@ export class BallyAstrocade extends BasicScanlineMachine implements AcceptsPaddl
|
||||
this.m.connectVideo(this.backbuffer);
|
||||
}
|
||||
preFrame() {
|
||||
this.m.resetWaitStates(0);
|
||||
this.m.resetWaitStates();
|
||||
}
|
||||
postFrame() {
|
||||
// copy back buffer to front buffer, omitting bottom non-visible pixels
|
||||
@@ -652,8 +712,8 @@ export class BallyAstrocade extends BasicScanlineMachine implements AcceptsPaddl
|
||||
|
||||
advanceCPU() {
|
||||
var clk = super.advanceCPU();
|
||||
// TODO: disable 33% of the time (hblank)
|
||||
var xtra = this.m.resetWaitStates(this.scanline);
|
||||
// charge the CPU for screen RAM contention during this instruction
|
||||
var xtra = this.m.resetWaitStates();
|
||||
if (xtra) {
|
||||
clk += xtra;
|
||||
this.probe.logClocks(xtra);
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
|
||||
var assert = require('assert');
|
||||
var fs = require('fs');
|
||||
var vm = require('vm');
|
||||
|
||||
// tss audio channels are plain scripts that install themselves as globals
|
||||
global.window = global;
|
||||
global.self = global;
|
||||
function includeInThisContext(path) {
|
||||
vm.runInThisContext(fs.readFileSync(path), path);
|
||||
}
|
||||
includeInThisContext('tss/js/Log.js');
|
||||
includeInThisContext('tss/js/tss/PsgDeviceChannel.js');
|
||||
includeInThisContext('tss/js/tss/MasterChannel.js');
|
||||
includeInThisContext('tss/js/tss/AudioLooper.js');
|
||||
|
||||
var astrocade = require('gen/machine/astrocade.js');
|
||||
|
||||
const CPU_FREQ = 1789000;
|
||||
|
||||
// put the raster somewhere specific, then count the wait states one access costs
|
||||
function waits(mach, scanline, rasterx, fn) {
|
||||
mach.scanline = scanline;
|
||||
mach.frameCycles = rasterx;
|
||||
mach.m.resetWaitStates();
|
||||
fn();
|
||||
return mach.m.resetWaitStates();
|
||||
}
|
||||
|
||||
describe('Bally Astrocade', function () {
|
||||
|
||||
// one emulated scanline is one NTSC line in the arcade and two in the console
|
||||
// (consumer mode displays each line of screen RAM twice), so both run the Z80
|
||||
// at 1.789 MHz over a 262-line frame
|
||||
it('should run the CPU at 1.789 MHz', function () {
|
||||
for (var arcade of [false, true]) {
|
||||
var mach = new astrocade.BallyAstrocade(arcade);
|
||||
var cyclesPerFrame = mach.numTotalScanlines * mach.cpuCyclesPerLine;
|
||||
assert.ok(Math.abs(cyclesPerFrame * 60 - CPU_FREQ) < CPU_FREQ * 0.01,
|
||||
'CPU runs at ' + (cyclesPerFrame * 60) + ' Hz, arcade=' + arcade);
|
||||
assert.equal(262, mach.numTotalScanlines * (arcade ? 1 : 2));
|
||||
assert.equal(31440, mach.sampleRate);
|
||||
}
|
||||
});
|
||||
|
||||
// "A Z80 access cycle to this memory always results in at least one wait state"
|
||||
// -- Bally Professional Arcade Video Hardware Description (Tony Miller, 2001) p.9
|
||||
it('should wait one cycle per screen RAM access outside of active video', function () {
|
||||
var mach = new astrocade.BallyAstrocade(false);
|
||||
var vblank = mach.numVisibleScanlines + 10;
|
||||
assert.equal(1, waits(mach, vblank, 0, () => mach.write(0x4000, 0x55)));
|
||||
assert.equal(1, waits(mach, vblank, 0, () => mach.m.membus.read(0x4000)));
|
||||
assert.equal(1, waits(mach, vblank, 0, () => mach.write(0x0000, 0x55))); // magic write
|
||||
// ...and during the border/hblank part of a visible line, too
|
||||
var hblank = mach.cpuCyclesPerLine - 1;
|
||||
assert.equal(1, waits(mach, 50, hblank, () => mach.write(0x4000, 0x55)));
|
||||
});
|
||||
|
||||
it('should not wait for ROM', function () {
|
||||
var mach = new astrocade.BallyAstrocade(false);
|
||||
assert.equal(0, waits(mach, 50, 0, () => mach.m.membus.read(0x0000))); // BIOS
|
||||
assert.equal(0, waits(mach, 50, 0, () => mach.m.membus.read(0x2000))); // cart
|
||||
});
|
||||
|
||||
// during active video the screen refresh takes every other memory timeslot in
|
||||
// consumer mode, so half of the Z80's accesses eat a second wait state
|
||||
it('should wait 1.5 cycles on average during active video (consumer)', function () {
|
||||
var mach = new astrocade.BallyAstrocade(false);
|
||||
var n = waits(mach, 50, 0, () => {
|
||||
for (var i = 0; i < 8; i++) mach.write(0x4000 + i, 0x55);
|
||||
});
|
||||
assert.equal(12, n);
|
||||
});
|
||||
|
||||
// in commercial mode it reads four bytes at a time, so it only takes one
|
||||
// timeslot in four and only a quarter of the accesses wait twice
|
||||
it('should wait 1.25 cycles on average during active video (arcade)', function () {
|
||||
var mach = new astrocade.BallyAstrocade(true);
|
||||
var n = waits(mach, 50, 0, () => {
|
||||
for (var i = 0; i < 8; i++) mach.write(0x4000 + i, 0x55);
|
||||
});
|
||||
assert.equal(10, n);
|
||||
// reads are contended just the same
|
||||
n = waits(mach, 50, 0, () => {
|
||||
for (var i = 0; i < 8; i++) mach.m.membus.read(0x4000 + i);
|
||||
});
|
||||
assert.equal(10, n);
|
||||
});
|
||||
|
||||
// the arcade's scratch RAM is on its own board, not the screen RAM DRAM bus
|
||||
it('should not wait for arcade scratch RAM', function () {
|
||||
var mach = new astrocade.BallyAstrocade(true);
|
||||
assert.equal(0, waits(mach, 50, 0, () => mach.write(0xd000, 0x55)));
|
||||
assert.equal(0, waits(mach, 50, 0, () => mach.m.membus.read(0xd000)));
|
||||
assert.equal(0x55, mach.read(0xd000));
|
||||
});
|
||||
|
||||
// the debugger reads memory constantly; that must not stall the CPU
|
||||
it('should not wait when peeking for the debugger', function () {
|
||||
var mach = new astrocade.BallyAstrocade(false);
|
||||
var n = waits(mach, 50, 0, () => {
|
||||
for (var i = 0; i < 0x1000; i++) mach.read(0x4000 + i);
|
||||
});
|
||||
assert.equal(0, n);
|
||||
});
|
||||
|
||||
// wait states have to reach the CPU's cycle count, whatever the raster is doing
|
||||
it('should charge wait states to the CPU', function () {
|
||||
for (var arcade of [false, true]) {
|
||||
var mach = new astrocade.BallyAstrocade(arcade);
|
||||
// LD HL,$4000 / LD (HL),A -- the store is 7 T-states on a plain Z80
|
||||
var prog = new Uint8Array([0x21, 0x00, 0x40, 0x77, 0x76]);
|
||||
if (arcade) mach.loadROM(prog); else mach.loadBIOS(prog);
|
||||
mach.reset();
|
||||
mach.scanline = mach.numVisibleScanlines + 10; // vblank: exactly one wait state
|
||||
mach.frameCycles = 0;
|
||||
mach.advanceCPU(); // LD HL,nn
|
||||
assert.equal(8, mach.advanceCPU(), 'arcade=' + arcade); // LD (HL),A = 7 + 1 wait
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user