Refactor disk parsing into webworker (#83)

* Refactor disk handling to allow disk processing to happen in a worker
* Type cleanup
* Convert format handlers to TypeScript
* Convert CFFA to TypeScript
This commit is contained in:
Will Scullin 2021-07-06 17:04:02 -07:00 committed by GitHub
parent 3abd168627
commit ce3631f3a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1692 additions and 1097 deletions

View File

@ -110,6 +110,14 @@
"env": {
"commonjs": true
}
},
{
"files": [
"workers/*"
],
"parserOptions": {
"project": "workers/tsconfig.json"
}
}
],
"ignorePatterns": ["coverage/**/*"]

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;
}
}
};
}

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

@ -0,0 +1,482 @@
/* 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, Restorable } 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 createBlockDisk from '../formats/block';
import { dump } from '../formats/prodos/utils';
import {
BlockDisk,
BlockFormat,
ENCODING_BLOCK,
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 interface CFFAState {
disks: Array<BlockDisk | null>
}
type Partition = ReturnType<typeof ProDOSVolume>
export default class CFFA implements Card, MassStorage, Restorable<CFFAState> {
// 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: Array<Partition|null> = [
// Drive 1
null,
// Drive 2
null
];
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() {
return {
disks: this._partitions.map(
(partition) => {
let result: BlockDisk | null = null;
if (partition) {
const disk: BlockDisk = partition.disk();
result = {
blocks: disk.blocks.map(
(block) => new Uint8Array(block)
),
encoding: ENCODING_BLOCK,
readOnly: disk.readOnly,
name: disk.name,
};
}
return result;
}
)
};
}
setState(state: CFFAState) {
state.disks.forEach(
(disk, idx) => {
if (disk) {
this.setBlockVolume(idx + 1, disk);
} else {
this.resetBlockVolume(idx + 1);
}
}
);
}
resetBlockVolume(drive: number) {
drive = drive - 1;
this._sectors[drive] = [];
this._identity[drive][IDENTITY.SectorCountHigh] = 0;
this._identity[drive][IDENTITY.SectorCountLow] = 0;
if (drive) {
rom[SETTINGS.Max32MBPartitionsDev1] = 0x0;
} else {
rom[SETTINGS.Max32MBPartitionsDev0] = 0x0;
}
}
setBlockVolume(drive: number, disk: BlockDisk) {
drive = drive - 1;
// 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;
}
// Assign a raw disk image to a drive. Must be 2mg or raw PO image.
setBinary(drive: number, name: string, ext: BlockFormat, rawData: ArrayBuffer) {
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 = createBlockDisk(options);
return this.setBlockVolume(drive, disk);
}
}

View File

@ -9,22 +9,42 @@
* 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,
NibbleFormat,
DISK_PROCESSED,
DRIVE_NUMBERS,
DriveNumber,
JSONDisk,
ENCODING_NIBBLE,
PROCESS_BINARY,
PROCESS_JSON_DISK,
PROCESS_JSON,
ENCODING_BITSTREAM,
} 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 +164,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,9 +171,10 @@ export interface Callbacks {
}
/** Common information for Nibble and WOZ disks. */
interface BaseDrive {
/** Current disk format. */
format: DiskFormat,
format: NibbleFormat,
/** Current disk volume number. */
volume: byte,
/** Displayed disk name */
@ -176,6 +193,8 @@ interface BaseDrive {
/** WOZ format track data from https://applesaucefdc.com/woz/reference2/. */
interface WozDrive extends BaseDrive {
/** Woz encoding */
encoding: typeof ENCODING_BITSTREAM
/** 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 +203,25 @@ interface WozDrive extends BaseDrive {
/** Nibble format track data. */
interface NibbleDrive extends BaseDrive {
/** Nibble encoding */
encoding: typeof 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 === ENCODING_NIBBLE;
}
function isWozDrive(drive: Drive): drive is WozDrive {
return drive.encoding === ENCODING_BITSTREAM;
}
// Does not support WOZ disks
interface DriveState {
format: DiskFormat,
format: NibbleFormat,
encoding: typeof ENCODING_BITSTREAM | typeof ENCODING_NIBBLE
volume: byte,
name: string,
tracks: memory[],
@ -205,6 +230,8 @@ interface DriveState {
phase: Phase,
readOnly: boolean,
dirty: boolean,
trackMap: number[],
rawTracks: bit[][],
}
interface State {
@ -216,10 +243,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 +254,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 === ENCODING_NIBBLE) {
result = {
format: state.format,
encoding: 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: ENCODING_BITSTREAM,
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 +320,7 @@ export default class DiskII implements Card {
private drives: Drive[] = [
{ // Drive 1
format: 'dsk',
encoding: ENCODING_NIBBLE,
volume: 254,
name: 'Disk 1',
tracks: [],
@ -273,10 +328,11 @@ export default class DiskII implements Card {
head: 0,
phase: 0,
readOnly: false,
dirty: false
dirty: false,
},
{ // Drive 2
format: 'dsk',
encoding: ENCODING_NIBBLE,
volume: 254,
name: 'Disk 2',
tracks: [],
@ -284,7 +340,7 @@ export default class DiskII implements Card {
head: 0,
phase: 0,
readOnly: false,
dirty: false
dirty: false,
}];
private skip = 0;
@ -307,6 +363,9 @@ export default class DiskII implements Card {
/** Current drive object. */
private cur = this.drives[this.drive - 1];
/** Nibbles read this on cycle */
private nibbleCount = 0;
/** Q0-Q3: Coil states. */
private q = [false, false, false, false];
@ -328,37 +387,37 @@ 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)) {
return;
}
const track: bit[] =
this.cur.rawTracks[this.cur.trackMap[this.cur.track]] || [0];
const cycles = this.io.cycles();
// Spin the disk the number of elapsed cycles since last call
let workCycles = (cycles - this.lastCycles) * 2;
this.lastCycles = cycles;
if (!isWozDrive(this.cur)) {
return;
}
const track: bit[] =
this.cur.rawTracks[this.cur.trackMap[this.cur.track]] || [0];
while (workCycles-- > 0) {
let pulse: bit = 0;
if (this.clock == 4) {
@ -531,6 +590,7 @@ export default class DiskII implements Card {
this.debug('Drive Off');
this.on = false;
this.callbacks.driveLight(this.drive, false);
this.debug('nibbles read', this.nibbleCount);
}, 1000);
}
}
@ -543,6 +603,7 @@ export default class DiskII implements Card {
}
if (!this.on) {
this.debug('Drive On');
this.nibbleCount = 0;
this.on = true;
this.lastCycles = this.io.cycles();
this.callbacks.driveLight(this.drive, true);
@ -619,6 +680,9 @@ export default class DiskII implements Card {
// also cause conflicts with the disk controller commands.
if ((off & 0x01) === 0) {
result = this.latch;
if (result & 0x80) {
this.nibbleCount++;
}
} else {
result = 0;
}
@ -665,7 +729,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 +745,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 +761,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 +785,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: PROCESS_JSON_DISK,
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 +817,78 @@ 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: PROCESS_JSON,
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];
setBinary(drive: DriveNumber, name: string, fmt: NibbleFormat, rawData: ArrayBuffer) {
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: PROCESS_BINARY,
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 DISK_PROCESSED:
{
const { drive, disk } = data.payload;
if (disk) {
const cur = this.drives[drive - 1];
Object.assign(cur, disk);
this.updateDirty(drive, true);
this.callbacks.label(drive, disk.name);
}
}
break;
}
});
}
// TODO(flan): Does not work with WOZ disks

View File

@ -12,16 +12,13 @@
import { debug, toHex } from '../util';
import { rom as smartPortRom } from '../roms/cards/smartport';
import { Card, Restorable, byte, word, rom } from '../types';
import { MassStorage, BlockDisk, ENCODING_BLOCK } from '../formats/types';
import CPU6502, { CpuState, flags } from '../cpu6502';
type SmartDisk = Uint8Array[];
interface BlockDevice {
blocks: Uint8Array
}
import { read2MGHeader } from '../formats/2mg';
import createBlockDisk from '../formats/block';
export interface SmartPortState {
disks: SmartDisk[]
disks: BlockDisk[]
}
export interface SmartPortOptions {
@ -100,10 +97,10 @@ 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[] = [];
private disks: BlockDisk[] = [];
constructor(private cpu: CPU6502, options: SmartPortOptions) {
if (options?.block) {
@ -117,13 +114,6 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
}
}
private decodeDisk(unit: number, disk: BlockDevice) {
this.disks[unit] = [];
for (let idx = 0; idx < disk.blocks.length; idx++) {
this.disks[unit][idx] = new Uint8Array(disk.blocks[idx]);
}
}
private debug(..._args: any[]) {
// debug.apply(this, arguments);
}
@ -131,16 +121,16 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
/*
* dumpBlock
*/
/*
dumpBlock(drive, block) {
const result = '';
const b;
const jdx;
for (const idx = 0; idx < 32; idx++) {
dumpBlock(drive: number, block: number) {
let result = '';
let b;
let jdx;
for (let idx = 0; idx < 32; idx++) {
result += toHex(idx << 4, 4) + ': ';
for (jdx = 0; jdx < 16; jdx++) {
b = disks[drive][block][idx * 16 + jdx];
b = this.disks[drive].blocks[block][idx * 16 + jdx];
if (jdx == 8) {
result += ' ';
}
@ -148,7 +138,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
}
result += ' ';
for (jdx = 0; jdx < 16; jdx++) {
b = disks[drive][block][idx * 16 + jdx] & 0x7f;
b = this.disks[drive].blocks[block][idx * 16 + jdx] & 0x7f;
if (jdx == 8) {
result += ' ';
}
@ -162,14 +152,14 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
}
return result;
}
*/
/*
* getDeviceInfo
*/
getDeviceInfo(state: CpuState, drive: number) {
if (this.disks[drive]) {
const blocks = this.disks[drive].length;
const blocks = this.disks[drive].blocks.length;
state.x = blocks & 0xff;
state.y = blocks >> 8;
@ -190,7 +180,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
this.debug('read buffer=' + buffer);
this.debug('read block=$' + toHex(block));
if (!this.disks[drive] || !this.disks[drive].length) {
if (!this.disks[drive]?.blocks.length) {
debug('Drive', drive, 'is empty');
return;
}
@ -198,7 +188,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
// debug('read', '\n' + dumpBlock(drive, block));
for (let idx = 0; idx < 512; idx++) {
buffer.writeByte(this.disks[drive][block][idx]);
buffer.writeByte(this.disks[drive].blocks[block][idx]);
buffer = buffer.inc(1);
}
@ -215,7 +205,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
this.debug('write buffer=' + buffer);
this.debug('write block=$' + toHex(block));
if (!this.disks[drive] || !this.disks[drive].length) {
if (!this.disks[drive]?.blocks.length) {
debug('Drive', drive, 'is empty');
return;
}
@ -223,7 +213,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
// debug('write', '\n' + dumpBlock(drive, block));
for (let idx = 0; idx < 512; idx++) {
this.disks[drive][block][idx] = buffer.readByte();
this.disks[drive].blocks[block][idx] = buffer.readByte();
buffer = buffer.inc(1);
}
state.a = 0;
@ -235,10 +225,10 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
*/
formatDevice(state: CpuState, drive: number) {
for (let idx = 0; idx < this.disks[drive].length; idx++) {
this.disks[drive][idx] = new Uint8Array();
for (let idx = 0; idx < this.disks[drive].blocks.length; idx++) {
this.disks[drive].blocks[idx] = new Uint8Array();
for (let jdx = 0; jdx < 512; jdx++) {
this.disks[drive][idx][jdx] = 0;
this.disks[drive].blocks[idx][jdx] = 0;
}
}
@ -371,7 +361,7 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
default: // Unit 1
switch (status) {
case 0:
blocks = this.disks[unit].length;
blocks = this.disks[unit].blocks.length;
buffer.writeByte(0xf0); // W/R Block device in drive
buffer.inc(1).writeByte(blocks & 0xff); // 1600 blocks
buffer.inc(2).writeByte((blocks & 0xff00) >> 8);
@ -433,33 +423,53 @@ export default class SmartPort implements Card, Restorable<SmartPortState> {
getState() {
return {
disks: this.disks.map(
(disk) => disk.map(
(block) => new Uint8Array(block)
)
(disk) => {
const result: BlockDisk = {
blocks: disk.blocks.map(
(block) => new Uint8Array(block)
),
encoding: ENCODING_BLOCK,
readOnly: disk.readOnly,
name: disk.name,
};
return result;
}
)
};
}
setState(state: SmartPortState) {
this.disks = state.disks.map(
(disk) => disk.map(
(block) => new Uint8Array(block)
)
(disk) => {
const result: BlockDisk = {
blocks: disk.blocks.map(
(block) => new Uint8Array(block)
),
encoding: ENCODING_BLOCK,
readOnly: disk.readOnly,
name: disk.name,
};
return result;
}
);
}
setBinary(drive: number, _name: string, fmt: string, data: ArrayBuffer) {
this.disks[drive] = [];
setBinary(drive: number, name: string, fmt: string, rawData: ArrayBuffer) {
const volume = 254;
const readOnly = false;
if (fmt == '2mg') {
data = data.slice(64);
}
for (let idx = 0; idx < data.byteLength; idx += 512) {
this.disks[drive].push(new Uint8Array(data.slice(idx, idx + 512)));
const { bytes, offset } = read2MGHeader(rawData);
rawData = rawData.slice(offset, offset + bytes);
}
const options = {
rawData,
name,
readOnly,
volume,
};
this.disks[drive] = createBlockDisk(options);
return true;
}
setDisk(drive: number, json: BlockDevice) {
this.decodeDisk(drive, json);
}
}

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;
}

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

@ -0,0 +1,95 @@
/* 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';
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);
return {
bytes,
creator,
format,
offset,
readOnly,
volume,
};
}
/**
* Returns a `Disk` object from a 2mg image.
* @param options the disk image and options
*/
export default function createDiskFrom2MG(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,28 @@
* implied warranty.
*/
import { DiskOptions, BlockDisk, ENCODING_BLOCK } 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}
* @param options the disk image and options
*/
export default function BlockVolume(options) {
var { rawData, readOnly, name } = options;
var disk;
export default function createBlockDisk(options: DiskOptions): BlockDisk {
const { rawData, readOnly, name } = options;
var blocks = [];
blocks = [];
var offset = 0;
if (!rawData) {
throw new Error('Requires rawData');
}
const blocks = [];
let offset = 0;
while (offset < rawData.byteLength) {
blocks.push(new Uint8Array(rawData.slice(offset, offset + 0x200)));
offset += 0x200;
}
disk = {
const disk: BlockDisk = {
encoding: ENCODING_BLOCK,
blocks,
name,
readOnly,

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

@ -0,0 +1,82 @@
import { includes, memory } from '../types';
import { base64_decode } from '../base64';
import { Disk, NibbleFormat, DiskOptions, JSONDisk, NIBBLE_FORMATS, NibbleDisk } from './types';
import createDiskFrom2MG from './2mg';
import createDiskFromD13 from './d13';
import createDiskFromDOS from './do';
import createDiskFromProDOS from './po';
import createDiskFromWoz from './woz';
import createDiskFromNibble from './nib';
/**
*
* @param fmt Type of
* @param options
* @returns A nibblized disk
*/
export function createDisk(fmt: NibbleFormat, options: DiskOptions): NibbleDisk | null {
let disk: NibbleDisk | null = null;
switch (fmt) {
case '2mg':
disk = createDiskFrom2MG(options);
break;
case 'd13':
disk = createDiskFromD13(options);
break;
case 'do':
case 'dsk':
disk = createDiskFromDOS(options);
break;
case 'nib':
disk = createDiskFromNibble(options);
break;
case 'po':
disk = createDiskFromProDOS(options);
break;
case 'woz':
disk = createDiskFromWoz(options);
break;
}
return disk;
}
export function createDiskFromJsonDisk(disk: JSONDisk): Disk | null {
const fmt = disk.type;
const readOnly = disk.readOnly;
const name = disk.name;
if (includes(NIBBLE_FORMATS, fmt)) {
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);
} else {
return null;
}
}

View File

@ -10,50 +10,56 @@
*/
import { explodeSector13, D13O } from './format_utils';
import { NibbleDisk, DiskOptions, ENCODING_NIBBLE } 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}
* @param options the disk image and options
* @returns A nibblized disk
*/
export default function DOS13(options) {
var { data, name, rawData, volume, readOnly } = options;
var disk = {
export default function createDiskFromDOS13(options: DiskOptions) {
const { data, name, rawData, volume, readOnly } = options;
const disk: NibbleDisk = {
format: 'd13',
encoding: 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, ENCODING_NIBBLE } from './types';
/**
* Returns a `Disk` object from DOS-ordered image data.
* @param {*} options the disk image and options
* @returns {import('./format_utils').Disk}
* @param options the disk image and options
* @returns A nibblized disk
*/
export default function DOS(options) {
var { data, name, rawData, volume, readOnly } = options;
var disk = {
export default function createDiskFromDOS(options: DiskOptions): NibbleDisk {
const { data, name, rawData, volume, readOnly } = options;
const disk: NibbleDisk = {
format: 'dsk',
encoding: 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, ENCODING_NIBBLE } from './types';
/**
* DOS 3.3 Physical sector order (index is physical sector, value is DOS sector).
@ -74,7 +20,7 @@ export interface Drive {
export const DO = [
0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4,
0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF
];
] as const;
/**
* DOS 3.3 Logical sector order (index is DOS sector, value is physical sector).
@ -82,7 +28,7 @@ export const DO = [
export const _DO = [
0x0, 0xD, 0xB, 0x9, 0x7, 0x5, 0x3, 0x1,
0xE, 0xC, 0xA, 0x8, 0x6, 0x4, 0x2, 0xF
];
] as const;
/**
* ProDOS Physical sector order (index is physical sector, value is ProDOS sector).
@ -90,7 +36,7 @@ export const _DO = [
export const PO = [
0x0, 0x8, 0x1, 0x9, 0x2, 0xa, 0x3, 0xb,
0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf
];
] as const;
/**
* ProDOS Logical sector order (index is ProDOS sector, value is physical sector).
@ -98,7 +44,7 @@ export const PO = [
export const _PO = [
0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe,
0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf
];
] as const;
/**
* DOS 13-sector disk physical sector order (index is disk sector, value is
@ -106,18 +52,18 @@ export const _PO = [
*/
export const D13O = [
0x0, 0xa, 0x7, 0x4, 0x1, 0xb, 0x8, 0x5, 0x2, 0xc, 0x9, 0x6, 0x3
];
] as const;
export const _D13O = [
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc
];
] as const;
const _trans53 = [
0xab, 0xad, 0xae, 0xaf, 0xb5, 0xb6, 0xb7, 0xba,
0xbb, 0xbd, 0xbe, 0xbf, 0xd6, 0xd7, 0xda, 0xdb,
0xdd, 0xde, 0xdf, 0xea, 0xeb, 0xed, 0xee, 0xef,
0xf5, 0xf6, 0xf7, 0xfa, 0xfb, 0xfd, 0xfe, 0xff
];
] as const;
const _trans62 = [
0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
@ -128,7 +74,7 @@ const _trans62 = [
0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec,
0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff
];
] as const;
export const detrans62 = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@ -147,10 +93,13 @@ export const detrans62 = [
0x00, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
0x00, 0x00, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
0x00, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F
];
] as const;
/**
* From Beneath Apple DOS
* Converts a byte into its 4x4 encoded representation
*
* @param val byte to encode.
* @returns A two byte array of representing the 4x4 encoding.
*/
export function fourXfour(val: byte): [xx: byte, yy: byte] {
let xx = val & 0xaa;
@ -163,11 +112,28 @@ export function fourXfour(val: byte): [xx: byte, yy: byte] {
return [xx, yy];
}
/**
* Converts 2 4x4 encoded bytes into a byte value
*
* @param xx First encoded byte.
* @param yy Second encoded byte.
* @returns The decoded value.
*/
export function defourXfour(xx: byte, yy: byte): byte {
return ((xx << 1) | 0x01) & yy;
}
export function explodeSector16(volume: byte, track: byte, sector: byte, data: memory) {
/**
* Converts a raw sector into a nibblized representation to be combined into a
* nibblized 16 sector track.
*
* @param volume volume number
* @param track track number
* @param sector sector number
* @param data sector data
* @returns a nibblized representation of the sector data
*/
export function explodeSector16(volume: byte, track: byte, sector: byte, data: memory): byte[] {
let buf = [];
let gap;
@ -255,7 +221,17 @@ export function explodeSector16(volume: byte, track: byte, sector: byte, data: m
return buf;
}
export function explodeSector13(volume: byte, track: byte, sector: byte, data: byte[]) {
/**
* Converts a raw sector into a nibblized representation to be combined into
* a nibblized 13 sector track.
*
* @param volume volume number
* @param track track number
* @param sector sector number
* @param data sector data
* @returns a nibblized representation of the sector data
*/
export function explodeSector13(volume: byte, track: byte, sector: byte, data: memory): byte[] {
let buf = [];
let gap;
@ -354,13 +330,22 @@ export function explodeSector13(volume: byte, track: byte, sector: byte, data: b
return buf;
}
// TODO(flan): Does not work on WOZ disks
export function readSector(drive: Drive, track: byte, sector: byte) {
const _sector = drive.format == 'po' ? _PO[sector] : _DO[sector];
/**
* Reads a sector of data from a nibblized disk
*
* TODO(flan): Does not work on WOZ disks
*
* @param disk Nibble disk
* @param track track number to read
* @param sector sector number to read
* @returns An array of sector data bytes.
*/
export function readSector(disk: NibbleDisk, track: byte, sector: byte): memory {
const _sector = disk.format == 'po' ? _PO[sector] : _DO[sector];
let val, state = 0;
let idx = 0;
let retry = 0;
const cur = drive.tracks[track];
const cur = disk.tracks[track];
function _readNext() {
const result = cur[idx++];
@ -450,32 +435,46 @@ export function readSector(drive: Drive, track: byte, sector: byte) {
return new Uint8Array();
}
export function jsonEncode(cur: Drive, pretty: boolean) {
/**
* Convert a nibblized disk into a JSON string for storage.
*
* @param disk Nibblized disk
* @param pretty Whether to format the output string
* @returns A JSON string representing the disk
*/
export function jsonEncode(disk: NibbleDisk, pretty: boolean): string {
// 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[][] = [];
let format = 'dsk';
for (let t = 0; t < cur.tracks.length; t++) {
for (let t = 0; t < disk.tracks.length; t++) {
data[t] = [];
if (cur.format === 'nib') {
if (disk.format === 'nib') {
format = 'nib';
data[t] = base64_encode(cur.tracks[t]);
data[t] = base64_encode(disk.tracks[t]);
} else {
for (let s = 0; s < 0x10; s++) {
(data[t] as string[])[s] = base64_encode(readSector(cur, t, s));
(data[t] as string[])[s] = base64_encode(readSector(disk, t, s));
}
}
}
return JSON.stringify({
'type': format,
'encoding': 'base64',
'volume': cur.volume,
'volume': disk.volume,
'data': data,
'readOnly': cur.readOnly,
'readOnly': disk.readOnly,
}, undefined, pretty ? ' ' : undefined);
}
export function jsonDecode(data: string) {
/**
* Convert a JSON string into a nibblized disk.
*
* @param data JSON string representing a disk image, created by [jsonEncode].
* @returns A nibblized disk
*/
export function jsonDecode(data: string): NibbleDisk {
const tracks: memory[] = [];
const json = JSON.parse(data);
const v = json.volume;
@ -483,20 +482,78 @@ export function jsonDecode(data: string) {
for (let t = 0; t < json.data.length; t++) {
let track: byte[] = [];
for (let s = 0; s < json.data[t].length; s++) {
const _s = 15 - s;
const _s = json.type == 'po' ? PO[s] : DO[s];
const sector: string = json.data[t][_s];
const d = base64_decode(sector);
track = track.concat(explodeSector16(v, t, s, d));
}
tracks[t] = bytify(track);
}
const cur: Drive = {
const disk: NibbleDisk = {
volume: v,
format: json.type,
encoding: ENCODING_NIBBLE,
name: json.name,
tracks,
readOnly,
dirty: false,
};
return cur;
return disk;
}
export function analyseDisk(disk: NibbleDisk) {
for (let track = 0; track < 35; track++) {
let outStr = `${toHex(track)}: `;
let val, state = 0;
let idx = 0;
const cur = disk.tracks[track];
const _readNext = () => {
const result = cur[idx++];
return result;
};
const _skipBytes = (count: number) => {
idx += count;
};
let t = 0, s = 0, v = 0, checkSum;
while (idx < cur.length) {
switch (state) {
case 0:
val = _readNext();
state = (val === 0xd5) ? 1 : 0;
break;
case 1:
val = _readNext();
state = (val === 0xaa) ? 2 : 0;
break;
case 2:
val = _readNext();
state = (val === 0x96) ? 3 : (val === 0xad ? 4 : 0);
break;
case 3: // Address
v = defourXfour(_readNext(), _readNext()); // Volume
t = defourXfour(_readNext(), _readNext());
s = defourXfour(_readNext(), _readNext());
checkSum = defourXfour(_readNext(), _readNext());
if (checkSum != (v ^ t ^ s)) {
debug('Invalid header checksum:', toHex(v), toHex(t), toHex(s), toHex(checkSum));
} else {
outStr += toHex(s, 1);
}
_skipBytes(3); // Skip footer
state = 0;
break;
case 4: // Valid header
_skipBytes(0x159); // Skip data, checksum and footer
state = 0;
break;
default:
break;
}
}
debug(outStr);
}
return new Uint8Array();
}

View File

@ -9,30 +9,34 @@
* implied warranty.
*/
import { NibbleDisk, DiskOptions, ENCODING_NIBBLE } 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}
* @param options the disk image and options
* @returns A nibblized disk
*/
export default function Nibble(options) {
var { data, name, rawData, volume, readOnly } = options;
var disk = {
export default function createDiskFromNibble(options: DiskOptions): NibbleDisk {
const { data, name, rawData, volume, readOnly } = options;
const disk: NibbleDisk = {
format: 'nib',
encoding: 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 { NibbleDisk, DiskOptions, ENCODING_NIBBLE } from './types';
/**
* Returns a `Disk` object from ProDOS-ordered image data.
* @param {*} options the disk image and options
* @returns {import('./format_utils').Disk}
* @param options the disk image and options
* @returns A nibblized disk
*/
export default function ProDOS(options) {
var { data, name, rawData, volume, readOnly } = options;
var disk = {
export default function createDiskFromProDOS(options: DiskOptions) {
const { data, name, rawData, volume, readOnly } = options;
const disk: NibbleDisk = {
format: 'nib',
encoding: 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)

View File

@ -18,6 +18,10 @@ export function ProDOSVolume(disk) {
var _bitMap;
return {
disk() {
return _disk;
},
blocks() {
return _disk.blocks;
},

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

@ -0,0 +1,193 @@
/* 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/types';
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 {
name: string
readOnly: boolean
}
export const ENCODING_NIBBLE = 'nibble';
export const ENCODING_BITSTREAM = 'bitstream';
export const ENCODING_BLOCK = 'block';
export interface NibbleDisk extends Disk {
encoding: typeof ENCODING_NIBBLE
format: DiskFormat
volume: byte
tracks: memory[]
}
export interface WozDisk extends Disk {
encoding: typeof ENCODING_BITSTREAM
trackMap: number[]
rawTracks: bit[][]
tracks: memory[]
}
export interface BlockDisk extends Disk {
encoding: typeof ENCODING_BLOCK
blocks: Uint8Array[]
}
/**
* File types supported by the disk format processors and
* block devices.
*/
export const NIBBLE_FORMATS = [
'2mg',
'd13',
'do',
'dsk',
'po',
'nib',
'woz'
] as const;
export const BLOCK_FORMATS = [
'2mg',
'hdv',
'po',
] as const;
export const DISK_FORMATS = [...NIBBLE_FORMATS, ...BLOCK_FORMATS ] as const;
export type NibbleFormat = MemberOf<typeof NIBBLE_FORMATS>;
export type BlockFormat = MemberOf<typeof BLOCK_FORMATS>;
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 PROCESS_BINARY = 'PROCESS_BINARY';
export const PROCESS_JSON_DISK = 'PROCESS_JSON_DISK';
export const PROCESS_JSON = 'PROCESS_JSON';
/** Binary disk file message */
export interface ProcessBinaryMessage {
type: typeof PROCESS_BINARY
payload: {
drive: DriveNumber
fmt: NibbleFormat
options: DiskOptions
}
}
/** Processed JSON file message (used for localStorage) */
export interface ProcessJsonDiskMessage {
type: typeof PROCESS_JSON_DISK
payload: {
drive: DriveNumber
jsonDisk: JSONDisk
}
}
/** Raw JSON file message */
export interface ProcessJsonMessage {
type: typeof PROCESS_JSON
payload: {
drive: DriveNumber
json: string
}
}
export type FormatWorkerMessage =
ProcessBinaryMessage |
ProcessJsonDiskMessage |
ProcessJsonMessage;
/**
* Format work result message type
*/
export const DISK_PROCESSED = 'DISK_PROCESSED';
export interface DiskProcessedResponse {
type: typeof DISK_PROCESSED
payload: {
drive: DriveNumber
disk: Disk | null
}
}
export type FormatWorkerResponse =
DiskProcessedResponse
/**
* Block device common interface
*/
export interface MassStorage {
setBinary(drive: number, name: string, ext: BlockFormat, data: ArrayBuffer): boolean
}

View File

@ -10,13 +10,14 @@
*/
import { debug, toHex } from '../util';
import { ENCODING_BITSTREAM } from './types';
var WOZ_HEADER_START = 0;
var WOZ_HEADER_SIZE = 12;
const WOZ_HEADER_START = 0;
const WOZ_HEADER_SIZE = 12;
var WOZ1_SIGNATURE = 0x315A4F57;
var WOZ2_SIGNATURE = 0x325A4F57;
var WOZ_INTEGRITY_CHECK = 0x0a0d0aff;
const WOZ1_SIGNATURE = 0x315A4F57;
const WOZ2_SIGNATURE = 0x325A4F57;
const WOZ_INTEGRITY_CHECK = 0x0a0d0aff;
function stringFromBytes(data, start, end) {
return String.fromCharCode.apply(
@ -26,11 +27,11 @@ function stringFromBytes(data, start, end) {
}
function grabNibble(bits, offset) {
var nibble = 0;
var waitForOne = true;
let nibble = 0;
let waitForOne = true;
while (offset < bits.length) {
var bit = bits[offset];
const bit = bits[offset];
if (bit) {
nibble = (nibble << 1) | 0x01;
waitForOne = false;
@ -79,7 +80,7 @@ function InfoChunk(data) {
function TMapChunk(data) {
this.trackMap = [];
for (var idx = 0; idx < 160; idx++) {
for (let idx = 0; idx < 160; idx++) {
this.trackMap.push(data.getUint8(idx));
}
@ -87,29 +88,28 @@ function TMapChunk(data) {
}
function TrksChunk(data) {
var WOZ_TRACK_SIZE = 6656;
var WOZ_TRACK_INFO_BITS = 6648;
const WOZ_TRACK_SIZE = 6656;
const WOZ_TRACK_INFO_BITS = 6648;
this.rawTracks = [];
this.tracks = [];
for (var trackNo = 0, idx = 0; idx < data.byteLength; idx += WOZ_TRACK_SIZE, trackNo++) {
var jdx;
var track = [];
var rawTrack = [];
var slice = data.buffer.slice(data.byteOffset + idx, data.byteOffset + idx + WOZ_TRACK_SIZE);
var trackData = new Uint8Array(slice);
var trackInfo = new DataView(slice);
var trackBitCount = trackInfo.getUint16(WOZ_TRACK_INFO_BITS, true);
for (jdx = 0; jdx < trackBitCount; jdx++) {
var byteIndex = jdx >> 3;
var bitIndex = 7 - (jdx & 0x07);
for (let trackNo = 0, idx = 0; idx < data.byteLength; idx += WOZ_TRACK_SIZE, trackNo++) {
let track = [];
const rawTrack = [];
const slice = data.buffer.slice(data.byteOffset + idx, data.byteOffset + idx + WOZ_TRACK_SIZE);
const trackData = new Uint8Array(slice);
const trackInfo = new DataView(slice);
const trackBitCount = trackInfo.getUint16(WOZ_TRACK_INFO_BITS, true);
for (let jdx = 0; jdx < trackBitCount; jdx++) {
const byteIndex = jdx >> 3;
const bitIndex = 7 - (jdx & 0x07);
rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x1;
}
track = [];
var offset = 0;
let offset = 0;
while (offset < rawTrack.length) {
var result = grabNibble(rawTrack, offset);
const result = grabNibble(rawTrack, offset);
if (!result.nibble) { break; }
track.push(result.nibble);
offset = result.offset + 1;
@ -123,12 +123,12 @@ function TrksChunk(data) {
}
function TrksChunk2(data) {
var trackNo;
let trackNo;
this.trks = [];
for (trackNo = 0; trackNo < 160; trackNo++) {
var startBlock = data.getUint16(trackNo * 8, true);
var blockCount = data.getUint16(trackNo * 8 + 2, true);
var bitCount = data.getUint32(trackNo * 8 + 4, true);
const startBlock = data.getUint16(trackNo * 8, true);
const blockCount = data.getUint16(trackNo * 8 + 2, true);
const bitCount = data.getUint32(trackNo * 8 + 4, true);
if (bitCount === 0) { break; }
this.trks.push({
startBlock: startBlock,
@ -139,42 +139,42 @@ function TrksChunk2(data) {
this.tracks = [];
this.rawTracks = [];
var bits = data.buffer;
const bits = data.buffer;
for (trackNo = 0; trackNo < this.trks.length; trackNo++) {
var trk = this.trks[trackNo];
var track = [];
var rawTrack = [];
var start = trk.startBlock * 512;
var end = start + trk.blockCount * 512;
var slice = bits.slice(start, end);
var trackData = new Uint8Array(slice);
for (var jdx = 0; jdx < trk.bitCount; jdx++) {
var byteIndex = jdx >> 3;
var bitIndex = 7 - (jdx & 0x07);
const trk = this.trks[trackNo];
let track = [];
const rawTrack = [];
const start = trk.startBlock * 512;
const end = start + trk.blockCount * 512;
const slice = bits.slice(start, end);
const trackData = new Uint8Array(slice);
for (let jdx = 0; jdx < trk.bitCount; jdx++) {
const byteIndex = jdx >> 3;
const bitIndex = 7 - (jdx & 0x07);
rawTrack[jdx] = (trackData[byteIndex] >> bitIndex) & 0x1;
}
track = [];
var offset = 0;
let offset = 0;
while (offset < rawTrack.length) {
var result = grabNibble(rawTrack, offset);
const result = grabNibble(rawTrack, offset);
if (!result.nibble) { break; }
track.push(result.nibble);
offset = result.offset + 1;
}
this.tracks[trackNo] = track;
this.rawTracks[trackNo] = rawTrack;
this.tracks[trackNo] = new Uint8Array(track);
this.rawTracks[trackNo] = new Uint8Array(rawTrack);
}
return this;
}
function MetaChunk(data) {
var infoStr = stringFromBytes(data, 0, data.byteLength);
var parts = infoStr.split('\n');
var info = parts.reduce(function(acc, part) {
var subParts = part.split('\t');
const infoStr = stringFromBytes(data, 0, data.byteLength);
const parts = infoStr.split('\n');
const info = parts.reduce(function(acc, part) {
const subParts = part.split('\t');
acc[subParts[0]] = subParts[1];
return acc;
}, {});
@ -189,19 +189,20 @@ function MetaChunk(data) {
* @param {*} options the disk image and options
* @returns {import('./format_utils').Disk}
*/
export default function Woz(options) {
var { rawData } = options;
var dv = new DataView(rawData, 0);
var dvOffset = 0;
var disk = {
format: 'woz'
export default function createDiskFromWoz(options) {
const { rawData } = options;
const dv = new DataView(rawData, 0);
let dvOffset = 0;
const disk = {
format: 'woz',
encoding: ENCODING_BITSTREAM,
};
var wozVersion;
var chunks = {};
let wozVersion;
const chunks = {};
function readHeader() {
var wozSignature = dv.getUint32(WOZ_HEADER_START + 0, true);
const wozSignature = dv.getUint32(WOZ_HEADER_START + 0, true);
switch (wozSignature) {
case WOZ1_SIGNATURE:
@ -226,9 +227,9 @@ export default function Woz(options) {
return null;
}
var type = dv.getUint32(dvOffset, true);
var size = dv.getUint32(dvOffset + 4, true);
var data = new DataView(dv.buffer, dvOffset + 8, size);
const type = dv.getUint32(dvOffset, true);
const size = dv.getUint32(dvOffset + 4, true);
const data = new DataView(dv.buffer, dvOffset + 8, size);
dvOffset += size + 8;
return {
@ -240,7 +241,7 @@ export default function Woz(options) {
if (readHeader()) {
dvOffset = WOZ_HEADER_SIZE;
var chunk = readChunk();
let chunk = readChunk();
while (chunk) {
switch (chunk.type) {
case 0x4F464E49: // INFO
@ -275,7 +276,7 @@ export default function Woz(options) {
disk.tracks = chunks.trks.tracks;
disk.rawTracks = chunks.trks.rawTracks;
disk.readOnly = true; //chunks.info.writeProtected === 1;
disk.name = chunks.info.title;
disk.name = chunks.meta?.title || options.name;
return disk;
}

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,22 +3,29 @@ 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 { initGamepad, GamepadConfiguration } from './gamepad';
import { byte, includes, word } from '../types';
import { BLOCK_FORMATS, MassStorage, NIBBLE_FORMATS } from '../formats/types';
import {
DISK_FORMATS,
DriveNumber,
DRIVE_NUMBERS,
JSONDisk
} from '../formats/types';
import { initGamepad } from './gamepad';
import KeyBoard from './keyboard';
import Tape, { TAPE_TYPES } from './tape';
import type { GamepadConfiguration } from './types';
import ApplesoftDump from '../applesoft/decompiler';
import ApplesoftCompiler from '../applesoft/compiler';
import { debug, gup, hup } from '../util';
import { debug } 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 +78,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;
@ -98,9 +105,10 @@ export function compileAppleSoftProgram(program: string) {
}
export function openLoad(driveString: string, event: MouseEvent) {
const drive = parseInt(driveString, 10);
const drive = parseInt(driveString, 10) as DriveNumber;
_currentDrive = drive;
if (event.metaKey && includes(DRIVE_NUMBERS, drive)) {
openLoadHTTP(drive);
openLoadHTTP();
} else {
if (disk_cur_cat[drive]) {
const element = document.querySelector<HTMLSelectElement>('#category_select')!;
@ -380,16 +388,25 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) {
files[drive - 1] = '';
document.location.hash = files.join('|');
if (result.byteLength >= 800 * 1024) {
if (_smartPort.setBinary(drive, name, ext, result)) {
initGamepad();
}
} else {
if (includes(DISK_FORMATS, ext)
&& _disk2.setBinary(drive, name, ext, result)) {
initGamepad();
if (includes(DISK_FORMATS, ext)) {
if (result.byteLength >= 800 * 1024) {
if (
includes(BLOCK_FORMATS, ext) &&
_massStorage.setBinary(drive, name, ext, result)
) {
initGamepad();
} else {
openAlert(`Unable to load ${name}`);
}
} else {
openAlert(`Unable to load ${name}`);
if (
includes(NIBBLE_FORMATS, ext) &&
_disk2.setBinary(drive, name, ext, result)
) {
initGamepad();
} else {
openAlert(`Unable to load ${name}`);
}
}
}
loadingStop();
@ -442,18 +459,24 @@ 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 (
includes(BLOCK_FORMATS, ext) &&
_massStorage.setBinary(drive, name, ext, data)
) {
initGamepad();
}
} else {
throw new Error(`Extension ${ext} not recognized.`);
if (
includes(NIBBLE_FORMATS, ext) &&
_disk2.setBinary(drive, name, ext, data)
) {
initGamepad();
}
}
} else {
throw new Error(`Extension ${ext} not recognized.`);
}
loadingStop();
}).catch(function (error) {
@ -463,8 +486,7 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) {
}
}
function openLoadHTTP(drive: DriveNumber) {
_currentDrive = drive;
function openLoadHTTP() {
MicroModal.show('http-modal');
}
@ -685,7 +707,6 @@ const categorySelect = document.querySelector<HTMLSelectElement>('#category_sele
declare global {
interface Window {
disk_index: DiskDescriptor[];
e: boolean;
}
}
@ -808,7 +829,29 @@ declare global {
}
}
function onLoaded(apple2: Apple2, disk2: DiskII, smartPort: SmartPort, printer: Printer, e: boolean) {
/**
* Returns the value of a query parameter or the empty string if it does not
* exist.
* @param name the parameter name. Note that `name` must not have any RegExp
* meta-characters except '[' and ']' or it will fail.
*/
function gup(name: string) {
const params = new URLSearchParams(window.location.search);
return params.get(name);
}
/** Returns the URL fragment. */
function hup() {
const regex = new RegExp('#(.*)');
const results = regex.exec(window.location.hash);
if (!results)
return '';
else
return results[1];
}
function onLoaded(apple2: Apple2, disk2: DiskII, massStorage: MassStorage, printer: Printer, e: boolean) {
_apple2 = apple2;
cpu = _apple2.getCPU();
io = _apple2.getIO();
@ -816,7 +859,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 +948,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

@ -10,50 +10,10 @@
*/
import Apple2IO from '../apple2io';
import { KnownKeys } from '../types';
import { BUTTON, ButtonType, GamepadConfiguration } from './types';
export let gamepad: Gamepad | null = null;
const BUTTON = {
// Buttons
'A': 0,
'B': 1,
'X': 2,
'Y': 3,
// Triggers
'L1': 4,
'R1': 5,
// Analog stick buttons
'L3': 6,
'R3': 7,
// Special
'START': 8,
'SELECT': 9,
'LOGO': 10,
// D pad
'UP': 11,
'DOWN': 12,
'LEFT': 13,
'RIGHT': 14
} as const;
type ButtonType = KnownKeys<typeof BUTTON>;
/**
* A `GamepadConfiguration` maps buttons on the controller to Apple Paddle
* buttons or keys on the keyboard. If the value is a number, it must be
* 0 | 1 | 2 and will map to the corresponding paddle button. If the value
* is a string, the _first_ character of the string is used as a key to
* press on the keyboard.
*/
export type GamepadConfiguration = {
[K in ButtonType]?: 0 | 1 | 2 | string;
};
const DEFAULT_GAMEPAD: GamepadConfiguration = {
'A': 0,
'B': 1,
@ -64,7 +24,7 @@ const DEFAULT_GAMEPAD: GamepadConfiguration = {
/**
* An array with 16 entries. For each entry _e_:
*
*
* * if _e_ <= 0, then _-e_ is 0 | 1 | 2 and represents a joystick button;
* * if _e_ > 0, then _e_ is a key on the keyboard that is pressed;
* * if _e_ is undefined, nothing happens.

52
js/ui/types.ts Normal file
View File

@ -0,0 +1,52 @@
/* Copyright 2010-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 { KnownKeys } from '../types';
export const BUTTON = {
// Buttons
'A': 0,
'B': 1,
'X': 2,
'Y': 3,
// Triggers
'L1': 4,
'R1': 5,
// Analog stick buttons
'L3': 6,
'R3': 7,
// Special
'START': 8,
'SELECT': 9,
'LOGO': 10,
// D pad
'UP': 11,
'DOWN': 12,
'LEFT': 13,
'RIGHT': 14
} as const;
export type ButtonType = KnownKeys<typeof BUTTON>;
/**
* A `GamepadConfiguration` maps buttons on the controller to Apple Paddle
* buttons or keys on the keyboard. If the value is a number, it must be
* 0 | 1 | 2 and will map to the corresponding paddle button. If the value
* is a string, the _first_ character of the string is used as a key to
* press on the keyboard.
*/
export type GamepadConfiguration = {
[K in ButtonType]?: 0 | 1 | 2 | string;
};

View File

@ -91,27 +91,6 @@ export function toBinary(v: byte) {
return result;
}
/**
* Returns the value of a query parameter or the empty string if it does not
* exist.
* @param name the parameter name. Note that `name` must not have any RegExp
* meta-characters except '[' and ']' or it will fail.
*/
export function gup(name: string) {
const params = new URLSearchParams(window.location.search);
return params.get(name);
}
/** Returns the URL fragment. */
export function hup() {
const regex = new RegExp('#(.*)');
const results = regex.exec(window.location.hash);
if (!results)
return '';
else
return results[1];
}
/** Packs a 32-bit integer into a string in little-endian order. */
export function numToString(num: number) {
let result = '';

1
package-lock.json generated
View File

@ -30,6 +30,7 @@
"jest-image-snapshot": "^4.5.0",
"node-forge": "^0.10.0",
"raw-loader": "^4.0.0",
"rimraf": "^3.0.2",
"ts-jest": "^26.5.0",
"ts-loader": "^8.0.15",
"typescript": "^4.1.3",

View File

@ -3,11 +3,11 @@
"version": "0.0.1",
"description": "Apple II Emulator in JavaScript",
"scripts": {
"build": "webpack --mode=production",
"build": "rimraf dist/* && webpack --mode=production",
"dev": "webpack serve --mode=development",
"index": "bin/index > json/disks/index.js",
"lint": "eslint '**/*.js' '**/*.ts'",
"start": "webpack serve --mode=development",
"start": "webpack serve --mode=development --progress",
"test": "jest"
},
"engines": {
@ -42,6 +42,7 @@
"jest-image-snapshot": "^4.5.0",
"node-forge": "^0.10.0",
"raw-loader": "^4.0.0",
"rimraf": "^3.0.2",
"ts-jest": "^26.5.0",
"ts-loader": "^8.0.15",
"typescript": "^4.1.3",

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", "ES6"],
"noImplicitAny": true,
"noImplicitThis": true,
"noUnusedLocals": true,

View File

@ -1,4 +1,5 @@
const path = require('path');
const { merge } = require('webpack-merge');
const baseConfig = {
devtool: 'source-map',
@ -16,22 +17,24 @@ const baseConfig = {
},
],
},
output: {
publicPath: 'dist/',
path: path.resolve('dist/'),
filename: '[name].bundle.js',
chunkFilename: '[name].bundle.js',
},
resolve: {
extensions: ['.ts', '.js'],
},
};
module.exports = [
const appConfig = merge(baseConfig,
{
...baseConfig,
entry: {
main2: path.resolve('js/entry2.js'),
main2e: path.resolve('js/entry2e.js')
},
output: {
path: path.resolve('dist/'),
filename: '[name].bundle.js',
chunkFilename: '[name].bundle.js',
library: {
name: 'Apple2',
type: 'umd',
@ -47,21 +50,34 @@ module.exports = [
directory: __dirname,
},
devMiddleware: {
publicPath: '/dist/',
publicPath: 'dist/',
},
},
},
}
);
const workletConfig = merge(baseConfig,
{
...baseConfig,
target: false,
entry: {
audio_worker: path.resolve('js/ui/audio_worker.ts')
},
output: {
publicPath: '/dist/',
path: path.resolve('dist/'),
filename: '[name].bundle.js',
globalObject: 'globalThis',
},
}
);
const workerConfig = merge(baseConfig,
{
target: false,
entry: {
format_worker: path.resolve('workers/format.worker.ts')
},
output: {
globalObject: 'globalThis',
},
},
];
);
exports.default = [appConfig, workletConfig, workerConfig];

67
workers/format.worker.ts Normal file
View File

@ -0,0 +1,67 @@
/* 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 { debug } from '../js/util';
import { jsonDecode } from '../js/formats/format_utils';
import {
createDisk,
createDiskFromJsonDisk,
} from '../js/formats/create_disk';
import {
FormatWorkerMessage,
Disk,
DiskProcessedResponse,
DISK_PROCESSED,
PROCESS_BINARY,
PROCESS_JSON_DISK,
PROCESS_JSON,
} from '../js/formats/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 PROCESS_BINARY: {
const { fmt, options } = data.payload;
disk = createDisk(fmt, options);
}
break;
case PROCESS_JSON_DISK: {
const { jsonDisk } = data.payload;
disk = createDiskFromJsonDisk(jsonDisk);
}
break;
case PROCESS_JSON: {
const { json } = data.payload;
disk = jsonDecode(json);
}
break;
}
const response: DiskProcessedResponse = {
type: DISK_PROCESSED,
payload: {
drive,
disk
}
};
self.postMessage(response);
debug('Worker complete', message.type);
});

9
workers/tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"lib": ["ES6", "WebWorker"]
},
"include": [
"./**/*"
]
}