replay slider for some platforms; fixed williams audio

This commit is contained in:
Steven Hugg 2018-08-21 23:39:34 -04:00
parent 2b41164b66
commit b386d2e87a
11 changed files with 232 additions and 128 deletions

View File

@ -122,6 +122,9 @@ div.mem_info a.selected {
.btn_stopped {
color: #ff9933;
}
.btn_recording {
color: #ff3333;
}
.seg_code { color: #ff9966; }
.seg_data { color: #66ff66; }
.seg_stack { color: #ffff66; }
@ -285,3 +288,12 @@ canvas.pixelated {
content: '\2713';
font-weight: 400;
}
div.replaydiv {
position:absolute;
padding:20px;
left:50%;
bottom:0;
width:50%;
background-color: #666;
margin-top: 20px auto 0;
}

View File

@ -138,6 +138,9 @@ if (window.location.host.endsWith('8bitworkshop.com')) {
<button id="dbg_profile" type="submit" title="Show Profile" style="display:none"><span class="glyphicon glyphicon-stats" aria-hidden="true"></span></button>
<button id="dbg_bitmap" type="submit" title="Edit Bitmap"><span class="glyphicon glyphicon-camera" aria-hidden="true"></span></button>
</span>
<span class="btn_group view_group" id="replay_bar" style="display:none">
<button id="dbg_record" type="submit" title="Toggle Replay Buffer"><span class="glyphicon glyphicon-record" aria-hidden="true"></span></button>
</span>
<span class="dropdown" style="float:right">
<a class="btn btn-secondary dropdown-toggle" id="booksMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
GET BOOKS <span class="caret"></span>
@ -174,6 +177,9 @@ if (window.location.host.endsWith('8bitworkshop.com')) {
</div>
<div id="mem_info" class="mem_info" style="display:none">
</div>
<div id="replaydiv" class="replaydiv" style="display:none">
<input type="range" min="0" max="0" value="0" class="slider" id="replayslider">
</div>
</div>
<!--
<div class="twitbtn">
@ -273,6 +279,7 @@ function require(modname) {
<script src="gen/project.js"></script>
<script src="gen/windows.js"></script>
<script src="gen/views.js"></script>
<script src="gen/recorder.js"></script>
<script src="gen/ui.js"></script>
<!-- <script src="src/audio/votrax.js"></script> -->

View File

@ -25,6 +25,12 @@
****************************************************************************/
var window = {};
var exports = {};
function require(modname) {
if (modname == 'jquery') return $;
else if (modname.startsWith('.')) return exports;
else { console.log("Unknown require()", modname); return exports; }
}
importScripts("../../gen/emu.js");
importScripts("../cpu/z80.js");

View File

@ -12,15 +12,15 @@ export interface OpcodeMetadata {
opcode: number;
}
interface CpuState {
export interface CpuState {
PC:number, T?:number, o?:number,/*opcode*/
SP?:number
/*
A:number, X:number, Y:number, SP:number, R:boolean,
N,V,D,Z,C:boolean*/
};
interface EmuState {c:CpuState, b?:number[]};
type DisasmLine = {line:string, nbytes:number};
export interface EmuState {c:CpuState, b?:number[]};
export type DisasmLine = {line:string, nbytes:number};
export interface Platform {
start() : void;
@ -57,6 +57,9 @@ export interface Platform {
getDebugCategories() : string[];
getDebugInfo(category:string, state:EmuState) : string;
setRecorder?(recorder : EmuRecorder) : void;
advance?(novideo? : boolean) : void;
}
export interface Preset {
@ -75,6 +78,11 @@ type DebugCondition = () => boolean;
type DebugEvalCondition = (c:CpuState) => boolean;
type BreakpointCallback = (EmuState) => void;
export interface EmuRecorder {
frameRequested() : boolean;
recordFrame(platform : Platform, state : EmuState);
}
/////
abstract class BaseDebugPlatform {
@ -84,6 +92,7 @@ abstract class BaseDebugPlatform {
debugBreakState : EmuState = null;
debugTargetClock : number = 0;
debugClock : number = 0;
recorder : EmuRecorder = null;
abstract getCPUState() : CpuState;
abstract saveState() : EmuState;
@ -117,6 +126,15 @@ abstract class BaseDebugPlatform {
this.debugBreakState = null;
this.resume();
}
setRecorder?(recorder : EmuRecorder) : void {
this.recorder = recorder;
}
updateRecorder() {
// are we recording and do we need to save a frame?
if (this.recorder && (<Platform><any>this).isRunning() && this.recorder.frameRequested()) {
this.recorder.recordFrame(<Platform><any>this, this.saveState());
}
}
}
abstract class BaseFrameBasedPlatform extends BaseDebugPlatform {
@ -128,11 +146,14 @@ abstract class BaseFrameBasedPlatform extends BaseDebugPlatform {
}
}
restartDebugState() {
if (this.debugCondition && !this.debugBreakState) {
// save state every frame and rewind debug clocks
var debugging = this.debugCondition && !this.debugBreakState;
if (debugging) {
this.debugSavedState = this.saveState();
this.debugTargetClock -= this.debugClock;
this.debugClock = 0;
}
this.updateRecorder();
}
breakpointHit(targetClock : number) {
this.debugTargetClock = targetClock;
@ -389,6 +410,7 @@ export abstract class BaseZ80Platform extends BaseDebugPlatform {
this.debugSavedState.c.T = 0;
this.loadState(this.debugSavedState);
}
this.updateRecorder();
}
breakpointHit(targetClock : number) {
this.debugTargetClock = targetClock;

View File

@ -48,6 +48,7 @@ var Apple2Platform = function(mainElement) {
// value to add when reading & writing each of these banks
// bank 1 is E000-FFFF, bank 2 is D000-DFFF
var bank2rdoffset=0, bank2wroffset=0;
var grparams;
this.getPresets = function() {
return APPLE2_PRESETS;
@ -177,37 +178,40 @@ var Apple2Platform = function(mainElement) {
}
});
var idata = video.getFrameData();
var grparams = {dirty:grdirty, grswitch:grswitch, mem:ram.mem};
grparams = {dirty:grdirty, grswitch:grswitch, mem:ram.mem};
ap2disp = new Apple2Display(idata, grparams);
var colors = [0xffff0000, 0xff00ff00];
timer = new AnimationTimer(60, function() {
// 262.5 scanlines per frame
var clock = 0;
var debugCond = self.getDebugCallback();
var rendered = false;
for (var sl=0; sl<262; sl++) {
for (var i=0; i<cpuCyclesPerLine; i++) {
if (debugCond && debugCond()) {
grparams.dirty = grdirty;
grparams.grswitch = grswitch;
ap2disp.updateScreen();
debugCond = null;
rendered = true;
}
clock++;
cpu.clockPulse();
audio.feedSample(soundstate, 1);
timer = new AnimationTimer(60, this.advance.bind(this));
}
this.advance = function(novideo : boolean) {
// 262.5 scanlines per frame
var clock = 0;
var debugCond = this.getDebugCallback();
var rendered = false;
for (var sl=0; sl<262; sl++) {
for (var i=0; i<cpuCyclesPerLine; i++) {
if (debugCond && debugCond()) {
grparams.dirty = grdirty;
grparams.grswitch = grswitch;
ap2disp.updateScreen();
debugCond = null;
rendered = true;
}
clock++;
cpu.clockPulse();
audio.feedSample(soundstate, 1);
}
}
if (!novideo) {
if (!rendered) {
grparams.dirty = grdirty;
grparams.grswitch = grswitch;
ap2disp.updateScreen();
}
video.updateFrame();
soundstate = 0; // to prevent clicking
self.restartDebugState(); // reset debug start state
});
}
//soundstate = 0; // to prevent clicking
this.restartDebugState(); // reset debug start state
}
function doLanguageCardIO(address, value)

View File

@ -294,35 +294,36 @@ var GalaxianPlatform = function(mainElement, options) {
var idata = video.getFrameData();
setKeyboardFromMap(video, inputs, keyMap);
pixels = video.getFrameData();
timer = new AnimationTimer(60, function() {
if (!self.isRunning())
return;
var debugCond = self.getDebugCallback();
var targetTstates = cpu.getTstates();
// TODO: get raster position
for (var sl=0; sl<scanlinesPerFrame; sl++) {
drawScanline(pixels, sl);
targetTstates += cpuCyclesPerLine;
if (debugCond) {
while (cpu.getTstates() < targetTstates) {
if (debugCond && debugCond()) { debugCond = null; }
cpu.runFrame(cpu.getTstates() + 1);
}
} else {
cpu.runFrame(targetTstates);
timer = new AnimationTimer(60, this.advance.bind(this));
}
this.advance = function(novideo : boolean) {
var debugCond = this.getDebugCallback();
var targetTstates = cpu.getTstates();
for (var sl=0; sl<scanlinesPerFrame; sl++) {
if (!novideo) {
drawScanline(pixels, sl);
}
targetTstates += cpuCyclesPerLine;
if (debugCond) {
while (cpu.getTstates() < targetTstates) {
if (debugCond && debugCond()) { debugCond = null; }
cpu.runFrame(cpu.getTstates() + 1);
}
} else {
cpu.runFrame(targetTstates);
}
// visible area is 256x224 (before rotation)
video.updateFrame(0, 0, 0, 0, showOffscreenObjects ? 264 : 256, 264);
frameCounter = (frameCounter + 1) & 0xff;
if (watchdog_counter-- <= 0) {
console.log("WATCHDOG FIRED, PC ", hex(cpu.getPC())); // TODO: alert on video
self.reset();
}
self.restartDebugState();
// NMI interrupt @ 0x66
if (interruptEnabled) { cpu.nonMaskableInterrupt(); }
});
}
// visible area is 256x224 (before rotation)
video.updateFrame(0, 0, 0, 0, showOffscreenObjects ? 264 : 256, 264);
frameCounter = (frameCounter + 1) & 0xff;
if (watchdog_counter-- <= 0) {
console.log("WATCHDOG FIRED, PC ", hex(cpu.getPC())); // TODO: alert on video
this.reset();
}
// NMI interrupt @ 0x66
if (interruptEnabled) { cpu.nonMaskableInterrupt(); }
this.restartDebugState(); // TODO: after interrupt?
}
var bitcolors = [
@ -397,7 +398,6 @@ var GalaxianPlatform = function(mainElement, options) {
}
var GalaxianScramblePlatform = function(mainElement) {
var self = this;
this.__proto__ = new GalaxianPlatform(mainElement, {
romSize: 0x5020,
gfxBase: 0x4000,

View File

@ -113,38 +113,40 @@ var Midway8080BWPlatform = function(mainElement) {
var idata = video.getFrameData();
setKeyboardFromMap(video, inputs, SPACEINV_KEYCODE_MAP);
pixels = video.getFrameData();
timer = new AnimationTimer(60, function() {
if (!self.isRunning())
return;
var debugCond = self.getDebugCallback();
var targetTstates = cpu.getTstates();
for (var sl=0; sl<224; sl++) {
targetTstates += cpuCyclesPerLine;
if (debugCond) {
while (cpu.getTstates() < targetTstates) {
if (debugCond && debugCond()) {
debugCond = null;
break;
}
cpu.runFrame(cpu.getTstates() + 1);
}
if (!debugCond)
timer = new AnimationTimer(60, this.advance.bind(this));
}
this.advance = function(novideo : boolean) {
var debugCond = this.getDebugCallback();
var targetTstates = cpu.getTstates();
for (var sl=0; sl<224; sl++) {
targetTstates += cpuCyclesPerLine;
if (debugCond) {
while (cpu.getTstates() < targetTstates) {
if (debugCond && debugCond()) {
debugCond = null;
break;
} else {
cpu.runFrame(targetTstates);
}
cpu.runFrame(cpu.getTstates() + 1);
}
if (sl == 95)
cpu.requestInterrupt(0x8); // RST $8
else if (sl == 223)
cpu.requestInterrupt(0x10); // RST $10
if (!debugCond)
break;
} else {
cpu.runFrame(targetTstates);
}
if (sl == 95)
cpu.requestInterrupt(0x8); // RST $8
else if (sl == 223)
cpu.requestInterrupt(0x10); // RST $10
}
if (!novideo) {
video.updateFrame();
if (watchdog_counter-- <= 0) {
console.log("WATCHDOG FIRED"); // TODO: alert on video
self.reset();
}
self.restartDebugState();
});
}
if (watchdog_counter-- <= 0) {
console.log("WATCHDOG FIRED"); // TODO: alert on video
this.reset();
}
this.restartDebugState();
}
this.loadROM = function(title, data) {

View File

@ -127,6 +127,10 @@ var JSNESPlatform = function(mainElement) {
nes.buttonUp(o.index+1, o.mask); // controller, button
});
}
this.advance = function(novideo : boolean) {
nes.frame();
}
this.loadROM = function(title, data) {
var romstr = String.fromCharCode.apply(null, data);

View File

@ -142,23 +142,24 @@ var VicDualPlatform = function(mainElement) {
reset_disable_timer = setTimeout(function() { reset_disable = false; }, 1100);
});
pixels = video.getFrameData();
timer = new AnimationTimer(60, function() {
if (!self.isRunning())
return;
var debugCond = self.getDebugCallback();
var targetTstates = cpu.getTstates();
for (var sl=0; sl<scanlinesPerFrame; sl++) {
inputs[2] &= ~0x8;
inputs[2] |= ((cpu.getTstates() / cyclesPerTimerTick) & 1) << 3;
drawScanline(pixels, sl);
targetTstates += cpuCyclesPerLine;
if (sl == vblankStart) inputs[1] |= 0x8;
if (sl == vsyncEnd) inputs[1] &= ~0x8;
self.runCPU(cpu, targetTstates - cpu.getTstates());
timer = new AnimationTimer(60, this.advance.bind(this));
}
this.advance = function(novideo : boolean) {
var targetTstates = cpu.getTstates();
for (var sl=0; sl<scanlinesPerFrame; sl++) {
inputs[2] &= ~0x8;
inputs[2] |= ((cpu.getTstates() / cyclesPerTimerTick) & 1) << 3;
if (!novideo) {
drawScanline(pixels, sl);
}
video.updateFrame();
self.restartDebugState();
});
targetTstates += cpuCyclesPerLine;
if (sl == vblankStart) inputs[1] |= 0x8;
if (sl == vsyncEnd) inputs[1] &= ~0x8;
this.runCPU(cpu, targetTstates - cpu.getTstates());
}
video.updateFrame();
this.restartDebugState();
}
this.loadROM = function(title, data) {

View File

@ -306,38 +306,38 @@ var WilliamsPlatform = function(mainElement, proto) {
var idata = video.getFrameData();
setKeyboardFromMap(video, pia6821, ROBOTRON_KEYCODE_MAP);
pixels = video.getFrameData();
timer = new AnimationTimer(60, function() {
if (!self.isRunning())
return;
var cpuCyclesPerSection = Math.round(cpuCyclesPerFrame / 65);
for (var sl=0; sl<256; sl+=4) {
video_counter = sl;
// interrupts happen every 1/4 of the screen
if (sl == 0 || sl == 0x3c || sl == 0xbc || sl == 0xfc) {
if (membus.read != memread_defender || pia6821[7] == 0x3c) { // TODO?
if (cpu.interrupt)
cpu.interrupt();
if (cpu.requestInterrupt)
cpu.requestInterrupt();
}
timer = new AnimationTimer(60, this.advance.bind(this));
}
this.advance = function(novideo:boolean) {
var cpuCyclesPerSection = Math.round(cpuCyclesPerFrame / 65);
for (var sl=0; sl<256; sl+=4) {
video_counter = sl;
// interrupts happen every 1/4 of the screen
if (sl == 0 || sl == 0x3c || sl == 0xbc || sl == 0xfc) {
if (membus.read != memread_defender || pia6821[7] == 0x3c) { // TODO?
if (cpu.interrupt)
cpu.interrupt();
if (cpu.requestInterrupt)
cpu.requestInterrupt();
}
self.runCPU(cpu, cpuCyclesPerSection);
if (sl < 256) video.updateFrame(0, 0, 256-4-sl, 0, 4, 304);
}
// last 6 lines
self.runCPU(cpu, cpuCyclesPerSection*2);
if (screenNeedsRefresh) {
for (var i=0; i<0x9800; i++)
drawDisplayByte(i, ram.mem[i]);
screenNeedsRefresh = false;
}
if (watchdog_counter-- <= 0) {
console.log("WATCHDOG FIRED, PC =", cpu.getPC().toString(16)); // TODO: alert on video
// TODO: self.breakpointHit(cpu.T());
self.reset();
}
self.restartDebugState();
});
this.runCPU(cpu, cpuCyclesPerSection);
if (sl < 256) video.updateFrame(0, 0, 256-4-sl, 0, 4, 304);
}
// last 6 lines
this.runCPU(cpu, cpuCyclesPerSection*2);
if (screenNeedsRefresh && !novideo) {
for (var i=0; i<0x9800; i++)
drawDisplayByte(i, ram.mem[i]);
screenNeedsRefresh = false;
}
if (watchdog_counter-- <= 0) {
console.log("WATCHDOG FIRED, PC =", cpu.getPC().toString(16)); // TODO: alert on video
// TODO: this.breakpointHit(cpu.T());
this.reset();
}
this.restartDebugState();
}
this.loadSoundROM = function(data) {

View File

@ -12,6 +12,7 @@ import { PLATFORMS } from "./emu";
import * as Views from "./views";
import { createNewPersistentStore } from "./store";
import { getFilenameForPath, getFilenamePrefix, highlightDifferences, invertMap } from "./util";
import { StateRecorderImpl } from "./recorder";
// external libs (TODO)
declare var Octokat, ga, Tour, GIF, saveAs;
@ -29,6 +30,7 @@ var current_project : CodeProject; // current CodeProject object
var projectWindows : ProjectWindows; // window manager
var stateRecorder : StateRecorderImpl = new StateRecorderImpl();
// TODO: codemirror multiplex support?
var TOOL_TO_SOURCE_STYLE = {
@ -460,6 +462,7 @@ function setDebugButtonState(btnid:string, btnstate:string) {
}
function setupBreakpoint(btnid? : string) {
_disableRecording();
platform.setupDebug(function(state) {
lastDebugState = state;
showDebugInfo(state);
@ -541,6 +544,7 @@ function runStepBackwards() {
}
function clearBreakpoint() {
_disableRecording();
lastDebugState = null;
if (platform.clearDebug) platform.clearDebug();
showDebugInfo();
@ -679,6 +683,33 @@ function traceTiming() {
}
}
var recorderActive = false;
function _disableRecording() {
if (recorderActive) {
platform.setRecorder(null);
$("#dbg_record").removeClass("btn_recording");
$("#replaydiv").hide();
recorderActive = false;
}
}
function _enableRecording() {
stateRecorder.reset();
platform.setRecorder(stateRecorder);
$("#dbg_record").addClass("btn_recording");
$("#replaydiv").show();
recorderActive = true;
}
function _toggleRecording() {
if (recorderActive) {
_disableRecording();
} else {
_enableRecording();
}
}
function setupDebugControls(){
$("#dbg_reset").click(resetAndDebug);
$("#dbg_pause").click(pause);
@ -730,6 +761,21 @@ function setupDebugControls(){
$("#dbg_fastest").click(_fastestFrameRate);
}
updateDebugWindows();
// setup replay slider
if (platform.advance) {
$("#dbg_record").click(_toggleRecording);
var replayslider = $("#replayslider");
stateRecorder.callbackStateChanged = () => {
replayslider.attr('min', 0);
replayslider.attr('max', stateRecorder.numFrames()-1);
replayslider.attr('value', stateRecorder.numFrames()-1); // TODO: doesn't always move
};
replayslider.on('input', function(e) {
_pause();
stateRecorder.loadFrame(platform, (<any>e.target).value);
});
$("#replay_bar").show();
}
}
function showWelcomeMessage() {