mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-23 17:02:18 +00:00
astrocade: improved astrocade-arcade platform, fixed astrocade gfx bugs, machine options (e.g. video rotate)
This commit is contained in:
1 parent
cc1709b63e
commit
bb9a401875
6 files changed
+181
-22
No files matched your search
@@ -67,6 +67,8 @@ See [extension/CHANGELOG.md](extension/CHANGELOG.md).
|
||||
- PC Engine Chase example; BASIC uppercase-only dialect fixes; 6502 KIL instructions halt the emulator.
|
||||
- C64: openroms-compatible cc65 `_cgetc`/`PLOT` overrides.
|
||||
- Verilog: fixed a non-convergence bug on settle and a shift-state bug on digit keys.
|
||||
- Astrocade fixes. Astrocade arcade (`astrocade-arcade`) closer to running Gorf.
|
||||
- Vertical-monitor games can turn the screen with `?options=rotate%3D-90`.
|
||||
|
||||
### Build tools
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ void render_sprite(const byte* src, byte x, byte y, byte op) {
|
||||
byte w = *src++; // get width from 1st byte of sprite
|
||||
byte h = *src++; // get height from 2nd byte of sprite
|
||||
byte* dest = &vmagic[y][x>>2];// destination address
|
||||
hw_magic = M_SHIFT(x) | op; // set magic register
|
||||
// M_ERASE is not a real hardware mode (it's the rotate bit); erase by writing zeros
|
||||
hw_magic = (op == M_ERASE) ? M_MOVE : M_SHIFT(x) | op; // set magic register
|
||||
// y clipping off bottom
|
||||
if (y+h >= VHEIGHT) {
|
||||
if (y >= VHEIGHT) return;
|
||||
|
||||
@@ -814,6 +814,7 @@ export function hasSerialIO(arg: any): arg is HasSerialIO {
|
||||
export abstract class BaseMachinePlatform<T extends Machine> extends BaseDebugPlatform implements Platform {
|
||||
machine: T;
|
||||
mainElement: HTMLElement;
|
||||
options: any; // URL options (?options=key%3Dvalue), if any
|
||||
timer: AnimationTimer;
|
||||
video: RasterVideo;
|
||||
audio: SampledAudio;
|
||||
@@ -834,9 +835,10 @@ export abstract class BaseMachinePlatform<T extends Machine> extends BaseDebugPl
|
||||
abstract getDefaultExtensions(): string[];
|
||||
abstract getPresets(): Preset[];
|
||||
|
||||
constructor(mainElement: HTMLElement) {
|
||||
constructor(mainElement: HTMLElement, options?: any) {
|
||||
super();
|
||||
this.mainElement = mainElement;
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
reset() {
|
||||
|
||||
+96
-19
@@ -5,6 +5,8 @@ import { KeyFlags, newAddressDecoder, padBytes, Keys, makeKeycodeMap, newKeyboar
|
||||
import { TssChannelAdapter, MasterAudio, AY38910_Audio } from "../common/audio";
|
||||
import { hex, rgb2bgr, lzgmini, stringToByteArray } from "../common/util";
|
||||
|
||||
const SC01_CLOCK = 720000;
|
||||
|
||||
// http://metopal.com/projects/ballybook/doku.php
|
||||
|
||||
// TODO: fix keys, more controllers, vibrato/noise, border color
|
||||
@@ -187,7 +189,8 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
(((rotdata[0] >> sh) & 3) << 0);
|
||||
} else {
|
||||
// fill buffer
|
||||
rotdata[rotcount & 3] = v;
|
||||
rotdata[rotcount++ & 3] = v;
|
||||
return; // the first four writes only fill the buffer
|
||||
}
|
||||
rotcount++;
|
||||
} else {
|
||||
@@ -214,13 +217,13 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
if ((oldv & 0x30) && (v & 0x30)) icpt |= 0x2;
|
||||
if ((oldv & 0x0c) && (v & 0x0c)) icpt |= 0x4;
|
||||
if ((oldv & 0x03) && (v & 0x03)) icpt |= 0x8;
|
||||
// apply op
|
||||
// apply op (OR wins if both are set)
|
||||
if (magicop & 0x10)
|
||||
v |= oldv;
|
||||
if (magicop & 0x20)
|
||||
v ^= oldv; // TODO: what if both?
|
||||
// upper 4 bits persist, lower are just since last write
|
||||
intst = (intst & 0xf0) | icpt | (icpt << 4);
|
||||
else
|
||||
v ^= oldv;
|
||||
// lower 4 bits accumulate until INTST is read, upper are just this write
|
||||
intst = (intst & 0x0f) | icpt | (icpt << 4);
|
||||
}
|
||||
// commit write to ram/screen
|
||||
ramwrite(a, v);
|
||||
@@ -249,7 +252,7 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
let m_pattern_skip = patboard[3];
|
||||
let m_pattern_dest = (patdest & 0xff) | (patboard[4] << 8);
|
||||
let m_pattern_width = patboard[5];
|
||||
let m_pattern_height = patboard[6] + 1;
|
||||
let m_pattern_height = patboard[6]; // rows - 1; the loop below runs until it goes negative
|
||||
|
||||
let curwidth: number;
|
||||
let u13ff: number = 0;
|
||||
@@ -299,7 +302,7 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
if (curwidth === 0 && (m_pattern_mode & 0x08) !== 0) {
|
||||
busdata = 0;
|
||||
} else {
|
||||
busdata = membus.read(m_pattern_source);
|
||||
busdata = membus.read(busaddr & 0xffff);
|
||||
}
|
||||
|
||||
if ((m_pattern_mode & 0x01) === 0) {
|
||||
@@ -310,7 +313,7 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
|
||||
// Write Phase
|
||||
busaddr = (m_pattern_mode & 0x01) !== 0 ? m_pattern_source : m_pattern_dest;
|
||||
ramwrite(busaddr, busdata);
|
||||
membus.write(busaddr & 0xffff, busdata); // through the bus, like MAME's space.write_byte
|
||||
|
||||
if ((m_pattern_mode & 0x01) === 0) {
|
||||
incrementDest();
|
||||
@@ -347,6 +350,12 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
cpu.retryInterrupts = !(inmod & 0x4);
|
||||
this.probe && this.probe.logInterrupt(infbk);
|
||||
cpu.interrupt(infbk);
|
||||
} else if (arcade && (inmod & 0x12)) {
|
||||
// arcade boards wire horizontal drive to the lightpen interrupt, so it
|
||||
// fires every line when enabled (INMOD bit 1 or 4); bit 0 is the mode
|
||||
cpu.retryInterrupts = !(inmod & 0x1);
|
||||
this.probe && this.probe.logInterrupt(infbk & 0xf0);
|
||||
cpu.interrupt(infbk & 0xf0);
|
||||
}
|
||||
// refresh this line in frame buffer?
|
||||
if (sl < sheight && refreshlines > 0) {
|
||||
@@ -389,7 +398,7 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
[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
|
||||
[0x8000, 0xbfff, 0x3fff, function(a) { return rom ? rom[a + 0x8000] : 0; }], // ROM
|
||||
]),
|
||||
write: newAddressDecoder([
|
||||
[0x4000, 0x7fff, 0x3fff, ramwrite],
|
||||
@@ -400,6 +409,7 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
}
|
||||
iobus = {
|
||||
read: function(addr) {
|
||||
var hi = (addr >> 8) & 0xff;
|
||||
addr &= 0x1f;
|
||||
var rtn;
|
||||
switch (addr) {
|
||||
@@ -407,6 +417,17 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
rtn = intst;
|
||||
intst = 0;
|
||||
break;
|
||||
case 0x12:
|
||||
rtn = inputs[addr];
|
||||
// Gorf/Wizard of Wor: Votrax request line on bit 7
|
||||
if (machine.sc01 && machine.sc01.ready) rtn |= 0x80;
|
||||
break;
|
||||
case 0x17:
|
||||
// a read cycle on the sound chip's SO7 latches the high address byte
|
||||
// (Gorf's IN A,(C) with B = phoneme) into the Votrax
|
||||
if (machine.sc01) machine.speechOut(hi);
|
||||
rtn = inputs[addr];
|
||||
break;
|
||||
default:
|
||||
rtn = inputs[addr];
|
||||
break;
|
||||
@@ -421,10 +442,10 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
}
|
||||
addr &= 0xff;
|
||||
// pattern board
|
||||
if (addr > 0x78 && addr < 0x80) {
|
||||
if (addr >= 0x78 && addr < 0x7f) {
|
||||
patboard[addr & 7] = val;
|
||||
if (addr == 0x72) { patdest = 0; }
|
||||
if (addr == 0x74) { patdest = (patdest + patboard[3]) & 0xff; }
|
||||
if (addr == 0x7a) { patdest = 0; } // mode write clears dest low byte
|
||||
if (addr == 0x7c) { patdest = (patdest + patboard[3]) & 0xff; } // dest high write adds skip
|
||||
if (addr == 0x7e) { xfer_patboard(); }
|
||||
return;
|
||||
}
|
||||
@@ -447,8 +468,11 @@ const _BallyAstrocade = function(arcade:boolean) {
|
||||
refreshall();
|
||||
break;
|
||||
case 0xa: // VERBL (vertical blank)
|
||||
verbl = arcade ? val : val >> 1;
|
||||
refreshall();
|
||||
// the arcade board always shows its full 204-line raster (Gorf writes 0 here)
|
||||
if (!arcade) {
|
||||
verbl = val >> 1;
|
||||
refreshall();
|
||||
}
|
||||
break;
|
||||
case 0xb: // OTIR (set palette)
|
||||
var c = cpu.saveState();
|
||||
@@ -628,6 +652,11 @@ export class BallyAstrocade extends BasicScanlineMachine implements AcceptsPaddl
|
||||
cpu : Z80;
|
||||
m; // _BallyAstrocade
|
||||
arcade : boolean;
|
||||
sc01 : any; // SC01 | null = null; // Votrax speech (arcade only)
|
||||
speech : any; // Votrax_Audio | null = null;
|
||||
sc01Cycles = 0; // fractional master clock cycles carried between scanlines
|
||||
speechCount = 0; // phonemes strobed, for the debug view
|
||||
rotate = 0; // display rotation in degrees (arcade; Gorf's monitor is on its side: -90)
|
||||
|
||||
psg: AstrocadeAudio;
|
||||
audioadapter;
|
||||
@@ -648,7 +677,7 @@ export class BallyAstrocade extends BasicScanlineMachine implements AcceptsPaddl
|
||||
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.defaultROMSize = arcade ? 0xc000 : 0x2000;
|
||||
this.ram = new Uint8Array(arcade ? 0x5000 : 0x1000);
|
||||
this.numVisibleScanlines = arcade ? 204 : 102;
|
||||
this.canvasWidth = arcade ? 320 : 160;
|
||||
@@ -658,6 +687,19 @@ export class BallyAstrocade extends BasicScanlineMachine implements AcceptsPaddl
|
||||
this.m = new _BallyAstrocade(arcade);
|
||||
this.m.init(this, this.cpu, this.ram, this.inputs, this.psg);
|
||||
if (arcade) {
|
||||
/*
|
||||
this.speech = new Votrax_Audio(this.psg.master);
|
||||
const host : SC01Host = {
|
||||
phonemeMsec: (code) => this.speech.phonemeDuration(code),
|
||||
speak: (code, msec, scale) => this.speech.speak(code, msec, {
|
||||
pitch: SC01_PITCH[code >> 6] * scale,
|
||||
formantScale: scale,
|
||||
}),
|
||||
inflectionChanged: () => {},
|
||||
readyChanged: () => {},
|
||||
};
|
||||
this.sc01 = new SC01(host);
|
||||
*/
|
||||
this.inputs[0x10] = 0xff; // switches (active low)
|
||||
this.inputs[0x11] = 0xff; // switches (active low)
|
||||
this.inputs[0x12] = 0x00;
|
||||
@@ -697,6 +739,21 @@ export class BallyAstrocade extends BasicScanlineMachine implements AcceptsPaddl
|
||||
|
||||
startScanline() {
|
||||
this.audio && this.audioadapter.generate(this.audio);
|
||||
if (this.sc01) {
|
||||
this.sc01Cycles += this.sc01.clock / (60 * this.numTotalScanlines);
|
||||
const n = this.sc01Cycles | 0;
|
||||
this.sc01Cycles -= n;
|
||||
this.sc01.step(n);
|
||||
}
|
||||
}
|
||||
|
||||
// the data a program put on SO7 (MAME votrax_speech_w): bit 7 clear raises the
|
||||
// inflection, bit 6 picks the 782 kHz clock instead of 756 kHz
|
||||
speechOut(data:number) {
|
||||
const sc = this.sc01;
|
||||
sc.clock = (data & 0x40) ? 782000 : 756000;
|
||||
this.speechCount++;
|
||||
sc.load(data & 0x3f, (data & 0x80) ? 0 : 2);
|
||||
}
|
||||
|
||||
drawScanline() {
|
||||
@@ -733,19 +790,39 @@ export class BallyAstrocade extends BasicScanlineMachine implements AcceptsPaddl
|
||||
reset() {
|
||||
super.reset();
|
||||
this.m.reset();
|
||||
if (this.sc01) {
|
||||
this.sc01.clock = SC01_CLOCK;
|
||||
this.sc01.reset();
|
||||
this.sc01Cycles = 0;
|
||||
this.speech?.stop();
|
||||
}
|
||||
}
|
||||
loadState(state) {
|
||||
this.m.loadState(state);
|
||||
if (this.sc01 && state.sc01) {
|
||||
this.sc01.loadState(state.sc01);
|
||||
this.sc01Cycles = state.sc01Cycles || 0;
|
||||
this.sc01.clock = state.sc01Clock || SC01_CLOCK;
|
||||
}
|
||||
}
|
||||
saveState() {
|
||||
return this.m.saveState();
|
||||
var s = this.m.saveState();
|
||||
if (this.sc01) {
|
||||
s.sc01 = this.sc01.saveState();
|
||||
s.sc01Cycles = this.sc01Cycles;
|
||||
s.sc01Clock = this.sc01.clock;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
getDebugCategories() {
|
||||
return ['CPU','Stack','Astro'];
|
||||
}
|
||||
getDebugInfo(category, state) {
|
||||
switch (category) {
|
||||
case 'Astro': return this.m.toLongString(state);
|
||||
case 'Astro':
|
||||
var s = this.m.toLongString(state);
|
||||
if (this.sc01) s += "\n SC-01: " + this.speechCount + " phonemes, " + (this.sc01.ready ? "ready" : "busy");
|
||||
return s;
|
||||
}
|
||||
}
|
||||
getRasterCanvasPosition() {
|
||||
@@ -753,7 +830,7 @@ export class BallyAstrocade extends BasicScanlineMachine implements AcceptsPaddl
|
||||
}
|
||||
getVideoParams() {
|
||||
if (this.arcade) {
|
||||
return { width: 320, height: 204, rotate: 180 };
|
||||
return { width: 320, height: 204, rotate: this.rotate };
|
||||
} else {
|
||||
return { width: 160, height: 102 };
|
||||
}
|
||||
|
||||
@@ -54,7 +54,12 @@ class BallyAstrocadeBIOSPlatform extends BallyAstrocadePlatform implements Platf
|
||||
|
||||
class BallyArcadePlatform extends BallyAstrocadePlatform implements Platform {
|
||||
|
||||
newMachine() { return new BallyAstrocade(true); }
|
||||
// ?options=rotate%3D-90 turns the screen for vertical-monitor games like Gorf
|
||||
newMachine() {
|
||||
const m = new BallyAstrocade(true);
|
||||
if (this.options.rotate != null) m.rotate = parseFloat(this.options.rotate) || 0;
|
||||
return m;
|
||||
}
|
||||
getPresets() { return ASTROCADE_ARCADE_PRESETS; }
|
||||
|
||||
getMemoryMap = function() { return { main:[
|
||||
|
||||
@@ -119,4 +119,76 @@ describe('Bally Astrocade', function () {
|
||||
assert.equal(8, mach.advanceCPU(), 'arcade=' + arcade); // LD (HL),A = 7 + 1 wait
|
||||
}
|
||||
});
|
||||
// the arcade ROM image is flat: $0000-$3FFF and $8000-$BFFF map straight through
|
||||
it('should map a flat arcade ROM image', function () {
|
||||
var mach = new astrocade.BallyAstrocade(true);
|
||||
var rom = new Uint8Array(0xc000);
|
||||
rom[0x0123] = 0x55;
|
||||
rom[0x8123] = 0x77;
|
||||
rom[0xbfff] = 0x99;
|
||||
mach.loadROM(rom);
|
||||
assert.equal(0x55, mach.m.membus.read(0x0123));
|
||||
assert.equal(0x77, mach.m.membus.read(0x8123));
|
||||
assert.equal(0x99, mach.m.membus.read(0xbfff));
|
||||
});
|
||||
|
||||
// port $78+n: 0/1 source, 2 mode, 3 skip, 4 dest high, 5 width, 6 height (starts the blit)
|
||||
it('should run pattern board blits through the memory bus', function () {
|
||||
var mach = new astrocade.BallyAstrocade(true);
|
||||
mach.loadROM(new Uint8Array(0xc000));
|
||||
var out = (port, v) => mach.m.iobus.write(port, v);
|
||||
mach.write(0xd100, 0xaa); // static RAM, must survive
|
||||
for (var i = 0; i < 8; i++) mach.write(0x4000 + i, 0xff);
|
||||
mach.write(0xd12b, 0x00); // source byte
|
||||
out(0x7a, 0x20); // mode: dest++, source fixed
|
||||
out(0x78, 0x2b); out(0x79, 0xd1);
|
||||
out(0x7b, 0x00); // skip
|
||||
out(0x7c, 0x40); // dest high
|
||||
out(0x7b, 0x01);
|
||||
out(0x7d, 0x03); // 4 bytes wide
|
||||
out(0x7e, 0x00); // 1 row
|
||||
for (var i = 0; i < 4; i++) assert.equal(0, mach.read(0x4000 + i), 'cleared ' + i);
|
||||
assert.equal(0xff, mach.read(0x4004));
|
||||
// height is rows - 1: two rows of two bytes, 'skip' bytes apart
|
||||
mach.write(0x4101, 0xff); mach.write(0x4102, 0xff); mach.write(0x4181, 0xff); mach.write(0x4200, 0xff);
|
||||
out(0x7a, 0x20);
|
||||
out(0x7b, 0x00); out(0x7c, 0x41); // dest $4100
|
||||
out(0x7b, 0x80); // skip
|
||||
out(0x7d, 0x01);
|
||||
out(0x7e, 0x01);
|
||||
assert.equal(0, mach.read(0x4100));
|
||||
assert.equal(0, mach.read(0x4101));
|
||||
assert.equal(0xff, mach.read(0x4102));
|
||||
assert.equal(0, mach.read(0x4180));
|
||||
assert.equal(0, mach.read(0x4181));
|
||||
assert.equal(0xff, mach.read(0x4200), 'no third row');
|
||||
assert.equal(0xaa, mach.read(0xd100));
|
||||
});
|
||||
|
||||
// arcade horizontal drive is wired to the lightpen interrupt (INMOD bit 1 or 4)
|
||||
it('should fire the arcade lightpen interrupt every line when enabled', function () {
|
||||
for (var arcade of [false, true]) {
|
||||
var mach = new astrocade.BallyAstrocade(arcade);
|
||||
mach.connectVideo(new Uint32Array(320 * 262));
|
||||
var n = 0;
|
||||
var cpu = mach.cpu;
|
||||
var orig = cpu.interrupt;
|
||||
cpu.interrupt = function (data) { n++; mach.m.lastvec = data; };
|
||||
mach.m.iobus.write(0x0d, 0xc4);
|
||||
mach.m.iobus.write(0x0e, 0x18);
|
||||
mach.m.iobus.write(0x0f, 0xff); // line interrupt never matches a visible line
|
||||
for (var sl = 0; sl < 10; sl++) mach.m.drawScanline(sl);
|
||||
assert.equal(arcade ? 10 : 0, n, 'arcade=' + arcade);
|
||||
if (arcade) assert.equal(0xc0, mach.m.lastvec);
|
||||
cpu.interrupt = orig;
|
||||
}
|
||||
});
|
||||
|
||||
// arcade screens are upright unless a vertical-monitor game asks (?options=rotate%3D-90)
|
||||
it('should only rotate the arcade screen when asked', function () {
|
||||
var mach = new astrocade.BallyAstrocade(true);
|
||||
assert.equal(0, mach.getVideoParams().rotate);
|
||||
mach.rotate = -90;
|
||||
assert.equal(-90, mach.getVideoParams().rotate);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user