fixed replay; use nextFrame/preFrame/postFrame/advance; repeatable random noise()

This commit is contained in:
Steven Hugg 2018-08-23 16:02:13 -04:00
parent 0dd741f446
commit e808d817f0
15 changed files with 187 additions and 85 deletions

View File

@ -296,3 +296,7 @@ div.replaydiv {
width:50%;
background-color: #666;
}
.slider {
margin-left: 1em;
margin-right: 1em;
}

View File

@ -139,7 +139,7 @@ if (window.location.host.endsWith('8bitworkshop.com')) {
<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>
<button id="dbg_record" type="submit" title="Start/Stop Replay Recording"><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">
@ -178,7 +178,14 @@ if (window.location.host.endsWith('8bitworkshop.com')) {
<div id="mem_info" class="mem_info" style="display:none">
</div>
<div id="replaydiv" class="replaydiv" style="display:none">
<div style="display:flex">
<button id="replay_min" type="submit" title="Start of replay"><span class="glyphicon glyphicon-fast-backward" aria-hidden="true"></span></button>
<button id="replay_back" type="submit" title="Back one frame"><span class="glyphicon glyphicon-backward" aria-hidden="true"></span></button>
<span id="replay_frame" style="text-align:center;width:3em;margin-left:1em;color:#ccc">-</span>
<input type="range" min="0" max="0" value="0" class="slider" id="replayslider">
<button id="replay_fwd" type="submit" title="Ahead one frame"><span class="glyphicon glyphicon-forward" aria-hidden="true"></span></button>
<button id="replay_max" type="submit" title="End of replay"><span class="glyphicon glyphicon-fast-forward" aria-hidden="true"></span></button>
</div>
</div>
</div>
<!--

View File

@ -110,6 +110,7 @@ abstract class BaseDebugPlatform {
abstract pause() : void;
abstract resume() : void;
abstract readAddress?(addr:number) : number;
abstract advance?(novideo? : boolean) : void;
getDebugCallback() : DebugCondition {
return this.debugCondition;
@ -145,6 +146,16 @@ abstract class BaseDebugPlatform {
this.recorder.recordFrame(this.saveState());
}
}
preFrame() {
this.updateRecorder();
}
postFrame() {
}
nextFrame(novideo : boolean) {
this.preFrame();
this.advance(novideo);
this.postFrame();
}
}
abstract class BaseFrameBasedPlatform extends BaseDebugPlatform {
@ -155,7 +166,7 @@ abstract class BaseFrameBasedPlatform extends BaseDebugPlatform {
this.debugCondition();
}
}
restartDebugState() {
postFrame() {
// save state every frame and rewind debug clocks
var debugging = this.debugCondition && !this.debugBreakState;
if (debugging) {
@ -163,7 +174,6 @@ abstract class BaseFrameBasedPlatform extends BaseDebugPlatform {
this.debugTargetClock -= this.debugClock;
this.debugClock = 0;
}
this.updateRecorder();
}
breakpointHit(targetClock : number) {
this.debugTargetClock = targetClock;
@ -412,7 +422,7 @@ export abstract class BaseZ80Platform extends BaseDebugPlatform {
}
return cpu.getTstates() - targetTstates;
}
restartDebugState() {
postFrame() {
if (this.debugCondition && !this.debugBreakState) {
this.debugSavedState = this.saveState();
if (this.debugTargetClock > 0)
@ -420,7 +430,6 @@ 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

@ -11,8 +11,22 @@ export var PLATFORMS = {};
export var frameUpdateFunction : (Canvas) => void = null;
var _random_state = 1;
export function noise() {
return (Math.random() * 256) & 0xff;
let x = _random_state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
return (_random_state = x) & 0xff;
}
export function getNoiseSeed() {
return _random_state;
}
export function setNoiseSeed(x : number) {
_random_state = x;
}
type KeyboardCallback = (which:number, charCode:number, flags:number) => void;

View File

@ -24,9 +24,17 @@ const GR_MIXMODE = 2;
const GR_PAGE1 = 4;
const GR_HIRES = 8;
type AppleGRParams = {dirty:boolean[], grswitch:number, mem:number[]};
const _Apple2Platform = function(mainElement) {
var cpuFrequency = 1023000;
var cpuCyclesPerLine = 65;
const cpuFrequency = 1023000;
const cpuCyclesPerLine = 65;
const VM_BASE = 0x803; // where to JMP after pr#6
const LOAD_BASE = VM_BASE; //0x7c9; // where to load ROM
const PGM_BASE = VM_BASE; //0x800; // where to load ROM
const HDR_SIZE = PGM_BASE - LOAD_BASE;
var cpu, ram, bus;
var video, ap2disp, audio, timer;
var grdirty = new Array(0xc000 >> 7);
@ -34,10 +42,6 @@ const _Apple2Platform = function(mainElement) {
var kbdlatch = 0;
var soundstate = 0;
var pgmbin;
var VM_BASE = 0x803; // where to JMP after pr#6
var LOAD_BASE = VM_BASE; //0x7c9; // where to load ROM
var PGM_BASE = VM_BASE; //0x800; // where to load ROM
var HDR_SIZE = PGM_BASE - LOAD_BASE;
// language card switches
var auxRAMselected = false;
var auxRAMbank = 1;
@ -45,7 +49,7 @@ const _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;
var grparams : AppleGRParams;
class Apple2Platform extends Base6502Platform {
@ -157,7 +161,7 @@ const _Apple2Platform = function(mainElement) {
video = new RasterVideo(mainElement,280,192);
audio = new SampleAudio(cpuFrequency);
video.create();
video.setKeyboardEvents(function(key,code,flags) {
video.setKeyboardEvents((key,code,flags) => {
if (flags & 1) {
if (code) {
// convert to uppercase for Apple ][
@ -178,9 +182,9 @@ const _Apple2Platform = function(mainElement) {
var idata = video.getFrameData();
grparams = {dirty:grdirty, grswitch:grswitch, mem:ram.mem};
ap2disp = new Apple2Display(idata, grparams);
timer = new AnimationTimer(60, this.advance.bind(this));
timer = new AnimationTimer(60, this.nextFrame.bind(this));
}
advance(novideo : boolean) {
// 262.5 scanlines per frame
var clock = 0;
@ -208,8 +212,6 @@ const _Apple2Platform = function(mainElement) {
}
video.updateFrame();
}
//soundstate = 0; // to prevent clicking
this.restartDebugState(); // reset debug start state
}
loadROM(title, data) {
@ -253,6 +255,7 @@ const _Apple2Platform = function(mainElement) {
auxRAMbank = state.lc.b;
writeinhibit = state.lc.w;
setupLanguageCardConstants();
ap2disp.invalidate(); // repaint entire screen
}
saveState() {
return {
@ -268,7 +271,7 @@ const _Apple2Platform = function(mainElement) {
}
saveControlsState() {
return {
kbd:kbdlatch,
kbd:kbdlatch
};
}
getCPUState() {
@ -349,7 +352,7 @@ const _Apple2Platform = function(mainElement) {
return new Apple2Platform(); // return inner class from constructor
};
var Apple2Display = function(pixels, apple) {
var Apple2Display = function(pixels : number[], apple : AppleGRParams) {
var XSIZE = 280;
var YSIZE = 192;
var PIXELON = 0xffffffff;
@ -358,22 +361,22 @@ var Apple2Display = function(pixels, apple) {
var oldgrmode = -1;
var textbuf = new Array(40*24);
var flashInterval = 500;
const flashInterval = 500;
var loresColor = [
const loresColor = [
(0xff000000), (0xffff00ff), (0xff00007f), (0xff7f007f),
(0xff007f00), (0xff7f7f7f), (0xff0000bf), (0xff0000ff),
(0xffbf7f00), (0xffffbf00), (0xffbfbfbf), (0xffff7f7f),
(0xff00ff00), (0xffffff00), (0xff00bf7f), (0xffffffff),
];
var text_lut = [
const text_lut = [
0x000, 0x080, 0x100, 0x180, 0x200, 0x280, 0x300, 0x380,
0x028, 0x0a8, 0x128, 0x1a8, 0x228, 0x2a8, 0x328, 0x3a8,
0x050, 0x0d0, 0x150, 0x1d0, 0x250, 0x2d0, 0x350, 0x3d0
];
var hires_lut = [
const hires_lut = [
0x0000, 0x0400, 0x0800, 0x0c00, 0x1000, 0x1400, 0x1800, 0x1c00,
0x0080, 0x0480, 0x0880, 0x0c80, 0x1080, 0x1480, 0x1880, 0x1c80,
0x0100, 0x0500, 0x0900, 0x0d00, 0x1100, 0x1500, 0x1900, 0x1d00,
@ -627,7 +630,7 @@ var Apple2Display = function(pixels, apple) {
for (var x=0; x<40; x++)
textbuf[y*40+x] = -1;
}
for (var i=0; i<384; i++)
for (var i=0; i<apple.dirty.length; i++)
apple.dirty[i] = true;
}
@ -658,14 +661,18 @@ var Apple2Display = function(pixels, apple) {
for (y=20; y<24; y++)
drawLoresLine(y);
}
for (var i=0; i<384; i++)
for (var i=0; i<apple.dirty.length; i++)
apple.dirty[i] = false;
}
this.invalidate = function() {
oldgrmode = -1;
}
}
/*exported apple2_charset */
var apple2_charset = [
const apple2_charset = [
0x00,0x1c,0x22,0x2a,0x2e,0x2c,0x20,0x1e,
0x00,0x08,0x14,0x22,0x22,0x3e,0x22,0x22,
0x00,0x3c,0x22,0x22,0x3c,0x22,0x22,0x3c,
@ -925,7 +932,7 @@ var apple2_charset = [
];
// public domain ROM
var APPLEIIGO_LZG = [
const APPLEIIGO_LZG = [
76,90,71,0,0,48,0,0,0,10,174,5,108,198,141,1,25,52,55,59,65,80,80,76,69,73,73,71,79,32,82,79,
77,49,46,49,255,59,31,59,31,59,31,59,31,59,31,59,31,59,31,59,3,133,109,132,110,56,165,150,229,155,133,94,
168,165,151,229,156,170,232,152,240,35,165,150,56,229,94,133,150,176,3,198,151,56,165,148,55,3,148,176,8,198,149,144,

View File

@ -296,7 +296,7 @@ const _GalaxianPlatform = function(mainElement, options) {
var idata = video.getFrameData();
setKeyboardFromMap(video, inputs, keyMap);
pixels = video.getFrameData();
timer = new AnimationTimer(60, this.advance.bind(this));
timer = new AnimationTimer(60, this.nextFrame.bind(this));
}
readAddress(a) {
@ -321,7 +321,9 @@ const _GalaxianPlatform = function(mainElement, options) {
}
}
// visible area is 256x224 (before rotation)
video.updateFrame(0, 0, 0, 0, showOffscreenObjects ? 264 : 256, 264);
if (!novideo) {
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
@ -329,7 +331,6 @@ const _GalaxianPlatform = function(mainElement, options) {
}
// NMI interrupt @ 0x66
if (interruptEnabled) { cpu.nonMaskableInterrupt(); }
this.restartDebugState(); // TODO: after interrupt?
}
loadROM(title, data) {

View File

@ -112,7 +112,7 @@ const _Midway8080BWPlatform = function(mainElement) {
var idata = video.getFrameData();
setKeyboardFromMap(video, inputs, SPACEINV_KEYCODE_MAP);
pixels = video.getFrameData();
timer = new AnimationTimer(60, this.advance.bind(this));
timer = new AnimationTimer(60, this.nextFrame.bind(this));
}
readAddress(addr) {
@ -149,7 +149,6 @@ const _Midway8080BWPlatform = function(mainElement) {
console.log("WATCHDOG FIRED"); // TODO: alert on video
this.reset();
}
this.restartDebugState();
}
loadROM(title, data) {

View File

@ -87,7 +87,6 @@ const _JSNESPlatform = function(mainElement) {
for (var i=0; i<frameBuffer.length; i++)
idata[i] = frameBuffer[i] | 0xff000000;
video.updateFrame();
self.restartDebugState();
frameindex++;
//if (frameindex == 2000) console.log(nsamples*60/frameindex,'Hz');
},
@ -117,9 +116,7 @@ const _JSNESPlatform = function(mainElement) {
self.evalDebugCondition();
return cycles;
}
timer = new AnimationTimer(60, function() {
nes.frame();
});
timer = new AnimationTimer(60, this.nextFrame.bind(this));
// set keyboard map
setKeyboardFromMap(video, [], JSNES_KEYCODE_MAP, function(o,key,code,flags) {
if (flags & 1)
@ -131,7 +128,7 @@ const _JSNESPlatform = function(mainElement) {
advance(novideo : boolean) {
nes.frame();
}
}
loadROM(title, data) {
var romstr = String.fromCharCode.apply(null, data);

View File

@ -62,8 +62,8 @@ class VCSPlatform {
// intercept clockPulse function
Javatari.room.console.oldClockPulse = Javatari.room.console.clockPulse;
Javatari.room.console.clockPulse = function() {
this.oldClockPulse();
self.updateRecorder();
this.oldClockPulse();
}
this.paused = false;
}

View File

@ -143,7 +143,7 @@ const _VicDualPlatform = function(mainElement) {
reset_disable_timer = setTimeout(function() { reset_disable = false; }, 1100);
});
pixels = video.getFrameData();
timer = new AnimationTimer(60, this.advance.bind(this));
timer = new AnimationTimer(60, this.nextFrame.bind(this));
}
readAddress(addr) {
@ -164,7 +164,6 @@ const _VicDualPlatform = function(mainElement) {
this.runCPU(cpu, targetTstates - cpu.getTstates());
}
video.updateFrame();
this.restartDebugState();
}
loadROM(title, data) {

View File

@ -306,7 +306,7 @@ var WilliamsPlatform = function(mainElement, proto) {
var idata = video.getFrameData();
setKeyboardFromMap(video, pia6821, ROBOTRON_KEYCODE_MAP);
pixels = video.getFrameData();
timer = new AnimationTimer(60, this.advance.bind(this));
timer = new AnimationTimer(60, this.nextFrame.bind(this));
}
this.advance = function(novideo:boolean) {
@ -337,7 +337,6 @@ var WilliamsPlatform = function(mainElement, proto) {
// TODO: this.breakpointHit(cpu.T());
this.reset();
}
this.restartDebugState();
}
this.loadSoundROM = function(data) {

View File

@ -1,5 +1,8 @@
import { Platform, EmuState, EmuControlsState, EmuRecorder } from "./baseplatform";
import { getNoiseSeed, setNoiseSeed } from "./emu";
type FrameRec = {controls:EmuControlsState, seed:number};
export class StateRecorderImpl implements EmuRecorder {
checkpointInterval : number = 60;
@ -7,8 +10,8 @@ export class StateRecorderImpl implements EmuRecorder {
maxCheckpoints : number = 120;
platform : Platform;
buffer : EmuState[];
controls : EmuControlsState[];
checkpoints : EmuState[];
framerecs : FrameRec[];
frameCount : number;
lastSeekFrame : number = -1;
@ -18,35 +21,37 @@ export class StateRecorderImpl implements EmuRecorder {
}
reset() {
this.buffer = [];
this.controls = [];
this.checkpoints = [];
this.framerecs = [];
this.frameCount = 0;
this.lastSeekFrame = -1;
}
frameRequested() : boolean {
// buffer full?
if (this.buffer.length >= this.maxCheckpoints) {
// checkpoints full?
if (this.checkpoints.length >= this.maxCheckpoints) {
return false;
}
// record the control state, if available
if (this.platform.saveControlsState) {
this.controls.push(this.platform.saveControlsState());
this.framerecs.push({
controls:this.platform.saveControlsState(),
seed:getNoiseSeed()
});
}
// pick up where we left off, if we used the seek function
if (this.lastSeekFrame >= 0) {
this.frameCount = this.lastSeekFrame;
this.lastSeekFrame = -1;
// truncate buffers
this.buffer = this.buffer.slice(0, Math.floor((this.frameCount + this.checkpointInterval - 1) / this.checkpointInterval));
this.controls = this.controls.slice(0, this.frameCount);
this.checkpoints = this.checkpoints.slice(0, Math.floor((this.frameCount + this.checkpointInterval - 1) / this.checkpointInterval));
this.framerecs = this.framerecs.slice(0, this.frameCount);
}
// time to save next frame?
this.frameCount++;
if (this.callbackStateChanged) {
this.callbackStateChanged();
}
return (this.frameCount % this.checkpointInterval) == 0;
return (this.frameCount++ % this.checkpointInterval) == 0;
}
numFrames() : number {
@ -54,30 +59,36 @@ export class StateRecorderImpl implements EmuRecorder {
}
recordFrame(state : EmuState) {
this.buffer.push(state);
this.checkpoints.push(state);
}
getStateAtOrBefore(frame : number) : {frame : number, state : EmuState} {
var bufidx = Math.floor(frame / this.checkpointInterval);
var foundidx = bufidx < this.buffer.length ? bufidx : this.buffer.length-1;
var foundidx = bufidx < this.checkpoints.length ? bufidx : this.checkpoints.length-1;
var foundframe = foundidx * this.checkpointInterval;
return {frame:foundframe, state:this.buffer[foundidx]};
return {frame:foundframe, state:this.checkpoints[foundidx]};
}
loadFrame(seekframe : number) {
let {frame,state} = this.getStateAtOrBefore(seekframe);
loadFrame(seekframe : number) : number {
if (seekframe == this.lastSeekFrame)
return seekframe; // already set to this frame
// TODO: what if < 1?
let {frame,state} = this.getStateAtOrBefore(seekframe-1);
if (state) {
this.platform.pause();
this.platform.loadState(state);
while (frame < seekframe) {
if (frame < this.controls.length) {
this.platform.loadControlsState(this.controls[frame]);
if (frame < this.framerecs.length) {
this.platform.loadControlsState(this.framerecs[frame].controls);
setNoiseSeed(this.framerecs[frame].seed);
}
this.platform.advance(true); // TODO: infinite loop?
frame++;
this.platform.advance(frame < seekframe); // TODO: infinite loop?
}
this.platform.advance();
this.lastSeekFrame = seekframe;
return seekframe;
} else {
return 0;
}
}
}

View File

@ -762,21 +762,47 @@ function setupDebugControls(){
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((<any>e.target).value);
});
$("#replay_bar").show();
setupReplaySlider();
}
}
function setupReplaySlider() {
var replayslider = $("#replayslider");
var replayframeno = $("#replay_frame");
var updateFrameNo = (n) => {
replayframeno.text(n+"");
};
var sliderChanged = (e) => {
_pause();
var frame = (<any>e.target).value;
if (stateRecorder.loadFrame(frame)) {
updateFrameNo(frame);
}
};
var setFrameTo = (frame:number) => {
_pause();
if (stateRecorder.loadFrame(frame)) {
replayslider.val(frame);
updateFrameNo(frame);
console.log('seek to frame',frame);
}
};
stateRecorder.callbackStateChanged = () => {
replayslider.attr('min', 1);
replayslider.attr('max', stateRecorder.numFrames());
replayslider.val(stateRecorder.numFrames());
updateFrameNo(stateRecorder.numFrames());
};
replayslider.on('input', sliderChanged);
replayslider.on('change', sliderChanged);
$("#replay_min").click(() => { setFrameTo(1) });
$("#replay_max").click(() => { setFrameTo(stateRecorder.numFrames()); });
$("#replay_back").click(() => { setFrameTo(parseInt(replayslider.val()) - 1); });
$("#replay_fwd").click(() => { setFrameTo(parseInt(replayslider.val()) + 1); });
$("#replay_bar").show();
$("#dbg_record").click(_toggleRecording);
}
function showWelcomeMessage() {
if (!localStorage.getItem("8bitworkshop.hello")) {
// Instance the tour

View File

@ -8,17 +8,17 @@ const jsdom = require('jsdom');
const { JSDOM } = jsdom;
const { window } = new JSDOM();
global['Javatari'] = {AUTO_START:false};
const dom = new JSDOM(`<!DOCTYPE html><div id="emulator"></div>`);
global['window'] = dom.window;
global['document'] = dom.window.document;
global.window = dom.window;
global.document = dom.window.document;
dom.window.Audio = null;
global.Image = function() { }
//var javatari = require("javatari.js/release/javatari/javatari.js");
includeInThisContext("javatari.js/release/javatari/javatari.js");
Javatari.AUTO_START = false;
includeInThisContext('src/cpu/z80fast.js');
//includeInThisContext('tss/js/Log.js');
global['Log'] = require('tss/js/Log.js').Log;
global.Log = require('tss/js/Log.js').Log;
includeInThisContext('tss/js/tss/PsgDeviceChannel.js');
includeInThisContext('tss/js/tss/MasterChannel.js');
includeInThisContext('tss/js/tss/AudioLooper.js');
@ -27,7 +27,9 @@ var jsnes = require("jsnes/jsnes.min.js");
var emu = require('gen/emu.js');
var audio = require('gen/audio.js');
var recorder = require('gen/recorder.js');
var vicdual = require('gen/platform/vicdual.js');
var apple2 = require('gen/platform/apple2.js');
//
@ -39,17 +41,44 @@ emu.RasterVideo = function(mainElement, width, height, options) {
this.setKeyboardEvents = function(callback) {
}
this.getFrameData = function() { return datau32; }
this.updateFrame = function() { }
}
//
function testPlatform(platid) {
function testPlatform(platid, romname, maxframes, callback) {
var emudiv = document.getElementById('emulator');
var platform = new emu.PLATFORMS[platid](emudiv);
var rec = new recorder.StateRecorderImpl(platform);
platform.start();
// TODO
var rom = fs.readFileSync('./test/roms/' + platid + '/' + romname);
rom = new Uint8Array(rom);
platform.loadROM(rom);
platform.resume(); // so that recorder works
platform.setRecorder(rec);
for (var i=0; i<maxframes; i++) {
if (callback) callback(platform, i);
platform.nextFrame();
}
platform.pause();
assert.equal(maxframes, rec.numFrames());
var state1 = platform.saveState();
assert.equal(1, rec.loadFrame(1));
assert.equal(maxframes, rec.loadFrame(maxframes));
var state2 = platform.saveState();
assert.deepEqual(state1, state2);
return platform;
}
describe('VIC Dual Platform', function() {
testPlatform('vicdual');
describe('Platform Replay', () => {
it('Should run apple2', () => {
var platform = testPlatform('apple2', 'cosmic.c.rom', 70, (platform, frameno) => {
if (frameno == 60) {
var cstate = platform.saveControlsState();
cstate.kbd = 0x80 | 0x20;
platform.loadControlsState(cstate);
}
});
assert.equal(platform.saveState().kbd, 0x20); // strobe cleared
});
});

Binary file not shown.