From 985b36da730b46100b36c1b815b8e2665b7313f0 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Tue, 7 Jan 2020 23:21:32 -0500 Subject: [PATCH 1/8] Starts towards STX support. --- Analyser/Static/StaticAnalyser.cpp | 2 + .../Clock Signal.xcodeproj/project.pbxproj | 6 + OSBindings/Mac/Clock Signal/Info.plist | 1 + Storage/Disk/DiskImage/Formats/STX.cpp | 413 ++++++++++++++++++ Storage/Disk/DiskImage/Formats/STX.hpp | 48 ++ 5 files changed, 470 insertions(+) create mode 100644 Storage/Disk/DiskImage/Formats/STX.cpp create mode 100644 Storage/Disk/DiskImage/Formats/STX.hpp diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp index 4d4801b98..9652c8ae1 100644 --- a/Analyser/Static/StaticAnalyser.cpp +++ b/Analyser/Static/StaticAnalyser.cpp @@ -47,6 +47,7 @@ #include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp" #include "../../Storage/Disk/DiskImage/Formats/SSD.hpp" #include "../../Storage/Disk/DiskImage/Formats/ST.hpp" +#include "../../Storage/Disk/DiskImage/Formats/STX.hpp" #include "../../Storage/Disk/DiskImage/Formats/WOZ.hpp" // Mass Storage Devices (i.e. usually, hard disks) @@ -147,6 +148,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: Format("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SMS Format("ssd", result.disks, Disk::DiskImageHolder, TargetPlatform::Acorn) // SSD Format("st", result.disks, Disk::DiskImageHolder, TargetPlatform::AtariST) // ST + Format("stx", result.disks, Disk::DiskImageHolder, TargetPlatform::AtariST) // STX Format("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore) // TAP (Commodore) Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric) // TAP (Oric) Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX) // TSX diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index b6332489b..a463621cf 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -371,6 +371,7 @@ 4B7A90ED20410A85008514A2 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7A90EC20410A85008514A2 /* StaticAnalyser.cpp */; }; 4B7BA03023C2B19C00B98D9E /* Jasmin.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7BA02E23C2B19B00B98D9E /* Jasmin.cpp */; }; 4B7BA03123C2B19C00B98D9E /* Jasmin.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7BA02E23C2B19B00B98D9E /* Jasmin.cpp */; }; + 4B7BA03423C58B1F00B98D9E /* STX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7BA03323C58B1E00B98D9E /* STX.cpp */; }; 4B7BC7F51F58F27800D1B1B4 /* 6502AllRAM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B6A4C911F58F09E00E3F787 /* 6502AllRAM.cpp */; }; 4B7F188E2154825E00388727 /* MasterSystem.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7F188C2154825D00388727 /* MasterSystem.cpp */; }; 4B7F188F2154825E00388727 /* MasterSystem.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7F188C2154825D00388727 /* MasterSystem.cpp */; }; @@ -1182,6 +1183,8 @@ 4B7A90EC20410A85008514A2 /* StaticAnalyser.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = StaticAnalyser.cpp; sourceTree = ""; }; 4B7BA02E23C2B19B00B98D9E /* Jasmin.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Jasmin.cpp; path = Oric/Jasmin.cpp; sourceTree = ""; }; 4B7BA02F23C2B19B00B98D9E /* Jasmin.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Jasmin.hpp; path = Oric/Jasmin.hpp; sourceTree = ""; }; + 4B7BA03223C58B1E00B98D9E /* STX.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = STX.hpp; sourceTree = ""; }; + 4B7BA03323C58B1E00B98D9E /* STX.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = STX.cpp; sourceTree = ""; }; 4B7F188C2154825D00388727 /* MasterSystem.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MasterSystem.cpp; sourceTree = ""; }; 4B7F188D2154825D00388727 /* MasterSystem.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = MasterSystem.hpp; sourceTree = ""; }; 4B7F1895215486A100388727 /* StaticAnalyser.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = StaticAnalyser.hpp; sourceTree = ""; }; @@ -2269,6 +2272,7 @@ 4B4518971F75FD1B00926311 /* OricMFMDSK.cpp */, 4B4518991F75FD1B00926311 /* SSD.cpp */, 4BE0A3EC237BB170002AB46F /* ST.cpp */, + 4B7BA03323C58B1E00B98D9E /* STX.cpp */, 4B6ED2EE208E2F8A0047B343 /* WOZ.cpp */, 4B45188E1F75FD1B00926311 /* AcornADF.hpp */, 4B0333AE2094081A0050B93D /* AppleDSK.hpp */, @@ -2285,6 +2289,7 @@ 4B4518981F75FD1B00926311 /* OricMFMDSK.hpp */, 4B45189A1F75FD1B00926311 /* SSD.hpp */, 4BE0A3ED237BB170002AB46F /* ST.hpp */, + 4B7BA03223C58B1E00B98D9E /* STX.hpp */, 4B6ED2EF208E2F8A0047B343 /* WOZ.hpp */, 4BFDD7891F7F2DB4008579B9 /* Utility */, ); @@ -4541,6 +4546,7 @@ 4BBC951E1F368D83008F4C34 /* i8272.cpp in Sources */, 4B89449520194CB3007DE474 /* MachineForTarget.cpp in Sources */, 4B4A76301DB1A3FA007AAE2E /* AY38910.cpp in Sources */, + 4B7BA03423C58B1F00B98D9E /* STX.cpp in Sources */, 4B6A4C991F58F09E00E3F787 /* 6502Base.cpp in Sources */, 4B4518871F75E91A00926311 /* DigitalPhaseLockedLoop.cpp in Sources */, 4B98A05E1FFAD3F600ADF63B /* CSROMFetcher.mm in Sources */, diff --git a/OSBindings/Mac/Clock Signal/Info.plist b/OSBindings/Mac/Clock Signal/Info.plist index 9d46facbf..7f07c53ec 100644 --- a/OSBindings/Mac/Clock Signal/Info.plist +++ b/OSBindings/Mac/Clock Signal/Info.plist @@ -394,6 +394,7 @@ msa st + stx CFBundleTypeIconFile floppy35.png diff --git a/Storage/Disk/DiskImage/Formats/STX.cpp b/Storage/Disk/DiskImage/Formats/STX.cpp new file mode 100644 index 000000000..5e4662694 --- /dev/null +++ b/Storage/Disk/DiskImage/Formats/STX.cpp @@ -0,0 +1,413 @@ +// +// STX.cpp +// Clock Signal +// +// Created by Thomas Harte on 13/11/2019. +// Copyright © 2019 Thomas Harte. All rights reserved. +// + +#include "STX.hpp" + +#include "../../Encodings/MFM/Constants.hpp" +#include "../../Encodings/MFM/Shifter.hpp" +#include "../../Encodings/MFM/Encoder.hpp" +#include "../../Track/PCMTrack.hpp" + +#include "Utility/ImplicitSectors.hpp" + +#include +#include + +using namespace Storage::Disk; + +namespace { + +class TrackConstructor { + public: + +}; + +} + +STX::STX(const std::string &file_name) : file_(file_name) { + // Require that this be a version 3 Pasti. + if(!file_.check_signature("RSY", 4)) throw Error::InvalidFormat; + if(file_.get16le() != 3) throw Error::InvalidFormat; + + // Skip: tool used, 2 reserved bytes. + file_.seek(4, SEEK_CUR); + + // Grab the track count and test for a new-style encoding, and skip a reserved area. + track_count_ = file_.get8(); + is_new_format_ = file_.get8() == 2; + file_.seek(4, SEEK_CUR); + + // Set all tracks absent. + memset(offset_by_track_, 0, sizeof(offset_by_track_)); + + // Parse the tracks table to fill in offset_by_track_. The only available documentation + // for STX is unofficial and makes no promise about track order. Hence the bucket sort, + // effectively putting them into track order. + // + // Track descriptor layout: + // + // 0 4 Record size. + // 4 4 Number of bytes in fuzzy mask record. + // 8 2 Number of sectors on track. + // 10 2 Track flags. + // 12 2 Total number of bytes on track. + // 14 1 Track number (b7 = side, b0-b6 = track). + // 15 1 Track type. + while(true) { + const long offset = file_.tell(); + const uint32_t size = file_.get32le(); + if(file_.eof()) break; + + // Skip fields other than track position, then fill in table position and advance. + file_.seek(10, SEEK_CUR); + + const uint8_t track_position = file_.get8(); + offset_by_track_[track_position] = offset; + + // Seek next track start. + file_.seek(offset + size, SEEK_SET); + } +} + +HeadPosition STX::get_maximum_head_position() { + return HeadPosition(80); +} + +int STX::get_head_count() { + return 2; +} + +std::shared_ptr<::Storage::Disk::Track> STX::get_track_at_position(::Storage::Disk::Track::Address address) { + // These images have two sides, at most. + if(address.head > 1) return nullptr; + + // If no track was found, there's nothing to do here. + const int track_index = (address.head * 0x80) + address.position.as_int(); + if(!offset_by_track_[track_index]) return nullptr; + + // Seek to the track (skipping the record size field). + file_.seek(offset_by_track_[track_index] + 4, SEEK_SET); + + // Grab the track description. + const uint32_t fuzzy_size = file_.get32le(); + const uint16_t sector_count = file_.get16le(); + const uint16_t flags = file_.get16le(); + const size_t track_length = size_t(file_.get16le() << 3); // Convert bytes to bits. + file_.seek(2, SEEK_CUR); // Skip track type; despite being named, it's apparently unused. + + // If this is a trivial .ST-style sector dump, life is easy. + if(!(flags & 1)) { + const auto sector_contents = file_.read(sector_count * 512); + return track_for_sectors(sector_contents.data(), sector_count, uint8_t(address.position.as_int()), uint8_t(address.head), 1, 2, true); + } + + // Grab sector records, if provided. + struct Sector { + // Records explicitly present in the sector table. + uint32_t data_offset = 0; + size_t bit_position = 0; + uint16_t data_duration = 0; + uint8_t address[6] = {0, 0, 0, 0, 0, 0}; + uint8_t status = 0; + + // Other facts that will either be supplied by the STX or which + // will be empty. + std::vector fuzzy_mask; + std::vector contents; + + // Information accumulated locally during processing. + bool address_has_crc = true; + size_t track_offset_of_header = 0; + size_t track_offset_of_data = 0; + + // Accessors. + uint32_t data_size() { + return uint32_t(128 << address[3]); + } + }; + std::vector sectors; + if(flags & 1) { + // Read sector records first. + for(uint16_t c = 0; c < sector_count; ++c) { + sectors.emplace_back(); + sectors.back().data_offset = file_.get32le(); + sectors.back().bit_position = file_.get16le(); + sectors.back().data_duration = file_.get16le(); + file_.read(sectors.back().address, 6); + sectors.back().status = file_.get8(); + file_.seek(1, SEEK_CUR); + } + + // Now read fuzzy masks, if available. + if(fuzzy_size) { + uint32_t fuzzy_bytes_read = 0; + for(auto §or: sectors) { + // Check for the fuzzy bit mask; if it's not set then + // there's nothing for this sector. + if(!(sector.status & 0x80)) continue; + + // Make sure there are enough bytes left. + const uint32_t expected_bytes = sector.data_size(); + if(fuzzy_bytes_read + expected_bytes > fuzzy_size) break; + + // Okay, there are, so read them. + sector.fuzzy_mask = file_.read(expected_bytes); + fuzzy_bytes_read += expected_bytes; + } + + // It should be true that the number of fuzzy masks caused + // exactly the correct number of fuzzy bytes to be read. + // But, just in case, check and possibly skip some. + file_.seek(long(fuzzy_size) - fuzzy_bytes_read, SEEK_CUR); + } + } else { + // No sector records, so there should be no fuzzy records. + // Skip the supplied size, just in case. + file_.seek(fuzzy_size, SEEK_CUR); + } + + // From here: there's either a track image or there isn't. + // + // If there is then it may or may not contain the sector bodies. + // The sectors themselves will be the guide — if they have + // offsets within the track image then that's that; if it's + // outside then that implies extra sector contents. + // + // If there isn't a track image at all then either the sectors + // were explicit or they're completely implicit, like an ST file. + + // Grab the read-track-esque track contents, if available. + std::vector track_data; + long sector_start = file_.tell(); + if(flags & 0x40) { + if(flags & 0x80) { + const uint16_t first_sync = file_.get16le(); + const uint16_t image_size = file_.get16le(); + track_data = file_.read(image_size); + + // TODO: and encode... ignoring sector contents? + (void)first_sync; + } else { + const uint16_t image_size = file_.get16le(); + track_data = file_.read(image_size); + } + } + + // Grab all sector contents. + if(sectors.empty()) { + // No explicit sectors were given, so create the implied sort. + for(int c = 0; c < sector_count; ++c) { + sectors.emplace_back(); + sectors.back().address[0] = uint8_t(address.position.as_int()); // Track. + sectors.back().address[1] = uint8_t(address.head); // Head. + sectors.back().address[2] = uint8_t(c + 1); // Sector. + sectors.back().address[3] = uint8_t(c + 1); // Size. + sectors.back().address_has_crc = false; + + sectors.back().contents = file_.read(512); + sectors.back().bit_position = size_t(c); // For the sake of ordering only. + } + } else { + long end_of_data = file_.tell(); + for(auto §or: sectors) { + if(!(sector.status & 0x10)) { + file_.seek(sector.data_offset + sector_start, SEEK_SET); + sector.contents = file_.read(sector.data_size()); + end_of_data = std::max(end_of_data, file_.tell()); + } + } + file_.seek(end_of_data, SEEK_SET); + } + + // Check for timing info. + if(is_new_format_) { + // Do something, do something, else, else. + } + + /* + Having reached here: + + * if track_data is not empty, it is what you'd see from a read track command; + * the vector of sectors will contain sectors to be written; contents will be populated, + and each individually may or may not have a fuzzy_mask and/or timing. + + Also note track_length, which is the perceived length of the track, rounded to whole bytes. + */ + + // Sort the sectors by starting position. It's perfectly possible that they're always + // sorted in STX but, again, the reverse-engineered documentation doesn't make the + // promise, so that's that. + std::sort(sectors.begin(), sectors.end(), [] (Sector &lhs, Sector &rhs) { return lhs.bit_position < rhs.bit_position; }); + + if(track_data.empty()) { + + } else { + // Locate things that might be ID or data address marks; as a side effect of the way + // this is implemented, the byte_locations will be set to the first bit of apparent + // content for an ID or data mark. + struct PotentialMark { + enum class Type { ID, Data } type; + size_t byte_location; + + PotentialMark(Type type, size_t byte_location) : type(type), byte_location(byte_location) {} + }; + std::vector potential_marks; + { + const uint32_t id_mark = 0xa1a1fe; + const uint32_t data_mark = 0xa1a1fb; + uint32_t shifter = 0; + for(size_t c = 0; c < track_data.size(); ++c) { + shifter = ((shifter << 8) | track_data[c]) & 0xffffff; + + if(shifter == id_mark) { + potential_marks.emplace_back(PotentialMark::Type::ID, c); + } else if(shifter == data_mark) { + potential_marks.emplace_back(PotentialMark::Type::Data, c); + } + } + } + + // For each sector that exists, locate the correlated potential marks. + // Since sectors are now in track order, a forward walk through potential + // marks should work. + auto next_mark = potential_marks.begin(); + for(auto §or: sectors) { + if(sector.data_offset < track_data.size()) { + // The sector already tells us where its body is, so life is easy. + // Link the body to its known position, and backtrack to find the ID. + sector.track_offset_of_data = sector.data_offset; + + // Search for an unconsumed data mark at this location. + auto data_search = next_mark; + while( + data_search != potential_marks.end() && + !(data_search->type == PotentialMark::Type::Data && data_search->byte_location == sector.track_offset_of_data)) + ++data_search; + + // Advance the potential mark consumption pointer. + next_mark = data_search + 1; + + // Recede to a previous ID mark if possible. + while(data_search >= potential_marks.begin() && + !(data_search->type == PotentialMark::Type::ID && data_search->byte_location >= sector.track_offset_of_data - 150)) + --data_search; + + if(data_search >= potential_marks.begin()) { + sector.track_offset_of_header = data_search->byte_location; + } else { + // Couldn't figure this one out; just make a geuss. + sector.track_offset_of_header = sector.track_offset_of_data - 50; + } + } else { + // For either approach below, the next ID is needed. + while(next_mark != potential_marks.end() && next_mark->type != PotentialMark::Type::ID) + ++next_mark; + + if(next_mark == potential_marks.end()) break; + + // This sector's body isn't accurately represented within the read track + // image (or, at least, isn't decalred to be), so look for a suitable + // ID mark and then — if it has a body — consume the next data mark too. + if(sector.status & 0x10) { + // There's no placement information to go from, so compare by ID fields. As long + // as at least two bytes match, that'll do. Arbitrarily. + int matches = 0; + for(size_t c = 0; c < 4; ++c) { + matches += track_data[next_mark->byte_location + c] == sector.address[c]; + } + if(matches >= 2) { + sector.track_offset_of_header = next_mark->byte_location; + ++ next_mark; + } else { + // Desperation. The meaning of bit_position versus the track_contents is + // fairly undefined at the best of times, but seems to correlate with data + // rather than the header anyway. So, ummm... + sector.track_offset_of_header = sector.bit_position >> 3; + } + } else { + // If the next potential marks are an ID/data pair, and the stated data location is within + // 100 bytes of that encoded in the sector, take it. + auto data_mark = next_mark + 1; + if( + next_mark->type == PotentialMark::Type::ID && + data_mark->type == PotentialMark::Type::Data && + std::abs(int(next_mark->byte_location - (sector.bit_position >> 3))) < 100) { + sector.track_offset_of_header = next_mark->byte_location; + sector.track_offset_of_data = data_mark->byte_location; + next_mark += 2; + } else { + // Don't know. TODO? + } + } + } + } + + + // The game: take bytes from track_data unless or until a sector is hit. + auto next_sector = sectors.begin(); + size_t bytes_consumed = 0; + std::unique_ptr encoder; + std::unique_ptr segment; + while(bytes_consumed < track_length) { + // Next event is either the next sector or the end of the track. Let's see. + size_t bytes_to_consume = + ((next_sector != sectors.end()) ? + next_sector->track_offset_of_header : track_length) - bytes_consumed; + + // Write from bits_written to bits_written + bits_to_consume from track_data + // to an encoder. If there is no encoder right now, create one. + if(!encoder) { + segment.reset(new PCMSegment); + encoder = Encodings::MFM::GetMFMEncoder(segment->data); + } + + // Output bytes up to the sector. + while(bytes_to_consume--) { + encoder->add_byte(track_data[bytes_consumed]); + ++bytes_consumed; + } + + // Chuck out a sector if it's time for one. + if(next_sector != sectors.end()) { + // Output header. + encoder->add_ID_address_mark(); // This is four 'bytes', but pretend it's three. + encoder->add_byte(next_sector->address[0]); + encoder->add_byte(next_sector->address[1]); + encoder->add_byte(next_sector->address[2]); + encoder->add_byte(next_sector->address[3]); + if(next_sector->address_has_crc) { + encoder->add_byte(next_sector->address[4]); + encoder->add_byte(next_sector->address[5]); + } else { + encoder->add_crc((next_sector->status & 0x18) == 0x18); + } + bytes_consumed += 9; + + if(!(next_sector->status & 0x10)) { + while(bytes_consumed < next_sector->track_offset_of_data) { + encoder->add_byte(track_data[bytes_consumed]); + ++bytes_consumed; + } + + encoder->add_data_address_mark(); // Also four bytes, which we'll model as three. + for(const auto byte: next_sector->contents) { + encoder->add_byte(byte); + } + encoder->add_crc(next_sector->status & 0x8); + bytes_consumed += next_sector->contents.size() + 5; + } + + ++next_sector; + } + } + + return std::make_shared(*segment); + } + + return nullptr; +} diff --git a/Storage/Disk/DiskImage/Formats/STX.hpp b/Storage/Disk/DiskImage/Formats/STX.hpp new file mode 100644 index 000000000..dc21028dc --- /dev/null +++ b/Storage/Disk/DiskImage/Formats/STX.hpp @@ -0,0 +1,48 @@ +// +// STX.hpp +// Clock Signal +// +// Created by Thomas Harte on 13/11/2019. +// Copyright © 2019 Thomas Harte. All rights reserved. +// + +#ifndef STX_hpp +#define STX_hpp + +#include "../DiskImage.hpp" +#include "../../../FileHolder.hpp" + +namespace Storage { +namespace Disk { + +/*! + Provides a @c Disk containing an STX disk image: sector contents plus a bunch of annotations as to sizing, + placement, bit density, fuzzy bits, etc. +*/ +class STX: public DiskImage { + public: + /*! + Construct an @c STX containing content from the file with name @c file_name. + + @throws Storage::FileHolder::Error::CantOpen if this file can't be opened. + @throws Error::InvalidFormat if the file doesn't appear to contain a .STX format image. + */ + STX(const std::string &file_name); + + HeadPosition get_maximum_head_position() final; + int get_head_count() final; + + std::shared_ptr<::Storage::Disk::Track> get_track_at_position(::Storage::Disk::Track::Address address) final; + + private: + FileHolder file_; + + int track_count_; + bool is_new_format_; + long offset_by_track_[256]; +}; + +} +} + +#endif /* STX_hpp */ From b4befd57a975cfea8595995484a4f663791bf8f4 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 9 Jan 2020 21:03:01 -0500 Subject: [PATCH 2/8] Advances to being able to cope with STXs with no special features whatsoever. Well, other than perhaps a broken data CRC. Fuzzy bits, timing differences and the stuff between sectors are all currently absent. --- Storage/Disk/DiskImage/Formats/STX.cpp | 281 +++++++++++++++---------- 1 file changed, 172 insertions(+), 109 deletions(-) diff --git a/Storage/Disk/DiskImage/Formats/STX.cpp b/Storage/Disk/DiskImage/Formats/STX.cpp index 5e4662694..09473a41a 100644 --- a/Storage/Disk/DiskImage/Formats/STX.cpp +++ b/Storage/Disk/DiskImage/Formats/STX.cpp @@ -24,6 +24,79 @@ namespace { class TrackConstructor { public: + constexpr static uint16_t NoFirstOffset = std::numeric_limits::max(); + + struct Sector { + // Records explicitly present in the sector table. + uint32_t data_offset = 0; + size_t bit_position = 0; + uint16_t data_duration = 0; + uint8_t address[6] = {0, 0, 0, 0, 0, 0}; + uint8_t status = 0; + + // Other facts that will either be supplied by the STX or which + // will be empty. + std::vector fuzzy_mask; + std::vector contents; + std::vector timing; + + // Accessors. + uint32_t data_size() { + return uint32_t(128 << address[3]); + } +// std::vector get_track_header_image() { +// +// } + }; + + + TrackConstructor(const std::vector &track_data, const std::vector §ors, size_t track_size, uint16_t first_sync) : + track_data_(track_data), sectors_(sectors), track_size_(track_size), first_sync_(first_sync) { + } + + std::shared_ptr get_track() { + std::unique_ptr encoder; + std::unique_ptr segment; + + // To reconcile the list of sectors with the WD get track-style track image, + // use sector bodies as definitive and refer to the track image for in-fill. + for(const auto §or: sectors_) { + // HACK: assume nothing between sectors. Crazy time! + + if(!encoder) { + segment.reset(new PCMSegment); + encoder = Storage::Encodings::MFM::GetMFMEncoder(segment->data); + } + + // Add sector header. + encoder->add_ID_address_mark(); + for(int c = 0; c < 6; ++c) + encoder->add_byte(sector.address[c]); + + // Add a gap. + for(int c = 0; c < 12; ++c) + encoder->add_byte(0x4e); + + // Add sector body. + encoder->add_data_address_mark(); + for(const auto byte: sector.contents) { + encoder->add_byte(byte); + } + encoder->add_crc(sector.status & 0x8); // Get the CRC wrong if required. (TODO: take from track image, if possible?) + + // Add a gap. + for(int c = 0; c < 42; ++c) + encoder->add_byte(0x4e); + } + + return std::make_shared(*segment); + } + + private: + const std::vector &track_data_; + const std::vector §ors_; + const size_t track_size_; + const uint16_t first_sync_; }; @@ -107,131 +180,127 @@ std::shared_ptr<::Storage::Disk::Track> STX::get_track_at_position(::Storage::Di } // Grab sector records, if provided. - struct Sector { - // Records explicitly present in the sector table. - uint32_t data_offset = 0; - size_t bit_position = 0; - uint16_t data_duration = 0; - uint8_t address[6] = {0, 0, 0, 0, 0, 0}; - uint8_t status = 0; + std::vector sectors; + std::vector track_data; + uint16_t first_sync = TrackConstructor::NoFirstOffset; - // Other facts that will either be supplied by the STX or which - // will be empty. - std::vector fuzzy_mask; - std::vector contents; - - // Information accumulated locally during processing. - bool address_has_crc = true; - size_t track_offset_of_header = 0; - size_t track_offset_of_data = 0; - - // Accessors. - uint32_t data_size() { - return uint32_t(128 << address[3]); - } - }; - std::vector sectors; - if(flags & 1) { - // Read sector records first. - for(uint16_t c = 0; c < sector_count; ++c) { - sectors.emplace_back(); - sectors.back().data_offset = file_.get32le(); - sectors.back().bit_position = file_.get16le(); - sectors.back().data_duration = file_.get16le(); - file_.read(sectors.back().address, 6); - sectors.back().status = file_.get8(); - file_.seek(1, SEEK_CUR); - } - - // Now read fuzzy masks, if available. - if(fuzzy_size) { - uint32_t fuzzy_bytes_read = 0; - for(auto §or: sectors) { - // Check for the fuzzy bit mask; if it's not set then - // there's nothing for this sector. - if(!(sector.status & 0x80)) continue; - - // Make sure there are enough bytes left. - const uint32_t expected_bytes = sector.data_size(); - if(fuzzy_bytes_read + expected_bytes > fuzzy_size) break; - - // Okay, there are, so read them. - sector.fuzzy_mask = file_.read(expected_bytes); - fuzzy_bytes_read += expected_bytes; - } - - // It should be true that the number of fuzzy masks caused - // exactly the correct number of fuzzy bytes to be read. - // But, just in case, check and possibly skip some. - file_.seek(long(fuzzy_size) - fuzzy_bytes_read, SEEK_CUR); - } - } else { - // No sector records, so there should be no fuzzy records. - // Skip the supplied size, just in case. - file_.seek(fuzzy_size, SEEK_CUR); + // Sector records come first. + for(uint16_t c = 0; c < sector_count; ++c) { + sectors.emplace_back(); + sectors.back().data_offset = file_.get32le(); + sectors.back().bit_position = file_.get16le(); + sectors.back().data_duration = file_.get16le(); + file_.read(sectors.back().address, 6); + sectors.back().status = file_.get8(); + file_.seek(1, SEEK_CUR); } - // From here: there's either a track image or there isn't. - // - // If there is then it may or may not contain the sector bodies. - // The sectors themselves will be the guide — if they have - // offsets within the track image then that's that; if it's - // outside then that implies extra sector contents. - // - // If there isn't a track image at all then either the sectors - // were explicit or they're completely implicit, like an ST file. + // If fuzzy masks are specified, attach them to their corresponding sectors. + if(fuzzy_size) { + uint32_t fuzzy_bytes_read = 0; + for(auto §or: sectors) { + // Check for the fuzzy bit mask; if it's not set then + // there's nothing for this sector. + if(!(sector.status & 0x80)) continue; + + // Make sure there are enough bytes left. + const uint32_t expected_bytes = sector.data_size(); + if(fuzzy_bytes_read + expected_bytes > fuzzy_size) break; + + // Okay, there are, so read them. + sector.fuzzy_mask = file_.read(expected_bytes); + fuzzy_bytes_read += expected_bytes; + } + + // It should be true that the number of fuzzy masks caused + // exactly the correct number of fuzzy bytes to be read. + // But, just in case, check and possibly skip some. + file_.seek(long(fuzzy_size) - fuzzy_bytes_read, SEEK_CUR); + } + + // There may or may not be a track image. Grab it if so. // Grab the read-track-esque track contents, if available. - std::vector track_data; long sector_start = file_.tell(); if(flags & 0x40) { + // Bit 6 => there is a track to read; + // bit if(flags & 0x80) { - const uint16_t first_sync = file_.get16le(); + first_sync = file_.get16le(); const uint16_t image_size = file_.get16le(); track_data = file_.read(image_size); - - // TODO: and encode... ignoring sector contents? - (void)first_sync; } else { const uint16_t image_size = file_.get16le(); track_data = file_.read(image_size); } } - // Grab all sector contents. - if(sectors.empty()) { - // No explicit sectors were given, so create the implied sort. - for(int c = 0; c < sector_count; ++c) { - sectors.emplace_back(); - sectors.back().address[0] = uint8_t(address.position.as_int()); // Track. - sectors.back().address[1] = uint8_t(address.head); // Head. - sectors.back().address[2] = uint8_t(c + 1); // Sector. - sectors.back().address[3] = uint8_t(c + 1); // Size. - sectors.back().address_has_crc = false; - - sectors.back().contents = file_.read(512); - sectors.back().bit_position = size_t(c); // For the sake of ordering only. + // Grab sector contents. + long end_of_data = file_.tell(); + for(auto §or: sectors) { + // If the FDC record-not-found flag is set, there's no sector body to find. + // Otherwise there's a sector body in the file somewhere. + if(!(sector.status & 0x10)) { + file_.seek(sector.data_offset + sector_start, SEEK_SET); + sector.contents = file_.read(sector.data_size()); + end_of_data = std::max(end_of_data, file_.tell()); } - } else { - long end_of_data = file_.tell(); - for(auto §or: sectors) { - if(!(sector.status & 0x10)) { - file_.seek(sector.data_offset + sector_start, SEEK_SET); - sector.contents = file_.read(sector.data_size()); - end_of_data = std::max(end_of_data, file_.tell()); + } + file_.seek(end_of_data, SEEK_SET); + + // Grab timing info if available. + file_.seek(4, SEEK_CUR); // Skip the timing descriptor, as it includes no new information. + for(auto §or: sectors) { + // Skip any sector with no intra-sector bit width variation. + if(!(sector.status&1)) continue; + + const auto timing_record_size = sector.data_size() >> 4; // Use one entry per 16 bytes. + sector.timing.resize(timing_record_size); + + if(!is_new_format_) { + // Generate timing records for Macrodos/Speedlock. + // Timing is specified in quarters. Which might or might not be + // quantities of 128 bytes, who knows? + for(size_t c = 0; c < timing_record_size; ++c) { + if(c < (timing_record_size >> 2)) { + sector.timing[c] = 127; + } else if(c < ((timing_record_size*2) >> 2)) { + sector.timing[c] = 133; + } else if(c < ((timing_record_size*3) >> 2)) { + sector.timing[c] = 121; + } else { + sector.timing[c] = 127; + } } + + continue; + } + + // This is going to be a new-format record. + for(size_t c = 0; c < timing_record_size; ++c) { + sector.timing[c] = file_.get16be(); // These values are big endian, unlike the rest of the file. } - file_.seek(end_of_data, SEEK_SET); } - // Check for timing info. - if(is_new_format_) { - // Do something, do something, else, else. - } + // Sort the sectors by starting position. It's perfectly possible that they're always + // sorted in STX but, again, the reverse-engineered documentation doesn't make the + // promise, so that's that. + std::sort(sectors.begin(), sectors.end(), + [] (TrackConstructor::Sector &lhs, TrackConstructor::Sector &rhs) { + return lhs.bit_position < rhs.bit_position; + }); + /* - Having reached here: + Having reached here, the actual stuff of parsing the file structure should be done. + So hand off to the TrackConstructor. + */ + + TrackConstructor constructor(track_data, sectors, track_length, first_sync); + return constructor.get_track(); + + /* * if track_data is not empty, it is what you'd see from a read track command; * the vector of sectors will contain sectors to be written; contents will be populated, and each individually may or may not have a fuzzy_mask and/or timing. @@ -239,12 +308,7 @@ std::shared_ptr<::Storage::Disk::Track> STX::get_track_at_position(::Storage::Di Also note track_length, which is the perceived length of the track, rounded to whole bytes. */ - // Sort the sectors by starting position. It's perfectly possible that they're always - // sorted in STX but, again, the reverse-engineered documentation doesn't make the - // promise, so that's that. - std::sort(sectors.begin(), sectors.end(), [] (Sector &lhs, Sector &rhs) { return lhs.bit_position < rhs.bit_position; }); - - if(track_data.empty()) { +/* if(track_data.empty()) { } else { // Locate things that might be ID or data address marks; as a side effect of the way @@ -407,7 +471,6 @@ std::shared_ptr<::Storage::Disk::Track> STX::get_track_at_position(::Storage::Di } return std::make_shared(*segment); - } + }*/ - return nullptr; } From 64517a02b76c53fa0e244a0e4b44896000a7486e Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 9 Jan 2020 21:50:32 -0500 Subject: [PATCH 3/8] Adds code to deal with sector-free tracks. --- Storage/Disk/DiskImage/Formats/STX.cpp | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Storage/Disk/DiskImage/Formats/STX.cpp b/Storage/Disk/DiskImage/Formats/STX.cpp index 09473a41a..738a92539 100644 --- a/Storage/Disk/DiskImage/Formats/STX.cpp +++ b/Storage/Disk/DiskImage/Formats/STX.cpp @@ -55,6 +55,24 @@ class TrackConstructor { } std::shared_ptr get_track() { + // If no contents are supplied, return an unformatted track. + if(sectors_.empty() && track_data_.empty()) { + return nullptr; + } + + // If no sectors are on this track, just encode the track data. STX allows speed + // changes and fuzzy bits in sectors only. + if(sectors_.empty()) { + PCMSegment segment; + std::unique_ptr encoder = Storage::Encodings::MFM::GetMFMEncoder(segment.data); + for(auto c: track_data_) { + encoder->add_byte(c); + } + return std::make_shared(segment); + } + + // Otherwise, seek to encode the sectors, using the track data to + // fill in the gaps (if provided). std::unique_ptr encoder; std::unique_ptr segment; @@ -89,6 +107,14 @@ class TrackConstructor { encoder->add_byte(0x4e); } +// while(segment->data.size() < track_size_ * 16) { +// encoder->add_byte(0x4e); +// } + + while(segment->data.size() < 6250 * 16) { + encoder->add_byte(0x4e); + } + return std::make_shared(*segment); } From 2b4c9243990bf2af90972da00e48d4c4baa16eba Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 9 Jan 2020 23:28:07 -0500 Subject: [PATCH 4/8] Makes an effort to locate address and data bodies within track. "Not completely successful" would be the polite term. --- Storage/Disk/DiskImage/Formats/STX.cpp | 68 +++++++++++++++++++++++--- Storage/FileHolder.hpp | 6 +++ 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/Storage/Disk/DiskImage/Formats/STX.cpp b/Storage/Disk/DiskImage/Formats/STX.cpp index 738a92539..d2c6886cd 100644 --- a/Storage/Disk/DiskImage/Formats/STX.cpp +++ b/Storage/Disk/DiskImage/Formats/STX.cpp @@ -31,7 +31,7 @@ class TrackConstructor { uint32_t data_offset = 0; size_t bit_position = 0; uint16_t data_duration = 0; - uint8_t address[6] = {0, 0, 0, 0, 0, 0}; + std::array address = {0, 0, 0, 0, 0, 0}; uint8_t status = 0; // Other facts that will either be supplied by the STX or which @@ -41,12 +41,48 @@ class TrackConstructor { std::vector timing; // Accessors. - uint32_t data_size() { + uint32_t data_size() const { return uint32_t(128 << address[3]); } -// std::vector get_track_header_image() { -// -// } + std::vector get_track_address_image() const { + return track_encoding(address.begin(), address.begin() + 4, {0xa1, 0xfe}); + } + std::vector get_track_data_image() const { + return track_encoding(contents.begin(), contents.end(), {0xa1, 0xfb}); + } + + private: + template static std::vector track_encoding(T begin, T end, std::initializer_list prefix) { + std::vector result; + result.reserve(size_t(end - begin) + prefix.size()); + + // Encode as MFM. + PCMSegment segment; + std::unique_ptr encoder = Storage::Encodings::MFM::GetMFMEncoder(segment.data); + while(begin != end) { + encoder->add_byte(*begin); + ++begin; + } + + // Decode, obeying false syncs. + using Shifter = Storage::Encodings::MFM::Shifter; + Shifter shifter; + shifter.set_should_obey_syncs(true); + + // Add the prefix. + std::copy(prefix.begin(), prefix.end(), std::back_inserter(result)); + + // Add whatever comes from the track. + for(auto bit: segment.data) { + shifter.add_input_bit(int(bit)); + + if(shifter.get_token() != Shifter::None) { + result.push_back(shifter.get_byte()); + } + } + + return result; + } }; @@ -78,7 +114,25 @@ class TrackConstructor { // To reconcile the list of sectors with the WD get track-style track image, // use sector bodies as definitive and refer to the track image for in-fill. + auto track_position = track_data_.begin(); for(const auto §or: sectors_) { + // Find out what the header would look like, if found in a read track. + const auto track_address = sector.get_track_address_image(); + const auto track_data = sector.get_track_data_image(); + + // Try to locate the header within the track image. + const auto address_position = std::search(track_position, track_data_.end(), track_address.begin(), track_address.end()); + const auto data_position = std::search(track_position, track_data_.end(), track_data.begin(), track_data.end()); + + if(address_position == track_data_.end()) { + printf("?\n"); + } + if(data_position == track_data_.end()) { + printf("??\n"); + } + + printf("%lu / %lu\n", address_position - track_data_.begin(), data_position - track_data_.begin()); + // HACK: assume nothing between sectors. Crazy time! if(!encoder) { @@ -88,7 +142,7 @@ class TrackConstructor { // Add sector header. encoder->add_ID_address_mark(); - for(int c = 0; c < 6; ++c) + for(size_t c = 0; c < 6; ++c) encoder->add_byte(sector.address[c]); // Add a gap. @@ -216,7 +270,7 @@ std::shared_ptr<::Storage::Disk::Track> STX::get_track_at_position(::Storage::Di sectors.back().data_offset = file_.get32le(); sectors.back().bit_position = file_.get16le(); sectors.back().data_duration = file_.get16le(); - file_.read(sectors.back().address, 6); + file_.read(sectors.back().address); sectors.back().status = file_.get8(); file_.seek(1, SEEK_CUR); } diff --git a/Storage/FileHolder.hpp b/Storage/FileHolder.hpp index f09f8f4bc..02e698b2f 100644 --- a/Storage/FileHolder.hpp +++ b/Storage/FileHolder.hpp @@ -10,6 +10,7 @@ #define FileHolder_hpp #include +#include #include #include #include @@ -126,6 +127,11 @@ class FileHolder final { /*! Reads @c size bytes and returns them as a vector. */ std::vector read(std::size_t size); + /*! Reads @c a.size() bytes into @c a.data(). */ + template std::size_t read(std::array &a) { + return read(a.data(), a.size()); + } + /*! Reads @c size bytes and writes them to @c buffer. */ std::size_t read(uint8_t *buffer, std::size_t size); From f81a7f0faf3deb61b1b3c9280b1b4c171f77c804 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sat, 11 Jan 2020 22:10:41 -0500 Subject: [PATCH 5/8] Ensures prefixes are MFM encoded and decoded. --- .../xcshareddata/xcschemes/Clock Signal.xcscheme | 2 +- Storage/Disk/DiskImage/Formats/STX.cpp | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme index 47f9c7286..1465a4f62 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme +++ b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme @@ -67,7 +67,7 @@ result; result.reserve(size_t(end - begin) + prefix.size()); - // Encode as MFM. PCMSegment segment; std::unique_ptr encoder = Storage::Encodings::MFM::GetMFMEncoder(segment.data); + + // Encode prefix. + for(auto c: prefix) { + encoder->add_byte(c); + } + + // Encode body. while(begin != end) { encoder->add_byte(*begin); ++begin; @@ -69,9 +75,6 @@ class TrackConstructor { Shifter shifter; shifter.set_should_obey_syncs(true); - // Add the prefix. - std::copy(prefix.begin(), prefix.end(), std::back_inserter(result)); - // Add whatever comes from the track. for(auto bit: segment.data) { shifter.add_input_bit(int(bit)); @@ -243,6 +246,10 @@ std::shared_ptr<::Storage::Disk::Track> STX::get_track_at_position(::Storage::Di const int track_index = (address.head * 0x80) + address.position.as_int(); if(!offset_by_track_[track_index]) return nullptr; + if(track_index == 41) { + printf("Y\n"); + } else printf("N\n"); + // Seek to the track (skipping the record size field). file_.seek(offset_by_track_[track_index] + 4, SEEK_SET); From 83ed36eb0822de3d841fc0281f8cfd3c40bf6986 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 12 Jan 2020 17:56:04 -0500 Subject: [PATCH 6/8] Add missing #include. --- Storage/Disk/DiskImage/Formats/STX.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Storage/Disk/DiskImage/Formats/STX.cpp b/Storage/Disk/DiskImage/Formats/STX.cpp index c53ce4ea0..50426cd49 100644 --- a/Storage/Disk/DiskImage/Formats/STX.cpp +++ b/Storage/Disk/DiskImage/Formats/STX.cpp @@ -17,6 +17,7 @@ #include #include +#include using namespace Storage::Disk; From 2d233b635867bbeb99faf6b1d2a8211f615bf69f Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 12 Jan 2020 22:18:31 -0500 Subject: [PATCH 7/8] Makes a more concrete attempt at track/sector combination. --- Components/1770/1770.cpp | 1 + Storage/Disk/DPLL/DigitalPhaseLockedLoop.hpp | 22 +- Storage/Disk/DiskImage/Formats/STX.cpp | 338 +++++++------------ 3 files changed, 135 insertions(+), 226 deletions(-) diff --git a/Components/1770/1770.cpp b/Components/1770/1770.cpp index 92409bc69..aa76f635b 100644 --- a/Components/1770/1770.cpp +++ b/Components/1770/1770.cpp @@ -336,6 +336,7 @@ void WD1770::posit_event(int new_event_type) { READ_ID(); if(index_hole_count_ == 6) { + LOG("Nothing found to verify"); update_status([] (Status &status) { status.seek_error = true; }); diff --git a/Storage/Disk/DPLL/DigitalPhaseLockedLoop.hpp b/Storage/Disk/DPLL/DigitalPhaseLockedLoop.hpp index b81643149..487303763 100644 --- a/Storage/Disk/DPLL/DigitalPhaseLockedLoop.hpp +++ b/Storage/Disk/DPLL/DigitalPhaseLockedLoop.hpp @@ -83,7 +83,7 @@ template class DigitalPhaseL total_spacing_ -= offset_history_[offset_history_pointer_].spacing; // Fill in the new fields. - const auto multiple = (new_offset + (clocks_per_bit_ >> 1)) / clocks_per_bit_; + const auto multiple = std::max((new_offset + (clocks_per_bit_ >> 1)) / clocks_per_bit_, Cycles::IntType(1)); offset_history_[offset_history_pointer_].divisor = multiple; offset_history_[offset_history_pointer_].spacing = new_offset; @@ -94,19 +94,19 @@ template class DigitalPhaseL // Advance the write slot. offset_history_pointer_ = (offset_history_pointer_ + 1) % offset_history_.size(); +#ifndef NDEBUG + Cycles::IntType td = 0, ts = 0; + for(auto offset: offset_history_) { + td += offset.divisor; + ts += offset.spacing; + } + assert(ts == total_spacing_); + assert(td == total_divisor_); +#endif + // In net: use an unweighted average of the stored offsets to compute current window size, // bucketing them by rounding to the nearest multiple of the base clocks per bit window_length_ = total_spacing_ / total_divisor_; -#ifndef NDEBUG - bool are_all_filled = true; - for(auto offset: offset_history_) { - if(offset.spacing == 1) { - are_all_filled = false; - break; - } - } - assert(!are_all_filled || (window_length_ >= ((clocks_per_bit_ * 9) / 10) && window_length_ <= ((clocks_per_bit_ * 11) / 10))); -#endif // Also apply a difference to phase, use a simple spring mechanism as a lowpass filter. const auto error = new_phase - (window_length_ >> 1); diff --git a/Storage/Disk/DiskImage/Formats/STX.cpp b/Storage/Disk/DiskImage/Formats/STX.cpp index 50426cd49..5a6cc10bc 100644 --- a/Storage/Disk/DiskImage/Formats/STX.cpp +++ b/Storage/Disk/DiskImage/Formats/STX.cpp @@ -42,17 +42,25 @@ class TrackConstructor { std::vector timing; // Accessors. + + /// @returns The byte size of this sector, according to its address mark. uint32_t data_size() const { return uint32_t(128 << address[3]); } + + /// @returns The byte stream this sector address would produce if a WD read track command were to observe it. std::vector get_track_address_image() const { - return track_encoding(address.begin(), address.begin() + 4, {0xa1, 0xfe}); + return track_encoding(address.begin(), address.begin() + 4, {0xa1, 0xa1, 0xfe}); } + + /// @returns The byte stream this sector data would produce if a WD read track command were to observe it. std::vector get_track_data_image() const { - return track_encoding(contents.begin(), contents.end(), {0xa1, 0xfb}); + return track_encoding(contents.begin(), contents.end(), {0xa1, 0xa1, 0xfb}); } private: + /// @returns The effect of encoding @c prefix followed by the bytes from @c begin to @c end as MFM data and then decoding them as if + /// observed by a WD read track command. template static std::vector track_encoding(T begin, T end, std::initializer_list prefix) { std::vector result; result.reserve(size_t(end - begin) + prefix.size()); @@ -119,57 +127,135 @@ class TrackConstructor { // To reconcile the list of sectors with the WD get track-style track image, // use sector bodies as definitive and refer to the track image for in-fill. auto track_position = track_data_.begin(); + const auto address_mark = {0xa1, 0xa1, 0xfe}; + const auto track_mark = {0xa1, 0xa1, 0xfb}; + struct Location { + enum Type { + Address, Data + } type; + std::vector::const_iterator position; + const Sector §or; + + Location(Type type, std::vector::const_iterator position, const Sector §or) : type(type), position(position), sector(sector) {} + }; + std::vector locations; for(const auto §or: sectors_) { - // Find out what the header would look like, if found in a read track. - const auto track_address = sector.get_track_address_image(); - const auto track_data = sector.get_track_data_image(); + { + // Find out what the address would look like, if found in a read track. + const auto track_address = sector.get_track_address_image(); - // Try to locate the header within the track image. - const auto address_position = std::search(track_position, track_data_.end(), track_address.begin(), track_address.end()); - const auto data_position = std::search(track_position, track_data_.end(), track_data.begin(), track_data.end()); + // Try to locate the header within the track image; if it can't be found then settle for + // the next thing that looks like a header of any sort. + auto address_position = std::search(track_position, track_data_.end(), track_address.begin(), track_address.end()); + if(address_position == track_data_.end()) { + address_position = std::search(track_position, track_data_.end(), address_mark.begin(), address_mark.end()); + } - if(address_position == track_data_.end()) { - printf("?\n"); - } - if(data_position == track_data_.end()) { - printf("??\n"); + // Stop now if there's nowhere obvious to put this sector. + if(address_position == track_data_.end()) break; + locations.emplace_back(Location::Address, address_position, sector); + + // Advance the track position. + track_position = address_position; } - printf("%lu / %lu\n", address_position - track_data_.begin(), data_position - track_data_.begin()); + // Do much the same thing for the data, if it exists. + if(!(sector.status & 0x10)) { + const auto track_data = sector.get_track_data_image(); - // HACK: assume nothing between sectors. Crazy time! + auto data_position = std::search(track_position, track_data_.end(), track_data.begin(), track_data.end()); + if(data_position == track_data_.end()) { + data_position = std::search(track_position, track_data_.end(), track_mark.begin(), track_mark.end()); + } + if(data_position == track_data_.end()) break; + locations.emplace_back(Location::Data, data_position, sector); + track_position = data_position; + } + } + + // Write out, being wary of potential overlapping sectors, and copying from track_data_ to fill in gaps. + auto location = locations.begin(); + track_position = track_data_.begin(); + while(location != locations.end()) { + // Just create an encoder if one doesn't exist. TODO: factor in data rate. if(!encoder) { segment.reset(new PCMSegment); encoder = Storage::Encodings::MFM::GetMFMEncoder(segment->data); } - // Add sector header. - encoder->add_ID_address_mark(); - for(size_t c = 0; c < 6; ++c) - encoder->add_byte(sector.address[c]); - - // Add a gap. - for(int c = 0; c < 12; ++c) - encoder->add_byte(0x4e); - - // Add sector body. - encoder->add_data_address_mark(); - for(const auto byte: sector.contents) { - encoder->add_byte(byte); + // Advance to location.position. + while(track_position != location->position) { + encoder->add_byte(*track_position); + ++track_position; } - encoder->add_crc(sector.status & 0x8); // Get the CRC wrong if required. (TODO: take from track image, if possible?) - // Add a gap. - for(int c = 0; c < 42; ++c) - encoder->add_byte(0x4e); + // Write the relevant mark and fill in a default number of bytes to write. + size_t bytes_to_write; + switch(location->type) { + default: + case Location::Address: + encoder->add_ID_address_mark(); + bytes_to_write = 6; + break; + case Location::Data: + if(location->sector.status & 0x20) + encoder->add_deleted_data_address_mark(); + else + encoder->add_data_address_mark(); + bytes_to_write = location->sector.data_size() + 2; + break; + } + track_position += 3; + + // Decide how much data to write for real; this [partially] allows for overlapping sectors. + auto next_location = location + 1; + if(next_location != locations.end()) { + bytes_to_write = std::min(bytes_to_write, size_t(next_location->position - track_position)); + } + + // Skip that many bytes from the underlying track image. + track_position += ssize_t(bytes_to_write); + + // Write bytes. + switch(location->type) { + default: + case Location::Address: + for(size_t c = 0; c < bytes_to_write; ++c) + encoder->add_byte(location->sector.address[c]); + break; + case Location::Data: { + const auto body_bytes = std::min(bytes_to_write, size_t(location->sector.data_size())); + for(size_t c = 0; c < body_bytes; ++c) + encoder->add_byte(location->sector.contents[c]); + + // Add a CRC only if it fits (TODO: crop if necessary?). + if(bytes_to_write & 127) { + encoder->add_crc((location->sector.status & 0x18) == 0x10); + } + } break; + } + + // Advance location. + ++location; } -// while(segment->data.size() < track_size_ * 16) { -// encoder->add_byte(0x4e); -// } + // Write anything remaining from the track image. + while(track_position < track_data_.end()) { + encoder->add_byte(*track_position); + ++track_position; + } - while(segment->data.size() < 6250 * 16) { + // Write generic padding up until the specified track size. + while(segment->data.size() < track_size_ * 16) { + encoder->add_byte(0x4e); + } + + // Pad out to the minimum size a WD can actually make sense of. + // I've no idea why it's valid for tracks to be shorter than this, + // so likely I'm suffering a comprehansion deficiency. + // TODO: determine why this isn't correct (or, possibly, is). + while(segment->data.size() < 5750 * 16) { encoder->add_byte(0x4e); } @@ -247,10 +333,6 @@ std::shared_ptr<::Storage::Disk::Track> STX::get_track_at_position(::Storage::Di const int track_index = (address.head * 0x80) + address.position.as_int(); if(!offset_by_track_[track_index]) return nullptr; - if(track_index == 41) { - printf("Y\n"); - } else printf("N\n"); - // Seek to the track (skipping the record size field). file_.seek(offset_by_track_[track_index] + 4, SEEK_SET); @@ -258,7 +340,7 @@ std::shared_ptr<::Storage::Disk::Track> STX::get_track_at_position(::Storage::Di const uint32_t fuzzy_size = file_.get32le(); const uint16_t sector_count = file_.get16le(); const uint16_t flags = file_.get16le(); - const size_t track_length = size_t(file_.get16le() << 3); // Convert bytes to bits. + const size_t track_length = file_.get16le(); file_.seek(2, SEEK_CUR); // Skip track type; despite being named, it's apparently unused. // If this is a trivial .ST-style sector dump, life is easy. @@ -387,178 +469,4 @@ std::shared_ptr<::Storage::Disk::Track> STX::get_track_at_position(::Storage::Di TrackConstructor constructor(track_data, sectors, track_length, first_sync); return constructor.get_track(); - - /* - * if track_data is not empty, it is what you'd see from a read track command; - * the vector of sectors will contain sectors to be written; contents will be populated, - and each individually may or may not have a fuzzy_mask and/or timing. - - Also note track_length, which is the perceived length of the track, rounded to whole bytes. - */ - -/* if(track_data.empty()) { - - } else { - // Locate things that might be ID or data address marks; as a side effect of the way - // this is implemented, the byte_locations will be set to the first bit of apparent - // content for an ID or data mark. - struct PotentialMark { - enum class Type { ID, Data } type; - size_t byte_location; - - PotentialMark(Type type, size_t byte_location) : type(type), byte_location(byte_location) {} - }; - std::vector potential_marks; - { - const uint32_t id_mark = 0xa1a1fe; - const uint32_t data_mark = 0xa1a1fb; - uint32_t shifter = 0; - for(size_t c = 0; c < track_data.size(); ++c) { - shifter = ((shifter << 8) | track_data[c]) & 0xffffff; - - if(shifter == id_mark) { - potential_marks.emplace_back(PotentialMark::Type::ID, c); - } else if(shifter == data_mark) { - potential_marks.emplace_back(PotentialMark::Type::Data, c); - } - } - } - - // For each sector that exists, locate the correlated potential marks. - // Since sectors are now in track order, a forward walk through potential - // marks should work. - auto next_mark = potential_marks.begin(); - for(auto §or: sectors) { - if(sector.data_offset < track_data.size()) { - // The sector already tells us where its body is, so life is easy. - // Link the body to its known position, and backtrack to find the ID. - sector.track_offset_of_data = sector.data_offset; - - // Search for an unconsumed data mark at this location. - auto data_search = next_mark; - while( - data_search != potential_marks.end() && - !(data_search->type == PotentialMark::Type::Data && data_search->byte_location == sector.track_offset_of_data)) - ++data_search; - - // Advance the potential mark consumption pointer. - next_mark = data_search + 1; - - // Recede to a previous ID mark if possible. - while(data_search >= potential_marks.begin() && - !(data_search->type == PotentialMark::Type::ID && data_search->byte_location >= sector.track_offset_of_data - 150)) - --data_search; - - if(data_search >= potential_marks.begin()) { - sector.track_offset_of_header = data_search->byte_location; - } else { - // Couldn't figure this one out; just make a geuss. - sector.track_offset_of_header = sector.track_offset_of_data - 50; - } - } else { - // For either approach below, the next ID is needed. - while(next_mark != potential_marks.end() && next_mark->type != PotentialMark::Type::ID) - ++next_mark; - - if(next_mark == potential_marks.end()) break; - - // This sector's body isn't accurately represented within the read track - // image (or, at least, isn't decalred to be), so look for a suitable - // ID mark and then — if it has a body — consume the next data mark too. - if(sector.status & 0x10) { - // There's no placement information to go from, so compare by ID fields. As long - // as at least two bytes match, that'll do. Arbitrarily. - int matches = 0; - for(size_t c = 0; c < 4; ++c) { - matches += track_data[next_mark->byte_location + c] == sector.address[c]; - } - if(matches >= 2) { - sector.track_offset_of_header = next_mark->byte_location; - ++ next_mark; - } else { - // Desperation. The meaning of bit_position versus the track_contents is - // fairly undefined at the best of times, but seems to correlate with data - // rather than the header anyway. So, ummm... - sector.track_offset_of_header = sector.bit_position >> 3; - } - } else { - // If the next potential marks are an ID/data pair, and the stated data location is within - // 100 bytes of that encoded in the sector, take it. - auto data_mark = next_mark + 1; - if( - next_mark->type == PotentialMark::Type::ID && - data_mark->type == PotentialMark::Type::Data && - std::abs(int(next_mark->byte_location - (sector.bit_position >> 3))) < 100) { - sector.track_offset_of_header = next_mark->byte_location; - sector.track_offset_of_data = data_mark->byte_location; - next_mark += 2; - } else { - // Don't know. TODO? - } - } - } - } - - - // The game: take bytes from track_data unless or until a sector is hit. - auto next_sector = sectors.begin(); - size_t bytes_consumed = 0; - std::unique_ptr encoder; - std::unique_ptr segment; - while(bytes_consumed < track_length) { - // Next event is either the next sector or the end of the track. Let's see. - size_t bytes_to_consume = - ((next_sector != sectors.end()) ? - next_sector->track_offset_of_header : track_length) - bytes_consumed; - - // Write from bits_written to bits_written + bits_to_consume from track_data - // to an encoder. If there is no encoder right now, create one. - if(!encoder) { - segment.reset(new PCMSegment); - encoder = Encodings::MFM::GetMFMEncoder(segment->data); - } - - // Output bytes up to the sector. - while(bytes_to_consume--) { - encoder->add_byte(track_data[bytes_consumed]); - ++bytes_consumed; - } - - // Chuck out a sector if it's time for one. - if(next_sector != sectors.end()) { - // Output header. - encoder->add_ID_address_mark(); // This is four 'bytes', but pretend it's three. - encoder->add_byte(next_sector->address[0]); - encoder->add_byte(next_sector->address[1]); - encoder->add_byte(next_sector->address[2]); - encoder->add_byte(next_sector->address[3]); - if(next_sector->address_has_crc) { - encoder->add_byte(next_sector->address[4]); - encoder->add_byte(next_sector->address[5]); - } else { - encoder->add_crc((next_sector->status & 0x18) == 0x18); - } - bytes_consumed += 9; - - if(!(next_sector->status & 0x10)) { - while(bytes_consumed < next_sector->track_offset_of_data) { - encoder->add_byte(track_data[bytes_consumed]); - ++bytes_consumed; - } - - encoder->add_data_address_mark(); // Also four bytes, which we'll model as three. - for(const auto byte: next_sector->contents) { - encoder->add_byte(byte); - } - encoder->add_crc(next_sector->status & 0x8); - bytes_consumed += next_sector->contents.size() + 5; - } - - ++next_sector; - } - } - - return std::make_shared(*segment); - }*/ - } From b3b2e18c4bc2c8b12eca0c42ae00e96a492d1dc9 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 12 Jan 2020 22:23:34 -0500 Subject: [PATCH 8/8] Ensures head and track counts are reported accurately. --- Storage/Disk/DiskImage/Formats/STX.cpp | 14 ++++++++++---- Storage/Disk/DiskImage/Formats/STX.hpp | 2 ++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Storage/Disk/DiskImage/Formats/STX.cpp b/Storage/Disk/DiskImage/Formats/STX.cpp index 5a6cc10bc..38c499076 100644 --- a/Storage/Disk/DiskImage/Formats/STX.cpp +++ b/Storage/Disk/DiskImage/Formats/STX.cpp @@ -280,8 +280,8 @@ STX::STX(const std::string &file_name) : file_(file_name) { // Skip: tool used, 2 reserved bytes. file_.seek(4, SEEK_CUR); - // Grab the track count and test for a new-style encoding, and skip a reserved area. - track_count_ = file_.get8(); + // Skip the track count, test for a new-style encoding, skip a reserved area. + file_.seek(1, SEEK_CUR); is_new_format_ = file_.get8() == 2; file_.seek(4, SEEK_CUR); @@ -301,6 +301,8 @@ STX::STX(const std::string &file_name) : file_(file_name) { // 12 2 Total number of bytes on track. // 14 1 Track number (b7 = side, b0-b6 = track). // 15 1 Track type. + track_count_ = 0; + head_count_ = 1; while(true) { const long offset = file_.tell(); const uint32_t size = file_.get32le(); @@ -312,17 +314,21 @@ STX::STX(const std::string &file_name) : file_(file_name) { const uint8_t track_position = file_.get8(); offset_by_track_[track_position] = offset; + // Update the maximum surface dimensions. + track_count_ = std::max(track_count_, track_position & 0x7f); + head_count_ = std::max(head_count_, ((track_position & 0x80) >> 6)); + // Seek next track start. file_.seek(offset + size, SEEK_SET); } } HeadPosition STX::get_maximum_head_position() { - return HeadPosition(80); + return HeadPosition(track_count_ + 1); // Same issue as MSA; must fix! } int STX::get_head_count() { - return 2; + return head_count_; } std::shared_ptr<::Storage::Disk::Track> STX::get_track_at_position(::Storage::Disk::Track::Address address) { diff --git a/Storage/Disk/DiskImage/Formats/STX.hpp b/Storage/Disk/DiskImage/Formats/STX.hpp index dc21028dc..c7aba86d8 100644 --- a/Storage/Disk/DiskImage/Formats/STX.hpp +++ b/Storage/Disk/DiskImage/Formats/STX.hpp @@ -38,6 +38,8 @@ class STX: public DiskImage { FileHolder file_; int track_count_; + int head_count_; + bool is_new_format_; long offset_by_track_[256]; };