diff --git a/css/ui.css b/css/ui.css index b7cc9027..9dcf5e12 100644 --- a/css/ui.css +++ b/css/ui.css @@ -4,9 +4,6 @@ .gutter-offset { width: 3em; } -.CodeMirror-gutter-elt:hover { - text-decoration: underline; -} .gutter-bytes { width: 6em; } @@ -17,6 +14,10 @@ width: 1em; cursor: cell; } +.CodeMirror-gutter-elt:hover { + text-decoration: underline; + cursor: cell; +} .currentpc-span { background-color: #7e2a70; } @@ -659,6 +660,13 @@ div.asset_toolbar { padding-left: 0.5em; padding-right: 0.5em; } +.transcript-style-16 { /* alternate */ + color: #6666ff; + background-color: #eeeeff; +} +.transcript-style-32 { /* alternate 2 */ + background-color: #eee; +} .transcript-reverse { background-color: #666; color: #ddd; diff --git a/doc/notes.txt b/doc/notes.txt index 2589edcc..c887ce72 100644 --- a/doc/notes.txt +++ b/doc/notes.txt @@ -188,6 +188,7 @@ TODO: - no import in workers - copy to gen/ directory (allowJs is weird) - ca65 line numbers are not aligned with source code + - neither are DASM when you change comments - segments not read properly - Debug Browser - more stuff like 7800 display lists @@ -197,6 +198,7 @@ TODO: Probing - probe log doesn't start @ reset +Debug, play then halt cpu doesn't highlight final line WEB WORKER FORMAT diff --git a/src/common/baseplatform.ts b/src/common/baseplatform.ts index 7bca3418..74926372 100644 --- a/src/common/baseplatform.ts +++ b/src/common/baseplatform.ts @@ -135,6 +135,9 @@ export interface Platform { stopProbing?() : void; isBlocked?() : boolean; // is blocked, halted, or waiting for input? + + readFile?(path: string) : FileData; + writeFile?(path: string, data: FileData) : boolean; } export interface Preset { @@ -186,6 +189,7 @@ export interface EmuRecorder { export abstract class BasePlatform { recorder : EmuRecorder = null; debugSymbols : DebugSymbols; + internalFiles : {[path:string] : FileData} = {}; abstract loadState(state : EmuState) : void; abstract saveState() : EmuState; @@ -208,6 +212,13 @@ export abstract class BasePlatform { getDebugTree() : {} { return this.saveState(); } + readFile(path: string) : FileData { + return this.internalFiles[path]; + } + writeFile(path: string, data: FileData) : boolean { + this.internalFiles[path] = data; + return true; + } } export abstract class BaseDebugPlatform extends BasePlatform { @@ -283,7 +294,7 @@ export abstract class BaseDebugPlatform extends BasePlatform { } pollControls() { } - nextFrame(novideo : boolean) { + nextFrame(novideo : boolean) : number { this.pollControls(); this.updateRecorder(); this.preFrame(); @@ -1021,7 +1032,7 @@ export function lookupSymbol(platform:Platform, addr:number, extra:boolean) { /// new Machine platform adapters -import { Bus, Resettable, FrameBased, VideoSource, SampledAudioSource, AcceptsROM, AcceptsBIOS, AcceptsKeyInput, SavesState, SavesInputState, HasCPU, TrapCondition, CPU } from "./devices"; +import { Bus, Resettable, FrameBased, VideoSource, SampledAudioSource, AcceptsROM, AcceptsBIOS, AcceptsKeyInput, SavesState, SavesInputState, HasCPU, TrapCondition, CPU, HasSerialIO, SerialIOInterface } from "./devices"; import { Probeable, RasterFrameBased, AcceptsPaddleInput, SampledAudioSink, ProbeAll, NullProbe } from "./devices"; import { SampledAudio } from "./audio"; import { ProbeRecorder } from "./recorder"; @@ -1050,6 +1061,9 @@ export function hasProbe(arg:any): arg is Probeable { export function hasBIOS(arg:any): arg is AcceptsBIOS { return typeof arg.loadBIOS == 'function'; } +export function hasSerialIO(arg:any): arg is HasSerialIO { + return typeof arg.connectSerialIO === 'function'; +} export abstract class BaseMachinePlatform extends BaseDebugPlatform implements Platform { machine : T; @@ -1123,6 +1137,9 @@ export abstract class BaseMachinePlatform extends BaseDebugPl m.loadBIOS(data, title); }; } + if (hasSerialIO(m) && this.serialIOInterface) { + m.connectSerialIO(this.serialIOInterface); + } } loadROM(title, data) { @@ -1130,7 +1147,9 @@ export abstract class BaseMachinePlatform extends BaseDebugPl this.reset(); } - loadBIOS; // only set if hasBIOS() is true + loadBIOS : (title, data) => void; // only set if hasBIOS() is true + + serialIOInterface : SerialIOInterface; // set if hasSerialIO() is true pollControls() { this.poller && this.poller.poll(); diff --git a/src/common/devices.ts b/src/common/devices.ts index 63ce23bd..760cfce4 100644 --- a/src/common/devices.ts +++ b/src/common/devices.ts @@ -107,6 +107,26 @@ export interface AcceptsPaddleInput { setPaddleInput(controller:number, value:number) : void; } +// SERIAL I/O + +export interface SerialIOInterface { + // from machine to platform + clearToSend() : boolean; + sendByte(b : number); + // from platform to machine + byteAvailable() : boolean; + recvByte() : number; + // implement these too + reset() : void; + advance(clocks: number) : void; +} + +export interface HasSerialIO { + connectSerialIO(serial: SerialIOInterface); +} + +/// PROFILER + export interface Probeable { connectProbe(probe: ProbeAll) : void; } @@ -118,8 +138,6 @@ export function xorshift32(x : number) : number { return x; } -/// PROFILER - export interface ProbeTime { logClocks(clocks:number); logNewScanline(); diff --git a/src/common/teletype.ts b/src/common/teletype.ts index b3ebc53f..05ba9d09 100644 --- a/src/common/teletype.ts +++ b/src/common/teletype.ts @@ -159,6 +159,30 @@ export class TeleType { $(this.page).css('font-size', charwidth + 'px'); this.scrollToBottom(); } + saveState() { + return { + curstyle: this.curstyle, + reverse: this.reverse, + col: this.col, + row: this.row, + ncharsout : this.ncharsout, + lines: this.lines.map((line) => line.cloneNode(true)), + } + } + loadState(state) { + console.log(state); + this.curstyle = state.curstyle; + this.reverse = state.reverse; + this.col = state.col; + this.row = state.row; + this.ncharsout = state.ncharsout; + $(this.page).empty(); + for (var i=0; i { return true; }); // break immediately } else { - platform.reset(); + resetPlatform(); _resume(); } if (wasRecording) _enableRecording(); @@ -1930,8 +1934,9 @@ function globalErrorHandler(msgevent) { } else { var err = msgevent.error || msgevent.reason; if (err != null && err instanceof EmuHalt) { - msg = (err && err.message) || msg; - showExceptionAsError(err, msg); + setDebugButtonState("pause", "stopped"); + emulationHalted(err); + // TODO: reset platform? } } } @@ -2294,6 +2299,23 @@ redirectToHTTPS(); //// ELECTRON STUFF +export function setTestInput(path: string, data: FileData) { + platform.writeFile(path, data); +} + +export function getTestOutput(path: string) : FileData { + return platform.readFile(path); +} + +export function getSaveState() { + return platform.saveState(); +} + +export function emulationHalted(err: EmuHalt) { + var msg = (err && err.message) || msg; + showExceptionAsError(err, msg); +} + // get remote file from local fs declare var getWorkspaceFile, putWorkspaceFile; export function getElectronFile(url:string, success:(text:string|Uint8Array)=>void, datatype:'text'|'arraybuffer') { diff --git a/src/machine/devel.ts b/src/machine/devel.ts index 60b5fc7c..b2833437 100644 --- a/src/machine/devel.ts +++ b/src/machine/devel.ts @@ -1,30 +1,41 @@ import { MOS6502 } from "../common/cpu/MOS6502"; -import { BasicHeadlessMachine } from "../common/devices"; +import { BasicHeadlessMachine, HasSerialIO, SerialIOInterface } from "../common/devices"; import { padBytes, newAddressDecoder } from "../common/emu"; // TODO -export class Devel6502 extends BasicHeadlessMachine { +const INPUT_HALTED = 31; + +export class Devel6502 extends BasicHeadlessMachine implements HasSerialIO { + cpuFrequency = 1000000; defaultROMSize = 0x8000; cpu = new MOS6502(); ram = new Uint8Array(0x4000); rom : Uint8Array; - - digits = []; + serial : SerialIOInterface; constructor() { super(); this.connectCPUMemoryBus(this); } + + connectSerialIO(serial: SerialIOInterface) { + this.serial = serial; + } read = newAddressDecoder([ [0x0000, 0x3fff, 0x3fff, (a) => { return this.ram[a]; }], + [0x4000, 0x4000, 0xffff, (a) => { return this.serial.byteAvailable() ? 0x80 : 0 }], + [0x4001, 0x4001, 0xffff, (a) => { return this.serial.recvByte() }], + [0x4002, 0x4002, 0xffff, (a) => { return this.serial.clearToSend() ? 0x80 : 0 }], [0x8000, 0xffff, 0x7fff, (a) => { return this.rom && this.rom[a]; }], ]); write = newAddressDecoder([ [0x0000, 0x3fff, 0x3fff, (a,v) => { this.ram[a] = v; }], + [0x4003, 0x4003, 0xffff, (a,v) => { return this.serial.sendByte(v) }], + [0x400f, 0x400f, 0xffff, (a,v) => { this.inputs[INPUT_HALTED] = 1; }], ]); readConst(a:number) : number { @@ -39,5 +50,20 @@ export class Devel6502 extends BasicHeadlessMachine { } return clock; } + + advanceCPU() { + if (this.isHalted()) return 1; + var n = super.advanceCPU(); + if (this.serial) this.serial.advance(n); + return n; + } + + reset() { + this.inputs[INPUT_HALTED] = 0; + super.reset(); + if (this.serial) this.serial.reset(); + } + + isHalted() { return this.inputs[INPUT_HALTED] != 0; } } diff --git a/src/platform/devel.ts b/src/platform/devel.ts index 1a70a420..83eeaabf 100644 --- a/src/platform/devel.ts +++ b/src/platform/devel.ts @@ -1,15 +1,169 @@ -import { Platform, getOpcodeMetadata_6502, getToolForFilename_6502 } from "../common/baseplatform"; -import { PLATFORMS } from "../common/emu"; +import { Platform } from "../common/baseplatform"; +import { EmuHalt, PLATFORMS } from "../common/emu"; import { Devel6502 } from "../machine/devel"; import { Base6502MachinePlatform } from "../common/baseplatform"; +import { SerialIOInterface } from "../common/devices"; +import { convertDataToUint8Array, hex } from "../common/util"; +import { TeleType } from "../common/teletype"; +import { emulationHalted, loadScript } from "../ide/ui"; var DEVEL_6502_PRESETS = [ {id:'hello.dasm', name:'Hello World (ASM)'}, ]; +class SerialInOutViewer { + div : HTMLElement; + tty : TeleType; + + constructor(div: HTMLElement) { + div.style.overflowY = 'auto'; + var gameport = $('
').appendTo(div); + var windowport = $('
').appendTo(gameport); + this.div = windowport[0]; + } + start() { + this.tty = new TeleType(this.div, false); + //this.tty.ncols = 40; + } + reset() { + this.tty.clear(); + } + saveState() { + return this.tty.saveState(); + } + loadState(state) { + this.tty.loadState(state); + } +} + +function byteToASCII(b: number) : string { + if (b == 10) return ''; + if (b < 32) + return String.fromCharCode(b + 0x2400); + else + return String.fromCharCode(b); +} + +export class SerialTestHarness implements SerialIOInterface { + + viewer : SerialInOutViewer; + bufferedRead : boolean = true; + cyclesPerByte = 1000000/(57600/8); // 138.88888 cycles + maxOutputBytes = 4096; + inputBytes : Uint8Array; + + outputBytes : number[]; + inputIndex : number; + clk : number; + bufin : string; + + clearToSend(): boolean { + return this.outputBytes.length < this.maxOutputBytes; + } + sendByte(b: number) { + if (this.clearToSend()) { + this.outputBytes.push(b); + this.viewer.tty.addtext(byteToASCII(b), 2|32); + if (b == 10) this.viewer.tty.newline(); + if (!this.clearToSend()) { + this.viewer.tty.newline(); + this.viewer.tty.addtext("⚠️ OUTPUT BUFFER FULL ⚠️", 4); + } + } + } + byteAvailable(): boolean { + return this.readIndex() > this.inputIndex; + } + recvByte(): number { + var index = this.readIndex(); + this.inputIndex = index; + var b = this.inputBytes[index] | 0; + //this.bufin += byteToASCII(b); + this.viewer.tty.addtext(byteToASCII(b), 2|16); + if (b == 10) this.viewer.tty.newline(); + return b; + } + readIndex(): number { + return this.bufferedRead ? (this.inputIndex+1) : Math.floor(this.clk / this.cyclesPerByte); + } + + reset() { + this.inputIndex = -1; + this.clk = 0; + this.outputBytes = []; + this.bufin = ''; + } + + advance(clocks: number) { + this.clk += clocks; + } + + saveState() { + return { + clk: this.clk, + idx: this.inputIndex, + out: this.outputBytes.slice() + } + } + loadState(state) { + this.clk = state.clk; + this.inputIndex = state.idx; + this.outputBytes = state.out.slice(); + } +} + class Devel6502Platform extends Base6502MachinePlatform implements Platform { + serial : SerialTestHarness; + serview : SerialInOutViewer; + + constructor(mainElement: HTMLElement) { + super(mainElement); + this.serview = new SerialInOutViewer(mainElement); + } + + async start() { + super.start(); + await loadScript('./gen/common/teletype.js'); + this.serial = new SerialTestHarness(); + this.serial.viewer = this.serview; + this.serview.start(); + this.machine.connectSerialIO(this.serial); + } + + reset() { + this.serial.inputBytes = convertDataToUint8Array(this.internalFiles['serialin.dat']); + super.reset(); + this.serview.reset(); + } + + isBlocked() { + return this.machine.isHalted(); + } + + advance(novideo: boolean) { + if (this.isBlocked()) { + this.internalFiles['serialout.dat'] = new Uint8Array(this.serial.outputBytes); + throw new EmuHalt(null); // TODO: throws nasty exception + } + return super.advance(novideo); + } + + saveState() { + var state : any = super.saveState(); + state.serial = this.serial.saveState(); + state.serview = this.serview.saveState(); + return state; + } + + loadState(state) { + super.loadState(state); + this.serial.loadState(state.serial); + this.serview.loadState(state.serview); + // TODO: reload tty UI + } + newMachine() { return new Devel6502(); } getPresets() { return DEVEL_6502_PRESETS; } getDefaultExtension() { return ".dasm"; }; diff --git a/src/tools/runmachine.ts b/src/tools/runmachine.ts index e3daa4a9..3e4a1ee1 100644 --- a/src/tools/runmachine.ts +++ b/src/tools/runmachine.ts @@ -1,15 +1,67 @@ -import { hasAudio, hasVideo, Machine } from "../common/baseplatform"; -import { SampledAudioSink } from "../common/devices"; +import { hasAudio, hasSerialIO, hasVideo, Machine } from "../common/baseplatform"; +import { SampledAudioSink, SerialIOInterface } from "../common/devices"; + +global.atob = require('atob'); +global.btoa = require('btoa'); class NullAudio implements SampledAudioSink { feedSample(value: number, count: number): void { - } + } } -class MachineRunner { +// TODO: merge with platform +class SerialTestHarness implements SerialIOInterface { + + bufferedRead: boolean = true; + cyclesPerByte = 1000000 / (57600 / 8); // 138.88888 cycles + maxOutputBytes = 4096; + inputBytes: Uint8Array; + + outputBytes: number[]; + inputIndex: number; + clk: number; + bufin: string; + + clearToSend(): boolean { + return this.outputBytes.length < this.maxOutputBytes; + } + sendByte(b: number) { + if (this.clearToSend()) { + this.outputBytes.push(b); + } + } + byteAvailable(): boolean { + return this.readIndex() > this.inputIndex; + } + recvByte(): number { + var index = this.readIndex(); + this.inputIndex = index; + var b = this.inputBytes[index] | 0; + return b; + } + readIndex(): number { + return this.bufferedRead ? (this.inputIndex + 1) : Math.floor(this.clk / this.cyclesPerByte); + } + + reset() { + this.inputIndex = -1; + this.clk = 0; + this.outputBytes = []; + this.bufin = ''; + } + + advance(clocks: number) { + this.clk += clocks; + } +} + +/// + +export class MachineRunner { machine: Machine; pixels: Uint32Array; + serial: SerialTestHarness; constructor(machine: Machine) { this.machine = machine; @@ -23,6 +75,10 @@ class MachineRunner { if (hasAudio(this.machine)) { this.machine.connectAudio(new NullAudio()); } + if (hasSerialIO(this.machine)) { + this.serial = new SerialTestHarness(); + this.machine.connectSerialIO(this.serial); + } this.machine.reset(); } run() { @@ -30,8 +86,8 @@ class MachineRunner { } } -async function loadMachine(modname: string, clsname: string) : Promise { - var mod = await import('../machine/'+modname); +async function loadMachine(modname: string, clsname: string): Promise { + var mod = await import('../machine/' + modname); var cls = mod[clsname]; var machine = new cls(); return machine; @@ -45,7 +101,6 @@ async function runMachine() { console.log(runner.machine.saveState()); } -global.atob = require('atob'); -global.btoa = require('btoa'); -runMachine(); - +if (require.main === module) { + runMachine(); +} diff --git a/src/worker/workermain.ts b/src/worker/workermain.ts index b168ea4c..b1a3608a 100644 --- a/src/worker/workermain.ts +++ b/src/worker/workermain.ts @@ -267,6 +267,11 @@ var PLATFORM_PARAMS = { extra_link_args: ['crt0-zx.rel'], extra_link_files: ['crt0-zx.rel', 'crt0-zx.lst'], }, + 'devel-6502': { + cfgfile: 'devel-6502.cfg', + libargs: ['crt0.o', 'sim6502.lib'], + extra_link_files: ['crt0.o', 'devel-6502.cfg'], + }, }; PLATFORM_PARAMS['sms-sms-libcv'] = PLATFORM_PARAMS['sms-sg1000-libcv']; @@ -532,6 +537,7 @@ function setupFS(FS, name:string) { var WORKERFS = FS.filesystems['WORKERFS']; if (name === '65-vector') name = '65-sim6502'; // TODO if (name === '65-atari7800') name = '65-sim6502'; // TODO + if (name === '65-devel') name = '65-sim6502'; // TODO if (!fsMeta[name]) throw Error("No filesystem for '" + name + "'"); FS.mkdir('/share'); FS.mount(WORKERFS, { @@ -2712,6 +2718,8 @@ var TOOL_PRELOADFS = { 'ca65-vector': '65-sim6502', 'cc65-atari7800': '65-sim6502', 'ca65-atari7800': '65-sim6502', + 'cc65-devel': '65-sim6502', + 'ca65-devel': '65-sim6502', 'sdasz80': 'sdcc', 'sdcc': 'sdcc', 'sccz80': 'sccz80', @@ -2788,6 +2796,8 @@ function handleMessage(data : WorkerMessage) : WorkerResult { // preload file system if (data.preload) { var fs = TOOL_PRELOADFS[data.preload]; + if (!fs && data.platform) + fs = TOOL_PRELOADFS[data.preload+'-'+getBasePlatform(data.platform)]; if (!fs && data.platform) fs = TOOL_PRELOADFS[data.preload+'-'+getRootBasePlatform(data.platform)]; if (fs && !fsMeta[fs])