Remove the `Disk` object from the `Drive` object
This change splits out the disk objects into a record parallel to the drive objects. The idea is that the `Drive` structure becomes a representation of the state of the drive that is separate from the disk image actually in the drive. This helps in an upcoming refactoring. This also changes the default empty disks to be writable. While odd, the write protect switch should be in the "off" position since there is no disk pressing on it. Finally, `insertDisk` now resets the head position to 0 since there is no way of preserving the head position across disks. (Even in the real world, the motor-off delay plus spindle spin-down would make it impossible to know the disk head position with any accuracy.)
This commit is contained in:
parent
f98aa93f24
commit
2c01448f07
|
@ -19,7 +19,6 @@ import {
|
|||
PROCESS_JSON,
|
||||
MassStorage,
|
||||
MassStorageData,
|
||||
DiskMetadata,
|
||||
SupportedSectors,
|
||||
FloppyDisk,
|
||||
FloppyFormat,
|
||||
|
@ -218,10 +217,6 @@ export interface Callbacks {
|
|||
|
||||
/** Common information for Nibble and WOZ disks. */
|
||||
interface Drive {
|
||||
/** The disk in the drive. */
|
||||
disk: FloppyDisk;
|
||||
/** Metadata about the disk image */
|
||||
metadata: DiskMetadata;
|
||||
/** Whether the drive write protect is on. */
|
||||
readOnly: boolean;
|
||||
/** Quarter track position of read/write head. */
|
||||
|
@ -236,7 +231,6 @@ interface Drive {
|
|||
|
||||
interface DriveState {
|
||||
disk: FloppyDisk;
|
||||
metadata: DiskMetadata;
|
||||
readOnly: boolean;
|
||||
track: byte;
|
||||
head: byte;
|
||||
|
@ -252,19 +246,6 @@ interface State {
|
|||
controllerState: ControllerState;
|
||||
}
|
||||
|
||||
function getDriveState(drive: Drive): DriveState {
|
||||
const { metadata, readOnly, track, head, phase, dirty } = drive;
|
||||
return {
|
||||
disk: getDiskState(drive.disk),
|
||||
metadata: {...metadata},
|
||||
readOnly,
|
||||
track,
|
||||
head,
|
||||
phase,
|
||||
dirty,
|
||||
};
|
||||
}
|
||||
|
||||
function getDiskState(disk: NoFloppyDisk): NoFloppyDisk;
|
||||
function getDiskState(disk: NibbleDisk): NibbleDisk;
|
||||
function getDiskState(disk: WozDisk): WozDisk;
|
||||
|
@ -315,20 +296,6 @@ function getDiskState(disk: FloppyDisk): FloppyDisk {
|
|||
throw new Error('Unknown drive state');
|
||||
}
|
||||
|
||||
function setDriveState(state: DriveState) {
|
||||
const { track, head, phase, readOnly, dirty, metadata } = state;
|
||||
const result: Drive = {
|
||||
disk: getDiskState(state.disk),
|
||||
track,
|
||||
head,
|
||||
phase,
|
||||
readOnly,
|
||||
dirty,
|
||||
metadata: { ...metadata },
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller.
|
||||
*/
|
||||
|
@ -336,29 +303,30 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
|
||||
private drives: Record<DriveNumber, Drive> = {
|
||||
1: { // Drive 1
|
||||
disk: {
|
||||
encoding: NO_DISK,
|
||||
readOnly: true,
|
||||
metadata: { name: 'Disk 1' },
|
||||
},
|
||||
track: 0,
|
||||
head: 0,
|
||||
phase: 0,
|
||||
readOnly: false,
|
||||
dirty: false,
|
||||
metadata: { name: 'Disk 1' },
|
||||
},
|
||||
2: { // Drive 2
|
||||
disk: {
|
||||
encoding: NO_DISK,
|
||||
readOnly: true,
|
||||
metadata: { name: 'Disk 2' },
|
||||
},
|
||||
track: 0,
|
||||
head: 0,
|
||||
phase: 0,
|
||||
readOnly: false,
|
||||
dirty: false,
|
||||
}
|
||||
};
|
||||
|
||||
private disks: Record<DriveNumber, FloppyDisk> = {
|
||||
1: {
|
||||
encoding: NO_DISK,
|
||||
readOnly: false,
|
||||
metadata: { name: 'Disk 1' },
|
||||
},
|
||||
2: {
|
||||
encoding: NO_DISK,
|
||||
readOnly: false,
|
||||
metadata: { name: 'Disk 2' },
|
||||
}
|
||||
};
|
||||
|
@ -373,8 +341,10 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
private skip = 0;
|
||||
/** Drive off timeout id or null. */
|
||||
private offTimeout: number | null = null;
|
||||
/** Current drive object. */
|
||||
private cur: Drive;
|
||||
/** Current drive object. Must only be set by `updateActiveDrive()`. */
|
||||
private curDrive: Drive;
|
||||
/** Current disk object. Must only be set by `updateActiveDrive()`. */
|
||||
private curDisk: FloppyDisk;
|
||||
|
||||
/** Nibbles read this on cycle */
|
||||
private nibbleCount = 0;
|
||||
|
@ -412,17 +382,23 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
state: 2,
|
||||
};
|
||||
|
||||
this.cur = this.drives[this.state.drive];
|
||||
this.updateActiveDrive();
|
||||
|
||||
this.initWorker();
|
||||
}
|
||||
|
||||
/** Updates the active drive based on the controller state. */
|
||||
private updateActiveDrive() {
|
||||
this.curDrive = this.drives[this.state.drive];
|
||||
this.curDisk = this.disks[this.state.drive];
|
||||
}
|
||||
|
||||
private debug(..._args: unknown[]) {
|
||||
// debug(..._args);
|
||||
}
|
||||
|
||||
public head(): number {
|
||||
return this.cur.head;
|
||||
return this.curDrive.head;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -467,18 +443,18 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
let workCycles = (cycles - this.lastCycles) * 2;
|
||||
this.lastCycles = cycles;
|
||||
|
||||
if (!isWozDisk(this.cur.disk)) {
|
||||
if (!isWozDisk(this.curDisk)) {
|
||||
return;
|
||||
}
|
||||
const track =
|
||||
this.cur.disk.rawTracks[this.cur.disk.trackMap[this.cur.track]] || [0];
|
||||
this.curDisk.rawTracks[this.curDisk.trackMap[this.curDrive.track]] || [0];
|
||||
|
||||
const state = this.state;
|
||||
|
||||
while (workCycles-- > 0) {
|
||||
let pulse: number = 0;
|
||||
if (state.clock === 4) {
|
||||
pulse = track[this.cur.head];
|
||||
pulse = track[this.curDrive.head];
|
||||
if (!pulse) {
|
||||
// More than 2 zeros can not be read reliably.
|
||||
if (++this.zeros > 2) {
|
||||
|
@ -511,7 +487,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
break;
|
||||
case 0xA: // SR
|
||||
state.latch >>= 1;
|
||||
if (this.cur.readOnly) {
|
||||
if (this.curDrive.readOnly) {
|
||||
state.latch |= 0x80;
|
||||
}
|
||||
break;
|
||||
|
@ -530,12 +506,12 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
if (state.clock === 4) {
|
||||
if (state.on) {
|
||||
if (state.q7) {
|
||||
track[this.cur.head] = state.state & 0x8 ? 0x01 : 0x00;
|
||||
track[this.curDrive.head] = state.state & 0x8 ? 0x01 : 0x00;
|
||||
this.debug('Wrote', state.state & 0x8 ? 0x01 : 0x00);
|
||||
}
|
||||
|
||||
if (++this.cur.head >= track.length) {
|
||||
this.cur.head = 0;
|
||||
if (++this.curDrive.head >= track.length) {
|
||||
this.curDrive.head = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -548,29 +524,29 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
|
||||
// Only called for non-WOZ disks
|
||||
private readWriteNext() {
|
||||
if (!isNibbleDisk(this.cur.disk)) {
|
||||
if (!isNibbleDisk(this.curDisk)) {
|
||||
return;
|
||||
}
|
||||
const state = this.state;
|
||||
if (state.on && (this.skip || state.q7)) {
|
||||
const track = this.cur.disk.tracks[this.cur.track >> 2];
|
||||
const track = this.curDisk.tracks[this.curDrive.track >> 2];
|
||||
if (track && track.length) {
|
||||
if (this.cur.head >= track.length) {
|
||||
this.cur.head = 0;
|
||||
if (this.curDrive.head >= track.length) {
|
||||
this.curDrive.head = 0;
|
||||
}
|
||||
|
||||
if (state.q7) {
|
||||
if (!this.cur.readOnly) {
|
||||
track[this.cur.head] = state.bus;
|
||||
if (!this.cur.dirty) {
|
||||
if (!this.curDrive.readOnly) {
|
||||
track[this.curDrive.head] = state.bus;
|
||||
if (!this.curDrive.dirty) {
|
||||
this.updateDirty(state.drive, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state.latch = track[this.cur.head];
|
||||
state.latch = track[this.curDrive.head];
|
||||
}
|
||||
|
||||
++this.cur.head;
|
||||
++this.curDrive.head;
|
||||
}
|
||||
} else {
|
||||
state.latch = 0;
|
||||
|
@ -599,24 +575,24 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
|
||||
this.debug(`phase ${phase}${on ? ' on' : ' off'}`);
|
||||
if (on) {
|
||||
this.cur.track += PHASE_DELTA[this.cur.phase][phase] * 2;
|
||||
this.cur.phase = phase;
|
||||
this.curDrive.track += PHASE_DELTA[this.curDrive.phase][phase] * 2;
|
||||
this.curDrive.phase = phase;
|
||||
}
|
||||
|
||||
// The emulator clamps the track to the valid track range available
|
||||
// in the image, but real Disk II drives can seek past track 34 by
|
||||
// at least a half track, usually a full track. Some 3rd party
|
||||
// drives can seek to track 39.
|
||||
const maxTrack = isNibbleDisk(this.cur.disk)
|
||||
? this.cur.disk.tracks.length * 4 - 1
|
||||
: (isWozDisk(this.cur.disk)
|
||||
? this.cur.disk.trackMap.length - 1
|
||||
const maxTrack = isNibbleDisk(this.curDisk)
|
||||
? this.curDisk.tracks.length * 4 - 1
|
||||
: (isWozDisk(this.curDisk)
|
||||
? this.curDisk.trackMap.length - 1
|
||||
: 0);
|
||||
if (this.cur.track > maxTrack) {
|
||||
this.cur.track = maxTrack;
|
||||
if (this.curDrive.track > maxTrack) {
|
||||
this.curDrive.track = maxTrack;
|
||||
}
|
||||
if (this.cur.track < 0x0) {
|
||||
this.cur.track = 0x0;
|
||||
if (this.curDrive.track < 0x0) {
|
||||
this.curDrive.track = 0x0;
|
||||
}
|
||||
|
||||
// debug(
|
||||
|
@ -688,7 +664,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
case LOC.DRIVE1: // 0x0a
|
||||
this.debug('Disk 1');
|
||||
state.drive = 1;
|
||||
this.cur = this.drives[state.drive];
|
||||
this.updateActiveDrive();
|
||||
if (state.on) {
|
||||
this.callbacks.driveLight(2, false);
|
||||
this.callbacks.driveLight(1, true);
|
||||
|
@ -697,7 +673,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
case LOC.DRIVE2: // 0x0b
|
||||
this.debug('Disk 2');
|
||||
state.drive = 2;
|
||||
this.cur = this.drives[state.drive];
|
||||
this.updateActiveDrive();
|
||||
if (state.on) {
|
||||
this.callbacks.driveLight(1, false);
|
||||
this.callbacks.driveLight(2, true);
|
||||
|
@ -709,7 +685,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
if (state.q7) {
|
||||
this.debug('clearing _q6/SHIFT');
|
||||
}
|
||||
if (isNibbleDisk(this.cur.disk)) {
|
||||
if (isNibbleDisk(this.curDisk)) {
|
||||
this.readWriteNext();
|
||||
}
|
||||
break;
|
||||
|
@ -719,9 +695,9 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
if (state.q7) {
|
||||
this.debug('setting _q6/LOAD');
|
||||
}
|
||||
if (isNibbleDisk(this.cur.disk)) {
|
||||
if (isNibbleDisk(this.curDisk)) {
|
||||
if (readMode && !state.q7) {
|
||||
if (this.cur.readOnly) {
|
||||
if (this.curDrive.readOnly) {
|
||||
state.latch = 0xff;
|
||||
this.debug('Setting readOnly');
|
||||
} else {
|
||||
|
@ -794,38 +770,65 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
state.q7 = false;
|
||||
state.on = false;
|
||||
state.drive = 1;
|
||||
this.cur = this.drives[state.drive];
|
||||
}
|
||||
this.updateActiveDrive();
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.moveHead();
|
||||
}
|
||||
|
||||
private getDriveState(drive: DriveNumber): DriveState {
|
||||
const curDrive = this.drives[drive];
|
||||
const curDisk = this.disks[drive];
|
||||
const { readOnly, track, head, phase, dirty } = curDrive;
|
||||
return {
|
||||
disk: getDiskState(curDisk),
|
||||
readOnly,
|
||||
track,
|
||||
head,
|
||||
phase,
|
||||
dirty,
|
||||
};
|
||||
}
|
||||
|
||||
getState(): State {
|
||||
const result = {
|
||||
drives: [] as DriveState[],
|
||||
skip: this.skip,
|
||||
controllerState: { ...this.state },
|
||||
};
|
||||
result.drives[1] = getDriveState(this.drives[1]);
|
||||
result.drives[2] = getDriveState(this.drives[2]);
|
||||
result.drives[1] = this.getDriveState(1);
|
||||
result.drives[2] = this.getDriveState(2);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private setDriveState(drive: DriveNumber, state: DriveState) {
|
||||
const { track, head, phase, readOnly, dirty } = state;
|
||||
this.drives[drive] = {
|
||||
track,
|
||||
head,
|
||||
phase,
|
||||
readOnly,
|
||||
dirty,
|
||||
};
|
||||
this.disks[drive] = getDiskState(state.disk);
|
||||
}
|
||||
|
||||
|
||||
setState(state: State) {
|
||||
this.skip = state.skip;
|
||||
this.state = { ...state.controllerState };
|
||||
for (const d of DRIVE_NUMBERS) {
|
||||
this.drives[d] = setDriveState(state.drives[d]);
|
||||
const { name, side } = state.drives[d].metadata;
|
||||
this.setDriveState(d, state.drives[d]);
|
||||
const { name, side } = state.drives[d].disk.metadata;
|
||||
const { dirty } = state.drives[d];
|
||||
this.callbacks.label(d, name, side);
|
||||
this.callbacks.driveLight(d, this.state.on);
|
||||
this.callbacks.dirty(d, dirty);
|
||||
}
|
||||
this.cur = this.drives[this.state.drive];
|
||||
this.updateActiveDrive();
|
||||
}
|
||||
|
||||
getMetadata(driveNo: DriveNumber) {
|
||||
|
@ -840,12 +843,12 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
}
|
||||
|
||||
// TODO(flan): Does not work on WOZ disks
|
||||
rwts(disk: DriveNumber, track: byte, sector: byte) {
|
||||
const cur = this.drives[disk];
|
||||
if (!isNibbleDisk(cur.disk)) {
|
||||
rwts(drive: DriveNumber, track: byte, sector: byte) {
|
||||
const curDisk = this.disks[drive];
|
||||
if (!isNibbleDisk(curDisk)) {
|
||||
throw new Error('Can\'t read WOZ disks');
|
||||
}
|
||||
return readSector(cur.disk, track, sector);
|
||||
return readSector(curDisk, track, sector);
|
||||
}
|
||||
|
||||
/** Sets the data for `drive` from `disk`, which is expected to be JSON. */
|
||||
|
@ -872,11 +875,11 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
}
|
||||
|
||||
getJSON(drive: DriveNumber, pretty: boolean = false) {
|
||||
const cur = this.drives[drive];
|
||||
if (!isNibbleDisk(cur.disk)) {
|
||||
const curDisk = this.disks[drive];
|
||||
if (!isNibbleDisk(curDisk)) {
|
||||
throw new Error('Can\'t save WOZ disks to JSON');
|
||||
}
|
||||
return jsonEncode(cur.disk, pretty);
|
||||
return jsonEncode(curDisk, pretty);
|
||||
}
|
||||
|
||||
setJSON(drive: DriveNumber, json: string) {
|
||||
|
@ -955,22 +958,22 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
}
|
||||
|
||||
private insertDisk(drive: DriveNumber, disk: FloppyDisk) {
|
||||
const cur = this.drives[drive];
|
||||
cur.disk = disk as WozDisk | NibbleDisk;
|
||||
cur.metadata = disk.metadata;
|
||||
const { name, side } = cur.metadata;
|
||||
this.disks[drive] = disk;
|
||||
this.drives[drive].head = 0;
|
||||
this.updateActiveDrive();
|
||||
const { name, side } = disk.metadata;
|
||||
this.updateDirty(drive, true);
|
||||
this.callbacks.label(drive, name, side);
|
||||
}
|
||||
|
||||
// TODO(flan): Does not work with WOZ or D13 disks
|
||||
getBinary(drive: DriveNumber, ext?: Exclude<NibbleFormat, 'woz' | 'd13'>): MassStorageData | null {
|
||||
const cur = this.drives[drive];
|
||||
if (!isNibbleDisk(cur.disk)) {
|
||||
const curDisk = this.disks[drive];
|
||||
if (!isNibbleDisk(curDisk)) {
|
||||
return null;
|
||||
}
|
||||
const { format, readOnly, tracks, volume } = cur.disk;
|
||||
const { name } = cur.metadata;
|
||||
const { format, readOnly, tracks, volume } = curDisk;
|
||||
const { name } = curDisk.metadata;
|
||||
const len = format === 'nib' ?
|
||||
tracks.reduce((acc, track) => acc + track.length, 0) :
|
||||
this.sectors * tracks.length * 256;
|
||||
|
@ -984,7 +987,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
idx += tracks[t].length;
|
||||
} else {
|
||||
for (let s = 0; s < 0x10; s++) {
|
||||
const sector = readSector({ ...cur.disk, format: extension }, t, s);
|
||||
const sector = readSector({ ...curDisk, format: extension }, t, s);
|
||||
data.set(sector, idx);
|
||||
idx += sector.length;
|
||||
}
|
||||
|
@ -1002,18 +1005,18 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
|||
|
||||
// TODO(flan): Does not work with WOZ or D13 disks
|
||||
getBase64(drive: DriveNumber) {
|
||||
const cur = this.drives[drive];
|
||||
if (!isNibbleDisk(cur.disk)) {
|
||||
const curDisk = this.disks[drive];
|
||||
if (!isNibbleDisk(curDisk)) {
|
||||
return null;
|
||||
}
|
||||
const data: string[][] | string[] = [];
|
||||
for (let t = 0; t < cur.disk.tracks.length; t++) {
|
||||
if (isNibbleDisk(cur.disk)) {
|
||||
data[t] = base64_encode(cur.disk.tracks[t]);
|
||||
for (let t = 0; t < curDisk.tracks.length; t++) {
|
||||
if (isNibbleDisk(curDisk)) {
|
||||
data[t] = base64_encode(curDisk.tracks[t]);
|
||||
} else {
|
||||
const track: string[] = [];
|
||||
for (let s = 0; s < 0x10; s++) {
|
||||
track[s] = base64_encode(readSector(cur.disk, t, s));
|
||||
track[s] = base64_encode(readSector(curDisk, t, s));
|
||||
}
|
||||
data[t] = track;
|
||||
}
|
||||
|
|
|
@ -824,7 +824,7 @@ class TestDiskReader {
|
|||
|
||||
rawTracks() {
|
||||
// NOTE(flan): Hack to access private properties.
|
||||
const disk = (this.diskII as unknown as { cur: { disk: WozDisk } }).cur.disk;
|
||||
const disk = (this.diskII as unknown as { curDisk: WozDisk }).curDisk;
|
||||
const result: Uint8Array[] = [];
|
||||
for (let i = 0; i < disk.rawTracks.length; i++) {
|
||||
result[i] = disk.rawTracks[i].slice(0);
|
||||
|
|
Loading…
Reference in New Issue