Split handling of nibble disks and WOZ disks into separate drivers

Before, the `DiskII` object took care of both nibble disks and WOZ
disks at the same time. This made it hard to see which code affected
which disks.

Now, nibble disks are handled by the `NibbleDiskDriver` and WOZ disks
are handled by the `WozDiskDriver`. This separation of code should
lead to easeir testing and, perhaps, faster evolution of disk
handling.

An upcoming change will move the `NibbleDiskDriver` and the
`WozDiskDriver` to separate files.

This passes all tests, compiles, and runs both nibble disks and WOZ
disks.
This commit is contained in:
Ian Flanigan 2022-09-18 10:24:24 +02:00
parent e280c3d7b8
commit 51ba03ac28
No known key found for this signature in database
GPG Key ID: 035F657DAE4AE7EC
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);