diff --git a/Analyser/Static/Acorn/Disk.cpp b/Analyser/Static/Acorn/Disk.cpp index 9b1516381..5e06708e4 100644 --- a/Analyser/Static/Acorn/Disk.cpp +++ b/Analyser/Static/Acorn/Disk.cpp @@ -13,6 +13,7 @@ #include "../../../Numeric/CRC.hpp" #include +#include using namespace Analyser::Static::Acorn; @@ -99,8 +100,11 @@ std::unique_ptr Analyser::Static::Acorn::GetADFSCatalogue(const std:: // Quick sanity checks. if(root_directory[0x4cb]) return nullptr; - if(root_directory[1] != 'H' || root_directory[2] != 'u' || root_directory[3] != 'g' || root_directory[4] != 'o') return nullptr; - if(root_directory[0x4FB] != 'H' || root_directory[0x4FC] != 'u' || root_directory[0x4FD] != 'g' || root_directory[0x4FE] != 'o') return nullptr; + catalogue->is_hugo = !memcmp(&root_directory[1], "Hugo", 4) && !memcmp(&root_directory[0x4FB], "Hugo", 4); + const bool is_nick = !memcmp(&root_directory[1], "Nick", 4) && !memcmp(&root_directory[0x4FB], "Nick", 4); + if(!catalogue->is_hugo && !is_nick) { + return nullptr; + } switch(free_space_map_second_half->samples[0][0xfd]) { default: catalogue->bootOption = Catalogue::BootOption::None; break; diff --git a/Analyser/Static/Acorn/Disk.hpp b/Analyser/Static/Acorn/Disk.hpp index f77a0fe47..a7305955a 100644 --- a/Analyser/Static/Acorn/Disk.hpp +++ b/Analyser/Static/Acorn/Disk.hpp @@ -15,6 +15,7 @@ namespace Analyser::Static::Acorn { /// Describes a DFS- or ADFS-format catalogue(/directory): the list of files available and the catalogue's boot option. struct Catalogue { + bool is_hugo = false; std::string name; std::vector files; enum class BootOption { diff --git a/Analyser/Static/Acorn/StaticAnalyser.cpp b/Analyser/Static/Acorn/StaticAnalyser.cpp index d5a34167b..7246972de 100644 --- a/Analyser/Static/Acorn/StaticAnalyser.cpp +++ b/Analyser/Static/Acorn/StaticAnalyser.cpp @@ -62,10 +62,10 @@ static std::vector> Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &media, const std::string &, TargetPlatform::IntType) { auto target = std::make_unique(); - // strip out inappropriate cartridges + // Strip out inappropriate cartridges. target->media.cartridges = AcornCartridgesFrom(media.cartridges); - // if there are any tapes, attempt to get data from the first + // If there are any tapes, attempt to get data from the first. if(!media.tapes.empty()) { std::shared_ptr tape = media.tapes.front(); std::vector files = GetFiles(tape); @@ -101,11 +101,15 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &me } if(!media.disks.empty()) { + // TODO: below requires an [8-bit compatible] 'Hugo' ADFS catalogue, disallowing + // [Archimedes-exclusive] 'Nick' catalogues. + // + // Would be better to form the appropriate target in the latter case. std::shared_ptr disk = media.disks.front(); std::unique_ptr dfs_catalogue, adfs_catalogue; dfs_catalogue = GetDFSCatalogue(disk); if(dfs_catalogue == nullptr) adfs_catalogue = GetADFSCatalogue(disk); - if(dfs_catalogue || adfs_catalogue) { + if(dfs_catalogue || (adfs_catalogue && adfs_catalogue->is_hugo)) { // Accept the disk and determine whether DFS or ADFS ROMs are implied. // Use the Pres ADFS if using an ADFS, as it leaves Page at &EOO. target->media.disks = media.disks; diff --git a/Storage/Disk/DiskImage/Formats/AcornADF.cpp b/Storage/Disk/DiskImage/Formats/AcornADF.cpp index 57e333c19..02947932b 100644 --- a/Storage/Disk/DiskImage/Formats/AcornADF.cpp +++ b/Storage/Disk/DiskImage/Formats/AcornADF.cpp @@ -10,36 +10,105 @@ #include "Utility/ImplicitSectors.hpp" -namespace { - constexpr int sectors_per_track = 16; - constexpr int sector_size = 1; -} +#include using namespace Storage::Disk; AcornADF::AcornADF(const std::string &file_name) : MFMSectorDump(file_name) { // Check that the disk image contains a whole number of sector. using sizeT = decltype(file_.stats().st_size); - if(file_.stats().st_size % sizeT(128 << sector_size)) throw Error::InvalidFormat; + const auto size = file_.stats().st_size; + + if(size < 1024) throw Error::InvalidFormat; + + // Definitely true: a directory signature of 'Hugo' can be read by both 8-bit + // machines and the Archimedes. 'Nick' can be read only by the Archimedes. + // + // https://mdfs.net/Docs/Comp/Disk/Format/ADFS then falsely states that: + // + // The type of ADFS filesystem can be determined by looking for the "Hugo"/ + // "Nick" identifier that marks the start of the root directory 512 bytes into + // the filesystem and 1024 bytes in. + // + // In terms of .ADF files: + // + // all 8-bit files seem to have 'Hugo' at offset 513; + // ADFS-D (early Arc, late BBC Master) has 'Nick' or 'Hugo' at 1025; but + // ADFS-E (most Arc) has 'Hugo' at 2049. + // + // Even allowing for the document having failed to account for the directory ID, + // I can't reconcile that 2049 offset with being 1024 bytes into the file system. + // + // That document claims that ADFS-D and ADFS-E are logically interleaved but + // https://github.com/android444/fluxengine/blob/master/doc/disk-acornadfs.md + // states that: + // + // Acorn logical block numbering goes all the way up side 0 and then all the + // way up side 1. However, FluxEngine uses traditional disk images with alternating + // sides, with the blocks from track 0 side 0 then track 0 side 1 then track 1 side 0 etc. + // Most Acorn emulators will use both formats, but they might require nudging as the side + // order can't be reliably autodetected. + // + // So then .ADF files might be track-interleaved and might not be. + + const auto has_identifier = [&](long location, bool permit_hugo, bool permit_nick) -> bool { + file_.seek(location, SEEK_SET); + + uint8_t bytes[4]; + file_.read(bytes, 4); + + return + (permit_hugo && !memcmp(bytes, "Hugo", 4)) || + (permit_nick && !memcmp(bytes, "Nick", 4)); + }; + + const auto file_size = file_.stats().st_size; + Encodings::MFM::Density density = Encodings::MFM::Density::Double; + if(has_identifier(513, true, false)) { + // One of: + // + // ADFS-S: 1 side, 40 tracks, 16 sectors, 256 bytes = 160K old map, old dir + // ADFS-M: 1 side, 80 tracks, 16 sectors, 256 bytes = 320K old map, old dir + // ADFS-L: 2 sides, 80 tracks, 16 sectors, 256 bytes = 640K old map, old dir + + head_count_ = file_.stats().st_size > 80*16*256 ? 2 : 1; + sector_size_ = 1; + sectors_per_track_ = 16; + } else if(has_identifier(1025, true, true)) { + // ADFS-D: 80 tracks, 2 sides, 5 sectors, 1024 bytes = 800K old map, new dir + head_count_ = 2; + sector_size_ = 3; + sectors_per_track_ = 5; + } else if(has_identifier(2049, false, true)) { + // One of: + // + // ADFS-E: 80 tracks, 2 sides, 5 sectors, 1024 bytes = 800K new map, new dir + // ADFS-F: 80 tracks, 2 sides, 10 sectors, 1024 bytes = 1600K new map, new dir + // ADFS-G: 80 tracks, 2 sides, 20 sectors, 1024 bytes = 3200K new map, new dir + + head_count_ = 2; + sector_size_ = 3; + if(file_size > 80*2*10*1024) { + sectors_per_track_ = 20; + density = Encodings::MFM::Density::High; // Or, presumably, higher than high? + } else if(file_size > 80*2*10*1024) { + sectors_per_track_ = 10; + density = Encodings::MFM::Density::High; + } else { + sectors_per_track_ = 5; + } + } else { + throw Error::InvalidFormat; + } + + // TODO: possibly this image is side-interleaved if sectors per track is less than 16. + // Figure that out. // Check that the disk image is at least large enough to hold an ADFS catalogue. - if(file_.stats().st_size < 7 * sizeT(128 << sector_size)) throw Error::InvalidFormat; - - // Check that the initial directory's 'Hugo's are present. - file_.seek(513, SEEK_SET); - uint8_t bytes[4]; - file_.read(bytes, 4); - if(bytes[0] != 'H' || bytes[1] != 'u' || bytes[2] != 'g' || bytes[3] != 'o') throw Error::InvalidFormat; - - file_.seek(0x6fb, SEEK_SET); - file_.read(bytes, 4); - if(bytes[0] != 'H' || bytes[1] != 'u' || bytes[2] != 'g' || bytes[3] != 'o') throw Error::InvalidFormat; - - // Pick a number of heads; treat this image as double sided if it's too large to be single-sided. - head_count_ = 1 + (file_.stats().st_size > sectors_per_track * sizeT(128 << sector_size) * 80); + if(file_.stats().st_size < 7 * sizeT(128 << sector_size_)) throw Error::InvalidFormat; // Announce disk geometry. - set_geometry(sectors_per_track, sector_size, 0, Encodings::MFM::Density::Double); + set_geometry(sectors_per_track_, sector_size_, 0, density); } HeadPosition AcornADF::get_maximum_head_position() { @@ -51,5 +120,5 @@ int AcornADF::get_head_count() { } long AcornADF::get_file_offset_for_position(Track::Address address) { - return (address.position.as_int() * head_count_ + address.head) * (128 << sector_size) * sectors_per_track; + return (address.position.as_int() * head_count_ + address.head) * (128 << sector_size_) * sectors_per_track_; } diff --git a/Storage/Disk/DiskImage/Formats/AcornADF.hpp b/Storage/Disk/DiskImage/Formats/AcornADF.hpp index 54a8d12e7..68a446ec7 100644 --- a/Storage/Disk/DiskImage/Formats/AcornADF.hpp +++ b/Storage/Disk/DiskImage/Formats/AcornADF.hpp @@ -33,6 +33,8 @@ class AcornADF: public MFMSectorDump { private: long get_file_offset_for_position(Track::Address address) final; int head_count_ = 1; + uint8_t sector_size_ = 1; + int sectors_per_track_ = 16; }; } diff --git a/Storage/TargetPlatforms.hpp b/Storage/TargetPlatforms.hpp index c352efe9f..94ecb1473 100644 --- a/Storage/TargetPlatforms.hpp +++ b/Storage/TargetPlatforms.hpp @@ -23,24 +23,25 @@ enum Type: IntType { AcornAtom = 1 << 5, AcornElectron = 1 << 6, Amiga = 1 << 7, - BBCMaster = 1 << 8, - BBCModelA = 1 << 9, - BBCModelB = 1 << 10, - Coleco = 1 << 11, - Commodore = 1 << 12, - DiskII = 1 << 13, - Enterprise = 1 << 14, - Sega = 1 << 15, - Macintosh = 1 << 16, - MSX = 1 << 17, - Oric = 1 << 18, - ZX80 = 1 << 19, - ZX81 = 1 << 20, - ZXSpectrum = 1 << 21, - PCCompatible = 1 << 22, - FAT12 = 1 << 23, + Archimedes = 1 << 8, + BBCMaster = 1 << 9, + BBCModelA = 1 << 10, + BBCModelB = 1 << 11, + Coleco = 1 << 12, + Commodore = 1 << 13, + DiskII = 1 << 14, + Enterprise = 1 << 15, + Sega = 1 << 16, + Macintosh = 1 << 17, + MSX = 1 << 18, + Oric = 1 << 19, + ZX80 = 1 << 20, + ZX81 = 1 << 21, + ZXSpectrum = 1 << 22, + PCCompatible = 1 << 23, + FAT12 = 1 << 24, - Acorn = AcornAtom | AcornElectron | BBCMaster | BBCModelA | BBCModelB, + Acorn = AcornAtom | AcornElectron | BBCMaster | BBCModelA | BBCModelB | Archimedes, ZX8081 = ZX80 | ZX81, AllCartridge = Atari2600 | AcornElectron | Coleco | MSX, AllDisk = Acorn | AmstradCPC | Commodore | Oric | MSX | ZXSpectrum | Macintosh | AtariST | DiskII | Amiga | PCCompatible | FAT12,