diff --git a/apple2Setup.go b/apple2Setup.go index 735b31e..ed802f4 100644 --- a/apple2Setup.go +++ b/apple2Setup.go @@ -105,7 +105,9 @@ func (a *Apple2) AddDisk2(slot int, diskRomFile string, diskImage string) error a.insertCard(&c, slot) if diskImage != "" { - diskette, err := loadDisquette(diskImage) + //diskette, err := loadDisquette(diskImage) + //diskette, err := loadDisquetteTimed(diskImage) + diskette, err := loadDisquetteWoz(diskImage) if err != nil { return err } diff --git a/apple2main.go b/apple2main.go index f55162c..7c39c32 100644 --- a/apple2main.go +++ b/apple2main.go @@ -104,11 +104,11 @@ func MainApple() *Apple2 { flag.Parse() if *wozImage != "" { - d, err := loadDisquetteWoz(*wozImage) + f, err := loadFileWoz(*wozImage) if err != nil { panic(err) } - d.dump() + f.dump() panic("Woz loaded") } diff --git a/cardDisk2.go b/cardDisk2.go index d2e8555..4ea007b 100644 --- a/cardDisk2.go +++ b/cardDisk2.go @@ -168,6 +168,10 @@ func (c *cardDisk2) processQ6Q7(in uint8) { c.dataLatch = in } } + + if c.dataLatch >= 0x80 { + //fmt.Printf("Datalacth: 0x%.2x in cycle %v\n", c.dataLatch, c.a.cpu.GetCycles()) + } } /* diff --git a/diskette16sector.go b/diskette16sector.go index 9eb50c8..9d2fc16 100644 --- a/diskette16sector.go +++ b/diskette16sector.go @@ -1,67 +1,25 @@ package apple2 -import ( - "errors" - "os" -) - /* See: "Beneath Apple DOS" https://fabiensanglard.net/fd_proxy/prince_of_persia/Beneath%20Apple%20DOS.pdf https://github.com/TomHarte/CLK/wiki/Apple-GCR-disk-encoding */ -const ( - numberOfTracks = 35 - numberOfSectors = 16 - bytesPerSector = 256 - bytesPerTrack = numberOfSectors * bytesPerSector - nibBytesPerTrack = 6656 - nibImageSize = numberOfTracks * nibBytesPerTrack - dskImageSize = numberOfTracks * numberOfSectors * bytesPerSector - defaultVolumeTag = 254 - cyclesPerBit = 4 -) - type diskette16sector struct { - track [numberOfTracks][]byte - timeBased bool - // Not time based implementation - position int // For not time based implemenation - // Time based implementation, expermiental - cycleOn uint64 // Cycle when the disk was last turned on + nib *fileNib + position int } func (d *diskette16sector) powerOn(cycle uint64) { - d.cycleOn = cycle + // Not used } func (d *diskette16sector) powerOff(_ uint64) { - // Not needed -} - -func (d *diskette16sector) getBitPositionInTrack(cycle uint64) int { - // Calculate how long the disk has been spinning. We move one bit every 4 cycles. - // In this implementation we don't take into account hot long the motor takes to be at full speed. - cycles := cycle - d.cycleOn - position := cycles / cyclesPerBit - return int(position % (8 * nibBytesPerTrack)) // Ignore full turns + // Not used } func (d *diskette16sector) read(quarterTrack int, cycle uint64) uint8 { - track := d.track[quarterTrack/stepsPerTrack] - if d.timeBased { - bitPosition := d.getBitPositionInTrack(cycle) - bytePosition := bitPosition / 8 - shift := uint(bitPosition % 8) - if shift == 1 { - // We continue having the previous data for a little longer - shift = 0 - } - value := track[bytePosition] - value >>= shift - //fmt.Printf("%v, %v, %v, %x\n", bitPosition, shift, bytePosition, uint8(data)) - return value - } + track := d.nib.track[quarterTrack/stepsPerTrack] value := track[d.position] d.position = (d.position + 1) % nibBytesPerTrack //fmt.Printf("%v, %v, %v, %x\n", 0, 0, d.position, uint8(value)) @@ -69,155 +27,18 @@ func (d *diskette16sector) read(quarterTrack int, cycle uint64) uint8 { } func (d *diskette16sector) write(quarterTrack int, value uint8, _ uint64) { - if d.timeBased { - panic("Write not implmented on time based disk implementation") - } track := quarterTrack / stepsPerTrack - d.track[track][d.position] = value + d.nib.track[track][d.position] = value d.position = (d.position + 1) % nibBytesPerTrack } -func loadDisquette(filename string) (*diskette16sector, error) { - var d diskette16sector +func loadDisquette(filename string) (*diskette16sectorTimed, error) { + var d diskette16sectorTimed - // Experimental - d.timeBased = true - - data, err := loadResource(filename) + f, err := loadNibOrDsk(filename) if err != nil { return nil, err } - size := len(data) - - if size == nibImageSize { - // Load file already in nib format - for i := 0; i < numberOfTracks; i++ { - d.track[i] = data[nibBytesPerTrack*i : nibBytesPerTrack*(i+1)] - } - } else if size == dskImageSize { - // Convert to nib - for i := 0; i < numberOfTracks; i++ { - trackData := data[i*bytesPerTrack : (i+1)*bytesPerTrack] - d.track[i] = nibEncodeTrack(trackData, defaultVolumeTag, byte(i)) - } - } else { - return nil, errors.New("Invalid disk size") - } - + d.nib = f return &d, nil } - -func (d *diskette16sector) saveNib(filename string) error { - f, err := os.Create(filename) - if err != nil { - return err - } - defer f.Close() - - for _, v := range d.track { - _, err := f.Write(v) - if err != nil { - return err - } - } - - return nil -} - -var dos33SectorsLogicOrder = [16]int{ - 0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4, - 0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF, -} - -var sixAndTwoTranslateTable = [0x40]byte{ - 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, -} - -const ( - gap1Len = 48 - gap2Len = 5 - primaryBufferSize = bytesPerSector - secondaryBufferSize = bytesPerSector/3 + 1 -) - -func oddEvenEncodeByte(b byte) []byte { - /* - A byte is encoded in two bytes to make sure the bytes start with 1 and - does not have two consecutive zeros. - Data byte: D7-D6-D5-D4-D3-D2-D1-D0 - resutl[0]: 1-D7- 1-D5- 1-D3-1 -D1 - resutl[1]: 1-D6- 1-D4- 1-D2-1 -D0 - */ - e := make([]byte, 2) - e[0] = ((b >> 1) & 0x55) | 0xaa - e[1] = (b & 0x55) | 0xaa - return e -} - -func nibEncodeTrack(data []byte, volume byte, track byte) []byte { - b := make([]byte, 0, nibBytesPerTrack) // Buffer slice with enough capacity - // Initialize gaps to be copied for each sector - gap1 := make([]byte, gap1Len) - for i := range gap1 { - gap1[i] = 0xff - } - gap2 := make([]byte, gap2Len) - for i := range gap2 { - gap2[i] = 0xff - } - for physicalSector := byte(0); physicalSector < numberOfSectors; physicalSector++ { - /* On the DSK file the sectors are in DOS3.3 logical order - but on the physical encoded track as well as in the nib - files they are in phisical order. - */ - logicalSector := dos33SectorsLogicOrder[physicalSector] - sectorData := data[logicalSector*bytesPerSector : (logicalSector+1)*bytesPerSector] - - // 6and2 prenibbilizing. - primaryBuffer := make([]byte, primaryBufferSize) - secondaryBuffer := make([]byte, secondaryBufferSize) - for i, v := range sectorData { - // Primary buffer is easy: the 6 MSB - primaryBuffer[i] = v >> 2 - // Secondary buffer: the 2 LSB reversed, shifted and in their place - shift := uint((i / secondaryBufferSize) * 2) - bit0 := ((v & 0x01) << 1) << shift - bit1 := ((v & 0x02) >> 1) << shift - position := i % secondaryBufferSize - secondaryBuffer[position] |= bit0 | bit1 - } - - // Render sector - // Address field - b = append(b, gap1...) - b = append(b, 0xd5, 0xaa, 0x96) // Address prolog - b = append(b, oddEvenEncodeByte(volume)...) // 4-4 encoded volume - b = append(b, oddEvenEncodeByte(track)...) // 4-4 encoded track - b = append(b, oddEvenEncodeByte(physicalSector)...) // 4-4 encoded sector - b = append(b, oddEvenEncodeByte(volume^track^physicalSector)...) // Checksum - b = append(b, 0xde, 0xaa, 0xeb) // Epilog - // Data field - b = append(b, gap2...) - b = append(b, 0xd5, 0xaa, 0xad) // Data prolog - prevV := byte(0) - for _, v := range secondaryBuffer { - b = append(b, sixAndTwoTranslateTable[v^prevV]) - prevV = v - } - for _, v := range primaryBuffer { - b = append(b, sixAndTwoTranslateTable[v^prevV]) - prevV = v - } - b = append(b, sixAndTwoTranslateTable[prevV]) // Checksum - b = append(b, 0xde, 0xaa, 0xeb) // Data epilog - } - - return b -} diff --git a/diskette16sectorTimed.go b/diskette16sectorTimed.go new file mode 100644 index 0000000..c659c7d --- /dev/null +++ b/diskette16sectorTimed.go @@ -0,0 +1,52 @@ +package apple2 + +type diskette16sectorTimed struct { + nib *fileNib + cycleOn uint64 // Cycle when the disk was last turned on +} + +func (d *diskette16sectorTimed) powerOn(cycle uint64) { + d.cycleOn = cycle +} +func (d *diskette16sectorTimed) powerOff(_ uint64) { + // Not needed +} + +func (d *diskette16sectorTimed) getBitPositionInTrack(cycle uint64) int { + // Calculate how long the disk has been spinning. We move one bit every 4 cycles. + // In this implementation we don't take into account how long the motor takes to be at full speed. + cycles := cycle - d.cycleOn + position := cycles / cyclesPerBit + return int(position % (8 * nibBytesPerTrack)) // Ignore full turns +} + +func (d *diskette16sectorTimed) read(quarterTrack int, cycle uint64) uint8 { + track := d.nib.track[quarterTrack/stepsPerTrack] + bitPosition := d.getBitPositionInTrack(cycle) + bytePosition := bitPosition / 8 + shift := uint(bitPosition % 8) + if shift == 1 { + // We continue having the unshifted byte for a little longer (4 cycles) + shift = 0 + } + value := track[bytePosition] + value >>= shift + //fmt.Printf("%v, %v, %v, %x\n", bitPosition, shift, bytePosition, uint8(value)) + return value +} + +func (d *diskette16sectorTimed) write(quarterTrack int, value uint8, _ uint64) { + panic("Write not implemented on time based disk implementation") +} + +func loadDisquetteTimed(filename string) (*diskette16sectorTimed, error) { + var d diskette16sectorTimed + + f, err := loadNibOrDsk(filename) + if err != nil { + return nil, err + } + d.nib = f + + return &d, nil +} diff --git a/disketteWoz.go b/disketteWoz.go index 817e389..a3f9279 100644 --- a/disketteWoz.go +++ b/disketteWoz.go @@ -1,218 +1,74 @@ package apple2 -import ( - "bytes" - "encoding/binary" - "errors" - "fmt" - "strings" -) - -/* -See: - https://applesaucefdc.com/woz/ -*/ - type disketteWoz struct { - version int - info woz2Info - trackMap []uint8 - tracks [wozMaxTrack]disketteTrackWoz - meta map[string]string + data *fileWoz + cycleOn uint64 // Cycle when the disk was last turned on + turning bool + + latch uint8 + position uint32 + cycle uint64 + trackSize uint32 + + visibleLatch uint8 + visibleLatchCountDown int8 // The visible latch stores a valid latch reading for 2 bit timings } -type disketteTrackWoz struct { - bitCount uint32 - data []uint8 +func (d *disketteWoz) powerOn(cycle uint64) { + d.turning = true + d.cycleOn = cycle } -// Structures from the WOZ Disk Image Reference for deserialization -type wozChunkHeader struct { - ID [4]byte - Size uint32 +func (d *disketteWoz) powerOff(_ uint64) { + d.turning = false } -type woz1Info struct { - Version uint8 // 2 - DiskType uint8 - WriteProtected uint8 - Synchronized uint8 - Cleaned uint8 - Creator [32]byte +func (d *disketteWoz) read(quarterTrack int, cycle uint64) uint8 { + // Count cycles to know how many bits have been read + cycles := cycle - d.cycle + deltaBits := cycles / cyclesPerBit // TODO: Use Woz optimal bit timing + + // Process bits from woz + // TODO: avoid processing too many bits if delta is big + for i := uint64(0); i < deltaBits; i++ { + d.position++ + bit := d.data.getBit(d.position, quarterTrack) + d.latch = (d.latch << 1) + bit + if d.latch >= 0x80 { + // Valid byte, store value a bit longer and clear the internal latch + //fmt.Printf("Valid 0x%.2x\n", d.latch) + d.visibleLatch = d.latch + d.visibleLatchCountDown = 1 + d.latch = 0 + } else if d.visibleLatchCountDown > 0 { + // Continue showing the valid byte + d.visibleLatchCountDown-- + } else { + // The valid byte is lost, show the internal latch + d.visibleLatch = d.latch + } + } + + //fmt.Printf("Visible: 0x%.2x, latch: 0x%.2x, bits: %v, cycles: %v\n", d.visibleLatch, d.latch, deltaBits, cycle-d.cycle) + + // Update the internal last cycle without losing the remainder not processed + d.cycle += deltaBits * cyclesPerBit + + return d.visibleLatch } -type woz2Info struct { - woz1Info - DiskSides uint8 - BootSectorFormat uint8 - OptimalBitTiming uint8 - CompatibleHardware uint16 - RequiredRAM uint16 - LargestTrack uint16 +func (d *disketteWoz) write(quarterTrack int, value uint8, _ uint64) { + panic("Write not implemented on woz disk implementation") } -type woz1TrackFooter struct { - BytesUsed uint16 - BitCount uint16 - SplicePoint uint16 - SpliceNibble uint8 - SpliceBitCount uint8 - Reserved uint16 -} - -type woz2TrackHeader struct { - StartingBlock uint16 - BlockCount uint16 - BitCount uint32 -} - -const ( - wozFirstChunkPos = 12 - wozChunkHeaderLen = 8 - wozMaxTrack = 160 - woz1TrackDataSize = 6656 - woz1TrackFooterOffset = 6646 - woz2TrackBlockSize = 512 - woz2FirstTrackBlock = 3 // The bits on the TRKS block start on 3*512 - woz2TrackBitsOffset = 1280 -) - -var headerWoz1 = []uint8{0x57, 0x4f, 0x5A, 0x31, 0xFF, 0x0A, 0x0D, 0x0A} -var headerWoz2 = []uint8{0x57, 0x4f, 0x5A, 0x32, 0xFF, 0x0A, 0x0D, 0x0A} - func loadDisquetteWoz(filename string) (*disketteWoz, error) { var d disketteWoz - data, err := loadResource(filename) + f, err := loadFileWoz(filename) if err != nil { return nil, err } - - // Verify header. Note, the CRC is not verified - header := data[:len(headerWoz2)] - if bytes.Equal(headerWoz1, header) { - d.version = 1 - } else if bytes.Equal(headerWoz2, header) { - d.version = 2 - } else { - return nil, errors.New("Invalid WOZ header") - } - - // Extract the chunks - i := wozFirstChunkPos - var chunkHeader wozChunkHeader - chunks := make(map[string][]uint8) - for i+wozChunkHeaderLen < len(data) { - binary.Read(bytes.NewReader(data[i:]), binary.LittleEndian, &chunkHeader) - - i += wozChunkHeaderLen - iNext := i + int(chunkHeader.Size) - if i == iNext || iNext > len(data) { - return nil, errors.New("Invalid chunk in WOZ file") - } - - id := string(chunkHeader.ID[:]) - chunks[id] = data[i:iNext] - i = iNext - - //fmt.Printf("Chunk %v, size %v - %v\n", id, chunkHeader.Size, len(chunks[id])) - } - - // Read the INFO chunk - infoData, ok := chunks["INFO"] - if !ok { - return nil, errors.New("Chunk INFO missing from WOZ file") - } - switch d.version { - case 1: - binary.Read(bytes.NewReader(infoData), binary.LittleEndian, &d.info.woz1Info) - case 2: - binary.Read(bytes.NewReader(infoData), binary.LittleEndian, &d.info) - } - - // Read the optional META chunk - metaData, ok := chunks["META"] - if ok { - d.meta = make(map[string]string) - text := string(metaData) - entries := strings.Split(text, "\n") - for _, entry := range entries { - parts := strings.Split(entry, "\t") - if len(parts) >= 2 { - d.meta[parts[0]] = parts[1] - //fmt.Printf("*** %v: %v\n", parts[0], parts[1]) - } - } - } - - // Read the TMAP chunk - trackMap, ok := chunks["TMAP"] - if !ok { - return nil, errors.New("Chunk INFO missing from WOZ file") - } - d.trackMap = trackMap - - // Read the TRKS chunk - tracksData, ok := chunks["TRKS"] - if d.version == 1 { - i := 0 - track := 0 - for i+woz1TrackDataSize <= len(tracksData) { - var trackFooter woz1TrackFooter - binary.Read(bytes.NewReader(tracksData[i+woz1TrackFooterOffset:]), binary.LittleEndian, &trackFooter) - d.tracks[track].bitCount = uint32(trackFooter.BitCount) - d.tracks[track].data = tracksData[i : i+int(trackFooter.BytesUsed)] - i += woz1TrackDataSize - track++ - } - } else if d.version == 2 { - reader := bytes.NewReader(tracksData) - for i := 0; i < wozMaxTrack; i++ { - var trackHeader woz2TrackHeader - binary.Read(reader, binary.LittleEndian, &trackHeader) - if trackHeader.BitCount != 0 { - d.tracks[i].bitCount = trackHeader.BitCount - - dataPos := woz2TrackBlockSize*(int(trackHeader.StartingBlock)-woz2FirstTrackBlock) + woz2TrackBitsOffset - dataSize := woz2TrackBlockSize * int(trackHeader.BlockCount) - //fmt.Printf("@%v %v:%v (%v) of %v\n", trackHeader.StartingBlock, dataPos, dataPos+dataSize, dataSize, len(tracksData)) - d.tracks[i].data = tracksData[dataPos : dataPos+dataSize] - } - } - } else { - return nil, errors.New("Woz version not supported") - } + d.data = f return &d, nil } - -func (d *disketteWoz) dump() { - fmt.Printf("Woz image:\n") - fmt.Printf(" Version: %v\n", d.info.Version) - fmt.Printf(" Disk type: %v\n", d.info.DiskType) - fmt.Printf(" Write protected: %v\n", d.info.WriteProtected) - fmt.Printf(" Synchronized: %v\n", d.info.Synchronized) - fmt.Printf(" Cleaned: %v\n", d.info.Cleaned) - fmt.Printf(" Creator: %v\n", string(d.info.Creator[:])) - if d.info.Version >= 2 { - fmt.Printf(" Disk sides: %v\n", d.info.DiskSides) - fmt.Printf(" Boot sector format: %v\n", d.info.BootSectorFormat) - fmt.Printf(" Optimal bit timing: %v ns\n", 125*int(d.info.OptimalBitTiming)) - fmt.Printf(" Compatible hardware: 0x%x\n", d.info.CompatibleHardware) - fmt.Printf(" Required RAM: %vKB\n", d.info.RequiredRAM) - fmt.Printf(" Largest track: %v blocks\n", d.info.LargestTrack) - } - if d.meta != nil { - fmt.Printf(" Metadata:\n") - for k, v := range d.meta { - fmt.Printf(" %v: %v\n", k, v) - } - } - fmt.Printf(" Tracks:\n") - for i, track := range d.trackMap { - if track != 255 { - fmt.Printf(" Track %.2f: %v (%v bits, %v bytes)\n", - 0.25*float32(i), track, d.tracks[track].bitCount, len(d.tracks[track].data)) - } - } -} diff --git a/fileNib.go b/fileNib.go new file mode 100644 index 0000000..af027fe --- /dev/null +++ b/fileNib.go @@ -0,0 +1,170 @@ +package apple2 + +import ( + "errors" + "os" +) + +/* +See: + "Beneath Apple DOS" https://fabiensanglard.net/fd_proxy/prince_of_persia/Beneath%20Apple%20DOS.pdf + https://github.com/TomHarte/CLK/wiki/Apple-GCR-disk-encoding +*/ + +const ( + numberOfTracks = 35 + numberOfSectors = 16 + bytesPerSector = 256 + bytesPerTrack = numberOfSectors * bytesPerSector + nibBytesPerTrack = 6656 + nibImageSize = numberOfTracks * nibBytesPerTrack + dskImageSize = numberOfTracks * numberOfSectors * bytesPerSector + defaultVolumeTag = 254 + cyclesPerBit = 4 +) + +type fileNib struct { + track [numberOfTracks][]byte +} + +func loadNibOrDsk(filename string) (*fileNib, error) { + var f fileNib + + data, err := loadResource(filename) + if err != nil { + return nil, err + } + size := len(data) + + if size == nibImageSize { + // Load file already in nib format + for i := 0; i < numberOfTracks; i++ { + f.track[i] = data[nibBytesPerTrack*i : nibBytesPerTrack*(i+1)] + } + } else if size == dskImageSize { + // Convert to nib + for i := 0; i < numberOfTracks; i++ { + trackData := data[i*bytesPerTrack : (i+1)*bytesPerTrack] + f.track[i] = nibEncodeTrack(trackData, defaultVolumeTag, byte(i)) + } + } else { + return nil, errors.New("Invalid disk size") + } + + return &f, nil +} + +func (f *fileNib) saveNib(filename string) error { + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + + for _, v := range f.track { + _, err := file.Write(v) + if err != nil { + return err + } + } + + return nil +} + +var dos33SectorsLogicOrder = [16]int{ + 0x0, 0x7, 0xE, 0x6, 0xD, 0x5, 0xC, 0x4, + 0xB, 0x3, 0xA, 0x2, 0x9, 0x1, 0x8, 0xF, +} + +var sixAndTwoTranslateTable = [0x40]byte{ + 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, +} + +const ( + gap1Len = 48 + gap2Len = 5 + primaryBufferSize = bytesPerSector + secondaryBufferSize = bytesPerSector/3 + 1 +) + +func oddEvenEncodeByte(b byte) []byte { + /* + A byte is encoded in two bytes to make sure the bytes start with 1 and + does not have two consecutive zeros. + Data byte: D7-D6-D5-D4-D3-D2-D1-D0 + resutl[0]: 1-D7- 1-D5- 1-D3-1 -D1 + resutl[1]: 1-D6- 1-D4- 1-D2-1 -D0 + */ + e := make([]byte, 2) + e[0] = ((b >> 1) & 0x55) | 0xaa + e[1] = (b & 0x55) | 0xaa + return e +} + +func nibEncodeTrack(data []byte, volume byte, track byte) []byte { + b := make([]byte, 0, nibBytesPerTrack) // Buffer slice with enough capacity + // Initialize gaps to be copied for each sector + gap1 := make([]byte, gap1Len) + for i := range gap1 { + gap1[i] = 0xff + } + gap2 := make([]byte, gap2Len) + for i := range gap2 { + gap2[i] = 0xff + } + for physicalSector := byte(0); physicalSector < numberOfSectors; physicalSector++ { + /* On the DSK file the sectors are in DOS3.3 logical order + but on the physical encoded track as well as in the nib + files they are in phisical order. + */ + logicalSector := dos33SectorsLogicOrder[physicalSector] + sectorData := data[logicalSector*bytesPerSector : (logicalSector+1)*bytesPerSector] + + // 6and2 prenibbilizing. + primaryBuffer := make([]byte, primaryBufferSize) + secondaryBuffer := make([]byte, secondaryBufferSize) + for i, v := range sectorData { + // Primary buffer is easy: the 6 MSB + primaryBuffer[i] = v >> 2 + // Secondary buffer: the 2 LSB reversed, shifted and in their place + shift := uint((i / secondaryBufferSize) * 2) + bit0 := ((v & 0x01) << 1) << shift + bit1 := ((v & 0x02) >> 1) << shift + position := i % secondaryBufferSize + secondaryBuffer[position] |= bit0 | bit1 + } + + // Render sector + // Address field + b = append(b, gap1...) + b = append(b, 0xd5, 0xaa, 0x96) // Address prolog + b = append(b, oddEvenEncodeByte(volume)...) // 4-4 encoded volume + b = append(b, oddEvenEncodeByte(track)...) // 4-4 encoded track + b = append(b, oddEvenEncodeByte(physicalSector)...) // 4-4 encoded sector + b = append(b, oddEvenEncodeByte(volume^track^physicalSector)...) // Checksum + b = append(b, 0xde, 0xaa, 0xeb) // Epilog + // Data field + b = append(b, gap2...) + b = append(b, 0xd5, 0xaa, 0xad) // Data prolog + prevV := byte(0) + for _, v := range secondaryBuffer { + b = append(b, sixAndTwoTranslateTable[v^prevV]) + prevV = v + } + for _, v := range primaryBuffer { + b = append(b, sixAndTwoTranslateTable[v^prevV]) + prevV = v + } + b = append(b, sixAndTwoTranslateTable[prevV]) // Checksum + b = append(b, 0xde, 0xaa, 0xeb) // Data epilog + } + + return b +} diff --git a/fileWoz.go b/fileWoz.go new file mode 100644 index 0000000..f9b56af --- /dev/null +++ b/fileWoz.go @@ -0,0 +1,246 @@ +package apple2 + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "strings" +) + +/* +See: + https://applesaucefdc.com/woz/ +*/ + +type fileWoz struct { + version int + info woz2Info + trackMap []uint8 + tracks [wozMaxTrack]disketteTrackWoz + meta map[string]string +} + +type disketteTrackWoz struct { + bitCount uint32 + data []uint8 +} + +// Structures from the WOZ Disk Image Reference for deserialization +type wozChunkHeader struct { + ID [4]byte + Size uint32 +} + +type woz1Info struct { + Version uint8 + DiskType uint8 + WriteProtected uint8 + Synchronized uint8 + Cleaned uint8 + Creator [32]byte +} + +type woz2Info struct { + woz1Info + DiskSides uint8 + BootSectorFormat uint8 + OptimalBitTiming uint8 + CompatibleHardware uint16 + RequiredRAM uint16 + LargestTrack uint16 +} + +type woz1TrackFooter struct { + BytesUsed uint16 + BitCount uint16 + SplicePoint uint16 + SpliceNibble uint8 + SpliceBitCount uint8 + Reserved uint16 +} + +type woz2TrackHeader struct { + StartingBlock uint16 + BlockCount uint16 + BitCount uint32 +} + +const ( + wozFirstChunkPos = 12 + wozChunkHeaderLen = 8 + wozMaxTrack = 160 + woz1TrackDataSize = 6656 + woz1TrackFooterOffset = 6646 + woz2TrackBlockSize = 512 + woz2FirstTrackBlock = 3 // The bits on the TRKS block start on 3*512 + woz2TrackBitsOffset = 1280 +) + +var headerWoz1 = []uint8{0x57, 0x4f, 0x5A, 0x31, 0xFF, 0x0A, 0x0D, 0x0A} +var headerWoz2 = []uint8{0x57, 0x4f, 0x5A, 0x32, 0xFF, 0x0A, 0x0D, 0x0A} + +func (f *fileWoz) getBit(position uint32, quarterTrack int) uint8 { + trackWoz := f.tracks[f.trackMap[quarterTrack]] + position %= trackWoz.bitCount + return trackWoz.data[position/8] >> (7 - position%8) & 1 +} + +func loadFileWoz(filename string) (*fileWoz, error) { + var f fileWoz + + data, err := loadResource(filename) + if err != nil { + return nil, err + } + + // Verify header. Note, the CRC is not verified + header := data[:len(headerWoz2)] + if bytes.Equal(headerWoz1, header) { + f.version = 1 + } else if bytes.Equal(headerWoz2, header) { + f.version = 2 + } else { + return nil, errors.New("Invalid WOZ header") + } + + // Extract the chunks + i := wozFirstChunkPos + var chunkHeader wozChunkHeader + chunks := make(map[string][]uint8) + for i+wozChunkHeaderLen < len(data) { + binary.Read(bytes.NewReader(data[i:]), binary.LittleEndian, &chunkHeader) + + i += wozChunkHeaderLen + iNext := i + int(chunkHeader.Size) + if i == iNext || iNext > len(data) { + return nil, errors.New("Invalid chunk in WOZ file") + } + + id := string(chunkHeader.ID[:]) + chunks[id] = data[i:iNext] + i = iNext + + //fmt.Printf("Chunk %v, size %v - %v\n", id, chunkHeader.Size, len(chunks[id])) + } + + // Read the INFO chunk + infoData, ok := chunks["INFO"] + if !ok { + return nil, errors.New("Chunk INFO missing from WOZ file") + } + switch f.version { + case 1: + binary.Read(bytes.NewReader(infoData), binary.LittleEndian, &f.info.woz1Info) + case 2: + binary.Read(bytes.NewReader(infoData), binary.LittleEndian, &f.info) + } + + // Read the optional META chunk + metaData, ok := chunks["META"] + if ok { + f.meta = make(map[string]string) + text := string(metaData) + entries := strings.Split(text, "\n") + for _, entry := range entries { + parts := strings.Split(entry, "\t") + if len(parts) >= 2 { + f.meta[parts[0]] = parts[1] + //fmt.Printf("*** %v: %v\n", parts[0], parts[1]) + } + } + } + + // Read the TMAP chunk + trackMap, ok := chunks["TMAP"] + if !ok { + return nil, errors.New("Chunk TMAP missing from WOZ file") + } + f.trackMap = trackMap + + // Read the TRKS chunk + tracksData, ok := chunks["TRKS"] + if !ok { + return nil, errors.New("Chunk TRKS missing from WOZ file") + } + if f.version == 1 { + i := 0 + track := 0 + for i+woz1TrackDataSize <= len(tracksData) { + var trackFooter woz1TrackFooter + binary.Read(bytes.NewReader(tracksData[i+woz1TrackFooterOffset:]), binary.LittleEndian, &trackFooter) + f.tracks[track].bitCount = uint32(trackFooter.BitCount) + f.tracks[track].data = tracksData[i : i+int(trackFooter.BytesUsed)] + i += woz1TrackDataSize + track++ + } + } else if f.version == 2 { + reader := bytes.NewReader(tracksData) + for i := 0; i < wozMaxTrack; i++ { + var trackHeader woz2TrackHeader + binary.Read(reader, binary.LittleEndian, &trackHeader) + if trackHeader.BitCount != 0 { + f.tracks[i].bitCount = trackHeader.BitCount + + dataPos := woz2TrackBlockSize*(int(trackHeader.StartingBlock)-woz2FirstTrackBlock) + woz2TrackBitsOffset + dataSize := woz2TrackBlockSize * int(trackHeader.BlockCount) + //fmt.Printf("@%v %v:%v (%v) of %v\n", trackHeader.StartingBlock, dataPos, dataPos+dataSize, dataSize, len(tracksData)) + f.tracks[i].data = tracksData[dataPos : dataPos+dataSize] + } + } + } else { + return nil, errors.New("Woz version not supported") + } + + return &f, nil +} + +func (f *fileWoz) dumpTrackAsNib(quarterTrack int) []uint8 { + trackWoz := f.tracks[f.trackMap[quarterTrack]] + out := make([]uint8, 0, trackWoz.bitCount/8) + latch := uint8(0) + for iBit := uint32(0); iBit < trackWoz.bitCount; iBit++ { + bit := trackWoz.data[iBit/8] >> (7 - iBit%8) & 1 + latch = (latch << 1) + bit + if latch >= 0x80 { + // Valid reading + out = append(out, latch) + latch = 0 + } + } + return out +} + +func (f *fileWoz) dump() { + fmt.Printf("Woz image:\n") + fmt.Printf(" Version: %v\n", f.info.Version) + fmt.Printf(" Disk type: %v\n", f.info.DiskType) + fmt.Printf(" Write protected: %v\n", f.info.WriteProtected) + fmt.Printf(" Synchronized: %v\n", f.info.Synchronized) + fmt.Printf(" Cleaned: %v\n", f.info.Cleaned) + fmt.Printf(" Creator: %v\n", string(f.info.Creator[:])) + if f.info.Version >= 2 { + fmt.Printf(" Disk sides: %v\n", f.info.DiskSides) + fmt.Printf(" Boot sector format: %v\n", f.info.BootSectorFormat) + fmt.Printf(" Optimal bit timing: %v ns\n", 125*int(f.info.OptimalBitTiming)) + fmt.Printf(" Compatible hardware: 0x%x\n", f.info.CompatibleHardware) + fmt.Printf(" Required RAM: %vKB\n", f.info.RequiredRAM) + fmt.Printf(" Largest track: %v blocks\n", f.info.LargestTrack) + } + if f.meta != nil { + fmt.Printf(" Metadata:\n") + for k, v := range f.meta { + fmt.Printf(" %v: %v\n", k, v) + } + } + fmt.Printf(" Tracks:\n") + for i, track := range f.trackMap { + if track != 255 { + fmt.Printf(" Track %.2f: %v (%v bits, %v bytes)\n", + 0.25*float32(i), track, f.tracks[track].bitCount, len(f.tracks[track].data)) + } + } + + //nibs := f.dumpTrackAsNib(0) + //fmt.Printf(" Zero track: {%v} %x\n", len(nibs), nibs) +}