Skip to content

Commit

Permalink
More fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
mattgodbolt committed Jul 31, 2024
1 parent 471fc1c commit b9ff08a
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 20 deletions.
88 changes: 83 additions & 5 deletions disc.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class TrackBuilder {
if (this._index >= IbmDiscFormat.bytesPerTrack) throw new Error("Overflowed disc size");
const existingPulses = this._track.pulses2Us[this._index];
const mask = 0xffff << this._pulsesIndex;
this._pulsesIndex = (this._pulsesIndex + 16) & 15;
this._pulsesIndex = (this._pulsesIndex + 16) & 31;
this._track.pulses2Us[this._index] = (existingPulses & mask) | (pulses << this._pulsesIndex);
if (this._pulsesIndex === 0) this._index++;
return this;
Expand Down Expand Up @@ -217,8 +217,9 @@ class MfmReader {
constructor(rawReader) {
this._rawReader = rawReader;
}

read(numBytes) {
const data = new Uint8Array[numBytes]();
const data = new Uint8Array(numBytes);
let pulses = 0;
for (let offset = 0; offset < numBytes; ++offset) {
if ((offset & 1) === 0) {
Expand Down Expand Up @@ -261,6 +262,7 @@ class FmReader {
}
return { data, clocks, iffyPulses };
}

get initialCrc() {
return IbmDiscFormat.crcInit(false);
}
Expand Down Expand Up @@ -325,8 +327,9 @@ class Sector {
const dataMarker = this.isDeleted
? IbmDiscFormat.deletedDataMarkDataPattern
: IbmDiscFormat.dataMarkDataPattern;
const sectorStartByte = this.dataPosBitOffset / pulsesPerByte;
const sectorEndByte = nextSector ? nextSector.idPosBitOffset / pulsesPerByte : this.track.length;
const sectorStartByte = (this.dataPosBitOffset / pulsesPerByte) | 0;
const sectorEndByte =
(nextSector ? nextSector.idPosBitOffset / pulsesPerByte : (this.track.length * 32) / pulsesPerByte) | 0;
// Account for CRC and sync bytes.
let sectorSize = Sector.toSectorSize(sectorEndByte - sectorStartByte - 5);

Expand Down Expand Up @@ -413,7 +416,7 @@ class Track {
let dataByte = 0;
let sector = null;
for (let pulseIndex = 0; pulseIndex < bitLength; ++pulseIndex) {
if ((pulseIndex & 31) === 0) pulses = this.pulses2Us[pulseIndex >> 5];
if ((pulseIndex & 31) === 0) pulses = this.pulses2Us[pulseIndex >>> 5];
markDetectorPrev = (markDetectorPrev << 1n) & all64b;
markDetectorPrev |= markDetector >> 63n;
markDetector = (markDetector << 1n) & all64b;
Expand Down Expand Up @@ -605,6 +608,80 @@ export function loadSsd(disc, data, isDsd, onChange) {
return disc;
}

class AdfFormat {
static get sectorSize() {
return 256;
}

static get sectorsPerTrack() {
return 16;
}

static get tracksPerDisc() {
return 80;
}
}

/**
* @param {Disc} disc
* @param {Uint8Array} data
*/
export function loadAdf(disc, data) {
const blankSector = new Uint8Array(AdfFormat.sectorSize);
const numSides = 2;
if (data.length % AdfFormat.sectorSize !== 0) {
throw new Error("ADF file size is not a multiple of sector size");
}
const maxSize = AdfFormat.sectorSize * AdfFormat.sectorsPerTrack * AdfFormat.tracksPerDisc * numSides;
if (data.length > maxSize) {
throw new Error("ADF file is too large");
}

let offset = 0;
for (let track = 0; track < AdfFormat.tracksPerDisc; ++track) {
if (offset >= data.length) break;

for (let side = 0; side < numSides; ++side) {
// Using recommended values from the 177x datasheet.
const trackBuilder = disc.buildTrack(side === 1, track);
trackBuilder.appendRepeatMfmByte(0x4e, 60);
for (let sector = 0; sector < AdfFormat.sectorsPerTrack; ++sector) {
trackBuilder
.appendRepeatMfmByte(0x00, 12)
.resetCrc()
.appendMfm3xA1Sync()
.appendMfmByte(IbmDiscFormat.idMarkDataPattern)
.appendMfmByte(track)
.appendMfmByte(0)
.appendMfmByte(sector)
.appendMfmByte(1)
.appendCrc(true);

// Sync pattern between sector header and sector data, aka GAP 2.
trackBuilder.appendRepeatMfmByte(0x4e, 22).appendRepeatMfmByte(0x00, 12);

// Sector data.
const sectorData =
offset < data.length ? data.subarray(offset, offset + AdfFormat.sectorSize) : blankSector;
offset += AdfFormat.sectorSize;
trackBuilder
.resetCrc()
.appendMfm3xA1Sync()
.appendMfmByte(IbmDiscFormat.dataMarkDataPattern)
.appendMfmChunk(sectorData)
.appendCrc(true);

// Sync pattern between sectors, aka GAP 3.
trackBuilder.appendRepeatMfmByte(0x4e, 24);
}
trackBuilder.fillMfmByte(0x4e);
}
}

// TODO writeback
return disc;
}

/**
* @returns {Uint8Array}
* @param {Disc} disc
Expand Down Expand Up @@ -1018,6 +1095,7 @@ export class IbmDiscFormat {
}
return crc;
}

static crcAddBytes(crc, bytes) {
for (const byte of bytes) crc = IbmDiscFormat.crcAddByte(crc, byte);
return crc;
Expand Down
7 changes: 6 additions & 1 deletion fdc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Floppy disc controller and assorted utils.
import { Disc, DiscConfig, loadHfe, loadSsd } from "./disc.js";
import { Disc, DiscConfig, loadHfe, loadAdf, loadSsd } from "./disc.js";
import * as utils from "./utils.js";

const DiscTimeSlice = 16 * 16;
Expand Down Expand Up @@ -39,10 +39,15 @@ export function discFor(fdc, name, stringData, onChange) {
if (lowerName.endsWith(".hfe")) {
return loadHfe(disc, data);
}
if (lowerName.endsWith(".adl") || lowerName.endsWith(".adf")) {
return loadAdf(disc, data);
}
return loadSsd(disc, data, lowerName.endsWith(".dsd"), onChange);
}

// TODO remove all this
const prevData = new Uint8Array(data);

function changed() {
let res = false;
for (let i = 0; i < data.length; ++i) {
Expand Down
42 changes: 28 additions & 14 deletions tests/unit/test-disc.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it } from "mocha";
import assert from "assert";

import { Disc, DiscConfig, IbmDiscFormat, loadHfe, loadSsd, toSsdOrDsd } from "../../disc.js";
import { Disc, DiscConfig, IbmDiscFormat, loadHfe, loadSsd, loadAdf, toSsdOrDsd } from "../../disc.js";
import * as fs from "node:fs";

describe("IBM disc format tests", function () {
Expand Down Expand Up @@ -97,7 +97,7 @@ describe("Disc builder tests", () => {
const someData = new Uint8Array(256);
someData.fill(0x33);
it("should write a simple FM track without blowing up", () => {
const disc = new Disc(true, new DiscConfig());
const disc = new Disc(true, new DiscConfig(), "test.ssd");
const builder = disc.buildTrack(false, 0);
builder
.appendRepeatFmByte(0xff, IbmDiscFormat.stdGap1FFs)
Expand Down Expand Up @@ -144,7 +144,7 @@ describe("Disc builder tests", () => {
});

it("should note how much disc is being used", () => {
const disc = new Disc(true, new DiscConfig());
const disc = new Disc(true, new DiscConfig(), "test.ssd");
assert.equal(disc.tracksUsed, 0);
assert(!disc.isDoubleSided);
disc.buildTrack(false, 0);
Expand All @@ -159,15 +159,15 @@ describe("Disc builder tests", () => {
});

it("should build from FM pulses", () => {
const disc = new Disc(true, new DiscConfig());
const disc = new Disc(true, new DiscConfig(), "test.ssd");
const builder = disc.buildTrack(false, 0);
const pulses = [4, 4, 8, 8, 4, 8, 8, 8, 8, 8, 8, 8, 8];
builder.buildFromPulses(pulses, false);
assert.equal(builder.track.length, 1);
});

it("should build from MFM pulses", () => {
const disc = new Disc(true, new DiscConfig());
const disc = new Disc(true, new DiscConfig(), "test.ssd");
const builder = disc.buildTrack(false, 0);
const pulses = [4, 4, 6, 6, 4, 6, 6, 6, 6, 6, 6, 6, 6, 6];
builder.buildFromPulses(pulses, true);
Expand All @@ -177,14 +177,14 @@ describe("Disc builder tests", () => {

describe("SSD loader tests", function () {
const data = fs.readFileSync("discs/elite.ssd");
this.timeout(5000); // roundtripping elite can be slow
this.timeout(5000); // roundtripping elite can be slow
it("should load Elite", () => {
const disc = new Disc(true, new DiscConfig());
const disc = new Disc(true, new DiscConfig(), "test.ssd");
loadSsd(disc, data, false);
assert.equal(disc.tracksUsed, 46);
});
it("should roundtrip Elite", () => {
const disc = new Disc(true, new DiscConfig());
const disc = new Disc(true, new DiscConfig(), "test.ssd");
loadSsd(disc, data, false);
const ssdSaved = toSsdOrDsd(disc);
// // Check the first few bytes, else a diff blows things up
Expand All @@ -201,9 +201,9 @@ describe("SSD loader tests", function () {
}
});
it("should have sane tracks", () => {
const disc = new Disc(true, new DiscConfig());
const disc = new Disc(true, new DiscConfig(), "test.ssd");
loadSsd(disc, data, false);
const sectors = disc.getTrack(0, 0).findSectors();
const sectors = disc.getTrack(false, 0).findSectors();
assert.equal(sectors.length, 10);
for (const sector of sectors) {
assert(!sector.hasHeaderCrcError);
Expand All @@ -214,16 +214,30 @@ describe("SSD loader tests", function () {

describe("HFE loader tests", function () {
const data = fs.readFileSync("discs/elite.hfe");
this.timeout(5000); // roundtripping elite can be slow
it("should load Elite", () => {
const disc = new Disc(true, new DiscConfig());
loadHfe(disc, data, false);
const disc = new Disc(true, new DiscConfig(), "test.hfe");
loadHfe(disc, data);
assert.equal(disc.tracksUsed, 81);
const sectors = disc.getTrack(0, 0).findSectors();
const sectors = disc.getTrack(false, 0).findSectors();
assert.equal(sectors.length, 10);
for (const sector of sectors) {
assert(!sector.hasHeaderCrcError);
assert(!sector.hasDataCrcError);
}
});
});

describe("ADF loader tests", function () {
it("should load a somewhat blank ADFS disc", () => {
const data = new Uint8Array(327680);
const disc = new Disc(true, new DiscConfig(), "test.adf");
loadAdf(disc, data);
assert.equal(disc.tracksUsed, 40);
const sectors = disc.getTrack(false, 0).findSectors();
assert.equal(sectors.length, 16);
for (const sector of sectors) {
assert(!sector.hasHeaderCrcError);
assert(!sector.hasDataCrcError);
}
});
});

0 comments on commit b9ff08a

Please sign in to comment.