mirror of
https://github.com/TomHarte/CLK.git
synced 2025-02-05 21:32:55 +00:00
Merge pull request #865 from TomHarte/ADL
Electron: adds support for the ADL file format, and logic for AP6 and sideways RAM selection
This commit is contained in:
commit
d54085c7fd
@ -50,7 +50,10 @@ std::unique_ptr<Catalogue> 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<Catalogue> 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<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk) {
|
||||
auto catalogue = std::make_unique<Catalogue>();
|
||||
Storage::Encodings::MFM::Parser parser(true, disk);
|
||||
@ -101,5 +109,73 @@ std::unique_ptr<Catalogue> 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;
|
||||
}
|
||||
|
@ -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<uint8_t> 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;
|
||||
|
@ -12,6 +12,8 @@
|
||||
#include "Tape.hpp"
|
||||
#include "Target.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace Analyser::Static::Acorn;
|
||||
|
||||
static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,7 +109,12 @@ static std::unique_ptr<File> GetNextFile(std::deque<File::Chunk> &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) {
|
||||
|
@ -20,6 +20,8 @@ namespace Acorn {
|
||||
struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<Target> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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<Storage::Disk::AcornADF>, TargetPlatform::Acorn) // ADF
|
||||
Format("adl", result.disks, Disk::DiskImageHolder<Storage::Disk::AcornADF>, 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
|
||||
|
@ -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<Plus3>();
|
||||
|
||||
@ -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.
|
||||
|
@ -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
|
@ -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;
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ class AcornADF: public MFMSectorDump {
|
||||
|
||||
private:
|
||||
long get_file_offset_for_position(Track::Address address) final;
|
||||
int head_count_ = 1;
|
||||
};
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user