From 41e0609f55a8a06bf4476782a49d6338dc6683da Mon Sep 17 00:00:00 2001 From: Ian Flanigan Date: Thu, 1 Sep 2022 03:55:01 +0200 Subject: [PATCH] Floppy controller refactorings 1 (#155) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add `DiskMetada` to the `Disk` interface Before, metadata about the image, such as name, side, etc. was mixed in with actual disk image information. This change breaks that information into a separate structure called `DiskMetadata`. Currently, the only two fields are `name` and `side`, but the idea is that more fields could be added as necessary, like a description, a scan of the disk or label, etc. In a follow-on change, the default write-protection status will come from the metadata as well. The current implementation copies the metadata when saving/restoring state, loading disk images, etc. In the future, the metadata should passed around until the format is required to change (like saving one disk image format as another). Likewise, in the future, in may be desirable to be able to override the disk image metadata with user-supplied metadata. This could be use, for example, to temporarily add or remove write-protection from a disk image. All existing tests pass and the emulator builds with no errors. * Rename `writeMode` to `q7` Before, nibble disk emulation used the `writeMode` field to keep track of whether the drive should be read from or written to, but the WOZ emulation used `q7` to keep track of the same state. This change renames `writeMode` to `q7` because it more accurately reflects the state of the Disk II controller as specified in the manuals, DOS source, and, especially, _Understanding the Apple //e_ by Jim Sather. * Remove the coil state Before, `q` captured the state of the coils. But it was never read. This change just deletes it. * Use the bootstrap and sequencer ROMs with indirection Before, the contents of the bootstrap ROM and sequencer ROM were set directly on fields of the controller. These were not saved or restored with the state in `getState` and `setState`. (It would have been very space inefficient if they had). Now, these ROMs are used from constants indexed by the number of sectors the card supports. This, in turn, means that if the number of sectors is saved with the state, it can be easily restored. * Split out the Disk II controller state This change factors the emulated hardware state into a separate structure in the Disk II controller. The idea is that this hardware state will be able to be shared with the WOZ and nibble disk code instead of sharing _all_ of the controller state (like callbacks and so forth). * Factor out disk insertion Before, several places in the code essentially inserted a new disk image into the drive, which similar—but not always exactly the same—code. Now there is an `insertDisk` method that is responsible for inserting a new `FloppyDisk`. All tests pass, everything compiles, manually tested nibble disks and WOZ disks. --- js/cards/cffa.ts | 9 +- js/cards/disk2.ts | 337 +++++++++++++++------------- js/cards/smartport.ts | 9 +- js/components/Apple2.tsx | 3 +- js/components/DownloadModal.tsx | 3 +- js/components/Drives.tsx | 4 +- js/components/debugger/Disks.tsx | 3 +- js/components/util/systems.ts | 4 +- js/formats/block.ts | 2 +- js/formats/d13.ts | 3 +- js/formats/do.ts | 3 +- js/formats/format_utils.ts | 2 +- js/formats/nib.ts | 3 +- js/formats/po.ts | 3 +- js/formats/types.ts | 25 ++- js/formats/woz.ts | 6 +- js/main2.ts | 3 +- test/js/cards/disk2.spec.ts | 8 +- test/js/formats/2mg.spec.ts | 2 +- test/js/formats/create_disk.spec.ts | 6 +- test/js/formats/woz.spec.ts | 8 +- workers/format.worker.ts | 4 +- 22 files changed, 243 insertions(+), 207 deletions(-) diff --git a/js/cards/cffa.ts b/js/cards/cffa.ts index aee0677..b3df6f6 100644 --- a/js/cards/cffa.ts +++ b/js/cards/cffa.ts @@ -390,7 +390,7 @@ export default class CFFA implements Card, MassStorage, Restorable< ), encoding: ENCODING_BLOCK, readOnly: disk.readOnly, - name: disk.name, + metadata: { ...disk.metadata }, }; } return result; @@ -441,7 +441,7 @@ export default class CFFA implements Card, MassStorage, Restorable< const prodos = new ProDOSVolume(disk); - this._name[drive] = disk.name; + this._name[drive] = disk.metadata.name; this._partitions[drive] = prodos; if (drive) { @@ -483,7 +483,8 @@ export default class CFFA implements Card, MassStorage, Restorable< if (!blockDisk) { return null; } - const { name, blocks, readOnly } = blockDisk; + const { blocks, readOnly } = blockDisk; + const { name } = blockDisk.metadata; let ext; let data: ArrayBuffer; if (this._metadata[drive]) { @@ -498,7 +499,7 @@ export default class CFFA implements Card, MassStorage, Restorable< data = dataArray.buffer; } return { - name, + metadata: { name }, ext, data, readOnly, diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index e21188c..e4faa9e 100644 --- a/js/cards/disk2.ts +++ b/js/cards/disk2.ts @@ -4,7 +4,7 @@ import type { Card, memory, nibble, - rom, + ReadonlyUint8Array, } from '../types'; import { @@ -22,6 +22,9 @@ import { ENCODING_BITSTREAM, MassStorage, MassStorageData, + DiskMetadata, + SupportedSectors, + FloppyDisk, } from '../formats/types'; import { @@ -121,6 +124,18 @@ const SEQUENCER_ROM_16 = [ 0xDD, 0x4D, 0xE0, 0xE0, 0x0A, 0x0A, 0x0A, 0x0A, 0x88, 0x88, 0x08, 0x08, 0x88, 0x88, 0x08, 0x08 // F ] as const; +/** Contents of the P6 sequencer ROM. */ +const SEQUENCER_ROM: Record> = { + 13: SEQUENCER_ROM_13, + 16: SEQUENCER_ROM_16, +}; + +/** Contents of the P5 ROM at 0xCnXX. */ +const BOOTSTRAP_ROM: Record = { + 13: BOOTSTRAP_ROM_13, + 16: BOOTSTRAP_ROM_16, +}; + type LssClockCycle = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; type Phase = 0 | 1 | 2 | 3; @@ -138,11 +153,10 @@ type Phase = 0 | 1 | 2 | 3; * head momentum. * * Examining the https://computerhistory.org/blog/apple-ii-dos-source-code/, - * one finds that the SEEK routine on line 4831 of `appdos31.lst`. It uses - * `ONTABLE` and `OFFTABLE` (each 12 bytes) to know exactly how many - * microseconds to power on/off each coil as the head accelerates. At the end, - * the final coil is left powered on 9.5 milliseconds to ensure the head has - * settled. + * one finds the SEEK routine on line 4831 of `appdos31.lst`. It uses `ONTABLE` + * and `OFFTABLE` (each 12 bytes) to know exactly how many microseconds to + * power on/off each coil as the head accelerates. At the end, the final coil + * is left powered on 9.5 milliseconds to ensure the head has settled. * * https://embeddedmicro.weebly.com/apple-2iie.html shows traces of the boot * seek (which is slightly different) and a regular seek. @@ -153,23 +167,56 @@ const PHASE_DELTA = [ [-2, -1, 0, 1], [1, -2, -1, 0] ] as const; + +/** + * State of the controller. + */ + interface ControllerState { + /** Sectors supported by the controller. */ + sectors: SupportedSectors; + + /** Is the active drive powered on? */ + on: boolean; + + /** The active drive. */ + drive: DriveNumber; + + /** The 8-cycle LSS clock. */ + clock: LssClockCycle; + /** Current state of the Logic State Sequencer. */ + state: nibble; + + /** Q6 (Shift/Load) */ + q6: boolean; + /** Q7 (Read/Write) */ + q7: boolean; + + /** Last data from the disk drive. */ + latch: byte; + /** Last data written by the CPU to card softswitch 0x8D. */ + bus: byte; +} + +/** Callbacks triggered by events of the drive or controller. */ export interface Callbacks { + /** Called when a drive turns on or off. */ driveLight: (drive: DriveNumber, on: boolean) => void; + /** + * Called when a disk has been written to. For performance and integrity, + * this is only called when the drive stops spinning or is removed from + * the drive. + */ dirty: (drive: DriveNumber, dirty: boolean) => void; + /** Called when a disk is inserted or removed from the drive. */ label: (drive: DriveNumber, name?: string, side?: string) => void; } /** Common information for Nibble and WOZ disks. */ - interface BaseDrive { /** Current disk format. */ format: NibbleFormat; /** Current disk volume number. */ volume: byte; - /** Displayed disk name */ - name: string; - /** (Optional) Disk side (Front/Back, A/B) */ - side?: string | undefined; /** Quarter track position of read/write head. */ track: byte; /** Position of the head on the track. */ @@ -180,6 +227,8 @@ interface BaseDrive { readOnly: boolean; /** Whether the drive has been written to since it was loaded. */ dirty: boolean; + /** Metadata about the disk image */ + metadata: DiskMetadata; } /** WOZ format track data from https://applesaucefdc.com/woz/reference2/. */ @@ -214,8 +263,6 @@ interface DriveState { format: NibbleFormat; encoding: typeof ENCODING_BITSTREAM | typeof ENCODING_NIBBLE; volume: byte; - name: string; - side?: string | undefined; tracks: memory[]; track: byte; head: byte; @@ -224,15 +271,15 @@ interface DriveState { dirty: boolean; trackMap: number[]; rawTracks: Uint8Array[]; + metadata: DiskMetadata; } +/** State of the controller for saving/restoring. */ +// TODO(flan): It's unclear whether reusing ControllerState here is a good idea. interface State { drives: DriveState[]; skip: number; - latch: number; - writeMode: boolean; - on: boolean; - drive: DriveNumber; + controllerState: ControllerState; } function getDriveState(drive: Drive): DriveState { @@ -240,8 +287,6 @@ function getDriveState(drive: Drive): DriveState { format: drive.format, encoding: drive.encoding, volume: drive.volume, - name: drive.name, - side: drive.side, tracks: [], track: drive.track, head: drive.head, @@ -250,6 +295,7 @@ function getDriveState(drive: Drive): DriveState { dirty: drive.dirty, trackMap: [], rawTracks: [], + metadata: { ...drive.metadata }, }; if (isNibbleDrive(drive)) { @@ -273,14 +319,13 @@ function setDriveState(state: DriveState) { format: state.format, encoding: ENCODING_NIBBLE, volume: state.volume, - name: state.name, - side: state.side, tracks: [], track: state.track, head: state.head, phase: state.phase, readOnly: state.readOnly, dirty: state.dirty, + metadata: { ...state.metadata }, }; for (let idx = 0; idx < state.tracks.length; idx++) { result.tracks.push(new Uint8Array(state.tracks[idx])); @@ -290,8 +335,6 @@ function setDriveState(state: DriveState) { format: state.format, encoding: ENCODING_BITSTREAM, volume: state.volume, - name: state.name, - side: state.side, track: state.track, head: state.head, phase: state.phase, @@ -299,6 +342,7 @@ function setDriveState(state: DriveState) { dirty: state.dirty, trackMap: [...state.trackMap], rawTracks: [], + metadata: { ...state.metadata }, }; for (let idx = 0; idx < state.rawTracks.length; idx++) { result.rawTracks.push(new Uint8Array(state.rawTracks[idx])); @@ -317,65 +361,46 @@ export default class DiskII implements Card, MassStorage { format: 'dsk', encoding: ENCODING_NIBBLE, volume: 254, - name: 'Disk 1', tracks: [], track: 0, head: 0, phase: 0, readOnly: false, dirty: false, + metadata: { name: 'Disk 1' }, }, 2: { // Drive 2 format: 'dsk', encoding: ENCODING_NIBBLE, volume: 254, - name: 'Disk 2', tracks: [], track: 0, head: 0, phase: 0, readOnly: false, dirty: false, + metadata: { name: 'Disk 2' }, } }; + private state: ControllerState; + /** * 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 * sequencer. */ private skip = 0; - /** Last data written by the CPU to card softswitch 0x8D. */ - private bus = 0; - /** Drive data register. */ - private latch = 0; /** Drive off timeout id or null. */ private offTimeout: number | null = null; - /** Q6 (Shift/Load): Used by WOZ disks. */ - private q6 = 0; - /** Q7 (Read/Write): Used by WOZ disks. */ - private q7: boolean = false; - /** Q7 (Read/Write): Used by Nibble disks. */ - private writeMode = false; - /** Whether the selected drive is on. */ - private on = false; - /** Current drive number (1, 2). */ - private drive: DriveNumber = 1; /** Current drive object. */ - private cur = this.drives[this.drive]; + private cur: Drive; /** Nibbles read this on cycle */ private nibbleCount = 0; - /** Q0-Q3: Coil states. */ - private q = [false, false, false, false]; - - /** The 8-cycle LSS clock. */ - private clock: LssClockCycle = 0; /** Current CPU cycle count. */ private lastCycles = 0; - /** Current state of the Logic State Sequencer. */ - private state: nibble = 0; /** * 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 @@ -383,26 +408,31 @@ export default class DiskII implements Card, MassStorage { */ private zeros = 0; - /** Contents of the P5 ROM at 0xCnXX. */ - private bootstrapRom: rom; - /** Contents of the P6 ROM. */ - private sequencerRom: typeof SEQUENCER_ROM_16 | typeof SEQUENCER_ROM_13; - private worker: Worker; /** Builds a new Disk ][ card. */ - constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors = 16) { + constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors: SupportedSectors = 16) { this.debug('Disk ]['); this.lastCycles = this.io.cycles(); - this.bootstrapRom = this.sectors === 16 ? BOOTSTRAP_ROM_16 : BOOTSTRAP_ROM_13; - this.sequencerRom = this.sectors === 16 ? SEQUENCER_ROM_16 : SEQUENCER_ROM_13; - // 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. - this.state = 2; + 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.cur = this.drives[this.state.drive]; this.initWorker(); } @@ -462,10 +492,12 @@ export default class DiskII implements Card, MassStorage { } const track = this.cur.rawTracks[this.cur.trackMap[this.cur.track]] || [0]; + + const state = this.state; while (workCycles-- > 0) { let pulse: number = 0; - if (this.clock === 4) { + if (state.clock === 4) { pulse = track[this.cur.head]; if (!pulse) { // More than 2 zeros can not be read reliably. @@ -479,47 +511,47 @@ export default class DiskII implements Card, MassStorage { let idx = 0; idx |= pulse ? 0x00 : 0x01; - idx |= this.latch & 0x80 ? 0x02 : 0x00; - idx |= this.q6 ? 0x04 : 0x00; - idx |= this.q7 ? 0x08 : 0x00; - idx |= this.state << 4; + idx |= state.latch & 0x80 ? 0x02 : 0x00; + idx |= state.q6 ? 0x04 : 0x00; + idx |= state.q7 ? 0x08 : 0x00; + idx |= state.state << 4; - const command = this.sequencerRom[idx]; + const command = SEQUENCER_ROM[this.sectors][idx]; - this.debug(`clock: ${this.clock} state: ${toHex(this.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${this.q6} latch: ${toHex(this.latch)}`); + this.debug(`clock: ${state.clock} state: ${toHex(state.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${state.q6} latch: ${toHex(state.latch)}`); switch (command & 0xf) { case 0x0: // CLR - this.latch = 0; + state.latch = 0; break; case 0x8: // NOP break; case 0x9: // SL0 - this.latch = (this.latch << 1) & 0xff; + state.latch = (state.latch << 1) & 0xff; break; case 0xA: // SR - this.latch >>= 1; + state.latch >>= 1; if (this.cur.readOnly) { - this.latch |= 0x80; + state.latch |= 0x80; } break; case 0xB: // LD - this.latch = this.bus; - this.debug('Loading', toHex(this.latch), 'from bus'); + state.latch = state.bus; + this.debug('Loading', toHex(state.latch), 'from bus'); break; case 0xD: // SL1 - this.latch = ((this.latch << 1) | 0x01) & 0xff; + state.latch = ((state.latch << 1) | 0x01) & 0xff; break; default: this.debug(`unknown command: ${toHex(command & 0xf)}`); } - this.state = (command >> 4 & 0xF) as nibble; + state.state = (command >> 4 & 0xF) as nibble; - if (this.clock === 4) { - if (this.on) { - if (this.q7) { - track[this.cur.head] = this.state & 0x8 ? 0x01 : 0x00; - this.debug('Wrote', this.state & 0x8 ? 0x01 : 0x00); + if (state.clock === 4) { + if (state.on) { + if (state.q7) { + track[this.cur.head] = state.state & 0x8 ? 0x01 : 0x00; + this.debug('Wrote', state.state & 0x8 ? 0x01 : 0x00); } if (++this.cur.head >= track.length) { @@ -528,8 +560,8 @@ export default class DiskII implements Card, MassStorage { } } - if (++this.clock > 7) { - this.clock = 0; + if (++state.clock > 7) { + state.clock = 0; } } } @@ -539,28 +571,29 @@ export default class DiskII implements Card, MassStorage { if (!isNibbleDrive(this.cur)) { return; } - if (this.on && (this.skip || this.writeMode)) { + const state = this.state; + if (state.on && (this.skip || state.q7)) { const track = this.cur.tracks[this.cur.track >> 2]; if (track && track.length) { if (this.cur.head >= track.length) { this.cur.head = 0; } - if (this.writeMode) { + if (state.q7) { if (!this.cur.readOnly) { - track[this.cur.head] = this.bus; + track[this.cur.head] = state.bus; if (!this.cur.dirty) { - this.updateDirty(this.drive, true); + this.updateDirty(state.drive, true); } } } else { - this.latch = track[this.cur.head]; + state.latch = track[this.cur.head]; } ++this.cur.head; } } else { - this.latch = 0; + state.latch = 0; } this.skip = (++this.skip % 2); } @@ -579,7 +612,7 @@ export default class DiskII implements Card, MassStorage { // 5. [...] enables head positioning [...] // // Therefore do nothing if no drive is on. - if (!this.on) { + if (!this.state.on) { this.debug(`ignoring phase ${phase}${on ? ' on' : ' off'}`); return; } @@ -608,11 +641,10 @@ export default class DiskII implements Card, MassStorage { // 'Drive', _drive, 'track', toHex(_cur.track >> 2) + '.' + (_cur.track & 0x3), // '(' + toHex(_cur.track) + ')', // '[' + phase + ':' + (on ? 'on' : 'off') + ']'); - - this.q[phase] = on; } private access(off: byte, val?: byte) { + const state = this.state; let result = 0; const readMode = val === undefined; @@ -644,13 +676,13 @@ export default class DiskII implements Card, MassStorage { case LOC.DRIVEOFF: // 0x08 if (!this.offTimeout) { - if (this.on) { + if (state.on) { // TODO(flan): This is fragile because it relies on // wall-clock time instead of emulator time. this.offTimeout = window.setTimeout(() => { this.debug('Drive Off'); - this.on = false; - this.callbacks.driveLight(this.drive, false); + state.on = false; + this.callbacks.driveLight(state.drive, false); this.debug('nibbles read', this.nibbleCount); }, 1000); } @@ -662,37 +694,37 @@ export default class DiskII implements Card, MassStorage { window.clearTimeout(this.offTimeout); this.offTimeout = null; } - if (!this.on) { + if (!state.on) { this.debug('Drive On'); this.nibbleCount = 0; - this.on = true; + state.on = true; this.lastCycles = this.io.cycles(); - this.callbacks.driveLight(this.drive, true); + this.callbacks.driveLight(state.drive, true); } break; case LOC.DRIVE1: // 0x0a this.debug('Disk 1'); - this.drive = 1; - this.cur = this.drives[this.drive]; - if (this.on) { + state.drive = 1; + this.cur = this.drives[state.drive]; + if (state.on) { this.callbacks.driveLight(2, false); this.callbacks.driveLight(1, true); } break; case LOC.DRIVE2: // 0x0b this.debug('Disk 2'); - this.drive = 2; - this.cur = this.drives[this.drive]; - if (this.on) { + state.drive = 2; + this.cur = this.drives[state.drive]; + if (state.on) { this.callbacks.driveLight(1, false); this.callbacks.driveLight(2, true); } break; case LOC.DRIVEREAD: // 0x0c (Q6L) Shift - this.q6 = 0; - if (this.writeMode) { + state.q6 = false; + if (state.q7) { this.debug('clearing _q6/SHIFT'); } if (isNibbleDrive(this.cur)) { @@ -701,17 +733,17 @@ export default class DiskII implements Card, MassStorage { break; case LOC.DRIVEWRITE: // 0x0d (Q6H) LOAD - this.q6 = 1; - if (this.writeMode) { + state.q6 = true; + if (state.q7) { this.debug('setting _q6/LOAD'); } if (isNibbleDrive(this.cur)) { - if (readMode && !this.writeMode) { + if (readMode && !state.q7) { if (this.cur.readOnly) { - this.latch = 0xff; + state.latch = 0xff; this.debug('Setting readOnly'); } else { - this.latch = this.latch >> 1; + state.latch = state.latch >> 1; this.debug('Clearing readOnly'); } } @@ -720,13 +752,11 @@ export default class DiskII implements Card, MassStorage { case LOC.DRIVEREADMODE: // 0x0e (Q7L) this.debug('Read Mode'); - this.q7 = false; - this.writeMode = false; + state.q7 = false; break; case LOC.DRIVEWRITEMODE: // 0x0f (Q7H) this.debug('Write Mode'); - this.q7 = true; - this.writeMode = true; + state.q7 = true; break; default: @@ -740,7 +770,7 @@ export default class DiskII implements Card, MassStorage { // used to read the data register onto the CPU bus, although some // also cause conflicts with the disk controller commands. if ((off & 0x01) === 0) { - result = this.latch; + result = state.latch; if (result & 0x80) { this.nibbleCount++; } @@ -750,7 +780,7 @@ export default class DiskII implements Card, MassStorage { } else { // It's not explicitly stated, but writes to any address set the // data register. - this.bus = val; + state.bus = val; } return result; @@ -768,7 +798,7 @@ export default class DiskII implements Card, MassStorage { } read(_page: byte, off: byte) { - return this.bootstrapRom[off]; + return BOOTSTRAP_ROM[this.sectors][off]; } write() { @@ -776,15 +806,13 @@ export default class DiskII implements Card, MassStorage { } reset() { - if (this.on) { - this.callbacks.driveLight(this.drive, false); - this.writeMode = false; - this.on = false; - this.drive = 1; - this.cur = this.drives[this.drive]; - } - for (let idx = 0; idx < 4; idx++) { - this.q[idx] = false; + const state = this.state; + if (state.on) { + this.callbacks.driveLight(state.drive, false); + state.q7 = false; + state.on = false; + state.drive = 1; + this.cur = this.drives[state.drive]; } } @@ -793,16 +821,10 @@ export default class DiskII implements Card, MassStorage { } getState(): State { - // TODO(flan): This does not accurately save state. It's missing - // all of the state for WOZ disks and the current status of the - // bus. const result = { drives: [] as DriveState[], skip: this.skip, - latch: this.latch, - writeMode: this.writeMode, - on: this.on, - drive: this.drive + controllerState: { ...this.state }, }; result.drives[1] = getDriveState(this.drives[1]); result.drives[2] = getDriveState(this.drives[2]); @@ -812,18 +834,16 @@ export default class DiskII implements Card, MassStorage { setState(state: State) { this.skip = state.skip; - this.latch = state.latch; - this.writeMode = state.writeMode; - this.on = state.on; - this.drive = state.drive; + this.state = { ...state.controllerState }; for (const d of DRIVE_NUMBERS) { this.drives[d] = setDriveState(state.drives[d]); - const { name, side, dirty } = state.drives[d]; + const { name, side } = state.drives[d].metadata; + const { dirty } = state.drives[d]; this.callbacks.label(d, name, side); - this.callbacks.driveLight(d, this.on); + this.callbacks.driveLight(d, this.state.on); this.callbacks.dirty(d, dirty); } - this.cur = this.drives[this.drive]; + this.cur = this.drives[this.state.drive]; } getMetadata(driveNo: DriveNumber) { @@ -864,10 +884,7 @@ export default class DiskII implements Card, MassStorage { } else { const disk = createDiskFromJsonDisk(jsonDisk); if (disk) { - const cur = this.drives[drive]; - Object.assign(cur, disk); - this.updateDirty(drive, false); - this.callbacks.label(drive, disk.name, disk.side); + this.insertDisk(drive, disk); return true; } } @@ -893,8 +910,8 @@ export default class DiskII implements Card, MassStorage { }; this.worker.postMessage(message); } else { - const cur = this.drives[drive]; - Object.assign(cur, jsonDecode(json)); + const disk = jsonDecode(json); + this.insertDisk(drive, disk); } return true; } @@ -924,12 +941,7 @@ export default class DiskII implements Card, MassStorage { } else { const disk = createDisk(fmt, options); if (disk) { - const cur = this.drives[drive]; - const { name, side } = cur; - Object.assign(cur, disk); - this.updateDirty(drive, true); - this.callbacks.label(drive, name, side); - + this.insertDisk(drive, disk); return true; } } @@ -951,11 +963,7 @@ export default class DiskII implements Card, MassStorage { { const { drive, disk } = data.payload; if (disk) { - const cur = this.drives[drive]; - Object.assign(cur, disk); - const { name, side } = cur; - this.updateDirty(drive, true); - this.callbacks.label(drive, name, side); + this.insertDisk(drive, disk); } } break; @@ -966,13 +974,22 @@ export default class DiskII implements Card, MassStorage { } } + private insertDisk(drive: DriveNumber, disk: FloppyDisk) { + const cur = this.drives[drive]; + Object.assign(cur, disk); + const { name, side } = cur.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?: NibbleFormat): MassStorageData | null { const cur = this.drives[drive]; if (!isNibbleDrive(cur)) { return null; } - const { format, name, readOnly, tracks, volume } = cur; + const { format, readOnly, tracks, volume } = cur; + const { name } = cur.metadata; const len = format === 'nib' ? tracks.reduce((acc, track) => acc + track.length, 0) : this.sectors * tracks.length * 256; @@ -995,7 +1012,7 @@ export default class DiskII implements Card, MassStorage { return { ext, - name, + metadata: { name }, data: data.buffer, readOnly, volume, diff --git a/js/cards/smartport.ts b/js/cards/smartport.ts index 6cea143..5dccb1c 100644 --- a/js/cards/smartport.ts +++ b/js/cards/smartport.ts @@ -523,7 +523,7 @@ export default class SmartPort implements Card, MassStorage, Restor ), encoding: ENCODING_BLOCK, readOnly: disk.readOnly, - name: disk.name, + metadata: { ...disk.metadata }, }; return result; } @@ -540,7 +540,7 @@ export default class SmartPort implements Card, MassStorage, Restor ), encoding: ENCODING_BLOCK, readOnly: disk.readOnly, - name: disk.name, + metadata: { ...disk.metadata }, }; return result; } @@ -580,7 +580,8 @@ export default class SmartPort implements Card, MassStorage, Restor } const disk = this.disks[drive]; const ext = this.ext[drive]; - const { name, readOnly } = disk; + const { readOnly } = disk; + const { name } = disk.metadata; let data: ArrayBuffer; if (ext === '2mg') { data = create2MGFromBlockDisk(this.metadata[drive], disk); @@ -593,7 +594,7 @@ export default class SmartPort implements Card, MassStorage, Restor data = byteArray.buffer; } return { - name, + metadata: { name }, ext, data, readOnly, diff --git a/js/components/Apple2.tsx b/js/components/Apple2.tsx index 58ab4f0..29fcaf2 100644 --- a/js/components/Apple2.tsx +++ b/js/components/Apple2.tsx @@ -17,6 +17,7 @@ import { Videoterm } from './Videoterm'; import { spawn, Ready } from './util/promises'; import styles from './css/Apple2.module.css'; +import { SupportedSectors } from 'js/formats/types'; declare global { interface Window { @@ -33,7 +34,7 @@ export interface Apple2Props { e: boolean; gl: boolean; rom: string; - sectors: number; + sectors: SupportedSectors; } /** diff --git a/js/components/DownloadModal.tsx b/js/components/DownloadModal.tsx index f628c62..fd337bb 100644 --- a/js/components/DownloadModal.tsx +++ b/js/components/DownloadModal.tsx @@ -21,7 +21,8 @@ export const DownloadModal = ({ massStorage, number, onClose, isOpen } : Downloa if (isOpen) { const storageData = massStorage.getBinary(number); if (storageData) { - const { name, ext, data } = storageData; + const { ext, data } = storageData; + const { name } = storageData.metadata; if (data.byteLength) { const blob = new Blob( [data], diff --git a/js/components/Drives.tsx b/js/components/Drives.tsx index 6297079..c44d6e2 100644 --- a/js/components/Drives.tsx +++ b/js/components/Drives.tsx @@ -10,7 +10,7 @@ import { ErrorModal } from './ErrorModal'; import { ProgressModal } from './ProgressModal'; import { loadHttpUnknownFile, getHashParts, loadJSON, SmartStorageBroker } from './util/files'; import { useHash } from './hooks/useHash'; -import { DISK_FORMATS, DriveNumber } from 'js/formats/types'; +import { DISK_FORMATS, DriveNumber, SupportedSectors } from 'js/formats/types'; import { spawn, Ready } from './util/promises'; import styles from './css/Drives.module.css'; @@ -32,7 +32,7 @@ export interface DrivesProps { cpu: CPU6502 | undefined; io: Apple2IO | undefined; enhanced: boolean; - sectors: number; + sectors: SupportedSectors; ready: Ready; } diff --git a/js/components/debugger/Disks.tsx b/js/components/debugger/Disks.tsx index e01c273..11d5e84 100644 --- a/js/components/debugger/Disks.tsx +++ b/js/components/debugger/Disks.tsx @@ -254,7 +254,8 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => { const disk = useMemo(() => { const massStorageData = massStorage.getBinary(drive, 'po'); if (massStorageData) { - const { name, data, readOnly, ext } = massStorageData; + const { data, readOnly, ext } = massStorageData; + const { name } = massStorageData.metadata; let disk: BlockDisk | NibbleDisk | null = null; if (ext === '2mg') { disk = createDiskFrom2MG({ diff --git a/js/components/util/systems.ts b/js/components/util/systems.ts index 6d5f34d..4653c4d 100644 --- a/js/components/util/systems.ts +++ b/js/components/util/systems.ts @@ -14,7 +14,7 @@ export const defaultSystem = { e: true, enhanced: true, sectors: 16, -}; +} as const; export const systemTypes: Record> = { // Apple //e @@ -68,4 +68,4 @@ export const systemTypes: Record> = { characterRom: 'apple2_char', e: false, } -}; +} as const; diff --git a/js/formats/block.ts b/js/formats/block.ts index f20caa0..1750ea8 100644 --- a/js/formats/block.ts +++ b/js/formats/block.ts @@ -21,7 +21,7 @@ export default function createBlockDisk(options: DiskOptions): BlockDisk { const disk: BlockDisk = { encoding: ENCODING_BLOCK, blocks, - name, + metadata: { name }, readOnly, }; diff --git a/js/formats/d13.ts b/js/formats/d13.ts index ac5f9cc..20adac2 100644 --- a/js/formats/d13.ts +++ b/js/formats/d13.ts @@ -11,8 +11,7 @@ export default function createDiskFromDOS13(options: DiskOptions) { const disk: NibbleDisk = { format: 'd13', encoding: ENCODING_NIBBLE, - name, - side, + metadata: { name, side }, volume, readOnly, tracks: [] diff --git a/js/formats/do.ts b/js/formats/do.ts index 1a4f2b4..c0315d7 100644 --- a/js/formats/do.ts +++ b/js/formats/do.ts @@ -13,8 +13,7 @@ export default function createDiskFromDOS(options: DiskOptions): NibbleDisk { const disk: NibbleDisk = { format: 'dsk', encoding: ENCODING_NIBBLE, - name, - side, + metadata: { name, side }, volume, readOnly, tracks: [], diff --git a/js/formats/format_utils.ts b/js/formats/format_utils.ts index 0d70c02..0bc409a 100644 --- a/js/formats/format_utils.ts +++ b/js/formats/format_utils.ts @@ -554,7 +554,7 @@ export function jsonDecode(data: string): NibbleDisk { volume: v, format: json.type, encoding: ENCODING_NIBBLE, - name: json.name, + metadata: { name: json.name }, tracks, readOnly, }; diff --git a/js/formats/nib.ts b/js/formats/nib.ts index 2758b35..51d34e8 100644 --- a/js/formats/nib.ts +++ b/js/formats/nib.ts @@ -11,8 +11,7 @@ export default function createDiskFromNibble(options: DiskOptions): NibbleDisk { const disk: NibbleDisk = { format: 'nib', encoding: ENCODING_NIBBLE, - name, - side, + metadata: { name, side }, volume: volume || 254, readOnly: readOnly || false, tracks: [] diff --git a/js/formats/po.ts b/js/formats/po.ts index 23de4c2..41150b0 100644 --- a/js/formats/po.ts +++ b/js/formats/po.ts @@ -13,8 +13,7 @@ export default function createDiskFromProDOS(options: DiskOptions) { const disk: NibbleDisk = { format: 'po', encoding: ENCODING_NIBBLE, - name, - side, + metadata: { name, side }, volume: volume || 254, tracks: [], readOnly: readOnly || false, diff --git a/js/formats/types.ts b/js/formats/types.ts index 7ca8692..272bc9e 100644 --- a/js/formats/types.ts +++ b/js/formats/types.ts @@ -2,13 +2,15 @@ import type { byte, memory, MemberOf, word } from '../types'; import type { GamepadConfiguration } from '../ui/types'; import { InfoChunk } from './woz'; +export const SUPPORTED_SECTORS = [13, 16] as const; +export type SupportedSectors = MemberOf; + export const DRIVE_NUMBERS = [1, 2] as const; export type DriveNumber = MemberOf; /** * Arguments for the disk format processors. */ - export interface DiskOptions { name: string; side?: string | undefined; @@ -33,7 +35,6 @@ export interface DiskDescriptor { /** * JSON binary image (not used?) */ - export interface JSONBinaryImage { type: 'binary'; start: word; @@ -42,14 +43,24 @@ export interface JSONBinaryImage { gamepad?: GamepadConfiguration; } +/** + * Information about a disk image not directly related to the + * disk contents. For example, the name or even a scan of the + * disk label are "metadata", but the volume number is not. + */ +export interface DiskMetadata { + /** Displayed disk name */ + name: string; + /** (Optional) Disk side (Front/Back, A/B) */ + side?: string | undefined; +} + /** * Return value from disk format processors. Describes raw disk * data which the DiskII card can process. */ - export interface Disk { - name: string; - side?: string | undefined; + metadata: DiskMetadata; readOnly: boolean; } @@ -207,7 +218,7 @@ export interface DiskProcessedResponse { type: typeof DISK_PROCESSED; payload: { drive: DriveNumber; - disk: Disk | null; + disk: FloppyDisk | null; }; } @@ -215,7 +226,7 @@ export type FormatWorkerResponse = DiskProcessedResponse; export interface MassStorageData { - name: string; + metadata: DiskMetadata; ext: string; readOnly: boolean; volume?: byte; diff --git a/js/formats/woz.ts b/js/formats/woz.ts index dd3b4a1..c632407 100644 --- a/js/formats/woz.ts +++ b/js/formats/woz.ts @@ -297,8 +297,10 @@ export default function createDiskFromWoz(options: DiskOptions): WozDisk { tracks: trks?.tracks || [], rawTracks: trks?.rawTracks || [], readOnly: true, //chunks.info.writeProtected === 1; - name: meta?.values['title'] || options.name, - side: meta?.values['side_name'] || meta?.values['side'], + metadata: { + name: meta?.values['title'] || options.name, + side: meta?.values['side_name'] || meta?.values['side'] + }, info }; diff --git a/js/main2.ts b/js/main2.ts index 9eaba1f..ef4a2ac 100644 --- a/js/main2.ts +++ b/js/main2.ts @@ -12,12 +12,13 @@ import Thunderclock from './cards/thunderclock'; import VideoTerm from './cards/videoterm'; import { Apple2 } from './apple2'; +import { SupportedSectors } from './formats/types'; const prefs = new Prefs(); const romVersion = prefs.readPref('computer_type2'); let rom: string; let characterRom: string; -let sectors = 16; +let sectors: SupportedSectors = 16; switch (romVersion) { case 'apple2': diff --git a/test/js/cards/disk2.spec.ts b/test/js/cards/disk2.spec.ts index 663454e..38f6b5e 100644 --- a/test/js/cards/disk2.spec.ts +++ b/test/js/cards/disk2.spec.ts @@ -66,11 +66,11 @@ describe('DiskII', () => { const state = diskII.getState(); // These are just arbitrary changes, not an exhaustive list of fields. - state.drive = 2; state.skip = 1; - state.latch = 0x42; - state.on = true; - state.writeMode = true; + state.controllerState.drive = 2; + state.controllerState.latch = 0x42; + state.controllerState.on = true; + state.controllerState.q7 = true; state.drives[2].tracks[14][12] = 0x80; state.drives[2].head = 1000; state.drives[2].phase = 3; diff --git a/test/js/formats/2mg.spec.ts b/test/js/formats/2mg.spec.ts index 90dbe02..653acaa 100644 --- a/test/js/formats/2mg.spec.ts +++ b/test/js/formats/2mg.spec.ts @@ -190,7 +190,7 @@ describe('2mg format', () => { } const disk: BlockDisk = { blocks, - name: 'Good disk', + metadata: { name: 'Good disk' }, readOnly: false, encoding: ENCODING_BLOCK, }; diff --git a/test/js/formats/create_disk.spec.ts b/test/js/formats/create_disk.spec.ts index 32f64ac..a1d0081 100644 --- a/test/js/formats/create_disk.spec.ts +++ b/test/js/formats/create_disk.spec.ts @@ -7,9 +7,11 @@ describe('createDiskFromJsonDisk', () => { expect(disk).toEqual({ encoding: 'nibble', format: 'dsk', - name: 'Test Disk', + metadata: { + name: 'Test Disk', + side: 'Front', + }, readOnly: undefined, - side: 'Front', volume: 254, tracks: expect.any(Array) as number[][] }); diff --git a/test/js/formats/woz.spec.ts b/test/js/formats/woz.spec.ts index 673fd6b..9855351 100644 --- a/test/js/formats/woz.spec.ts +++ b/test/js/formats/woz.spec.ts @@ -21,7 +21,7 @@ describe('woz', () => { const disk = createDiskFromWoz(options); expect(disk).toEqual({ - name: 'Mock Woz 1', + metadata: { name: 'Mock Woz 1' }, readOnly: true, encoding: ENCODING_BITSTREAM, trackMap: mockTMAP, @@ -58,8 +58,10 @@ describe('woz', () => { const disk = createDiskFromWoz(options); expect(disk).toEqual({ - name: 'Mock Woz 2', - side: 'B', + metadata: { + name: 'Mock Woz 2', + side: 'B', + }, readOnly: true, encoding: ENCODING_BITSTREAM, trackMap: mockTMAP, diff --git a/workers/format.worker.ts b/workers/format.worker.ts index b970a72..21df0e0 100644 --- a/workers/format.worker.ts +++ b/workers/format.worker.ts @@ -6,12 +6,12 @@ import { } from '../js/formats/create_disk'; import { FormatWorkerMessage, - Disk, DiskProcessedResponse, DISK_PROCESSED, PROCESS_BINARY, PROCESS_JSON_DISK, PROCESS_JSON, + FloppyDisk, } from '../js/formats/types'; debug('Worker loaded'); @@ -20,7 +20,7 @@ addEventListener('message', (message: MessageEvent) => { debug('Worker started', message.type); const data = message.data; const { drive } = data.payload; - let disk: Disk | null = null; + let disk: FloppyDisk | null = null; switch (data.type) { case PROCESS_BINARY: {