1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-02 20:30:00 +00:00

Merge branch '68000Mk2' of github.com:TomHarte/CLK into 68000Mk2

This commit is contained in:
Thomas Harte 2022-06-02 21:43:28 -04:00
commit 6cb559f65e
11 changed files with 661 additions and 57 deletions

View File

@ -43,11 +43,12 @@
#include "../../Storage/Disk/DiskImage/Formats/AppleDSK.hpp"
#include "../../Storage/Disk/DiskImage/Formats/CPCDSK.hpp"
#include "../../Storage/Disk/DiskImage/Formats/D64.hpp"
#include "../../Storage/Disk/DiskImage/Formats/MacintoshIMG.hpp"
#include "../../Storage/Disk/DiskImage/Formats/G64.hpp"
#include "../../Storage/Disk/DiskImage/Formats/DMK.hpp"
#include "../../Storage/Disk/DiskImage/Formats/FAT12.hpp"
#include "../../Storage/Disk/DiskImage/Formats/HFE.hpp"
#include "../../Storage/Disk/DiskImage/Formats/IPF.hpp"
#include "../../Storage/Disk/DiskImage/Formats/MacintoshIMG.hpp"
#include "../../Storage/Disk/DiskImage/Formats/MSA.hpp"
#include "../../Storage/Disk/DiskImage/Formats/NIB.hpp"
#include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp"
@ -103,8 +104,8 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
#define InsertInstance(list, instance, platforms) \
list.emplace_back(instance);\
potential_platforms |= platforms;\
TargetPlatform::TypeDistinguisher *distinguisher = dynamic_cast<TargetPlatform::TypeDistinguisher *>(list.back().get());\
if(distinguisher) potential_platforms &= distinguisher->target_platform_type(); \
TargetPlatform::TypeDistinguisher *const distinguisher = dynamic_cast<TargetPlatform::TypeDistinguisher *>(list.back().get());\
if(distinguisher) potential_platforms &= distinguisher->target_platform_type();
#define Insert(list, class, platforms, ...) \
InsertInstance(list, new Storage::class(__VA_ARGS__), platforms);
@ -161,7 +162,11 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
// HFE (TODO: switch to AllDisk once the MSX stops being so greedy)
Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2)
Format("image", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2)
Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::FAT12>, TargetPlatform::Enterprise) // IMG (Enterprise/MS-DOS style)
Format("img", result.disks, Disk::DiskImageHolder<Storage::Disk::FAT12>, TargetPlatform::Enterprise) // IMG (Enterprise/MS-DOS style)
Format( "ipf",
result.disks,
Disk::DiskImageHolder<Storage::Disk::IPF>,
TargetPlatform::Amiga | TargetPlatform::AtariST | TargetPlatform::AmstradCPC | TargetPlatform::ZXSpectrum) // IPF
Format("msa", result.disks, Disk::DiskImageHolder<Storage::Disk::MSA>, TargetPlatform::AtariST) // MSA
Format("nib", result.disks, Disk::DiskImageHolder<Storage::Disk::NIB>, TargetPlatform::DiskII) // NIB
Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // O

View File

@ -12,47 +12,47 @@ using namespace Concurrency;
AsyncTaskQueue::AsyncTaskQueue()
#ifndef USE_GCD
: should_destruct_(false)
#endif
{
#ifdef USE_GCD
serial_dispatch_queue_ = dispatch_queue_create("com.thomasharte.clocksignal.asyntaskqueue", DISPATCH_QUEUE_SERIAL);
:
should_destruct_(false),
thread_([this] () {
while(!should_destruct_) {
std::function<void(void)> next_function;
// Take lock, check for a new task.
std::unique_lock lock(queue_mutex_);
if(!pending_tasks_.empty()) {
next_function = pending_tasks_.front();
pending_tasks_.pop_front();
}
if(next_function) {
// If there is a task, release lock and perform it.
lock.unlock();
next_function();
} else {
// If there isn't a task, atomically block on the processing condition and release the lock
// until there's something pending (and then release it again via scope).
processing_condition_.wait(lock);
}
}
})
#else
thread_ = std::make_unique<std::thread>([this]() {
while(!should_destruct_) {
std::function<void(void)> next_function;
// Take lock, check for a new task
std::unique_lock lock(queue_mutex_);
if(!pending_tasks_.empty()) {
next_function = pending_tasks_.front();
pending_tasks_.pop_front();
}
if(next_function) {
// If there is a task, release lock and perform it
lock.unlock();
next_function();
} else {
// If there isn't a task, atomically block on the processing condition and release the lock
// until there's something pending (and then release it again via scope)
processing_condition_.wait(lock);
}
}
});
: serial_dispatch_queue_(dispatch_queue_create("com.thomasharte.clocksignal.asyntaskqueue", DISPATCH_QUEUE_SERIAL))
#endif
}
{}
AsyncTaskQueue::~AsyncTaskQueue() {
#ifdef USE_GCD
flush();
dispatch_release(serial_dispatch_queue_);
serial_dispatch_queue_ = nullptr;
#else
// Set should destruct, and then give the thread a bit of a nudge
// via an empty enqueue.
should_destruct_ = true;
enqueue([](){});
thread_->join();
thread_.reset();
// Wait for the thread safely to terminate.
thread_.join();
#endif
}
@ -88,18 +88,22 @@ DeferringAsyncTaskQueue::~DeferringAsyncTaskQueue() {
void DeferringAsyncTaskQueue::defer(std::function<void(void)> function) {
if(!deferred_tasks_) {
deferred_tasks_ = std::make_shared<std::list<std::function<void(void)>>>();
deferred_tasks_ = std::make_unique<TaskList>();
}
deferred_tasks_->push_back(function);
}
void DeferringAsyncTaskQueue::perform() {
if(!deferred_tasks_) return;
std::shared_ptr<std::list<std::function<void(void)>>> deferred_tasks = deferred_tasks_;
deferred_tasks_.reset();
enqueue([deferred_tasks] {
enqueue([deferred_tasks_raw = deferred_tasks_.release()] {
std::unique_ptr<TaskList> deferred_tasks(deferred_tasks_raw);
for(const auto &function : *deferred_tasks) {
function();
}
});
}
void DeferringAsyncTaskQueue::flush() {
perform();
AsyncTaskQueue::flush();
}

View File

@ -23,6 +23,8 @@
namespace Concurrency {
using TaskList = std::list<std::function<void(void)>>;
/*!
An async task queue allows a caller to enqueue void(void) functions. Those functions are guaranteed
to be performed serially and asynchronously from the caller. A caller may also request to flush,
@ -51,12 +53,12 @@ class AsyncTaskQueue {
#ifdef USE_GCD
dispatch_queue_t serial_dispatch_queue_;
#else
std::unique_ptr<std::thread> thread_;
std::mutex queue_mutex_;
std::list<std::function<void(void)>> pending_tasks_;
std::condition_variable processing_condition_;
std::atomic_bool should_destruct_;
std::condition_variable processing_condition_;
std::mutex queue_mutex_;
TaskList pending_tasks_;
std::thread thread_;
#endif
};
@ -87,10 +89,13 @@ class DeferringAsyncTaskQueue: public AsyncTaskQueue {
*/
void perform();
/*!
Blocks the caller until all previously-enqueud functions have completed.
*/
void flush();
private:
// TODO: this is a shared_ptr because of the issues capturing moveables in C++11;
// switch to a unique_ptr if/when adapting to C++14
std::shared_ptr<std::list<std::function<void(void)>>> deferred_tasks_;
std::unique_ptr<TaskList> deferred_tasks_;
};
}

View File

@ -276,6 +276,8 @@
4B59199C1DAC6C46005BB85C /* OricTAP.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B59199A1DAC6C46005BB85C /* OricTAP.cpp */; };
4B595FAD2086DFBA0083CAA8 /* AudioToggle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B595FAC2086DFBA0083CAA8 /* AudioToggle.cpp */; };
4B595FAE2086DFBA0083CAA8 /* AudioToggle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B595FAC2086DFBA0083CAA8 /* AudioToggle.cpp */; };
4B5B37312777C7FC0047F238 /* IPF.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5B372F2777C7FC0047F238 /* IPF.cpp */; };
4B5B37322777C7FC0047F238 /* IPF.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5B372F2777C7FC0047F238 /* IPF.cpp */; };
4B5D5C9725F56FC7001B4623 /* Spectrum.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5D5C9525F56FC7001B4623 /* Spectrum.cpp */; };
4B5D5C9825F56FC7001B4623 /* Spectrum.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5D5C9525F56FC7001B4623 /* Spectrum.cpp */; };
4B5FADBA1DE3151600AEC565 /* FileHolder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5FADB81DE3151600AEC565 /* FileHolder.cpp */; };
@ -1393,6 +1395,8 @@
4B59199B1DAC6C46005BB85C /* OricTAP.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = OricTAP.hpp; sourceTree = "<group>"; };
4B595FAB2086DFBA0083CAA8 /* AudioToggle.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = AudioToggle.hpp; sourceTree = "<group>"; };
4B595FAC2086DFBA0083CAA8 /* AudioToggle.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AudioToggle.cpp; sourceTree = "<group>"; };
4B5B372F2777C7FC0047F238 /* IPF.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = IPF.cpp; sourceTree = "<group>"; };
4B5B37302777C7FC0047F238 /* IPF.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = IPF.hpp; sourceTree = "<group>"; };
4B5D5C9525F56FC7001B4623 /* Spectrum.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Spectrum.cpp; path = Parsers/Spectrum.cpp; sourceTree = "<group>"; };
4B5D5C9625F56FC7001B4623 /* Spectrum.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Spectrum.hpp; path = Parsers/Spectrum.hpp; sourceTree = "<group>"; };
4B5FADB81DE3151600AEC565 /* FileHolder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FileHolder.cpp; sourceTree = "<group>"; };
@ -2859,6 +2863,7 @@
4BEBFB4B2002C4BF000708CC /* FAT12.cpp */,
4B4518931F75FD1B00926311 /* G64.cpp */,
4B4518951F75FD1B00926311 /* HFE.cpp */,
4B5B372F2777C7FC0047F238 /* IPF.cpp */,
4BB4BFAE22A42F290069048D /* MacintoshIMG.cpp */,
4B58601C1F806AB200AEE2E3 /* MFMSectorDump.cpp */,
4BC131782346DF2B00E4FF3D /* MSA.cpp */,
@ -2878,6 +2883,7 @@
4BEBFB4C2002C4BF000708CC /* FAT12.hpp */,
4B4518941F75FD1B00926311 /* G64.hpp */,
4B4518961F75FD1B00926311 /* HFE.hpp */,
4B5B37302777C7FC0047F238 /* IPF.hpp */,
4BB4BFAF22A42F290069048D /* MacintoshIMG.hpp */,
4B58601D1F806AB200AEE2E3 /* MFMSectorDump.hpp */,
4BC131792346DF2B00E4FF3D /* MSA.hpp */,
@ -5550,6 +5556,7 @@
4B7962A22819681F008130F9 /* Decoder.cpp in Sources */,
4B055ACF1FAE9B030060FFFF /* SoundGenerator.cpp in Sources */,
4B4DEC08252BFA56004583AC /* 65816Base.cpp in Sources */,
4B5B37322777C7FC0047F238 /* IPF.cpp in Sources */,
4B894519201967B4007DE474 /* ConfidenceCounter.cpp in Sources */,
4B055AEE1FAE9BBF0060FFFF /* Keyboard.cpp in Sources */,
4B055AED1FAE9BA20060FFFF /* Z80Storage.cpp in Sources */,
@ -5704,6 +5711,7 @@
4B89451E201967B4007DE474 /* Tape.cpp in Sources */,
4BAF2B4E2004580C00480230 /* DMK.cpp in Sources */,
4BB697CE1D4BA44400248BDF /* CommodoreGCR.cpp in Sources */,
4B5B37312777C7FC0047F238 /* IPF.cpp in Sources */,
4B0ACC3023775819008902D0 /* TIASound.cpp in Sources */,
4B7136861F78724F008B8ED9 /* Encoder.cpp in Sources */,
4B0E04EA1FC9E5DA00F43484 /* CAS.cpp in Sources */,

View File

@ -652,6 +652,26 @@
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>ipf</string>
</array>
<key>CFBundleTypeName</key>
<string>Software Preservation Society Disk Image</string>
<key>CFBundleTypeOSTypes</key>
<array>
<string>????</string>
</array>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSTypeIsPackage</key>
<false/>
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>

View File

@ -870,7 +870,16 @@
XCTAssertEqual(state.registers.data[1], 0x1fffffff);
XCTAssertEqual(state.registers.supervisor_stack_pointer, initial_sp - 6);
XCTAssertEqual(state.registers.status & ConditionCode::AllConditions, ConditionCode::Extend);
XCTAssertEqual(44, self.machine->get_cycle_count());
// Total expected bus pattern:
//
// np | nn nn | nw nw nw np np np n np = 42 cycles
//
// Noted: Yacht shows a total of three nps for a DIVS #;
// I believe this is incorrect as it includes two in the
// '1st op (ea)' stage, but an immediate word causes
// only one elsewhere.
XCTAssertEqual(42, self.machine->get_cycle_count());
// Check stack contents; should be PC.l, PC.h and status register.
// Assumed: the program counter on the stack is that of the
@ -908,7 +917,11 @@
const auto state = self.machine->get_processor_state();
XCTAssertEqual(state.registers.data[1], 0x4768f231);
XCTAssertEqual(state.registers.status & ConditionCode::AllConditions, ConditionCode::Extend | ConditionCode::Negative | ConditionCode::Overflow);
// This test should produce overflow; so don't test N or Z flags.
XCTAssertEqual(
state.registers.status & (ConditionCode::Carry | ConditionCode::Overflow | ConditionCode::Extend),
ConditionCode::Extend | ConditionCode::Overflow);
XCTAssertEqual(14, self.machine->get_cycle_count());
}
@ -917,7 +930,10 @@
const auto state = self.machine->get_processor_state();
XCTAssertEqual(state.registers.data[1], 0x4768f231);
XCTAssertEqual(state.registers.status & ConditionCode::AllConditions, ConditionCode::Extend | ConditionCode::Negative | ConditionCode::Overflow);
// This test should also produce overflow; so don't test N or Z flags.
XCTAssertEqual(
state.registers.status & (ConditionCode::Carry | ConditionCode::Overflow | ConditionCode::Extend),
ConditionCode::Extend | ConditionCode::Overflow);
XCTAssertEqual(14, self.machine->get_cycle_count());
}

View File

@ -2547,7 +2547,7 @@ template <bool did_overflow> void ProcessorBase::did_divu(uint32_t dividend, uin
}
if(did_overflow) {
dynamic_instruction_length_ = 3; // Just a quick nn n, and then on to prefetch.
dynamic_instruction_length_ = 3; // Covers the nn n to get into the loop.
return;
}
@ -2556,13 +2556,15 @@ template <bool did_overflow> void ProcessorBase::did_divu(uint32_t dividend, uin
// since this is a classic divide algorithm, but would rather that
// errors produce incorrect timing only, not incorrect timing plus
// incorrect results.
dynamic_instruction_length_ = 3; // Covers the nn n to get into the loop.
dynamic_instruction_length_ =
3 + // nn n to get into the loop;
30 + // nn per iteration of the loop below;
3; // n nn upon completion of the loop.
divisor <<= 16;
for(int c = 0; c < 15; ++c) {
if(dividend & 0x80000000) {
if(dividend & 0x8000'0000) {
dividend = (dividend << 1) - divisor;
dynamic_instruction_length_ += 2; // The fixed nn iteration cost.
} else {
dividend <<= 1;
@ -2570,9 +2572,9 @@ template <bool did_overflow> void ProcessorBase::did_divu(uint32_t dividend, uin
// and test the sign of the result, but this is easier to follow:
if (dividend >= divisor) {
dividend -= divisor;
dynamic_instruction_length_ += 3; // i.e. the original nn plus one further n before going down the MSB=0 route.
dynamic_instruction_length_ += 1; // i.e. the original nn plus one further n before going down the MSB=0 route.
} else {
dynamic_instruction_length_ += 4; // The costliest path (since in real life it's a subtraction and then a step
dynamic_instruction_length_ += 2; // The costliest path (since in real life it's a subtraction and then a step
// back from there) — all costs accrue. So the fixed nn loop plus another n,
// plus another one.
}

View File

@ -14,6 +14,7 @@
#include "../Disk.hpp"
#include "../Track/Track.hpp"
#include "../../TargetPlatforms.hpp"
namespace Storage {
namespace Disk {
@ -86,8 +87,11 @@ class DiskImageHolderBase: public Disk {
Provides a wrapper that wraps a DiskImage to make it into a Disk, providing caching and,
thereby, an intermediate store for modified tracks so that mutable disk images can either
update on the fly or perform a block update on closure, as appropriate.
Implements TargetPlatform::TypeDistinguisher to return either no information whatsoever, if
the underlying image doesn't implement TypeDistinguisher, or else to pass the call along.
*/
template <typename T> class DiskImageHolder: public DiskImageHolderBase {
template <typename T> class DiskImageHolder: public DiskImageHolderBase, public TargetPlatform::TypeDistinguisher {
public:
template <typename... Ts> DiskImageHolder(Ts&&... args) :
disk_image_(args...) {}
@ -103,6 +107,14 @@ template <typename T> class DiskImageHolder: public DiskImageHolderBase {
private:
T disk_image_;
TargetPlatform::Type target_platform_type() final {
if constexpr (std::is_base_of<TargetPlatform::TypeDistinguisher, T>::value) {
return static_cast<TargetPlatform::TypeDistinguisher *>(&disk_image_)->target_platform_type();
} else {
return TargetPlatform::Type(~0);
}
}
};
#include "DiskImageImplementation.hpp"

View File

@ -0,0 +1,437 @@
//
// IPF.cpp
// Clock Signal
//
// Created by Thomas Harte on 25/12/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "IPF.hpp"
#include "../../Encodings/MFM/Encoder.hpp"
#include <cassert>
using namespace Storage::Disk;
namespace {
constexpr uint32_t block(const char (& src)[5]) {
return uint32_t(
(uint32_t(src[0]) << 24) |
(uint32_t(src[1]) << 16) |
(uint32_t(src[2]) << 8) |
uint32_t(src[3])
);
}
constexpr size_t block_size(Storage::FileHolder &file, uint8_t header) {
uint8_t size_width = header >> 5;
size_t length = 0;
while(size_width--) {
length = (length << 8) | file.get8();
}
return length;
}
}
IPF::IPF(const std::string &file_name) : file_(file_name) {
std::map<uint32_t, Track::Address> tracks_by_data_key;
// For now, just build up a list of tracks that exist, noting the file position at which their data begins
// plus the other fields that'll be necessary to convert them into flux on demand later.
while(true) {
const auto start_of_block = file_.tell();
const uint32_t type = file_.get32be();
uint32_t length = file_.get32be(); // Can't be const because of the dumb encoding of DATA blocks.
[[maybe_unused]] const uint32_t crc = file_.get32be();
if(file_.eof()) break;
// Sanity check: the first thing in a file should be the CAPS record.
if(!start_of_block && type != block("CAPS")) {
throw Error::InvalidFormat;
}
switch(type) {
default:
printf("Ignoring %c%c%c%c, starting at %ld of length %d\n", (type >> 24), (type >> 16) & 0xff, (type >> 8) & 0xff, type & 0xff, start_of_block, length);
break;
case block("CAPS"):
// Analogously to the sanity check above, if a CAPS block is anywhere other
// than first then something is amiss.
if(start_of_block) {
throw Error::InvalidFormat;
}
break;
case block("INFO"): {
// There are a lot of useful archival fields in the info chunk, which for emulation
// aren't that interesting.
// Make sure this is a floppy disk.
const uint32_t media_type = file_.get32be();
if(media_type != 1) {
throw Error::InvalidFormat;
}
// Determine whether this is a newer SPS-style file.
is_sps_format_ = file_.get32be() > 1;
// Skip: revision, file key and revision, CRC of the original .ctr, and minimum track.
file_.seek(20, SEEK_CUR);
track_count_ = int(1 + file_.get32be());
// Skip: min side.
file_.seek(4, SEEK_CUR);
head_count_ = int(1 + file_.get32be());
// Skip: creation date, time.
file_.seek(8, SEEK_CUR);
platform_type_ = 0;
for(int c = 0; c < 4; c++) {
const uint8_t platform = file_.get8();
switch(platform) {
default: break;
case 1: platform_type_ |= TargetPlatform::Amiga; break;
case 2: platform_type_ |= TargetPlatform::AtariST; break;
/* Omitted: 3 -> IBM PC */
case 4: platform_type_ |= TargetPlatform::AmstradCPC; break;
case 5: platform_type_ |= TargetPlatform::ZXSpectrum; break;
/* Omitted: 6 -> Sam Coupé */
/* Omitted: 7 -> Archimedes */
/* Omitted: 8 -> C64 */
/* Omitted: 9 -> Atari 8-bit */
}
}
// If the file didn't declare anything, default to supporting everything.
if(!platform_type_) {
platform_type_ = ~0;
}
// Ignore: disk number, creator ID, reserved area.
} break;
case block("IMGE"): {
// Get track location.
const uint32_t track = file_.get32be();
const uint32_t side = file_.get32be();
const Track::Address address{int(side), HeadPosition(int(track))};
// Hence generate a TrackDescription.
auto pair = tracks_.emplace(address, TrackDescription());
TrackDescription &description = pair.first->second;
// Read those fields of interest...
// Bit density. I've no idea why the density can't just be given as a measurement.
description.density = TrackDescription::Density(file_.get32be());
if(description.density > TrackDescription::Density::Max) {
description.density = TrackDescription::Density::Unknown;
}
file_.seek(12, SEEK_CUR); // Skipped: signal type, track bytes, start byte position.
description.start_bit_pos = file_.get32be();
description.data_bits = file_.get32be();
description.gap_bits = file_.get32be();
file_.seek(4, SEEK_CUR); // Skipped: track bits, which is entirely redundant.
description.block_count = file_.get32be();
file_.seek(4, SEEK_CUR); // Skipped: encoder process.
description.has_fuzzy_bits = file_.get32be() & 1;
// For some reason the authors decided to introduce another primary key,
// in addition to that which naturally exists of (track, side). So set up
// a mapping from the one to the other.
const uint32_t data_key = file_.get32be();
tracks_by_data_key.emplace(data_key, address);
} break;
case block("DATA"): {
length += file_.get32be();
file_.seek(8, SEEK_CUR); // Skipped: bit size, CRC.
// Grab the data key and use that to establish the file starting
// position for this track.
//
// Assumed here: DATA records will come after corresponding IMGE records.
const uint32_t data_key = file_.get32be();
const auto pair = tracks_by_data_key.find(data_key);
if(pair == tracks_by_data_key.end()) {
break;
}
auto description = tracks_.find(pair->second);
if(description == tracks_.end()) {
break;
}
description->second.file_offset = file_.tell();
} break;
}
file_.seek(start_of_block + length, SEEK_SET);
}
}
HeadPosition IPF::get_maximum_head_position() {
return HeadPosition(track_count_);
}
int IPF::get_head_count() {
return head_count_;
}
std::shared_ptr<Track> IPF::get_track_at_position([[maybe_unused]] Track::Address address) {
// Get the track description, if it exists, and check either that the file has contents for the track.
auto pair = tracks_.find(address);
if(pair == tracks_.end()) {
return nullptr;
}
const TrackDescription &description = pair->second;
if(!description.file_offset) {
return nullptr;
}
// Seek to track content.
file_.seek(description.file_offset, SEEK_SET);
// Read the block descriptions up front.
//
// This is less efficient than just seeking for each block in turn,
// but is a useful crutch to comprehension of the file format on a
// first run through.
struct BlockDescriptor {
uint32_t data_bits = 0;
uint32_t gap_bits = 0;
uint32_t gap_offset = 0;
bool is_mfm = false;
bool has_forward_gap = false;
bool has_backwards_gap = false;
bool data_unit_is_bits = false;
uint32_t default_gap_value = 0;
uint32_t data_offset = 0;
};
std::vector<BlockDescriptor> blocks;
blocks.reserve(description.block_count);
for(uint32_t c = 0; c < description.block_count; c++) {
auto &block = blocks.emplace_back();
block.data_bits = file_.get32be();
block.gap_bits = file_.get32be();
if(is_sps_format_) {
block.gap_offset = file_.get32be();
file_.seek(4, SEEK_CUR); // Skip 'cell type' which appears to provide no content.
} else {
// Skip potlower-resolution copies of data_bits and gap_bits.
file_.seek(8, SEEK_CUR);
}
block.is_mfm = file_.get32be() == 1;
const uint32_t flags = file_.get32be();
block.has_forward_gap = flags & 1;
block.has_backwards_gap = flags & 2;
block.data_unit_is_bits = flags & 4;
block.default_gap_value = file_.get32be();
block.data_offset = file_.get32be();
}
std::vector<Storage::Disk::PCMSegment> segments;
int block_count = 0;
for(auto &block: blocks) {
const auto length_of_a_bit = bit_length(description.density, block_count);
if(block.gap_offset) {
file_.seek(description.file_offset + block.gap_offset, SEEK_SET);
while(true) {
const uint8_t gap_header = file_.get8();
if(!gap_header) break;
// Decompose the header and read the length.
enum class Type {
None, GapLength, SampleLength
} type = Type(gap_header & 0x1f);
const size_t length = block_size(file_, gap_header);
switch(type) {
case Type::GapLength:
printf("Adding gap length %zu bits\n", length);
add_gap(segments, length_of_a_bit, length, block.default_gap_value);
break;
default:
case Type::SampleLength:
printf("Adding sampled gap length %zu bits\n", length);
add_raw_data(segments, length_of_a_bit, length);
// file_.seek(long(length >> 3), SEEK_CUR);
break;
}
}
} else if(block.gap_bits) {
add_gap(segments, length_of_a_bit, block.gap_bits, block.default_gap_value);
}
if(block.data_offset) {
file_.seek(description.file_offset + block.data_offset, SEEK_SET);
while(true) {
const uint8_t data_header = file_.get8();
if(!data_header) break;
// Decompose the header and read the length.
enum class Type {
None, Sync, Data, Gap, Raw, Fuzzy
} type = Type(data_header & 0x1f);
const size_t length = block_size(file_, data_header) * (block.data_unit_is_bits ? 1 : 8);
#ifndef NDEBUG
const auto next_chunk = file_.tell() + long(length >> 3);
#endif
switch(type) {
case Type::Gap:
case Type::Data:
add_unencoded_data(segments, length_of_a_bit, length);
break;
case Type::Sync:
case Type::Raw:
add_raw_data(segments, length_of_a_bit, length);
break;
default:
printf("Unhandled data type %d, length %zu bits\n", int(type), length);
break;
}
assert(file_.tell() == next_chunk);
}
}
++block_count;
}
return std::make_shared<Storage::Disk::PCMTrack>(segments);
}
/// @returns The correct bit length for @c block on a track of @c density.
///
/// @discussion At least to me, this is the least well-designed part] of the IPF specification; rather than just dictating cell
/// densities (or, equivalently, lengths) in the file, densities are named according to their protection scheme and the decoder
/// is required to know all named protection schemes. Which makes IPF unable to handle arbitrary disks (or, indeed, disks
/// with multiple protection schemes on a single track).
Storage::Time IPF::bit_length(TrackDescription::Density density, int block) {
constexpr unsigned int us = 100'000'000;
static constexpr auto us170 = Storage::Time::simplified(170, us);
static constexpr auto us180 = Storage::Time::simplified(180, us);
static constexpr auto us189 = Storage::Time::simplified(189, us);
static constexpr auto us190 = Storage::Time::simplified(190, us);
static constexpr auto us199 = Storage::Time::simplified(199, us);
static constexpr auto us200 = Storage::Time::simplified(200, us);
static constexpr auto us209 = Storage::Time::simplified(209, us);
static constexpr auto us210 = Storage::Time::simplified(210, us);
static constexpr auto us220 = Storage::Time::simplified(220, us);
switch(density) {
default:
break;
case TrackDescription::Density::CopylockAmiga:
if(block == 4) return us189;
if(block == 5) return us199;
if(block == 6) return us209;
break;
case TrackDescription::Density::CopylockAmigaNew:
if(block == 0) return us189;
if(block == 1) return us199;
if(block == 2) return us209;
break;
case TrackDescription::Density::CopylockST:
if(block == 5) return us210;
break;
case TrackDescription::Density::SpeedlockAmiga:
if(block == 1) return us220;
if(block == 2) return us180;
break;
case TrackDescription::Density::OldSpeedlockAmiga:
if(block == 1) return us210;
break;
case TrackDescription::Density::AdamBrierleyAmiga:
if(block == 1) return us220;
if(block == 2) return us210;
if(block == 3) return us200;
if(block == 4) return us190;
if(block == 5) return us180;
if(block == 6) return us170;
break;
// TODO: AdamBrierleyDensityKeyAmiga.
}
return us200; // i.e. default to 2µs.
}
void IPF::add_gap(std::vector<Storage::Disk::PCMSegment> &track, Time bit_length, size_t num_bits, uint32_t value) {
auto &segment = track.emplace_back();
segment.length_of_a_bit = bit_length;
// Empirically, I think gaps require MFM encoding.
const auto byte_length = (num_bits + 7) >> 3;
segment.data.reserve(byte_length * 16);
auto encoder = Storage::Encodings::MFM::GetMFMEncoder(segment.data);
while(segment.data.size() < num_bits) {
encoder->add_byte(uint8_t(value >> 24));
value = (value << 8) | (value >> 24);
}
assert(segment.data.size() <= (byte_length * 16));
segment.data.resize(num_bits);
}
void IPF::add_unencoded_data(std::vector<Storage::Disk::PCMSegment> &track, Time bit_length, size_t num_bits) {
auto &segment = track.emplace_back();
segment.length_of_a_bit = bit_length;
// Length appears to be in pre-encoded bits; double that to get encoded bits.
const auto byte_length = (num_bits + 7) >> 3;
segment.data.reserve(num_bits * 16);
auto encoder = Storage::Encodings::MFM::GetMFMEncoder(segment.data);
for(size_t c = 0; c < num_bits; c += 8) {
encoder->add_byte(file_.get8());
}
assert(segment.data.size() <= (byte_length * 16));
segment.data.resize(num_bits * 2);
}
void IPF::add_raw_data(std::vector<Storage::Disk::PCMSegment> &track, Time bit_length, size_t num_bits) {
auto &segment = track.emplace_back();
segment.length_of_a_bit = bit_length;
const auto num_bits_ceiling = size_t(num_bits + 7) & size_t(~7);
segment.data.reserve(num_bits_ceiling);
for(size_t bit = 0; bit < num_bits; bit += 8) {
const uint8_t next = file_.get8();
segment.data.push_back(next & 0x80);
segment.data.push_back(next & 0x40);
segment.data.push_back(next & 0x20);
segment.data.push_back(next & 0x10);
segment.data.push_back(next & 0x08);
segment.data.push_back(next & 0x04);
segment.data.push_back(next & 0x02);
segment.data.push_back(next & 0x01);
}
assert(segment.data.size() <= num_bits_ceiling);
segment.data.resize(num_bits);
}

View File

@ -0,0 +1,91 @@
//
// IPF.hpp
// Clock Signal
//
// Created by Thomas Harte on 25/12/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef IPF_hpp
#define IPF_hpp
#include "../DiskImage.hpp"
#include "../../Track/PCMTrack.hpp"
#include "../../../FileHolder.hpp"
#include "../../../TargetPlatforms.hpp"
#include <string>
#include <map>
namespace Storage {
namespace Disk {
/*!
Provides a @c DiskImage containing an IPF, which is a mixed stream of raw flux windows and
unencoded MFM sections along with gap records that can be used to record write splices, all
of which is variably clocked (albeit not at flux transition resolution; as a result IPF files tend to be
close in size to more primitive formats).
*/
class IPF: public DiskImage, public TargetPlatform::TypeDistinguisher {
public:
/*!
Construct an @c IPF 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 an .HFE format image.
@throws Error::UnknownVersion if the file looks correct but is an unsupported version.
*/
IPF(const std::string &file_name);
// implemented to satisfy @c Disk
HeadPosition get_maximum_head_position() final;
int get_head_count() final;
std::shared_ptr<Track> get_track_at_position(Track::Address address) final;
private:
Storage::FileHolder file_;
uint16_t seek_track(Track::Address address);
struct TrackDescription {
long file_offset = 0;
enum class Density {
Unknown,
Noise,
Auto,
CopylockAmiga,
CopylockAmigaNew,
CopylockST,
SpeedlockAmiga,
OldSpeedlockAmiga,
AdamBrierleyAmiga,
AdamBrierleyDensityKeyAmiga,
Max = AdamBrierleyDensityKeyAmiga
} density = Density::Unknown;
uint32_t start_bit_pos = 0;
uint32_t data_bits = 0;
uint32_t gap_bits = 0;
uint32_t block_count;
bool has_fuzzy_bits = false;
};
int head_count_;
int track_count_;
std::map<Track::Address, TrackDescription> tracks_;
bool is_sps_format_ = false;
TargetPlatform::Type target_platform_type() final {
return TargetPlatform::Type(platform_type_);
}
TargetPlatform::IntType platform_type_ = TargetPlatform::Amiga;
Time bit_length(TrackDescription::Density, int block);
void add_gap(std::vector<Storage::Disk::PCMSegment> &, Time bit_length, size_t num_bits, uint32_t value);
void add_unencoded_data(std::vector<Storage::Disk::PCMSegment> &, Time bit_length, size_t num_bits);
void add_raw_data(std::vector<Storage::Disk::PCMSegment> &, Time bit_length, size_t num_bits);
};
}
}
#endif /* IPF_hpp */

View File

@ -33,6 +33,10 @@ struct Time {
Time(float value) {
install_float(value);
}
static constexpr Time simplified(unsigned int _length, unsigned int _clock_rate) {
const auto gcd = std::gcd(_length, _clock_rate);
return Time(_length / gcd, _clock_rate / gcd);
}
/*!
Reduces this @c Time to its simplest form; eliminates all common factors from @c length