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, PROCESS_JSON,
MassStorage, MassStorage,
MassStorageData, MassStorageData,
DiskMetadata,
SupportedSectors, SupportedSectors,
FloppyDisk, FloppyDisk,
FloppyFormat, FloppyFormat,
@ -218,10 +217,6 @@ export interface Callbacks {
/** Common information for Nibble and WOZ disks. */ /** Common information for Nibble and WOZ disks. */
interface Drive { interface Drive {
/** The disk in the drive. */
disk: FloppyDisk;
/** Metadata about the disk image */
metadata: DiskMetadata;
/** Whether the drive write protect is on. */ /** Whether the drive write protect is on. */
readOnly: boolean; readOnly: boolean;
/** Quarter track position of read/write head. */ /** Quarter track position of read/write head. */
@ -236,7 +231,6 @@ interface Drive {
interface DriveState { interface DriveState {
disk: FloppyDisk; disk: FloppyDisk;
metadata: DiskMetadata;
readOnly: boolean; readOnly: boolean;
track: byte; track: byte;
head: byte; head: byte;
@ -252,19 +246,6 @@ interface State {
controllerState: ControllerState; 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: NoFloppyDisk): NoFloppyDisk;
function getDiskState(disk: NibbleDisk): NibbleDisk; function getDiskState(disk: NibbleDisk): NibbleDisk;
function getDiskState(disk: WozDisk): WozDisk; function getDiskState(disk: WozDisk): WozDisk;
@ -315,20 +296,6 @@ function getDiskState(disk: FloppyDisk): FloppyDisk {
throw new Error('Unknown drive state'); 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. * 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> = { private drives: Record<DriveNumber, Drive> = {
1: { // Drive 1 1: { // Drive 1
disk: {
encoding: NO_DISK,
readOnly: true,
metadata: { name: 'Disk 1' },
},
track: 0, track: 0,
head: 0, head: 0,
phase: 0, phase: 0,
readOnly: false, readOnly: false,
dirty: false, dirty: false,
metadata: { name: 'Disk 1' },
}, },
2: { // Drive 2 2: { // Drive 2
disk: {
encoding: NO_DISK,
readOnly: true,
metadata: { name: 'Disk 2' },
},
track: 0, track: 0,
head: 0, head: 0,
phase: 0, phase: 0,
readOnly: false, readOnly: false,
dirty: 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' }, metadata: { name: 'Disk 2' },
} }
}; };
@ -373,8 +341,10 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
private skip = 0; private skip = 0;
/** Drive off timeout id or null. */ /** Drive off timeout id or null. */
private offTimeout: number | null = null; private offTimeout: number | null = null;
/** Current drive object. */ /** Current drive object. Must only be set by `updateActiveDrive()`. */
private cur: Drive; private curDrive: Drive;
/** Current disk object. Must only be set by `updateActiveDrive()`. */
private curDisk: FloppyDisk;
/** Nibbles read this on cycle */ /** Nibbles read this on cycle */
private nibbleCount = 0; private nibbleCount = 0;
@ -412,17 +382,23 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
state: 2, state: 2,
}; };
this.cur = this.drives[this.state.drive]; this.updateActiveDrive();
this.initWorker(); 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[]) { private debug(..._args: unknown[]) {
// debug(..._args); // debug(..._args);
} }
public head(): number { 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; let workCycles = (cycles - this.lastCycles) * 2;
this.lastCycles = cycles; this.lastCycles = cycles;
if (!isWozDisk(this.cur.disk)) { if (!isWozDisk(this.curDisk)) {
return; return;
} }
const track = 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; const state = this.state;
while (workCycles-- > 0) { while (workCycles-- > 0) {
let pulse: number = 0; let pulse: number = 0;
if (state.clock === 4) { if (state.clock === 4) {
pulse = track[this.cur.head]; pulse = track[this.curDrive.head];
if (!pulse) { if (!pulse) {
// More than 2 zeros can not be read reliably. // More than 2 zeros can not be read reliably.
if (++this.zeros > 2) { if (++this.zeros > 2) {
@ -511,7 +487,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
break; break;
case 0xA: // SR case 0xA: // SR
state.latch >>= 1; state.latch >>= 1;
if (this.cur.readOnly) { if (this.curDrive.readOnly) {
state.latch |= 0x80; state.latch |= 0x80;
} }
break; break;
@ -530,12 +506,12 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
if (state.clock === 4) { if (state.clock === 4) {
if (state.on) { if (state.on) {
if (state.q7) { 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); this.debug('Wrote', state.state & 0x8 ? 0x01 : 0x00);
} }
if (++this.cur.head >= track.length) { if (++this.curDrive.head >= track.length) {
this.cur.head = 0; this.curDrive.head = 0;
} }
} }
} }
@ -548,29 +524,29 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
// Only called for non-WOZ disks // Only called for non-WOZ disks
private readWriteNext() { private readWriteNext() {
if (!isNibbleDisk(this.cur.disk)) { if (!isNibbleDisk(this.curDisk)) {
return; return;
} }
const state = this.state; const state = this.state;
if (state.on && (this.skip || state.q7)) { 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 (track && track.length) {
if (this.cur.head >= track.length) { if (this.curDrive.head >= track.length) {
this.cur.head = 0; this.curDrive.head = 0;
} }
if (state.q7) { if (state.q7) {
if (!this.cur.readOnly) { if (!this.curDrive.readOnly) {
track[this.cur.head] = state.bus; track[this.curDrive.head] = state.bus;
if (!this.cur.dirty) { if (!this.curDrive.dirty) {
this.updateDirty(state.drive, true); this.updateDirty(state.drive, true);
} }
} }
} else { } else {
state.latch = track[this.cur.head]; state.latch = track[this.curDrive.head];
} }
++this.cur.head; ++this.curDrive.head;
} }
} else { } else {
state.latch = 0; state.latch = 0;
@ -599,24 +575,24 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
this.debug(`phase ${phase}${on ? ' on' : ' off'}`); this.debug(`phase ${phase}${on ? ' on' : ' off'}`);
if (on) { if (on) {
this.cur.track += PHASE_DELTA[this.cur.phase][phase] * 2; this.curDrive.track += PHASE_DELTA[this.curDrive.phase][phase] * 2;
this.cur.phase = phase; this.curDrive.phase = phase;
} }
// The emulator clamps the track to the valid track range available // 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 // 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 // at least a half track, usually a full track. Some 3rd party
// drives can seek to track 39. // drives can seek to track 39.
const maxTrack = isNibbleDisk(this.cur.disk) const maxTrack = isNibbleDisk(this.curDisk)
? this.cur.disk.tracks.length * 4 - 1 ? this.curDisk.tracks.length * 4 - 1
: (isWozDisk(this.cur.disk) : (isWozDisk(this.curDisk)
? this.cur.disk.trackMap.length - 1 ? this.curDisk.trackMap.length - 1
: 0); : 0);
if (this.cur.track > maxTrack) { if (this.curDrive.track > maxTrack) {
this.cur.track = maxTrack; this.curDrive.track = maxTrack;
} }
if (this.cur.track < 0x0) { if (this.curDrive.track < 0x0) {
this.cur.track = 0x0; this.curDrive.track = 0x0;
} }
// debug( // debug(
@ -688,7 +664,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
case LOC.DRIVE1: // 0x0a case LOC.DRIVE1: // 0x0a
this.debug('Disk 1'); this.debug('Disk 1');
state.drive = 1; state.drive = 1;
this.cur = this.drives[state.drive]; this.updateActiveDrive();
if (state.on) { if (state.on) {
this.callbacks.driveLight(2, false); this.callbacks.driveLight(2, false);
this.callbacks.driveLight(1, true); this.callbacks.driveLight(1, true);
@ -697,7 +673,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
case LOC.DRIVE2: // 0x0b case LOC.DRIVE2: // 0x0b
this.debug('Disk 2'); this.debug('Disk 2');
state.drive = 2; state.drive = 2;
this.cur = this.drives[state.drive]; this.updateActiveDrive();
if (state.on) { if (state.on) {
this.callbacks.driveLight(1, false); this.callbacks.driveLight(1, false);
this.callbacks.driveLight(2, true); this.callbacks.driveLight(2, true);
@ -709,7 +685,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
if (state.q7) { if (state.q7) {
this.debug('clearing _q6/SHIFT'); this.debug('clearing _q6/SHIFT');
} }
if (isNibbleDisk(this.cur.disk)) { if (isNibbleDisk(this.curDisk)) {
this.readWriteNext(); this.readWriteNext();
} }
break; break;
@ -719,9 +695,9 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
if (state.q7) { if (state.q7) {
this.debug('setting _q6/LOAD'); this.debug('setting _q6/LOAD');
} }
if (isNibbleDisk(this.cur.disk)) { if (isNibbleDisk(this.curDisk)) {
if (readMode && !state.q7) { if (readMode && !state.q7) {
if (this.cur.readOnly) { if (this.curDrive.readOnly) {
state.latch = 0xff; state.latch = 0xff;
this.debug('Setting readOnly'); this.debug('Setting readOnly');
} else { } else {
@ -794,38 +770,65 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
state.q7 = false; state.q7 = false;
state.on = false; state.on = false;
state.drive = 1; state.drive = 1;
this.cur = this.drives[state.drive];
} }
this.updateActiveDrive();
} }
tick() { tick() {
this.moveHead(); 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 { getState(): State {
const result = { const result = {
drives: [] as DriveState[], drives: [] as DriveState[],
skip: this.skip, skip: this.skip,
controllerState: { ...this.state }, controllerState: { ...this.state },
}; };
result.drives[1] = getDriveState(this.drives[1]); result.drives[1] = this.getDriveState(1);
result.drives[2] = getDriveState(this.drives[2]); result.drives[2] = this.getDriveState(2);
return result; 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) { setState(state: State) {
this.skip = state.skip; this.skip = state.skip;
this.state = { ...state.controllerState }; this.state = { ...state.controllerState };
for (const d of DRIVE_NUMBERS) { for (const d of DRIVE_NUMBERS) {
this.drives[d] = setDriveState(state.drives[d]); this.setDriveState(d, state.drives[d]);
const { name, side } = state.drives[d].metadata; const { name, side } = state.drives[d].disk.metadata;
const { dirty } = state.drives[d]; const { dirty } = state.drives[d];
this.callbacks.label(d, name, side); this.callbacks.label(d, name, side);
this.callbacks.driveLight(d, this.state.on); this.callbacks.driveLight(d, this.state.on);
this.callbacks.dirty(d, dirty); this.callbacks.dirty(d, dirty);
} }
this.cur = this.drives[this.state.drive]; this.updateActiveDrive();
} }
getMetadata(driveNo: DriveNumber) { getMetadata(driveNo: DriveNumber) {
@ -840,12 +843,12 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
} }
// TODO(flan): Does not work on WOZ disks // TODO(flan): Does not work on WOZ disks
rwts(disk: DriveNumber, track: byte, sector: byte) { rwts(drive: DriveNumber, track: byte, sector: byte) {
const cur = this.drives[disk]; const curDisk = this.disks[drive];
if (!isNibbleDisk(cur.disk)) { if (!isNibbleDisk(curDisk)) {
throw new Error('Can\'t read WOZ disks'); 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. */ /** 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) { getJSON(drive: DriveNumber, pretty: boolean = false) {
const cur = this.drives[drive]; const curDisk = this.disks[drive];
if (!isNibbleDisk(cur.disk)) { if (!isNibbleDisk(curDisk)) {
throw new Error('Can\'t save WOZ disks to JSON'); throw new Error('Can\'t save WOZ disks to JSON');
} }
return jsonEncode(cur.disk, pretty); return jsonEncode(curDisk, pretty);
} }
setJSON(drive: DriveNumber, json: string) { setJSON(drive: DriveNumber, json: string) {
@ -955,22 +958,22 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
} }
private insertDisk(drive: DriveNumber, disk: FloppyDisk) { private insertDisk(drive: DriveNumber, disk: FloppyDisk) {
const cur = this.drives[drive]; this.disks[drive] = disk;
cur.disk = disk as WozDisk | NibbleDisk; this.drives[drive].head = 0;
cur.metadata = disk.metadata; this.updateActiveDrive();
const { name, side } = cur.metadata; const { name, side } = disk.metadata;
this.updateDirty(drive, true); this.updateDirty(drive, true);
this.callbacks.label(drive, name, side); this.callbacks.label(drive, name, side);
} }
// TODO(flan): Does not work with WOZ or D13 disks // TODO(flan): Does not work with WOZ or D13 disks
getBinary(drive: DriveNumber, ext?: Exclude<NibbleFormat, 'woz' | 'd13'>): MassStorageData | null { getBinary(drive: DriveNumber, ext?: Exclude<NibbleFormat, 'woz' | 'd13'>): MassStorageData | null {
const cur = this.drives[drive]; const curDisk = this.disks[drive];
if (!isNibbleDisk(cur.disk)) { if (!isNibbleDisk(curDisk)) {
return null; return null;
} }
const { format, readOnly, tracks, volume } = cur.disk; const { format, readOnly, tracks, volume } = curDisk;
const { name } = cur.metadata; const { name } = curDisk.metadata;
const len = format === 'nib' ? const len = format === 'nib' ?
tracks.reduce((acc, track) => acc + track.length, 0) : tracks.reduce((acc, track) => acc + track.length, 0) :
this.sectors * tracks.length * 256; this.sectors * tracks.length * 256;
@ -984,7 +987,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
idx += tracks[t].length; idx += tracks[t].length;
} else { } else {
for (let s = 0; s < 0x10; s++) { 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); data.set(sector, idx);
idx += sector.length; 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 // TODO(flan): Does not work with WOZ or D13 disks
getBase64(drive: DriveNumber) { getBase64(drive: DriveNumber) {
const cur = this.drives[drive]; const curDisk = this.disks[drive];
if (!isNibbleDisk(cur.disk)) { if (!isNibbleDisk(curDisk)) {
return null; return null;
} }
const data: string[][] | string[] = []; const data: string[][] | string[] = [];
for (let t = 0; t < cur.disk.tracks.length; t++) { for (let t = 0; t < curDisk.tracks.length; t++) {
if (isNibbleDisk(cur.disk)) { if (isNibbleDisk(curDisk)) {
data[t] = base64_encode(cur.disk.tracks[t]); data[t] = base64_encode(curDisk.tracks[t]);
} else { } else {
const track: string[] = []; const track: string[] = [];
for (let s = 0; s < 0x10; s++) { 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; data[t] = track;
} }

View File

@ -824,7 +824,7 @@ class TestDiskReader {
rawTracks() { rawTracks() {
// NOTE(flan): Hack to access private properties. // 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[] = []; const result: Uint8Array[] = [];
for (let i = 0; i < disk.rawTracks.length; i++) { for (let i = 0; i < disk.rawTracks.length; i++) {
result[i] = disk.rawTracks[i].slice(0); result[i] = disk.rawTracks[i].slice(0);