----SpiraDisk Format Reference---- Reverse engineered by Matthew D'Asaro and qkumba primarily from analysis of "The Official FROGGER™ - by SEGA® (Sierra ON-LINE INC)" but also from examining Lunar Leepers, MAZE CRAZE CONSTRUCTION SET, Jawbreaker ][, and Pest Patrol. Note: This document is intended to assist with decoding SpiraDisc formatted floppy disks, but is not complete enough to allow for encoding one from scratch. It also does not cover how the SpiraDisc code works, simply how it is written to the disc. Basic Format: Track 0 is always a "normal" (non-spiraling) track that contains 15 sectors. Those sectors are: - 1x boot sector (contains the magic D5 AA 96) which is decoded and checksummed in the normal 6-and-2 16-sector way except that the address field is shortened. - 3x 4-and-4 encoded sectors - see below - 11x SpiraDisc format - see below - Following the 4x4 sectors and following the boot sector are sections of filler designed to confuse bit copiers. This can be safely ignored. After track 0, there may be tracks which contain SpiraDisc format sectors but which do not spiral. After Track 0 and any optional non-spiraling tracks, the data is recorded in a spiral with a pattern that, starting on a whole-number track, goes: 4 sectors, step 1/4 track, 4 sectors, step 1/4 track, etc. Each set of 4-sectors will be referred to as a "quadrant". In FROGGER, for example, this region spans track 1.00 to track 8.75 which leads to a total number of sectors of 8 tracks (9-1) * 4 quadrants / track * 4 sectors / quadrant = 128 SpiraDisc sectors. Note that there may be multiple spirals on the disk with space between them, but each spiral has to start on its own whole-number track. 4x4 Sector Format: ------SYNC FIELD------ -SEC ID- ------SYNC FIELD------ FF 99 9C --1536 NIBBLES DATA-- D5 -CKSUM- ... FF10 FF10 FF10 FF9 97 BB EA ...FF10 FF10 FF10 FF10 FF 99 9C DD DD DD ... DD DD DD D5 CK1 CK2 Decoding 4x4 sector data: - Read 4 nibbles (n0,n1, where n0 comes first on the sector) - This decodes to one byte as: decoded_byte = ((n0 << 1) | 0x01) & n1 Checksumming 4x4 Format Sectors: Thanks to some excellent work by qkumba, we now know how the checksum works. "EOR all of the 768 decoded data bytes together, store at X, EOR X with CK1 (no store) and AND with #$55. Should be zero. Read CK2, ASL, EOR with X, AND with #$AA. Should be zero." This process will produce 768 bytes of decoded data which is three 256-byte sectors. SpiraDisc Sector Format: ----SYNC FIELD---- -SEC ID- ----------------------SYNC FIELD---------------------- 9A --344 NIBBLES DATA-- 9B -----CKSUM----- FF10 FF10 FF10 FF9 97 XX EA FF10 FF10 FF10 FF10 FF10 FF10 FF10 FF10 FF10 FF10 FF10 9A DD DD DD .. DD DD DD 9B CK1 CK2 CK2 CK2 Between sectors in the same quadrant is a single nibble worth of "garbage" from when writing stopped and restarted. Between quadrants there is a gap of about 900 nibbles although this seems to vary a bit. Decoding SpiraDisc sector data: 1) The data is written in a 6-6-6-6 format where each nibble encodes six bits of useful data with the other two bits present to assure that there are not more than two zeros in a row and that each nibble starts with a 1. 2) Each sector contains 344 nibbles of data, or 43 groups of four nibbles. Each group will decode to three bytes (6-bits per nibble x 4 nibbles per group = 24 bits or three bytes per group). This means that each sector actually contains 258 bytes of data of which it appears that only 256 are actually decoded and used. 3) The decoding process goes as follows. - Read 4 nibbles (n0..n3, where n0 comes first on the sector) - Look up the six bits that each nibble represents in 4 separate tables, one for each of n0..n3. - Concatenate the four sets of six bits each to form three bytes. See below: n0 n1 n2 n3 aaaaaa bbbbbb cccccc dddddd b2 b1 b0 ddddddcc ccccbbbb bbaaaaaa - Store those three bytes in memory in a rather bizarre pattern: * Store b0 at offset 0xFF * Discard b1 for the first two groups of nibbles read, then starting with the third group store it at offset 0xAB. * Store b2 at offset 0x55. * Decrement (NOT INCREMENT) each offset for each new group of four nibbles read. Checksumming SpiraDisc Format Sectors: There are (at least?) two different versions of the checksum algorithm used in the titles I have found so far. The first, used in the earlier titles (~1981) including Frogger, Lunar Leepers, Pest Patrol, and Jawbreaker ][, is very simple. Each of the raw nibbles is combined together in a running XOR for all raw nibbles in the sector. E.g. n0^n1^n2^n3... The second, used in later titles (~1983) including MAZE CRAZE, is much more complex. First, instead of a running XOR of the raw nibbles, it is a running XOR of the decoded bytes. Note that this includes the two "discarded" bytes. Second, this result is also XORed with a value equal to the quarter-track-number (4x the track number.) E.g. for a sector on track 3.25, this would be 3.25*4 = 13^b0^b1^b2... Important note: For tracks > 14.75, the quarter-track number can't be represented as a single nibble (there are only 64 possible values for a single nibble) so in the one example we have of this (Lunar Leepers) the sector ID starts over at zero for track 16.00 (track 15 is skipped). It is unknown if the quarter-track number used for the sector checksum also starts over or if it is continued (MAZE CRAZE is the only example we have of this alternate checksumming and it has less than 14 tracks). Thus, I recommend that both possibilities be checked when validating a sector on a track >14.75! Update: It appears that the checksum is using the actual quarter-track number as for track 0, which uses a different sector ID system, the checksum passes consistently when 0x00 is used. Once the running XOR is known, thanks to some excellent work by qkumba, we now know how the actual checksum works. "Take the running XOR, store at X, EOR X with CK1 (no store) and AND with #$55. Should be zero. Read CK2, ASL, EOR with X, AND with #$AA. Should be zero." SpiraDisc Sector ID Encoding: 1) Read the ID nibble for each sector on a given quadrant. There are 4 sectors per quadrant, so I will refer to these IDs as ID0-ID3. 2) Decode each nibble using n0decodetable[] to produce DID0 - DID3. Eg: DID0 = n0decodetable[ID0]; DID1 = n0decodetable[ID1]; DID2 = n0decodetable[ID2]; DID3 = n0decodetable[ID3]; 3) To find the track number divide DID0 by 4.0 (as a float). For example: if DID0 = 5, 5/4 = 1.25, track = 1.25. However, due to there only being 64 nibble values, this pattern cannot encode all possible tracks. Thus, after track 14.75, track 15.0-15.75 is skipped (left blank) and the pattern repeats on track 16.00 starting at track ID 0. 4) The values of DID1-DID3 are computed as (note integer math): if((DID0 / 4) % 4 == 0){ DID1 = DID0 + 4; DID2 = DID0 + 8; DID3 = DID0 + 12; } if((DID0 / 4) % 4 == 1){ DID1 = DID0 - 4; DID2 = DID0 + 8; DID3 = DID0 + 4; } if((DID0 / 4) % 4 == 2){ DID1 = DID0 + 4; DID2 = DID0 - 8; DID3 = DID0 - 4; } if((DID0 / 4) % 4 == 3){ DID1 = DID0 - 4; DID2 = DID0 - 8; DID3 = DID0 - 4; } Exceptions: 1) The 11 "SpiraDisc" sectors on track 0 do not follow this ID format. Rather they are always 94, 95, 96, BB, 97, 99, 9A, 9B, 9C, 9D, 9E, 9F in that order, with the boot sector existing between 94 and 95 and the BB sector mark identifying the 4x4 sectors. 2) For tracks that contain SpiraDisc sectors but are recorded in a linear (not spiral) pattern, the first fours sectors IDs on the track will decode to the actual track, but each next set of four sectors will have the track number incremented by four tracks. For example in MAZE CRAZE, the first four sectors of track 1 decode to track 1.00, but the next four decode to track 5.00, the next four track 9.00, and the last four track 13.00. Other Notes: The issue with producing working .woz files from these disks appears to be that the alignment is really critical. I have noticed that it is difficult to get Frogger to consistently boot even on real hardware and the success in doing so appears to depend greatly on the drive being used. I am pretty sure that the issue is that the drives do not always move a perfectly consistent 1/4 track resulting in slight head misalignment when reading the spirals. These disks were mastered on normal Apple II hardware, so the alignment errors would be compounded between the drives they were mastered on and the ones they are read on. In the three copies of Frogger that I have tried to image, track xx.75 seems to be the most difficult to consistently read. The alignment problem is especially bad for imaging disks as compared to booting them because when it is being booted, it can repeatedly try to re-seek and re-read to find a given sector. However, when imaging the disk applesauce, has no knowledge of the format so it can't do this. That said, if applesauce could understand the format, it could read from adjacent quarter tracks to get a successful image even if the alignment is a bit off. Based on this alignment hypothesis, I made the first (that I am aware of anywhere) working .woz image of the original 1981 FROGGER for Apple II+ (NOT IIe) by carefully editing the 2.75, 3.75, etc tracks to correct errors from misalignment. I used the 2.5, 3.5, etc tracks as reference for this process as they were closer to being in proper alignment and thus read correctly. The working image is called "FROGGER - Disk 1, Side A track 2.75 3.75 4.75 5.75 6.75 7.75 repair.woz" and it proves that my alignment hypothesis described above is basically correct. Nibble Decoding Tables: Note that these were "black-box" reverse-engineered by Matthew D'Asaro from FROGGER by comparing the splash-screen image as decoded and stored in memory with its corresponding raw sector data (which spans tracks 1.00 to 2.75). To fill in the remaining missing nibble values, a specially crafted sector was edited onto the disk, the disk was booted, and the sector data was then compared with the decoded data in memory. In the original SpiraDisc, this data is apparently generated algorithmically, but I haven't worked out how that was done. Memory is cheap these days anyway, so here are the tables, ready to be pasted into a c program. unsigned char n0decodetable[256]={0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06,0xFF,0xFF,0x07,0x08,0x09,0x0A,0x0B,0x0C,0xFF,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0xFF,0xFF,0x1B,0x1C,0x1D,0x1E,0x1F,0x20,0xFF,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0xFF,0xFF,0xFF,0xFF,0x28,0x29,0x2A,0x2B,0xFF,0x2C,0x2D,0x2E,0x2F,0x30,0x31,0x32,0xFF,0xFF,0x33,0x34,0x35,0x36,0x37,0x38,0xFF,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F}; unsigned char n1decodetable[256]={0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0x04,0x08,0x0C,0x10,0x14,0x18,0xFF,0xFF,0x1C,0x20,0x24,0x28,0x2C,0x30,0xFF,0x34,0x38,0x3C,0x01,0x05,0x09,0x0D,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x11,0x15,0x19,0x1D,0x21,0x25,0x29,0xFF,0xFF,0x2D,0x31,0x35,0x39,0x3D,0x02,0xFF,0x06,0x0A,0x0E,0x12,0x16,0x1A,0x1E,0xFF,0xFF,0xFF,0xFF,0x22,0x26,0x2A,0x2E,0xFF,0x32,0x36,0x3A,0x3E,0x03,0x07,0x0B,0xFF,0xFF,0x0F,0x13,0x17,0x1B,0x1F,0x23,0xFF,0x27,0x2B,0x2F,0x33,0x37,0x3B,0x3F}; unsigned char n2decodetable[256]={0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0x10,0x20,0x30,0x01,0x11,0x21,0xFF,0xFF,0x31,0x02,0x12,0x22,0x32,0x03,0xFF,0x13,0x23,0x33,0x04,0x14,0x24,0x34,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x05,0x15,0x25,0x35,0x06,0x16,0x26,0xFF,0xFF,0x36,0x07,0x17,0x27,0x37,0x08,0xFF,0x18,0x28,0x38,0x09,0x19,0x29,0x39,0xFF,0xFF,0xFF,0xFF,0x0A,0x1A,0x2A,0x3A,0xFF,0x0B,0x1B,0x2B,0x3B,0x0C,0x1C,0x2C,0xFF,0xFF,0x3C,0x0D,0x1D,0x2D,0x3D,0x0E,0xFF,0x1E,0x2E,0x3E,0x0F,0x1F,0x2F,0x3F}; unsigned char n3decodetable[256]={0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06,0xFF,0xFF,0x07,0x08,0x09,0x0A,0x0B,0x0C,0xFF,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0xFF,0xFF,0x1B,0x1C,0x1D,0x1E,0x1F,0x20,0xFF,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0xFF,0xFF,0xFF,0xFF,0x28,0x29,0x2A,0x2B,0xFF,0x2C,0x2D,0x2E,0x2F,0x30,0x31,0x32,0xFF,0xFF,0x33,0x34,0x35,0x36,0x37,0x38,0xFF,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F};