Skip to content

Commit

Permalink
Add wrapping of HFS disk images into full device images
Browse files Browse the repository at this point in the history
Required for allowing DingusPPC to boot from disk images. Also allows Saved
HD to be exported to an .hda file that can be used with BlueSCSI and other
software that expects a full device image.

The base device image header was generated by Disk Jockey 2.5.2.2 (it's the
first 0xc000 or 48 KB of an empty device image - the HFS image can be appended
to it and a couple of fields be updated).

Updates #219
Updates #234
  • Loading branch information
mihaip committed Oct 1, 2023
1 parent d6c54f5 commit bfc5b19
Show file tree
Hide file tree
Showing 19 changed files with 201 additions and 22 deletions.
3 changes: 3 additions & 0 deletions Images/System 7.1.2 HD.dsk
Git LFS file not shown
3 changes: 3 additions & 0 deletions scripts/disks.py
Expand Up @@ -96,6 +96,8 @@ def path(self) -> str:

SYSTEM_711 = Disk(name="System 7.1.1 HD.dsk")

SYSTEM_712 = Disk(name="System 7.1.2 HD.dsk")

SYSTEM_712_DISK_TOOLS = Disk(name="System 7.1.2 Disk Tools FD.dsk")

SYSTEM_75 = Disk(name="System 7.5 HD.dsk")
Expand Down Expand Up @@ -173,6 +175,7 @@ def path(self) -> str:
SYSTEM_70,
SYSTEM_71,
SYSTEM_711,
SYSTEM_712,
SYSTEM_712_DISK_TOOLS,
SYSTEM_75,
SYSTEM_75_DISK_TOOLS,
Expand Down
Binary file added src/Data/Device Image Header.hda
Binary file not shown.
4 changes: 2 additions & 2 deletions src/Mac.tsx
Expand Up @@ -589,9 +589,9 @@ export default function Mac({
);
varz.increment("emulator_disk_saver_import");
}}
onSaveImage={() => {
onSaveImage={deviceImage => {
emulatorRef.current?.restart(() =>
saveDiskSaverImage(SAVED_HD)
saveDiskSaverImage(SAVED_HD, deviceImage)
);
varz.increment("emulator_disk_saver_save_image");
}}
Expand Down
21 changes: 16 additions & 5 deletions src/MacSettings.tsx
Expand Up @@ -33,7 +33,7 @@ export function MacSettings({
onStorageReset: () => void;
onStorageExport: () => void;
onStorageImport: () => void;
onSaveImage: () => void;
onSaveImage: (deviceImage?: boolean) => void;
onDone: () => void;
}) {
const [storagePersistenceStatus, setStoragePersistenceStatus] = useState<
Expand Down Expand Up @@ -186,17 +186,22 @@ export function MacSettings({
appearance={appearance}
visible={saveImageVisible}
setVisible={setSaveImageVisible}
title="Save Disk Image"
body="Saving a disk image requires that the Mac be shut down. It will take a little while to generate, and will be downloaded as a .dsk file. Generate the disk image now?"
title="Save Image"
body="Saving a disk image requires that the Mac be shut down. It will take a little while to generate, and will be downloaded as a .dsk or .hda file. Generate the disk image now?"
onAccept={() => {
onSaveImage();
onDone();
}}
otherLabel="Save Device Image (.hda)"
onOther={() => {
onSaveImage(true);
onDone();
}}
/>
<div className="Dialog-Description">
You can also save the contents of Saved HD as a
<code>.dsk</code> file that can be loaded into other
emulators.
<code>.dsk</code> or <code>.hda</code> file that can
be loaded into other emulators.
</div>
</div>
<div className="MacSettings-Row">
Expand Down Expand Up @@ -237,13 +242,17 @@ function StorageConfirmDialog({
title,
body,
onAccept,
onOther,
otherLabel,
appearance,
}: {
visible: boolean;
setVisible: (visible: boolean) => void;
title: string;
body: string;
onAccept: () => void;
onOther?: () => void;
otherLabel?: string;
appearance: Appearance;
}) {
if (!visible) {
Expand All @@ -257,6 +266,8 @@ function StorageConfirmDialog({
onAccept();
}}
doneLabel={title}
onOther={onOther}
otherLabel={otherLabel}
onCancel={() => setVisible(false)}
appearance={appearance}>
<div style={{maxWidth: 400}}>{body}</div>
Expand Down
2 changes: 1 addition & 1 deletion src/controls/Dialog.css
Expand Up @@ -89,6 +89,6 @@
color: #00c;
}

.Dialog-Cancel {
.Dialog-Normal-Button {
font-weight: normal;
}
17 changes: 16 additions & 1 deletion src/controls/Dialog.tsx
Expand Up @@ -11,6 +11,8 @@ export function Dialog({
onDone,
doneLabel = "Done",
doneEnabled = true,
onOther,
otherLabel,
onCancel,
appearance = "Classic",
className,
Expand All @@ -20,6 +22,8 @@ export function Dialog({
onDone: (e: React.MouseEvent) => void;
doneLabel?: string;
doneEnabled?: boolean;
onOther?: (e: React.MouseEvent) => void;
otherLabel?: string;
onCancel?: () => void;
appearance?: Appearance;
className?: string;
Expand All @@ -39,7 +43,7 @@ export function Dialog({
<footer>
{onCancel && (
<Button
className="Dialog-Cancel"
className="Dialog-Normal-Button"
appearance={appearance}
onClick={e => {
e.preventDefault();
Expand All @@ -48,6 +52,17 @@ export function Dialog({
Cancel
</Button>
)}
{onOther && (
<Button
className="Dialog-Normal-Button"
appearance={appearance}
onClick={e => {
e.preventDefault();
onOther(e);
}}>
{otherLabel}
</Button>
)}
<Button
disabled={!doneEnabled}
appearance={appearance}
Expand Down
10 changes: 6 additions & 4 deletions src/disks.ts
Expand Up @@ -326,13 +326,15 @@ const SYSTEM_7_1_1: SystemDiskDef = {
generatedSpec: () => import("./Data/System 7.1.1 HD.dsk.json"),
};

const SYSTEM_7_1_2: PlaceholderDiskDef = {
type: "placeholder",
const SYSTEM_7_1_2: SystemDiskDef = {
displayName: "System 7.1.2",
description:
"Initial system software for the Power Macintosh computers. Does not run under emulation.",
"Initial system software for the first Power Macintosh computers.",
releaseDate: [1994, 3, 14],
machines: [POWER_MACINTOSH_9500],
prefetchChunks: [0, 1, 2],
machines: [POWER_MACINTOSH_6100],
isUnstable: true,
generatedSpec: () => import("./Data/System 7.1.2 HD.dsk.json"),
};

const SYSTEM_7_1_2_DISK_TOOLS: SystemDiskDef = {
Expand Down
27 changes: 27 additions & 0 deletions src/emulator/emulator-common-device-image.ts
@@ -0,0 +1,27 @@
const BLOCK_SIZE = 512;

export function generateDeviceImageHeader(
baseHeader: ArrayBuffer,
hfsPartitionSize: number
): ArrayBuffer {
const headerSize = baseHeader.byteLength;
if (headerSize !== 0xc000) {
console.warn(
"Expected base header to be 0xc000 bytes, got 0x" +
headerSize.toString(16) +
" bytes, results may be unpredictable"
);
}
const header = baseHeader.slice(0);
const headerView = new DataView(header);

const totalSize = hfsPartitionSize + headerSize;
const totalBlocks = totalSize / BLOCK_SIZE;
headerView.setInt32(0x4, totalBlocks); // sbBlkCount

const hfsBlocks = hfsPartitionSize / BLOCK_SIZE;
headerView.setInt32(0x060c, hfsBlocks); // pmPartBlkCnt?
headerView.setInt32(0x0654, hfsBlocks); // pmDataCnt?

return header;
}
4 changes: 4 additions & 0 deletions src/emulator/emulator-common-emulators.ts
Expand Up @@ -46,6 +46,10 @@ export function emulatorSupportsDownloadsFolder(type: EmulatorType): boolean {
return type === "BasiliskII" || type === "SheepShaver";
}

export function emulatorNeedsDeviceImage(type: EmulatorType): boolean {
return type === "DingusPPC";
}

export function emulatorCpuId(
type: EmulatorType,
cpu: EmulatorCpu
Expand Down
1 change: 1 addition & 0 deletions src/emulator/emulator-common.ts
Expand Up @@ -124,6 +124,7 @@ export type EmulatorWorkerConfig = {
wasmUrl: string;
disks: EmulatorChunkedFileSpec[];
delayedDisks?: EmulatorChunkedFileSpec[];
deviceImageHeader: ArrayBuffer;
cdroms: EmulatorCDROM[];
useCDROM: boolean;
autoloadFiles: {[name: string]: ArrayBuffer};
Expand Down
19 changes: 18 additions & 1 deletion src/emulator/emulator-ui-disk-saver.ts
Expand Up @@ -3,6 +3,8 @@ import {saveAs} from "file-saver";
import {type EmulatorDiskDef} from "../disks";
import {dirtyChunksFileName, dataFileName} from "./emulator-common-disk-saver";
import {generateChunkUrl} from "./emulator-common";
import deviceImageHeaderPath from "../Data/Device Image Header.hda";
import {generateDeviceImageHeader} from "./emulator-common-device-image";

export async function resetDiskSaver(disk: EmulatorDiskDef) {
const spec = (await disk.generatedSpec()).default;
Expand Down Expand Up @@ -91,7 +93,10 @@ function pickDiskSaverFile() {
});
}

export async function saveDiskSaverImage(disk: EmulatorDiskDef) {
export async function saveDiskSaverImage(
disk: EmulatorDiskDef,
deviceImage?: boolean
) {
const spec = (await disk.generatedSpec()).default;
const opfsRoot = await navigator.storage.getDirectory();

Expand Down Expand Up @@ -136,5 +141,17 @@ export async function saveDiskSaverImage(disk: EmulatorDiskDef) {
image.set(new Uint8Array(chunk), chunkIndex * spec.chunkSize);
}

if (deviceImage) {
const baseDeviceImageHeader = await (
await fetch(deviceImageHeaderPath)
).arrayBuffer();
const deviceImageHeader = generateDeviceImageHeader(
baseDeviceImageHeader,
spec.totalSize
);
saveAs(new Blob([deviceImageHeader, image]), spec.name + ".hda");
return;
}

saveAs(new Blob([image]), spec.name + ".dsk");
}
25 changes: 23 additions & 2 deletions src/emulator/emulator-ui.ts
Expand Up @@ -63,6 +63,7 @@ import {
} from "./emulator-ui-clipboard";
import {type MachineDefRAMSize, type MachineDef} from "../machines";
import {type EmulatorDiskDef} from "../disks";
import deviceImageHeaderPath from "../Data/Device Image Header.hda";

export type EmulatorConfig = {
machine: MachineDef;
Expand Down Expand Up @@ -276,9 +277,13 @@ export class Emulator {
// Fetch all of the dependent files ourselves, to avoid a waterfall
// if we let Emscripten handle it (it would first load the JS, and
// then that would load the WASM and data files).
const [[wasmBlobUrl], [rom, basePrefs]] = await load(
const [[wasmBlobUrl], [rom, basePrefs, deviceImageHeader]] = await load(
[emulatorWasmPath],
[this.#config.machine.romPath, this.#config.machine.prefsPath],
[
this.#config.machine.romPath,
this.#config.machine.prefsPath,
deviceImageHeaderPath,
],
(total, left) => {
this.#delegate?.emulatorDidMakeLoadingProgress?.(
this,
Expand Down Expand Up @@ -333,6 +338,7 @@ export class Emulator {
wasmUrl: wasmBlobUrl,
disks,
delayedDisks,
deviceImageHeader,
cdroms: this.#config.cdroms,
useCDROM: emulatorUsesCDROMDrive(emulatorType),
autoloadFiles: {
Expand Down Expand Up @@ -364,6 +370,7 @@ export class Emulator {
}
this.#worker.postMessage({type: "start", config}, [
rom,
deviceImageHeader,
...(prefsBuffer ? [prefsBuffer] : []),
]);
}
Expand Down Expand Up @@ -897,7 +904,21 @@ function configToEmulatorArgs(
if (config.debugLog) {
args.push("--log-to-stderr", "--log-to-stderr-verbose");
}
let addedHardDisk = false;
let addedFloppy = false;
for (const spec of disks) {
// TODO: support more than one disk in DingusPPC
if (spec.isFloppy) {
if (addedFloppy) {
continue;
}
addedFloppy = true;
} else {
if (addedHardDisk) {
continue;
}
addedHardDisk = true;
}
args.push(spec.isFloppy ? "--fdd_img" : "--hdd_img", spec.name);
}
for (const spec of config.cdroms) {
Expand Down
58 changes: 58 additions & 0 deletions src/emulator/emulator-worker-device-image-disk.ts
@@ -0,0 +1,58 @@
import {generateDeviceImageHeader} from "./emulator-common-device-image";
import {type EmulatorWorkerDisk} from "./emulator-worker-disks";

export class EmulatorWorkerDeviceImageDisk implements EmulatorWorkerDisk {
#disk: EmulatorWorkerDisk;
#deviceHeader: ArrayBuffer;

constructor(disk: EmulatorWorkerDisk, baseDeviceHeader: ArrayBuffer) {
this.#disk = disk;
this.#deviceHeader = generateDeviceImageHeader(
baseDeviceHeader,
disk.size
);
}

get name(): string {
return this.#disk.name;
}

get size(): number {
return this.#disk.size + this.#deviceHeader.byteLength;
}

read(buffer: Uint8Array, offset: number, length: number): number {
if (offset < this.#deviceHeader.byteLength) {
if (offset + length > this.#deviceHeader.byteLength) {
throw new Error(
"Cannot read from both device header and disk in one op"
);
}
buffer.set(new Uint8Array(this.#deviceHeader, offset, length));
return length;
}
const diskOffset = offset - this.#deviceHeader.byteLength;
return this.#disk.read(buffer, diskOffset, length);
}

write(buffer: Uint8Array, offset: number, length: number): number {
if (offset < this.#deviceHeader.byteLength) {
if (offset + length > this.#deviceHeader.byteLength) {
throw new Error(
"Cannot write to both device header and disk in one op"
);
}

new Uint8Array(this.#deviceHeader, offset).set(
buffer.subarray(0, length)
);
return length;
}
const diskOffset = offset - this.#deviceHeader.byteLength;
return this.#disk.write(buffer, diskOffset, length);
}

validate?(): void {
this.#disk.validate?.();
}
}

0 comments on commit bfc5b19

Please sign in to comment.