mirror of
https://github.com/TomHarte/CLK.git
synced 2025-04-09 00:37:27 +00:00
Merge branch '68000Mk2' into InMacintosh
This commit is contained in:
commit
93749cd650
@ -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
|
||||
|
@ -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,16 +88,15 @@ 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();
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
@ -93,9 +95,7 @@ class DeferringAsyncTaskQueue: public AsyncTaskQueue {
|
||||
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_;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ void Executor<model, BusHandler>::signal_bus_error(FunctionCode code, uint32_t a
|
||||
template <Model model, typename BusHandler>
|
||||
void Executor<model, BusHandler>::set_interrupt_level(int level) {
|
||||
state_.interrupt_input_ = level;
|
||||
state_.stopped &= state_.interrupt_input_ <= state_.status.interrupt_level;
|
||||
state_.stopped &= !state_.status.would_accept_interrupt(level);
|
||||
}
|
||||
|
||||
template <Model model, typename BusHandler>
|
||||
@ -324,7 +324,7 @@ template <Model model, typename BusHandler>
|
||||
void Executor<model, BusHandler>::State::run(int &count) {
|
||||
while(count--) {
|
||||
// Check for a new interrupt.
|
||||
if(interrupt_input > status.interrupt_level) {
|
||||
if(status.would_accept_interrupt(interrupt_input)) {
|
||||
const int vector = bus_handler_.acknowlege_interrupt(interrupt_input);
|
||||
if(vector >= 0) {
|
||||
raise_exception<false>(vector);
|
||||
|
@ -533,7 +533,7 @@ template <
|
||||
return; \
|
||||
} \
|
||||
\
|
||||
const auto quotient = dividend / divisor; \
|
||||
const auto quotient = int64_t(dividend) / int64_t(divisor); \
|
||||
if(quotient != Type32(Type16(quotient))) { \
|
||||
status.overflow_flag = 1; \
|
||||
flow_controller.template flow_function<true>(dividend, divisor); \
|
||||
|
@ -113,7 +113,7 @@ struct Status {
|
||||
}
|
||||
|
||||
/// Evaluates @c condition.
|
||||
bool evaluate_condition(Condition condition) {
|
||||
constexpr bool evaluate_condition(Condition condition) const {
|
||||
switch(condition) {
|
||||
default:
|
||||
case Condition::True: return true;
|
||||
@ -138,6 +138,13 @@ struct Status {
|
||||
return !zero_result || (negative_flag && !overflow_flag) || (!negative_flag && overflow_flag);
|
||||
}
|
||||
}
|
||||
|
||||
/// @returns @c true if an interrupt at level @c level should be accepted; @c false otherwise.
|
||||
constexpr bool would_accept_interrupt(int level) const {
|
||||
// TODO: is level seven really non-maskable? If so then what mechanism prevents
|
||||
// rapid stack overflow upon a level-seven interrupt?
|
||||
return level > interrupt_level;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -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 */,
|
||||
|
@ -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>
|
||||
|
@ -12,96 +12,25 @@
|
||||
|
||||
#include "TestRunner68000.hpp"
|
||||
|
||||
/*
|
||||
class CPU::MC68000::ProcessorStorageTests {
|
||||
public:
|
||||
ProcessorStorageTests(const CPU::MC68000::ProcessorStorage &storage, const char *coverage_file_name) {
|
||||
false_valids_ = [NSMutableSet set];
|
||||
false_invalids_ = [NSMutableSet set];
|
||||
|
||||
FILE *source = fopen(coverage_file_name, "rb");
|
||||
|
||||
// The file format here is [2 bytes opcode][2 ASCII characters:VA for valid, IN for invalid]...
|
||||
// The file terminates with four additional bytes that begin with two zero bytes.
|
||||
//
|
||||
// The version of the file I grabbed seems to cover all opcodes, making their enumeration
|
||||
// arguably redundant; the code below nevertheless uses the codes from the file.
|
||||
//
|
||||
// Similarly, I'm testing for exactly the strings VA or IN to ensure no further
|
||||
// types creep into any updated version of the table that I then deal with incorrectly.
|
||||
uint16_t last_observed = 0;
|
||||
while(true) {
|
||||
// Fetch opcode number.
|
||||
uint16_t next_opcode = fgetc(source) << 8;
|
||||
next_opcode |= fgetc(source);
|
||||
if(next_opcode < last_observed) break;
|
||||
last_observed = next_opcode;
|
||||
|
||||
// Determine whether it's meant to be valid.
|
||||
char type[3];
|
||||
type[0] = fgetc(source);
|
||||
type[1] = fgetc(source);
|
||||
type[2] = '\0';
|
||||
|
||||
// TEMPORARY: factor out A- and F-line exceptions.
|
||||
if((next_opcode&0xf000) == 0xa000) continue;
|
||||
if((next_opcode&0xf000) == 0xf000) continue;
|
||||
|
||||
if(!strcmp(type, "VA")) {
|
||||
// Test for validity.
|
||||
if(storage.instructions[next_opcode].micro_operations == std::numeric_limits<uint32_t>::max()) {
|
||||
[false_invalids_ addObject:@(next_opcode)];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!strcmp(type, "IN")) {
|
||||
// Test for invalidity.
|
||||
if(storage.instructions[next_opcode].micro_operations != std::numeric_limits<uint32_t>::max()) {
|
||||
[false_valids_ addObject:@(next_opcode)];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
assert(false);
|
||||
}
|
||||
|
||||
fclose(source);
|
||||
}
|
||||
|
||||
NSSet<NSNumber *> *false_valids() const {
|
||||
return false_valids_;
|
||||
}
|
||||
|
||||
NSSet<NSNumber *> *false_invalids() const {
|
||||
return false_invalids_;
|
||||
}
|
||||
|
||||
private:
|
||||
NSMutableSet<NSNumber *> *false_invalids_;
|
||||
NSMutableSet<NSNumber *> *false_valids_;
|
||||
};
|
||||
*/
|
||||
|
||||
@interface NSSet (CSHexDump)
|
||||
|
||||
- (NSString *)hexDump;
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSSet (CSHexDump)
|
||||
|
||||
- (NSString *)hexDump {
|
||||
NSMutableArray<NSString *> *components = [NSMutableArray array];
|
||||
|
||||
for(NSNumber *number in [[self allObjects] sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
[components addObject:[NSString stringWithFormat:@"%04x", number.intValue]];
|
||||
}
|
||||
|
||||
return [components componentsJoinedByString:@" "];
|
||||
}
|
||||
|
||||
@end
|
||||
//@interface NSSet (CSHexDump)
|
||||
//
|
||||
//- (NSString *)hexDump;
|
||||
//
|
||||
//@end
|
||||
//
|
||||
//@implementation NSSet (CSHexDump)
|
||||
//
|
||||
//- (NSString *)hexDump {
|
||||
// NSMutableArray<NSString *> *components = [NSMutableArray array];
|
||||
//
|
||||
// for(NSNumber *number in [[self allObjects] sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
// [components addObject:[NSString stringWithFormat:@"%04x", number.intValue]];
|
||||
// }
|
||||
//
|
||||
// return [components componentsJoinedByString:@" "];
|
||||
//}
|
||||
//
|
||||
//@end
|
||||
|
||||
|
||||
@interface M68000Tests : XCTestCase
|
||||
@ -140,12 +69,12 @@ class CPU::MC68000::ProcessorStorageTests {
|
||||
|
||||
- (void)testDivideByZero {
|
||||
_machine->set_program({
|
||||
0x7000, // MOVE #0, D0; location 0x400
|
||||
0x3200, // MOVE D0, D1; location 0x402
|
||||
0x7000, // MOVE #0, D0; location 0x1000
|
||||
0x3200, // MOVE D0, D1; location 0x1002
|
||||
|
||||
0x82C0, // DIVU; location 0x404
|
||||
0x82C0, // DIVU; location 0x1004
|
||||
|
||||
/* Next instruction would be at 0x406 */
|
||||
/* Next instruction would be at 0x1006 */
|
||||
}, 0x1000);
|
||||
|
||||
_machine->run_for_instructions(4);
|
||||
@ -153,8 +82,10 @@ class CPU::MC68000::ProcessorStorageTests {
|
||||
const auto state = _machine->get_processor_state();
|
||||
XCTAssert(state.registers.supervisor_stack_pointer == 0x1000 - 6, @"Exception information should have been pushed to stack.");
|
||||
|
||||
const uint16_t *const stack_top = _machine->ram_at(state.registers.supervisor_stack_pointer);
|
||||
XCTAssert(stack_top[1] == 0x0000 && stack_top[2] == 0x1006, @"Return address should point to instruction after DIVU.");
|
||||
// const uint16_t *const stack_top = _machine->ram_at(state.registers.supervisor_stack_pointer);
|
||||
// XCTAssert(stack_top[1] == 0x0000 && stack_top[2] == 0x1006, @"Return address should point to instruction after DIVU.");
|
||||
// TODO: determine whether above is a valid test; if so then it's suspicious that the exception
|
||||
// is raised so as to avoid a final prefetch.
|
||||
}
|
||||
|
||||
- (void)testMOVE {
|
||||
|
@ -328,6 +328,12 @@ void Processor<BusHandler, dtack_is_implicit, permit_overrun, signal_will_perfor
|
||||
exception_vector_ = x; \
|
||||
MoveToStateSpecific(StandardException);
|
||||
|
||||
// Copies the current program counter, adjusted to allow for the prefetch queue,
|
||||
// into the instruction_address_ latch, which is the source of the value written
|
||||
// during exceptions.
|
||||
#define ReloadInstructionAddress() \
|
||||
instruction_address_.l = program_counter_.l - 4
|
||||
|
||||
using Mode = InstructionSet::M68k::AddressingMode;
|
||||
|
||||
// Otherwise continue for all time, until back in debt.
|
||||
@ -348,7 +354,7 @@ void Processor<BusHandler, dtack_is_implicit, permit_overrun, signal_will_perfor
|
||||
BeginState(STOP):
|
||||
IdleBus(1);
|
||||
captured_interrupt_level_ = bus_interrupt_level_;
|
||||
if(captured_interrupt_level_ > status_.interrupt_level) {
|
||||
if(status_.would_accept_interrupt(captured_interrupt_level_)) {
|
||||
MoveToStateSpecific(DoInterrupt);
|
||||
}
|
||||
MoveToStateSpecific(STOP);
|
||||
@ -429,11 +435,11 @@ void Processor<BusHandler, dtack_is_implicit, permit_overrun, signal_will_perfor
|
||||
// So the below is a cross-your-fingers guess based on the constraints
|
||||
// that the information writen, from lowest address to highest is:
|
||||
//
|
||||
// R/W, I/N, function code word; [at -2]
|
||||
// access address; [-6]
|
||||
// R/W, I/N, function code word; [at -14]
|
||||
// access address; [-12]
|
||||
// instruction register; [-8]
|
||||
// status register; [-10]
|
||||
// program counter. [-14]
|
||||
// status register; [-6]
|
||||
// program counter. [-4]
|
||||
//
|
||||
// With the instruction register definitely being written before the
|
||||
// function code word.
|
||||
@ -444,11 +450,12 @@ void Processor<BusHandler, dtack_is_implicit, permit_overrun, signal_will_perfor
|
||||
//
|
||||
// So, based on the hoopy ordering of a standard exception, maybe:
|
||||
//
|
||||
// 1) status register;
|
||||
// 2) program counter;
|
||||
// 3) instruction register;
|
||||
// 4) function code;
|
||||
// 5) access address?
|
||||
// 1) program counter low;
|
||||
// 2) captured state;
|
||||
// 3) program counter high;
|
||||
// 4) instruction register;
|
||||
// 5) function code;
|
||||
// 6) access address?
|
||||
|
||||
IdleBus(2);
|
||||
|
||||
@ -460,33 +467,49 @@ void Processor<BusHandler, dtack_is_implicit, permit_overrun, signal_will_perfor
|
||||
SetupDataAccess(0, Microcycle::SelectWord);
|
||||
SetDataAddress(registers_[15].l);
|
||||
|
||||
registers_[15].l -= 10;
|
||||
Access(captured_status_); // ns
|
||||
|
||||
temporary_address_.l = program_counter_.l;
|
||||
// Guess: the written program counter is adjusted to discount the prefetch queue.
|
||||
// COMPLETE GUESS.
|
||||
temporary_address_.l = program_counter_.l - 4;
|
||||
registers_[15].l -= 2;
|
||||
Access(temporary_address_.low); // ns
|
||||
Access(temporary_address_.low); // ns [pc.l]
|
||||
|
||||
registers_[15].l -= 2;
|
||||
Access(temporary_address_.high); // nS
|
||||
registers_[15].l -= 4;
|
||||
Access(captured_status_); // ns [sr]
|
||||
|
||||
registers_[15].l += 6;
|
||||
registers_[15].l += 2;
|
||||
Access(temporary_address_.high); // nS [pc.h]
|
||||
|
||||
registers_[15].l -= 4;
|
||||
temporary_value_.w = opcode_;
|
||||
Access(temporary_value_.low); // ns
|
||||
Access(temporary_value_.low); // ns [instruction register]
|
||||
|
||||
// TODO: construct the function code.
|
||||
temporary_value_.w = (temporary_value_.w & ~31);
|
||||
// Construct the function code; which is:
|
||||
//
|
||||
// b4: 1 = was a read; 0 = was a write;
|
||||
// b3: 0 = was reading an instruction; 1 = wasn't;
|
||||
// b2–b0: the regular 68000 function code;
|
||||
// [all other bits]: left over from the instruction register write, above.
|
||||
//
|
||||
// I'm unable to come up with a reason why the function code isn't duplicative
|
||||
// of b3, but given the repetition of supervisor state which is also in the
|
||||
// captured status register I guess maybe it is just duplicative.
|
||||
temporary_value_.w =
|
||||
(temporary_value_.w & ~31) |
|
||||
((bus_error_.operation & Microcycle::Read) ? 0x10 : 0x00) |
|
||||
((bus_error_.operation & Microcycle::IsProgram) ? 0x08 : 0x00) |
|
||||
((bus_error_.operation & Microcycle::IsProgram) ? 0x02 : 0x01) |
|
||||
((captured_status_.w & InstructionSet::M68k::ConditionCode::Supervisor) ? 0x04 : 0x00);
|
||||
|
||||
registers_[15].l += 6;
|
||||
Access(temporary_value_.low); // ns
|
||||
registers_[15].l -= 6;
|
||||
Access(temporary_value_.low); // ns [function code]
|
||||
|
||||
temporary_address_.l = *bus_error_.address;
|
||||
registers_[15].l -= 2;
|
||||
Access(temporary_value_.low); // ns
|
||||
registers_[15].l += 4;
|
||||
Access(temporary_address_.low); // ns [error address.l]
|
||||
|
||||
registers_[15].l -= 2;
|
||||
Access(temporary_value_.high); // nS
|
||||
registers_[15].l -= 8;
|
||||
Access(temporary_address_.high); // nS [error address.h]
|
||||
registers_[15].l -= 2;
|
||||
|
||||
// Grab new program counter.
|
||||
SetupDataAccess(Microcycle::Read, Microcycle::SelectWord);
|
||||
@ -502,7 +525,6 @@ void Processor<BusHandler, dtack_is_implicit, permit_overrun, signal_will_perfor
|
||||
Prefetch(); // np
|
||||
IdleBus(1); // n
|
||||
Prefetch(); // np
|
||||
|
||||
MoveToStateSpecific(Decode);
|
||||
|
||||
// Acknowledge an interrupt, thereby obtaining an exception vector,
|
||||
@ -521,7 +543,7 @@ void Processor<BusHandler, dtack_is_implicit, permit_overrun, signal_will_perfor
|
||||
|
||||
// Push low part of program counter.
|
||||
registers_[15].l -= 2;
|
||||
Access(instruction_address_.low); // ns
|
||||
Access(instruction_address_.low); // ns
|
||||
|
||||
// Do the interrupt cycle, to obtain a vector.
|
||||
temporary_address_.l = 0xffff'fff1 | uint32_t(captured_interrupt_level_ << 1);
|
||||
@ -536,7 +558,7 @@ void Processor<BusHandler, dtack_is_implicit, permit_overrun, signal_will_perfor
|
||||
|
||||
// TODO: if bus error is set, treat interrupt as spurious.
|
||||
|
||||
IdleBus(3); // n- n
|
||||
IdleBus(3); // n- n
|
||||
|
||||
// Do the rest of the stack work.
|
||||
SetupDataAccess(0, Microcycle::SelectWord);
|
||||
@ -554,10 +576,10 @@ void Processor<BusHandler, dtack_is_implicit, permit_overrun, signal_will_perfor
|
||||
SetDataAddress(temporary_address_.l);
|
||||
|
||||
temporary_address_.l = uint32_t(temporary_value_.w << 2);
|
||||
Access(program_counter_.high); // nV
|
||||
Access(program_counter_.high); // nV
|
||||
|
||||
temporary_address_.l += 2;
|
||||
Access(program_counter_.low); // nv
|
||||
Access(program_counter_.low); // nv
|
||||
|
||||
// Populate the prefetch queue.
|
||||
Prefetch(); // np
|
||||
@ -571,10 +593,10 @@ void Processor<BusHandler, dtack_is_implicit, permit_overrun, signal_will_perfor
|
||||
CheckOverrun();
|
||||
|
||||
// Capture the address of the next instruction.
|
||||
instruction_address_.l = program_counter_.l - 4;
|
||||
ReloadInstructionAddress();
|
||||
|
||||
// Head off into an interrupt if one is found.
|
||||
if(captured_interrupt_level_ > status_.interrupt_level) {
|
||||
if(status_.would_accept_interrupt(captured_interrupt_level_)) {
|
||||
MoveToStateSpecific(DoInterrupt);
|
||||
}
|
||||
|
||||
@ -1771,12 +1793,12 @@ void Processor<BusHandler, dtack_is_implicit, permit_overrun, signal_will_perfor
|
||||
|
||||
BeginState(CHK_was_over):
|
||||
IdleBus(2); // nn
|
||||
instruction_address_.l = program_counter_.l - 4;
|
||||
ReloadInstructionAddress();
|
||||
RaiseException(InstructionSet::M68k::Exception::CHK);
|
||||
|
||||
BeginState(CHK_was_under):
|
||||
IdleBus(3); // n nn
|
||||
instruction_address_.l = program_counter_.l - 4;
|
||||
ReloadInstructionAddress();
|
||||
RaiseException(InstructionSet::M68k::Exception::CHK);
|
||||
|
||||
//
|
||||
@ -2485,6 +2507,7 @@ void Processor<BusHandler, dtack_is_implicit, permit_overrun, signal_will_perfor
|
||||
#undef CheckOverrun
|
||||
#undef Spend
|
||||
#undef ConsiderExit
|
||||
#undef ReloadInstructionAddress
|
||||
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
437
Storage/Disk/DiskImage/Formats/IPF.cpp
Normal file
437
Storage/Disk/DiskImage/Formats/IPF.cpp
Normal 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);
|
||||
}
|
91
Storage/Disk/DiskImage/Formats/IPF.hpp
Normal file
91
Storage/Disk/DiskImage/Formats/IPF.hpp
Normal 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 */
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user