diff --git a/Analyser/Static/Acorn/Disk.cpp b/Analyser/Static/Acorn/Disk.cpp index eada88f3c..220f6d5c9 100644 --- a/Analyser/Static/Acorn/Disk.cpp +++ b/Analyser/Static/Acorn/Disk.cpp @@ -50,7 +50,10 @@ std::unique_ptr Analyser::Static::Acorn::GetDFSCatalogue(const std::s new_file.name = name; new_file.load_address = uint32_t(details->samples[0][file_offset] | (details->samples[0][file_offset+1] << 8) | ((details->samples[0][file_offset+6]&0x0c) << 14)); new_file.execution_address = uint32_t(details->samples[0][file_offset+2] | (details->samples[0][file_offset+3] << 8) | ((details->samples[0][file_offset+6]&0xc0) << 10)); - new_file.is_protected = names->samples[0][file_offset + 7] & 0x80; + if(names->samples[0][file_offset + 7] & 0x80) { + // File is locked; it may not be altered or deleted. + new_file.flags |= File::Flags::Locked; + } long data_length = long(details->samples[0][file_offset+4] | (details->samples[0][file_offset+5] << 8) | ((details->samples[0][file_offset+6]&0x30) << 12)); int start_sector = details->samples[0][file_offset+7] | ((details->samples[0][file_offset+6]&0x03) << 8); @@ -69,11 +72,16 @@ std::unique_ptr Analyser::Static::Acorn::GetDFSCatalogue(const std::s new_file.data.insert(new_file.data.end(), next_sector->samples[0].begin(), next_sector->samples[0].begin() + length_from_sector); data_length -= length_from_sector; } - if(!data_length) catalogue->files.push_back(new_file); + if(!data_length) catalogue->files.push_back(std::move(new_file)); } return catalogue; } + +/* + Primary resource used: "Acorn 8-Bit ADFS Filesystem Structure"; + http://mdfs.net/Docs/Comp/Disk/Format/ADFS +*/ std::unique_ptr Analyser::Static::Acorn::GetADFSCatalogue(const std::shared_ptr &disk) { auto catalogue = std::make_unique(); Storage::Encodings::MFM::Parser parser(true, disk); @@ -101,5 +109,73 @@ std::unique_ptr Analyser::Static::Acorn::GetADFSCatalogue(const std:: case 3: catalogue->bootOption = Catalogue::BootOption::ExecBOOT; break; } + // Parse the root directory, at least. + for(std::size_t file_offset = 0x005; file_offset < 0x4cb; file_offset += 0x1a) { + // Obtain the name, which will be at most ten characters long, and will + // be terminated by either a NULL character or a \r. + char name[11]; + std::size_t c = 0; + for(; c < 10; c++) { + const char next = root_directory[file_offset + c] & 0x7f; + name[c] = next; + if(next == '\0' || next == '\r') break; + } + name[c] = '\0'; + + // Skip if the name is empty. + if(name[0] == '\0') continue; + + // Populate a file then. + File new_file; + new_file.name = name; + new_file.flags = + (root_directory[file_offset + 0] & 0x80 ? File::Flags::Readable : 0) | + (root_directory[file_offset + 1] & 0x80 ? File::Flags::Writable : 0) | + (root_directory[file_offset + 2] & 0x80 ? File::Flags::Locked : 0) | + (root_directory[file_offset + 3] & 0x80 ? File::Flags::IsDirectory : 0) | + (root_directory[file_offset + 4] & 0x80 ? File::Flags::ExecuteOnly : 0) | + (root_directory[file_offset + 5] & 0x80 ? File::Flags::PubliclyReadable : 0) | + (root_directory[file_offset + 6] & 0x80 ? File::Flags::PubliclyWritable : 0) | + (root_directory[file_offset + 7] & 0x80 ? File::Flags::PubliclyExecuteOnly : 0) | + (root_directory[file_offset + 8] & 0x80 ? File::Flags::IsPrivate : 0); + + new_file.load_address = + (uint32_t(root_directory[file_offset + 0x0a]) << 0) | + (uint32_t(root_directory[file_offset + 0x0b]) << 8) | + (uint32_t(root_directory[file_offset + 0x0c]) << 16) | + (uint32_t(root_directory[file_offset + 0x0d]) << 24); + + new_file.execution_address = + (uint32_t(root_directory[file_offset + 0x0e]) << 0) | + (uint32_t(root_directory[file_offset + 0x0f]) << 8) | + (uint32_t(root_directory[file_offset + 0x10]) << 16) | + (uint32_t(root_directory[file_offset + 0x11]) << 24); + + new_file.sequence_number = root_directory[file_offset + 0x19]; + + const uint32_t size = + (uint32_t(root_directory[file_offset + 0x12]) << 0) | + (uint32_t(root_directory[file_offset + 0x13]) << 8) | + (uint32_t(root_directory[file_offset + 0x14]) << 16) | + (uint32_t(root_directory[file_offset + 0x15]) << 24); + + uint32_t start_sector = + (uint32_t(root_directory[file_offset + 0x16]) << 0) | + (uint32_t(root_directory[file_offset + 0x17]) << 8) | + (uint32_t(root_directory[file_offset + 0x18]) << 16); + + new_file.data.reserve(size); + while(new_file.data.size() < size) { + const Storage::Encodings::MFM::Sector *const sector = parser.get_sector(start_sector / (80 * 16), (start_sector / 16) % 80, start_sector % 16); + if(!sector) break; + + const auto length_from_sector = std::min(size - new_file.data.size(), sector->samples[0].size()); + new_file.data.insert(new_file.data.end(), sector->samples[0].begin(), sector->samples[0].begin() + ssize_t(length_from_sector)); + ++start_sector; + } + + catalogue->files.push_back(std::move(new_file)); + } + return catalogue; } diff --git a/Analyser/Static/Acorn/File.hpp b/Analyser/Static/Acorn/File.hpp index 11df52237..f0a9d612e 100644 --- a/Analyser/Static/Acorn/File.hpp +++ b/Analyser/Static/Acorn/File.hpp @@ -19,19 +19,38 @@ namespace Acorn { struct File { std::string name; - uint32_t load_address; - uint32_t execution_address; - bool is_protected; + uint32_t load_address = 0; + uint32_t execution_address = 0; + + enum Flags: uint16_t { + Readable = 1 << 0, + Writable = 1 << 1, + Locked = 1 << 2, + IsDirectory = 1 << 3, + ExecuteOnly = 1 << 4, + PubliclyReadable = 1 << 5, + PubliclyWritable = 1 << 6, + PubliclyExecuteOnly = 1 << 7, + IsPrivate = 1 << 8, + }; + uint16_t flags = Flags::Readable | Flags::Readable | Flags::PubliclyReadable | Flags::PubliclyWritable; + uint8_t sequence_number = 0; + std::vector data; + /// Describes a single chunk of file data; these relate to the tape and ROM filing system. + /// The File-level fields contain a 'definitive' version of the load and execution addresses, + /// but both of those filing systems also store them per chunk. + /// + /// Similarly, the file-level data will contain the aggregate data of all chunks. struct Chunk { std::string name; - uint32_t load_address; - uint32_t execution_address; - uint16_t block_number; - uint16_t block_length; - uint8_t block_flag; - uint32_t next_address; + uint32_t load_address = 0; + uint32_t execution_address = 0; + uint16_t block_number = 0; + uint16_t block_length = 0; + uint32_t next_address = 0; + uint8_t block_flag = 0; bool header_crc_matched; bool data_crc_matched; diff --git a/Analyser/Static/Acorn/StaticAnalyser.cpp b/Analyser/Static/Acorn/StaticAnalyser.cpp index 51d8b394a..76d655cfc 100644 --- a/Analyser/Static/Acorn/StaticAnalyser.cpp +++ b/Analyser/Static/Acorn/StaticAnalyser.cpp @@ -12,6 +12,8 @@ #include "Tape.hpp" #include "Target.hpp" +#include + using namespace Analyser::Static::Acorn; static std::vector> @@ -77,8 +79,8 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &me if(!files.empty()) { bool is_basic = true; - // protected files are always for *RUNning only - if(files.front().is_protected) is_basic = false; + // If a file is execute-only, that means *RUN. + if(files.front().flags & File::Flags::ExecuteOnly) is_basic = false; // check also for a continuous threading of BASIC lines; if none then this probably isn't BASIC code, // so that's also justification to *RUN @@ -108,15 +110,37 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &me dfs_catalogue = GetDFSCatalogue(disk); if(dfs_catalogue == nullptr) adfs_catalogue = GetADFSCatalogue(disk); if(dfs_catalogue || adfs_catalogue) { + // Accept the disk and determine whether DFS or ADFS ROMs are implied. target->media.disks = media.disks; - target->has_dfs = !!dfs_catalogue; - target->has_adfs = !!adfs_catalogue; + target->has_dfs = bool(dfs_catalogue); + target->has_adfs = bool(adfs_catalogue); + // Check whether a simple shift+break will do for loading this disk. Catalogue::BootOption bootOption = (dfs_catalogue ?: adfs_catalogue)->bootOption; - if(bootOption != Catalogue::BootOption::None) + if(bootOption != Catalogue::BootOption::None) { target->should_shift_restart = true; - else + } else { target->loading_command = "*CAT\n"; + } + + // Check whether adding the AP6 ROM is justified. + // For now this is an incredibly dense text search; + // if any of the commands that aren't usually present + // on a stock Electron are here, add the AP6 ROM and + // some sideways RAM such that the SR commands are useful. + for(const auto &file: dfs_catalogue ? dfs_catalogue->files : adfs_catalogue->files) { + for(const auto &command: { + "AQRPAGE", "BUILD", "DUMP", "FORMAT", "INSERT", "LANG", "LIST", "LOADROM", + "LOCK", "LROMS", "RLOAD", "ROMS", "RSAVE", "SAVEROM", "SRLOAD", "SRPAGE", + "SRUNLOCK", "SRWIPE", "TUBE", "TYPE", "UNLOCK", "UNPLUG", "UROMS", + "VERIFY", "ZERO" + }) { + if(std::search(file.data.begin(), file.data.end(), command, command+strlen(command)) != file.data.end()) { + target->has_ap6_rom = true; + target->has_sideways_ram = true; + } + } + } } } diff --git a/Analyser/Static/Acorn/Tape.cpp b/Analyser/Static/Acorn/Tape.cpp index 461f01960..c812d21ad 100644 --- a/Analyser/Static/Acorn/Tape.cpp +++ b/Analyser/Static/Acorn/Tape.cpp @@ -109,7 +109,12 @@ static std::unique_ptr GetNextFile(std::deque &chunks) { file->name = file->chunks.front().name; file->load_address = file->chunks.front().load_address; file->execution_address = file->chunks.front().execution_address; - file->is_protected = !!(file->chunks.back().block_flag & 0x01); // I think the last flags are the ones that count; TODO: check. + // I think the final chunk's flags are the ones that count; TODO: check. + if(file->chunks.back().block_flag & 0x01) { + // File is locked, which in more generalised terms means it is + // for execution only. + file->flags |= File::Flags::ExecuteOnly; + } // copy all data into a single big block for(File::Chunk chunk : file->chunks) { diff --git a/Analyser/Static/Acorn/Target.hpp b/Analyser/Static/Acorn/Target.hpp index 1ef46c291..f415f4b30 100644 --- a/Analyser/Static/Acorn/Target.hpp +++ b/Analyser/Static/Acorn/Target.hpp @@ -20,6 +20,8 @@ namespace Acorn { struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl { bool has_adfs = false; bool has_dfs = false; + bool has_ap6_rom = false; + bool has_sideways_ram = false; bool should_shift_restart = false; std::string loading_command; @@ -27,6 +29,8 @@ struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl< if(needs_declare()) { DeclareField(has_adfs); DeclareField(has_dfs); + DeclareField(has_ap6_rom); + DeclareField(has_sideways_ram); } } }; diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp index 35e11e5ce..ad80cd8fb 100644 --- a/Analyser/Static/StaticAnalyser.cpp +++ b/Analyser/Static/StaticAnalyser.cpp @@ -98,6 +98,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: Format("81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 81 Format("a26", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600) // A26 Format("adf", result.disks, Disk::DiskImageHolder, TargetPlatform::Acorn) // ADF + Format("adl", result.disks, Disk::DiskImageHolder, TargetPlatform::Acorn) // ADL Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge) // BIN (cartridge dump) Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX) // CAS Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC) // CDT diff --git a/Machines/Electron/Electron.cpp b/Machines/Electron/Electron.cpp index 53fb9b028..80bc3ccf7 100644 --- a/Machines/Electron/Electron.cpp +++ b/Machines/Electron/Electron.cpp @@ -72,6 +72,10 @@ class ConcreteMachine: if(target.has_dfs) { required_roms.emplace_back(machine_name, "the 1770 DFS ROM", "DFS-1770-2.20.rom", 16*1024, 0xf3dc9bc5); } + const size_t ap6_rom_position = required_roms.size(); + if(target.has_ap6_rom) { + required_roms.emplace_back(machine_name, "the 8kb Advanced Plus 6 ROM", "AP6v133.rom", 8*1024, 0xe0013cfc); + } const auto roms = rom_fetcher(required_roms); for(const auto &rom: roms) { @@ -82,6 +86,15 @@ class ConcreteMachine: set_rom(ROM::BASIC, *roms[0], false); set_rom(ROM::OS, *roms[1], false); + /* + ROM slot mapping applied: + + * the keyboard and BASIC ROMs occupy slots 8, 9, 10 and 11; + * the DFS, if in use, occupies slot 1; + * the ADFS, if in use, occupies slots 4 and 5; + * the AP6, if in use, occupies slot 15; and + * if sideways RAM was asked for, all otherwise unused slots are populated with sideways RAM. + */ if(target.has_dfs || target.has_adfs) { plus3_ = std::make_unique(); @@ -94,6 +107,18 @@ class ConcreteMachine: } } + if(target.has_ap6_rom) { + set_rom(ROM::Slot15, *roms[ap6_rom_position], true); + } + + if(target.has_sideways_ram) { + for(int c = 0; c < 16; c++) { + if(rom_inserted_[c]) continue; + if(c >= int(ROM::Keyboard) && c < int(ROM::BASIC)+1) continue; + set_sideways_ram(ROM(c)); + } + } + insert_media(target.media); if(!target.loading_command.empty()) { @@ -517,8 +542,20 @@ class ConcreteMachine: rom_ptr += size_to_copy; } - if(int(slot) < 16) + if(int(slot) < 16) { rom_inserted_[int(slot)] = true; + } + } + + /*! + Enables @c slot as sideways RAM; ensures that it does not currently contain a valid ROM signature. + */ + void set_sideways_ram(ROM slot) { + std::memset(roms_[int(slot)], 0xff, 16*1024); + if(int(slot) < 16) { + rom_inserted_[int(slot)] = true; + rom_write_masks_[int(slot)] = true; + } } // MARK: - Work deferral updates. diff --git a/ROMImages/Electron/readme.txt b/ROMImages/Electron/readme.txt index accfc031e..995145ee9 100644 --- a/ROMImages/Electron/readme.txt +++ b/ROMImages/Electron/readme.txt @@ -4,13 +4,41 @@ Expected files: basic.rom os.rom -plus1.rom -DFS-1770-2.20.rom -ADFS-E00_1.rom +DFS-1770-2.20.rom — used only if the user opens a DFS disk image +ADFS-E00_1.rom — used only if the user opens an ADFS disk image ADFS-E00_2.rom +AP6v133.rom — used only if the user opens a disk image that makes use of any of the commands given below. -Likely to be desired in the future: +Possibly to be desired in the future: +* adfs.rom +* ElectronExpansionRomPresAP2-v1.23.rom +* os300.rom -adfs.rom -ElectronExpansionRomPresAP2-v1.23.rom -os300.rom +Commands that trigger a request for the AP6v133 ROM: +* *AQRPAGE +* *BUILD +* *DUMP +* *FORMAT +* *INSERT +* *LANG +* *LIST +* *LOADROM +* *LOCK +* *LROMS +* *RLOAD +* *ROMS +* *RSAVE +* *SAVEROM +* *SRLOAD +* *SRLOCK +* *SRPAGE +* *SRSAVE +* *SRUNLOCK +* *SRWIPE +* *TUBE +* *TYPE +* *UNLOCK +* *UNPLUG +* *UROMS +* *VERIFY +* *ZERO \ No newline at end of file diff --git a/Storage/Disk/DiskImage/Formats/AcornADF.cpp b/Storage/Disk/DiskImage/Formats/AcornADF.cpp index 7fc657273..e5b78e686 100644 --- a/Storage/Disk/DiskImage/Formats/AcornADF.cpp +++ b/Storage/Disk/DiskImage/Formats/AcornADF.cpp @@ -18,12 +18,13 @@ namespace { using namespace Storage::Disk; AcornADF::AcornADF(const std::string &file_name) : MFMSectorDump(file_name) { - // very loose validation: the file needs to be a multiple of 256 bytes - // and not ungainly large + // Check that the disk image contains a whole number of sector. if(file_.stats().st_size % off_t(128 << sector_size)) throw Error::InvalidFormat; + + // Check that the disk image is at least large enough to hold an ADFS catalogue. if(file_.stats().st_size < 7 * off_t(128 << sector_size)) throw Error::InvalidFormat; - // check that the initial directory's 'Hugo's are present + // Check that the initial directory's 'Hugo's are present. file_.seek(513, SEEK_SET); uint8_t bytes[4]; file_.read(bytes, 4); @@ -33,6 +34,10 @@ AcornADF::AcornADF(const std::string &file_name) : MFMSectorDump(file_name) { 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 * off_t(128 << sector_size) * 80); + + // Announce disk geometry. set_geometry(sectors_per_track, sector_size, 0, true); } @@ -41,9 +46,9 @@ HeadPosition AcornADF::get_maximum_head_position() { } int AcornADF::get_head_count() { - return 1; + return head_count_; } long AcornADF::get_file_offset_for_position(Track::Address address) { - return address.position.as_int() * (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 c0d040d56..1988579f1 100644 --- a/Storage/Disk/DiskImage/Formats/AcornADF.hpp +++ b/Storage/Disk/DiskImage/Formats/AcornADF.hpp @@ -34,6 +34,7 @@ class AcornADF: public MFMSectorDump { private: long get_file_offset_for_position(Track::Address address) final; + int head_count_ = 1; }; }