Merge pull request #161 from iflan/split-out-drivers

Split handling of nibble disks and WOZ disks into separate drivers
This commit is contained in:
Will Scullin 2022-10-01 11:16:14 -07:00 committed by GitHub
commit 0be830784a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 462 additions and 197 deletions

View File

@ -132,15 +132,16 @@ const SEQUENCER_ROM_16 = [
const SEQUENCER_ROM: Record<SupportedSectors, ReadonlyArray<byte>> = { const SEQUENCER_ROM: Record<SupportedSectors, ReadonlyArray<byte>> = {
13: SEQUENCER_ROM_13, 13: SEQUENCER_ROM_13,
16: SEQUENCER_ROM_16, 16: SEQUENCER_ROM_16,
}; } as const;
/** Contents of the P5 ROM at 0xCnXX. */ /** Contents of the P5 ROM at 0xCnXX. */
const BOOTSTRAP_ROM: Record<SupportedSectors, ReadonlyUint8Array> = { const BOOTSTRAP_ROM: Record<SupportedSectors, ReadonlyUint8Array> = {
13: BOOTSTRAP_ROM_13, 13: BOOTSTRAP_ROM_13,
16: BOOTSTRAP_ROM_16, 16: BOOTSTRAP_ROM_16,
}; } as const;
type LssClockCycle = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; type LssClockCycle = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
type LssState = nibble;
type Phase = 0 | 1 | 2 | 3; type Phase = 0 | 1 | 2 | 3;
/** /**
@ -175,7 +176,7 @@ const PHASE_DELTA = [
/** /**
* State of the controller. * State of the controller.
*/ */
interface ControllerState { interface ControllerState {
/** Sectors supported by the controller. */ /** Sectors supported by the controller. */
sectors: SupportedSectors; sectors: SupportedSectors;
@ -201,6 +202,20 @@ const PHASE_DELTA = [
bus: byte; bus: byte;
} }
/** Interface for drivers for various disk types. */
interface DiskDriver {
tick(): void;
onQ6Low(): void;
onQ6High(readMode: boolean): void;
onDriveOn(): void;
onDriveOff(): void;
clampTrack(): void;
getState(): DriverState;
setState(state: DriverState): void;
}
type DriverState = EmptyDriverState | NibbleDiskDriverState | WozDiskDriverState;
/** Callbacks triggered by events of the drive or controller. */ /** Callbacks triggered by events of the drive or controller. */
export interface Callbacks { export interface Callbacks {
/** Called when a drive turns on or off. */ /** Called when a drive turns on or off. */
@ -231,6 +246,7 @@ interface Drive {
interface DriveState { interface DriveState {
disk: FloppyDisk; disk: FloppyDisk;
driver: DriverState;
readOnly: boolean; readOnly: boolean;
track: byte; track: byte;
head: byte; head: byte;
@ -242,7 +258,7 @@ interface DriveState {
// TODO(flan): It's unclear whether reusing ControllerState here is a good idea. // TODO(flan): It's unclear whether reusing ControllerState here is a good idea.
interface State { interface State {
drives: DriveState[]; drives: DriveState[];
skip: number; driver: DriverState[];
controllerState: ControllerState; controllerState: ControllerState;
} }
@ -255,7 +271,7 @@ function getDiskState(disk: FloppyDisk): FloppyDisk {
const { encoding, metadata, readOnly } = disk; const { encoding, metadata, readOnly } = disk;
return { return {
encoding, encoding,
metadata: {...metadata}, metadata: { ...metadata },
readOnly, readOnly,
}; };
} }
@ -296,61 +312,223 @@ function getDiskState(disk: FloppyDisk): FloppyDisk {
throw new Error('Unknown drive state'); throw new Error('Unknown drive state');
} }
/** interface EmptyDriverState { }
* Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller.
*/
export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
private drives: Record<DriveNumber, Drive> = { class EmptyDriver implements DiskDriver {
1: { // Drive 1 constructor(private readonly drive: Drive) { }
track: 0,
head: 0, tick(): void {
phase: 0, // do nothing
readOnly: false, }
dirty: false,
}, onQ6Low(): void {
2: { // Drive 2 // do nothing
track: 0, }
head: 0,
phase: 0, onQ6High(_readMode: boolean): void {
readOnly: false, // do nothing
dirty: false, }
onDriveOn(): void {
// do nothing
}
onDriveOff(): void {
// do nothing
}
clampTrack(): void {
// For empty drives, the emulator clamps the track to 0 to 34,
// 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.
if (this.drive.track < 0) {
this.drive.track = 0;
} }
}; if (this.drive.track > 34) {
this.drive.track = 34;
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' },
} }
}; }
private state: ControllerState; getState() {
return {};
}
setState(_state: EmptyDriverState): void {
// do nothing
}
}
abstract class BaseDiskDriver implements DiskDriver {
constructor(
protected readonly driveNumber: DriveNumber,
protected readonly drive: Drive,
protected readonly disk: NibbleDisk | WozDisk,
protected readonly controller: ControllerState) { }
/** Called frequently to ensure the disk is spinning. */
abstract tick(): void;
/** Called when Q6 is set LOW. */
abstract onQ6Low(): void;
/** Called when Q6 is set HIGH. */
abstract onQ6High(readMode: boolean): void;
/**
* Called when drive is turned on. This is guaranteed to be called
* only when the associated drive is toggled from off to on. This
* is also guaranteed to be called when a new disk is inserted when
* the drive is already on.
*/
abstract onDriveOn(): void;
/**
* Called when drive is turned off. This is guaranteed to be called
* only when the associated drive is toggled from on to off.
*/
abstract onDriveOff(): void;
debug(..._args: unknown[]) {
// debug(...args);
}
/**
* Called every time the head moves to clamp the track to a valid
* range.
*/
abstract clampTrack(): void;
isOn(): boolean {
return this.controller.on && this.controller.drive === this.driveNumber;
}
isWriteProtected(): boolean {
return this.drive.readOnly;
}
abstract getState(): DriverState;
abstract setState(state: DriverState): void;
}
interface NibbleDiskDriverState {
skip: number;
nibbleCount: number;
}
class NibbleDiskDriver extends BaseDiskDriver {
/** /**
* When `1`, the next nibble will be available for read; when `0`, * When `1`, the next nibble will be available for read; when `0`,
* the card is pretending to wait for data to be shifted in by the * the card is pretending to wait for data to be shifted in by the
* sequencer. * sequencer.
*/ */
private skip = 0; private skip: number = 0;
/** Drive off timeout id or null. */ /** Number of nibbles reads since the drive was turned on. */
private offTimeout: number | null = null; private nibbleCount: number = 0;
/** 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 */ constructor(
private nibbleCount = 0; driveNumber: DriveNumber,
drive: Drive,
readonly disk: NibbleDisk,
controller: ControllerState,
private readonly onDirty: () => void) {
super(driveNumber, drive, disk, controller);
}
tick(): void {
// do nothing
}
onQ6Low(): void {
const drive = this.drive;
const disk = this.disk;
if (this.isOn() && (this.skip || this.controller.q7)) {
const track = disk.tracks[drive.track >> 2];
if (track && track.length) {
if (drive.head >= track.length) {
drive.head = 0;
}
if (this.controller.q7) {
const writeProtected = disk.readOnly;
if (!writeProtected) {
track[drive.head] = this.controller.bus;
drive.dirty = true;
this.onDirty();
}
} else {
this.controller.latch = track[drive.head];
this.nibbleCount++;
}
++drive.head;
}
} else {
this.controller.latch = 0;
}
this.skip = (++this.skip % 2);
}
onQ6High(readMode: boolean): void {
const drive = this.drive;
if (readMode && !this.controller.q7) {
const writeProtected = drive.readOnly;
if (writeProtected) {
this.controller.latch = 0xff;
this.debug('Setting readOnly');
} else {
this.controller.latch >>= 1;
this.debug('Clearing readOnly');
}
}
}
onDriveOn(): void {
this.nibbleCount = 0;
}
onDriveOff(): void {
this.debug('nibbles read', this.nibbleCount);
}
clampTrack(): void {
// For NibbleDisks, the emulator clamps the track to the available
// range.
if (this.drive.track < 0) {
this.drive.track = 0;
}
const lastTrack = 35 * 4 - 1;
if (this.drive.track > lastTrack) {
this.drive.track = lastTrack;
}
}
getState(): NibbleDiskDriverState {
const { skip, nibbleCount } = this;
return { skip, nibbleCount };
}
setState(state: NibbleDiskDriverState) {
this.skip = state.skip;
this.nibbleCount = state.nibbleCount;
}
}
interface WozDiskDriverState {
clock: LssClockCycle;
state: LssState;
lastCycles: number;
zeros: number;
}
class WozDiskDriver extends BaseDiskDriver {
/** Logic state sequencer clock cycle. */
private clock: LssClockCycle;
/** Logic state sequencer state. */
private state: LssState;
/** Current CPU cycle count. */ /** Current CPU cycle count. */
private lastCycles = 0; private lastCycles: number = 0;
/** /**
* Number of zeros read in a row. The Disk ][ can only read two zeros in a * Number of zeros read in a row. The Disk ][ can only read two zeros in a
* row reliably; above that and the drive starts reporting garbage. See * row reliably; above that and the drive starts reporting garbage. See
@ -358,47 +536,30 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
*/ */
private zeros = 0; private zeros = 0;
private worker: Worker; constructor(
driveNumber: DriveNumber,
drive: Drive,
readonly disk: WozDisk,
controller: ControllerState,
private readonly onDirty: () => void,
private readonly io: Apple2IO) {
super(driveNumber, drive, disk, controller);
/** Builds a new Disk ][ card. */ // From the example in UtA2e, p. 9-29, col. 1, para. 1., this is
constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors: SupportedSectors = 16) { // essentially the start of the sequencer loop and produces
this.debug('Disk ]['); // correctly synced nibbles immediately. Starting at state 0
// would introduce a spurrious 1 in the latch at the beginning,
// which requires reading several more sync bytes to sync up.
this.state = 2;
this.clock = 0;
}
onDriveOn(): void {
this.lastCycles = this.io.cycles(); this.lastCycles = this.io.cycles();
this.state = {
sectors,
bus: 0,
latch: 0,
drive: 1,
on: false,
q6: false,
q7: false,
clock: 0,
// From the example in UtA2e, p. 9-29, col. 1, para. 1., this is
// essentially the start of the sequencer loop and produces
// correctly synced nibbles immediately. Starting at state 0
// would introduce a spurrious 1 in the latch at the beginning,
// which requires reading several more sync bytes to sync up.
state: 2,
};
this.updateActiveDrive();
this.initWorker();
} }
/** Updates the active drive based on the controller state. */ onDriveOff(): void {
private updateActiveDrive() { // nothing
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.curDrive.head;
} }
/** /**
@ -443,22 +604,27 @@ 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.curDisk)) { const drive = this.drive;
return; const disk = this.disk;
} const controller = this.controller;
// TODO(flan): Improve unformatted track behavior. The WOZ
// documentation suggests using an empty track of 6400 bytes
// (51,200 bits).
const track = const track =
this.curDisk.rawTracks[this.curDisk.trackMap[this.curDrive.track]] || [0]; disk.rawTracks[disk.trackMap[drive.track]] || [0];
const state = this.state;
while (workCycles-- > 0) { while (workCycles-- > 0) {
let pulse: number = 0; let pulse: number = 0;
if (state.clock === 4) { if (this.clock === 4) {
pulse = track[this.curDrive.head]; pulse = track[drive.head];
if (!pulse) { if (!pulse) {
// More than 2 zeros can not be read reliably. // More than 2 zeros can not be read reliably.
// TODO(flan): Revisit with the new MC3470
// suggested 4-bit window behavior.
if (++this.zeros > 2) { if (++this.zeros > 2) {
pulse = Math.random() >= 0.5 ? 1 : 0; const r = Math.random();
pulse = r >= 0.5 ? 1 : 0;
} }
} else { } else {
this.zeros = 0; this.zeros = 0;
@ -467,91 +633,197 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
let idx = 0; let idx = 0;
idx |= pulse ? 0x00 : 0x01; idx |= pulse ? 0x00 : 0x01;
idx |= state.latch & 0x80 ? 0x02 : 0x00; idx |= controller.latch & 0x80 ? 0x02 : 0x00;
idx |= state.q6 ? 0x04 : 0x00; idx |= controller.q6 ? 0x04 : 0x00;
idx |= state.q7 ? 0x08 : 0x00; idx |= controller.q7 ? 0x08 : 0x00;
idx |= state.state << 4; idx |= this.state << 4;
const command = SEQUENCER_ROM[this.sectors][idx]; const command = SEQUENCER_ROM[controller.sectors][idx];
this.debug(`clock: ${state.clock} state: ${toHex(state.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${state.q6} latch: ${toHex(state.latch)}`); this.debug(`clock: ${this.clock} state: ${toHex(this.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${controller.q6} latch: ${toHex(controller.latch)}`);
switch (command & 0xf) { switch (command & 0xf) {
case 0x0: // CLR case 0x0: // CLR
state.latch = 0; controller.latch = 0;
break; break;
case 0x8: // NOP case 0x8: // NOP
break; break;
case 0x9: // SL0 case 0x9: // SL0
state.latch = (state.latch << 1) & 0xff; controller.latch = (controller.latch << 1) & 0xff;
break; break;
case 0xA: // SR case 0xA: // SR
state.latch >>= 1; controller.latch >>= 1;
if (this.curDrive.readOnly) { if (this.isWriteProtected()) {
state.latch |= 0x80; controller.latch |= 0x80;
} }
break; break;
case 0xB: // LD case 0xB: // LD
state.latch = state.bus; controller.latch = controller.bus;
this.debug('Loading', toHex(state.latch), 'from bus'); this.debug('Loading', toHex(controller.latch), 'from bus');
break; break;
case 0xD: // SL1 case 0xD: // SL1
state.latch = ((state.latch << 1) | 0x01) & 0xff; controller.latch = ((controller.latch << 1) | 0x01) & 0xff;
break; break;
default: default:
this.debug(`unknown command: ${toHex(command & 0xf)}`); this.debug(`unknown command: ${toHex(command & 0xf)}`);
} }
state.state = (command >> 4 & 0xF) as nibble; this.state = (command >> 4 & 0xF) as LssState;
if (state.clock === 4) { if (this.clock === 4) {
if (state.on) { if (this.isOn()) {
if (state.q7) { if (controller.q7) {
track[this.curDrive.head] = state.state & 0x8 ? 0x01 : 0x00; // TODO(flan): This assumes that writes are happening in
this.debug('Wrote', state.state & 0x8 ? 0x01 : 0x00); // a "friendly" way, namely where the track was originally
// written. To do this correctly, the virtual head should
// actually keep track of the current quarter track plus
// the one on each side. Then, when writing, it should
// check that all three are actually the same rawTrack. If
// they aren't, then the trackMap has to be updated as
// well.
track[drive.head] = this.state & 0x8 ? 0x01 : 0x00;
this.debug('Wrote', this.state & 0x8 ? 0x01 : 0x00);
drive.dirty = true;
this.onDirty();
} }
if (++this.curDrive.head >= track.length) { if (++drive.head >= track.length) {
this.curDrive.head = 0; drive.head = 0;
} }
} }
} }
if (++state.clock > 7) { if (++this.clock > 7) {
state.clock = 0; this.clock = 0;
} }
} }
} }
// Only called for non-WOZ disks tick(): void {
private readWriteNext() { this.moveHead();
if (!isNibbleDisk(this.curDisk)) { }
return;
}
const state = this.state;
if (state.on && (this.skip || state.q7)) {
const track = this.curDisk.tracks[this.curDrive.track >> 2];
if (track && track.length) {
if (this.curDrive.head >= track.length) {
this.curDrive.head = 0;
}
if (state.q7) { onQ6High(_readMode: boolean): void {
if (!this.curDrive.readOnly) { // nothing?
track[this.curDrive.head] = state.bus; }
if (!this.curDrive.dirty) {
this.updateDirty(state.drive, true);
}
}
} else {
state.latch = track[this.curDrive.head];
}
++this.curDrive.head; onQ6Low(): void {
} // nothing?
} else { }
state.latch = 0;
clampTrack(): void {
// For NibbleDisks, the emulator clamps the track to the available
// range.
if (this.drive.track < 0) {
this.drive.track = 0;
} }
this.skip = (++this.skip % 2); const lastTrack = this.disk.trackMap.length - 1;
if (this.drive.track > lastTrack) {
this.drive.track = lastTrack;
}
}
getState(): WozDiskDriverState {
const { clock, state, lastCycles, zeros } = this;
return { clock, state, lastCycles, zeros };
}
setState(state: WozDiskDriverState) {
this.clock = state.clock;
this.state = state.state;
this.lastCycles = state.lastCycles;
this.zeros = state.zeros;
}
}
/**
* Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller.
*/
export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
private drives: Record<DriveNumber, Drive> = {
1: { // Drive 1
track: 0,
head: 0,
phase: 0,
readOnly: false,
dirty: false,
},
2: { // Drive 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' },
}
};
private driver: Record<DriveNumber, DiskDriver> = {
1: new EmptyDriver(this.drives[1]),
2: new EmptyDriver(this.drives[2]),
};
private state: ControllerState;
/** Drive off timeout id or null. */
private offTimeout: number | null = null;
/** Current drive object. Must only be set by `updateActiveDrive()`. */
private curDrive: Drive;
/** Current driver object. Must only be set by `updateAcivetDrive()`. */
private curDriver: DiskDriver;
private worker: Worker;
/** Builds a new Disk ][ card. */
constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors: SupportedSectors = 16) {
this.debug('Disk ][');
this.state = {
sectors,
bus: 0,
latch: 0,
drive: 1,
on: false,
q6: false,
q7: false,
clock: 0,
// From the example in UtA2e, p. 9-29, col. 1, para. 1., this is
// essentially the start of the sequencer loop and produces
// correctly synced nibbles immediately. Starting at state 0
// would introduce a spurrious 1 in the latch at the beginning,
// which requires reading several more sync bytes to sync up.
state: 2,
};
this.updateActiveDrive();
this.initWorker();
}
/** Updates the active drive based on the controller state. */
private updateActiveDrive() {
this.curDrive = this.drives[this.state.drive];
this.curDriver = this.driver[this.state.drive];
}
private debug(..._args: unknown[]) {
// debug(..._args);
}
public head(): number {
return this.curDrive.head;
} }
/** /**
@ -579,21 +851,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
this.curDrive.phase = phase; this.curDrive.phase = phase;
} }
// The emulator clamps the track to the valid track range available this.curDriver.clampTrack();
// 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.curDisk)
? this.curDisk.tracks.length * 4 - 1
: (isWozDisk(this.curDisk)
? this.curDisk.trackMap.length - 1
: 0);
if (this.curDrive.track > maxTrack) {
this.curDrive.track = maxTrack;
}
if (this.curDrive.track < 0x0) {
this.curDrive.track = 0x0;
}
// debug( // debug(
// 'Drive', _drive, 'track', toHex(_cur.track >> 2) + '.' + (_cur.track & 0x3), // 'Drive', _drive, 'track', toHex(_cur.track >> 2) + '.' + (_cur.track & 0x3),
@ -641,7 +899,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
this.debug('Drive Off'); this.debug('Drive Off');
state.on = false; state.on = false;
this.callbacks.driveLight(state.drive, false); this.callbacks.driveLight(state.drive, false);
this.debug('nibbles read', this.nibbleCount); this.curDriver.onDriveOff();
}, 1000); }, 1000);
} }
} }
@ -654,10 +912,9 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
} }
if (!state.on) { if (!state.on) {
this.debug('Drive On'); this.debug('Drive On');
this.nibbleCount = 0;
state.on = true; state.on = true;
this.lastCycles = this.io.cycles();
this.callbacks.driveLight(state.drive, true); this.callbacks.driveLight(state.drive, true);
this.curDriver.onDriveOn();
} }
break; break;
@ -682,30 +939,12 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
case LOC.DRIVEREAD: // 0x0c (Q6L) Shift case LOC.DRIVEREAD: // 0x0c (Q6L) Shift
state.q6 = false; state.q6 = false;
if (state.q7) { this.curDriver.onQ6Low();
this.debug('clearing _q6/SHIFT');
}
if (isNibbleDisk(this.curDisk)) {
this.readWriteNext();
}
break; break;
case LOC.DRIVEWRITE: // 0x0d (Q6H) LOAD case LOC.DRIVEWRITE: // 0x0d (Q6H) LOAD
state.q6 = true; state.q6 = true;
if (state.q7) { this.curDriver.onQ6High(readMode);
this.debug('setting _q6/LOAD');
}
if (isNibbleDisk(this.curDisk)) {
if (readMode && !state.q7) {
if (this.curDrive.readOnly) {
state.latch = 0xff;
this.debug('Setting readOnly');
} else {
state.latch = state.latch >> 1;
this.debug('Clearing readOnly');
}
}
}
break; break;
case LOC.DRIVEREADMODE: // 0x0e (Q7L) case LOC.DRIVEREADMODE: // 0x0e (Q7L)
@ -721,7 +960,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
break; break;
} }
this.moveHead(); this.tick();
if (readMode) { if (readMode) {
// According to UtAIIe, p. 9-13 to 9-14, any even address can be // According to UtAIIe, p. 9-13 to 9-14, any even address can be
@ -729,9 +968,6 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
// also cause conflicts with the disk controller commands. // also cause conflicts with the disk controller commands.
if ((off & 0x01) === 0) { if ((off & 0x01) === 0) {
result = state.latch; result = state.latch;
if (result & 0x80) {
this.nibbleCount++;
}
} else { } else {
result = 0; result = 0;
} }
@ -775,27 +1011,29 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
} }
tick() { tick() {
this.moveHead(); this.curDriver.tick();
} }
private getDriveState(drive: DriveNumber): DriveState { private getDriveState(drive: DriveNumber): DriveState {
const curDrive = this.drives[drive]; const curDrive = this.drives[drive];
const curDisk = this.disks[drive]; const curDisk = this.disks[drive];
const curDriver = this.driver[drive];
const { readOnly, track, head, phase, dirty } = curDrive; const { readOnly, track, head, phase, dirty } = curDrive;
return { return {
disk: getDiskState(curDisk), disk: getDiskState(curDisk),
driver: curDriver.getState(),
readOnly, readOnly,
track, track,
head, head,
phase, phase,
dirty, dirty,
}; };
} }
getState(): State { getState(): State {
const result = { const result = {
drives: [] as DriveState[], drives: [] as DriveState[],
skip: this.skip, driver: [] as DriverState[],
controllerState: { ...this.state }, controllerState: { ...this.state },
}; };
result.drives[1] = this.getDriveState(1); result.drives[1] = this.getDriveState(1);
@ -813,12 +1051,13 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
readOnly, readOnly,
dirty, dirty,
}; };
this.disks[drive] = getDiskState(state.disk); const disk = getDiskState(state.disk);
this.setDiskInternal(drive, disk);
this.driver[drive].setState(state.driver);
} }
setState(state: State) { setState(state: State) {
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.setDriveState(d, state.drives[d]); this.setDriveState(d, state.drives[d]);
@ -957,12 +1196,38 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
} }
} }
private insertDisk(drive: DriveNumber, disk: FloppyDisk) { private setDiskInternal(drive: DriveNumber, disk: FloppyDisk) {
this.disks[drive] = disk; this.disks[drive] = disk;
this.drives[drive].head = 0; if (isNoFloppyDisk(disk)) {
this.driver[drive] = new EmptyDriver(this.drives[drive]);
} else if (isNibbleDisk(disk)) {
this.driver[drive] =
new NibbleDiskDriver(
drive,
this.drives[drive],
disk,
this.state,
() => this.updateDirty(drive, true));
} else if (isWozDisk(disk)) {
this.driver[drive] =
new WozDiskDriver(
drive,
this.drives[drive],
disk,
this.state,
() => this.updateDirty(drive, true),
this.io);
} else {
throw new Error(`Unknown disk format ${disk.encoding}`);
}
this.updateActiveDrive(); this.updateActiveDrive();
}
private insertDisk(drive: DriveNumber, disk: FloppyDisk) {
this.setDiskInternal(drive, disk);
this.drives[drive].head = 0;
const { name, side } = disk.metadata; const { name, side } = disk.metadata;
this.updateDirty(drive, true); this.updateDirty(drive, this.drives[drive].dirty);
this.callbacks.label(drive, name, side); this.callbacks.label(drive, name, side);
} }

View File

@ -66,7 +66,7 @@ describe('DiskII', () => {
const state = diskII.getState(); const state = diskII.getState();
// These are just arbitrary changes, not an exhaustive list of fields. // These are just arbitrary changes, not an exhaustive list of fields.
state.skip = 1; (state.drives[1].driver as {skip:number}).skip = 1;
state.controllerState.drive = 2; state.controllerState.drive = 2;
state.controllerState.latch = 0x42; state.controllerState.latch = 0x42;
state.controllerState.on = true; state.controllerState.on = true;
@ -97,7 +97,7 @@ describe('DiskII', () => {
expect(callbacks.label).toHaveBeenCalledWith(2, 'Disk 2', undefined); expect(callbacks.label).toHaveBeenCalledWith(2, 'Disk 2', undefined);
expect(callbacks.dirty).toHaveBeenCalledTimes(2); expect(callbacks.dirty).toHaveBeenCalledTimes(2);
expect(callbacks.dirty).toHaveBeenCalledWith(1, true); expect(callbacks.dirty).toHaveBeenCalledWith(1, false);
expect(callbacks.dirty).toHaveBeenCalledWith(2, false); expect(callbacks.dirty).toHaveBeenCalledWith(2, false);
}); });
@ -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 { curDisk: WozDisk }).curDisk; const disk = (this.diskII as unknown as { disks: WozDisk[] }).disks[1];
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);