Refactor disk parsing into webworker

This commit is contained in:
Will Scullin 2021-06-20 15:34:44 -07:00
parent 3abd168627
commit 2947891eae
No known key found for this signature in database
GPG Key ID: 26DCD1042C6638CD
25 changed files with 1172 additions and 857 deletions

View File

@ -1,418 +0,0 @@
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
*
* Permission to use, copy, modify, distribute, and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
* the above copyright notice appear in all copies and that both that
* copyright notice and this permission notice appear in supporting
* documentation. No representations are made about the suitability of this
* software for any purpose. It is provided "as is" without express or
* implied warranty.
*/
import { debug, toHex } from '../util';
import { rom } from '../roms/cards/cffa';
import _2MG from '../formats/2mg';
import { ProDOSVolume } from '../formats/prodos';
import BlockVolume from '../formats/block';
import { dump } from '../formats/prodos/utils';
export default function CFFA() {
var COMMANDS = {
ATACRead: 0x20,
ATACWrite: 0x30,
ATAIdentify: 0xEC
};
// CFFA Card Settings
var SETTINGS = {
Max32MBPartitionsDev0: 0x800,
Max32MBPartitionsDev1: 0x801,
DefaultBootDevice: 0x802,
DefaultBootPartition: 0x803,
Reserved: 0x804, // 4 bytes
WriteProtectBits: 0x808,
MenuSnagMask: 0x809,
MenuSnagKey: 0x80A,
BootTimeDelayTenths: 0x80B,
BusResetSeconds: 0x80C,
CheckDeviceTenths: 0x80D,
ConfigOptionBits: 0x80E,
BlockOffsetDev0: 0x80F, // 3 bytes
BlockOffsetDev1: 0x812, // 3 bytes
Unused: 0x815
};
// CFFA ATA Register Locations
var LOC = {
ATADataHigh: 0x80,
SetCSMask: 0x81,
ClearCSMask: 0x82,
WriteEEPROM: 0x83,
NoWriteEEPROM: 0x84,
ATADevCtrl: 0x86,
ATAAltStatus: 0x86,
ATADataLow: 0x88,
AError: 0x89,
ASectorCnt: 0x8a,
ASector: 0x8b,
ATACylinder: 0x8c,
ATACylinderH: 0x8d,
ATAHead: 0x8e,
ATACommand: 0x8f,
ATAStatus: 0x8f
};
// ATA Status Bits
var STATUS = {
BSY: 0x80, // Busy
DRDY: 0x40, // Drive ready. 1 when ready
DWF: 0x20, // Drive write fault. 1 when fault
DSC: 0x10, // Disk seek complete. 1 when not seeking
DRQ: 0x08, // Data request. 1 when ready to write
CORR: 0x04, // Correct data. 1 on correctable error
IDX: 0x02, // 1 once per revolution
ERR: 0x01 // Error. 1 on error
};
// ATA Identity Block Locations
var IDENTITY = {
SectorCountLow: 58,
SectorCountHigh: 57
};
// CFFA internal Flags
var _disableSignalling = false;
var _writeEEPROM = true;
var _lba = true;
// LBA/CHS registers
var _sectorCnt = 1;
var _sector = 0;
var _cylinder = 0;
var _cylinderH = 0;
var _head = 0;
var _drive = 0;
// CFFA Data High register
var _dataHigh = 0;
// Current Sector
var _curSector = [];
var _curWord = 0;
// ATA Status registers
var _interruptsEnabled = false;
var _altStatus = 0;
var _error = 0;
var _identity = [[], []];
// Disk data
var _partitions = [
// Drive 1
[],
// Drive 2
[]
];
var _sectors = [
// Drive 1
[],
// Drive 2
[]
];
function _init() {
debug('CFFA');
for (var idx = 0; idx < 0x100; idx++) {
_identity[0][idx] = 0;
_identity[1][idx] = 0;
}
rom[SETTINGS.Max32MBPartitionsDev0] = 0x1;
rom[SETTINGS.Max32MBPartitionsDev1] = 0x0;
rom[SETTINGS.BootTimeDelayTenths] = 0x5; // 0.5 seconds
rom[SETTINGS.CheckDeviceTenths] = 0x5; // 0.5 seconds
}
// Verbose debug method
function _debug() {
// debug.apply(this, arguments);
}
function _reset() {
_debug('reset');
_sectorCnt = 1;
_sector = 0;
_cylinder = 0;
_cylinderH = 0;
_head = 0;
_drive = 0;
_dataHigh = 0;
}
// Convert status register into readable string
function _statusString(status) {
var statusArray = [];
for (var flag in STATUS) {
if(status & STATUS[flag]) {
statusArray.push(flag);
}
}
return statusArray.join('|');
}
// Dump sector as hex and ascii
function _dumpSector(sector) {
if (sector >= _sectors[_drive].length) {
_debug('dump sector out of range', sector);
return;
}
for (var idx = 0; idx < 16; idx++) {
var row = [];
var charRow = [];
for (var jdx = 0; jdx < 16; jdx++) {
var val = _sectors[_drive][sector][idx * 16 + jdx];
row.push(toHex(val, 4));
var low = val & 0x7f;
var hi = val >> 8 & 0x7f;
charRow.push(low > 0x1f ? String.fromCharCode(low) : '.');
charRow.push(hi > 0x1f ? String.fromCharCode(hi) : '.');
}
_debug(row.join(' '), ' ', charRow.join(''));
}
}
// Card I/O access
function _access(off, val) {
var readMode = val === undefined;
var retVal = readMode ? 0 : undefined;
var sector;
if (readMode) {
switch (off & 0x8f) {
case LOC.ATADataHigh: // 0x00
retVal = _dataHigh;
break;
case LOC.SetCSMask: // 0x01
_disableSignalling = true;
break;
case LOC.ClearCSMask: // 0x02
_disableSignalling = false;
break;
case LOC.WriteEEPROM: // 0x03
_writeEEPROM = true;
break;
case LOC.NoWriteEEPROM: // 0x04
_writeEEPROM = false;
break;
case LOC.ATAAltStatus: // 0x06
retVal = _altStatus;
break;
case LOC.ATADataLow: // 0x08
_dataHigh = _curSector[_curWord] >> 8;
retVal = _curSector[_curWord] & 0xff;
if (!_disableSignalling) {
_curWord++;
}
break;
case LOC.AError: // 0x09
retVal = _error;
break;
case LOC.ASectorCnt: // 0x0A
retVal = _sectorCnt;
break;
case LOC.ASector: // 0x0B
retVal = _sector;
break;
case LOC.ATACylinder: // 0x0C
retVal = _cylinder;
break;
case LOC.ATACylinderH: // 0x0D
retVal = _cylinderH;
break;
case LOC.ATAHead: // 0x0E
retVal = _head | (_lba ? 0x40 : 0) | (_drive ? 0x10 : 0) | 0xA0;
break;
case LOC.ATAStatus: // 0x0F
retVal = _sectors[_drive].length > 0 ? STATUS.DRDY | STATUS.DSC : 0;
_debug('returning status', _statusString(retVal));
break;
default:
debug('read unknown soft switch', toHex(off));
}
if (off & 0x7) { // Anything but data high/low
_debug('read soft switch', toHex(off), toHex(retVal));
}
} else {
if (off & 0x7) { // Anything but data high/low
_debug('write soft switch', toHex(off), toHex(val));
}
switch (off & 0x8f) {
case LOC.ATADataHigh: // 0x00
_dataHigh = val;
break;
case LOC.SetCSMask: // 0x01
_disableSignalling = true;
break;
case LOC.ClearCSMask: // 0x02
_disableSignalling = false;
break;
case LOC.WriteEEPROM: // 0x03
_writeEEPROM = true;
break;
case LOC.NoWriteEEPROM: // 0x04
_writeEEPROM = false;
break;
case LOC.ATADevCtrl: // 0x06
_debug('devCtrl:', toHex(val));
_interruptsEnabled = (val & 0x04) ? true : false;
_debug('Interrupts', _interruptsEnabled ? 'enabled' : 'disabled');
if (val & 0x02) {
_reset();
}
break;
case LOC.ATADataLow: // 0x08
_curSector[_curWord] = _dataHigh << 8 | val;
_curWord++;
break;
case LOC.ASectorCnt: // 0x0a
_debug('setting sector count', val);
_sectorCnt = val;
break;
case LOC.ASector: // 0x0b
_debug('setting sector', toHex(val));
_sector = val;
break;
case LOC.ATACylinder: // 0x0c
_debug('setting cylinder', toHex(val));
_cylinder = val;
break;
case LOC.ATACylinderH: // 0x0d
_debug('setting cylinder high', toHex(val));
_cylinderH = val;
break;
case LOC.ATAHead:
_head = val & 0xf;
_lba = val & 0x40 ? true : false;
_drive = val & 0x10 ? 1 : 0;
_debug('setting head', toHex(val & 0xf), 'drive', _drive);
if (!_lba) {
console.error('CHS mode not supported');
}
break;
case LOC.ATACommand: // 0x0f
_debug('command:', toHex(val));
sector = _head << 24 | _cylinderH << 16 | _cylinder << 8 | _sector;
_dumpSector(sector);
switch (val) {
case COMMANDS.ATAIdentify:
_debug('ATA identify');
_curSector = _identity[_drive];
_curWord = 0;
break;
case COMMANDS.ATACRead:
_debug('ATA read sector', toHex(_cylinderH), toHex(_cylinder), toHex(_sector), sector);
_curSector = _sectors[_drive][sector];
_curWord = 0;
break;
case COMMANDS.ATACWrite:
_debug('ATA write sector', toHex(_cylinderH), toHex(_cylinder), toHex(_sector), sector);
_curSector = _sectors[_drive][sector];
_curWord = 0;
break;
default:
debug('unknown command', toHex(val));
}
break;
default:
debug('write unknown soft switch', toHex(off), toHex(val));
}
}
return retVal;
}
_init();
return {
ioSwitch: function (off, val) {
return _access(off, val);
},
read: function(page, off) {
return rom[(page - 0xc0) << 8 | off];
},
write: function(page, off, val) {
if (_writeEEPROM) {
_debug('writing', toHex(page << 8 | off), toHex(val));
rom[(page - 0xc0) << 8 | off] - val;
}
},
getState() {
// TODO CFFA State
return {};
},
setState(_) {},
// Assign a raw disk image to a drive. Must be 2mg or raw PO image.
setBinary: function(drive, name, ext, rawData) {
drive = drive - 1;
var disk;
var options = {
rawData,
name
};
if (ext === '2mg') {
disk = new _2MG(options);
} else {
disk = new BlockVolume(options);
}
// Convert 512 byte blocks into 256 word sectors
_sectors[drive] = disk.blocks.map(function(block) {
return new Uint16Array(block.buffer);
});
_identity[drive][IDENTITY.SectorCountHigh] = _sectors[0].length & 0xffff;
_identity[drive][IDENTITY.SectorCountLow] = _sectors[0].length >> 16;
var prodos = new ProDOSVolume(disk);
dump(prodos);
_partitions[drive] = prodos;
if (drive) {
rom[SETTINGS.Max32MBPartitionsDev1] = 0x1;
} else {
rom[SETTINGS.Max32MBPartitionsDev0] = 0x1;
}
}
};
}

425
js/cards/cffa.ts Normal file
View File

@ -0,0 +1,425 @@
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
*
* Permission to use, copy, modify, distribute, and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
* the above copyright notice appear in all copies and that both that
* copyright notice and this permission notice appear in supporting
* documentation. No representations are made about the suitability of this
* software for any purpose. It is provided "as is" without express or
* implied warranty.
*/
import type { byte, Card } from '../types';
import { debug, toHex } from '../util';
import { rom as readOnlyRom } from '../roms/cards/cffa';
import { read2MGHeader } from '../formats/2mg';
import { ProDOSVolume } from '../formats/prodos';
import BlockVolume from '../formats/block';
import { dump } from '../formats/prodos/utils';
import { DiskFormat, MassStorage } from 'js/formats/types';
const rom = new Uint8Array(readOnlyRom);
const COMMANDS = {
ATACRead: 0x20,
ATACWrite: 0x30,
ATAIdentify: 0xEC
};
// CFFA Card Settings
const SETTINGS = {
Max32MBPartitionsDev0: 0x800,
Max32MBPartitionsDev1: 0x801,
DefaultBootDevice: 0x802,
DefaultBootPartition: 0x803,
Reserved: 0x804, // 4 bytes
WriteProtectBits: 0x808,
MenuSnagMask: 0x809,
MenuSnagKey: 0x80A,
BootTimeDelayTenths: 0x80B,
BusResetSeconds: 0x80C,
CheckDeviceTenths: 0x80D,
ConfigOptionBits: 0x80E,
BlockOffsetDev0: 0x80F, // 3 bytes
BlockOffsetDev1: 0x812, // 3 bytes
Unused: 0x815
};
// CFFA ATA Register Locations
const LOC = {
ATADataHigh: 0x80,
SetCSMask: 0x81,
ClearCSMask: 0x82,
WriteEEPROM: 0x83,
NoWriteEEPROM: 0x84,
ATADevCtrl: 0x86,
ATAAltStatus: 0x86,
ATADataLow: 0x88,
AError: 0x89,
ASectorCnt: 0x8a,
ASector: 0x8b,
ATACylinder: 0x8c,
ATACylinderH: 0x8d,
ATAHead: 0x8e,
ATACommand: 0x8f,
ATAStatus: 0x8f
};
// ATA Status Bits
const STATUS = {
BSY: 0x80, // Busy
DRDY: 0x40, // Drive ready. 1 when ready
DWF: 0x20, // Drive write fault. 1 when fault
DSC: 0x10, // Disk seek complete. 1 when not seeking
DRQ: 0x08, // Data request. 1 when ready to write
CORR: 0x04, // Correct data. 1 on correctable error
IDX: 0x02, // 1 once per revolution
ERR: 0x01 // Error. 1 on error
};
// ATA Identity Block Locations
const IDENTITY = {
SectorCountLow: 58,
SectorCountHigh: 57
};
export default class CFFA implements Card, MassStorage {
// CFFA internal Flags
private _disableSignalling = false;
private _writeEEPROM = true;
private _lba = true;
// LBA/CHS registers
private _sectorCnt = 1;
private _sector = 0;
private _cylinder = 0;
private _cylinderH = 0;
private _head = 0;
private _drive = 0;
// CFFA Data High register
private _dataHigh = 0;
// Current Sector
private _curSector: Uint16Array | number[];
private _curWord = 0;
// ATA Status registers
private _interruptsEnabled = false;
private _altStatus = 0;
private _error = 0;
private _identity: number[][] = [[], []];
// Disk data
private _partitions: any[] = [
// Drive 1
[],
// Drive 2
[]
];
private _sectors: Uint16Array[][] = [
// Drive 1
[],
// Drive 2
[]
];
constructor() {
debug('CFFA');
for (let idx = 0; idx < 0x100; idx++) {
this._identity[0][idx] = 0;
this._identity[1][idx] = 0;
}
rom[SETTINGS.Max32MBPartitionsDev0] = 0x1;
rom[SETTINGS.Max32MBPartitionsDev1] = 0x0;
rom[SETTINGS.BootTimeDelayTenths] = 0x5; // 0.5 seconds
rom[SETTINGS.CheckDeviceTenths] = 0x5; // 0.5 seconds
}
// Verbose debug method
private _debug(..._args: any[]) {
// debug.apply(this, arguments);
}
private _reset() {
this._debug('reset');
this._sectorCnt = 1;
this._sector = 0;
this._cylinder = 0;
this._cylinderH = 0;
this._head = 0;
this._drive = 0;
this._dataHigh = 0;
}
// Convert status register into readable string
private _statusString(status: byte) {
const statusArray = [];
let flag: keyof typeof STATUS;
for (flag in STATUS) {
if(status & STATUS[flag]) {
statusArray.push(flag);
}
}
return statusArray.join('|');
}
// Dump sector as hex and ascii
private _dumpSector(sector: number) {
if (sector >= this._sectors[this._drive].length) {
this._debug('dump sector out of range', sector);
return;
}
for (let idx = 0; idx < 16; idx++) {
const row = [];
const charRow = [];
for (let jdx = 0; jdx < 16; jdx++) {
const val = this._sectors[this._drive][sector][idx * 16 + jdx];
row.push(toHex(val, 4));
const low = val & 0x7f;
const hi = val >> 8 & 0x7f;
charRow.push(low > 0x1f ? String.fromCharCode(low) : '.');
charRow.push(hi > 0x1f ? String.fromCharCode(hi) : '.');
}
this._debug(row.join(' '), ' ', charRow.join(''));
}
}
// Card I/O access
private _access(off: byte, val: byte) {
const readMode = val === undefined;
let retVal;
let sector;
if (readMode) {
retVal = 0;
switch (off & 0x8f) {
case LOC.ATADataHigh: // 0x00
retVal = this._dataHigh;
break;
case LOC.SetCSMask: // 0x01
this._disableSignalling = true;
break;
case LOC.ClearCSMask: // 0x02
this._disableSignalling = false;
break;
case LOC.WriteEEPROM: // 0x03
this._writeEEPROM = true;
break;
case LOC.NoWriteEEPROM: // 0x04
this._writeEEPROM = false;
break;
case LOC.ATAAltStatus: // 0x06
retVal = this._altStatus;
break;
case LOC.ATADataLow: // 0x08
this._dataHigh = this._curSector[this._curWord] >> 8;
retVal = this._curSector[this._curWord] & 0xff;
if (!this._disableSignalling) {
this._curWord++;
}
break;
case LOC.AError: // 0x09
retVal = this._error;
break;
case LOC.ASectorCnt: // 0x0A
retVal = this._sectorCnt;
break;
case LOC.ASector: // 0x0B
retVal = this._sector;
break;
case LOC.ATACylinder: // 0x0C
retVal = this._cylinder;
break;
case LOC.ATACylinderH: // 0x0D
retVal = this._cylinderH;
break;
case LOC.ATAHead: // 0x0E
retVal = this._head | (this._lba ? 0x40 : 0) | (this._drive ? 0x10 : 0) | 0xA0;
break;
case LOC.ATAStatus: // 0x0F
retVal = this._sectors[this._drive].length > 0 ? STATUS.DRDY | STATUS.DSC : 0;
this._debug('returning status', this._statusString(retVal));
break;
default:
debug('read unknown soft switch', toHex(off));
}
if (off & 0x7) { // Anything but data high/low
this._debug('read soft switch', toHex(off), toHex(retVal));
}
} else {
if (off & 0x7) { // Anything but data high/low
this._debug('write soft switch', toHex(off), toHex(val));
}
switch (off & 0x8f) {
case LOC.ATADataHigh: // 0x00
this._dataHigh = val;
break;
case LOC.SetCSMask: // 0x01
this._disableSignalling = true;
break;
case LOC.ClearCSMask: // 0x02
this._disableSignalling = false;
break;
case LOC.WriteEEPROM: // 0x03
this._writeEEPROM = true;
break;
case LOC.NoWriteEEPROM: // 0x04
this._writeEEPROM = false;
break;
case LOC.ATADevCtrl: // 0x06
this._debug('devCtrl:', toHex(val));
this._interruptsEnabled = (val & 0x04) ? true : false;
this._debug('Interrupts', this._interruptsEnabled ? 'enabled' : 'disabled');
if (val & 0x02) {
this._reset();
}
break;
case LOC.ATADataLow: // 0x08
this._curSector[this._curWord] = this._dataHigh << 8 | val;
this._curWord++;
break;
case LOC.ASectorCnt: // 0x0a
this._debug('setting sector count', val);
this._sectorCnt = val;
break;
case LOC.ASector: // 0x0b
this._debug('setting sector', toHex(val));
this._sector = val;
break;
case LOC.ATACylinder: // 0x0c
this._debug('setting cylinder', toHex(val));
this._cylinder = val;
break;
case LOC.ATACylinderH: // 0x0d
this._debug('setting cylinder high', toHex(val));
this._cylinderH = val;
break;
case LOC.ATAHead:
this._head = val & 0xf;
this._lba = val & 0x40 ? true : false;
this._drive = val & 0x10 ? 1 : 0;
this._debug('setting head', toHex(val & 0xf), 'drive', this._drive);
if (!this._lba) {
console.error('CHS mode not supported');
}
break;
case LOC.ATACommand: // 0x0f
this._debug('command:', toHex(val));
sector = this._head << 24 | this._cylinderH << 16 | this._cylinder << 8 | this._sector;
this._dumpSector(sector);
switch (val) {
case COMMANDS.ATAIdentify:
this._debug('ATA identify');
this._curSector = this._identity[this._drive];
this._curWord = 0;
break;
case COMMANDS.ATACRead:
this._debug('ATA read sector', toHex(this._cylinderH), toHex(this._cylinder), toHex(this._sector), sector);
this._curSector = this._sectors[this._drive][sector];
this._curWord = 0;
break;
case COMMANDS.ATACWrite:
this._debug('ATA write sector', toHex(this._cylinderH), toHex(this._cylinder), toHex(this._sector), sector);
this._curSector = this._sectors[this._drive][sector];
this._curWord = 0;
break;
default:
debug('unknown command', toHex(val));
}
break;
default:
debug('write unknown soft switch', toHex(off), toHex(val));
}
}
return retVal;
}
ioSwitch(off: byte, val: byte) {
return this._access(off, val);
}
read(page: byte, off: byte) {
return rom[(page - 0xc0) << 8 | off];
}
write(page: byte, off: byte, val: byte) {
if (this._writeEEPROM) {
this._debug('writing', toHex(page << 8 | off), toHex(val));
rom[(page - 0xc0) << 8 | off] - val;
}
}
getState() {
// TODO CFFA State
return {};
}
setState(_: any) {}
// Assign a raw disk image to a drive. Must be 2mg or raw PO image.
setBinary(drive: number, name: string, ext: DiskFormat, rawData: ArrayBuffer) {
drive = drive - 1;
const volume = 254;
const readOnly = false;
if (ext === '2mg') {
const { bytes, offset } = read2MGHeader(rawData);
rawData = rawData.slice(offset, offset + bytes);
}
const options = {
rawData,
name,
volume,
readOnly
};
const disk = BlockVolume(options);
// Convert 512 byte blocks into 256 word sectors
this._sectors[drive] = disk.blocks.map(function(block) {
return new Uint16Array(block.buffer);
});
this._identity[drive][IDENTITY.SectorCountHigh] = this._sectors[0].length & 0xffff;
this._identity[drive][IDENTITY.SectorCountLow] = this._sectors[0].length >> 16;
const prodos = ProDOSVolume(disk);
dump(prodos);
this._partitions[drive] = prodos;
if (drive) {
rom[SETTINGS.Max32MBPartitionsDev1] = 0x1;
} else {
rom[SETTINGS.Max32MBPartitionsDev0] = 0x1;
}
return true;
}
}

View File

@ -9,22 +9,40 @@
* implied warranty.
*/
import { base64_decode, base64_encode} from '../base64';
import { bit, byte, Card, DiskFormat, MemberOf, memory, nibble, rom } from '../types';
import { base64_encode} from '../base64';
import type {
bit,
byte,
Card,
memory,
nibble,
rom,
} from '../types';
import {
FormatWorkerMessage,
FormatWorkerResponse,
DiskFormat,
DiskProcessedType,
DRIVE_NUMBERS,
DriveNumber,
JSONDisk,
ProcessBinaryType,
ProcessJsonDiskType,
ProcessJsonType,
} from '../formats/types';
import {
createDisk,
createDiskFromJsonDisk
} from '../formats/create_disk';
import { debug, toHex } from '../util';
import { Disk, JSONDisk, jsonDecode, jsonEncode, readSector } from '../formats/format_utils';
import { jsonDecode, jsonEncode, readSector } from '../formats/format_utils';
import { BOOTSTRAP_ROM_16, BOOTSTRAP_ROM_13 } from '../roms/cards/disk2';
import _2MG from '../formats/2mg';
import D13 from '../formats/d13';
import DOS from '../formats/do';
import ProDOS from '../formats/po';
import Woz from '../formats/woz';
import Nibble from '../formats/nib';
import Apple2IO from '../apple2io';
/** Softswitch locations */
const LOC = {
// Disk II Controller Commands
@ -144,10 +162,6 @@ const PHASE_DELTA = [
[-2, -1, 0, 1],
[1, -2, -1, 0]
] as const;
export const DRIVE_NUMBERS = [1, 2] as const;
export type DriveNumber = MemberOf<typeof DRIVE_NUMBERS>;
export interface Callbacks {
driveLight: (drive: DriveNumber, on: boolean) => void;
dirty: (drive: DriveNumber, dirty: boolean) => void;
@ -155,6 +169,7 @@ export interface Callbacks {
}
/** Common information for Nibble and WOZ disks. */
interface BaseDrive {
/** Current disk format. */
format: DiskFormat,
@ -176,6 +191,8 @@ interface BaseDrive {
/** WOZ format track data from https://applesaucefdc.com/woz/reference2/. */
interface WozDrive extends BaseDrive {
/** Woz encoding */
encoding: '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. */
@ -184,19 +201,25 @@ interface WozDrive extends BaseDrive {
/** Nibble format track data. */
interface NibbleDrive extends BaseDrive {
/** Nibble encoding */
encoding: 'nibble'
/** Nibble data. The index is the track number. */
tracks: memory[];
}
type Drive = WozDrive | NibbleDrive;
function isNibbleDrive(drive: Drive): drive is NibbleDrive {
return 'tracks' in drive;
function isNibbleDrive(drive: Drive): drive is NibbleDrive {
return drive.encoding === 'nibble';
}
function isWozDrive(drive: Drive): drive is WozDrive {
return drive.encoding === 'woz';
}
// Does not support WOZ disks
interface DriveState {
format: DiskFormat,
encoding: 'nibble' | 'woz'
volume: byte,
name: string,
tracks: memory[],
@ -205,6 +228,8 @@ interface DriveState {
phase: Phase,
readOnly: boolean,
dirty: boolean,
trackMap: number[],
rawTracks: bit[][],
}
interface State {
@ -216,10 +241,10 @@ interface State {
drive: DriveNumber;
}
// TODO(flan): Does not work for WOZ disks
function getDriveState(drive: Drive): DriveState {
const result: DriveState = {
format: drive.format,
encoding: drive.encoding,
volume: drive.volume,
name: drive.name,
tracks: [],
@ -227,34 +252,61 @@ function getDriveState(drive: Drive): DriveState {
head: drive.head,
phase: drive.phase,
readOnly: drive.readOnly,
dirty: drive.dirty
dirty: drive.dirty,
trackMap: [],
rawTracks: [],
};
if (!isNibbleDrive(drive)) {
throw Error('No tracks.');
if (isNibbleDrive(drive)) {
for (let idx = 0; idx < drive.tracks.length; idx++) {
result.tracks.push(new Uint8Array(drive.tracks[idx]));
}
}
for (let idx = 0; idx < drive.tracks.length; idx++) {
result.tracks.push(new Uint8Array(drive.tracks[idx]));
if (isWozDrive(drive)) {
result.trackMap = [...drive.trackMap];
for (let idx = 0; idx < drive.rawTracks.length; idx++) {
result.rawTracks.push([...drive.rawTracks[idx]]);
}
}
return result;
}
// TODO(flan): Does not work for WOZ disks
function setDriveState(state: DriveState) {
const result: Drive = {
format: state.format,
volume: state.volume,
name: state.name,
tracks: [] as memory[],
track: state.track,
head: state.head,
phase: state.phase,
readOnly: state.readOnly,
dirty: state.dirty
};
for (let idx = 0; idx < state.tracks.length; idx++) {
result.tracks!.push(new Uint8Array(state.tracks[idx]));
let result: Drive;
if (state.encoding === 'nibble') {
result = {
format: state.format,
encoding: 'nibble',
volume: state.volume,
name: state.name,
tracks: [],
track: state.track,
head: state.head,
phase: state.phase,
readOnly: state.readOnly,
dirty: state.dirty,
};
for (let idx = 0; idx < state.tracks.length; idx++) {
result.tracks.push(new Uint8Array(state.tracks[idx]));
}
} else {
result = {
format: state.format,
encoding: 'woz',
volume: state.volume,
name: state.name,
track: state.track,
head: state.head,
phase: state.phase,
readOnly: state.readOnly,
dirty: state.dirty,
trackMap: [...state.trackMap],
rawTracks: [],
};
for (let idx = 0; idx < state.rawTracks.length; idx++) {
result.rawTracks.push([...state.rawTracks[idx]]);
}
}
return result;
}
@ -266,6 +318,7 @@ export default class DiskII implements Card {
private drives: Drive[] = [
{ // Drive 1
format: 'dsk',
encoding: 'nibble',
volume: 254,
name: 'Disk 1',
tracks: [],
@ -273,10 +326,11 @@ export default class DiskII implements Card {
head: 0,
phase: 0,
readOnly: false,
dirty: false
dirty: false,
},
{ // Drive 2
format: 'dsk',
encoding: 'nibble',
volume: 254,
name: 'Disk 2',
tracks: [],
@ -284,7 +338,7 @@ export default class DiskII implements Card {
head: 0,
phase: 0,
readOnly: false,
dirty: false
dirty: false,
}];
private skip = 0;
@ -328,26 +382,26 @@ export default class DiskII implements Card {
/** 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) {
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;
this.init();
this.initWorker();
}
private debug(..._args: any[]) {
// debug.apply(this, arguments);
}
private init() {
this.debug('Disk ][');
}
// Only used for WOZ disks
private moveHead() {
if (isNibbleDrive(this.cur)) {
if (!isWozDrive(this.cur)) {
return;
}
const track: bit[] =
@ -665,7 +719,6 @@ export default class DiskII implements Card {
this.moveHead();
}
// TODO(flan): Does not work for WOZ disks
getState() {
const result = {
drives: [] as DriveState[],
@ -682,7 +735,6 @@ export default class DiskII implements Card {
return result;
}
// TODO(flan): Does not work for WOZ disks
setState(state: State) {
this.skip = state.skip;
this.latch = state.latch;
@ -699,22 +751,17 @@ export default class DiskII implements Card {
this.cur = this.drives[this.drive - 1];
}
// TODO(flan): Does not work for WOZ disks
getMetadata(driveNo: DriveNumber) {
const drive = this.drives[driveNo - 1];
if (isNibbleDrive(drive)) {
return {
format: drive.format,
volume: drive.volume,
track: drive.track,
head: drive.head,
phase: drive.phase,
readOnly: drive.readOnly,
dirty: drive.dirty
};
} else {
return null;
}
return {
format: drive.format,
volume: drive.volume,
track: drive.track,
head: drive.head,
phase: drive.phase,
readOnly: drive.readOnly,
dirty: drive.dirty
};
}
// TODO(flan): Does not work on WOZ disks
@ -728,63 +775,28 @@ export default class DiskII implements Card {
/** Sets the data for `drive` from `disk`, which is expected to be JSON. */
// TODO(flan): This implementation is not very safe.
setDisk(drive: DriveNumber, disk: JSONDisk) {
const fmt = disk.type;
const readOnly = disk.readOnly;
const name = disk.name;
let data: memory[] | memory[][];
if (disk.encoding == 'base64') {
data = [];
for (let t = 0; t < disk.data.length; t++) {
if (fmt == 'nib') {
data[t] = base64_decode(disk.data[t] as string);
} else {
data[t] = [];
for (let s = 0; s < disk.data[t].length; s++) {
data[t][s] = base64_decode(disk.data[t][s] as string);
}
}
}
setDisk(drive: DriveNumber, jsonDisk: JSONDisk) {
if (this.worker) {
const message: FormatWorkerMessage = {
type: ProcessJsonDiskType,
payload: {
drive,
jsonDisk
},
};
this.worker.postMessage(message);
return true;
} else {
data = disk.data;
const disk = createDiskFromJsonDisk(jsonDisk);
if (disk) {
const cur = this.drives[drive - 1];
Object.assign(cur, disk);
this.updateDirty(drive, false);
this.callbacks.label(drive, disk.name);
return true;
}
}
const cur = this.drives[drive - 1];
// var v = (fmt === 'dsk' ? data[0x11][0x00][0x06] : 0xfe);
// if (v == 0x00) {
const volume = disk.volume || 0xfe;
// }
const options = {
volume,
readOnly,
name,
data
};
let newDisk: Disk;
switch (fmt) {
case 'd13':
newDisk = D13(options);
break;
case 'do':
case 'dsk':
newDisk = DOS(options);
break;
case 'nib':
newDisk = Nibble(options);
break;
case 'po':
newDisk = ProDOS(options);
break;
default:
return false;
}
Object.assign(cur, newDisk);
this.updateDirty(drive, false);
this.callbacks.label(drive, name);
return false;
}
getJSON(drive: DriveNumber, pretty: boolean = false) {
@ -795,52 +807,77 @@ export default class DiskII implements Card {
return jsonEncode(cur, pretty);
}
setJSON(drive: DriveNumber, data: string) {
const cur = this.drives[drive - 1];
Object.assign(cur, jsonDecode(data));
setJSON(drive: DriveNumber, json: string) {
if (this.worker) {
const message: FormatWorkerMessage = {
type: ProcessJsonType,
payload: {
drive,
json
},
};
this.worker.postMessage(message);
} else {
const cur = this.drives[drive - 1];
Object.assign(cur, jsonDecode(json));
}
return true;
}
setBinary(drive: DriveNumber, name: string, fmt: DiskFormat, rawData: ArrayBuffer) {
let disk;
const cur = this.drives[drive - 1];
const readOnly = false;
const volume = 254;
const options = {
name,
rawData,
readOnly,
volume
volume,
};
switch (fmt) {
case '2mg':
disk = _2MG(options);
break;
case 'd13':
disk = D13(options);
break;
case 'do':
case 'dsk':
disk = DOS(options);
break;
case 'nib':
disk = Nibble(options);
break;
case 'po':
disk = ProDOS(options);
break;
case 'woz':
disk = Woz(options);
break;
default:
return false;
}
if (this.worker) {
const message: FormatWorkerMessage = {
type: ProcessBinaryType,
payload: {
drive,
fmt,
options,
}
};
this.worker.postMessage(message);
Object.assign(cur, disk);
this.updateDirty(drive, true);
this.callbacks.label(this.drive, name);
return true;
return true;
} else {
const disk = createDisk(fmt, options);
if (disk) {
const cur = this.drives[drive - 1];
Object.assign(cur, disk);
this.updateDirty(drive, true);
this.callbacks.label(this.drive, name);
return true;
}
}
return false;
}
initWorker() {
this.worker = new Worker('dist/format_worker.bundle.js');
this.worker.addEventListener('message', (message: MessageEvent<FormatWorkerResponse>) => {
const { data } = message;
switch (data.type) {
case DiskProcessedType: {
const { drive, disk } = data.payload;
if (disk) {
const cur = this.drives[drive - 1];
Object.assign(cur, disk);
this.updateDirty(drive, true);
this.callbacks.label(this.drive, disk.name);
}
}
break;
}
});
}
// TODO(flan): Does not work with WOZ disks

View File

@ -12,7 +12,9 @@
import { debug, toHex } from '../util';
import { rom as smartPortRom } from '../roms/cards/smartport';
import { Card, Restorable, byte, word, rom } from '../types';
import { MassStorage } from '../formats/types';
import CPU6502, { CpuState, flags } from '../cpu6502';
import { read2MGHeader } from '../formats/2mg';
type SmartDisk = Uint8Array[];
@ -100,7 +102,7 @@ const ADDRESS_LO = 0x44;
const BLOCK_LO = 0x46;
// const BLOCK_HI = 0x47;
export default class SmartPort implements Card, Restorable<SmartPortState> {
export default class SmartPort implements Card, MassStorage, Restorable<SmartPortState> {
private rom: rom;
private disks: SmartDisk[] = [];
@ -451,7 +453,8 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
setBinary(drive: number, _name: string, fmt: string, data: ArrayBuffer) {
this.disks[drive] = [];
if (fmt == '2mg') {
data = data.slice(64);
const { bytes, offset } = read2MGHeader(data);
data = data.slice(offset, offset + bytes);
}
for (let idx = 0; idx < data.byteLength; idx += 512) {
this.disks[drive].push(new Uint8Array(data.slice(idx, idx + 512)));

View File

@ -1,87 +0,0 @@
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
*
* Permission to use, copy, modify, distribute, and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
* the above copyright notice appear in all copies and that both that
* copyright notice and this permission notice appear in supporting
* documentation. No representations are made about the suitability of this
* software for any purpose. It is provided "as is" without express or
* implied warranty.
*/
import BlockVolume from './block';
import DOS from './do';
import Nibble from './nib';
import ProDOS from './po';
import { numToString, debug } from '../util';
/**
* Returns a `Disk` object from a 2mg image.
* @param {*} options the disk image and options
* @returns {import('./format_utils').Disk}
*/
export default function _2MG(options) {
var OFFSETS = {
CREATOR: 0x04,
FLAGS: 0x0A,
FORMAT: 0x0C,
BLOCKS: 0x14,
DATA_OFFSET: 0x18,
BYTES: 0x1C,
};
var FLAGS = {
READ_ONLY: 0x80000000,
VOLUME_VALID: 0x00000100,
VOLUME_MASK: 0x000000FF
};
var { rawData, arrayConstructor } = options;
var disk;
var volume = 254;
// Standard header size is 64 bytes. Make assumptions.
var prefix = new DataView(rawData);
var signature = numToString(prefix.getInt32(0x0, true));
if (signature !== '2IMG') {
throw new Error('Unrecognized 2mg signature: ' + signature);
}
var creator = numToString(prefix.getInt32(OFFSETS.CREATOR, true));
var format = prefix.getInt32(OFFSETS.FORMAT, true);
var bytes = prefix.getInt32(OFFSETS.BYTES, true);
var offset = prefix.getInt32(OFFSETS.DATA_OFFSET, true);
var flags = prefix.getInt32(OFFSETS.FLAGS, true);
var readOnly = (flags & FLAGS.READ_ONLY) !== 0;
if (flags & FLAGS.VOLUME_VALID) {
volume = flags & FLAGS.VOLUME_MASK;
}
debug('created by', creator);
rawData = rawData.slice(offset, offset + bytes);
var blockVolume = options.blockVolume || rawData.byteLength >= (800 * 1024);
options = { rawData, readOnly, volume, arrayConstructor };
if (blockVolume) {
disk = new BlockVolume(options);
} else {
// Check image format.
// Sure, it's really 64 bits. But only 2 are actually used.
switch (format) {
case 1: // PO
disk = new ProDOS(options);
break;
case 2: // NIB
disk = new Nibble(options);
break;
case 0: // dsk
default: // Something hinky, assume 'dsk'
disk = new DOS(options);
break;
}
}
return disk;
}

103
js/formats/2mg.ts Normal file
View File

@ -0,0 +1,103 @@
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
*
* Permission to use, copy, modify, distribute, and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
* the above copyright notice appear in all copies and that both that
* copyright notice and this permission notice appear in supporting
* documentation. No representations are made about the suitability of this
* software for any purpose. It is provided "as is" without express or
* implied warranty.
*/
import DOS from './do';
import Nibble from './nib';
import ProDOS from './po';
import { DiskOptions } from './types';
import { numToString, debug } from '../util';
export interface _2MGOptions {
blockVolume?: boolean
rawData: ArrayBuffer
readOnly: boolean
arrayConstructor: any
}
const OFFSETS = {
CREATOR: 0x04,
FLAGS: 0x0A,
FORMAT: 0x0C,
BLOCKS: 0x14,
DATA_OFFSET: 0x18,
BYTES: 0x1C,
};
const FLAGS = {
READ_ONLY: 0x80000000,
VOLUME_VALID: 0x00000100,
VOLUME_MASK: 0x000000FF
};
export function read2MGHeader(rawData: ArrayBuffer) {
const prefix = new DataView(rawData);
const signature = numToString(prefix.getInt32(0x0, true));
if (signature !== '2IMG') {
throw new Error('Unrecognized 2mg signature: ' + signature);
}
const creator = numToString(prefix.getInt32(OFFSETS.CREATOR, true));
const format = prefix.getInt32(OFFSETS.FORMAT, true);
const bytes = prefix.getInt32(OFFSETS.BYTES, true);
const offset = prefix.getInt32(OFFSETS.DATA_OFFSET, true);
const flags = prefix.getInt32(OFFSETS.FLAGS, true);
const readOnly = (flags & FLAGS.READ_ONLY) !== 0;
let volume = 254;
if (flags & FLAGS.VOLUME_VALID) {
volume = flags & FLAGS.VOLUME_MASK;
}
debug('created by', creator);
rawData = rawData.slice(offset, offset + bytes);
return {
bytes,
format,
offset,
readOnly,
volume,
};
}
/**
* Returns a `Disk` object from a 2mg image.
* @param {*} options the disk image and options
* @returns {import('./format_utils').Disk}
*/
export default function _2MG(options: DiskOptions) {
let { rawData } = options;
let disk;
if (!rawData) {
throw new Error('Requires rawData');
}
const { bytes, format, offset, readOnly, volume } = read2MGHeader(rawData);
rawData = rawData.slice(offset, offset + bytes);
options = { ...options, rawData, readOnly, volume };
// Check image format.
// Sure, it's really 64 bits. But only 2 are actually used.
switch (format) {
case 1: // PO
disk = ProDOS(options);
break;
case 2: // NIB
disk = Nibble(options);
break;
case 0: // dsk
default: // Something hinky, assume 'dsk'
disk = DOS(options);
break;
}
return disk;
}

View File

@ -9,24 +9,29 @@
* implied warranty.
*/
import { DiskOptions } from './types';
/**
* Returns a `Disk` object for a block volume with block-ordered data.
* @param {*} options the disk image and options
* @returns {import('./format_utils').Disk}
*/
export default function BlockVolume(options) {
var { rawData, readOnly, name } = options;
var disk;
export default function BlockVolume(options: DiskOptions) {
const { rawData, readOnly, name } = options;
var blocks = [];
if (!rawData) {
throw new Error('Requires rawData');
}
let blocks = [];
blocks = [];
var offset = 0;
let offset = 0;
while (offset < rawData.byteLength) {
blocks.push(new Uint8Array(rawData.slice(offset, offset + 0x200)));
offset += 0x200;
}
disk = {
const disk: { blocks: Uint8Array[], name: string, readOnly: boolean } = {
blocks,
name,
readOnly,

72
js/formats/create_disk.ts Normal file
View File

@ -0,0 +1,72 @@
import type { memory } from '../types';
import { base64_decode } from '../base64';
import type { Disk, DiskFormat, DiskOptions, JSONDisk } from './types';
import _2MG from './2mg';
import D13 from './d13';
import DOS from './do';
import ProDOS from './po';
import Woz from './woz';
import Nibble from './nib';
export function createDisk(fmt: DiskFormat, options: DiskOptions) {
let disk: Disk | null = null;
switch (fmt) {
case '2mg':
disk = _2MG(options) as Disk;
break;
case 'd13':
disk = D13(options);
break;
case 'do':
case 'dsk':
disk = DOS(options);
break;
case 'nib':
disk = Nibble(options);
break;
case 'po':
disk = ProDOS(options);
break;
case 'woz':
disk = Woz(options);
break;
}
return disk;
}
export function createDiskFromJsonDisk(disk: JSONDisk) {
const fmt = disk.type;
const readOnly = disk.readOnly;
const name = disk.name;
let trackData: memory[][];
if (disk.encoding == 'base64') {
trackData = [];
for (let t = 0; t < disk.data.length; t++) {
trackData[t] = [];
if (fmt == 'nib') {
trackData[t][0] = base64_decode(disk.data[t] as string);
} else {
for (let s = 0; s < disk.data[t].length; s++) {
trackData[t][s] = base64_decode(disk.data[t][s] as string);
}
}
}
} else {
trackData = disk.data;
}
const volume = disk.volume || 0xfe;
const options = {
volume,
readOnly,
name,
data: trackData
} as DiskOptions;
return createDisk(fmt, options);
}

View File

@ -10,50 +10,56 @@
*/
import { explodeSector13, D13O } from './format_utils';
import type { NibbleDisk, DiskOptions } from './types';
/**
* Returns a `Disk` object from DOS 3.2-ordered image data.
* @param {*} options the disk image and options
* @returns {import('./format_utils').Disk}
*/
export default function DOS13(options) {
var { data, name, rawData, volume, readOnly } = options;
var disk = {
export default function DOS13(options: DiskOptions) {
const { data, name, rawData, volume, readOnly } = options;
const disk: NibbleDisk = {
format: 'd13',
encoding: 'nibble',
name,
volume,
readOnly,
tracks: [],
trackMap: null,
rawTracks: null
tracks: []
};
if (!data && !rawData) {
throw new Error('data or rawData required');
}
/*
* DOS 13-sector disks have the physical sectors skewed on the track. The skew
* between physical sectors is 10 (A), resulting in the following physical order:
*
*
* 0 A 7 4 1 B 8 5 2 C 9 6 3
*
*
* Note that because physical sector == logical sector, this works slightly
* differently from the DOS and ProDOS nibblizers.
*/
for (var t = 0; t < 35; t++) {
var track = [];
for (var disk_sector = 0; disk_sector < 13; disk_sector++) {
var physical_sector = D13O[disk_sector];
var sector;
for (let t = 0; t < 35; t++) {
let track: number[] = [];
for (let disk_sector = 0; disk_sector < 13; disk_sector++) {
const physical_sector = D13O[disk_sector];
let sector: Uint8Array;
if (rawData) {
var off = (13 * t + physical_sector) * 256;
const off = (13 * t + physical_sector) * 256;
sector = new Uint8Array(rawData.slice(off, off + 256));
} else {
} else if (data) {
sector = data[t][physical_sector];
} else {
throw new Error('Requires data or rawData');
}
track = track.concat(
explodeSector13(volume, t, physical_sector, sector)
);
}
disk.tracks.push(track);
disk.tracks.push(new Uint8Array(track));
}
return disk;

View File

@ -11,34 +11,37 @@
import { explodeSector16, DO } from './format_utils';
import { bytify } from '../util';
import { byte } from '../types';
import { NibbleDisk, DiskOptions } from './types';
/**
* Returns a `Disk` object from DOS-ordered image data.
* @param {*} options the disk image and options
* @returns {import('./format_utils').Disk}
*/
export default function DOS(options) {
var { data, name, rawData, volume, readOnly } = options;
var disk = {
export default function DOS(options: DiskOptions) {
const { data, name, rawData, volume, readOnly } = options;
const disk: NibbleDisk = {
format: 'dsk',
encoding: 'nibble',
name,
volume,
readOnly,
tracks: [],
trackMap: null,
rawTracks: null
};
for (var t = 0; t < 35; t++) {
var track = [];
for (var physical_sector = 0; physical_sector < 16; physical_sector++) {
for (let t = 0; t < 35; t++) {
let track: byte[] = [];
for (let physical_sector = 0; physical_sector < 16; physical_sector++) {
const dos_sector = DO[physical_sector];
var sector;
let sector: Uint8Array;
if (rawData) {
const off = (16 * t + dos_sector) * 256;
sector = new Uint8Array(rawData.slice(off, off + 256));
} else if (data) {
sector = new Uint8Array(data[t][dos_sector]);
} else {
sector = data[t][dos_sector];
throw new Error('Requires data or rawData');
}
track = track.concat(
explodeSector16(volume, t, physical_sector, sector)

View File

@ -9,64 +9,10 @@
* implied warranty.
*/
import { byte, DiskFormat, memory } from '../types';
import { byte, memory } from '../types';
import { base64_decode, base64_encode } from '../base64';
import { bytify, debug, toHex } from '../util';
import { GamepadConfiguration } from '../ui/gamepad';
export interface Disk {
format: DiskFormat
name: string
volume: byte
tracks: memory[]
readOnly: boolean
}
/**
* Base format for JSON defined disks
*/
export class JSONDiskBase {
type: DiskFormat
name: string
disk?: number
category?: string
writeProtected?: boolean
volume: byte
readOnly: boolean
gamepad?: GamepadConfiguration
}
/**
* JSON Disk format with base64 encoded tracks
*/
export interface Base64JSONDisk extends JSONDiskBase {
encoding: 'base64'
data: string[]
}
/**
* JSON Disk format with byte array tracks
*/
export interface BinaryJSONDisk extends JSONDiskBase {
encoding: 'binary'
data: memory[][]
}
/**
* General JSON Disk format
*/
export type JSONDisk = Base64JSONDisk | BinaryJSONDisk;
export interface Drive {
format: DiskFormat
volume: byte
tracks: memory[]
readOnly: boolean
dirty: boolean
}
import { NibbleDisk } from './types';
/**
* DOS 3.3 Physical sector order (index is physical sector, value is DOS sector).
@ -255,7 +201,7 @@ export function explodeSector16(volume: byte, track: byte, sector: byte, data: m
return buf;
}
export function explodeSector13(volume: byte, track: byte, sector: byte, data: byte[]) {
export function explodeSector13(volume: byte, track: byte, sector: byte, data: memory) {
let buf = [];
let gap;
@ -355,7 +301,7 @@ export function explodeSector13(volume: byte, track: byte, sector: byte, data: b
}
// TODO(flan): Does not work on WOZ disks
export function readSector(drive: Drive, track: byte, sector: byte) {
export function readSector(drive: NibbleDisk, track: byte, sector: byte) {
const _sector = drive.format == 'po' ? _PO[sector] : _DO[sector];
let val, state = 0;
let idx = 0;
@ -450,7 +396,7 @@ export function readSector(drive: Drive, track: byte, sector: byte) {
return new Uint8Array();
}
export function jsonEncode(cur: Drive, pretty: boolean) {
export function jsonEncode(cur: NibbleDisk, pretty: boolean) {
// For 'nib', tracks are encoded as strings. For all other formats,
// tracks are arrays of sectors which are encoded as strings.
const data: string[] | string[][] = [];
@ -490,12 +436,13 @@ export function jsonDecode(data: string) {
}
tracks[t] = bytify(track);
}
const cur: Drive = {
const cur: NibbleDisk = {
volume: v,
format: json.type,
encoding: 'nibble',
name: json.name,
tracks,
readOnly,
dirty: false,
};
return cur;

View File

@ -9,30 +9,34 @@
* implied warranty.
*/
import { NibbleDisk, DiskOptions } from './types';
import { memory } from '../types';
/**
* Returns a `Disk` object from raw nibble image data.
* @param {*} options the disk image and options
* @returns {import('./format_utils').Disk}
*/
export default function Nibble(options) {
var { data, name, rawData, volume, readOnly } = options;
var disk = {
export default function Nibble(options: DiskOptions) {
const { data, name, rawData, volume, readOnly } = options;
const disk: NibbleDisk = {
format: 'nib',
encoding: 'nibble',
name,
volume: volume || 254,
readOnly: readOnly || false,
tracks: [],
trackMap: null,
rawTracks: null
tracks: []
};
for (var t = 0; t < 35; t++) {
var track;
for (let t = 0; t < 35; t++) {
let track: memory;
if (rawData) {
var off = t * 0x1a00;
track = new Uint8Array(data.slice(off, off + 0x1a00));
const off = t * 0x1a00;
track = new Uint8Array(rawData.slice(off, off + 0x1a00));
} else if (data) {
track = data[t][0];
} else {
track = data[t];
throw new Error('Requires data or rawData');
}
disk.tracks[t] = track;
}

View File

@ -11,34 +11,37 @@
import { explodeSector16, PO } from './format_utils';
import { bytify } from '../util';
import type { byte } from '../types';
import type { NibbleDisk, DiskOptions } from './types';
/**
* Returns a `Disk` object from ProDOS-ordered image data.
* @param {*} options the disk image and options
* @returns {import('./format_utils').Disk}
*/
export default function ProDOS(options) {
var { data, name, rawData, volume, readOnly } = options;
var disk = {
export default function ProDOS(options: DiskOptions) {
const { data, name, rawData, volume, readOnly } = options;
const disk: NibbleDisk = {
format: 'nib',
encoding: 'nibble',
name,
volume: volume || 254,
tracks: [],
readOnly: readOnly || false,
trackMap: null,
rawTracks: null
};
for (var physical_track = 0; physical_track < 35; physical_track++) {
var track = [];
for (var physical_sector = 0; physical_sector < 16; physical_sector++) {
for (let physical_track = 0; physical_track < 35; physical_track++) {
let track: byte[] = [];
for (let physical_sector = 0; physical_sector < 16; physical_sector++) {
const prodos_sector = PO[physical_sector];
var sector;
let sector;
if (rawData) {
var off = (16 * physical_track + prodos_sector) * 256;
const off = (16 * physical_track + prodos_sector) * 256;
sector = new Uint8Array(rawData.slice(off, off + 256));
} else {
} else if (data) {
sector = data[physical_track][prodos_sector];
} else {
throw new Error('Requires data or rawData');
}
track = track.concat(
explodeSector16(volume, physical_track, physical_sector, sector)

174
js/formats/types.ts Normal file
View File

@ -0,0 +1,174 @@
/* Copyright 2021 Will Scullin <scullin@scullinsteel.com>
*
* Permission to use, copy, modify, distribute, and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
* the above copyright notice appear in all copies and that both that
* copyright notice and this permission notice appear in supporting
* documentation. No representations are made about the suitability of this
* software for any purpose. It is provided "as is" without express or
* implied warranty.
*/
import type { bit, byte, memory, MemberOf } from '../types';
import type { GamepadConfiguration } from '../ui/gamepad';
export const DRIVE_NUMBERS = [1, 2] as const;
export type DriveNumber = MemberOf<typeof DRIVE_NUMBERS>;
/**
* Arguments for the disk format processors.
*/
export interface DiskOptions {
name: string
volume: byte
readOnly: boolean
data?: memory[][]
rawData?: ArrayBuffer
blockVolume?: boolean
}
/**
* Return value from disk format processors. Describes raw disk
* data which the DiskII card can process.
*/
export interface Disk {
format: DiskFormat
name: string
volume: byte
readOnly: boolean
}
export interface NibbleDisk extends Disk {
encoding: 'nibble'
tracks: memory[]
}
export interface WozDisk extends Disk {
encoding: 'woz'
trackMap: number[]
rawTracks: bit[][]
}
/**
* File types supported by the disk format processors and
* block devices.
*/
export const DISK_FORMATS = [
'2mg',
'd13',
'do',
'dsk',
'hdv',
'po',
'nib',
'woz'
] as const;
export type DiskFormat = MemberOf<typeof DISK_FORMATS>;
/**
* Base format for JSON defined disks
*/
export class JSONDiskBase {
type: DiskFormat
name: string
disk?: number
category?: string
writeProtected?: boolean
volume: byte
readOnly: boolean
gamepad?: GamepadConfiguration
}
/**
* JSON Disk format with base64 encoded tracks
*/
export interface Base64JSONDisk extends JSONDiskBase {
encoding: 'base64'
data: string[]
}
/**
* JSON Disk format with byte array tracks
*/
export interface BinaryJSONDisk extends JSONDiskBase {
encoding: 'binary'
data: memory[][]
}
/**
* General JSON Disk format
*/
export type JSONDisk = Base64JSONDisk | BinaryJSONDisk;
/**
* Process Disk message payloads for worker
*/
export const ProcessBinaryType = 'processBinary';
export const ProcessJsonDiskType = 'processJsonDisk';
export const ProcessJsonType = 'processJson';
/** Binary disk file message */
export interface ProcessBinaryMessage {
type: typeof ProcessBinaryType
payload: {
drive: DriveNumber
fmt: DiskFormat
options: DiskOptions
}
}
/** Processed JSON file message (used for localStorage) */
export interface ProcessJsonDiskMessage {
type: typeof ProcessJsonDiskType
payload: {
drive: DriveNumber
jsonDisk: JSONDisk
}
}
/** Raw JSON file message */
export interface ProcessJsonMessage {
type: typeof ProcessJsonType
payload: {
drive: DriveNumber
json: string
}
}
export type FormatWorkerMessage =
ProcessBinaryMessage |
ProcessJsonDiskMessage |
ProcessJsonMessage;
/**
* Format work result message type
*/
export const DiskProcessedType = 'diskProcessed';
export interface DiskProcessedResponse {
type: typeof DiskProcessedType
payload: {
drive: DriveNumber
disk: Disk | null
}
}
export type FormatWorkerResponse =
DiskProcessedResponse
/**
* Block device common interface
*/
export interface MassStorage {
setBinary(drive: number, name: string, ext: DiskFormat, data: ArrayBuffer): boolean
}

56
js/formats/worker.ts Normal file
View File

@ -0,0 +1,56 @@
import { debug } from '../util';
import { jsonDecode } from './format_utils';
import {
createDisk,
createDiskFromJsonDisk,
} from './create_disk';
import {
FormatWorkerMessage,
Disk,
DiskProcessedResponse,
DiskProcessedType,
ProcessBinaryType,
ProcessJsonDiskType,
ProcessJsonType,
} from './types';
debug('Worker loaded');
addEventListener('message', (message: MessageEvent<FormatWorkerMessage>) => {
debug('Worker started', message.type);
const data = message.data;
const { drive } = data.payload;
let disk: Disk | null = null;
switch (data.type) {
case ProcessBinaryType: {
const { fmt, options } = data.payload;
disk = createDisk(fmt, options);
}
break;
case ProcessJsonDiskType: {
const { jsonDisk } = data.payload;
disk = createDiskFromJsonDisk(jsonDisk);
}
break;
case ProcessJsonType: {
const { json } = data.payload;
disk = jsonDecode(json);
}
break;
}
const response: DiskProcessedResponse = {
type: DiskProcessedType,
payload: {
drive,
disk
}
};
self.postMessage(response);
debug('Worker complete', message.type);
});

View File

@ -1,4 +1,3 @@
/**
* Extracts the members of a constant array as a type. Used as:
*
@ -11,7 +10,7 @@ export type MemberOf<T extends ReadonlyArray<unknown>> =
/**
* Recursively extracts all members of a constant array as a type. Used as:
*
*
* @example
* const SOME_ARRAYS = [['a'],['b', 2], 3] as const;
* type SomeArrayValues = DeepMemberOf<typeof SOME_ARRAYS>; // 'a' | 'b' | 2 | 3
@ -23,7 +22,7 @@ export type DeepMemberOf<T extends ReadonlyArray<unknown>> =
/**
* Extracts the declared keys of a type by removing `string` and `number`.
*
*
* Cribbed from the interwebs:
* https://github.com/microsoft/TypeScript/issues/25987#issuecomment-408339599
*/
@ -39,7 +38,7 @@ export type KnownValues<T> = T extends {
/**
* Replacement for `includes` on constant types that is also a type assertion.
*
*
* @example
* const SOME_VALUES = [1, 2, 'a'] as const;
* let n: number = 1;
@ -101,35 +100,6 @@ export interface Card extends Memory, Restorable {
ioSwitch(off: byte, val?: byte): byte | undefined;
}
export const DISK_FORMATS = [
'2mg',
'd13',
'do',
'dsk',
'hdv',
'po',
'nib',
'woz'
] as const;
export type DiskFormat = MemberOf<typeof DISK_FORMATS>;
export interface Drive {
format: DiskFormat,
volume: number,
tracks: Array<byte[] | Uint8Array>,
trackMap: unknown,
}
export interface DiskIIDrive extends Drive {
rawTracks: unknown,
track: number,
head: number,
phase: number,
readOnly: boolean,
dirty: boolean,
}
export type TapeData = Array<[duration: number, high: boolean]>;
export interface Restorable<T = any> {

View File

@ -3,7 +3,14 @@ import MicroModal from 'micromodal';
import { base64_json_parse, base64_json_stringify } from '../base64';
import { Audio, SOUND_ENABLED_OPTION } from './audio';
import DriveLights from './drive_lights';
import { byte, DISK_FORMATS, includes, word } from '../types';
import { byte, includes, word } from '../types';
import { MassStorage } from '../formats/types';
import {
DISK_FORMATS,
DriveNumber,
DRIVE_NUMBERS,
JSONDisk
} from '../formats/types';
import { initGamepad, GamepadConfiguration } from './gamepad';
import KeyBoard from './keyboard';
import Tape, { TAPE_TYPES } from './tape';
@ -13,12 +20,11 @@ import ApplesoftCompiler from '../applesoft/compiler';
import { debug, gup, hup } from '../util';
import { Apple2, Stats } from '../apple2';
import DiskII, { DriveNumber, DRIVE_NUMBERS } from '../cards/disk2';
import SmartPort from '../cards/smartport';
import DiskII from '../cards/disk2';
import CPU6502 from '../cpu6502';
import { VideoModes } from '../videomodes';
import Apple2IO from '../apple2io';
import { JSONDisk } from '../formats/format_utils';
import { } from '../formats/format_utils';
import Printer from './printer';
import { OptionsModal } from './options_modal';
@ -71,7 +77,7 @@ let stats: Stats;
let vm: VideoModes;
let tape: Tape;
let _disk2: DiskII;
let _smartPort: SmartPort;
let _massStorage: MassStorage;
let _printer: Printer;
let audio: Audio;
let screen: Screen;
@ -381,8 +387,12 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) {
document.location.hash = files.join('|');
if (result.byteLength >= 800 * 1024) {
if (_smartPort.setBinary(drive, name, ext, result)) {
initGamepad();
if (includes(DISK_FORMATS, ext)) {
if (_massStorage.setBinary(drive, name, ext, result)) {
initGamepad();
} else {
openAlert(`Unable to load ${name}`);
}
}
} else {
if (includes(DISK_FORMATS, ext)
@ -442,18 +452,18 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) {
const fileParts = file.split('.');
const ext = fileParts.pop()!.toLowerCase();
const name = decodeURIComponent(fileParts.join('.'));
if (data.byteLength >= 800 * 1024) {
if (_smartPort.setBinary(drive, name, ext, data)) {
initGamepad();
}
} else {
if (includes(DISK_FORMATS, ext)) {
if (_disk2.setBinary(drive, name, ext, data)) {
if (includes(DISK_FORMATS, ext)) {
if (data.byteLength >= 800 * 1024) {
if (_massStorage.setBinary(drive, name, ext, data)) {
initGamepad();
}
} else {
throw new Error(`Extension ${ext} not recognized.`);
if (_disk2.setBinary(drive, name, ext, data)) {
initGamepad();
}
}
} else {
throw new Error(`Extension ${ext} not recognized.`);
}
loadingStop();
}).catch(function (error) {
@ -685,7 +695,6 @@ const categorySelect = document.querySelector<HTMLSelectElement>('#category_sele
declare global {
interface Window {
disk_index: DiskDescriptor[];
e: boolean;
}
}
@ -808,7 +817,7 @@ declare global {
}
}
function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer: Printer, e: boolean) {
function onLoaded(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, printer: Printer, e: boolean) {
_apple2 = apple2;
cpu = _apple2.getCPU();
io = _apple2.getIO();
@ -816,7 +825,7 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer:
vm = apple2.getVideoModes();
tape = new Tape(io);
_disk2 = disk2;
_smartPort = smartPort;
_massStorage = massStorage;
_printer = printer;
_e = e;
@ -905,8 +914,8 @@ function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer:
);
}
export function initUI(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer: Printer, e: boolean) {
export function initUI(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, printer: Printer, e: boolean) {
window.addEventListener('load', () => {
onLoaded(apple2, disk2, smartPort, printer, e);
onLoaded(apple2, disk2, massStorage, printer, e);
});
}

View File

@ -1,4 +1,5 @@
import { Callbacks, DriveNumber } from '../cards/disk2';
import { Callbacks } from '../cards/disk2';
import type { DriveNumber } from '../formats/types';
export default class DriveLights implements Callbacks {
public driveLight(drive: DriveNumber, on: boolean) {

View File

@ -347,4 +347,4 @@ describe('DOS-13 format', () => {
}
}
});
});
});

View File

@ -284,4 +284,4 @@ describe('DOS format', () => {
}
}
});
});
});

View File

@ -284,4 +284,4 @@ describe('ProDOS format', () => {
}
}
});
});
});

View File

@ -1,11 +1,11 @@
import { byte } from '../../../../js/types';
import { memory } from '../../../../js/types';
function generateBytesInOrder() {
const data: byte[][][] = [];
const data: memory[][] = [];
for (let t = 0; t < 35; t++) {
const track: byte[][] = [];
const track: memory[] = [];
for (let s = 0; s < 13; s++) {
const sector: byte[] = [];
const sector: memory = new Uint8Array(256);
for (let b = 0; b < 256; b++) {
sector[b] = b;
}
@ -16,14 +16,14 @@ function generateBytesInOrder() {
return data;
}
export const BYTES_IN_ORDER: byte[][][] = generateBytesInOrder();
export const BYTES_IN_ORDER: memory[][] = generateBytesInOrder();
function generateBytesBySector() {
const data: byte[][][] = [];
const data: memory[][] = [];
for (let t = 0; t < 35; t++) {
const track: byte[][] = [];
const track: memory[] = [];
for (let s = 0; s < 13; s++) {
const sector: byte[] = [];
const sector: memory = new Uint8Array(256);
for (let b = 0; b < 256; b++) {
sector[b] = s;
}
@ -34,14 +34,14 @@ function generateBytesBySector() {
return data;
}
export const BYTES_BY_SECTOR: byte[][][] = generateBytesBySector();
export const BYTES_BY_SECTOR: memory[][] = generateBytesBySector();
function generateBytesByTrack() {
const data: byte[][][] = [];
const data: memory[][] = [];
for (let t = 0; t < 35; t++) {
const track: byte[][] = [];
const track: memory[] = [];
for (let s = 0; s < 13; s++) {
const sector: byte[] = [];
const sector: memory = new Uint8Array(256);
for (let b = 0; b < 256; b++) {
sector[b] = t;
}
@ -52,4 +52,4 @@ function generateBytesByTrack() {
return data;
}
export const BYTES_BY_TRACK: byte[][][] = generateBytesByTrack();
export const BYTES_BY_TRACK: memory[][] = generateBytesByTrack();

View File

@ -1,11 +1,11 @@
import { byte } from '../../../../js/types';
import { memory } from '../../../../js/types';
function generateBytesInOrder() {
const data: byte[][][] = [];
const data: memory[][] = [];
for (let t = 0; t < 35; t++) {
const track: byte[][] = [];
const track: memory[] = [];
for (let s = 0; s < 16; s++) {
const sector: byte[] = [];
const sector = new Uint8Array(256);
for (let b = 0; b < 256; b++) {
sector[b] = b;
}
@ -16,14 +16,14 @@ function generateBytesInOrder() {
return data;
}
export const BYTES_IN_ORDER: byte[][][] = generateBytesInOrder();
export const BYTES_IN_ORDER: memory[][] = generateBytesInOrder();
function generateBytesBySector() {
const data: byte[][][] = [];
const data: memory[][] = [];
for (let t = 0; t < 35; t++) {
const track: byte[][] = [];
const track: memory[] = [];
for (let s = 0; s < 16; s++) {
const sector: byte[] = [];
const sector = new Uint8Array(256);
for (let b = 0; b < 256; b++) {
sector[b] = s;
}
@ -34,14 +34,14 @@ function generateBytesBySector() {
return data;
}
export const BYTES_BY_SECTOR: byte[][][] = generateBytesBySector();
export const BYTES_BY_SECTOR: memory[][] = generateBytesBySector();
function generateBytesByTrack() {
const data: byte[][][] = [];
const data: memory[][] = [];
for (let t = 0; t < 35; t++) {
const track: byte[][] = [];
const track: memory[] = [];
for (let s = 0; s < 16; s++) {
const sector: byte[] = [];
const sector = new Uint8Array(256);
for (let b = 0; b < 256; b++) {
sector[b] = t;
}
@ -52,4 +52,4 @@ function generateBytesByTrack() {
return data;
}
export const BYTES_BY_TRACK: byte[][][] = generateBytesByTrack();
export const BYTES_BY_TRACK: memory[][] = generateBytesByTrack();

View File

@ -4,6 +4,7 @@
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"target": "es6",
"lib": ["DOM", "ES2017", "WebWorker"],
"noImplicitAny": true,
"noImplicitThis": true,
"noUnusedLocals": true,

View File

@ -55,7 +55,8 @@ module.exports = [
...baseConfig,
target: false,
entry: {
audio_worker: path.resolve('js/ui/audio_worker.ts')
audio_worker: path.resolve('js/ui/audio_worker.ts'),
format_worker: path.resolve('js/formats/worker.ts')
},
output: {
publicPath: '/dist/',