diff --git a/Analyser/Static/PCCompatible/Target.hpp b/Analyser/Static/PCCompatible/Target.hpp index 0d7c3f759..de84c8fa0 100644 --- a/Analyser/Static/PCCompatible/Target.hpp +++ b/Analyser/Static/PCCompatible/Target.hpp @@ -20,10 +20,17 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl, TargetPlatform::PCCompatible) // IMG (MS-DOS style) Format("image", result.disks, Disk::DiskImageHolder, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2) + Format("imd", result.disks, Disk::DiskImageHolder, TargetPlatform::PCCompatible) // IMD Format("img", result.disks, Disk::DiskImageHolder, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2) // Treat PC booter as a potential backup only if this doesn't parse as a FAT12. diff --git a/Components/8272/Status.hpp b/Components/8272/Status.hpp index 5a9330ab0..c15a631d4 100644 --- a/Components/8272/Status.hpp +++ b/Components/8272/Status.hpp @@ -47,12 +47,12 @@ enum class Status1: uint8_t { }; enum class Status2: uint8_t { - DeletedControlMark = 0x40, - DataCRCError = 0x20, - WrongCyinder = 0x10, - ScanEqualHit = 0x08, - ScanNotSatisfied = 0x04, - BadCylinder = 0x02, + DeletedControlMark = 0x40, + DataCRCError = 0x20, + WrongCyinder = 0x10, + ScanEqualHit = 0x08, + ScanNotSatisfied = 0x04, + BadCylinder = 0x02, MissingDataAddressMark = 0x01, }; diff --git a/Machines/PCCompatible/CGA.hpp b/Machines/PCCompatible/CGA.hpp index 4e7b45d5e..4d0742b70 100644 --- a/Machines/PCCompatible/CGA.hpp +++ b/Machines/PCCompatible/CGA.hpp @@ -15,41 +15,6 @@ namespace PCCompatible { -static constexpr uint8_t DarkCyan = 0b00'10'10; -static constexpr uint8_t DarkMagenta = 0b10'00'10; -static constexpr uint8_t DarkGrey = 0b10'10'10; - -static constexpr uint8_t DarkGreen = 0b00'10'00; -static constexpr uint8_t DarkRed = 0b10'00'00; -static constexpr uint8_t DarkYellow = 0b10'10'00; - -static constexpr uint8_t Brown = 0b10'01'00; - -/// @returns @c Brown if @c source is @c DarkYellow; @c source otherwise. -constexpr uint8_t yellow_to_brown(uint8_t source) { - return (source == DarkYellow) ? Brown : source; -} - -/// @returns The brightened (i.e. high intensity) version of @c source. -constexpr uint8_t bright(uint8_t source) { - return source | (source >> 1); -} - -/// Maps the RGB TTL triplet @c source to an appropriate output colour. -constexpr uint8_t rgb(uint8_t source) { - return uint8_t( - ((source & 0x01) << 1) | - ((source & 0x02) << 2) | - ((source & 0x04) << 3) - ); -} - -/// Maps the RGBI value in @c source to an appropriate output colour, including potential yellow-to-brown conversion. -constexpr uint8_t rgbi(uint8_t source) { - const uint8_t result = rgb(source); - return (source & 0x10) ? bright(result) : yellow_to_brown(result); -} - class CGA { public: CGA() : crtc_(Motorola::CRTC::Personality::HD6845S, outputter_) {} @@ -102,7 +67,7 @@ class CGA { // b1: 1 => positive edge from light pen has set trigger; // b0: 1 => safe to write to VRAM now without causing snow. (crtc_.get_bus_state().vsync ? 0b1001 : 0b0000) | - (crtc_.get_bus_state().hsync ? 0b0001 : 0b0000) | + (crtc_.get_bus_state().display_enable ? 0b0000 : 0b0001) | 0b0100; default: return 0xff; @@ -113,6 +78,7 @@ class CGA { void set_display_type(Outputs::Display::DisplayType display_type) { outputter_.crt.set_display_type(display_type); + outputter_.set_is_composite(Outputs::Display::is_composite(display_type)); } Outputs::Display::DisplayType get_display_type() const { return outputter_.crt.get_display_type(); @@ -165,6 +131,11 @@ class CGA { update_palette(); } + void set_is_composite(bool is_composite) { + is_composite_ = is_composite; + update_palette(); + } + void set_colours(uint8_t value) { colours_ = value; update_palette(); @@ -207,6 +178,7 @@ class CGA { // Determine new output state. update_hsync(state.hsync); const OutputState new_state = implied_state(state); + static constexpr uint8_t colour_phase = 200; // Upon either a state change or just having accumulated too much local time... if( @@ -219,7 +191,7 @@ class CGA { // (1) flush preexisting state. if(count) { switch(output_state) { - case OutputState::Sync: crt.output_sync(count * active_clock_divider); break; + case OutputState::Sync: crt.output_sync(count * active_clock_divider); break; case OutputState::Border: if(active_border_colour) { crt.output_blank(count * active_clock_divider); @@ -227,8 +199,8 @@ class CGA { crt.output_level(count * active_clock_divider, active_border_colour); } break; - case OutputState::ColourBurst: crt.output_colour_burst(count * active_clock_divider, 0); break; - case OutputState::Pixels: flush_pixels(); break; + case OutputState::ColourBurst: crt.output_colour_burst(count * active_clock_divider, colour_phase); break; + case OutputState::Pixels: flush_pixels(); break; } } @@ -335,18 +307,21 @@ class CGA { // Apply blink or background intensity. if(control_ & 0x20) { + // Set both colours to black if within a blink; otherwise consider a yellow-to-brown conversion. if((attributes & 0x80) && (state.field_count & 16)) { - std::swap(colours[0], colours[1]); + colours[0] = colours[1] = 0; + } else { + colours[0] = yellow_to_brown(colours[0]); } } else { if(attributes & 0x80) { colours[0] = bright(colours[0]); + } else { + // Yellow to brown definitely doesn't apply if the colour has been brightened. + colours[0] = yellow_to_brown(colours[0]); } } - // Potentially remap dark yellow to brown. - colours[0] = yellow_to_brown(colours[0]); - // Draw according to ROM contents. pixel_pointer[0] = (row & 0x80) ? colours[1] : colours[0]; pixel_pointer[1] = (row & 0x40) ? colours[1] : colours[0]; @@ -384,6 +359,7 @@ class CGA { int pixels_per_tick = 8; uint8_t colours_ = 0; uint8_t control_ = 0; + bool is_composite_ = false; enum class Mode { Pixels640, Pixels320, Text, } mode_ = Mode::Text; @@ -425,10 +401,48 @@ class CGA { border_colour = (mode_ != Mode::Pixels640) ? palette320[0] : 0; } + // + // Named colours and mapping logic. + // + static constexpr uint8_t DarkCyan = 0b00'10'10; + static constexpr uint8_t DarkMagenta = 0b10'00'10; + static constexpr uint8_t DarkGrey = 0b10'10'10; + + static constexpr uint8_t DarkGreen = 0b00'10'00; + static constexpr uint8_t DarkRed = 0b10'00'00; + static constexpr uint8_t DarkYellow = 0b10'10'00; + + static constexpr uint8_t Brown = 0b10'01'00; + + /// @returns @c Brown if @c source is @c DarkYellow and composite output is not enabled; @c source otherwise. + constexpr uint8_t yellow_to_brown(uint8_t source) { + return (source == DarkYellow && !is_composite_) ? Brown : source; + } + + /// @returns The brightened (i.e. high intensity) version of @c source. + constexpr uint8_t bright(uint8_t source) { + return source | (source >> 1); + } + + /// Maps the RGB TTL triplet @c source to an appropriate output colour. + constexpr uint8_t rgb(uint8_t source) { + return uint8_t( + ((source & 0x01) << 1) | + ((source & 0x02) << 2) | + ((source & 0x04) << 3) + ); + } + + /// Maps the RGBI value in @c source to an appropriate output colour, including potential yellow-to-brown conversion. + constexpr uint8_t rgbi(uint8_t source) { + const uint8_t result = rgb(source); + return (source & 0x10) ? bright(result) : yellow_to_brown(result); + } } outputter_; Motorola::CRTC::CRTC6845 crtc_; int full_clock_ = 0; + }; } diff --git a/Machines/PCCompatible/DMA.hpp b/Machines/PCCompatible/DMA.hpp index ddd17f1ea..875449843 100644 --- a/Machines/PCCompatible/DMA.hpp +++ b/Machines/PCCompatible/DMA.hpp @@ -31,7 +31,7 @@ class i8237 { template void write(uint8_t value) { - printf("DMA: Write %02x to %d\n", value, address); +// printf("DMA: Write %02x to %d\n", value, address); switch(address) { default: { @@ -66,7 +66,7 @@ class i8237 { template uint8_t read() { - printf("DMA: Read %d\n", address); +// printf("DMA: Read %d\n", address); switch(address) { default: { constexpr int channel = (address >> 1) & 3; @@ -146,7 +146,7 @@ class i8237 { channel.transfer_complete = false; } - printf("DMA: status is %02x\n", result); +// printf("DMA: status is %02x\n", result); return result; } @@ -157,19 +157,19 @@ class i8237 { } void flip_flop_reset() { - printf("DMA: Flip flop reset\n"); +// printf("DMA: Flip flop reset\n"); next_access_low_ = true; } void mask_reset() { - printf("DMA: Mask reset\n"); +// printf("DMA: Mask reset\n"); for(auto &channel : channels_) { channel.mask = false; } } void master_reset() { - printf("DMA: Master reset\n"); +// printf("DMA: Master reset\n"); flip_flop_reset(); for(auto &channel : channels_) { channel.mask = true; @@ -183,17 +183,17 @@ class i8237 { } void set_reset_mask(uint8_t value) { - printf("DMA: Set/reset mask %02x\n", value); +// printf("DMA: Set/reset mask %02x\n", value); channels_[value & 3].mask = value & 4; } void set_reset_request(uint8_t value) { - printf("DMA: Set/reset request %02x\n", value); +// printf("DMA: Set/reset request %02x\n", value); channels_[value & 3].request = value & 4; } void set_mask(uint8_t value) { - printf("DMA: Set mask %02x\n", value); +// printf("DMA: Set mask %02x\n", value); channels_[0].mask = value & 1; channels_[1].mask = value & 2; channels_[2].mask = value & 4; @@ -201,7 +201,7 @@ class i8237 { } void set_mode(uint8_t value) { - printf("DMA: Set mode %02x\n", value); +// printf("DMA: Set mode %02x\n", value); channels_[value & 3].transfer = Channel::Transfer((value >> 2) & 3); channels_[value & 3].autoinitialise = value & 0x10; channels_[value & 3].address_decrement = value & 0x20; @@ -209,7 +209,7 @@ class i8237 { } void set_command(uint8_t value) { - printf("DMA: Set command %02x\n", value); +// printf("DMA: Set command %02x\n", value); enable_memory_to_memory_ = value & 0x01; enable_channel0_address_hold_ = value & 0x02; enable_controller_ = value & 0x04; diff --git a/Machines/PCCompatible/MDA.hpp b/Machines/PCCompatible/MDA.hpp index ff18706ce..72a0a5a36 100644 --- a/Machines/PCCompatible/MDA.hpp +++ b/Machines/PCCompatible/MDA.hpp @@ -90,7 +90,7 @@ class MDA { // TODO: really this should be a Luminance8 and set an appropriate modal tint colour; // consider whether that's worth building into the scan target. { -// crt.set_visible_area(Outputs::Display::Rect(0.1072f, 0.1f, 0.842105263157895f, 0.842105263157895f)); + crt.set_visible_area(Outputs::Display::Rect(0.028f, 0.025f, 0.98f, 0.98f)); crt.set_display_type(Outputs::Display::DisplayType::RGB); } diff --git a/Machines/PCCompatible/PCCompatible.cpp b/Machines/PCCompatible/PCCompatible.cpp index 55529d43b..fa2c5221d 100644 --- a/Machines/PCCompatible/PCCompatible.cpp +++ b/Machines/PCCompatible/PCCompatible.cpp @@ -113,14 +113,36 @@ class FloppyController { printf("TODO: implement FDC command %d\n", uint8_t(decoder_.command())); break; - case Command::ReadData: { - printf("FDC: Read from drive %d / head %d / track %d of head %d / track %d / sector %d\n", - decoder_.target().drive, - decoder_.target().head, - drives_[decoder_.target().drive].track, - decoder_.geometry().head, + case Command::WriteDeletedData: + case Command::WriteData: { + status_.begin(decoder_); + + // Just decline to write, for now. + status_.set(Intel::i8272::Status1::NotWriteable); + status_.set(Intel::i8272::Status0::BecameNotReady); + + results_.serialise( + status_, decoder_.geometry().cylinder, - decoder_.geometry().sector); + decoder_.geometry().head, + decoder_.geometry().sector, + decoder_.geometry().size); + + // TODO: what if head has changed? + drives_[decoder_.target().drive].status = decoder_.drive_head(); + drives_[decoder_.target().drive].raised_interrupt = true; + pic_.apply_edge<6>(true); + } break; + + case Command::ReadDeletedData: + case Command::ReadData: { +// printf("FDC: Read from drive %d / head %d / track %d of head %d / track %d / sector %d\n", +// decoder_.target().drive, +// decoder_.target().head, +// drives_[decoder_.target().drive].track, +// decoder_.geometry().head, +// decoder_.geometry().cylinder, +// decoder_.geometry().sector); status_.begin(decoder_); @@ -131,11 +153,13 @@ class FloppyController { bool found_sector = false; for(auto &pair: drives_[decoder_.target().drive].sectors(decoder_.target().head)) { + // TODO: I suspect that not all these fields are tested for equality. if( (pair.second.address.track == target.cylinder) && (pair.second.address.sector == target.sector) && (pair.second.address.side == target.head) && - (pair.second.size == target.size) + (pair.second.size == target.size) && + (pair.second.is_deleted == (decoder_.command() == Command::ReadDeletedData)) ) { found_sector = true; bool wrote_in_full = true; @@ -280,7 +304,7 @@ class FloppyController { private: void reset() { - printf("FDC reset\n"); +// printf("FDC reset\n"); decoder_.clear(); status_.reset(); @@ -884,7 +908,7 @@ class FlowController { bool halted_ = false; }; -template +template class ConcreteMachine: public Machine, public MachineTypes::TimedMachine, @@ -951,9 +975,15 @@ class ConcreteMachine: // MARK: - TimedMachine. void run_for(const Cycles duration) override { const auto pit_ticks = duration.as_integral(); - cpu_divisor_ += pit_ticks; - int ticks = cpu_divisor_ / 3; - cpu_divisor_ %= 3; + + int ticks; + if constexpr (!turbo) { + cpu_divisor_ += pit_ticks; + ticks = cpu_divisor_ / 3; + cpu_divisor_ %= 3; + } else { + ticks = pit_ticks; + } while(ticks--) { // @@ -965,23 +995,30 @@ class ConcreteMachine: // pit_.run_for(1); ++speaker_.cycles_since_update; - pit_.run_for(1); - ++speaker_.cycles_since_update; - pit_.run_for(1); - ++speaker_.cycles_since_update; + + if constexpr (!turbo) { + pit_.run_for(1); + ++speaker_.cycles_since_update; + pit_.run_for(1); + ++speaker_.cycles_since_update; + } // // Advance CRTC at a more approximate rate. // - video_.run_for(Cycles(3)); + video_.run_for(turbo ? Cycles(1) : Cycles(3)); + + // + // Give the keyboard a notification of passing time; it's very approximately clocked, + // really just including 'some' delays to avoid being instant. + // + keyboard_.run_for(Cycles(1)); // // Perform one CPU instruction every three PIT cycles. // i.e. CPU instruction rate is 1/3 * ~1.19Mhz ~= 0.4 MIPS. // - keyboard_.run_for(Cycles(1)); - // Query for interrupts and apply if pending. if(pic_.pending() && context.flags.template flag()) { // Regress the IP if a REP is in-progress so as to resume it later. @@ -998,58 +1035,77 @@ class ConcreteMachine: ); } - // Do nothing if halted. + // Do nothing if currently halted. if(context.flow_controller.halted()) { continue; } - // Get the next thing to execute. - if(!context.flow_controller.should_repeat()) { - // Decode from the current IP. - decoded_ip_ = context.registers.ip(); - const auto remainder = context.memory.next_code(); - decoded = decoder.decode(remainder.first, remainder.second); - - // If that didn't yield a whole instruction then the end of memory must have been hit; - // continue from the beginning. - if(decoded.first <= 0) { - const auto all = context.memory.all(); - decoded = decoder.decode(all.first, all.second); - } - - context.registers.ip() += decoded.first; + if constexpr (turbo) { + // There's no divider applied, so this makes for 2*PI = around 2.4 MIPS. + // That's broadly 80286 speed, if MIPS were a valid measure. + perform_instruction(); + perform_instruction(); } else { - context.flow_controller.begin_instruction(); + // With the clock divider above, this makes for a net of PIT/3 = around 0.4 MIPS. + // i.e. a shade more than 8086 speed, if MIPS were meaningful. + perform_instruction(); } -/* if(decoded_ip_ >= 0x7c00 && decoded_ip_ < 0x7c00 + 1024) { - const auto next = to_string(decoded, InstructionSet::x86::Model::i8086); -// if(next != previous) { - std::cout << std::hex << decoded_ip_ << " " << next; - - if(decoded.second.operation() == InstructionSet::x86::Operation::INT) { - std::cout << " dl:" << std::hex << +context.registers.dl() << "; "; - std::cout << "ah:" << std::hex << +context.registers.ah() << "; "; - std::cout << "ch:" << std::hex << +context.registers.ch() << "; "; - std::cout << "cl:" << std::hex << +context.registers.cl() << "; "; - std::cout << "dh:" << std::hex << +context.registers.dh() << "; "; - std::cout << "es:" << std::hex << +context.registers.es() << "; "; - std::cout << "bx:" << std::hex << +context.registers.bx(); - } - - std::cout << std::endl; -// previous = next; -// } - }*/ - - // Execute it. - InstructionSet::x86::perform( - decoded.second, - context - ); + // Other inevitably broad and fuzzy and inconsistent MIPS counts for my own potential future play: + // + // 80386 @ 20Mhz: 4–5 MIPS. + // 80486 @ 66Mhz: 25 MIPS. + // Pentium @ 100Mhz: 188 MIPS. } } + void perform_instruction() { + // Get the next thing to execute. + if(!context.flow_controller.should_repeat()) { + // Decode from the current IP. + decoded_ip_ = context.registers.ip(); + const auto remainder = context.memory.next_code(); + decoded = decoder.decode(remainder.first, remainder.second); + + // If that didn't yield a whole instruction then the end of memory must have been hit; + // continue from the beginning. + if(decoded.first <= 0) { + const auto all = context.memory.all(); + decoded = decoder.decode(all.first, all.second); + } + + context.registers.ip() += decoded.first; + } else { + context.flow_controller.begin_instruction(); + } + +/* if(decoded_ip_ >= 0x7c00 && decoded_ip_ < 0x7c00 + 1024) { + const auto next = to_string(decoded, InstructionSet::x86::Model::i8086); +// if(next != previous) { + std::cout << std::hex << decoded_ip_ << " " << next; + + if(decoded.second.operation() == InstructionSet::x86::Operation::INT) { + std::cout << " dl:" << std::hex << +context.registers.dl() << "; "; + std::cout << "ah:" << std::hex << +context.registers.ah() << "; "; + std::cout << "ch:" << std::hex << +context.registers.ch() << "; "; + std::cout << "cl:" << std::hex << +context.registers.cl() << "; "; + std::cout << "dh:" << std::hex << +context.registers.dh() << "; "; + std::cout << "es:" << std::hex << +context.registers.es() << "; "; + std::cout << "bx:" << std::hex << +context.registers.bx(); + } + + std::cout << std::endl; +// previous = next; +// } + }*/ + + // Execute it. + InstructionSet::x86::perform( + decoded.second, + context + ); + } + // MARK: - ScanProducer. void set_scan_target(Outputs::Display::ScanTarget *scan_target) override { video_.set_scan_target(scan_target); @@ -1109,10 +1165,9 @@ class ConcreteMachine: void set_display_type(Outputs::Display::DisplayType display_type) override { video_.set_display_type(display_type); - ppi_handler_.hint_is_composite( - (display_type == Outputs::Display::DisplayType::CompositeColour) || - (display_type == Outputs::Display::DisplayType::CompositeMonochrome) - ); + + // Give the PPI a shout-out in case it isn't too late to switch to CGA40. + ppi_handler_.hint_is_composite(Outputs::Display::is_composite(display_type)); } Outputs::Display::DisplayType get_display_type() const override { @@ -1181,10 +1236,18 @@ Machine *Machine::PCCompatible(const Analyser::Static::Target *target, const ROM using Target = Analyser::Static::PCCompatible::Target; const Target *const pc_target = dynamic_cast(target); - switch(pc_target->adaptor) { - case VideoAdaptor::MDA: return new PCCompatible::ConcreteMachine(*pc_target, rom_fetcher); - case VideoAdaptor::CGA: return new PCCompatible::ConcreteMachine(*pc_target, rom_fetcher); - default: return nullptr; + if(pc_target->speed == Target::Speed::Fast) { + switch(pc_target->adaptor) { + case VideoAdaptor::MDA: return new PCCompatible::ConcreteMachine(*pc_target, rom_fetcher); + case VideoAdaptor::CGA: return new PCCompatible::ConcreteMachine(*pc_target, rom_fetcher); + default: return nullptr; + } + } else { + switch(pc_target->adaptor) { + case VideoAdaptor::MDA: return new PCCompatible::ConcreteMachine(*pc_target, rom_fetcher); + case VideoAdaptor::CGA: return new PCCompatible::ConcreteMachine(*pc_target, rom_fetcher); + default: return nullptr; + } } } diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 812890027..6a99225bb 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -37,6 +37,8 @@ 42A5E8532ABBE16F00A0DD5D /* lax_test.bin in Resources */ = {isa = PBXBuildFile; fileRef = 42A5E8422ABBE16F00A0DD5D /* lax_test.bin */; }; 42A5E8542ABBE16F00A0DD5D /* branch_backwards_test.bin in Resources */ = {isa = PBXBuildFile; fileRef = 42A5E8432ABBE16F00A0DD5D /* branch_backwards_test.bin */; }; 42E5C3932AC46A7700DA093D /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 42E5C3922AC46A7700DA093D /* Carbon.framework */; }; + 42EB81282B23AAC300429AF4 /* IMD.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 42EB81262B23AAC300429AF4 /* IMD.cpp */; }; + 42EB81292B23AAC300429AF4 /* IMD.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 42EB81262B23AAC300429AF4 /* IMD.cpp */; }; 4B018B89211930DE002A3937 /* 65C02_extended_opcodes_test.bin in Resources */ = {isa = PBXBuildFile; fileRef = 4B018B88211930DE002A3937 /* 65C02_extended_opcodes_test.bin */; }; 4B01A6881F22F0DB001FD6E3 /* Z80MemptrTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B01A6871F22F0DB001FD6E3 /* Z80MemptrTests.swift */; }; 4B0333AF2094081A0050B93D /* AppleDSK.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0333AD2094081A0050B93D /* AppleDSK.cpp */; }; @@ -1195,6 +1197,8 @@ 42AD55312A0C4D5000ACE410 /* 68000Implementation.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = 68000Implementation.hpp; sourceTree = ""; }; 42E5C3922AC46A7700DA093D /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; 42EB81252B21788200429AF4 /* RTC.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RTC.hpp; sourceTree = ""; }; + 42EB81262B23AAC300429AF4 /* IMD.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = IMD.cpp; sourceTree = ""; }; + 42EB81272B23AAC300429AF4 /* IMD.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = IMD.hpp; sourceTree = ""; }; 4B018B88211930DE002A3937 /* 65C02_extended_opcodes_test.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = 65C02_extended_opcodes_test.bin; path = "Klaus Dormann/65C02_extended_opcodes_test.bin"; sourceTree = ""; }; 4B01A6871F22F0DB001FD6E3 /* Z80MemptrTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Z80MemptrTests.swift; sourceTree = ""; }; 4B0333AD2094081A0050B93D /* AppleDSK.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = AppleDSK.cpp; sourceTree = ""; }; @@ -3064,6 +3068,7 @@ 4BEBFB4B2002C4BF000708CC /* FAT12.cpp */, 4B4518931F75FD1B00926311 /* G64.cpp */, 4B4518951F75FD1B00926311 /* HFE.cpp */, + 42EB81262B23AAC300429AF4 /* IMD.cpp */, 4B5B372F2777C7FC0047F238 /* IPF.cpp */, 4BB4BFAE22A42F290069048D /* MacintoshIMG.cpp */, 4B58601C1F806AB200AEE2E3 /* MFMSectorDump.cpp */, @@ -3084,6 +3089,7 @@ 4BEBFB4C2002C4BF000708CC /* FAT12.hpp */, 4B4518941F75FD1B00926311 /* G64.hpp */, 4B4518961F75FD1B00926311 /* HFE.hpp */, + 42EB81272B23AAC300429AF4 /* IMD.hpp */, 4B5B37302777C7FC0047F238 /* IPF.hpp */, 4BB4BFAF22A42F290069048D /* MacintoshIMG.hpp */, 4B58601D1F806AB200AEE2E3 /* MFMSectorDump.hpp */, @@ -5959,6 +5965,7 @@ 4B051C912669C90B00CA44E8 /* ROMCatalogue.cpp in Sources */, 4B055AB91FAE86170060FFFF /* Acorn.cpp in Sources */, 4B302185208A550100773308 /* DiskII.cpp in Sources */, + 42EB81292B23AAC300429AF4 /* IMD.cpp in Sources */, 4B051CB1267C1CA200CA44E8 /* Keyboard.cpp in Sources */, 4B0F1BB32602645900B85C66 /* StaticAnalyser.cpp in Sources */, 4B055A931FAE85B50060FFFF /* BinaryDump.cpp in Sources */, @@ -6057,6 +6064,7 @@ 4B0333AF2094081A0050B93D /* AppleDSK.cpp in Sources */, 4B894518201967B4007DE474 /* ConfidenceCounter.cpp in Sources */, 4BCE005A227CFFCA000CA200 /* Macintosh.cpp in Sources */, + 42EB81282B23AAC300429AF4 /* IMD.cpp in Sources */, 4B6AAEA4230E3E1D0078E864 /* MassStorageDevice.cpp in Sources */, 4B89452E201967B4007DE474 /* StaticAnalyser.cpp in Sources */, 4BC890D3230F86020025A55A /* DirectAccessDevice.cpp in Sources */, diff --git a/OSBindings/Mac/Clock Signal/Info.plist b/OSBindings/Mac/Clock Signal/Info.plist index 37dbdb05d..9e367a765 100644 --- a/OSBindings/Mac/Clock Signal/Info.plist +++ b/OSBindings/Mac/Clock Signal/Info.plist @@ -691,6 +691,26 @@ NSDocumentClass $(PRODUCT_MODULE_NAME).MachineDocument + + CFBundleTypeExtensions + + imd + + CFBundleTypeName + IMD disk image + CFBundleTypeOSTypes + + ???? + + CFBundleTypeRole + Viewer + LSHandlerRank + Owner + LSTypeIsPackage + + NSDocumentClass + $(PRODUCT_MODULE_NAME).MachineDocument + CFBundleTypeExtensions diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h index 1dca59a35..1df0c77b7 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h @@ -123,8 +123,9 @@ typedef NS_ENUM(NSInteger, CSMachineMSXRegion) { CSMachineMSXRegionJapanese, }; -typedef NS_ENUM(NSInteger, CSPCCompatibleModel) { - CSPCCompatibleModelTurboXT, +typedef NS_ENUM(NSInteger, CSPCCompatibleSpeed) { + CSPCCompatibleSpeedOriginal, + CSPCCompatibleSpeedTurbo, }; typedef NS_ENUM(NSInteger, CSPCCompatibleVideoAdaptor) { @@ -152,7 +153,7 @@ typedef int Kilobytes; - (instancetype)initWithVic20Region:(CSMachineVic20Region)region memorySize:(Kilobytes)memorySize hasC1540:(BOOL)hasC1540; - (instancetype)initWithZX80MemorySize:(Kilobytes)memorySize useZX81ROM:(BOOL)useZX81ROM; - (instancetype)initWithZX81MemorySize:(Kilobytes)memorySize; -- (instancetype)initWithPCCompatibleModel:(CSPCCompatibleModel)model videoAdaptor:(CSPCCompatibleVideoAdaptor)adaptor; +- (instancetype)initWithPCCompatibleSpeed:(CSPCCompatibleSpeed)speed videoAdaptor:(CSPCCompatibleVideoAdaptor)adaptor; @property(nonatomic, readonly, nullable) NSString *optionsNibName; @property(nonatomic, readonly) NSString *displayName; diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm index 7ed70dbde..9bdcc1f9e 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm @@ -273,7 +273,7 @@ return self; } -- (instancetype)initWithPCCompatibleModel:(CSPCCompatibleModel)model videoAdaptor:(CSPCCompatibleVideoAdaptor)adaptor { +- (instancetype)initWithPCCompatibleSpeed:(CSPCCompatibleSpeed)speed videoAdaptor:(CSPCCompatibleVideoAdaptor)adaptor { self = [super init]; if(self) { using Target = Analyser::Static::PCCompatible::Target; @@ -282,6 +282,10 @@ case CSPCCompatibleVideoAdaptorMDA: target->adaptor = Target::VideoAdaptor::MDA; break; case CSPCCompatibleVideoAdaptorCGA: target->adaptor = Target::VideoAdaptor::CGA; break; } + switch(speed) { + case CSPCCompatibleSpeedOriginal: target->speed = Target::Speed::ApproximatelyOriginal; break; + case CSPCCompatibleSpeedTurbo: target->speed = Target::Speed::Fast; break; + } _targets.push_back(std::move(target)); } return self; diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib index 58ae4b8f1..d2ae6f057 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib +++ b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib @@ -750,13 +750,39 @@ Gw + + + + + + + + + + + + + + + + + + + + + + + + + - + + @@ -1069,6 +1095,7 @@ Gw + diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift b/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift index 168b2fc85..fa2462c33 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift +++ b/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift @@ -65,6 +65,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { // MARK: - PC compatible properties @IBOutlet var pcVideoAdaptorButton: NSPopUpButton! + @IBOutlet var pcSpeedButton: NSPopUpButton! // MARK: - Spectrum properties @IBOutlet var spectrumModelTypeButton: NSPopUpButton! @@ -155,6 +156,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { // PC settings pcVideoAdaptorButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.pcVideoAdaptor")) + pcSpeedButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.pcSpeed")) // Spectrum settings spectrumModelTypeButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.spectrumModel")) @@ -224,6 +226,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { // PC settings standardUserDefaults.set(pcVideoAdaptorButton.selectedTag(), forKey: "new.pcVideoAdaptor") + standardUserDefaults.set(pcSpeedButton.selectedTag(), forKey: "new.pcSpeed") // Spectrum settings standardUserDefaults.set(spectrumModelTypeButton.selectedTag(), forKey: "new.spectrumModel") @@ -416,7 +419,12 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { case 1: videoAdaptor = .CGA default: break } - return CSStaticAnalyser(pcCompatibleModel: .turboXT, videoAdaptor: videoAdaptor) + var speed: CSPCCompatibleSpeed = .original + switch pcSpeedButton.selectedTag() { + case 80286: speed = .turbo + default: break + } + return CSStaticAnalyser(pcCompatibleSpeed: speed, videoAdaptor: videoAdaptor) case "spectrum": var model: CSMachineSpectrumModel = .plus2a diff --git a/Outputs/ScanTarget.hpp b/Outputs/ScanTarget.hpp index 6b01fe3cc..487559d4f 100644 --- a/Outputs/ScanTarget.hpp +++ b/Outputs/ScanTarget.hpp @@ -51,6 +51,10 @@ enum class DisplayType { CompositeMonochrome }; +constexpr bool is_composite(DisplayType type) { + return type == DisplayType::CompositeColour || type == DisplayType::CompositeMonochrome; +} + /*! Enumerates the potential formats of input data. diff --git a/Storage/Disk/DiskImage/Formats/CPCDSK.hpp b/Storage/Disk/DiskImage/Formats/CPCDSK.hpp index 566cb2300..ab4d333cb 100644 --- a/Storage/Disk/DiskImage/Formats/CPCDSK.hpp +++ b/Storage/Disk/DiskImage/Formats/CPCDSK.hpp @@ -24,19 +24,18 @@ namespace Storage::Disk { class CPCDSK: public DiskImage { public: /*! - Construct an @c AcornADF containing content from the file with name @c file_name. + Construct a @c CPCDSK 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 Acorn .ADF format image. */ CPCDSK(const std::string &file_name); - // implemented to satisfy @c Disk + // DiskImage interface. HeadPosition get_maximum_head_position() final; int get_head_count() final; bool get_is_read_only() final; - - void set_tracks(const std::map> &tracks) final; + void set_tracks(const std::map<::Storage::Disk::Track::Address, std::shared_ptr<::Storage::Disk::Track>> &tracks) final; std::shared_ptr<::Storage::Disk::Track> get_track_at_position(::Storage::Disk::Track::Address address) final; private: diff --git a/Storage/Disk/DiskImage/Formats/IMD.cpp b/Storage/Disk/DiskImage/Formats/IMD.cpp new file mode 100644 index 000000000..379fddaf5 --- /dev/null +++ b/Storage/Disk/DiskImage/Formats/IMD.cpp @@ -0,0 +1,163 @@ +// +// IMD.cpp +// Clock Signal +// +// Created by Thomas Harte on 08/12/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#include "IMD.hpp" + +#include "../../Encodings/MFM/Constants.hpp" +#include "../../Encodings/MFM/Encoder.hpp" +#include "../../Encodings/MFM/SegmentParser.hpp" +#include "../../Track/TrackSerialiser.hpp" + +#include + +using namespace Storage::Disk; + +// Documentation source: https://oldcomputers-ddns.org/public/pub/manuals/imd.pdf + +IMD::IMD(const std::string &file_name) : file_(file_name) { + // Check for signature. + if(!file_.check_signature("IMD")) { + throw Error::InvalidFormat; + } + + // Skip rest of ASCII. + while(file_.get8() != 0x1a); + + // Build track map. + while(true) { + const auto location = file_.tell(); + + // Skip mode. + file_.seek(1, SEEK_CUR); + + // Grab relevant fields. + const uint8_t cylinder = file_.get8(); + const uint8_t head = file_.get8(); + const uint8_t sector_count = file_.get8(); + const uint8_t sector_size = file_.get8(); + if(file_.eof()) { + break; + } + + cylinders_ = std::max(cylinder, cylinders_); + heads_ = std::max(uint8_t(head & 1), heads_); + + // Update head and cylinder extents, record sector location for later. + track_locations_.emplace( + Storage::Disk::Track::Address(head & 1, HeadPosition(cylinder)), + location); + + // Skip sector numbers. + file_.seek(sector_count, SEEK_CUR); + + // Skip cylinder map. + if(head & 0x80) { + file_.seek(sector_count, SEEK_CUR); + } + + // Skip head map. + if(head & 0x40) { + file_.seek(sector_count, SEEK_CUR); + } + + // Skip sectors. + for(int c = 0; c < sector_count; c++) { + const uint8_t type = file_.get8(); + switch(type) { + case 0x00: break; // Sector couldn't be read. + + // Types with all sector data present. + case 0x01: case 0x03: case 0x05: case 0x07: + file_.seek(128 << sector_size, SEEK_CUR); + break; + + // Types with a single byte present. + case 0x02: case 0x04: case 0x06: case 0x08: + file_.seek(1, SEEK_CUR); + break; + } + } + } + + // Both heads_ and cylinders_ are now the maximum observed IDs, which + // are one less than the counts. + ++ cylinders_; + ++ heads_; +} + +HeadPosition IMD::get_maximum_head_position() { + return HeadPosition(cylinders_); +} + +int IMD::get_head_count() { + return heads_ + 1; +} + +std::shared_ptr<::Storage::Disk::Track> IMD::get_track_at_position(::Storage::Disk::Track::Address address) { + auto location = track_locations_.find(address); + if(location == track_locations_.end()) { + return nullptr; + } + + // Seek to track, parse fully this time. + file_.seek(location->second, SEEK_SET); + + const uint8_t mode = file_.get8(); + const uint8_t cylinder = file_.get8(); + const uint8_t head = file_.get8(); + const uint8_t sector_count = file_.get8(); + const uint8_t sector_size = file_.get8(); + + const std::vector sector_ids = file_.read(sector_count); + const std::vector cylinders = (head & 0x80) ? file_.read(sector_count) : std::vector{}; + const std::vector heads = (head & 0x40) ? file_.read(sector_count) : std::vector{}; + + std::vector sectors; + sectors.reserve(sector_count); + + for(size_t c = 0; c < sector_count; c++) { + sectors.emplace_back(); + Storage::Encodings::MFM::Sector §or = sectors.back(); + + // Set up sector address. + sector.address.track = cylinders.empty() ? cylinder : cylinders[c]; + sector.address.side = heads.empty() ? head & 1 : heads[c]; + sector.address.sector = sector_ids[c]; + sector.size = sector_size; + + const auto byte_size = size_t(128 << sector_size); + uint8_t type = file_.get8(); + + // Type 0: sector was present, but couldn't be read. + // Since body CRC errors are a separate item, just don't include a body at all. + if(!type) { + continue; + } + + // Decrement type to turn it into a bit field: + // + // b0 => is compressed within disk image; + // b1 => had deleted address mark; + // b2 => had data CRC error. + --type; + sector.is_deleted = type & 2; + sector.has_data_crc_error = type & 4; + if(type & 1) { + sector.samples.emplace_back(byte_size, file_.get8()); + } else { + sector.samples.push_back(file_.read(byte_size)); + } + } + + // Mode also indicates data density, but I don't have a good strategy for reconciling that if + // it were to disagree with the density implied by the quantity of sectors. So a broad 'is it MFM' test is + // applied only. + return (mode >= 3) ? + Storage::Encodings::MFM::GetMFMTrackWithSectors(sectors) : + Storage::Encodings::MFM::GetFMTrackWithSectors(sectors); +} diff --git a/Storage/Disk/DiskImage/Formats/IMD.hpp b/Storage/Disk/DiskImage/Formats/IMD.hpp new file mode 100644 index 000000000..2ce703e7a --- /dev/null +++ b/Storage/Disk/DiskImage/Formats/IMD.hpp @@ -0,0 +1,45 @@ +// +// IMD.hpp +// Clock Signal +// +// Created by Thomas Harte on 08/12/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#ifndef IMD_hpp +#define IMD_hpp + +#include "../DiskImage.hpp" +#include "../../../FileHolder.hpp" + +namespace Storage::Disk { + +/*! + Provides an @c DiskImage containing an IMD image, which is a collection of arbitrarily-numbered FM or MFM + sectors collected by track. +*/ + +class IMD: public DiskImage { + public: + /*! + Construct an @c IMD 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 Acorn .ADF format image. + */ + IMD(const std::string &file_name); + + // DiskImage interface. + HeadPosition get_maximum_head_position() final; + int get_head_count() final; + std::shared_ptr<::Storage::Disk::Track> get_track_at_position(::Storage::Disk::Track::Address address) final; + + private: + FileHolder file_; + std::map track_locations_; + uint8_t cylinders_ = 0, heads_ = 0; +}; + +} + +#endif /* IMD_hpp */