// // AcornAnalyser.cpp // Clock Signal // // Created by Thomas Harte on 29/08/2016. // Copyright 2016 Thomas Harte. All rights reserved. // #include "StaticAnalyser.hpp" #include "Disk.hpp" #include "Tape.hpp" #include "Target.hpp" #include "Numeric/StringSimilarity.hpp" #include #include using namespace Analyser::Static::Acorn; namespace { bool is_basic(const File &file) { std::size_t pointer = 0; const uint8_t *const data = file.data.data(); const std::size_t data_size = file.data.size(); while(true) { if(pointer >= data_size-1 || data[pointer] != 0x0d) { return false; } if((data[pointer+1]&0x7f) == 0x7f) break; pointer += data[pointer+3]; } return true; } } static std::vector> AcornCartridgesFrom(const std::vector> &cartridges) { std::vector> acorn_cartridges; for(const auto &cartridge : cartridges) { const auto &segments = cartridge->get_segments(); // Only one mapped item is allowed. if(segments.size() != 1) continue; // Cartridges must be 8 or 16 kb in size. const Storage::Cartridge::Cartridge::Segment &segment = segments.front(); if(segment.data.size() != 0x4000 && segment.data.size() != 0x2000) continue; // Check copyright string. const uint8_t copyright_offset = segment.data[7]; if( segment.data[copyright_offset] != 0x00 || segment.data[copyright_offset+1] != 0x28 || segment.data[copyright_offset+2] != 0x43 || segment.data[copyright_offset+3] != 0x29 ) continue; // Check language entry point. if(!( (segment.data[0] == 0x00 && segment.data[1] == 0x00 && segment.data[2] == 0x00) || (segment.data[0] != 0x00 && segment.data[2] >= 0x80 && segment.data[2] < 0xc0) )) continue; // Check service entry point. if(!(segment.data[5] >= 0x80 && segment.data[5] < 0xc0)) continue; // Probability of a random binary blob that isn't an Acorn ROM proceeding to here: // 1/(2^32) * // ( ((2^24)-1)/(2^24)*(1/4) + 1/(2^24) ) * // 1/4 // = something very improbable, around 1/16th of 1 in 2^32, but not exactly. acorn_cartridges.push_back(cartridge); } return acorn_cartridges; } Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets( const Media &media, const std::string &file_name, TargetPlatform::IntType, bool ) { const auto early_exit = [](auto &ptr) { TargetList list; list.push_back(std::move(ptr)); return list; }; auto targetElectron = std::make_unique(); auto targetBBC = std::make_unique(); auto targetArchimedes = std::make_unique(); int bbc_hits = 0; int electron_hits = 0; bool format_prefers_bbc = false; // Copy appropriate cartridges to the 8-bit targets. targetElectron->media.cartridges = AcornCartridgesFrom(media.cartridges); targetBBC->media.cartridges = AcornCartridgesFrom(media.cartridges); // If there are tapes, attempt to get data from the first. if(!media.tapes.empty()) { std::shared_ptr tape = media.tapes.front(); auto serialiser = tape->serialiser(); std::vector files = GetFiles(*serialiser); // Continue only if there are any files. if(!files.empty()) { // Inspect first file. If it's protected or doesn't look like BASIC // then the loading command is *RUN. Otherwise it's CHAIN"". targetElectron->loading_command = (files.front().flags & File::Flags::ExecuteOnly) || !is_basic(files.front()) ? "*RUN\n" : "CHAIN\"\"\n"; targetElectron->media.tapes = media.tapes; // TODO: my BBC Micro doesn't yet support tapes; evaluate here in the future. } } if(!media.disks.empty()) { std::shared_ptr disk = media.disks.front(); std::unique_ptr dfs_catalogue, adfs_catalogue; // Get any sort of catalogue that can be found. dfs_catalogue = GetDFSCatalogue(disk); if(dfs_catalogue == nullptr) adfs_catalogue = GetADFSCatalogue(disk); // 8-bit options: DFS and Hugo-style ADFS. if(dfs_catalogue || (adfs_catalogue && !adfs_catalogue->has_large_sectors && adfs_catalogue->is_hugo)) { // Accept the disk and determine whether DFS or ADFS ROMs are implied. // Electron: use the Pres ADFS if using an ADFS, as it leaves Page at &E00. targetElectron->media.disks = media.disks; targetElectron->has_dfs = bool(dfs_catalogue); targetElectron->has_pres_adfs = bool(adfs_catalogue); // BBC: only the 1770 DFS is currently supported, so use that. targetBBC->media.disks = media.disks; targetBBC->has_1770dfs = bool(dfs_catalogue); targetBBC->has_adfs = bool(adfs_catalogue); // Special case: if there's only one file, and it is called CPMDISC, // select a BBC with the Z80 second processor. const auto &files = dfs_catalogue ? dfs_catalogue->files : adfs_catalogue->files; if(files.size() == 1 && files[0].name == "$.CPMDISC") { targetBBC->tube_processor = BBCMicroTarget::TubeProcessor::Z80; return early_exit(targetBBC); } // Check whether a simple shift+break will do for loading this disk. const auto bootOption = (dfs_catalogue ?: adfs_catalogue)->bootOption; if(bootOption != Catalogue::BootOption::None) { targetBBC->should_shift_restart = targetElectron->should_shift_restart = true; } else { // Otherwise: if there's only one BASIC program then chain it. // Failing that, do a *CAT to be communicative. const File *sole_basic_file = nullptr; for(const auto &file: dfs_catalogue ? dfs_catalogue->files : adfs_catalogue->files) { if(is_basic(file)) { if(!sole_basic_file) { sole_basic_file = &file; } else { sole_basic_file = nullptr; break; } } } targetBBC->loading_command = targetElectron->loading_command = sole_basic_file ? "CHAIN \"" + sole_basic_file->name + "\"\n" : "*CAT\n"; } // Further special case: if any of the files have a top word of 0x0003 then // they're for a 6502 second processor, so provide a BBC with one of those. for(const auto &file: files) { if((file.load_address >> 16) == 3) { targetBBC->tube_processor = BBCMicroTarget::TubeProcessor::WDC65C02; return early_exit(targetBBC); } } // Add a slight preference for the BBC over the Electron, all else being equal, if this is a DFS floppy. format_prefers_bbc = bool(dfs_catalogue); for(const auto &file: files) { // Electron: 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 &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()) { targetElectron->has_ap6_rom = true; targetElectron->has_sideways_ram = true; } } // Look for any 'BBC indicators', i.e. direct access to BBC-specific hardware. // Also currently a dense search. const auto hits = [&](const std::initializer_list collection) { int hits = 0; for(const auto address: collection) { const uint8_t sta_address[3] = { 0x8d, uint8_t(address & 0xff), uint8_t(address >> 8) }; if(std::search( file.data.begin(), file.data.end(), std::begin(sta_address), std::end(sta_address) ) != file.data.end()) { ++hits; } // I think I'll want std::ranges::contains_subrange if/when building for C++23. } return hits; }; bbc_hits += hits({ // The video control registers. 0xfe20, 0xfe21, // The system VIA. 0xfe40, 0xfe41, 0xfe42, 0xfe43, 0xfe44, 0xfe45, 0xfe46, 0xfe47, 0xfe48, 0xfe49, 0xfe4a, 0xfe4b, 0xfe4c, 0xfe4d, 0xfe4e, 0xfe4f, // The user VIA. 0xfe60, 0xfe61, 0xfe62, 0xfe63, 0xfe64, 0xfe65, 0xfe66, 0xfe67, 0xfe68, 0xfe69, 0xfe6a, 0xfe6b, 0xfe6c, 0xfe6d, 0xfe6e, 0xfe6f, }); // BASIC for "MODE7". static constexpr uint8_t mode7[] = {0xeb, 0x37}; bbc_hits += std::search( file.data.begin(), file.data.end(), std::begin(mode7), std::end(mode7) ) != file.data.end(); electron_hits += hits({ // ULA addresses that aren't also the BBC's CRTC. 0xfe03, 0xfe04, 0xfe05, 0xfe06, 0xfe07, 0xfe08, 0xfe09, 0xfe0a, 0xfe0b, 0xfe0c, 0xfe0d, 0xfe0e, 0xfe0f, }); } } else if(adfs_catalogue) { // Archimedes options, implicitly: ADFS, non-Hugo. targetArchimedes->media.disks = media.disks; // Also look for the best possible startup program name, if it can be discerned. std::multimap> options; for(const auto &file: adfs_catalogue->files) { // Skip non-Pling files. if(file.name[0] != '!') continue; // Take whatever else comes with a preference for things that don't // have 'boot' or 'read' in them (the latter of which will tend to be // read_me or read_this or similar). static constexpr char read[] = "read"; static constexpr char boot[] = "boot"; const auto has = [&](const char *begin, const char *end) { return std::search( file.name.begin(), file.name.end(), begin, end - 1, // i.e. don't compare the trailing NULL. [](char lhs, char rhs) { return std::tolower(lhs) == rhs; } ) != file.name.end(); }; const auto has_read = has(std::begin(read), std::end(read)); const auto has_boot = has(std::begin(boot), std::end(boot)); const auto probability = Numeric::similarity(file.name, adfs_catalogue->name) + Numeric::similarity(file.name, file_name) - ((has_read || has_boot) ? 0.2 : 0.0); options.emplace(probability, file.name); } if(!options.empty()) { targetArchimedes->main_program = options.begin()->second; } } } // Enable the Acorn ADFS if a mass-storage device is attached; // unlike the Pres ADFS it retains SCSI logic. if(!media.mass_storage_devices.empty()) { targetElectron->has_pres_adfs = false; // To override a floppy selection, if one was made. targetElectron->has_acorn_adfs = true; // Assume some sort of later-era Acorn work is likely to happen; // so ensure *TYPE, etc are present. targetElectron->has_ap6_rom = true; targetElectron->has_sideways_ram = true; targetElectron->media.mass_storage_devices = media.mass_storage_devices; // Check for a boot option. const auto sector = targetElectron->media.mass_storage_devices.front()->get_block(1); if(sector[0xfd]) { targetElectron->should_shift_restart = true; } else { targetElectron->loading_command = "*CAT\n"; } } TargetList targets; if(!targetElectron->media.empty() && !targetBBC->media.empty()) { if(bbc_hits > electron_hits || (bbc_hits == electron_hits && format_prefers_bbc)) { targets.push_back(std::move(targetBBC)); } else { targets.push_back(std::move(targetElectron)); } } else { if(!targetElectron->media.empty()) { targets.push_back(std::move(targetElectron)); } else if(!targetBBC->media.empty()) { targets.push_back(std::move(targetBBC)); } } if(!targetArchimedes->media.empty()) { targets.push_back(std::move(targetArchimedes)); } return targets; }