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:
Ian Flanigan 2022-09-17 10:38:01 +02:00
parent f98aa93f24
commit 2c01448f07
No known key found for this signature in database
GPG Key ID: 035F657DAE4AE7EC
2 changed files with 111 additions and 108 deletions

View File

@ -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;
}

View File

@ -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);