testing out sub-frame replay slider, advanceFrameClock, fixed c64 memory corrupt

This commit is contained in:
Steven Hugg 2020-07-04 09:16:45 -05:00
parent 921d2b2253
commit 002aa41bad
13 changed files with 138 additions and 47 deletions

View File

@ -288,14 +288,24 @@ if (window.location.host.endsWith('8bitworkshop.com')) {
</div>
<div class="emulator disable-select" id="emulator">
<!-- replay slider -->
<div id="replaydiv" class="replaydiv" style="display:none">
<div style="display:flex">
<button id="replay_min" class="btn" title="Start of replay"><span class="glyphicon glyphicon-fast-backward" aria-hidden="true"></span></button>
<div id="replaydiv" class="replaydiv" style="display:none;color:#ccc;text-align:left">
<div style="display:flex; grid-gap:1em">
<button id="replay_back" class="btn" 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>
<div>
Frame<br>
<span id="replay_frame" style="width:3em">-</span>
</div>
<input type="range" min="0" max="0" value="0" class="slider" id="replayslider">
<button id="replay_fwd" class="btn" title="Ahead one frame"><span class="glyphicon glyphicon-forward" aria-hidden="true"></span></button>
<button id="replay_max" class="btn" title="End of replay"><span class="glyphicon glyphicon-fast-forward" aria-hidden="true"></span></button>
</div>
<div style="display:flex; grid-gap:1em" id="clockdiv">
<button id="clock_back" class="btn" title="Back one step"><span class="glyphicon glyphicon-backward" aria-hidden="true"></span></button>
<div>
Step<br>
<span id="replay_clock" style="width:3em">-</span>
</div>
<input type="range" min="0" max="0" value="0" class="slider" id="clockslider">
<button id="clock_fwd" class="btn" title="Forward one step"><span class="glyphicon glyphicon-forward" aria-hidden="true"></span></button>
</div>
</div>
<!-- emulator video -->

View File

@ -112,7 +112,8 @@ export interface Platform {
getMemoryMap?() : MemoryMap;
setRecorder?(recorder : EmuRecorder) : void;
advance?(novideo? : boolean) : void;
advance?(novideo? : boolean) : number;
advanceFrameClock?(trap:DebugCondition, step:number) : number;
showHelp?(tool:string, ident?:string) : void;
resize?() : void;
@ -179,7 +180,7 @@ export abstract class BasePlatform {
abstract saveState() : EmuState;
abstract pause() : void;
abstract resume() : void;
abstract advance(novideo? : boolean) : void;
abstract advance(novideo? : boolean) : number;
setRecorder(recorder : EmuRecorder) : void {
this.recorder = recorder;
@ -267,8 +268,9 @@ export abstract class BaseDebugPlatform extends BasePlatform {
this.pollControls();
this.updateRecorder();
this.preFrame();
this.advance(novideo);
var steps = this.advance(novideo);
this.postFrame();
return steps;
}
// default debugging
abstract getSP() : number;
@ -1058,8 +1060,20 @@ export abstract class BaseMachinePlatform<T extends Machine> extends BaseDebugPl
}
advance(novideo:boolean) {
this.machine.advanceFrame(this.getDebugCallback());
var steps = this.machine.advanceFrame(this.getDebugCallback());
if (!novideo && this.video) this.video.updateFrame();
return steps;
}
advanceFrameClock(trap, step) {
if (!(step > 0)) return;
if (this.machine instanceof BaseWASMMachine) {
return this.machine.advanceFrameClock(trap, step);
} else {
return this.machine.advanceFrame(() => {
return --step <= 0;
});
}
}
isRunning() {
@ -1164,6 +1178,8 @@ export abstract class BaseWASMMachine {
statearr : Uint8Array;
cpustateptr : number;
cpustatearr : Uint8Array;
ctrlstateptr : number;
ctrlstatearr : Uint8Array;
cpu : CPU;
romptr : number;
romlen : number;
@ -1215,6 +1231,9 @@ export abstract class BaseWASMMachine {
var statesize = this.exports.machine_get_state_size();
this.stateptr = this.exports.malloc(statesize);
this.statearr = new Uint8Array(this.exports.memory.buffer, this.stateptr, statesize);
var ctrlstatesize = this.exports.machine_get_controls_state_size();
this.ctrlstateptr = this.exports.malloc(ctrlstatesize);
this.ctrlstatearr = new Uint8Array(this.exports.memory.buffer, this.ctrlstateptr, ctrlstatesize);
var cpustatesize = this.exports.machine_get_cpu_state_size();
this.cpustateptr = this.exports.malloc(cpustatesize);
this.cpustatearr = new Uint8Array(this.exports.memory.buffer, this.cpustateptr, cpustatesize);
@ -1271,12 +1290,14 @@ export abstract class BaseWASMMachine {
}
// assume controls buffer is smaller than cpu buffer
saveControlsState() : any {
this.exports.machine_save_controls_state(this.sys, this.cpustateptr);
return { controls:this.cpustatearr.slice(0, this.exports.machine_get_controls_state_size()) }
//console.log(1, this.romptr, this.romlen, this.ctrlstateptr, this.romarr.slice(0,4), this.ctrlstatearr.slice(0,4));
this.exports.machine_save_controls_state(this.sys, this.ctrlstateptr);
//console.log(2, this.romptr, this.romlen, this.ctrlstateptr, this.romarr.slice(0,4), this.ctrlstatearr.slice(0,4));
return { controls:this.ctrlstatearr.slice(0) }
}
loadControlsState(state) : void {
this.cpustatearr.set(state.controls);
this.exports.machine_load_controls_state(this.sys, this.cpustateptr);
this.ctrlstatearr.set(state.controls);
this.exports.machine_load_controls_state(this.sys, this.ctrlstateptr);
}
connectAudio(audio : SampledAudioSink) : void {
this.audio = audio;

View File

@ -308,6 +308,7 @@ export abstract class BasicScanlineMachine extends BasicMachine implements Raste
this.preFrame();
var clock = 0;
var endLineClock = 0;
var steps = 0;
this.probe.logNewFrame();
for (var sl=0; sl<this.numTotalScanlines; sl++) {
endLineClock += this.cpuCyclesPerLine;
@ -320,13 +321,14 @@ export abstract class BasicScanlineMachine extends BasicMachine implements Raste
break;
}
clock += this.advanceCPU();
steps++;
}
this.drawScanline();
this.probe.logNewScanline();
this.probe.logClocks(clock-endLineClock);
}
this.postFrame();
return clock;
return steps; // TODO: return steps, not clock? for recorder
}
preFrame() { }
postFrame() { }

View File

@ -1,6 +1,5 @@
import { Platform, BasePlatform, EmuState, EmuControlsState, EmuRecorder } from "./baseplatform";
import { BaseDebugPlatform } from "./baseplatform";
import { Platform, EmuState, EmuControlsState, EmuRecorder } from "./baseplatform";
import { getNoiseSeed, setNoiseSeed } from "./emu";
// RECORDER
@ -8,16 +7,19 @@ import { getNoiseSeed, setNoiseSeed } from "./emu";
type FrameRec = {controls:EmuControlsState, seed:number};
export class StateRecorderImpl implements EmuRecorder {
checkpointInterval : number = 60;
checkpointInterval : number = 10;
callbackStateChanged : () => void;
callbackNewCheckpoint : (state:EmuState) => void;
maxCheckpoints : number = 120;
maxCheckpoints : number = 300;
platform : Platform;
checkpoints : EmuState[];
framerecs : FrameRec[];
frameCount : number;
lastSeekFrame : number;
lastSeekStep : number;
lastStepCount : number;
constructor(platform : Platform) {
this.reset();
@ -29,6 +31,7 @@ export class StateRecorderImpl implements EmuRecorder {
this.framerecs = [];
this.frameCount = 0;
this.lastSeekFrame = 0;
this.lastSeekStep = 0;
if (this.callbackStateChanged) this.callbackStateChanged();
}
@ -50,6 +53,7 @@ export class StateRecorderImpl implements EmuRecorder {
requested = (this.frameCount++ % this.checkpointInterval) == 0;
}
this.lastSeekFrame++;
this.lastSeekStep = 0;
if (this.callbackStateChanged) this.callbackStateChanged();
return requested;
}
@ -61,7 +65,11 @@ export class StateRecorderImpl implements EmuRecorder {
currentFrame() : number {
return this.lastSeekFrame;
}
currentStep() : number {
return this.lastSeekStep;
}
recordFrame(state : EmuState) {
this.checkpoints.push(state);
if (this.callbackNewCheckpoint) this.callbackNewCheckpoint(state);
@ -82,22 +90,35 @@ export class StateRecorderImpl implements EmuRecorder {
return {frame:foundframe, state:this.checkpoints[foundidx]};
}
loadFrame(seekframe : number) : number {
if (seekframe == this.lastSeekFrame)
loadFrame(seekframe : number, seekstep? : number) : number {
seekframe |= 0;
seekstep |= 0;
if (seekframe == this.lastSeekFrame && seekstep == this.lastSeekStep) {
return seekframe; // already set to this frame
}
// TODO: what if < 1?
let {frame,state} = this.getStateAtOrBefore(seekframe-1);
if (state) {
var numSteps = 0;
this.platform.pause();
this.platform.loadState(state);
// seek to frame index
while (frame < seekframe) {
if (frame < this.framerecs.length) {
this.loadControls(frame);
}
frame++;
this.platform.advance(frame < seekframe); // TODO: infinite loop?
numSteps = this.platform.advance(frame < seekframe); // TODO: infinite loop?
}
// seek to step index
// TODO: what if advance() returns clocks, but steps use insns?
if (seekstep > 0 && this.platform.advanceFrameClock) {
seekstep = this.platform.advanceFrameClock(null, seekstep);
}
// record new values
this.lastSeekFrame = seekframe;
this.lastSeekStep = seekstep;
this.lastStepCount = numSteps;
return seekframe;
} else {
return 0;

View File

@ -1261,6 +1261,7 @@ function clearBreakpoint() {
function resetAndDebug() {
if (!checkRunReady()) return;
var wasRecording = recorderActive;
_disableRecording();
if (platform.setupDebug && platform.readAddress) { // TODO??
clearBreakpoint();
@ -1274,6 +1275,7 @@ function resetAndDebug() {
} else {
platform.reset();
}
if (wasRecording) _enableRecording();
}
function _breakExpression() {
@ -1610,41 +1612,59 @@ function setupDebugControls() {
function setupReplaySlider() {
var replayslider = $("#replayslider");
var clockslider = $("#clockslider");
var replayframeno = $("#replay_frame");
var updateFrameNo = (n) => {
replayframeno.text(n+"");
var clockno = $("#replay_clock");
if (!platform.advanceFrameClock) $("#clockdiv").hide(); // TODO: put this test in recorder?
var updateFrameNo = () => {
replayframeno.text(stateRecorder.lastSeekFrame+"");
clockno.text(stateRecorder.lastSeekStep+"");
};
var sliderChanged = (e) => {
_pause();
var frame = (<any>e.target).value;
if (stateRecorder.loadFrame(frame)) {
updateFrameNo(frame);
projectWindows.tick();
showDebugInfo(platform.saveState());
var frame : number = parseInt(replayslider.val().toString());
var step : number = parseInt(clockslider.val().toString());
if (stateRecorder.loadFrame(frame, step)) {
clockslider.attr('min', 0);
clockslider.attr('max', stateRecorder.lastStepCount);
updateFrameNo();
uiDebugCallback(platform.saveState());
}
};
var setFrameTo = (frame:number) => {
_pause();
if (stateRecorder.loadFrame(frame)) {
replayslider.val(frame);
updateFrameNo(frame);
projectWindows.tick();
showDebugInfo(platform.saveState());
updateFrameNo();
uiDebugCallback(platform.saveState());
}
};
var setClockTo = (clock:number) => {
_pause();
var frame : number = parseInt(replayslider.val().toString());
if (stateRecorder.loadFrame(frame, clock)) {
clockslider.val(clock);
updateFrameNo();
uiDebugCallback(platform.saveState());
}
};
stateRecorder.callbackStateChanged = () => {
replayslider.attr('min', 1);
replayslider.attr('max', stateRecorder.numFrames());
replayslider.val(stateRecorder.currentFrame());
updateFrameNo(stateRecorder.currentFrame());
clockslider.val(stateRecorder.currentStep());
updateFrameNo();
showDebugInfo(platform.saveState());
};
replayslider.on('input', sliderChanged);
replayslider.on('change', sliderChanged);
clockslider.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().toString()) - 1); });
$("#replay_fwd").click(() => { setFrameTo(parseInt(replayslider.val().toString()) + 1); });
$("#clock_back").click(() => { setClockTo(parseInt(clockslider.val().toString()) - 1); });
$("#clock_fwd").click(() => { setClockTo(parseInt(clockslider.val().toString()) + 1); });
$("#replay_bar").show();
uitoolbar.add('ctrl+alt+0', 'Start/Stop Replay Recording', 'glyphicon-record', _toggleRecording).prop('id','dbg_record');
}

View File

@ -394,6 +394,7 @@ export class Atari7800 extends BasicMachine implements RasterFrameBased {
var rgb;
var mc = 0;
var fc = 0;
var steps = 0;
this.probe.logNewFrame();
//console.log(hex(this.cpu.getPC()), hex(this.maria.dll));
// visible lines
@ -411,6 +412,7 @@ export class Atari7800 extends BasicMachine implements RasterFrameBased {
break; // TODO?
}
mc += this.advanceCPU() << 2;
steps++;
}
// is this scanline visible?
if (visible) {
@ -443,6 +445,7 @@ export class Atari7800 extends BasicMachine implements RasterFrameBased {
break;
}
mc += this.advanceCPU() << 2;
steps++;
}
// audio
this.audio && this.audioadapter.generate(this.audio);
@ -455,7 +458,8 @@ export class Atari7800 extends BasicMachine implements RasterFrameBased {
// TODO let bkcol = this.maria.regs[0x0];
// TODO $(this.video.canvas).css('background-color', COLORS_WEB[bkcol]);
*/
return (this.lastFrameCycles = fc);
this.lastFrameCycles = fc;
return steps;
}
getRasterX() { return this.lastFrameCycles % colorClocksPerLine; }

View File

@ -40,6 +40,9 @@ class BallyAstrocadePlatform extends BaseZ80MachinePlatform<BallyAstrocade> impl
{name:'Screen RAM',start:0x4000,size:0x1000,type:'ram'},
{name:'BIOS Variables',start:0x4fce,size:0x5000-0x4fce,type:'ram'},
] } };
showHelp(tool:string, ident:string) {
window.open("https://8bitworkshop.com/blog/platforms/astrocade/", "_help"); // TODO
}
}
class BallyAstrocadeBIOSPlatform extends BallyAstrocadePlatform implements Platform {

View File

@ -489,12 +489,13 @@ const _Atari8Platform = function(mainElement) {
jacanvas.mousedown(rasterPosBreakFn);
}
advance(novideo : boolean) {
advance(novideo : boolean) : number {
var idata = video.getFrameData();
var iofs = 0;
var debugCond = this.getDebugCallback();
var rgb;
var freeClocks = 0;
var totalClocks = 0;
// load controls
// TODO
gtia.regs[0x10] = inputs[0] ^ 1;
@ -518,6 +519,7 @@ const _Atari8Platform = function(mainElement) {
break;
}
cpu.clockPulse();
totalClocks++;
}
// 4 ANTIC pulses = 8 pixels
if (antic.v >= 24 && antic.h >= 44 && antic.h < 44+176) { // TODO: const
@ -535,6 +537,7 @@ const _Atari8Platform = function(mainElement) {
let bkcol = gtia.regs[COLBK];
$(video.canvas).css('background-color', COLORS_WEB[bkcol]);
}
return totalClocks;
}
loadROM(title, data) {

View File

@ -318,13 +318,14 @@ const _GalaxianPlatform = function(mainElement, options) {
return (a == 0x7000 || a == 0x7800) ? null : membus.read(a); // ignore watchdog
}
advance(novideo: boolean) {
advance(novideo: boolean) : number {
var steps = 0;
for (var sl = 0; sl < scanlinesPerFrame; sl++) {
this.scanline = sl;
if (!novideo) {
gfx.drawScanline(pixels, sl);
}
this.runCPU(cpu, cpuCyclesPerLine);
steps += this.runCPU(cpu, cpuCyclesPerLine);
}
// visible area is 256x224 (before rotation)
if (!novideo) {
@ -337,6 +338,7 @@ const _GalaxianPlatform = function(mainElement, options) {
}
// NMI interrupt @ 0x66
if (interruptEnabled) { cpu.NMI(); }
return steps;
}
getRasterScanline() { return this.scanline; }

View File

@ -156,8 +156,9 @@ class JSNESPlatform extends Base6502Platform implements Platform, Probeable {
pollControls() { this.poller.poll(); }
advance(novideo : boolean) {
advance(novideo : boolean) : number {
this.nes.frame();
return 29780; //TODO
}
updateDebugViews() {
@ -479,7 +480,7 @@ class JSNESPlatform extends Base6502Platform implements Platform, Probeable {
] } };
showHelp(tool:string, ident:string) {
window.open("https://8bitworkshop.com/blog/platforms/nintendo-nes.md.html", "_help"); // TODO
window.open("https://8bitworkshop.com/blog/platforms/nes/", "_help"); // TODO
}
}

View File

@ -135,8 +135,9 @@ class VCSPlatform extends BasePlatform {
Javatari.room.speaker.powerOff();
Javatari.room.speaker.powerOn();
}
advance() {
advance() : number {
Javatari.room.console.clockPulse();
return 0; //TODO
}
// for unit test
nextFrame() {
@ -303,7 +304,7 @@ class VCSPlatform extends BasePlatform {
if (tool == 'bataribasic')
window.open("help/bataribasic/manual.html", "_help");
else
window.open("https://8bitworkshop.com/blog/platforms/atari-2600-vcs.md.html", "_help"); // TODO
window.open("https://8bitworkshop.com/blog/platforms/vcs/", "_help"); // TODO
}
getMemoryMap = function() { return {main:[

View File

@ -838,14 +838,16 @@ class VectrexPlatform extends Base6809Platform {
this.psg.psg.register[14] = ~this.inputs[2];
}
advance(novideo:boolean) {
advance(novideo:boolean) : number {
if (!novideo) this.video.clear();
this.updateControls();
this.probe.logNewFrame();
var cycles = 1500000 / 60;
while (cycles > 0) {
cycles -= this.step();
var frameCycles = 1500000 / 60;
var cycles = 0;
while (cycles < frameCycles) {
cycles += this.step();
}
return cycles;
}
step() {

View File

@ -460,13 +460,14 @@ var VerilogPlatform = function(mainElement, options) {
}
// TODO: merge with prev func
advance(novideo : boolean) {
advance(novideo : boolean) : number {
this.setGenInputs();
this.updateVideoFrameCycles(cyclesPerFrame, true, false);
gen.__unreset();
if (!novideo) {
this.refreshVideoFrame();
}
return cyclesPerFrame; //TODO?
}
refreshVideoFrame() {