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 {
byte,
Card,
memory,
nibble,
ReadonlyUint8Array,
} from '../types';
@ -26,6 +25,10 @@ import {
SupportedSectors,
FloppyDisk,
FloppyFormat,
WozDisk,
NibbleDisk,
isNibbleDisk,
isWozDisk,
} from '../formats/types';
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 Apple2IO from '../apple2io';
import { InfoChunk } from 'js/formats/woz';
/** Softswitch locations */
const LOC = {
@ -215,8 +217,8 @@ export interface Callbacks {
/** Common information for Nibble and WOZ disks. */
interface BaseDrive {
/** Current disk format. */
format: FloppyFormat;
/** The disk in the drive. */
disk: FloppyDisk;
/** Metadata about the disk image */
metadata: DiskMetadata;
/** Whether the drive write protect is on. */
@ -233,71 +235,34 @@ interface BaseDrive {
/** WOZ format track data from https://applesaucefdc.com/woz/reference2/. */
interface WozDrive extends BaseDrive {
/** Woz encoding */
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;
disk: WozDisk;
}
/** Nibble format track data. */
interface NibbleDrive extends BaseDrive {
/** Nibble encoding */
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[];
disk: NibbleDisk;
}
type Drive = WozDrive | 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 {
return drive.encoding === ENCODING_BITSTREAM;
return drive.disk.encoding === ENCODING_BITSTREAM;
}
interface NibbleDriveState {
format: Exclude<NibbleFormat, 'woz'>;
encoding: typeof ENCODING_NIBBLE;
volume: byte;
tracks: memory[];
interface DriveState {
disk: FloppyDisk;
metadata: DiskMetadata;
readOnly: boolean;
track: byte;
head: byte;
phase: Phase;
readOnly: 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. */
// TODO(flan): It's unclear whether reusing ControllerState here is a good idea.
interface State {
@ -307,47 +272,49 @@ interface State {
}
function getDriveState(drive: Drive): DriveState {
if (isNibbleDrive(drive)) {
const result: NibbleDriveState = {
format: drive.format,
encoding: drive.encoding,
volume: drive.volume,
return {
disk: getDiskState(drive.disk),
metadata: {...drive.metadata},
readOnly: drive.readOnly,
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: [],
track: drive.track,
head: drive.head,
phase: drive.phase,
readOnly: drive.readOnly,
dirty: drive.dirty,
trackMap: [],
rawTracks: [],
metadata: { ...drive.metadata },
readOnly: disk.readOnly,
metadata: { ...disk.metadata },
};
for (let idx = 0; idx < drive.tracks.length; idx++) {
result.tracks.push(new Uint8Array(drive.tracks[idx]));
for (let idx = 0; idx < disk.tracks.length; idx++) {
result.tracks.push(new Uint8Array(disk.tracks[idx]));
}
return result;
}
if (isWozDrive(drive)) {
const result: WozDriveState = {
format: drive.format,
encoding: drive.encoding,
tracks: [],
track: drive.track,
head: drive.head,
phase: drive.phase,
readOnly: drive.readOnly,
dirty: drive.dirty,
if (isWozDisk(disk)) {
const result: WozDisk = {
format: disk.format,
encoding: disk.encoding,
readOnly: disk.readOnly,
trackMap: [],
rawTracks: [],
metadata: { ...drive.metadata },
metadata: { ...disk.metadata },
info: disk.info,
};
result.trackMap = [...drive.trackMap];
for (let idx = 0; idx < drive.rawTracks.length; idx++) {
result.rawTracks.push(new Uint8Array(drive.rawTracks[idx]));
}
if (drive.info) {
result.info = drive.info;
result.trackMap = [...disk.trackMap];
for (let idx = 0; idx < disk.rawTracks.length; idx++) {
result.rawTracks.push(new Uint8Array(disk.rawTracks[idx]));
}
return result;
}
@ -356,13 +323,9 @@ function getDriveState(drive: Drive): DriveState {
}
function setDriveState(state: DriveState) {
let result: Drive;
if (state.encoding === ENCODING_NIBBLE) {
result = {
format: state.format,
encoding: ENCODING_NIBBLE,
volume: state.volume || 254,
tracks: [],
if (isNibbleDisk(state.disk)) {
const result: NibbleDrive = {
disk: getDiskState(state.disk),
track: state.track,
head: state.head,
phase: state.phase,
@ -370,27 +333,20 @@ function setDriveState(state: DriveState) {
dirty: state.dirty,
metadata: { ...state.metadata },
};
for (let idx = 0; idx < state.tracks.length; idx++) {
result.tracks.push(new Uint8Array(state.tracks[idx]));
}
} else {
result = {
format: state.format,
encoding: ENCODING_BITSTREAM,
return result;
} else if (isWozDisk(state.disk)) {
const result: WozDrive = {
disk: getDiskState(state.disk),
track: state.track,
head: state.head,
phase: state.phase,
readOnly: state.readOnly,
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]));
}
return result;
}
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> = {
1: { // Drive 1
format: 'dsk',
encoding: ENCODING_NIBBLE,
volume: 254,
tracks: [],
disk: {
format: 'dsk',
encoding: ENCODING_NIBBLE,
volume: 254,
tracks: [],
readOnly: true,
metadata: { name: 'Disk 1' },
},
track: 0,
head: 0,
phase: 0,
@ -412,10 +372,14 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
metadata: { name: 'Disk 1' },
},
2: { // Drive 2
format: 'dsk',
encoding: ENCODING_NIBBLE,
volume: 254,
tracks: [],
disk: {
format: 'dsk',
encoding: ENCODING_NIBBLE,
volume: 254,
tracks: [],
readOnly: true,
metadata: { name: 'Disk 2' },
},
track: 0,
head: 0,
phase: 0,
@ -533,7 +497,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
return;
}
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;
@ -615,7 +579,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
}
const state = this.state;
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 (this.cur.head >= track.length) {
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
// drives can seek to track 39.
const maxTrack = isNibbleDrive(this.cur)
? this.cur.tracks.length * 4 - 1
: this.cur.trackMap.length - 1;
? this.cur.disk.tracks.length * 4 - 1
: this.cur.disk.trackMap.length - 1;
if (this.cur.track > maxTrack) {
this.cur.track = maxTrack;
}
@ -891,7 +855,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
getMetadata(driveNo: DriveNumber) {
const drive = this.drives[driveNo];
return {
format: drive.format,
format: drive.disk.format,
track: drive.track,
head: drive.head,
phase: drive.phase,
@ -906,7 +870,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
if (!isNibbleDrive(cur)) {
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. */
@ -937,7 +901,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
if (!isNibbleDrive(cur)) {
throw new Error('Can\'t save WOZ disks to JSON');
}
return jsonEncode(cur, pretty);
return jsonEncode(cur.disk, pretty);
}
setJSON(drive: DriveNumber, json: string) {
@ -1017,7 +981,8 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
private insertDisk(drive: DriveNumber, disk: FloppyDisk) {
const cur = this.drives[drive];
Object.assign(cur, disk);
cur.disk = disk as WozDisk | NibbleDisk;
cur.metadata = disk.metadata;
const { name, side } = cur.metadata;
this.updateDirty(drive, true);
this.callbacks.label(drive, name, side);
@ -1029,7 +994,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
if (!isNibbleDrive(cur)) {
return null;
}
const { format, readOnly, tracks, volume } = cur;
const { format, readOnly, tracks, volume } = cur.disk;
const { name } = cur.metadata;
const len = format === 'nib' ?
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;
} else {
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);
idx += sector.length;
}
@ -1067,13 +1032,13 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
return null;
}
const data: string[][] | string[] = [];
for (let t = 0; t < cur.tracks.length; t++) {
if (cur.format === 'nib') {
data[t] = base64_encode(cur.tracks[t]);
for (let t = 0; t < cur.disk.tracks.length; t++) {
if (isNibbleDisk(cur.disk)) {
data[t] = base64_encode(cur.disk.tracks[t]);
} else {
const track: string[] = [];
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;
}

View File

@ -70,13 +70,13 @@ export const ENCODING_BLOCK = 'block';
export interface FloppyDisk extends Disk {
encoding: typeof ENCODING_NIBBLE | typeof ENCODING_BITSTREAM;
tracks: memory[];
}
export interface NibbleDisk extends FloppyDisk {
encoding: typeof ENCODING_NIBBLE;
format: Exclude<NibbleFormat, 'woz'>;
volume: byte;
tracks: memory[];
}
export interface WozDisk extends FloppyDisk {
@ -158,6 +158,11 @@ export function isNibbleDisk(disk: Disk): disk is NibbleDisk {
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
*/

View File

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

View File

@ -4,7 +4,7 @@ import fs from 'fs';
import Apple2IO from 'js/apple2io';
import DiskII, { Callbacks } from 'js/cards/disk2';
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 { toHex } from 'js/util';
import { VideoModes } from 'js/videomodes';
@ -71,7 +71,8 @@ describe('DiskII', () => {
state.controllerState.latch = 0x42;
state.controllerState.on = 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].phase = 3;
diskII.setState(state);
@ -478,21 +479,24 @@ describe('DiskII', () => {
it('writes a nibble to the disk', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
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);
diskII.ioSwitch(0x89); // turn on the motor
diskII.ioSwitch(0x8F, 0x80); // write
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);
});
it('writes two nibbles to the disk', () => {
const diskII = new DiskII(mockApple2IO, callbacks);
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);
diskII.ioSwitch(0x89); // turn on the motor
@ -501,7 +505,8 @@ describe('DiskII', () => {
diskII.ioSwitch(0x8F, 0x81); // write
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[1]).toBe(0x81);
});
@ -819,10 +824,10 @@ class TestDiskReader {
rawTracks() {
// 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[] = [];
for (let i = 0; i < disk.cur.rawTracks.length; i++) {
result[i] = disk.cur.rawTracks[i].slice(0);
for (let i = 0; i < disk.rawTracks.length; i++) {
result[i] = disk.rawTracks[i].slice(0);
}
return result;

View File

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