From dabda76bc0b7ca59d0480e7c6e5665073dda03a7 Mon Sep 17 00:00:00 2001 From: Will Scullin Date: Sun, 19 Jan 2025 10:37:49 -0800 Subject: [PATCH] Switch to allow async I/O using block disks --- .eslintrc.json | 1 + asm/.gitignore | 2 + asm/smartport.s | 36 ++- js/cards/cffa.ts | 227 +++++++++------ js/cards/disk2.ts | 15 +- js/cards/smartport.ts | 399 ++++++++++++++++---------- js/components/DownloadModal.tsx | 35 +-- js/components/debugger/Disks.tsx | 204 +++++++------ js/components/util/files.ts | 81 ++++-- js/components/util/http_block_disk.ts | 75 +++++ js/formats/2mg.ts | 18 +- js/formats/block.ts | 13 +- js/formats/po.ts | 18 +- js/formats/prodos/base_file.ts | 4 +- js/formats/prodos/bit_map.ts | 43 +-- js/formats/prodos/directory.ts | 35 ++- js/formats/prodos/file_entry.ts | 27 +- js/formats/prodos/prodos_volume.ts | 14 +- js/formats/prodos/sapling_file.ts | 44 +-- js/formats/prodos/seedling_file.ts | 28 +- js/formats/prodos/tree_file.ts | 57 ++-- js/formats/prodos/utils.ts | 4 +- js/formats/prodos/vdh.ts | 20 +- js/formats/types.ts | 38 ++- js/roms/cards/smartport.ts | 48 ++-- js/types.ts | 2 +- js/ui/apple2.ts | 94 +++--- js/videomodes.ts | 2 + test/js/cards/disk2.spec.ts | 251 +++++++++++----- test/js/formats/2mg.spec.ts | 19 +- test/js/formats/po.spec.ts | 11 +- 31 files changed, 1190 insertions(+), 675 deletions(-) create mode 100644 asm/.gitignore create mode 100644 js/components/util/http_block_disk.ts diff --git a/.eslintrc.json b/.eslintrc.json index c426746f..95039191 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -95,6 +95,7 @@ "allowBoolean": true } ], + "@typescript-eslint/require-await": ["off"], // react rules "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "error" diff --git a/asm/.gitignore b/asm/.gitignore new file mode 100644 index 00000000..d6f28d11 --- /dev/null +++ b/asm/.gitignore @@ -0,0 +1,2 @@ +smartport +_FileInformation.txt diff --git a/asm/smartport.s b/asm/smartport.s index 9af5b6f6..57ff718b 100644 --- a/asm/smartport.s +++ b/asm/smartport.s @@ -16,6 +16,10 @@ MSLOT EQU $7F8 ; Slot I/O addresses STATUS EQU $C080 +READY EQU $C081 +XREG EQU $C082 +YREG EQU $C083 +CARRY EQU $C084 ; ROM addresses BASIC EQU $E000 @@ -72,9 +76,35 @@ REENTRY PLA ; Restore slot address TAX JMP BOOT DS 2 -BLOCK_ENT RTS - DS 2 -SMARTPOINT_ENT RTS +BLOCK_ENT JMP COMMON_ENT +SMARTPORT_ENT JMP COMMON_ENT +COMMON_ENT LDA $00 ; Save $00 + PHA + LDA #$60 ; Create a known RTS because ROM may be unavailable + STA $00 + JSR $0000 + TSX + LDA $0100,X ; Load ROM high byte + ASL ; Convert to index for I/O register + ASL + ASL + ASL + TAX + PLA ; Restore $00 + STA $00 +BUSY_LOOP LDA READY,X ; STATUS will return $80 until ready + BMI BUSY_LOOP + PHA ; Save A + LDA XREG,X ; Read X register + PHA ; Save X + LDA YREG,X ; Read Y register + PHA ; Save Y + LDA CARRY,X ; Get Carry status + ROR A ; Set or clear carry + PLY ; Restore Y + PLX ; Restore X + PLA ; Restore A + RTS PADDING DS $C7FE - PADDING ORG $C7FE FLAGS DFB $D7 diff --git a/js/cards/cffa.ts b/js/cards/cffa.ts index 7ce6f18b..57542c00 100644 --- a/js/cards/cffa.ts +++ b/js/cards/cffa.ts @@ -1,4 +1,4 @@ -import type { byte, Card, Restorable } from '../types'; +import type { byte, Card, Restorable, word } from '../types'; import { debug, toHex } from '../util'; import { rom as readOnlyRom } from '../roms/cards/cffa'; import { @@ -10,9 +10,11 @@ import createBlockDisk from '../formats/block'; import { BlockDisk, BlockFormat, - ENCODING_BLOCK, + Disk, + DRIVE_NUMBERS, MassStorage, MassStorageData, + MemoryBlockDisk, } from 'js/formats/types'; const rom = new Uint8Array(readOnlyRom); @@ -84,8 +86,14 @@ const IDENTITY = { SectorCountHigh: 57, }; +export interface CFFADiskState { + disk: Disk; + format: BlockFormat; + blocks: Uint8Array[]; +} + export interface CFFAState { - disks: Array; + disks: Array; } export default class CFFA @@ -113,16 +121,20 @@ export default class CFFA // Current Sector - private _curSector: Uint16Array | number[]; + private _curSector: Uint16Array = new Uint16Array(512); private _curWord = 0; // ATA Status registers + private _status = STATUS.BSY; private _interruptsEnabled = false; private _altStatus = 0; private _error = 0; - private _identity: number[][] = [[], []]; + private _identity: Uint16Array[] = [ + new Uint16Array(512), + new Uint16Array(512), + ]; // Disk data @@ -133,13 +145,7 @@ export default class CFFA null, ]; - private _sectors: Uint16Array[][] = [ - // Drive 1 - [], - // Drive 2 - [], - ]; - + private _sectors: Uint16Array[][] = [[], []]; private _name: string[] = []; private _metadata: Array = []; @@ -191,16 +197,23 @@ export default class CFFA // Dump sector as hex and ascii - private _dumpSector(sector: number) { - if (sector >= this._sectors[this._drive].length) { + private async _dumpSector(sector: number) { + const partition = this._partitions[this._drive]; + if (!partition) { + this._debug('dump sector volume not mounted'); + return; + } + const blockCount = await partition.blockCount(); + if (sector >= blockCount) { this._debug('dump sector out of range', sector); return; } + const readSector = await partition.read(sector); 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]; + const val = readSector[idx * 16 + jdx]; row.push(toHex(val, 4)); const low = val & 0x7f; const hi = (val >> 8) & 0x7f; @@ -211,12 +224,38 @@ export default class CFFA } } + private async readSector(sector: number): Promise { + const partition = this._partitions[this._drive]; + if (!partition) { + throw new Error('readSector no partition'); + } + if (this._sectors[this._drive][sector] === undefined) { + const readSector = await partition.read(sector); + this._sectors[this._drive][sector] = new Uint16Array( + readSector.buffer + ); + } + return this._sectors[this._drive][sector]; + } + + private handleAsync(fn: () => Promise) { + this._status = STATUS.BSY; + fn() + .then((status) => { + this._status = status; + }) + .catch((error) => { + this._status = STATUS.ERR; + console.error(error); + }); + } + // Card I/O access private _access(off: byte, val: byte) { const readMode = val === undefined; - let retVal; - let sector; + let retVal: byte | undefined = undefined; + let sector: word; if (readMode) { retVal = 0; @@ -269,10 +308,7 @@ export default class CFFA 0xa0; break; case LOC.ATAStatus: // 0x0F - retVal = - this._sectors[this._drive].length > 0 - ? STATUS.DRDY | STATUS.DSC - : 0; + retVal = this._status; this._debug('returning status', this._statusString(retVal)); break; default: @@ -358,7 +394,7 @@ export default class CFFA (this._cylinderH << 16) | (this._cylinder << 8) | this._sector; - this._dumpSector(sector); + void this._dumpSector(sector); switch (val) { case COMMANDS.ATAIdentify: @@ -367,29 +403,41 @@ export default class CFFA 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; + this.handleAsync(async () => { + const partition = this._partitions[this._drive]; + if (!partition) { + return STATUS.ERR; + } + this._debug( + 'ATA read sector', + toHex(this._cylinderH), + toHex(this._cylinder), + toHex(this._sector), + sector + ); + this._curSector = await this.readSector(sector); + return STATUS.DSC; + }); 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; + this.handleAsync(async () => { + const partition = this._partitions[this._drive]; + if (!partition) { + return STATUS.ERR; + } + this._debug( + 'ATA write sector', + toHex(this._cylinderH), + toHex(this._cylinder), + toHex(this._sector), + sector + ); + this._curSector = await this.readSector(sector); + this._curWord = 0; + return STATUS.DSC; + }); break; + default: debug('unknown command', toHex(val)); } @@ -417,40 +465,56 @@ export default class CFFA } } - getState() { - return { - disks: this._partitions.map((disk) => { - let result: BlockDisk | null = null; + async getState() { + const disks = []; + for (let diskNo = 0; diskNo < 2; diskNo++) { + const diskState = async (disk: BlockDisk | null) => { + let result: CFFADiskState | null = null; if (disk) { + const blocks = []; + const blockCount = await disk.blockCount(); + for (let idx = 0; idx < blockCount; idx++) { + blocks.push(await disk.read(idx)); + } result = { - blocks: disk.blocks.map( - (block) => new Uint8Array(block) - ), - encoding: ENCODING_BLOCK, + blocks, format: disk.format, - readOnly: disk.readOnly, - metadata: { ...disk.metadata }, + disk: { + readOnly: disk.readOnly, + metadata: { ...disk.metadata }, + }, }; } return result; - }), + }; + const disk = this._partitions[diskNo]; + disks[diskNo] = await diskState(disk); + } + return { + disks, }; } - setState(state: CFFAState) { - state.disks.forEach((disk, idx) => { - if (disk) { - this.setBlockVolume(idx + 1, disk); + async setState(state: CFFAState) { + for (const idx of DRIVE_NUMBERS) { + const diskState = state.disks[idx]; + if (diskState) { + const disk = new MemoryBlockDisk( + diskState.format, + diskState.disk.metadata, + diskState.disk.readOnly, + diskState.blocks + ); + await this.setBlockVolume(idx, disk); } else { - this.resetBlockVolume(idx + 1); + this.resetBlockVolume(idx); } - }); + } } resetBlockVolume(drive: number) { drive = drive - 1; - this._sectors[drive] = []; this._name[drive] = ''; this._metadata[drive] = null; @@ -464,18 +528,16 @@ export default class CFFA } } - setBlockVolume(drive: number, disk: BlockDisk) { + async setBlockVolume(drive: number, disk: BlockDisk): Promise { drive = drive - 1; + const partition = this._partitions[drive]; + if (!partition) { + return; + } - // 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 blockCount = await partition.blockCount(); + this._identity[drive][IDENTITY.SectorCountHigh] = blockCount & 0xffff; + this._identity[drive][IDENTITY.SectorCountLow] = blockCount >> 16; this._name[drive] = disk.metadata.name; this._partitions[drive] = disk; @@ -485,7 +547,6 @@ export default class CFFA } else { rom[SETTINGS.Max32MBPartitionsDev0] = 0x1; } - return true; } // Assign a raw disk image to a drive. Must be 2mg or raw PO image. @@ -493,13 +554,13 @@ export default class CFFA setBinary( drive: number, name: string, - ext: BlockFormat, + format: BlockFormat, rawData: ArrayBuffer - ) { + ): Promise { const volume = 254; const readOnly = false; - if (ext === '2mg') { + if (format === '2mg') { const headerData = read2MGHeader(rawData); const { bytes, offset } = headerData; this._metadata[drive - 1] = headerData; @@ -513,29 +574,33 @@ export default class CFFA volume, readOnly, }; - const disk = createBlockDisk(ext, options); + const disk = createBlockDisk(format, options); return this.setBlockVolume(drive, disk); } - getBinary(drive: number): MassStorageData | null { + async getBinary(drive: number): Promise { drive = drive - 1; const blockDisk = this._partitions[drive]; if (!blockDisk) { return null; } - const { blocks, readOnly } = blockDisk; + const { readOnly } = blockDisk; const { name } = blockDisk.metadata; let ext: '2mg' | 'po'; let data: ArrayBuffer; if (this._metadata[drive]) { ext = '2mg'; - data = create2MGFromBlockDisk(this._metadata[drive - 1], blockDisk); + data = await create2MGFromBlockDisk( + this._metadata[drive - 1], + blockDisk + ); } else { ext = 'po'; - const dataArray = new Uint8Array(blocks.length * 512); - for (let idx = 0; idx < blocks.length; idx++) { - dataArray.set(blocks[idx], idx * 512); + const blockCount = await blockDisk.blockCount(); + const dataArray = new Uint8Array(blockCount * 512); + for (let idx = 0; idx < blockCount; idx++) { + dataArray.set(await blockDisk.read(idx), idx * 512); } data = dataArray.buffer; } diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index 16b59fcc..ada6f936 100644 --- a/js/cards/disk2.ts +++ b/js/cards/disk2.ts @@ -663,12 +663,12 @@ export default class DiskII implements Card, MassStorage { return true; } - setBinary( + async setBinary( driveNo: DriveNumber, name: string, fmt: FloppyFormat, rawData: ArrayBuffer - ) { + ): Promise { const readOnly = false; const volume = 254; const options = { @@ -688,16 +688,15 @@ export default class DiskII implements Card, MassStorage { }, }; this.worker.postMessage(message); - - return true; + return; } else { const disk = createDisk(fmt, options); if (disk) { this.insertDisk(driveNo, disk); - return true; + return; } } - return false; + throw new Error('Unable to load disk'); } initWorker() { @@ -774,10 +773,10 @@ export default class DiskII implements Card, MassStorage { * an error will be thrown. Using `ext == 'nib'` will always return * an image. */ - getBinary( + async getBinary( driveNo: DriveNumber, ext?: Exclude - ): MassStorageData | null { + ): Promise { const curDisk = this.disks[driveNo]; if (!isNibbleDisk(curDisk)) { return null; diff --git a/js/cards/smartport.ts b/js/cards/smartport.ts index 7f5dc69c..e08005c3 100644 --- a/js/cards/smartport.ts +++ b/js/cards/smartport.ts @@ -4,12 +4,14 @@ import { Card, Restorable, byte, word, rom } from '../types'; import { MassStorage, BlockDisk, - ENCODING_BLOCK, BlockFormat, MassStorageData, DiskFormat, + Disk, + MemoryBlockDisk, + DRIVE_NUMBERS, } from '../formats/types'; -import { CPU6502, CpuState, flags } from '@whscullin/cpu6502'; +import { CPU6502, flags } from '@whscullin/cpu6502'; import { create2MGFromBlockDisk, HeaderData, @@ -22,8 +24,14 @@ import { readFileName } from 'js/formats/prodos/utils'; const ID = 'SMARTPORT.J.S'; +export interface SmartPortDiskState { + disk: Disk; + format: BlockFormat; + blocks: Uint8Array[]; +} + export interface SmartPortState { - disks: BlockDisk[]; + disks: Array; } export interface SmartPortOptions { @@ -115,6 +123,7 @@ const ADDRESS_LO = 0x44; const BLOCK_LO = 0x46; // const BLOCK_HI = 0x47; +const OK = 0x00; // const IO_ERROR = 0x27; const NO_DEVICE_CONNECTED = 0x28; const WRITE_PROTECTED = 0x2b; @@ -124,6 +133,7 @@ const DEVICE_OFFLINE = 0x2f; // const VOLUME_CONTROL_BLOCK_FULL = 0x55; // const BAD_BUFFER_ADDRESS = 0x56; // const DUPLICATE_VOLUME_ONLINE = 0x57; +const BUSY = 0x80; // Type: Device // $00: Memory Expansion Card (RAM disk) @@ -152,6 +162,9 @@ export default class SmartPort private busyTimeout: ReturnType[] = []; private ext: DiskFormat[] = []; private metadata: Array = []; + private statusByte = 0x80; + private xReg = 0x00; + private yReg = 0x00; constructor( private cpu: CPU6502, @@ -189,15 +202,17 @@ export default class SmartPort * dumpBlock */ - dumpBlock(driveNo: DriveNumber, block: number) { + async dumpBlock(driveNo: DriveNumber, blockNumber: number) { let result = ''; let b; let jdx; + const block = await this.disks[driveNo].read(blockNumber); + for (let idx = 0; idx < 32; idx++) { result += toHex(idx << 4, 4) + ': '; for (jdx = 0; jdx < 16; jdx++) { - b = this.disks[driveNo].blocks[block][idx * 16 + jdx]; + b = block[idx * 16 + jdx]; if (jdx === 8) { result += ' '; } @@ -205,7 +220,7 @@ export default class SmartPort } result += ' '; for (jdx = 0; jdx < 16; jdx++) { - b = this.disks[driveNo].blocks[block][idx * 16 + jdx] & 0x7f; + b = block[idx * 16 + jdx] & 0x7f; if (jdx === 8) { result += ' '; } @@ -224,17 +239,15 @@ export default class SmartPort * getDeviceInfo */ - getDeviceInfo(state: CpuState, driveNo: DriveNumber) { + async getDeviceInfo(driveNo: DriveNumber): Promise { if (this.disks[driveNo]) { - const blocks = this.disks[driveNo].blocks.length; - state.x = blocks & 0xff; - state.y = blocks >> 8; + const blocks = await this.disks[driveNo].blockCount(); + this.xReg = blocks & 0xff; + this.yReg = blocks >> 8; - state.a = 0; - state.s &= ~flags.C; + return OK; } else { - state.a = NO_DEVICE_CONNECTED; - state.s |= flags.C; + return NO_DEVICE_CONNECTED; } } @@ -242,106 +255,112 @@ export default class SmartPort * readBlock */ - readBlock( - state: CpuState, + async readBlock( driveNo: DriveNumber, - block: number, + blockNUmber: number, buffer: Address - ) { + ): Promise { this.debug(`read drive=${driveNo}`); this.debug(`read buffer=${buffer.toString()}`); - this.debug(`read block=$${toHex(block)}`); + this.debug(`read block=$${toHex(blockNUmber)}`); - if (!this.disks[driveNo]?.blocks.length) { + const blockCount = await this.disks[driveNo]?.blockCount(); + if (!blockCount) { debug('Drive', driveNo, 'is empty'); - state.a = DEVICE_OFFLINE; - state.s |= flags.C; - return; + return DEVICE_OFFLINE; } // debug('read', '\n' + dumpBlock(drive, block)); this.driveLight(driveNo); + const block = await this.disks[driveNo].read(blockNUmber); for (let idx = 0; idx < 512; idx++) { - buffer.writeByte(this.disks[driveNo].blocks[block][idx]); + buffer.writeByte(block[idx]); buffer = buffer.inc(1); } - state.a = 0; - state.s &= ~flags.C; + return OK; } /* * writeBlock */ - writeBlock( - state: CpuState, + async writeBlock( driveNo: DriveNumber, - block: number, + blockNUmber: number, buffer: Address - ) { + ): Promise { this.debug(`write drive=${driveNo}`); this.debug(`write buffer=${buffer.toString()}`); - this.debug(`write block=$${toHex(block)}`); + this.debug(`write block=$${toHex(blockNUmber)}`); - if (!this.disks[driveNo]?.blocks.length) { + if (!this.disks[driveNo]) { debug('Drive', driveNo, 'is empty'); - state.a = DEVICE_OFFLINE; - state.s |= flags.C; - return; + return DEVICE_OFFLINE; } if (this.disks[driveNo].readOnly) { debug('Drive', driveNo, 'is write protected'); - state.a = WRITE_PROTECTED; - state.s |= flags.C; - return; + return WRITE_PROTECTED; } // debug('write', '\n' + dumpBlock(drive, block)); this.driveLight(driveNo); + const block = new Uint8Array(512); for (let idx = 0; idx < 512; idx++) { - this.disks[driveNo].blocks[block][idx] = buffer.readByte(); + block[idx] = buffer.readByte(); buffer = buffer.inc(1); } - state.a = 0; - state.s &= ~flags.C; + await this.disks[driveNo].write(blockNUmber, block); + return 0; } /* * formatDevice */ - formatDevice(state: CpuState, driveNo: DriveNumber) { - if (!this.disks[driveNo]?.blocks.length) { + async formatDevice(driveNo: DriveNumber): Promise { + if (!this.disks[driveNo]) { debug('Drive', driveNo, 'is empty'); - state.a = DEVICE_OFFLINE; - state.s |= flags.C; - return; + return DEVICE_OFFLINE; } if (this.disks[driveNo].readOnly) { debug('Drive', driveNo, 'is write protected'); - state.a = WRITE_PROTECTED; - state.s |= flags.C; - return; + return WRITE_PROTECTED; } - for (let idx = 0; idx < this.disks[driveNo].blocks.length; idx++) { - this.disks[driveNo].blocks[idx] = new Uint8Array(); + const blockCount = await this.disks[driveNo].blockCount(); + + for (let idx = 0; idx < blockCount; idx++) { + const block = new Uint8Array(512); for (let jdx = 0; jdx < 512; jdx++) { - this.disks[driveNo].blocks[idx][jdx] = 0; + block[jdx] = 0; } + await this.disks[driveNo].write(idx, block); } - state.a = 0; - state.s &= flags.C; + return 0; + } + + handleAsync(fn: () => Promise): void { + this.statusByte = BUSY; + this.xReg = 0x00; + this.yReg = 0x00; + fn() + .then((statusByte) => { + this.statusByte = statusByte; + }) + .catch((error) => { + console.error(error); + this.statusByte = DEVICE_OFFLINE; + }); } private access(off: byte, val: byte) { - let result; + let result = 0x00; const readMode = val === undefined; switch (off & 0x8f) { @@ -356,6 +375,18 @@ export default class SmartPort } } break; + case 0x81: + result = this.statusByte; + break; + case 0x82: + result = this.xReg; + break; + case 0x83: + result = this.yReg; + break; + case 0x84: + result = this.statusByte ? 0x01 : 0x00; + break; } return result; @@ -371,10 +402,10 @@ export default class SmartPort read(_page: byte, off: byte) { const state = this.cpu.getState(); - let cmd; - let unit; - let buffer; - let block; + let cmd: number; + let unit: number; + let buffer: Address; + let block: number; const blockOff = this.rom[0xff]; const smartOff = blockOff + 3; @@ -399,19 +430,23 @@ export default class SmartPort switch (cmd) { case 0: // INFO - this.getDeviceInfo(state, drive); + this.handleAsync(() => this.getDeviceInfo(drive)); break; case 1: // READ - this.readBlock(state, drive, block, buffer); + this.handleAsync(() => + this.readBlock(drive, block, buffer) + ); break; case 2: // WRITE - this.writeBlock(state, drive, block, buffer); + this.handleAsync(() => + this.writeBlock(drive, block, buffer) + ); break; case 3: // FORMAT - this.formatDevice(state, drive); + this.handleAsync(() => this.formatDevice(drive)); break; } } else if (off === smartOff && this.cpu.getSync()) { @@ -449,67 +484,81 @@ export default class SmartPort case 0: switch (status) { case 0: - buffer.writeByte(2); // two devices - buffer.inc(1).writeByte(1 << 6); // no interrupts - buffer.inc(2).writeByte(0x2); // Other vendor - buffer.inc(3).writeByte(0x0); // Other vendor - buffer.inc(4).writeByte(0); // reserved - buffer.inc(5).writeByte(0); // reserved - buffer.inc(6).writeByte(0); // reserved - buffer.inc(7).writeByte(0); // reserved - state.x = 8; - state.y = 0; - state.a = 0; - state.s &= ~flags.C; + this.handleAsync(async () => { + buffer.writeByte(2); // two devices + buffer.inc(1).writeByte(1 << 6); // no interrupts + buffer.inc(2).writeByte(0x2); // Other vendor + buffer.inc(3).writeByte(0x0); // Other vendor + buffer.inc(4).writeByte(0); // reserved + buffer.inc(5).writeByte(0); // reserved + buffer.inc(6).writeByte(0); // reserved + buffer.inc(7).writeByte(0); // reserved + this.xReg = 8; + this.yReg = 0; + return 0; + }); break; } break; default: // Unit 1 switch (status) { case 0: - blocks = - this.disks[unit]?.blocks.length ?? 0; - buffer.writeByte(0xf0); // W/R Block device in drive - buffer.inc(1).writeByte(blocks & 0xff); // 1600 blocks - buffer - .inc(2) - .writeByte((blocks & 0xff00) >> 8); - buffer - .inc(3) - .writeByte((blocks & 0xff0000) >> 16); - state.x = 4; - state.y = 0; - state.a = 0; - state.s &= ~flags.C; + this.handleAsync(async () => { + blocks = + (await this.disks[ + unit + ]?.blockCount()) ?? 0; + buffer.writeByte(0xf0); // W/R Block device in drive + buffer.inc(1).writeByte(blocks & 0xff); // 1600 blocks + buffer + .inc(2) + .writeByte((blocks & 0xff00) >> 8); + buffer + .inc(3) + .writeByte( + (blocks & 0xff0000) >> 16 + ); + this.xReg = 4; + this.yReg = 0; + return 0; + }); break; case 3: - blocks = - this.disks[unit]?.blocks.length ?? 0; - buffer.writeByte(0xf0); // W/R Block device in drive - buffer.inc(1).writeByte(blocks & 0xff); // Blocks low byte - buffer - .inc(2) - .writeByte((blocks & 0xff00) >> 8); // Blocks middle byte - buffer - .inc(3) - .writeByte((blocks & 0xff0000) >> 16); // Blocks high byte - buffer.inc(4).writeByte(ID.length); // Vendor ID length - for (let idx = 0; idx < ID.length; idx++) { - // Vendor ID + this.handleAsync(async () => { + blocks = + (await this.disks[ + unit + ]?.blockCount()) ?? 0; + buffer.writeByte(0xf0); // W/R Block device in drive + buffer.inc(1).writeByte(blocks & 0xff); // Blocks low byte buffer - .inc(5 + idx) - .writeByte(ID.charCodeAt(idx)); - } - buffer - .inc(21) - .writeByte(DEVICE_TYPE_SCSI_HD); // Device Type - buffer.inc(22).writeByte(0x0); // Device Subtype - buffer.inc(23).writeWord(0x0101); // Version - state.x = 24; - state.y = 0; - state.a = 0; - state.s &= ~flags.C; - break; + .inc(2) + .writeByte((blocks & 0xff00) >> 8); // Blocks middle byte + buffer + .inc(3) + .writeByte( + (blocks & 0xff0000) >> 16 + ); // Blocks high byte + buffer.inc(4).writeByte(ID.length); // Vendor ID length + for ( + let idx = 0; + idx < ID.length; + idx++ + ) { + // Vendor ID + buffer + .inc(5 + idx) + .writeByte(ID.charCodeAt(idx)); + } + buffer + .inc(21) + .writeByte(DEVICE_TYPE_SCSI_HD); // Device Type + buffer.inc(22).writeByte(0x0); // Device Subtype + buffer.inc(23).writeWord(0x0101); // Version + this.xReg = 24; + this.yReg = 0; + return OK; + }); } break; } @@ -519,16 +568,20 @@ export default class SmartPort case 0x01: // READ BLOCK block = cmdListAddr.inc(4).readWord(); - this.readBlock(state, drive, block, buffer); + this.handleAsync(() => + this.readBlock(drive, block, buffer) + ); break; case 0x02: // WRITE BLOCK block = cmdListAddr.inc(4).readWord(); - this.writeBlock(state, drive, block, buffer); + this.handleAsync(() => + this.writeBlock(drive, block, buffer) + ); break; case 0x03: // FORMAT - this.formatDevice(state, drive); + this.handleAsync(() => this.formatDevice(drive)); break; case 0x04: // CONTROL @@ -560,40 +613,71 @@ export default class SmartPort // not writable } - getState() { - return { - disks: this.disks.map((disk) => { - const result: BlockDisk = { - blocks: disk.blocks.map((block) => new Uint8Array(block)), - encoding: ENCODING_BLOCK, - format: disk.format, - readOnly: disk.readOnly, - metadata: { ...disk.metadata }, - }; + async getState(): Promise { + const disks = []; + for (let diskNo = 0; diskNo < 2; diskNo++) { + const diskState = async (disk: BlockDisk | null) => { + let result: SmartPortDiskState | null = null; + if (disk) { + const blocks = []; + const blockCount = await disk.blockCount(); + for (let idx = 0; idx < blockCount; idx++) { + blocks.push(await disk.read(idx)); + } + result = { + blocks, + format: disk.format, + disk: { + readOnly: disk.readOnly, + metadata: { ...disk.metadata }, + }, + }; + } return result; - }), + }; + const disk = this.disks[diskNo]; + disks[diskNo] = await diskState(disk); + } + return { + disks, }; } - setState(state: SmartPortState) { - this.disks = state.disks.map((disk) => { - const result: BlockDisk = { - blocks: disk.blocks.map((block) => new Uint8Array(block)), - encoding: ENCODING_BLOCK, - format: disk.format, - readOnly: disk.readOnly, - metadata: { ...disk.metadata }, - }; - return result; - }); + async setState(state: SmartPortState) { + for (const idx of DRIVE_NUMBERS) { + const diskState = state.disks[idx]; + if (diskState) { + const disk = new MemoryBlockDisk( + diskState.format, + diskState.disk.metadata, + diskState.disk.readOnly, + diskState.blocks + ); + await this.setBlockDisk(idx, disk); + } else { + this.resetBlockDisk(idx); + } + } } - setBinary( + async setBlockDisk(driveNo: DriveNumber, disk: BlockDisk) { + this.disks[driveNo] = disk; + const volumeName = await this.getVolumeName(driveNo); + const name = volumeName || disk.metadata.name; + + this.callbacks?.label(driveNo, name); + } + + resetBlockDisk(driveNo: DriveNumber) { + delete this.disks[driveNo]; + } + + async setBinary( driveNo: DriveNumber, name: string, fmt: BlockFormat, rawData: ArrayBuffer - ) { + ): Promise { let volume = 254; let readOnly = false; if (fmt === '2mg') { @@ -616,14 +700,12 @@ export default class SmartPort this.ext[driveNo] = fmt; this.disks[driveNo] = createBlockDisk(fmt, options); - name = this.getVolumeName(driveNo) || name; + name = (await this.getVolumeName(driveNo)) || name; this.callbacks?.label(driveNo, name); - - return true; } - getBinary(drive: number): MassStorageData | null { + async getBinary(drive: DriveNumber): Promise { if (!this.disks[drive]) { return null; } @@ -633,12 +715,13 @@ export default class SmartPort const { name } = disk.metadata; let data: ArrayBuffer; if (ext === '2mg') { - data = create2MGFromBlockDisk(this.metadata[drive], disk); + data = await create2MGFromBlockDisk(this.metadata[drive], disk); } else { - const { blocks } = disk; - const byteArray = new Uint8Array(blocks.length * 512); - for (let idx = 0; idx < blocks.length; idx++) { - byteArray.set(blocks[idx], idx * 512); + const blockCount = await disk.blockCount(); + const byteArray = new Uint8Array(blockCount * 512); + for (let idx = 0; idx < blockCount; idx++) { + const block = await disk.read(idx); + byteArray.set(block, idx * 512); } data = byteArray.buffer; } @@ -650,17 +733,17 @@ export default class SmartPort }; } - getVolumeName(driveNo: number): string | null { - const buffer = this.disks[driveNo]?.blocks[VDH_BLOCK]?.buffer; - if (!buffer) { + async getVolumeName(driveNo: number): Promise { + const vdhBlock = await this.disks[driveNo]?.read(VDH_BLOCK); + if (!vdhBlock?.buffer) { return null; } - const block = new DataView(buffer); + const dataView = new DataView(vdhBlock.buffer); - const nameLength = block.getUint8(VDH_OFFSETS.NAME_LENGTH) & 0xf; - const caseBits = block.getUint8(VDH_OFFSETS.CASE_BITS); + const nameLength = dataView.getUint8(VDH_OFFSETS.NAME_LENGTH) & 0xf; + const caseBits = dataView.getUint8(VDH_OFFSETS.CASE_BITS); return readFileName( - block, + dataView, VDH_OFFSETS.VOLUME_NAME, nameLength, caseBits diff --git a/js/components/DownloadModal.tsx b/js/components/DownloadModal.tsx index 587873a9..6c17c59e 100644 --- a/js/components/DownloadModal.tsx +++ b/js/components/DownloadModal.tsx @@ -23,24 +23,27 @@ export const DownloadModal = ({ const doCancel = useCallback(() => onClose(true), [onClose]); useEffect(() => { - if (isOpen) { - const storageData = massStorage.getBinary(driveNo); - if (storageData) { - const { ext, data } = storageData; - const { name } = storageData.metadata; - if (data.byteLength) { - const blob = new Blob([data], { - type: 'application/octet-stream', - }); - const href = window.URL.createObjectURL(blob); - setHref(href); - setDownloadName(`${name}.${ext}`); - return; + const loadDisk = async () => { + if (isOpen) { + const storageData = await massStorage.getBinary(driveNo); + if (storageData) { + const { ext, data } = storageData; + const { name } = storageData.metadata; + if (data.byteLength) { + const blob = new Blob([data], { + type: 'application/octet-stream', + }); + const href = window.URL.createObjectURL(blob); + setHref(href); + setDownloadName(`${name}.${ext}`); + return; + } } + setHref(''); + setDownloadName(''); } - setHref(''); - setDownloadName(''); - } + }; + void loadDisk(); }, [isOpen, driveNo, massStorage]); return ( diff --git a/js/components/debugger/Disks.tsx b/js/components/debugger/Disks.tsx index 93581154..9c0826f4 100644 --- a/js/components/debugger/Disks.tsx +++ b/js/components/debugger/Disks.tsx @@ -1,5 +1,5 @@ import { h, Fragment } from 'preact'; -import { useMemo } from 'preact/hooks'; +import { useEffect, useMemo } from 'preact/hooks'; import cs from 'classnames'; import { Apple2 as Apple2Impl } from 'js/apple2'; import { @@ -54,7 +54,7 @@ const formatDate = (date: Date) => { * @returns true if is BlockDisk */ function isBlockDisk(disk: FloppyDisk | BlockDisk): disk is BlockDisk { - return !!(disk as BlockDisk).blocks; + return !!(disk as BlockDisk).blockCount; } /** @@ -77,16 +77,20 @@ interface FileListingProps { const FileListing = ({ depth, fileEntry, setFileData }: FileListingProps) => { const deleted = fileEntry.storageType === STORAGE_TYPES.DELETED; const doSetFileData = useCallback(() => { - const binary = fileEntry.getFileData(); - const text = fileEntry.getFileText(); - if (binary && text) { - setFileData({ - binary, - text, - fileName: fileEntry.name, - }); - } + const getData = async () => { + const binary = await fileEntry.getFileData(); + const text = await fileEntry.getFileText(); + if (binary && text) { + setFileData({ + binary, + text, + fileName: fileEntry.name, + }); + } + }; + void getData(); }, [fileEntry, setFileData]); + return ( { ); }; +interface ProDOSData { + prodos: ProDOSVolume; + freeCount: number; + vdh: VDH; +} + /** * Props for DiskInfo component */ @@ -286,97 +296,101 @@ interface DiskInfoProps { * @returns DiskInfo component */ const DiskInfo = ({ massStorage, driveNo, setFileData }: DiskInfoProps) => { - const disk = useMemo(() => { - const massStorageData = massStorage.getBinary(driveNo, 'po'); - if (massStorageData) { - const { data, readOnly, ext } = massStorageData; - const { name } = massStorageData.metadata; - let disk: BlockDisk | FloppyDisk | null = null; - if (ext === '2mg') { - disk = createDiskFrom2MG({ - name, - rawData: data, - readOnly, - volume: 254, - }); - } else if (data.byteLength < 800 * 1024) { - const doData = massStorage.getBinary(driveNo, 'do'); - if (doData) { - if (isMaybeDOS33(doData)) { - disk = createDiskFromDOS({ - name, - rawData: doData.data, - readOnly, - volume: 254, - }); + const [disk, setDisk] = useState(); + const [proDOSData, setProDOSData] = useState(); + useEffect(() => { + const load = async () => { + const massStorageData = await massStorage.getBinary(driveNo, 'po'); + if (massStorageData) { + const { data, readOnly, ext } = massStorageData; + const { name } = massStorageData.metadata; + let disk: BlockDisk | FloppyDisk | null = null; + if (ext === '2mg') { + disk = createDiskFrom2MG({ + name, + rawData: data, + readOnly, + volume: 254, + }); + } else if (data.byteLength < 800 * 1024) { + const doData = await massStorage.getBinary(driveNo, 'do'); + if (doData) { + if (isMaybeDOS33(doData)) { + disk = createDiskFromDOS({ + name, + rawData: doData.data, + readOnly, + volume: 254, + }); + } } } + if (!disk && isBlockDiskFormat(ext)) { + disk = createBlockDisk(ext, { + name, + rawData: data, + readOnly, + volume: 254, + }); + } + if (disk && isBlockDisk(disk)) { + const prodos = new ProDOSVolume(disk); + const vdh = await prodos.vdh(); + const bitMap = await prodos.bitMap(); + const freeBlocks = await bitMap.freeBlocks(); + const freeCount = freeBlocks.length; + setProDOSData({ freeCount, prodos, vdh }); + } + setDisk(disk); } - if (!disk && isBlockDiskFormat(ext)) { - disk = createBlockDisk(ext, { - name, - rawData: data, - readOnly, - volume: 254, - }); - } - return disk; - } - return null; + return null; + }; + void load(); }, [massStorage, driveNo]); if (disk) { try { - if (isBlockDisk(disk)) { - if (disk.blocks.length) { - const prodos = new ProDOSVolume(disk); - const { totalBlocks } = prodos.vdh(); - const freeCount = prodos.bitMap().freeBlocks().length; - const usedCount = totalBlocks - freeCount; - return ( -
- - - - - - - - - - - - - - - - - - - - - -
- Filename - TypeAux - Blocks - - Created - - Modified -
- Blocks Free: {freeCount} - Used: {usedCount} - Total: {totalBlocks} -
-
- ); - } + if (proDOSData) { + const { totalBlocks } = proDOSData.vdh; + const freeCount = proDOSData.freeCount; + const usedCount = totalBlocks - freeCount; + return ( +
+ + + + + + + + + + + + + + + + + + + + + +
+ Filename + TypeAuxBlocksCreated + Modified +
+ Blocks Free: {freeCount} + Used: {usedCount}Total: {totalBlocks}
+
+ ); } else if (isNibbleDisk(disk)) { const dos = new DOS33(disk); return ( diff --git a/js/components/util/files.ts b/js/components/util/files.ts index 7950682a..5b1db1f6 100644 --- a/js/components/util/files.ts +++ b/js/components/util/files.ts @@ -13,6 +13,7 @@ import { } from 'js/formats/types'; import { includes, word } from 'js/types'; import { initGamepad } from 'js/ui/gamepad'; +import { HttpBlockDisk } from './http_block_disk'; type ProgressCallback = (current: number, total: number) => void; @@ -53,14 +54,16 @@ export const loadLocalFile = ( ) => { return new Promise((resolve, reject) => { const fileReader = new FileReader(); - fileReader.onload = function () { + fileReader.onload = async function () { const result = this.result as ArrayBuffer; const { name, ext } = getNameAndExtension(file.name); if (includes(formats, ext)) { initGamepad(); - if (storage.setBinary(driveNo, name, ext, result)) { + try { + await storage.setBinary(driveNo, name, ext, result); resolve(true); - } else { + } catch (error) { + console.error(error); reject(`Unable to load ${name}`); } } else { @@ -192,9 +195,25 @@ export const loadHttpBlockFile = async ( if (!includes(BLOCK_FORMATS, ext)) { throw new Error(`Extension "${ext}" not recognized.`); } - const data = await loadHttpFile(url, signal, onProgress); - smartPort.setBinary(driveNo, name, ext, data); - initGamepad(); + const header = await fetch(url, { method: 'HEAD' }); + if (!header.ok) { + throw new Error(`Error loading: ${header.statusText}`); + } + const contentLength = parseInt( + header.headers.get('content-length') || '0', + 10 + ); + const hasByteRange = header.headers.get('accept-ranges') === 'byte'; + if (hasByteRange) { + await smartPort.setBlockDisk( + driveNo, + new HttpBlockDisk(name, contentLength, url) + ); + } else { + const data = await loadHttpFile(url, signal, onProgress); + await smartPort.setBinary(driveNo, name, ext, data); + initGamepad(); + } return true; }; @@ -224,7 +243,7 @@ export const loadHttpNibbleFile = async ( throw new Error(`Extension "${ext}" not recognized.`); } const data = await loadHttpFile(url, signal, onProgress); - disk2.setBinary(driveNo, name, ext, data); + await disk2.setBinary(driveNo, name, ext, data); initGamepad(); return loadHttpFile(url, signal, onProgress); }; @@ -236,9 +255,10 @@ export const loadHttpUnknownFile = async ( signal?: AbortSignal, onProgress?: ProgressCallback ) => { - const data = await loadHttpFile(url, signal, onProgress); - const { name, ext } = getNameAndExtension(url); - smartStorageBroker.setBinary(driveNo, name, ext, data); + // const data = await loadHttpFile(url, signal, onProgress); + // const { name, ext } = getNameAndExtension(url); + await smartStorageBroker.setUrl(driveNo, url, signal, onProgress); + // await smartStorageBroker.setBinary(driveNo, name, ext, data); }; export class SmartStorageBroker implements MassStorage { @@ -247,31 +267,60 @@ export class SmartStorageBroker implements MassStorage { private smartPort: SmartPort ) {} - setBinary( + async setUrl( + driveNo: DriveNumber, + url: string, + signal?: AbortSignal, + onProgress?: ProgressCallback + ) { + const { name, ext } = getNameAndExtension(url); + if (includes(DISK_FORMATS, ext)) { + const head = await fetch(url, { method: 'HEAD' }); + const contentLength = parseInt( + head.headers.get('content-length') || '0', + 10 + ); + if (contentLength >= 800 * 1024) { + if (includes(BLOCK_FORMATS, ext)) { + await this.smartPort.setBlockDisk( + driveNo, + new HttpBlockDisk(name, contentLength, url) + ); + } else { + throw new Error(`Unable to load "${name}"`); + } + initGamepad(); + return; + } + } + const data = await loadHttpFile(url, signal, onProgress); + await this.setBinary(driveNo, name, ext, data); + } + + async setBinary( driveNo: DriveNumber, name: string, ext: string, data: ArrayBuffer - ): boolean { + ): Promise { if (includes(DISK_FORMATS, ext)) { if (data.byteLength >= 800 * 1024) { if (includes(BLOCK_FORMATS, ext)) { - this.smartPort.setBinary(driveNo, name, ext, data); + await this.smartPort.setBinary(driveNo, name, ext, data); } else { throw new Error(`Unable to load "${name}"`); } } else if (includes(FLOPPY_FORMATS, ext)) { - this.disk2.setBinary(driveNo, name, ext, data); + await this.disk2.setBinary(driveNo, name, ext, data); } else { throw new Error(`Unable to load "${name}"`); } } else { throw new Error(`Extension "${ext}" not recognized.`); } - return true; } - getBinary(_drive: number) { + async getBinary(_drive: number) { return null; } } diff --git a/js/components/util/http_block_disk.ts b/js/components/util/http_block_disk.ts new file mode 100644 index 00000000..6628d5ec --- /dev/null +++ b/js/components/util/http_block_disk.ts @@ -0,0 +1,75 @@ +import { + BlockDisk, + BlockFormat, + DiskMetadata, + ENCODING_BLOCK, +} from 'js/formats/types'; + +export class HttpBlockDisk implements BlockDisk { + encoding: typeof ENCODING_BLOCK = ENCODING_BLOCK; + format: BlockFormat = 'po'; + metadata: DiskMetadata; + readOnly: boolean = false; + + blocks: Uint8Array[] = []; + fetchMap: Promise[] = []; + + constructor( + name: string, + private contentLength: number, + private url: string + ) { + this.metadata = { name }; + } + + async blockCount(): Promise { + return this.contentLength; + } + + async read(blockNumber: number): Promise { + const blockCount = 5; + if (!this.blocks[blockNumber]) { + const fetchBlock = blockNumber >> blockCount; + const fetchPromise = this.fetchMap[fetchBlock]; + if (fetchPromise !== undefined) { + const response = await fetchPromise; + if (!response.ok) { + throw new Error(`Error loading: ${response.statusText}`); + } + if (!response.body) { + throw new Error('Error loading: no body'); + } + } else { + const start = 512 * (fetchBlock << blockCount); + const end = start + (512 << blockCount); + this.fetchMap[fetchBlock] = fetch(this.url, { + headers: { range: `bytes=${start}-${end}` }, + }); + const response = await this.fetchMap[fetchBlock]; + if (!response.ok) { + throw new Error(`Error loading: ${response.statusText}`); + } + if (!response.body) { + throw new Error('Error loading: no body'); + } + const blob = await response.blob(); + const buffer = await new Response(blob).arrayBuffer(); + const startBlock = fetchBlock << blockCount; + const endBlock = startBlock + (1 << blockCount); + let startOffset = 0; + for (let idx = startBlock; idx < endBlock; idx++) { + const endOffset = startOffset + 512; + this.blocks[idx] = new Uint8Array( + buffer.slice(startOffset, endOffset) + ); + startOffset += 512; + } + } + } + return this.blocks[blockNumber]; + } + + async write(blockNumber: number, block: Uint8Array) { + this.blocks[blockNumber] = block; + } +} diff --git a/js/formats/2mg.ts b/js/formats/2mg.ts index be96b988..631eef15 100644 --- a/js/formats/2mg.ts +++ b/js/formats/2mg.ts @@ -287,21 +287,23 @@ export const create2MGFragments = ( * @returns 2MS */ -export const create2MGFromBlockDisk = ( +export const create2MGFromBlockDisk = async ( headerData: HeaderData | null, - { blocks }: BlockDisk -): ArrayBuffer => { + disk: BlockDisk +): Promise => { + const blockCount = await disk.blockCount(); const { prefix, suffix } = create2MGFragments(headerData, { - blocks: blocks.length, + blocks: blockCount, }); - const imageLength = prefix.length + blocks.length * 512 + suffix.length; + const imageLength = prefix.length + blockCount * 512 + suffix.length; const byteArray = new Uint8Array(imageLength); byteArray.set(prefix); - for (let idx = 0; idx < blocks.length; idx++) { - byteArray.set(blocks[idx], prefix.length + idx * 512); + for (let idx = 0; idx < blockCount; idx++) { + const block = await disk.read(idx); + byteArray.set(block, prefix.length + idx * 512); } - byteArray.set(suffix, prefix.length + blocks.length * 512); + byteArray.set(suffix, prefix.length + blockCount * 512); return byteArray.buffer; }; diff --git a/js/formats/block.ts b/js/formats/block.ts index b06d3182..75c6698a 100644 --- a/js/formats/block.ts +++ b/js/formats/block.ts @@ -1,4 +1,4 @@ -import { DiskOptions, BlockDisk, ENCODING_BLOCK, BlockFormat } from './types'; +import { DiskOptions, BlockDisk, BlockFormat, MemoryBlockDisk } from './types'; /** * Returns a `Disk` object for a block volume with block-ordered data. @@ -21,13 +21,12 @@ export default function createBlockDisk( offset += 0x200; } - const disk: BlockDisk = { - encoding: ENCODING_BLOCK, - format: fmt, - blocks, - metadata: { name }, + const disk: BlockDisk = new MemoryBlockDisk( + fmt, + { name }, readOnly, - }; + blocks + ); return disk; } diff --git a/js/formats/po.ts b/js/formats/po.ts index e10285de..cb139a33 100644 --- a/js/formats/po.ts +++ b/js/formats/po.ts @@ -7,7 +7,7 @@ import { ENCODING_NIBBLE, BlockDisk, FloppyDisk, - ENCODING_BLOCK, + MemoryBlockDisk, } from './types'; import { BLOCK_SIZE } from './prodos/constants'; @@ -22,22 +22,22 @@ export default function createDiskFromProDOS( const { data, name, side, rawData, volume, readOnly } = options; let disk: BlockDisk | NibbleDisk; if (rawData && rawData.byteLength > 140 * 1025) { - disk = { - format: 'po', - encoding: ENCODING_BLOCK, - metadata: { name, side }, - readOnly: readOnly || false, - blocks: [], - } as BlockDisk; + const blocks: Uint8Array[] = []; for ( let offset = 0; offset < rawData.byteLength; offset += BLOCK_SIZE ) { - disk.blocks.push( + blocks.push( new Uint8Array(rawData.slice(offset, offset + BLOCK_SIZE)) ); } + disk = new MemoryBlockDisk( + 'po', + { name, side }, + readOnly || false, + blocks + ); } else { disk = { format: 'po', diff --git a/js/formats/prodos/base_file.ts b/js/formats/prodos/base_file.ts index ac468577..4c5ffcd4 100644 --- a/js/formats/prodos/base_file.ts +++ b/js/formats/prodos/base_file.ts @@ -9,6 +9,6 @@ export interface ProDOSFileData { export abstract class ProDOSFile { constructor(public volume: ProDOSVolume) {} - abstract read(): Uint8Array; - abstract write(data: Uint8Array): void; + abstract read(): Promise; + abstract write(data: Uint8Array): Promise; } diff --git a/js/formats/prodos/bit_map.ts b/js/formats/prodos/bit_map.ts index f640e2dd..4ddc18cb 100644 --- a/js/formats/prodos/bit_map.ts +++ b/js/formats/prodos/bit_map.ts @@ -1,25 +1,21 @@ import { word } from 'js/types'; import { ProDOSVolume } from '.'; -import type { VDH } from './vdh'; const BLOCK_ENTRIES = 4096; export class BitMap { - private vdh: VDH; - private blocks: Uint8Array[]; + constructor(private volume: ProDOSVolume) {} - constructor(volume: ProDOSVolume) { - this.vdh = volume.vdh(); - this.blocks = volume.blocks(); - } - - freeBlocks() { + async freeBlocks() { + const vdh = await this.volume.vdh(); const free: word[] = []; let blockOffset = 0; let byteOffset = 0; let bitOffset = 0; - let bitMapBlock = this.blocks[this.vdh.bitMapPointer + blockOffset]; - for (let idx = 0; idx < this.vdh.totalBlocks; idx++) { + let bitMapBlock = await this.volume + .disk() + .read(vdh.bitMapPointer + blockOffset); + for (let idx = 0; idx < vdh.totalBlocks; idx++) { const currentByte = bitMapBlock[byteOffset]; const mask = 1 << bitOffset; if (currentByte & mask) { @@ -32,19 +28,22 @@ export class BitMap { if (byteOffset > BLOCK_ENTRIES >> 3) { byteOffset = 0; blockOffset += 1; - bitMapBlock = - this.blocks[this.vdh.bitMapPointer + blockOffset]; + bitMapBlock = await this.volume + .disk() + .read(vdh.bitMapPointer + blockOffset); } } } return free; } - allocBlock() { - for (let idx = 0; idx < this.vdh.totalBlocks; idx++) { + async allocBlock() { + const vdh = await this.volume.vdh(); + for (let idx = 0; idx < vdh.totalBlocks; idx++) { const blockOffset = Math.floor(idx / BLOCK_ENTRIES); - const bitMapBlock = - this.blocks[this.vdh.bitMapPointer + blockOffset]; + const bitMapBlock = await this.volume + .disk() + .read(vdh.bitMapPointer + blockOffset); const byteOffset = (idx - blockOffset * BLOCK_ENTRIES) >> 8; const bits = bitMapBlock[byteOffset]; if (bits !== 0xff) { @@ -61,15 +60,19 @@ export class BitMap { throw new Error('Disk full'); } - freeBlock(block: word) { - if (block >= this.vdh.totalBlocks) { + async freeBlock(block: word) { + const vdh = await this.volume.vdh(); + + if (block >= vdh.totalBlocks) { throw new Error('Block out of range'); } const blockOffset = Math.floor(block / BLOCK_ENTRIES); const byteOffset = (block - blockOffset * BLOCK_ENTRIES) >> 8; const bitOffset = block & 0x7; - const bitMapBlock = this.blocks[this.vdh.bitMapPointer + blockOffset]; + const bitMapBlock = await this.volume + .disk() + .read(vdh.bitMapPointer + blockOffset); bitMapBlock[byteOffset] &= 0xff ^ (0x01 << bitOffset); } diff --git a/js/formats/prodos/directory.ts b/js/formats/prodos/directory.ts index 02c492c5..93a74cbc 100644 --- a/js/formats/prodos/directory.ts +++ b/js/formats/prodos/directory.ts @@ -8,7 +8,6 @@ import { FileEntry, readEntries, writeEntries } from './file_entry'; import { STORAGE_TYPES, ACCESS_TYPES } from './constants'; import { byte, word } from 'js/types'; import { ProDOSVolume } from '.'; -import { VDH } from './vdh'; export const DIRECTORY_OFFSETS = { PREV: 0x00, @@ -31,9 +30,6 @@ export const DIRECTORY_OFFSETS = { } as const; export class Directory { - blocks: Uint8Array[]; - vdh: VDH; - prev: word = 0; next: word = 0; storageType: byte = STORAGE_TYPES.DIRECTORY; @@ -51,18 +47,19 @@ export class Directory { constructor( private volume: ProDOSVolume, private fileEntry: FileEntry - ) { - this.blocks = this.volume.blocks(); - this.vdh = this.volume.vdh(); - this.read(); + ) {} + + async init() { + await this.read(); } - read(fileEntry?: FileEntry) { + async read(fileEntry?: FileEntry) { this.fileEntry = fileEntry ?? this.fileEntry; - const block = new DataView( - this.blocks[this.fileEntry.keyPointer].buffer - ); + const readBlock = await this.volume + .disk() + .read(this.fileEntry.keyPointer); + const block = new DataView(readBlock.buffer); this.prev = block.getUint16(DIRECTORY_OFFSETS.PREV, true); this.next = block.getUint16(DIRECTORY_OFFSETS.NEXT, true); @@ -92,13 +89,14 @@ export class Directory { DIRECTORY_OFFSETS.PARENT_ENTRY_LENGTH ); - this.entries = readEntries(this.volume, block, this); + this.entries = await readEntries(this.volume, block, this); } - write() { - const block = new DataView( - this.blocks[this.fileEntry.keyPointer].buffer - ); + async write() { + const readBlock = await this.volume + .disk() + .read(this.fileEntry.keyPointer); + const block = new DataView(readBlock.buffer); const nameLength = this.name.length & 0x0f; block.setUint8( @@ -133,6 +131,7 @@ export class Directory { this.parentEntryLength ); - writeEntries(this.volume, block, this.vdh); + const vdh = await this.volume.vdh(); + await writeEntries(this.volume, block, vdh); } } diff --git a/js/formats/prodos/file_entry.ts b/js/formats/prodos/file_entry.ts index 3a3e19bf..801b62d0 100644 --- a/js/formats/prodos/file_entry.ts +++ b/js/formats/prodos/file_entry.ts @@ -158,7 +158,7 @@ export class FileEntry { ); } - getFileData() { + async getFileData() { let file: ProDOSFile | null = null; switch (this.storageType) { @@ -178,8 +178,8 @@ export class FileEntry { } } - getFileText() { - const data = this.getFileData(); + async getFileText() { + const data = await this.getFileData(); let result: string | null = null; let address = 0; @@ -218,12 +218,11 @@ export class FileEntry { } } -export function readEntries( +export async function readEntries( volume: ProDOSVolume, block: DataView, header: VDH | Directory ) { - const blocks = volume.blocks(); const entries = []; let offset = header.entryLength + 0x4; let count = 2; @@ -239,7 +238,8 @@ export function readEntries( offset += header.entryLength; count++; if (count > header.entriesPerBlock) { - block = new DataView(blocks[next].buffer); + const readBlock = await volume.disk().read(next); + block = new DataView(readBlock.buffer); next = block.getUint16(0x02, true); offset = 0x4; count = 1; @@ -249,13 +249,12 @@ export function readEntries( return entries; } -export function writeEntries( +export async function writeEntries( volume: ProDOSVolume, block: DataView, header: VDH | Directory ) { - const blocks = volume.blocks(); - const bitMap = volume.bitMap(); + const bitMap = await volume.bitMap(); let offset = header.entryLength + 0x4; let count = 2; let next = header.next; @@ -268,9 +267,10 @@ export function writeEntries( if (count >= header.entriesPerBlock) { const prev = next; if (!next) { - next = bitMap.allocBlock(); + next = await bitMap.allocBlock(); } - block = new DataView(blocks[next].buffer); + const readBlock = await volume.disk().read(next); + block = new DataView(readBlock.buffer); block.setUint16(0x00, prev, true); next = block.getUint16(0x02, true); offset = 0x4; @@ -280,8 +280,9 @@ export function writeEntries( next = block.getUint16(0x02, true); block.setUint16(0x02, 0, true); while (next) { - block = new DataView(blocks[next].buffer); - bitMap.freeBlock(next); + const readBlock = await volume.disk().read(next); + block = new DataView(readBlock.buffer); + await bitMap.freeBlock(next); next = block.getUint16(0x02, true); } } diff --git a/js/formats/prodos/prodos_volume.ts b/js/formats/prodos/prodos_volume.ts index f06911a4..b3666021 100644 --- a/js/formats/prodos/prodos_volume.ts +++ b/js/formats/prodos/prodos_volume.ts @@ -3,8 +3,8 @@ import { BitMap } from './bit_map'; import { BlockDisk } from '../types'; export class ProDOSVolume { - _vdh: VDH; - _bitMap: BitMap; + private _vdh: VDH; + private _bitMap: BitMap; constructor(private _disk: BlockDisk) {} @@ -12,19 +12,15 @@ export class ProDOSVolume { return this._disk; } - blocks() { - return this._disk.blocks; - } - - vdh() { + async vdh() { if (!this._vdh) { this._vdh = new VDH(this); - this._vdh.read(); + await this._vdh.read(); } return this._vdh; } - bitMap() { + async bitMap() { if (!this._bitMap) { this._bitMap = new BitMap(this); } diff --git a/js/formats/prodos/sapling_file.ts b/js/formats/prodos/sapling_file.ts index aec393cd..94c8d21d 100644 --- a/js/formats/prodos/sapling_file.ts +++ b/js/formats/prodos/sapling_file.ts @@ -1,24 +1,20 @@ import { ProDOSVolume } from '.'; -import type { BitMap } from './bit_map'; import { BLOCK_SIZE, STORAGE_TYPES } from './constants'; import { FileEntry } from './file_entry'; import { ProDOSFile } from './base_file'; export class SaplingFile extends ProDOSFile { - blocks: Uint8Array[]; - bitMap: BitMap; - constructor( volume: ProDOSVolume, private fileEntry: FileEntry ) { super(volume); - this.blocks = this.volume.blocks(); - this.bitMap = this.volume.bitMap(); } - getBlockPointers() { - const saplingBlock = this.blocks[this.fileEntry.keyPointer]; + async getBlockPointers() { + const saplingBlock = await this.volume + .disk() + .read(this.fileEntry.keyPointer); const seedlingPointers = new DataView(saplingBlock.buffer); const pointers = [this.fileEntry.keyPointer]; @@ -34,8 +30,10 @@ export class SaplingFile extends ProDOSFile { } // TODO(whscullin): Why did I not use getBlockPointers for these... - read() { - const saplingBlock = this.blocks[this.fileEntry.keyPointer]; + async read() { + const saplingBlock = await this.volume + .disk() + .read(this.fileEntry.keyPointer); const seedlingPointers = new DataView(saplingBlock.buffer); let remainingLength = this.fileEntry.eof; @@ -47,7 +45,9 @@ export class SaplingFile extends ProDOSFile { seedlingPointers.getUint8(idx) | (seedlingPointers.getUint8(0x100 + idx) << 8); if (seedlingPointer) { - const seedlingBlock = this.blocks[seedlingPointer]; + const seedlingBlock = await this.volume + .disk() + .read(seedlingPointer); const bytes = seedlingBlock.slice( 0, Math.min(BLOCK_SIZE, remainingLength) @@ -62,11 +62,14 @@ export class SaplingFile extends ProDOSFile { return data; } - write(data: Uint8Array) { + async write(data: Uint8Array) { + const bitMap = await this.volume.bitMap(); this.fileEntry.storageType = STORAGE_TYPES.SAPLING; - this.fileEntry.keyPointer = this.bitMap.allocBlock(); + this.fileEntry.keyPointer = await bitMap.allocBlock(); this.fileEntry.eof = data.byteLength; - const saplingBlock = this.blocks[this.fileEntry.keyPointer]; + const saplingBlock = await this.volume + .disk() + .read(this.fileEntry.keyPointer); const seedlingPointers = new DataView(saplingBlock.buffer); let remainingLength = data.byteLength; @@ -74,10 +77,12 @@ export class SaplingFile extends ProDOSFile { let idx = 0; while (remainingLength > 0) { - const seedlingPointer = this.bitMap.allocBlock(); + const seedlingPointer = await bitMap.allocBlock(); seedlingPointers.setUint8(idx, seedlingPointer & 0xff); seedlingPointers.setUint8(0x100 + idx, seedlingPointer >> 8); - const seedlingBlock = this.blocks[seedlingPointer]; + const seedlingBlock = await this.volume + .disk() + .read(seedlingPointer); seedlingBlock.set( data.slice(offset, Math.min(BLOCK_SIZE, remainingLength)) ); @@ -88,10 +93,11 @@ export class SaplingFile extends ProDOSFile { this.fileEntry.write(); } - delete() { - const pointers = this.getBlockPointers(); + async delete() { + const bitMap = await this.volume.bitMap(); + const pointers = await this.getBlockPointers(); for (let idx = 0; idx < pointers.length; idx++) { - this.bitMap.freeBlock(pointers[idx]); + await bitMap.freeBlock(pointers[idx]); } } } diff --git a/js/formats/prodos/seedling_file.ts b/js/formats/prodos/seedling_file.ts index bd95151c..1d1e3aeb 100644 --- a/js/formats/prodos/seedling_file.ts +++ b/js/formats/prodos/seedling_file.ts @@ -1,20 +1,14 @@ import type { ProDOSVolume } from '.'; import { ProDOSFile } from './base_file'; -import { BitMap } from './bit_map'; import { STORAGE_TYPES } from './constants'; import { FileEntry } from './file_entry'; export class SeedlingFile extends ProDOSFile { - blocks: Uint8Array[]; - bitMap: BitMap; - constructor( volume: ProDOSVolume, private fileEntry: FileEntry ) { super(volume); - this.blocks = volume.blocks(); - this.bitMap = volume.bitMap(); } getBlockPointers() { @@ -22,29 +16,35 @@ export class SeedlingFile extends ProDOSFile { return pointers; } - read() { - const seedlingBlock = this.blocks[this.fileEntry.keyPointer]; + async read() { + const seedlingBlock = await this.volume + .disk() + .read(this.fileEntry.keyPointer); const data = new Uint8Array(this.fileEntry.eof); data.set(seedlingBlock.slice(0, this.fileEntry.eof)); return data; } - write(data: Uint8Array) { + async write(data: Uint8Array) { + const bitMap = await this.volume.bitMap(); if (this.fileEntry.keyPointer) { - this.delete(); + await this.delete(); } this.fileEntry.storageType = STORAGE_TYPES.SEEDLING; - this.fileEntry.keyPointer = this.bitMap.allocBlock(); + this.fileEntry.keyPointer = await bitMap.allocBlock(); this.fileEntry.eof = data.byteLength; - const seedlingBlock = this.blocks[this.fileEntry.keyPointer]; + const seedlingBlock = await this.volume + .disk() + .read(this.fileEntry.keyPointer); seedlingBlock.set(data); this.fileEntry.write(); } - delete() { + async delete() { + const bitMap = await this.volume.bitMap(); const pointers = this.getBlockPointers(); for (let idx = 0; idx < pointers.length; idx++) { - this.bitMap.freeBlock(pointers[idx]); + await bitMap.freeBlock(pointers[idx]); } } } diff --git a/js/formats/prodos/tree_file.ts b/js/formats/prodos/tree_file.ts index 2444ddf0..5ff8c87a 100644 --- a/js/formats/prodos/tree_file.ts +++ b/js/formats/prodos/tree_file.ts @@ -1,24 +1,20 @@ import type { ProDOSVolume } from '.'; import { ProDOSFile } from './base_file'; -import { BitMap } from './bit_map'; import { BLOCK_SIZE, STORAGE_TYPES } from './constants'; import type { FileEntry } from './file_entry'; export class TreeFile extends ProDOSFile { - private bitMap: BitMap; - private blocks: Uint8Array[]; - constructor( volume: ProDOSVolume, private fileEntry: FileEntry ) { super(volume); - this.blocks = volume.blocks(); - this.bitMap = volume.bitMap(); } - getBlockPointers() { - const treeBlock = this.blocks[this.fileEntry.keyPointer]; + async getBlockPointers() { + const treeBlock = await this.volume + .disk() + .read(this.fileEntry.keyPointer); const saplingPointers = new DataView(treeBlock.buffer); const pointers = []; for (let idx = 0; idx < 256; idx++) { @@ -27,9 +23,8 @@ export class TreeFile extends ProDOSFile { (saplingPointers.getUint8(0x100 + idx) << 8); if (saplingPointer) { pointers.push(saplingPointer); - const seedlingPointers = new DataView( - this.blocks[saplingPointer].buffer - ); + const readBlock = await this.volume.disk().read(saplingPointer); + const seedlingPointers = new DataView(readBlock.buffer); for (let jdx = 0; jdx < 256; jdx++) { const seedlingPointer = seedlingPointers.getUint8(idx) | @@ -44,8 +39,10 @@ export class TreeFile extends ProDOSFile { } // TODO(whscullin): Why did I not use getBlockPointers for these... - read() { - const treeBlock = this.blocks[this.fileEntry.keyPointer]; + async read() { + const treeBlock = await this.volume + .disk() + .read(this.fileEntry.keyPointer); const saplingPointers = new DataView(treeBlock.buffer); let remainingLength = this.fileEntry.eof; const data = new Uint8Array(remainingLength); @@ -58,7 +55,9 @@ export class TreeFile extends ProDOSFile { (saplingPointers.getUint8(0x100 + idx) << 8); let jdx = 0; if (saplingPointer) { - const saplingBlock = this.blocks[saplingPointer]; + const saplingBlock = await this.volume + .disk() + .read(saplingPointer); const seedlingPointers = new DataView(saplingBlock.buffer); while (jdx < 256 && remainingLength > 0) { @@ -66,7 +65,9 @@ export class TreeFile extends ProDOSFile { seedlingPointers.getUint8(idx) | (seedlingPointers.getUint8(0x100 + idx) << 8); if (seedlingPointer) { - const seedlingBlock = this.blocks[seedlingPointer]; + const seedlingBlock = await this.volume + .disk() + .read(seedlingPointer); const bytes = seedlingBlock.slice( Math.min(BLOCK_SIZE, remainingLength) ); @@ -86,12 +87,15 @@ export class TreeFile extends ProDOSFile { return data; } - write(data: Uint8Array) { + async write(data: Uint8Array) { + const bitMap = await this.volume.bitMap(); this.fileEntry.storageType = STORAGE_TYPES.TREE; - this.fileEntry.keyPointer = this.bitMap.allocBlock(); + this.fileEntry.keyPointer = await bitMap.allocBlock(); this.fileEntry.eof = data.byteLength; - const treeBlock = this.blocks[this.fileEntry.keyPointer]; + const treeBlock = await this.volume + .disk() + .read(this.fileEntry.keyPointer); const saplingPointers = new DataView(treeBlock.buffer); let remainingLength = this.fileEntry.eof; @@ -99,8 +103,8 @@ export class TreeFile extends ProDOSFile { let idx = 0; while (remainingLength > 0) { - const saplingPointer = this.bitMap.allocBlock(); - const saplingBlock = this.blocks[saplingPointer]; + const saplingPointer = await bitMap.allocBlock(); + const saplingBlock = await this.volume.disk().read(saplingPointer); saplingPointers.setUint8(idx, saplingPointer & 0xff); saplingPointers.setUint8(0x100 + idx, saplingPointer >> 8); const seedlingPointers = new DataView(saplingBlock.buffer); @@ -108,10 +112,12 @@ export class TreeFile extends ProDOSFile { let jdx = 0; while (jdx < 256 && remainingLength > 0) { - const seedlingPointer = this.bitMap.allocBlock(); + const seedlingPointer = await bitMap.allocBlock(); seedlingPointers.setUint8(idx, seedlingPointer & 0xff); seedlingPointers.setUint8(0x100 + idx, seedlingPointer >> 8); - const seedlingBlock = this.blocks[seedlingPointer]; + const seedlingBlock = await this.volume + .disk() + .read(seedlingPointer); seedlingBlock.set( data.slice(offset, Math.min(BLOCK_SIZE, remainingLength)) ); @@ -124,10 +130,11 @@ export class TreeFile extends ProDOSFile { this.fileEntry.write(); } - delete() { - const pointers = this.getBlockPointers(); + async delete() { + const bitMap = await this.volume.bitMap(); + const pointers = await this.getBlockPointers(); for (let idx = 0; idx < pointers.length; idx++) { - this.bitMap.freeBlock(pointers[idx]); + await bitMap.freeBlock(pointers[idx]); } } } diff --git a/js/formats/prodos/utils.ts b/js/formats/prodos/utils.ts index 25d8fa13..c0ef1202 100644 --- a/js/formats/prodos/utils.ts +++ b/js/formats/prodos/utils.ts @@ -106,8 +106,8 @@ export function dumpDirectory( return str; } -export function dump(volume: ProDOSVolume) { - const vdh = volume.vdh(); +export async function dump(volume: ProDOSVolume) { + const vdh = await volume.vdh(); let str = vdh.name; for (let idx = 0; idx < vdh.entries.length; idx++) { const fileEntry = vdh.entries[idx]; diff --git a/js/formats/prodos/vdh.ts b/js/formats/prodos/vdh.ts index 3ba33975..2664113b 100644 --- a/js/formats/prodos/vdh.ts +++ b/js/formats/prodos/vdh.ts @@ -30,8 +30,6 @@ export const VDH_OFFSETS = { } as const; export class VDH { - private blocks: Uint8Array[]; - prev: word = 0; next: word = 0; storageType: byte = STORAGE_TYPES.VDH_HEADER; @@ -45,12 +43,11 @@ export class VDH { totalBlocks: word = 0; entries: FileEntry[] = []; - constructor(private volume: ProDOSVolume) { - this.blocks = this.volume.blocks(); - } + constructor(private volume: ProDOSVolume) {} - read() { - const block = new DataView(this.blocks[VDH_BLOCK].buffer); + async read() { + const vdhBlock = await this.volume.disk().read(VDH_BLOCK); + const block = new DataView(vdhBlock.buffer); this.next = block.getUint16(VDH_OFFSETS.NEXT, true); this.storageType = block.getUint8(VDH_OFFSETS.STORAGE_TYPE) >> 4; @@ -72,11 +69,12 @@ export class VDH { this.bitMapPointer = block.getUint16(VDH_OFFSETS.BIT_MAP_POINTER, true); this.totalBlocks = block.getUint16(VDH_OFFSETS.TOTAL_BLOCKS, true); - this.entries = readEntries(this.volume, block, this); + this.entries = await readEntries(this.volume, block, this); } - write() { - const block = new DataView(this.blocks[VDH_BLOCK].buffer); + async write() { + const vdhBlock = await this.volume.disk().read(VDH_BLOCK); + const block = new DataView(vdhBlock.buffer); const nameLength = this.name.length & 0x0f; block.setUint8( @@ -101,6 +99,6 @@ export class VDH { block.setUint16(VDH_OFFSETS.BIT_MAP_POINTER, this.bitMapPointer, true); block.setUint16(VDH_OFFSETS.TOTAL_BLOCKS, this.totalBlocks, true); - writeEntries(this.volume, block, this); + await writeEntries(this.volume, block, this); } } diff --git a/js/formats/types.ts b/js/formats/types.ts index 69c60c7c..d52a2bd6 100644 --- a/js/formats/types.ts +++ b/js/formats/types.ts @@ -1,3 +1,4 @@ +import { BlockDisk } from 'js/components/BlockDisk'; import type { byte, memory, MemberOf, word } from '../types'; import type { GamepadConfiguration } from '../ui/types'; import { InfoChunk } from './woz'; @@ -98,7 +99,33 @@ export interface WozDisk extends FloppyDisk { export interface BlockDisk extends Disk { encoding: typeof ENCODING_BLOCK; format: BlockFormat; - blocks: Uint8Array[]; + + blockCount(): Promise; + read(block: number): Promise; + write(block: number, data: Uint8Array): Promise; +} + +export class MemoryBlockDisk implements BlockDisk { + encoding: typeof ENCODING_BLOCK = ENCODING_BLOCK; + + constructor( + readonly format: BlockFormat, + readonly metadata: DiskMetadata, + readonly readOnly = false, + private blocks: Uint8Array[] + ) {} + + async blockCount(): Promise { + return this.blocks.length; + } + + async read(block: number): Promise { + return this.blocks[block]; + } + + async write(block: number, data: Uint8Array): Promise { + this.blocks[block] = data; + } } /** @@ -278,6 +305,11 @@ export interface MassStorageData { * Block device common interface */ export interface MassStorage { - setBinary(drive: number, name: string, ext: T, data: ArrayBuffer): boolean; - getBinary(drive: number, ext?: T): MassStorageData | null; + setBinary( + drive: number, + name: string, + ext: T, + data: ArrayBuffer + ): Promise; + getBinary(drive: number, ext?: T): Promise; } diff --git a/js/roms/cards/smartport.ts b/js/roms/cards/smartport.ts index 712d9dba..5d64f5a7 100644 --- a/js/roms/cards/smartport.ts +++ b/js/roms/cards/smartport.ts @@ -8,20 +8,36 @@ import { ReadonlyUint8Array } from '../../types'; */ export const rom = new Uint8Array([ - 0xa2,0x20,0xa2,0x00,0xa2,0x03,0xa2,0x00, 0x20,0x58,0xff,0xba,0xbd,0x00,0x01,0x8d, - 0xf8,0x07,0x0a,0x0a,0x0a,0x0a,0xa8,0xb9, 0x80,0xc0,0x4a,0xb0,0x11,0xa5,0x00,0xd0, - 0x0a,0xa5,0x01,0xcd,0xf8,0x07,0xd0,0x03, 0x4c,0xba,0xfa,0x4c,0x00,0xe0,0xa2,0x01, - 0x86,0x42,0xca,0x86,0x46,0x86,0x47,0x86, 0x44,0xa2,0x08,0x86,0x45,0xad,0xf8,0x07, - 0x48,0x48,0xa9,0x47,0x48,0xb8,0x50,0x0b, 0x68,0x0a,0x0a,0x0a,0x0a,0xaa,0x4c,0x01, - 0x08,0x00,0x00,0x60,0x00,0x00,0x60,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, - 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0x00,0xd7,0x53, + 0xa2,0x20,0xa2,0x00,0xa2,0x03,0xa2,0x00, + 0x20,0x58,0xff,0xba,0xbd,0x00,0x01,0x8d, + 0xf8,0x07,0x0a,0x0a,0x0a,0x0a,0xa8,0xb9, + 0x80,0xc0,0x4a,0xb0,0x11,0xa5,0x00,0xd0, + 0x0a,0xa5,0x01,0xcd,0xf8,0x07,0xd0,0x03, + 0x4c,0xba,0xfa,0x4c,0x00,0xe0,0xa2,0x01, + 0x86,0x42,0xca,0x86,0x46,0x86,0x47,0x86, + 0x44,0xa2,0x08,0x86,0x45,0xad,0xf8,0x07, + 0x48,0x48,0xa9,0x47,0x48,0xb8,0x50,0x0b, + 0x68,0x0a,0x0a,0x0a,0x0a,0xaa,0x4c,0x01, + 0x08,0x00,0x00,0x4c,0x59,0xc7,0x4c,0x59, + 0xc7,0xa5,0x00,0x48,0xa9,0x60,0x85,0x00, + 0x20,0x00,0x00,0xba,0xbd,0x00,0x01,0x0a, + 0x0a,0x0a,0x0a,0xaa,0x68,0x85,0x00,0xbd, + 0x81,0xc0,0x30,0xfb,0x48,0xbd,0x82,0xc0, + 0x48,0xbd,0x83,0xc0,0x48,0xbd,0x84,0xc0, + 0x6a,0x7a,0xfa,0x68,0x60,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0xd7,0x53, ]) as ReadonlyUint8Array; diff --git a/js/types.ts b/js/types.ts index d35f85fa..649ea11e 100644 --- a/js/types.ts +++ b/js/types.ts @@ -124,7 +124,7 @@ export interface Card extends Memory, Restorable { export type TapeData = Array<[duration: number, high: boolean]>; export interface Restorable { - getState(): T; + getState(): T | Promise; setState(state: T): void; } diff --git a/js/ui/apple2.ts b/js/ui/apple2.ts index 94faab22..688cc3fe 100644 --- a/js/ui/apple2.ts +++ b/js/ui/apple2.ts @@ -119,30 +119,36 @@ export function openLoad(driveString: string, event: MouseEvent) { } export function openSave(driveString: string, event: MouseEvent) { - const driveNo = parseInt(driveString, 10) as DriveNumber; + _disk2 + .getBinary(driveNo) + .then((storageData) => { + const driveNo = parseInt(driveString, 10) as DriveNumber; - const mimeType = 'application/octet-stream'; - const storageData = _disk2.getBinary(driveNo); - const a = document.querySelector('#local_save_link')!; + const mimeType = 'application/octet-stream'; - if (!storageData) { - alert(`No data from drive ${driveNo}`); - return; - } + const a = + document.querySelector('#local_save_link')!; - const { data } = storageData; - const blob = new Blob([data], { type: mimeType }); - a.href = window.URL.createObjectURL(blob); - a.download = driveLights.label(driveNo) + '.dsk'; + if (!storageData) { + alert(`No data from drive ${driveNo}`); + return; + } - if (event.metaKey) { - dumpDisk(driveNo); - } else { - const saveName = - document.querySelector('#save_name')!; - saveName.value = driveLights.label(driveNo); - MicroModal.show('save-modal'); - } + const { data } = storageData; + const blob = new Blob([data], { type: mimeType }); + a.href = window.URL.createObjectURL(blob); + a.download = driveLights.label(driveNo) + '.dsk'; + + if (event.metaKey) { + dumpDisk(driveNo); + } else { + const saveName = + document.querySelector('#save_name')!; + saveName.value = driveLights.label(driveNo); + MicroModal.show('save-modal'); + } + }) + .catch((error) => console.error(error)); } export function openAlert(msg: string) { @@ -402,20 +408,26 @@ function doLoadLocalDisk(driveNo: DriveNumber, file: File) { if (includes(DISK_FORMATS, ext)) { if (result.byteLength >= 800 * 1024) { - if ( - includes(BLOCK_FORMATS, ext) && - _massStorage.setBinary(driveNo, name, ext, result) - ) { - initGamepad(); + if (includes(BLOCK_FORMATS, ext)) { + _massStorage + .setBinary(driveNo, name, ext, result) + .then(() => initGamepad()) + .catch((error) => { + console.error(error); + openAlert(`Unable to load ${name}`); + }); } else { openAlert(`Unable to load ${name}`); } } else { - if ( - includes(FLOPPY_FORMATS, ext) && - _disk2.setBinary(driveNo, name, ext, result) - ) { - initGamepad(); + if (includes(FLOPPY_FORMATS, ext)) { + _disk2 + .setBinary(driveNo, name, ext, result) + .then(() => initGamepad()) + .catch((error) => { + console.error(error); + openAlert(`Unable to load ${name}`); + }); } else { openAlert(`Unable to load ${name}`); } @@ -488,15 +500,23 @@ export function doLoadHTTP(driveNo: DriveNumber, url?: string) { if (includes(DISK_FORMATS, ext)) { if (data.byteLength >= 800 * 1024) { if (includes(BLOCK_FORMATS, ext)) { - _massStorage.setBinary(driveNo, name, ext, data); - initGamepad(); + _massStorage + .setBinary(driveNo, name, ext, data) + .then(() => initGamepad()) + .catch((error) => { + console.error(error); + openAlert(`Unable to load ${name}`); + }); } } else { - if ( - includes(FLOPPY_FORMATS, ext) && - _disk2.setBinary(driveNo, name, ext, data) - ) { - initGamepad(); + if (includes(FLOPPY_FORMATS, ext)) { + _disk2 + .setBinary(driveNo, name, ext, data) + .then(() => initGamepad()) + .catch((error) => { + console.error(error); + openAlert(`Unable to load ${name}`); + }); } } } else { diff --git a/js/videomodes.ts b/js/videomodes.ts index 81a3ae95..d26d8f9e 100644 --- a/js/videomodes.ts +++ b/js/videomodes.ts @@ -86,6 +86,8 @@ export interface VideoModes extends Restorable { getCanvasAsBlob(): Promise; ready: Promise; + + getState(): VideoModesState; } export async function copyScreenToClipboard(vm: VideoModes) { diff --git a/test/js/cards/disk2.spec.ts b/test/js/cards/disk2.spec.ts index 48ac91e2..6aadd49f 100644 --- a/test/js/cards/disk2.spec.ts +++ b/test/js/cards/disk2.spec.ts @@ -54,9 +54,9 @@ describe('DiskII', () => { expect(diskII).not.toBeNull(); }); - it('round-trips the state when there are no changes', () => { + it('round-trips the state when there are no changes', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); const state = diskII.getState(); diskII.setState(state); @@ -64,10 +64,15 @@ describe('DiskII', () => { expect(diskII.getState()).toEqual(state); }); - it('round-trips the state when there are changes', () => { + it('round-trips the state when there are changes', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); - diskII.setBinary(2, 'BYTES_BY_SECTOR', 'po', BYTES_BY_SECTOR_IMAGE); + await diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 2, + 'BYTES_BY_SECTOR', + 'po', + BYTES_BY_SECTOR_IMAGE + ); const state = diskII.getState(); // These are just arbitrary changes, not an exhaustive list of fields. @@ -85,9 +90,9 @@ describe('DiskII', () => { expect(diskII.getState()).toEqual(state); }); - it('calls all of the callbacks when state is restored', () => { + it('calls all of the callbacks when state is restored', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); jest.resetAllMocks(); const state = diskII.getState(); @@ -173,9 +178,14 @@ describe('DiskII', () => { }); describe('head positioning', () => { - it('does not allow head positioning when the drive is off', () => { + it('does not allow head positioning when the drive is off', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); diskII.ioSwitch(0x81); // coil 0 on diskII.ioSwitch(0x83); // coil 1 on @@ -189,9 +199,14 @@ describe('DiskII', () => { expect(state.drives[1].track).toBe(0); }); - it('allows head positioning when the drive is on', () => { + it('allows head positioning when the drive is on', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); diskII.ioSwitch(0x89); // turn on the motor diskII.ioSwitch(0x81); // coil 0 on @@ -206,9 +221,14 @@ describe('DiskII', () => { expect(state.drives[1].track).toBe(4); }); - it('moves the head to track 2 from track 0 when all phases are cycled', () => { + it('moves the head to track 2 from track 0 when all phases are cycled', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); diskII.ioSwitch(0x89); // turn on the motor diskII.ioSwitch(0x81); // coil 0 on @@ -227,9 +247,14 @@ describe('DiskII', () => { expect(state.drives[1].track).toBe(2 * STEPS_PER_TRACK); }); - it('moves the head to track 10 from track 8 when all phases are cycled', () => { + it('moves the head to track 10 from track 8 when all phases are cycled', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); setTrack(diskII, 8); diskII.ioSwitch(0x89); // turn on the motor @@ -249,9 +274,14 @@ describe('DiskII', () => { expect(state.drives[1].track).toBe(10 * STEPS_PER_TRACK); }); - it('stops the head at track 34', () => { + it('stops the head at track 34', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); setTrack(diskII, 33); diskII.ioSwitch(0x89); // turn on the motor @@ -281,9 +311,14 @@ describe('DiskII', () => { expect(state.drives[1].track).toBe(35 * STEPS_PER_TRACK - 1); }); - it('moves a half track when only one phase is activated', () => { + it('moves a half track when only one phase is activated', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); setTrack(diskII, 15); diskII.ioSwitch(0x89); // turn on the motor @@ -297,9 +332,14 @@ describe('DiskII', () => { expect(state.drives[1].track).toBe(15 * STEPS_PER_TRACK + 2); }); - it('moves backward one track when phases are cycled in reverse', () => { + it('moves backward one track when phases are cycled in reverse', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); setTrack(diskII, 15); diskII.ioSwitch(0x89); // turn on the motor @@ -315,9 +355,14 @@ describe('DiskII', () => { expect(state.drives[1].track).toBe(14 * STEPS_PER_TRACK); }); - it('moves backward two tracks when all phases are cycled in reverse', () => { + it('moves backward two tracks when all phases are cycled in reverse', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); setTrack(diskII, 15); diskII.ioSwitch(0x89); // turn on the motor @@ -337,9 +382,14 @@ describe('DiskII', () => { expect(state.drives[1].track).toBe(13 * STEPS_PER_TRACK); }); - it('does not move backwards past track 0', () => { + it('does not move backwards past track 0', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); setTrack(diskII, 1); diskII.ioSwitch(0x89); // turn on the motor @@ -359,9 +409,14 @@ describe('DiskII', () => { expect(state.drives[1].track).toBe(0); }); - it('moves backward one half track', () => { + it('moves backward one half track', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); setTrack(diskII, 15); diskII.ioSwitch(0x89); // turn on the motor @@ -378,9 +433,14 @@ describe('DiskII', () => { // The emulated Disk II is not able to step quarter tracks because // it does not track when phases are turned off. // eslint-disable-next-line jest/no-disabled-tests - it.skip('moves a quarter track when two neighboring phases are activated and held', () => { + it.skip('moves a quarter track when two neighboring phases are activated and held', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); setTrack(diskII, 15); diskII.ioSwitch(0x89); // turn on the motor @@ -395,9 +455,14 @@ describe('DiskII', () => { // The emulated Disk II is not able to step quarter tracks because // it does not track when phases are turned off. // eslint-disable-next-line jest/no-disabled-tests - it.skip('moves backward one quarter track', () => { + it.skip('moves backward one quarter track', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); setTrack(diskII, 15); diskII.ioSwitch(0x89); // turn on the motor @@ -411,9 +476,14 @@ describe('DiskII', () => { }); describe('reading nibble-based disks', () => { - it('spins the disk', () => { + it('spins the disk', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); diskII.ioSwitch(0x89); // turn on the motor diskII.ioSwitch(0x8e); // read mode @@ -430,9 +500,14 @@ describe('DiskII', () => { expect(spinning).toBeTruthy(); }); - it('after reading the data, the data register is set to zero', () => { + it('after reading the data, the data register is set to zero', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); diskII.ioSwitch(0x89); // turn on the motor diskII.ioSwitch(0x8e); // read mode @@ -449,9 +524,14 @@ describe('DiskII', () => { expect(nibble).toBe(0x00); }); - it('after reading the data, then zero, there is new data', () => { + it('after reading the data, then zero, there is new data', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); diskII.ioSwitch(0x89); // turn on the motor diskII.ioSwitch(0x8e); // read mode @@ -470,9 +550,14 @@ describe('DiskII', () => { expect(nibble).toBe(0xaa); }); - it('read write protect status', () => { + it('read write protect status', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); setWriteProtected(diskII, true); diskII.ioSwitch(0x89); // turn on the motor @@ -485,9 +570,14 @@ describe('DiskII', () => { }); describe('writing nibble-based disks', () => { - it('writes a nibble to the disk', () => { + it('writes a nibble to the disk', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); let disk1 = diskII.getState().drives[1].disk as NibbleDisk; let track0 = disk1.tracks[0]; expect(track0[0]).toBe(0xff); @@ -501,9 +591,14 @@ describe('DiskII', () => { expect(track0[0]).toBe(0x80); }); - it('writes two nibbles to the disk', () => { + it('writes two nibbles to the disk', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); let disk1 = diskII.getState().drives[1].disk as NibbleDisk; let track0 = disk1.tracks[0]; expect(track0[0]).toBe(0xff); @@ -520,9 +615,14 @@ describe('DiskII', () => { expect(track0[1]).toBe(0x81); }); - it('sets disk state to dirty and calls the dirty callback when written', () => { + it('sets disk state to dirty and calls the dirty callback when written', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); + await diskII.setBinary( + 1, + 'BYTES_BY_TRACK', + 'po', + BYTES_BY_TRACK_IMAGE + ); let state = diskII.getState(); state.drives[1].dirty = false; diskII.setState(state); @@ -545,9 +645,9 @@ describe('DiskII', () => { 'test/js/cards/data/DOS 3.3 System Master.woz' ).buffer; - it('accepts WOZ-based disks', () => { + it('accepts WOZ-based disks', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary( + await diskII.setBinary( 1, 'DOS 3.3 System Master', 'woz', @@ -557,9 +657,9 @@ describe('DiskII', () => { expect(true).toBeTruthy(); }); - it('stops the head at the end of the image', () => { + it('stops the head at the end of the image', async () => { const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary( + await diskII.setBinary( 1, 'DOS 3.3 System Master', 'woz', @@ -589,12 +689,12 @@ describe('DiskII', () => { expect(state.drives[1].track).toBe(40 * STEPS_PER_TRACK - 1); }); - it('spins the disk when motor is on', () => { + it('spins the disk when motor is on', async () => { let cycles: number = 0; (mockApple2IO.cycles as jest.Mock).mockImplementation(() => cycles); const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary( + await diskII.setBinary( 1, 'DOS 3.3 System Master', 'woz', @@ -612,12 +712,12 @@ describe('DiskII', () => { expect(state.drives[1].head).toBeGreaterThan(0); }); - it('does not spin the disk when motor is off', () => { + it('does not spin the disk when motor is off', async () => { let cycles: number = 0; (mockApple2IO.cycles as jest.Mock).mockImplementation(() => cycles); const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary( + await diskII.setBinary( 1, 'DOS 3.3 System Master', 'woz', @@ -634,12 +734,12 @@ describe('DiskII', () => { expect(state.drives[1].head).toBe(0); }); - it('reads an FF sync byte from the beginning of the image', () => { + it('reads an FF sync byte from the beginning of the image', async () => { let cycles: number = 0; (mockApple2IO.cycles as jest.Mock).mockImplementation(() => cycles); const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary( + await diskII.setBinary( 1, 'DOS 3.3 System Master', 'woz', @@ -665,12 +765,12 @@ describe('DiskII', () => { expect(nibble).toBe(0xff); }); - it('reads several FF sync bytes', () => { + it('reads several FF sync bytes', async () => { let cycles: number = 0; (mockApple2IO.cycles as jest.Mock).mockImplementation(() => cycles); const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary( + await diskII.setBinary( 1, 'DOS 3.3 System Master', 'woz', @@ -704,12 +804,12 @@ describe('DiskII', () => { }); // eslint-disable-next-line jest/no-disabled-tests - it.skip('reads random garbage on uninitialized tracks', () => { + it.skip('reads random garbage on uninitialized tracks', async () => { let cycles: number = 0; (mockApple2IO.cycles as jest.Mock).mockImplementation(() => cycles); const diskII = new DiskII(mockApple2IO, callbacks); - diskII.setBinary( + await diskII.setBinary( 1, 'DOS 3.3 System Master', 'woz', @@ -755,8 +855,8 @@ describe('DiskII', () => { expect(failures).toBeLessThan(2); }); - it('disk spins at a consistent speed', () => { - const reader = new TestDiskReader( + it('disk spins at a consistent speed', async () => { + const reader = await TestDiskReader.create( 1, 'DOS 3.3 System Master', DOS33_SYSTEM_MASTER_IMAGE, @@ -788,8 +888,8 @@ describe('DiskII', () => { 'test/js/cards/data/DOS 3.3 System Master.woz' ).buffer; - it('can write something', () => { - const reader = new TestDiskReader( + it('can write something', async () => { + const reader = await TestDiskReader.create( 1, 'DOS 3.3 System Master', DOS33_SYSTEM_MASTER_IMAGE, @@ -826,17 +926,30 @@ class TestDiskReader { nibbles = 0; diskII: DiskII; - constructor( + private constructor(apple2IO: Apple2IO, callbacks: Callbacks) { + (apple2IO.cycles as jest.Mock).mockImplementation(() => this.cycles); + + this.diskII = new DiskII(apple2IO, callbacks); + } + + private async init( + driveNo: DriveNumber, + label: string, + image: ArrayBufferLike + ) { + await this.diskII.setBinary(driveNo, label, 'woz', image); + } + + static async create( driveNo: DriveNumber, label: string, image: ArrayBufferLike, apple2IO: Apple2IO, callbacks: Callbacks - ) { - (apple2IO.cycles as jest.Mock).mockImplementation(() => this.cycles); - - this.diskII = new DiskII(apple2IO, callbacks); - this.diskII.setBinary(driveNo, label, 'woz', image); + ): Promise { + const reader = new TestDiskReader(apple2IO, callbacks); + await reader.init(driveNo, label, image); + return reader; } readNibble(): byte { diff --git a/test/js/formats/2mg.spec.ts b/test/js/formats/2mg.spec.ts index e29129e8..15162945 100644 --- a/test/js/formats/2mg.spec.ts +++ b/test/js/formats/2mg.spec.ts @@ -5,7 +5,7 @@ import { HeaderData, read2MGHeader, } from 'js/formats/2mg'; -import { BlockDisk, ENCODING_BLOCK } from 'js/formats/types'; +import { BlockDisk, MemoryBlockDisk } from 'js/formats/types'; import { concat } from 'js/util'; import { BYTES_BY_SECTOR_IMAGE } from './testdata/16sector'; @@ -198,20 +198,19 @@ describe('2mg format', () => { }); describe('create2MGFromBlockDisk', () => { - it('can create a 2mg disk', () => { + it('can create a 2mg disk', async () => { const header = read2MGHeader(VALID_PRODOS_IMAGE.buffer); const blocks = []; for (let idx = 0; idx < BYTES_BY_SECTOR_IMAGE.length; idx += 512) { blocks.push(BYTES_BY_SECTOR_IMAGE.slice(idx, idx + 512)); } - const disk: BlockDisk = { - blocks, - metadata: { name: 'Good disk' }, - readOnly: false, - encoding: ENCODING_BLOCK, - format: 'hdv', - }; - const image = create2MGFromBlockDisk(header, disk); + const disk: BlockDisk = new MemoryBlockDisk( + 'hdv', + { name: 'Good disk' }, + false, + blocks + ); + const image = await create2MGFromBlockDisk(header, disk); expect(VALID_PRODOS_IMAGE.buffer).toEqual(image); }); }); diff --git a/test/js/formats/po.spec.ts b/test/js/formats/po.spec.ts index 898945f8..bd02f4ae 100644 --- a/test/js/formats/po.spec.ts +++ b/test/js/formats/po.spec.ts @@ -268,14 +268,15 @@ describe('ProDOS format', () => { expect(disk.encoding).toEqual('block'); }); - it('has the correct block size', () => { - expect(disk.blocks).toHaveLength(BLOCK_COUNT); + it('has the correct block size', async () => { + expect(await disk.blockCount()).toEqual(BLOCK_COUNT); }); - it('Has the correct block data', () => { + it('Has the correct block data', async () => { for (let block = 0; block < BLOCK_COUNT; block++) { - expect(disk.blocks[block][0]).toBe(block & 0xff); - expect(disk.blocks[block][1]).toBe(block >> 8); + const readBlock = await disk.read(block); + expect(readBlock[0]).toBe(block & 0xff); + expect(readBlock[1]).toBe(block >> 8); } }); });