mii_emu/src/format/dsk.c

473 lines
15 KiB
C

// format/dsk.c
//
// Copyright (c) 2023 Micah John Cowan.
// This code is licensed under the MIT license.
// See the accompanying LICENSE file for details.
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/mman.h>
#include <assert.h>
#include "mii_disk_format.h"
#define NIBBLE_SECTOR_SIZE 416
#define NIBBLE_TRACK_SIZE 6656
#define DSK_SECTOR_SIZE 256
#define MAX_SECTORS 16
#define VOLUME_NUMBER 254
#define DSK_TRACK_SIZE (DSK_SECTOR_SIZE * MAX_SECTORS)
#define byte uint8_t
struct dskprivdat {
const char *path;
byte *realbuf;
byte *buf;
const byte *secmap;
int bytenum;
uint64_t dirty_tracks;
};
static const struct dskprivdat datinit = { 0 };
static const size_t nib_disksz = 232960;
static const size_t dsk_disksz = 143360;
// DOS 3.3 Physical sector order (index is physical sector,
// value is DOS sector)
const byte DO[] = {
0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4,
0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF
};
// ProDOS Physical sector order (index is physical sector,
// value is ProDOS sector).
const byte PO[] = {
0x0, 0x8, 0x1, 0x9, 0x2, 0xa, 0x3, 0xb,
0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf
};
const byte TRANS62[] = {
0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6,
0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3,
0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc,
0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, 0xd3,
0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde,
0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec,
0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6,
0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff
};
static const int DETRANS62[] = {
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, 0x00, 0x01,
-1, -1, 0x02, 0x03, -1, 0x04, 0x05, 0x06,
-1, -1, -1, -1, -1, -1, 0x07, 0x08,
-1, -1, -1, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
-1, -1, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13,
-1, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A,
-1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, 0x1B, -1, 0x1C, 0x1D, 0x1E,
-1, -1, -1, 0x1F, -1, -1, 0x20, 0x21,
-1, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
-1, -1, -1, -1, -1, 0x29, 0x2A, 0x2B,
-1, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
-1, -1, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
-1, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F
};
static void realign_tracks(DiskFormatDesc *desc)
{
/*
When we unpack a .dsk into nibblized form, it's
automatically aligned neatly within our track-storage
boundaries within the disk buffer, because that's how
we laid 'em out.
When a sector is written or updated, it will
tend to stay aligned, because the program had to
find the existing sector to write to, to keep things
in order.
However, in the event of reformatting, there are no
guarantees about the track being aligned conveniently.
The formatting program can start at any old position
and just start writing, because no existing data is going
to be preserved.
To deal with this, we could either:
1) Be prepared to loop back around our track's buffer,
mid-sector
2) Re-align the track at our convenience, to start
somewhere that we know for sure can't be the middle of
sector data.
I've opted for option (2). It would be a bad option if
we're reading and writing a .nib file, because we'd be
unnecessarily altering the file's structure (in cases
where it had NOT been reformatted), but it's perfectly
fine when we'll be discarding the nibblized format anyway.
We handle this by seeking forward to the first sector-field
start boundary (D5 AA 96) that we can find, and make that
the new "start" of our track. It doesn't matter if it's a
false-start that doesn't start a real sector-field, because one
thing we know for *sure* is that first D5 can't be found
in the middle of a legitimate header or data field.
*/
struct dskprivdat *dat = desc->privdat;
byte *buf = dat->buf;
byte *secbuf = malloc(NIBBLE_TRACK_SIZE);
for (int t=0; t != NUM_TRACKS; ++t) {
byte *tstart = buf + (t * NIBBLE_TRACK_SIZE);
byte *tend = buf + ((t + 1) * NIBBLE_TRACK_SIZE);
byte *talign;
for (talign = tstart; talign <= (tend - 3); ++talign) {
if (talign[0] == 0xD5 && talign[1] == 0xAA && talign[2] == 0x96) {
if (talign == tstart) {
// Nothing to do, already aligned.
} else {
size_t rollsz = talign - tstart;
memcpy(secbuf, tstart, rollsz);
memmove(tstart, talign, tend-talign);
memcpy(tend - rollsz, secbuf, rollsz);
}
break; // handle next track
}
}
}
free(secbuf);
}
static void implodeDo(DiskFormatDesc *desc)
{
struct dskprivdat *dat = desc->privdat;
realign_tracks(desc);
const byte *rd = dat->buf; // nibble buf
const byte *end = dat->buf + nib_disksz;
bool warned = false;
for (;;) {
// Scan forward for a sector header
const int sector_hdr_sz = 11; // counts prologue, but not epilogue
for (;;) {
// This is the only place we're "allowed" to end processing.
if (rd >= (end - sector_hdr_sz)) goto done;
if (rd[0] == 0xD5 && rd[1] == 0xAA && rd[2] == 0x96) break;
++rd;
}
header:
rd += 3;
int v = ((rd[0] << 1) | 0x1) & rd[1];
rd += 2;
int t = ((rd[0] << 1) | 0x1) & rd[1];
rd += 2;
int s = ((rd[0] << 1) | 0x1) & rd[1];
rd += 2;
int checkSum = ((rd[0] << 1) | 0x1) & rd[1];
rd += 2;
if (checkSum != (v ^ t ^ s)) {
WARN("Sector header checksum failed, t=%d s=%d"
" at nibblized byte %zu.\n", t, s, rd - dat->buf);
WARN("Probable disk corruption for %s\n", dat->path);
}
byte truet = (rd - dat->buf)/NIBBLE_TRACK_SIZE;
if (t != truet) {
WARN("Sector header lying about track number"
" at nibblized pos %zu\n", (size_t)(rd - dat->buf));
WARN(" (says %d but we're on track %d). Skipping sector.\n",
(int)t, (int)truet);
continue;
}
if (s >= MAX_SECTORS) {
WARN("Sector header sec# too high (%d; max is %d).\n",
(int)s, (int)MAX_SECTORS);
WARN(" Skipping sector.\n");
continue;
}
// const int data_field_sz = 0x15A; //counts prologue, not epilogue
for (;;) {
if (rd >= (end - 0x15A)) goto bail;
if (rd[0] == 0xD5 && rd[1] == 0xAA) {
if (rd[2] == 0x96) goto header;
if (rd[2] == 0xAD) break;
}
++rd;
}
rd += 3;
// Translate sector data field
{
int s_ = dat->secmap[s];
byte *data = dat->realbuf + (t * DSK_TRACK_SIZE)
+ (s_ * DSK_SECTOR_SIZE);
byte data2[0x56];
byte last = 0;
int val;
if (0) {
WARN("Translating track %d, phys sec %d (image sec %d)\n",
t, s, s_);
WARN(" starting at dsk pos %zu\n", (size_t)(data - dat->realbuf));
}
for (int j = 0x55; j >= 0; --j) {
val = DETRANS62[*rd - 0x80];
if (val == -1 && !warned) {
warned = true;
WARN("Untranslatable nibble at (nibblized) pos %zu,"
" disk %s.\n", (size_t)(rd - dat->buf), dat->path);
if (rd <= end - 4) {
WARN("%02X %02X %02X %02X [%02X] %02X %02X %02X\n",
rd[-4], rd[-3], rd[-2], rd[-1],
rd[0], rd[1], rd[2], rd[3]);
}
WARN("CORRUPT DISK SAVE TO %s\n", dat->path);
}
val ^= last;
data2[j] = val;
last = val;
++rd;
}
for (int j = 0; j < 0x100; ++j) {
val = DETRANS62[*rd - 0x80];
if (val == -1 && !warned) {
warned = true;
WARN("Untranslatable nibble at t=%d, phys s=%d,"
" disk %s.\n", t, s, dat->path);
WARN("CORRUPT DISK SAVE TO %s\n", dat->path);
}
val ^= last;
data[j] = val;
last = val;
++rd;
}
int checkSum = DETRANS62[*rd++ - 0x80];
if (checkSum != -1) checkSum ^= last;
if (checkSum != 0) {
WARN("Bad sector data checksum at t=%d, phys s=%d,"
" disk %s.\n", t, s, dat->path);
}
for (int k = 0, j = 0x55; k < 0x100; ++k) {
data[k] <<= 1;
if ((data2[j] & 0x1) != 0) {
data[k] |= 0x01;
}
data2[j] >>= 1;
data[k] <<= 1;
if ((data2[j] & 0x01) != 0) {
data[k] |= 0x01;
}
data2[j] >>= 0x01;
if (--j < 0) j = 0x55;
}
}
}
bail:
WARN("Error translating to dsk: ended mid-sector!\n");
WARN("Probable disk corruption for %s\n", dat->path);
done:
return;
}
static void spin(DiskFormatDesc *desc, bool b)
{
struct dskprivdat *dat = desc->privdat;
if (!b && dat->dirty_tracks != 0) {
implodeDo(desc);
// For now, sync the entire disk
errno = 0;
int err = msync(dat->realbuf, dsk_disksz, MS_SYNC);
if (err < 0) {
DIE(1,"Couldn't sync to disk file %s: %s\n",
dat->path, strerror(errno));
}
dat->dirty_tracks = 0;
}
}
static byte read_byte(DiskFormatDesc *desc)
{
struct dskprivdat *dat = desc->privdat;
size_t pos = (desc->halftrack/2) * NIBBLE_TRACK_SIZE;
pos += (dat->bytenum % NIBBLE_TRACK_SIZE);
byte val = dat->buf[pos];
dat->bytenum = (dat->bytenum + 1) % NIBBLE_TRACK_SIZE;
return val;
}
static void write_byte(DiskFormatDesc *desc, byte val)
{
struct dskprivdat *dat = desc->privdat;
if ((val & 0x80) == 0) {
// D2DBG("dodged write $%02X", val);
return; // must have high bit
}
dat->dirty_tracks |= 1 << (desc->halftrack/2);
size_t pos = (desc->halftrack/2) * NIBBLE_TRACK_SIZE;
pos += (dat->bytenum % NIBBLE_TRACK_SIZE);
//D2DBG("write byte $%02X at pos $%04zX", (unsigned int)val, pos);
dat->buf[pos] = val;
dat->bytenum = (dat->bytenum + 1) % NIBBLE_TRACK_SIZE;
}
static void eject(DiskFormatDesc *desc)
{
// free dat->path and dat, and unmap disk image
struct dskprivdat *dat = desc->privdat;
(void) munmap(dat->buf, dsk_disksz);
free((void*)dat->path);
free(dat);
}
// This function is derived from Scullin Steel Co.'s apple2js code
// https://github.com/whscullin/apple2js/blob/e280c3d/js/formats/format_utils.ts#L140
static void explodeSector(byte vol, byte track, byte sector,
byte **nibSec, const byte *data)
{
byte *wr = *nibSec;
unsigned int gap;
// Gap 1/3 (40/0x28 bytes)
if (sector == 0) // Gap 1
gap = 0x80;
else { // Gap 3
gap = track == 0? 0x28 : 0x26;
}
for (uint8_t i = 0; i != gap; ++i) {
*wr++ = 0xFF;
}
// Address Field
const byte checksum = vol ^ track ^ sector;
*wr++ = 0xD5; *wr++ = 0xAA; *wr++ = 0x96; // Address Prolog D5 AA 96
*wr++ = (vol >> 1) | 0xAA; *wr++ = vol | 0xAA;
*wr++ = (track >> 1) | 0xAA; *wr++ = track | 0xAA;
*wr++ = (sector >> 1) | 0xAA; *wr++ = sector | 0xAA;
*wr++ = (checksum >> 1) | 0xAA; *wr++ = checksum | 0xAA;
*wr++ = 0xDE; *wr++ = 0xAA; *wr++ = 0xEB; // Epilogue DE AA EB
// Gap 2 (5 bytes)
for (int i = 0; i != 5; ++i) {
*wr++ = 0xFF;
}
// Data Field
*wr++ = 0xD5; *wr++ = 0xAA; *wr++ = 0xAD; // Data Prolog D5 AA AD
byte *nibbles = wr;
const unsigned ptr2 = 0;
const unsigned ptr6 = 0x56;
for (int i = 0; i != 0x156; ++i) {
nibbles[i] = 0;
}
int i2 = 0x55;
for (int i6 = 0x101; i6 >= 0; --i6) {
byte val6 = data[i6 % 0x100];
byte val2 = nibbles[ptr2 + i2];
val2 = (val2 << 1) | (val6 & 1);
val6 >>= 1;
val2 = (val2 << 1) | (val6 & 1);
val6 >>= 1;
nibbles[ptr6 + i6] = val6;
nibbles[ptr2 + i2] = val2;
if (--i2 < 0)
i2 = 0x55;
}
byte last = 0;
for (int i = 0; i != 0x156; ++i) {
const byte val = nibbles[i];
nibbles[i] = TRANS62[last ^ val];
last = val;
}
wr += 0x156; // advance write-pointer
*wr++ = TRANS62[last];
*wr++ = 0xDE; *wr++ = 0xAA; *wr++ = 0xEB; // Epilogue DE AA EB
// Gap 3
*wr++ = 0xFF;
*nibSec = wr;
}
static void explodeDsk(byte *nibbleBuf, byte *dskBuf, const byte *secmap)
{
for (int t = 0; t < NUM_TRACKS; ++t) {
byte *writePtr = nibbleBuf;
for (int phys_sector = 0; phys_sector < MAX_SECTORS; ++phys_sector) {
const byte dos_sector = secmap[phys_sector];
const size_t off = ((MAX_SECTORS * t + dos_sector)
* DSK_SECTOR_SIZE);
explodeSector(VOLUME_NUMBER, t, phys_sector,
&writePtr, &dskBuf[off]);
}
assert(writePtr - nibbleBuf <= NIBBLE_TRACK_SIZE);
for (; writePtr != (nibbleBuf + NIBBLE_TRACK_SIZE); ++writePtr) {
*writePtr = 0xFF;
}
nibbleBuf += NIBBLE_TRACK_SIZE;
}
}
DiskFormatDesc dsk_insert(const char *path, byte *buf, size_t sz)
{
if (sz != dsk_disksz) {
DIE(0,"Wrong disk image size for %s:\n", path);
DIE(1," Expected %zu, got %zu.\n", dsk_disksz, sz);
}
struct dskprivdat *dat = malloc(sizeof *dat);
*dat = datinit;
dat->realbuf = buf;
dat->path = strdup(path);
dat->buf = calloc(1, nib_disksz);
const char *ext = rindex(path, '.');
ext = ext ? ext+1 : "";
if (!strcasecmp(ext, "PO")) {
INFO("Opening %s as PO.\n", dat->path);
dat->secmap = PO;
} else {
INFO("Opening %s as DO.\n", dat->path);
dat->secmap = DO;
}
explodeDsk(dat->buf, dat->realbuf, dat->secmap);
return (DiskFormatDesc){
.privdat = dat,
.spin = spin,
.read_byte = read_byte,
.write_byte = write_byte,
.eject = eject,
};
}