atari8: fixes, faked .xex loading

This commit is contained in:
Steven Hugg 2022-09-02 11:05:03 -05:00
parent c572834c8c
commit 33b2e92396
9 changed files with 207 additions and 60 deletions

View File

@ -850,7 +850,7 @@ export abstract class BaseMachinePlatform<T extends Machine> extends BaseDebugPl
}
loadROM(title, data) {
this.machine.loadROM(data);
this.machine.loadROM(data, title);
this.reset();
}

View File

@ -308,7 +308,7 @@ export abstract class BasicMachine extends BasicHeadlessMachine implements Sampl
abstract sampleRate : number;
overscan : boolean = false;
rotate : number = 0;
aspectRatio : number = 1.0;
aspectRatio : number;
pixels : Uint32Array;
audio : SampledAudioSink;

View File

@ -746,8 +746,9 @@ export function gtia_ntsc_to_rgb(val: number) {
let cr = (val >> 4) & 15;
let lm = val & 15;
let crlv = cr ? color : 0;
let phase = ((cr - 1) * 25 - 38) * (2 * Math.PI / 360);
let y = 256 * bright * Math.pow((lm + 1) / 16, gamma);
if (cr) lm += 1;
let phase = ((cr - 1) * 25 - 25) * (2 * Math.PI / 360);
let y = 256 * bright * Math.pow(lm / 16, gamma);
let i = crlv * Math.cos(phase);
let q = crlv * Math.sin(phase);
var r = y + 0.956 * i + 0.621 * q;

View File

@ -563,7 +563,15 @@ export class AddressHeatMapView extends ProbeBitmapViewBase implements ProjectVi
this.canvas.onclick = (e) => {
var pos = getMousePos(this.canvas, e);
var opaddr = Math.floor(pos.x) + Math.floor(pos.y) * 256;
runToPC(opaddr & 0xffff);
var lastpc = -1;
var runpc = -1;
this.redraw( (op,addr) => {
if (runpc < 0 && lastpc >= 0 && addr == opaddr) {
runpc = lastpc;
}
if (op == ProbeFlags.EXECUTE) lastpc = addr;
});
if (runpc >= 0) runToPC(runpc);
}
}

View File

@ -1,9 +1,9 @@
import { newPOKEYAudio, TssChannelAdapter } from "../common/audio";
import { EmuState, Machine } from "../common/baseplatform";
import { Machine } from "../common/baseplatform";
import { MOS6502 } from "../common/cpu/MOS6502";
import { AcceptsKeyInput, AcceptsPaddleInput, AcceptsROM, BasicScanlineMachine, FrameBased, Probeable, RasterFrameBased, TrapCondition, VideoSource } from "../common/devices";
import { dumpRAM, EmuHalt, KeyFlags, Keys, makeKeycodeMap, newAddressDecoder, newKeyboardHandler } from "../common/emu";
import { hex, lpad, lzgmini, rgb2bgr, safe_extend, stringToByteArray } from "../common/util";
import { AcceptsKeyInput, AcceptsPaddleInput, AcceptsROM, BasicScanlineMachine, FrameBased, Probeable, TrapCondition, VideoSource } from "../common/devices";
import { KeyFlags, Keys, makeKeycodeMap, newAddressDecoder, newKeyboardHandler } from "../common/emu";
import { hex } from "../common/util";
import { BaseWASIMachine } from "../common/wasmplatform";
import { ANTIC, MODE_SHIFT } from "./chips/antic";
import { CONSOL, GTIA, TRIG0 } from "./chips/gtia";
@ -46,12 +46,11 @@ export class Atari800 extends BasicScanlineMachine {
cpuFrequency = 1789773;
numTotalScanlines = 262;
cpuCyclesPerLine = 114;
canvasWidth = 348; // TODO?
canvasWidth = 336;
numVisibleScanlines = 224;
aspectRatio = 240 / 172;
aspectRatio = this.canvasWidth / this.numVisibleScanlines * 0.857;
firstVisibleScanline = 16;
firstVisibleClock = 44 * 2; // ... to 215 * 2
// TODO: for 400/800/5200
firstVisibleClock = (44 - 6) * 2; // ... to 215 * 2
defaultROMSize = 0x8000;
overscan = true;
audioOversample = 4;
@ -59,7 +58,6 @@ export class Atari800 extends BasicScanlineMachine {
cpu: MOS6502;
ram: Uint8Array;
rom: Uint8Array;
bios: Uint8Array;
bus;
audio_pokey;
@ -73,6 +71,7 @@ export class Atari800 extends BasicScanlineMachine {
keycode = 0;
cart_80 = false;
cart_a0 = false;
xexdata = null;
// TODO: save/load vars
constructor() {
@ -106,10 +105,12 @@ export class Atari800 extends BasicScanlineMachine {
[0xd800, 0xffff, 0xffff, (a) => { return this.bios[a - 0xd800]; }],
]),
write: newAddressDecoder([
[0x0000, 0xbfff, 0xffff, (a, v) => { this.ram[a] = v; }],
[0x0000, 0xbffa, 0xffff, (a, v) => { this.ram[a] = v; }],
[0xbffb, 0xbfff, 0xffff, (a, v) => { this.ram[a] = v; this.initCartA(); }],
[0xd000, 0xd0ff, 0x1f, (a, v) => { this.gtia.setReg(a, v); }],
[0xd200, 0xd2ff, 0xf, (a, v) => { this.writePokey(a, v); }],
[0xd400, 0xd4ff, 0xf, (a, v) => { this.antic.setReg(a, v); }],
[0xd500, 0xd5ff, 0xff, (a, v) => { this.writeMapper(a, v); }],
]),
};
}
@ -119,11 +120,11 @@ export class Atari800 extends BasicScanlineMachine {
}
reset() {
console.log(this.saveState());
super.reset();
this.antic.reset();
this.gtia.reset();
this.keycode = 0;
if (this.xexdata) this.cart_a0 = true; // TODO
}
read(a) {
@ -197,8 +198,6 @@ export class Atari800 extends BasicScanlineMachine {
// update GTIA
// get X coordinate within scanline
let xofs = this.antic.h * 4 - this.firstVisibleClock;
// correct for HSCROL
if (this.antic.dliop & 0x10) xofs += (this.antic.regs[4] & 1) << 1;
// GTIA tick functions
let gtiatick1 = () => {
this.gtia.clockPulse1();
@ -209,8 +208,17 @@ export class Atari800 extends BasicScanlineMachine {
this.linergb[xofs++] = this.gtia.rgb;
}
// tick 4 GTIA clocks for each CPU/ANTIC cycle
this.gtia.clockPulse4();
// correct for HSCROL -- bias antic +2, bias gtia -1
if ((this.antic.dliop & 0x10) && (this.antic.regs[4] & 1)) {
xofs += 2;
this.gtia.setBias(-1);
} else {
this.gtia.setBias(0);
}
let bp = MODE_SHIFT[this.antic.mode];
if (bp < 8 || (xofs & 4) == 0) { this.gtia.an = this.antic.shiftout(); }
let odd = this.antic.h & 1;
if (bp < 8 || odd) { this.gtia.an = this.antic.shiftout(); }
gtiatick1();
if (bp == 1) { this.gtia.an = this.antic.shiftout(); }
gtiatick2();
@ -222,12 +230,12 @@ export class Atari800 extends BasicScanlineMachine {
}
loadState(state: any) {
this.loadControlsState(state);
this.cpu.loadState(state.c);
this.ram.set(state.ram);
this.antic.loadState(state.antic);
this.gtia.loadState(state.gtia);
this.irq_pokey.loadState(state.pokey);
this.loadControlsState(state);
this.lastdmabyte = state.lastdmabyte;
this.keycode = state.keycode;
}
@ -240,7 +248,7 @@ export class Atari800 extends BasicScanlineMachine {
pokey: this.irq_pokey.saveState(),
inputs: this.inputs.slice(0),
lastdmabyte: this.lastdmabyte,
keycode: this.keycode, // TODO: inputs?
keycode: this.keycode,
};
}
loadControlsState(state) {
@ -300,9 +308,26 @@ export class Atari800 extends BasicScanlineMachine {
this.probe.logInterrupt(1);
}
loadROM(rom: Uint8Array) {
if (rom.length != 0x2000 && rom.length != 0x4000 && rom.length != 0x8000)
throw new Error("Sorry, this platform can only load 8/16/32 KB cartridges at the moment.");
loadROM(rom: Uint8Array, title: string) {
// XEX file?
if (title && title.toLowerCase().endsWith('.xex') && rom[0] == 0xff && rom[1] == 0xff) {
// TODO: we fake a cartridge
this.xexdata = rom;
let cart = new Uint8Array(0x1000);
cart.set([0x00, 0x01, 0x00, 0x04, 0x00, 0x01], 0xffa);
this.loadCartridge(cart);
} else {
this.loadCartridge(rom);
}
}
loadCartridge(rom: Uint8Array) {
// strip off header
if (rom[0] == 0x43 && rom[1] == 0x41 && rom[2] == 0x52 && rom[3] == 0x54) {
rom = rom.slice(16);
}
if (rom.length != 0x1000 && rom.length != 0x2000 && rom.length != 0x4000 && rom.length != 0x8000)
throw new Error("Sorry, this platform can only load 4/8/16/32 KB cartridges at the moment.");
// TODO: support other than 8 KB carts
// support 4/8/16/32 KB carts
let rom2 = new Uint8Array(0x8000);
@ -310,9 +335,63 @@ export class Atari800 extends BasicScanlineMachine {
rom2.set(rom, i);
}
this.cart_a0 = true; // TODO
if (rom.length == 0x4000) { this.cart_80 = true; }
this.cart_80 = rom.length == 0x4000;
super.loadROM(rom2);
}
writeMapper(addr:number, value:number) {
if (addr == 0xff) {
if (value == 0x80) this.cart_80 = false;
if (value == 0xa0) this.cart_a0 = false;
}
}
// TODO
loadXEX(rom: Uint8Array) {
let ofs = 2;
let cart = this.ram;
let cartofs = 0x100; // stub routine in stack page
while (ofs < rom.length) {
let start = rom[ofs+0] + rom[ofs+1] * 256;
let end = rom[ofs+2] + rom[ofs+3] * 256;
console.log('XEX', ofs, hex(start), hex(end));
ofs += 4;
for (let i=start; i<=end; i++) {
this.ram[i] = rom[ofs++];
}
var runaddr = this.ram[0x2e0] + this.ram[0x2e1]*256;
var initaddr = this.ram[0x2e2] + this.ram[0x2e3]*256;
console.log('XEX run', hex(runaddr), 'init', hex(initaddr));
if (initaddr) {
cart[cartofs++] = 0x20;
cart[cartofs++] = initaddr & 0xff;
cart[cartofs++] = initaddr >> 8;
}
if (ofs > rom.length) throw new Error("Bad .XEX file format");
}
if (runaddr) {
cart[cartofs++] = 0xa9; // lda #$a0
cart[cartofs++] = 0xa0;
cart[cartofs++] = 0x8d; // sta $d5ff (disable cart)
cart[cartofs++] = 0xff;
cart[cartofs++] = 0xd5;
cart[cartofs++] = 0x4c; // jmp runaddr
cart[cartofs++] = runaddr & 0xff;
cart[cartofs++] = runaddr >> 8;
}
}
initCartA() {
//console.log('init', hex(this.cpu.getPC()));
// disable cartridges and load XEX
if (this.cpu.getPC() == 0xf17f) {
if (this.xexdata) {
this.loadXEX(this.xexdata);
}
//this.cart_80 = this.cart_a0 = false;
}
}
}
export class Atari5200 extends Atari800 {

View File

@ -10,8 +10,8 @@ import { hex, lpad, safe_extend } from "../../common/util";
// http://www.atarimuseum.com/videogames/consoles/5200/conv_to_5200.html
// https://www.virtualdub.org/downloads/Altirra%20Hardware%20Reference%20Manual.pdf
const PF_LEFT = [0, 29, 21, 13];
const PF_RIGHT = [0, 29 + 64, 21 + 80, 13 + 96];
const PF_LEFT = [0, 25, 17, 9];
const PF_RIGHT = [0, 25 + 64, 17 + 80, 9 + 96];
const DMACTL = 0;
const CHACTL = 1;
@ -38,8 +38,9 @@ const NMIST_CYCLE = 12;
const NMI_CYCLE = 24;
const WSYNC_CYCLE = 212;
const ANTIC_LEFT = 17; // gtia 34
const ANTIC_RIGHT = 110; // gtia 221
const ANTIC_LEFT = 17 - 4; // gtia 34, 4 cycle delay
const ANTIC_RIGHT = 110 - 4; // gtia 221, 4 cycle delay
const LAST_DMA_H = 105; // last DMA cycle
const MODE_LINES = [0, 0, 8, 10, 8, 16, 8, 16, 8, 4, 4, 2, 1, 2, 1, 1];
// how many bits before DMA clock repeats?
@ -203,9 +204,12 @@ export class ANTIC {
nextScreen(): number {
let b = this.read(this.scanaddr);
this.scanaddr = ((this.scanaddr + 1) & 0xfff) | (this.scanaddr & ~0xfff);
this.incScanAddr();
return b;
}
incScanAddr() {
this.scanaddr = ((this.scanaddr + 1) & 0xfff) | (this.scanaddr & ~0xfff);
}
dlDMAEnabled() { return this.regs[DMACTL] & 0b100000; }
@ -216,10 +220,10 @@ export class ANTIC {
return this.dma_enabled && !this.linesleft;
}
isPlayerDMAEnabled() {
return this.dma_enabled && this.regs[DMACTL] & 0b1000;
return this.regs[DMACTL] & 0b1000;
}
isMissileDMAEnabled() {
return this.dma_enabled && this.regs[DMACTL] & 0b1100;
return this.regs[DMACTL] & 0b1100;
}
clockPulse(): boolean {
@ -280,7 +284,7 @@ export class ANTIC {
}
this.output = 0; // background color (TODO: only for blank lines)
if (this.mode >= 2 && this.period) {
let candma = this.h < 106;
let candma = this.h <= LAST_DMA_H;
this.dmaclock <<= 1;
if (this.dmaclock & (1 << this.period)) {
this.dmaclock |= 1;
@ -289,13 +293,21 @@ export class ANTIC {
if (this.h == this.right) { this.dmaclock &= ~1; this.dmaidx++; }
if (this.dmaclock & 1) {
if (this.mode < 8 && this.yofs == 0) { // only read chars on 1st line
this.linebuf[this.dmaidx] = this.nextScreen(); // read char name
if (candma) {
this.linebuf[this.dmaidx] = this.nextScreen(); // read char name
} else {
this.incScanAddr();
}
did_dma = candma;
}
this.dmaidx++;
} else if (this.dmaclock & 8) {
this.ch = this.linebuf[this.dmaidx - 4 / this.period]; // latch char
this.readBitmapData(); // read bitmap
if (candma) {
this.readBitmapData(); // read bitmap
} else {
if (this.mode >= 8) this.incScanAddr();
}
did_dma = candma;
}
this.output = this.h >= this.left + 3 && this.h <= this.right + 2 ? 4 : 0;

View File

@ -33,6 +33,8 @@ const P0PL = 0xc;
export const TRIG0 = 0x10;
export const CONSOL = 0x1f;
const HOFFSET = -9; // bias to account for antic->gtia delay
const PRIOR_TABLE : number[] = [
0,1,2,3, 7,7,7,7, 8,8,8,8, 4,5,6,7, // 0001 - 0
0,1,2,3, 7,7,7,7, 8,8,8,8, 4,5,6,7, // 0001
@ -52,6 +54,13 @@ const PRIOR_TABLE : number[] = [
2,3,4,5, 7,7,7,7, 8,8,8,8, 0,1,6,7, // 1000
];
const MODE_9_LOOKUP = [
COLPM0+0, COLPM0+1, COLPM0+2, COLPM0+3,
COLPF0+0, COLPF0+1, COLPF0+2, COLPF0+3,
COLBK, COLBK, COLBK, COLBK,
COLPF0+0, COLPF0+1, COLPF0+2, COLPF0+3,
]
export class GTIA {
regs = new Uint8Array(0x20);
readregs = new Uint8Array(0x20);
@ -62,6 +71,9 @@ export class GTIA {
an = 0;
rgb = 0;
pmcol = 0;
gtiacol = 0;
gtiacol2 = 0;
hbias = HOFFSET;
reset() {
this.regs.fill(0);
@ -77,6 +89,11 @@ export class GTIA {
}
setReg(a: number, v: number) {
switch (a) {
case COLPM0: case COLPM0+1: case COLPM0+2: case COLPM0+3:
case COLPF0: case COLPF0+1: case COLPF0+2: case COLPF0+3:
case COLBK:
v &= 0xfe; // bit 0 unused in color regs
break;
case HITCLR:
this.readregs.fill(0, 0, 16);
return;
@ -93,6 +110,9 @@ export class GTIA {
sync() {
this.count = 0;
}
setBias(b: number) {
this.hbias = HOFFSET + b;
}
updateGfx(h: number, data: number) {
switch (h) {
case 0:
@ -104,32 +124,32 @@ export class GTIA {
}
}
getPlayfieldColor(): number {
switch (this.an) {
// which GTIA mode?
switch (this.regs[PRIOR] >> 6) {
// normal mode
case 0:
return COLBK;
case 4: case 5: case 6: case 7:
return COLPF0 + this.an - 4;
case 8:
// combine PF2 hue and PF1 luminance
return (this.regs[COLPF2] & 0xf0) | (this.regs[COLPF1] & 0x0f) | 0x100;
switch (this.an) {
case 0:
return COLBK;
case 4: case 5: case 6: case 7:
return COLPF0 + this.an - 4;
case 8:
// combine PF2 hue and PF1 luminance
return (this.regs[COLPF2] & 0xf0) | (this.regs[COLPF1] & 0x0f) | 0x100;
}
break;
// mode 9 -- 16 luminances
case 1:
return (this.regs[COLBK] & 0xf0) | (this.gtiacol & 0xf) | 0x100;
// mode 10 -- 9 colors from registers
case 2:
return MODE_9_LOOKUP[this.gtiacol];
// mode 11 -- 16 hues
case 3:
return (this.regs[COLBK] & 0xf) | (this.gtiacol << 4) | 0x100;
}
return 0x100; // black
}
clockPulse1(): void {
this.processPlayerMissile();
this.clockPulse2();
this.count++;
}
clockPulse2(): void {
var col: number;
if (this.pmcol >= 0) {
col = this.pmcol;
} else {
let pf = this.getPlayfieldColor();
col = pf & 0x100 ? pf & 0xff : this.regs[pf];
}
this.rgb = COLORS_RGBA[col];
}
anySpriteActive() {
return this.shiftregs[0] | this.shiftregs[1] | this.shiftregs[2]
| this.shiftregs[3] | this.shiftregs[4] | this.shiftregs[5]
@ -144,6 +164,8 @@ export class GTIA {
this.pmcol = -1;
return;
}
// TODO: multiple color player enable
// TODO: gtia, hi-res mode collisions
// compute gfx and collisions for players/missiles
let priobias = (this.regs[PRIOR] & 15) << 4; // TODO
let topprio = PRIOR_TABLE[(this.an & 7) + 8 + priobias];
@ -175,7 +197,7 @@ export class GTIA {
this.readregs[M0PF + i] |= 1 << pfset;
}
this.readregs[M0PL + i] |= ppmask;
let prio = PRIOR_TABLE[i + 4 + priobias];
let prio = PRIOR_TABLE[i + priobias];
if (prio < topprio) {
topobj = i + 4;
topprio = prio;
@ -194,7 +216,7 @@ export class GTIA {
shiftObject(i: number) {
let bit = (this.shiftregs[i] & 0x80000000) != 0;
this.shiftregs[i] <<= 1;
if (this.regs[HPOSP0 + i] - 1 == this.count) {
if (this.regs[HPOSP0 + i] + this.hbias == this.count) {
this.triggerObject(i);
}
return bit;
@ -221,6 +243,30 @@ export class GTIA {
this.shiftregs[i] = data;
}
clockPulse1(): void {
this.processPlayerMissile();
this.clockPulse2();
this.count++;
}
clockPulse2(): void {
var col: number;
if (this.pmcol >= 0) {
col = this.pmcol;
} else {
let pf = this.getPlayfieldColor();
col = pf & 0x100 ? pf & 0xff : this.regs[pf];
}
this.rgb = COLORS_RGBA[col];
// TODO: hires modes return 8, so other modes wont work
this.gtiacol2 = (this.gtiacol2 << 1) | (this.an >> 3);
}
clockPulse4() {
// latch GTIA buffer
this.gtiacol = this.gtiacol2 & 15;
}
static stateToLongString(state): string {
let s = ''
s += `X: ${lpad(state.count, 3)} ANTIC: ${hex(state.an, 1)} PM: ${hex(state.pmcol, 3)}\n`;

View File

@ -195,6 +195,7 @@ class Atari5200Platform extends Atari800Platform {
///
PLATFORMS['atari8-800.xlmame'] = Atari800MAMEPlatform
PLATFORMS['atari8-800xl.mame'] = Atari800MAMEPlatform // for dithertron
PLATFORMS['atari8-5200.mame'] = Atari5200MAMEPlatform
PLATFORMS['atari8-800.xlwasm'] = Atari800WASMPlatform
PLATFORMS['atari8-800'] = Atari800Platform

View File

@ -367,7 +367,7 @@ describe('Platform Replay', () => {
});
});
it('Should run atari5200', async () => {
await testPlatform('atari8-5200', 'acid5200.rom', 1000, (platform, frameno) => {
await testPlatform('atari8-5200', 'acid5200.rom', 1100, (platform, frameno) => {
if (frameno == 999) {
let s = '';
for (let i=0; i<40; i++) {