Move disk data to a disk field in the drive

Before, disk data was mixed in with state about the drive itself (like
track, motor phase, etc.). This made it hard to know exactly what data
was necessary for different image formats.

Now, the disk data is in a `disk` field whose type depends on the
drive type.  This makes responisbility a bit easier.

One oddity, though, is that the `Drive` has metadata _and_ the `Disk`
has metadata.  When a disk is in the drive, these should be `===`, but
when there is no disk in the drive, obviously only the drive metadata
is set.

All tests pass, everything compiles, and both WOZ and nibble disks
work in the emulator (both preact and classic).
This commit is contained in:
Ian Flanigan 2022-09-09 10:26:29 +02:00
parent 3a28fcb9fb
commit 62b48d3893
No known key found for this signature in database
GPG Key ID: 035F657DAE4AE7EC
5 changed files with 109 additions and 137 deletions

View File

@ -2,7 +2,6 @@ import { base64_encode } from '../base64';
import type { import type {
byte, byte,
Card, Card,
memory,
nibble, nibble,
ReadonlyUint8Array, ReadonlyUint8Array,
} from '../types'; } from '../types';
@ -26,6 +25,10 @@ import {
SupportedSectors, SupportedSectors,
FloppyDisk, FloppyDisk,
FloppyFormat, FloppyFormat,
WozDisk,
NibbleDisk,
isNibbleDisk,
isWozDisk,
} from '../formats/types'; } from '../formats/types';
import { import {
@ -38,7 +41,6 @@ import { jsonDecode, jsonEncode, readSector } from '../formats/format_utils';
import { BOOTSTRAP_ROM_16, BOOTSTRAP_ROM_13 } from '../roms/cards/disk2'; import { BOOTSTRAP_ROM_16, BOOTSTRAP_ROM_13 } from '../roms/cards/disk2';
import Apple2IO from '../apple2io'; import Apple2IO from '../apple2io';
import { InfoChunk } from 'js/formats/woz';
/** Softswitch locations */ /** Softswitch locations */
const LOC = { const LOC = {
@ -215,8 +217,8 @@ export interface Callbacks {
/** Common information for Nibble and WOZ disks. */ /** Common information for Nibble and WOZ disks. */
interface BaseDrive { interface BaseDrive {
/** Current disk format. */ /** The disk in the drive. */
format: FloppyFormat; disk: FloppyDisk;
/** Metadata about the disk image */ /** Metadata about the disk image */
metadata: DiskMetadata; metadata: DiskMetadata;
/** Whether the drive write protect is on. */ /** Whether the drive write protect is on. */
@ -233,71 +235,34 @@ interface BaseDrive {
/** WOZ format track data from https://applesaucefdc.com/woz/reference2/. */ /** WOZ format track data from https://applesaucefdc.com/woz/reference2/. */
interface WozDrive extends BaseDrive { interface WozDrive extends BaseDrive {
/** Woz encoding */ disk: WozDisk;
encoding: typeof ENCODING_BITSTREAM;
format: 'woz';
/** Maps quarter tracks to data in rawTracks; `0xFF` = random garbage. */
trackMap: byte[];
/** Unique track bitstreams. The index is arbitrary; it is NOT the track number. */
rawTracks: Uint8Array[];
/** Optional `INFO` chunk from WOZ image. */
info?: InfoChunk;
} }
/** Nibble format track data. */ /** Nibble format track data. */
interface NibbleDrive extends BaseDrive { interface NibbleDrive extends BaseDrive {
/** Nibble encoding */ disk: NibbleDisk;
encoding: typeof ENCODING_NIBBLE;
/** Format */
format: Exclude<NibbleFormat, 'woz'>;
/** Current disk volume number. */
volume: byte;
/** Nibble data. The index is the track number. */
tracks: memory[];
} }
type Drive = WozDrive | NibbleDrive; type Drive = WozDrive | NibbleDrive;
function isNibbleDrive(drive: Drive): drive is NibbleDrive { function isNibbleDrive(drive: Drive): drive is NibbleDrive {
return drive.encoding === ENCODING_NIBBLE; return drive.disk.encoding === ENCODING_NIBBLE;
} }
function isWozDrive(drive: Drive): drive is WozDrive { function isWozDrive(drive: Drive): drive is WozDrive {
return drive.encoding === ENCODING_BITSTREAM; return drive.disk.encoding === ENCODING_BITSTREAM;
} }
interface NibbleDriveState { interface DriveState {
format: Exclude<NibbleFormat, 'woz'>; disk: FloppyDisk;
encoding: typeof ENCODING_NIBBLE; metadata: DiskMetadata;
volume: byte; readOnly: boolean;
tracks: memory[];
track: byte; track: byte;
head: byte; head: byte;
phase: Phase; phase: Phase;
readOnly: boolean;
dirty: boolean; dirty: boolean;
trackMap: number[];
rawTracks: Uint8Array[];
metadata: DiskMetadata;
} }
interface WozDriveState {
format: 'woz';
encoding: typeof ENCODING_BITSTREAM;
tracks: memory[];
track: byte;
head: byte;
phase: Phase;
readOnly: boolean;
dirty: boolean;
trackMap: number[];
rawTracks: Uint8Array[];
metadata: DiskMetadata;
info?: InfoChunk;
}
type DriveState = NibbleDriveState | WozDriveState;
/** State of the controller for saving/restoring. */ /** State of the controller for saving/restoring. */
// 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 {
@ -307,47 +272,49 @@ interface State {
} }
function getDriveState(drive: Drive): DriveState { function getDriveState(drive: Drive): DriveState {
if (isNibbleDrive(drive)) { return {
const result: NibbleDriveState = { disk: getDiskState(drive.disk),
format: drive.format, metadata: {...drive.metadata},
encoding: drive.encoding, readOnly: drive.readOnly,
volume: drive.volume, track: drive.track,
head: drive.head,
phase: drive.phase,
dirty: drive.dirty,
};
}
function getDiskState(disk: NibbleDisk): NibbleDisk;
function getDiskState(disk: WozDisk): WozDisk;
function getDiskState(disk: FloppyDisk): FloppyDisk;
function getDiskState(disk: FloppyDisk): FloppyDisk {
if (isNibbleDisk(disk)) {
const result: NibbleDisk = {
format: disk.format,
encoding: disk.encoding,
volume: disk.volume,
tracks: [], tracks: [],
track: drive.track, readOnly: disk.readOnly,
head: drive.head, metadata: { ...disk.metadata },
phase: drive.phase,
readOnly: drive.readOnly,
dirty: drive.dirty,
trackMap: [],
rawTracks: [],
metadata: { ...drive.metadata },
}; };
for (let idx = 0; idx < drive.tracks.length; idx++) { for (let idx = 0; idx < disk.tracks.length; idx++) {
result.tracks.push(new Uint8Array(drive.tracks[idx])); result.tracks.push(new Uint8Array(disk.tracks[idx]));
} }
return result; return result;
} }
if (isWozDrive(drive)) { if (isWozDisk(disk)) {
const result: WozDriveState = { const result: WozDisk = {
format: drive.format, format: disk.format,
encoding: drive.encoding, encoding: disk.encoding,
tracks: [], readOnly: disk.readOnly,
track: drive.track,
head: drive.head,
phase: drive.phase,
readOnly: drive.readOnly,
dirty: drive.dirty,
trackMap: [], trackMap: [],
rawTracks: [], rawTracks: [],
metadata: { ...drive.metadata }, metadata: { ...disk.metadata },
info: disk.info,
}; };
result.trackMap = [...drive.trackMap]; result.trackMap = [...disk.trackMap];
for (let idx = 0; idx < drive.rawTracks.length; idx++) { for (let idx = 0; idx < disk.rawTracks.length; idx++) {
result.rawTracks.push(new Uint8Array(drive.rawTracks[idx])); result.rawTracks.push(new Uint8Array(disk.rawTracks[idx]));
}
if (drive.info) {
result.info = drive.info;
} }
return result; return result;
} }
@ -356,13 +323,9 @@ function getDriveState(drive: Drive): DriveState {
} }
function setDriveState(state: DriveState) { function setDriveState(state: DriveState) {
let result: Drive; if (isNibbleDisk(state.disk)) {
if (state.encoding === ENCODING_NIBBLE) { const result: NibbleDrive = {
result = { disk: getDiskState(state.disk),
format: state.format,
encoding: ENCODING_NIBBLE,
volume: state.volume || 254,
tracks: [],
track: state.track, track: state.track,
head: state.head, head: state.head,
phase: state.phase, phase: state.phase,
@ -370,27 +333,20 @@ function setDriveState(state: DriveState) {
dirty: state.dirty, dirty: state.dirty,
metadata: { ...state.metadata }, metadata: { ...state.metadata },
}; };
for (let idx = 0; idx < state.tracks.length; idx++) { return result;
result.tracks.push(new Uint8Array(state.tracks[idx])); } else if (isWozDisk(state.disk)) {
} const result: WozDrive = {
} else { disk: getDiskState(state.disk),
result = {
format: state.format,
encoding: ENCODING_BITSTREAM,
track: state.track, track: state.track,
head: state.head, head: state.head,
phase: state.phase, phase: state.phase,
readOnly: state.readOnly, readOnly: state.readOnly,
dirty: state.dirty, dirty: state.dirty,
trackMap: [...state.trackMap],
rawTracks: [],
metadata: { ...state.metadata }, metadata: { ...state.metadata },
}; };
for (let idx = 0; idx < state.rawTracks.length; idx++) { return result;
result.rawTracks.push(new Uint8Array(state.rawTracks[idx]));
}
} }
return result; throw new Error('Unable to restore drive state');
} }
/** /**
@ -400,10 +356,14 @@ 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
format: 'dsk', disk: {
encoding: ENCODING_NIBBLE, format: 'dsk',
volume: 254, encoding: ENCODING_NIBBLE,
tracks: [], volume: 254,
tracks: [],
readOnly: true,
metadata: { name: 'Disk 1' },
},
track: 0, track: 0,
head: 0, head: 0,
phase: 0, phase: 0,
@ -412,10 +372,14 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
metadata: { name: 'Disk 1' }, metadata: { name: 'Disk 1' },
}, },
2: { // Drive 2 2: { // Drive 2
format: 'dsk', disk: {
encoding: ENCODING_NIBBLE, format: 'dsk',
volume: 254, encoding: ENCODING_NIBBLE,
tracks: [], volume: 254,
tracks: [],
readOnly: true,
metadata: { name: 'Disk 2' },
},
track: 0, track: 0,
head: 0, head: 0,
phase: 0, phase: 0,
@ -533,7 +497,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
return; return;
} }
const track = const track =
this.cur.rawTracks[this.cur.trackMap[this.cur.track]] || [0]; this.cur.disk.rawTracks[this.cur.disk.trackMap[this.cur.track]] || [0];
const state = this.state; const state = this.state;
@ -615,7 +579,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
} }
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.tracks[this.cur.track >> 2]; const track = this.cur.disk.tracks[this.cur.track >> 2];
if (track && track.length) { if (track && track.length) {
if (this.cur.head >= track.length) { if (this.cur.head >= track.length) {
this.cur.head = 0; this.cur.head = 0;
@ -670,8 +634,8 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
// 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 = isNibbleDrive(this.cur) const maxTrack = isNibbleDrive(this.cur)
? this.cur.tracks.length * 4 - 1 ? this.cur.disk.tracks.length * 4 - 1
: this.cur.trackMap.length - 1; : this.cur.disk.trackMap.length - 1;
if (this.cur.track > maxTrack) { if (this.cur.track > maxTrack) {
this.cur.track = maxTrack; this.cur.track = maxTrack;
} }
@ -891,7 +855,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
getMetadata(driveNo: DriveNumber) { getMetadata(driveNo: DriveNumber) {
const drive = this.drives[driveNo]; const drive = this.drives[driveNo];
return { return {
format: drive.format, format: drive.disk.format,
track: drive.track, track: drive.track,
head: drive.head, head: drive.head,
phase: drive.phase, phase: drive.phase,
@ -906,7 +870,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
if (!isNibbleDrive(cur)) { if (!isNibbleDrive(cur)) {
throw new Error('Can\'t read WOZ disks'); throw new Error('Can\'t read WOZ disks');
} }
return readSector(cur, track, sector); return readSector(cur.disk, 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. */
@ -937,7 +901,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
if (!isNibbleDrive(cur)) { if (!isNibbleDrive(cur)) {
throw new Error('Can\'t save WOZ disks to JSON'); throw new Error('Can\'t save WOZ disks to JSON');
} }
return jsonEncode(cur, pretty); return jsonEncode(cur.disk, pretty);
} }
setJSON(drive: DriveNumber, json: string) { setJSON(drive: DriveNumber, json: string) {
@ -1017,7 +981,8 @@ 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]; const cur = this.drives[drive];
Object.assign(cur, disk); cur.disk = disk as WozDisk | NibbleDisk;
cur.metadata = disk.metadata;
const { name, side } = cur.metadata; const { name, side } = cur.metadata;
this.updateDirty(drive, true); this.updateDirty(drive, true);
this.callbacks.label(drive, name, side); this.callbacks.label(drive, name, side);
@ -1029,7 +994,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
if (!isNibbleDrive(cur)) { if (!isNibbleDrive(cur)) {
return null; return null;
} }
const { format, readOnly, tracks, volume } = cur; const { format, readOnly, tracks, volume } = cur.disk;
const { name } = cur.metadata; const { name } = cur.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) :
@ -1044,7 +1009,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, format: extension }, t, s); const sector = readSector({ ...cur.disk, format: extension }, t, s);
data.set(sector, idx); data.set(sector, idx);
idx += sector.length; idx += sector.length;
} }
@ -1067,13 +1032,13 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
return null; return null;
} }
const data: string[][] | string[] = []; const data: string[][] | string[] = [];
for (let t = 0; t < cur.tracks.length; t++) { for (let t = 0; t < cur.disk.tracks.length; t++) {
if (cur.format === 'nib') { if (isNibbleDisk(cur.disk)) {
data[t] = base64_encode(cur.tracks[t]); data[t] = base64_encode(cur.disk.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, t, s)); track[s] = base64_encode(readSector(cur.disk, t, s));
} }
data[t] = track; data[t] = track;
} }

View File

@ -70,13 +70,13 @@ export const ENCODING_BLOCK = 'block';
export interface FloppyDisk extends Disk { export interface FloppyDisk extends Disk {
encoding: typeof ENCODING_NIBBLE | typeof ENCODING_BITSTREAM; encoding: typeof ENCODING_NIBBLE | typeof ENCODING_BITSTREAM;
tracks: memory[];
} }
export interface NibbleDisk extends FloppyDisk { export interface NibbleDisk extends FloppyDisk {
encoding: typeof ENCODING_NIBBLE; encoding: typeof ENCODING_NIBBLE;
format: Exclude<NibbleFormat, 'woz'>; format: Exclude<NibbleFormat, 'woz'>;
volume: byte; volume: byte;
tracks: memory[];
} }
export interface WozDisk extends FloppyDisk { export interface WozDisk extends FloppyDisk {
@ -158,6 +158,11 @@ export function isNibbleDisk(disk: Disk): disk is NibbleDisk {
return (disk as NibbleDisk)?.encoding === ENCODING_NIBBLE; return (disk as NibbleDisk)?.encoding === ENCODING_NIBBLE;
} }
/** Type guard for NibbleDisks */
export function isWozDisk(disk: Disk): disk is WozDisk {
return (disk as WozDisk)?.encoding === ENCODING_BITSTREAM;
}
/** /**
* Base format for JSON defined disks * Base format for JSON defined disks
*/ */

View File

@ -295,7 +295,6 @@ export default function createDiskFromWoz(options: DiskOptions): WozDisk {
encoding: ENCODING_BITSTREAM, encoding: ENCODING_BITSTREAM,
format: 'woz', format: 'woz',
trackMap: tmap?.trackMap || [], trackMap: tmap?.trackMap || [],
tracks: trks?.tracks || [],
rawTracks: trks?.rawTracks || [], rawTracks: trks?.rawTracks || [],
readOnly: true, //chunks.info.writeProtected === 1; readOnly: true, //chunks.info.writeProtected === 1;
metadata: { metadata: {

View File

@ -4,7 +4,7 @@ import fs from 'fs';
import Apple2IO from 'js/apple2io'; import Apple2IO from 'js/apple2io';
import DiskII, { Callbacks } from 'js/cards/disk2'; import DiskII, { Callbacks } from 'js/cards/disk2';
import CPU6502 from 'js/cpu6502'; import CPU6502 from 'js/cpu6502';
import { DriveNumber } from 'js/formats/types'; import { DriveNumber, NibbleDisk, WozDisk } from 'js/formats/types';
import { byte } from 'js/types'; import { byte } from 'js/types';
import { toHex } from 'js/util'; import { toHex } from 'js/util';
import { VideoModes } from 'js/videomodes'; import { VideoModes } from 'js/videomodes';
@ -71,7 +71,8 @@ describe('DiskII', () => {
state.controllerState.latch = 0x42; state.controllerState.latch = 0x42;
state.controllerState.on = true; state.controllerState.on = true;
state.controllerState.q7 = true; state.controllerState.q7 = true;
state.drives[2].tracks[14][12] = 0x80; const disk2 = state.drives[2].disk as NibbleDisk;
disk2.tracks[14][12] = 0x80;
state.drives[2].head = 1000; state.drives[2].head = 1000;
state.drives[2].phase = 3; state.drives[2].phase = 3;
diskII.setState(state); diskII.setState(state);
@ -478,21 +479,24 @@ describe('DiskII', () => {
it('writes a nibble to the disk', () => { it('writes a nibble to the disk', () => {
const diskII = new DiskII(mockApple2IO, callbacks); const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
let track0 = diskII.getState().drives[1].tracks[0]; let disk1 = diskII.getState().drives[1].disk as NibbleDisk;
let track0 = disk1.tracks[0];
expect(track0[0]).toBe(0xFF); expect(track0[0]).toBe(0xFF);
diskII.ioSwitch(0x89); // turn on the motor diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x8F, 0x80); // write diskII.ioSwitch(0x8F, 0x80); // write
diskII.ioSwitch(0x8C); // shift diskII.ioSwitch(0x8C); // shift
track0 = diskII.getState().drives[1].tracks[0]; disk1 = diskII.getState().drives[1].disk as NibbleDisk;
track0 = disk1.tracks[0];
expect(track0[0]).toBe(0x80); expect(track0[0]).toBe(0x80);
}); });
it('writes two nibbles to the disk', () => { it('writes two nibbles to the disk', () => {
const diskII = new DiskII(mockApple2IO, callbacks); const diskII = new DiskII(mockApple2IO, callbacks);
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
let track0 = diskII.getState().drives[1].tracks[0]; let disk1 = diskII.getState().drives[1].disk as NibbleDisk;
let track0 = disk1.tracks[0];
expect(track0[0]).toBe(0xFF); expect(track0[0]).toBe(0xFF);
diskII.ioSwitch(0x89); // turn on the motor diskII.ioSwitch(0x89); // turn on the motor
@ -501,7 +505,8 @@ describe('DiskII', () => {
diskII.ioSwitch(0x8F, 0x81); // write diskII.ioSwitch(0x8F, 0x81); // write
diskII.ioSwitch(0x8C); // shift diskII.ioSwitch(0x8C); // shift
track0 = diskII.getState().drives[1].tracks[0]; disk1 = diskII.getState().drives[1].disk as NibbleDisk;
track0 = disk1.tracks[0];
expect(track0[0]).toBe(0x80); expect(track0[0]).toBe(0x80);
expect(track0[1]).toBe(0x81); expect(track0[1]).toBe(0x81);
}); });
@ -819,10 +824,10 @@ 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: { rawTracks: Uint8Array[] } }; const disk = (this.diskII as unknown as { cur: { disk: WozDisk } }).cur.disk;
const result: Uint8Array[] = []; const result: Uint8Array[] = [];
for (let i = 0; i < disk.cur.rawTracks.length; i++) { for (let i = 0; i < disk.rawTracks.length; i++) {
result[i] = disk.cur.rawTracks[i].slice(0); result[i] = disk.rawTracks[i].slice(0);
} }
return result; return result;

View File

@ -21,7 +21,7 @@ describe('woz', () => {
const disk = createDiskFromWoz(options); const disk = createDiskFromWoz(options);
expect(disk).toEqual({ expect(disk).toEqual({
metadata: { name: 'Mock Woz 1' }, metadata: { name: 'Mock Woz 1', side: undefined },
readOnly: true, readOnly: true,
encoding: ENCODING_BITSTREAM, encoding: ENCODING_BITSTREAM,
format: 'woz', format: 'woz',
@ -31,7 +31,6 @@ describe('woz', () => {
1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0,
1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0,
])], ])],
tracks: [new Uint8Array([0xD5, 0xAA, 0x96])],
info: { info: {
bitTiming: 0, bitTiming: 0,
bootSector: 0, bootSector: 0,
@ -72,7 +71,6 @@ describe('woz', () => {
1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0,
1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0,
])], ])],
tracks: [new Uint8Array([0xD5, 0xAA, 0x96])],
info: { info: {
bitTiming: 0, bitTiming: 0,
bootSector: 0, bootSector: 0,