From 0af405aa46622040917d7421a057fff0fab02ecf Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 14 Apr 2021 21:37:10 -0400 Subject: [PATCH 01/12] Starts working in the 48kb and 128kb Spectrums. --- Analyser/Static/ZXSpectrum/Target.hpp | 4 + Machines/Sinclair/ZXSpectrum/Video.hpp | 4 +- Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp | 123 +++++++++++------- .../Machine/StaticAnalyser/CSStaticAnalyser.h | 4 + .../StaticAnalyser/CSStaticAnalyser.mm | 8 +- .../Base.lproj/MachinePicker.xib | 22 ++-- .../MachinePicker/MachinePicker.swift | 4 + ROMImages/ZXSpectrum/128.rom | Bin 0 -> 32768 bytes ROMImages/ZXSpectrum/48.rom | Bin 0 -> 16384 bytes ROMImages/ZXSpectrum/plus2.rom | Bin 0 -> 32768 bytes ROMImages/ZXSpectrum/readme.txt | 3 + 11 files changed, 116 insertions(+), 56 deletions(-) create mode 100644 ROMImages/ZXSpectrum/128.rom create mode 100644 ROMImages/ZXSpectrum/48.rom create mode 100644 ROMImages/ZXSpectrum/plus2.rom diff --git a/Analyser/Static/ZXSpectrum/Target.hpp b/Analyser/Static/ZXSpectrum/Target.hpp index e2711ec55..4996d21fa 100644 --- a/Analyser/Static/ZXSpectrum/Target.hpp +++ b/Analyser/Static/ZXSpectrum/Target.hpp @@ -19,6 +19,10 @@ namespace ZXSpectrum { struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl { ReflectableEnum(Model, + SixteenK, + FortyEightK, + OneTwoEightK, + Plus2, Plus2a, Plus3, ); diff --git a/Machines/Sinclair/ZXSpectrum/Video.hpp b/Machines/Sinclair/ZXSpectrum/Video.hpp index 371697511..4270815dc 100644 --- a/Machines/Sinclair/ZXSpectrum/Video.hpp +++ b/Machines/Sinclair/ZXSpectrum/Video.hpp @@ -18,7 +18,9 @@ namespace Sinclair { namespace ZXSpectrum { enum class VideoTiming { - Plus3 + FortyEightK, + OneTwoEightK, + Plus3, }; /* diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp index 717026ee4..0947312b2 100644 --- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp +++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp @@ -81,8 +81,29 @@ template class ConcreteMachine: // With only the +2a and +3 currently supported, the +3 ROM is always // the one required. - const auto roms = - rom_fetcher({ {"ZXSpectrum", "the +2a/+3 ROM", "plus3.rom", 64 * 1024, 0x96e3c17a} }); + std::vector rom_names; + const std::string machine = "ZXSpectrum"; + switch(model) { + case Model::SixteenK: + case Model::FortyEightK: + rom_names.emplace_back(machine, "the 48kb ROM", "48.rom", 16 * 1024, 0xddee531f); + break; + + case Model::OneTwoEightK: + rom_names.emplace_back(machine, "the 128kb ROM", "128.rom", 32 * 1024, 0x2cbe8995); + break; + + case Model::Plus2: + rom_names.emplace_back(machine, "the +2 ROM", "plus2.rom", 32 * 1024, 0xe7a517dc); + break; + + case Model::Plus2a: + case Model::Plus3: { + const std::initializer_list crc32s = { 0x96e3c17a, 0xbe0d9ec4 }; + rom_names.emplace_back(machine, "the +2a/+3 ROM", "plus3.rom", 64 * 1024, crc32s); + } break; + } + const auto roms = rom_fetcher(rom_names); if(!roms[0]) throw ROMMachine::Error::MissingROMs; memcpy(rom_.data(), roms[0]->data(), std::min(rom_.size(), roms[0]->size())); @@ -110,7 +131,7 @@ template class ConcreteMachine: } static constexpr unsigned int clock_rate() { -// constexpr unsigned int ClockRate = 3'500'000; + constexpr unsigned int OriginalClockRate = 3'500'000; constexpr unsigned int Plus3ClockRate = 3'546'875; // See notes below; this is a guess. // Notes on timing for the +2a and +3: @@ -137,7 +158,7 @@ template class ConcreteMachine: // the Spectrum is a PAL machine with a fixed colour phase relationship. For // this emulator's world, that's a first! - return Plus3ClockRate; + return model < Model::OneTwoEightK ? OriginalClockRate : Plus3ClockRate; } // MARK: - TimedMachine. @@ -189,18 +210,22 @@ template class ConcreteMachine: const uint16_t address = cycle.address ? *cycle.address : 0x0000; // Apply contention if necessary. - if( - is_contended_[address >> 14] && - cycle.operation >= PartialMachineCycle::ReadOpcodeStart && - cycle.operation <= PartialMachineCycle::WriteStart) { - // Assumption here: the trigger for the ULA inserting a delay is the falling edge - // of MREQ, which is always half a cycle into a read or write. - // - // TODO: somehow provide that information in the PartialMachineCycle? + if constexpr (model >= Model::Plus2a) { + if( + is_contended_[address >> 14] && + cycle.operation >= PartialMachineCycle::ReadOpcodeStart && + cycle.operation <= PartialMachineCycle::WriteStart) { + // Assumption here: the trigger for the ULA inserting a delay is the falling edge + // of MREQ, which is always half a cycle into a read or write. + // + // TODO: somehow provide that information in the PartialMachineCycle? - const HalfCycles delay = video_.last_valid()->access_delay(video_.time_since_flush() + HalfCycles(1)); - advance(cycle.length + delay); - return delay; + const HalfCycles delay = video_.last_valid()->access_delay(video_.time_since_flush() + HalfCycles(1)); + advance(cycle.length + delay); + return delay; + } + } else { + // TODO. } // For all other machine cycles, model the action as happening at the end of the machine cycle; @@ -263,39 +288,45 @@ template class ConcreteMachine: } // Test for classic 128kb paging register (i.e. port 7ffd). - if((address & 0xc002) == 0x4000) { - port7ffd_ = *cycle.value; - update_memory_map(); + if constexpr (model >= Model::OneTwoEightK) { + if((address & 0xc002) == 0x4000) { + port7ffd_ = *cycle.value; + update_memory_map(); - // Set the proper video base pointer. - set_video_address(); + // Set the proper video base pointer. + set_video_address(); - // Potentially lock paging, _after_ the current - // port values have taken effect. - disable_paging_ |= *cycle.value & 0x20; - } - - // Test for +2a/+3 paging (i.e. port 1ffd). - if((address & 0xf002) == 0x1000) { - port1ffd_ = *cycle.value; - update_memory_map(); - update_video_base(); - - if constexpr (model == Model::Plus3) { - fdc_->set_motor_on(*cycle.value & 0x08); + // Potentially lock paging, _after_ the current + // port values have taken effect. + disable_paging_ |= *cycle.value & 0x20; } } - if((address & 0xc002) == 0xc000) { - // Select AY register. - update_audio(); - GI::AY38910::Utility::select_register(ay_, *cycle.value); + // Test for +2a/+3 paging (i.e. port 1ffd). + if constexpr (model >= Model::Plus2a) { + if((address & 0xf002) == 0x1000) { + port1ffd_ = *cycle.value; + update_memory_map(); + update_video_base(); + + if constexpr (model == Model::Plus3) { + fdc_->set_motor_on(*cycle.value & 0x08); + } + } } - if((address & 0xc002) == 0x8000) { - // Write to AY register. - update_audio(); - GI::AY38910::Utility::write_data(ay_, *cycle.value); + if constexpr (model >= Model::OneTwoEightK) { + if((address & 0xc002) == 0xc000) { + // Select AY register. + update_audio(); + GI::AY38910::Utility::select_register(ay_, *cycle.value); + } + + if((address & 0xc002) == 0x8000) { + // Write to AY register. + update_audio(); + GI::AY38910::Utility::write_data(ay_, *cycle.value); + } } if constexpr (model == Model::Plus3) { @@ -574,7 +605,7 @@ template class ConcreteMachine: is_contended_[bank] = (source >= 4 && source < 8); pages_[bank] = source; - uint8_t *read = (source < 0x80) ? &ram_[source * 16384] : &rom_[(source & 0x7f) * 16384]; + uint8_t *const read = (source < 0x80) ? &ram_[source * 16384] : &rom_[(source & 0x7f) * 16384]; const auto offset = bank*16384; read_pointers_[bank] = read - offset; @@ -712,8 +743,12 @@ Machine *Machine::ZXSpectrum(const Analyser::Static::Target *target, const ROMMa const auto zx_target = dynamic_cast(target); switch(zx_target->model) { - case Model::Plus2a: return new ConcreteMachine(*zx_target, rom_fetcher); - case Model::Plus3: return new ConcreteMachine(*zx_target, rom_fetcher); + case Model::SixteenK: return new ConcreteMachine(*zx_target, rom_fetcher); + case Model::FortyEightK: return new ConcreteMachine(*zx_target, rom_fetcher); + case Model::OneTwoEightK: return new ConcreteMachine(*zx_target, rom_fetcher); + case Model::Plus2: return new ConcreteMachine(*zx_target, rom_fetcher); + case Model::Plus2a: return new ConcreteMachine(*zx_target, rom_fetcher); + case Model::Plus3: return new ConcreteMachine(*zx_target, rom_fetcher); } return nullptr; diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h index 1193281f4..62df779b9 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.h @@ -63,6 +63,10 @@ typedef NS_ENUM(NSInteger, CSMachineOricDiskInterface) { }; typedef NS_ENUM(NSInteger, CSMachineSpectrumModel) { + CSMachineSpectrumModelSixteenK, + CSMachineSpectrumModelFortyEightK, + CSMachineSpectrumModelOneTwoEightK, + CSMachineSpectrumModelPlus2, CSMachineSpectrumModelPlus2a, CSMachineSpectrumModelPlus3, }; diff --git a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm index 993a5c047..8f4c693b4 100644 --- a/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm +++ b/OSBindings/Mac/Clock Signal/Machine/StaticAnalyser/CSStaticAnalyser.mm @@ -194,8 +194,12 @@ using Target = Analyser::Static::ZXSpectrum::Target; auto target = std::make_unique(); switch(model) { - case CSMachineSpectrumModelPlus2a: target->model = Target::Model::Plus2a; break; - case CSMachineSpectrumModelPlus3: target->model = Target::Model::Plus3; break; + case CSMachineSpectrumModelSixteenK: target->model = Target::Model::SixteenK; break; + case CSMachineSpectrumModelFortyEightK: target->model = Target::Model::FortyEightK; break; + case CSMachineSpectrumModelOneTwoEightK: target->model = Target::Model::OneTwoEightK; break; + case CSMachineSpectrumModelPlus2: target->model = Target::Model::Plus2; break; + case CSMachineSpectrumModelPlus2a: target->model = Target::Model::Plus2a; break; + case CSMachineSpectrumModelPlus3: target->model = Target::Model::Plus3; break; } _targets.push_back(std::move(target)); } diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib index 31e1925f8..cc68c2170 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib +++ b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib @@ -18,7 +18,7 @@ - + @@ -584,11 +584,11 @@ Gw - + - + @@ -602,7 +602,7 @@ Gw - + @@ -622,24 +622,28 @@ Gw - + - - + + - + + + + + - + diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift b/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift index f358c8666..3445c27da 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift +++ b/OSBindings/Mac/Clock Signal/MachinePicker/MachinePicker.swift @@ -298,6 +298,10 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate { case "spectrum": var model: CSMachineSpectrumModel = .plus2a switch spectrumModelTypeButton.selectedItem!.tag { + case 16: model = .sixteenK + case 48: model = .fortyEightK + case 128: model = .oneTwoEightK + case 2: model = .plus2 case 21: model = .plus2a case 3: model = .plus3 default: break diff --git a/ROMImages/ZXSpectrum/128.rom b/ROMImages/ZXSpectrum/128.rom new file mode 100644 index 0000000000000000000000000000000000000000..1ddee8c967ca8ec1fa9f73a826f5d1b013758ae9 GIT binary patch literal 32768 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 literal 0 HcmV?d00001 diff --git a/ROMImages/ZXSpectrum/48.rom b/ROMImages/ZXSpectrum/48.rom new file mode 100644 index 0000000000000000000000000000000000000000..4d6895e0bbf3fa543f192a66a297192b3c969ac9 GIT binary patch literal 16384 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

5RYmg65FY9s z2YvlApymZI*>p_X!F&>jI3I7MZ@~317ujUFY!zVVCnBLz>sjXy(%{uy{ugu1iDNLsmQa$#sm30{R z7ox2c*v2qF>g~HeWz*pq>^ZCkek2{u+X!Dq(ki~1q#yF%rK9_8VRj$yOou%LZbbNi zRPpUIpvcQ9Ya*c)J|tr!O95)$OtU6kYQNNWiQ{kS&w+<<4(1;~f$|s5giI%|WwsrS z?n{ZgMBD?EXdgchFfOFyf#xW*%A&2ZON30A)x%Guj{zMd?)aP&sN5dZLl^24c~|Gv ziIA=6iZDuH#o-x6Sr!q_J4hG-%CUiyahwsW^%W?IYfVNu!pNHUsGN;cV*^I2$6&}K z!fd2poge9i)1(f`J1iOjQ4H0D3bDCcKUI4INIdr(M z79VT9XHm6lG-j#Q5Nm2hnFz>QkaJ*3Ph|q~^ggil8)ylV3eelrK_~>M(BTRGGd)Nw zJX;JHKcokiJfaI$z05+ek2*Z;w!2S|Ga?xPFDyrBB0!Vrr3VU}NCK}G#s3gBKz8RN zBPb(47@5S=4iY5_0BQu1pfP#dSpG@a32!ydk^LlBu0 z+d=mdOovR)NWqxO7^sCvxIa$+8Y?K6Xge)GGS8nu<88Kfev82>|I2>3$>si<3ajET z`wt&D+)A}^`#UtUPExFq6+&|bC}@%S@9lsAK!M#F;xdv8h56kJ0`xJJorc5#XYJm5 z<=c#kuJqjWuJm{O_utvr4)5Rb|KlCGys6zU=Rk(}_Y4&hyIdRg<4!sgRrm}Lmjp|8 zv5{9+L8A><*u&NJ?jjOD;C`GGy)&=X`9(AzPgS_!@}%Q&8`3|}lN#?+Pl80xK@v4( z9>$FYj9qxiECu^P931}>WBJjz3-s@<4A*#;R|4-k%?e~39L_Hd<0tVRkr*G<^x#>e z@H^lTG2xu`Ft{ckT)Z10k?U}h8E9^h*>SdJeBX`hkR(0JY{q0F25Ngm4pElTi+hcl zQK@khpQ1sI`hmBR&P85a>JZ^T!^z0?s&7<$NRpNA5|KTklOl&tGI~{_@!}5AsN||o zB0gyZWFjU-c*$|46^O@~Jm3(WIo_jzM|@)qW0Jd4iB#&D?*ml_#l z1Z4j2dL?F&^}7i~3{+>OpAuyOHypsrEJ>E;pg7eD>_v0ke%vhEJlMUKkRR>aR5X}~ zJ=K=}6Gr?uM*eG?$KfI*wn2}>xQlK3aYcLnPZ$-gDzJe#c-pnFYeC=cj{--|Wz)a$ zD8yv#=YKp?hE*%C!*M6Epn|<)@VjqAK`?MgOUpn@PU#j{r*_o4(ZcF5JQ)eBfApQT z?SA4$VZH1r*Pt)uFfGX^QwnS{c|4VJ_+3(?z2|T^tety7!`zD{qP#iBtN&e&PV&6-w@v9p|ipM@IH z!KxQn-jq3hP?M9JD9Pz8!m!S4(Lphs={S^$?6m3&p>A;0@tYDMo{k55E7SIV zX%j}wBKJ|@1|xER=8CWv<{Nfq!N*PFYmH;XO&Mm>|7I47+-24J;7n!tK*w<=uj@2{ zxqtyXgdiz%zd(x8-W)_+Qtt3Csu*9$5Xv3lX&W7xiM#F{u=97!K{y86BiIxCi+U9{ z$el~vAwt1i5|SYH{Wd~^sXx_W^tg*N@j2Jpg;#t66yKgPw}fWF!{Su=)Ky zO(kNKZxH$pMFvlryl4U;I{o(qzE49Vl@Af88uLOM;sg|C)1v<50Tj6yquFbacpV%= zBI|_;FO#>>|W6MZ160J!vCy(J=u3i*)8^q#@~*TZ?8hX0-pG5(m4B5AKpD=sR~i z;V3`~!k6HU1ndstto#1J2z04*e?zDS1_pd1=ryqUzSpgFZ{>h|yD>l@KGKLDSf&jc{egK4gV4ypn>#l%v9#!CusPiMCd;k{F zh_liNjmzPjOq7)VCICc`>jD8H!#Cmu6GFP%>00+TQU<nZ*rCuMXkB+Y}$WjDQkc=qo746_9jZ;pnPpH4_Lt0*D}bIZWWI^APxN9mG?R~{IIR;fd>3R*WMl6T3t zSS`p(thEQ;9KK6>`G1-qNMyh&W6(EI2sFPy%XBgM+VYXOKZM6f+N;Dx8YNY2J>C7RwAbYDLedy^=&U1v@9ha_ z7*M)Q5Q9VYG^Oyu;S!^59h5;1nOadj2V9FB-C1E;W3nTzf#O7Q1X>sFqx>7Q!4+X5 zr()o|io^Xa7)d>N#@k5ITxnoz<|K&A71HQ+WuR2{9V3Y09QqrWwZD!z+BeO_C~9^x zFm}-8D_#0M$66hbg`r@U9BowV`e#!ufQ}YfNJsE^B)1Jm2RKNmExIfeEjlNxx-6`N z+YU}RMJ7Y~aBL_GHWs4-i-ONw`7kR$j2BlqO_{=6S_3I7VJ=CJL0iT^M4>zAcZl=H z4?U) zB|(v>)0PQmHif)Jh_cH=W=Q>_t*`F2k1wv^j~T}yqMm@BLeRtl6sjCt(X|kg2R&p5)cknxBcCHYbpDlck4Md z=CfeHrycL^Yo$8&ybC8)Vdrz40V^yWKctz(Knvespqzg(vwN?*y|`+}fS0RJ+P~J; z9ib^_Jca!Kej=HLtoP*!DI7};!P!1I>S@8EvvM(&h_RY4aYn|Pd1?HB^Avcl9-jve zf}Rv##lfS41E3XvTBhyjB4uz|v`(fN^bB~2`thrbeaOx`GVAj`U|-MvfbHOInb6K5 z_9Cy%mwk{eNZOyTe;?N4?pCj}f9_DHFoTJM7DMt1{Riys*0x|Nv>{#(@PC=-aX;AQ z8S1QXQ|?#~?6Svt^zVU@jFabpe?H9W%4*%y>fg-UqewDu|eN@R{tC3euw5JMCS466@Sc zRi;9&7Kq^`m*gQkyjsGV!KMGR&EpGT1Z|TfJ=d?Bv-`CNJ4F zz}YQWS04p&qG0NCfhJKF*>X+xc|e!;e!GM7O+bpMm1?hcArld(Uo!}~pEWRJUWW%g zg}hu>pP)Aa4T+l-UoN0jnRW^vwppXCkyovx^Y{$*wwxYnq zUYG{pe#qPE+q5p(69_&Q*!hd5^bb7&Z@L*p`NQ*aXv(w8-|3-RO?3K>VCkZ@9WbG8N{xF4Cm5bk7;J2_qZb-)wwIJ|8y!O_WrMM;a&ZhLx%FKmP60>Eb#|O+J6$4lFTtMC#2ky z*y+ik2_NNw*rhGWotWMV`k-HdyO{pIH+bQx~BC!-^_$B@fPLP0-%8k9(f13AqXQp@Hpl7G%M` z83eg93uYyW$Ax_)+Ox5CzHK^YBE{Cu!zR)KnrYHxC5ZXvIfL_h_aSjJnP6ph(74L| zK&IH+$w*cvVtZ+Mg*y*q?m>k4_MrnegQbH3Q-zyBUs`IxOxlNjIjyX)PP%7lzk9ni zFu?I|WC!eBJ!mGofp4v{eV~vO9~aZwx+i3N0@B&$@7zi>HvzOOYT7!$VMnmSBN%SR zLY0~;hu|^lHA{ZZ!TB2VX>)hh<4o#;j0Fq-;~Q_z{EyPY1%FuZ)q=m}lvx^cHe0@0 z&|>*C_r2UJx$~J4=4x(S?kP*RiJJ{3tg*`_Oz_=5Mnrb5e6Mb2K^p=L_1hH?p2#-|qbyr=2i z38$LUVV8(7D?Iqa$z`jR9(j}QQufS95?9|a|Jop^nGGx)&ITNWa}c6ol#m23X4+ds zSUGTnf=9UZ&%zx<`kD)J$a1+Y#_nH4iYR+;O{`VAkBURbw;b0%getnFeX()GgRZ`b zL0I~H-~5440ddPFm%oHIH@QK`BclWUJ@dVQ)Z^2;vOWC0`2dIH&li|_4YD^s;N@h} zlxjFoN5yIH1-J@+&HOCLMjtqt1vv%>g!sMl`StTPU|AO2@QN8uHVN5vSgkS)>{&F` z=I=ZP>!;Pkf%fHd!RAEB z1PNG@N`YvAQ4Z~8l7$3wLVm$3ue>6>V}_#`JJ^?OW?Ta9Lch}Is<2bX(6`Ou?SVaE z=*Bul5+UuT4x8jYE7nT2Qc11paBcO1F1(%-xSUIjMIO#|5noL`45oI+rp69%ZSb!t z^(0=PF)Jo~e16VAAOvN(W`ft4iTQ8~nFd|RqucrEwKg*PsDqY-77>XdBJL=yN_m4( zlELUW>Y9ldafG@e&cwu*_=3>H#cy*$nv3`XG`=YWzCjmk1>A!m09NRfPdI%CZ%+4c z(gz2NQTjwp#!Ls^02<)>qfoB0t@qmg2rc4!UKF;09~m%N&|W?4a6ycyBIVwxXbs%J zsc>4Dj=YEiD2z9(f`sSe?9{O`{#qtFL^d$Btr|&8!v10;k-I+)L7@b$KLK2KvpD8n zEYN|#ou4m&!2s!a3Yx%>38n;uYa_eBOG+pY=UKBOX@7mPhl`6#Nlon{$30LPf|bKT zmMLpgI%^c~k%&i`h%Y0_W4Nrm9`PtFc=17ytnq@J5cA%`*^;bTPBF?6{WQ+sJOODD=i^svzn9cOYlSDL*lHQ zY*KPa{Bzw0@yZof{`{ob#qNFYk6WA89duuLSUKj_W)mAdh|iVudU)v5oK0DM+DPD&Y$|7|wpEE)Cq!BohnS^flPL=cJt z_%x26Yd?xpFaP=%PyFC-t9}>$BW8L2z!%?q@^tYj|EEgJiR+f_sjL6*@>frvxVi4& z$1xFlp&{hq=z|(*Mi!#q@Fy{%FUyRj9b(3DxXT}vFwRpJ5j-jn;JKLWATAXhWVq>n z2mJ5tza0242mZ@}{~vH*>Z~kWweW$nb67^kR8@vO1AZ8$u+r8js{&j9Lxpx`Il$3c zV`(1L11$PT6!4K?7>N*&P!X@ND&pa;fIIqc%*bN|*g93hFoJ+5kwk&u^UEKrU7rti zG~fY<{9;jlp&i|0M8#0AAn3(*z+*4m87b!%?kq<2cGMow<`?Hj%5b%cpdKiU{318h z0e~U;Fw6>H_&^EKo?k3cV1PIOu@!3yp&hih7Z+pL&SJp76O{`KQ9o^^#l@vkoMoUb!N7WJr#@AL_T8db1)@m7mhZYhbkfhCK8t4OHtu_YQZ(0T)xFc|Y2YjWa z0FTOT5e6W9vZ;Pa^uCah0N=IDbbHhc!IzfOH1Go}1#~pJ(^?wggV%^2Y$uF=CmO#w cGJeDcB!^qIF_H1dqc({BZyr4OW^w-i174kF(*OVf literal 0 HcmV?d00001 diff --git a/ROMImages/ZXSpectrum/plus2.rom b/ROMImages/ZXSpectrum/plus2.rom new file mode 100644 index 0000000000000000000000000000000000000000..2bd54e18e168bd878d1341478b37dc5282bb92b7 GIT binary patch literal 32768 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 literal 0 HcmV?d00001 diff --git a/ROMImages/ZXSpectrum/readme.txt b/ROMImages/ZXSpectrum/readme.txt index 63a56a9eb..afcbe6bef 100644 --- a/ROMImages/ZXSpectrum/readme.txt +++ b/ROMImages/ZXSpectrum/readme.txt @@ -9,3 +9,6 @@ material but retain that copyright"." With that in mind, Amstrad have kindly given their permission for the redistribution of their copyrighted material but retain that copyright. Material expected here, copyright Amstrad: plus3.rom — the +2a/+3 ROM file, 64kb in size. +plus2.rom – the +2 ROM file, 32kb in size. +128.rom – the 128kb ROM file, 32kb in size. +48.rom – the 16/48kb ROM file, 16kb in size. \ No newline at end of file From f10ec8015342729dcdcdfd725cb9a23821769430 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 14 Apr 2021 22:23:27 -0400 Subject: [PATCH 02/12] Gets started on different video timings. --- Machines/Sinclair/ZXSpectrum/Video.hpp | 164 +++++++++++++++----- Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp | 11 +- 2 files changed, 131 insertions(+), 44 deletions(-) diff --git a/Machines/Sinclair/ZXSpectrum/Video.hpp b/Machines/Sinclair/ZXSpectrum/Video.hpp index 4270815dc..a37536f00 100644 --- a/Machines/Sinclair/ZXSpectrum/Video.hpp +++ b/Machines/Sinclair/ZXSpectrum/Video.hpp @@ -69,48 +69,111 @@ template class Video { }; static constexpr Timings get_timings() { - // Amstrad gate array timings, classic statement: - // - // Contention begins 14361 cycles "after interrupt" and follows the pattern [1, 0, 7, 6 5 4, 3, 2]. - // The first four bytes of video are fetched at 14365–14368 cycles, in the order [pixels, attribute, pixels, attribute]. - // - // For my purposes: - // - // Video fetching always begins at 0. Since there are 311*228 = 70908 cycles per frame, and the interrupt - // should "occur" (I assume: begin) 14365 before that, it should actually begin at 70908 - 14365 = 56543. - // - // Contention begins four cycles before the first video fetch, so it begins at 70904. I don't currently - // know whether the four cycles is true across all models, so it's given here as convention_leadin. - // - // ... except that empirically that all seems to be two cycles off. So maybe I misunderstand what the - // contention patterns are supposed to indicate relative to MREQ? It's frustrating that all documentation - // I can find is vaguely in terms of contention patterns, and what they mean isn't well-defined in terms - // of regular Z80 signalling. - constexpr Timings result = { - .cycles_per_line = 228 * 2, - .lines_per_frame = 311, + if constexpr (timing == VideoTiming::Plus3) { + // Amstrad gate array timings, classic statement: + // + // Contention begins 14361 cycles "after interrupt" and follows the pattern [1, 0, 7, 6 5 4, 3, 2]. + // The first four bytes of video are fetched at 14365–14368 cycles, in the order [pixels, attribute, pixels, attribute]. + // + // For my purposes: + // + // Video fetching always begins at 0. Since there are 311*228 = 70908 cycles per frame, and the interrupt + // should "occur" (I assume: begin) 14365 before that, it should actually begin at 70908 - 14365 = 56543. + // + // Contention begins four cycles before the first video fetch, so it begins at 70904. I don't currently + // know whether the four cycles is true across all models, so it's given here as convention_leadin. + // + // ... except that empirically that all seems to be two cycles off. So maybe I misunderstand what the + // contention patterns are supposed to indicate relative to MREQ? It's frustrating that all documentation + // I can find is vaguely in terms of contention patterns, and what they mean isn't well-defined in terms + // of regular Z80 signalling. + constexpr Timings result = { + .cycles_per_line = 228 * 2, + .lines_per_frame = 311, - // i.e. video fetching begins five cycles after the start of the - // contended memory pattern below; that should put a clear two - // cycles between a Z80 access and the first video fetch. - .contention_leadin = 5 * 2, - .contention_duration = 129 * 2, + // i.e. video fetching begins five cycles after the start of the + // contended memory pattern below; that should put a clear two + // cycles between a Z80 access and the first video fetch. + .contention_leadin = 5 * 2, + .contention_duration = 129 * 2, - // i.e. interrupt is first signalled 14368 cycles before the first video fetch. - .interrupt_time = (228*311 - 14368) * 2, + // i.e. interrupt is first signalled 14368 cycles before the first video fetch. + .interrupt_time = (228*311 - 14368) * 2, - .delays = { - 2, 1, - 0, 0, - 14, 13, - 12, 11, - 10, 9, - 8, 7, - 6, 5, - 4, 3, - } - }; - return result; + .delays = { + 2, 1, + 0, 0, + 14, 13, + 12, 11, + 10, 9, + 8, 7, + 6, 5, + 4, 3, + } + }; + + return result; + } + + // TODO: fix 48kb and 128kb timings, below. + + if constexpr (timing == VideoTiming::OneTwoEightK) { + constexpr Timings result = { + .cycles_per_line = 228 * 2, + .lines_per_frame = 311, + + // i.e. video fetching begins five cycles after the start of the + // contended memory pattern below; that should put a clear two + // cycles between a Z80 access and the first video fetch. + .contention_leadin = 5 * 2, + .contention_duration = 128 * 2, + + // i.e. interrupt is first signalled 14368 cycles before the first video fetch. + .interrupt_time = (228*311 - 14361) * 2, + + .delays = { + 12, 11, + 10, 9, + 8, 7, + 6, 5, + 4, 3, + 2, 1, + 0, 0, + 0, 0, + } + }; + + return result; + } + + if constexpr (timing == VideoTiming::FortyEightK) { + constexpr Timings result = { + .cycles_per_line = 224 * 2, + .lines_per_frame = 312, + + // i.e. video fetching begins five cycles after the start of the + // contended memory pattern below; that should put a clear two + // cycles between a Z80 access and the first video fetch. + .contention_leadin = 5 * 2, + .contention_duration = 128 * 2, + + // i.e. interrupt is first signalled 14368 cycles before the first video fetch. + .interrupt_time = (224*312 - 14361) * 2, + + .delays = { + 12, 11, + 10, 9, + 8, 7, + 6, 5, + 4, 3, + 2, 1, + 0, 0, + 0, 0, + } + }; + + return result; + } } // TODO: how long is the interrupt line held for? @@ -238,9 +301,14 @@ template class Video { if(offset >= burst_position && offset < burst_position+burst_length && end_offset > offset) { const int burst_duration = std::min(burst_position + burst_length, end_offset) - offset; - crt_.output_colour_burst(burst_duration, 116, is_alternate_line_); + + if constexpr (timing >= VideoTiming::OneTwoEightK) { + crt_.output_colour_burst(burst_duration, 116, is_alternate_line_); + // The colour burst phase above is an empirical guess. I need to research further. + } else { + crt_.output_default_colour_burst(burst_duration); + } offset += burst_duration; - // The colour burst phase above is an empirical guess. I need to research further. } if(offset >= burst_position+burst_length && end_offset > offset) { @@ -261,9 +329,21 @@ template class Video { crt_.output_level(duration); } + static constexpr int half_cycles_per_line() { + if constexpr (timing == VideoTiming::FortyEightK) { + // TODO: determine real figure here, if one exists. + // The source I'm looking at now suggests that the theoretical + // ideal of 224*2 ignores the real-life effects of separate + // crystals, so I've nudged this experimentally. + return 224*2 - 1; + } else { + return 227*2; + } + } + public: Video() : - crt_(227 * 2, 2, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2) + crt_(half_cycles_per_line(), 2, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2) { // Show only the centre 80% of the TV frame. crt_.set_display_type(Outputs::Display::DisplayType::RGB); diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp index 0947312b2..be0f7bb6a 100644 --- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp +++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp @@ -638,8 +638,15 @@ template class ConcreteMachine: } // MARK: - Video. - static constexpr VideoTiming video_timing = VideoTiming::Plus3; - JustInTimeActor> video_; + using VideoType = + std::conditional_t< + model <= Model::FortyEightK, Video, + std::conditional_t< + model <= Model::Plus2, Video, + Video + > + >; + JustInTimeActor video_; // MARK: - Keyboard. Sinclair::ZX::Keyboard::Keyboard keyboard_; From f5c77464937ea3340b0ef95b343b25144b0e9648 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 15 Apr 2021 17:31:42 -0400 Subject: [PATCH 03/12] Extends fast loading support to the just-introduced models. --- Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp index be0f7bb6a..3b0b9de2e 100644 --- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp +++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp @@ -239,7 +239,7 @@ template class ConcreteMachine: // Fast loading: ROM version. // // The below patches over part of the 'LD-BYTES' routine from the 48kb ROM. - if(use_fast_tape_hack_ && address == 0x056b && read_pointers_[0] == &rom_[0xc000]) { + if(use_fast_tape_hack_ && address == 0x056b && read_pointers_[0] == &rom_[classic_rom_offset()]) { // Stop pressing enter, if neccessry. if(duration_to_press_enter_ > Cycles(0)) { duration_to_press_enter_ = Cycles(0); @@ -733,6 +733,22 @@ template class ConcreteMachine: return true; } + static constexpr int classic_rom_offset() { + switch(model) { + case Model::SixteenK: + case Model::FortyEightK: + return 0x0000; + + case Model::OneTwoEightK: + case Model::Plus2: + return 0x4000; + + case Model::Plus2a: + case Model::Plus3: + return 0xc000; + } + } + // MARK: - Disc. JustInTimeActor fdc_; From b4214c6e0884870db47324e213a2c4111c2b09ab Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 15 Apr 2021 18:04:16 -0400 Subject: [PATCH 04/12] Blocks off the AY from inputs in 48kb mode. --- Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp | 56 +++++++++++++++------ 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp index 3b0b9de2e..7684a78cf 100644 --- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp +++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp @@ -211,21 +211,43 @@ template class ConcreteMachine: // Apply contention if necessary. if constexpr (model >= Model::Plus2a) { + // Model applied: the trigger for the ULA inserting a delay is the falling edge + // of MREQ, which is always half a cycle into a read or write. if( is_contended_[address >> 14] && cycle.operation >= PartialMachineCycle::ReadOpcodeStart && cycle.operation <= PartialMachineCycle::WriteStart) { - // Assumption here: the trigger for the ULA inserting a delay is the falling edge - // of MREQ, which is always half a cycle into a read or write. - // - // TODO: somehow provide that information in the PartialMachineCycle? const HalfCycles delay = video_.last_valid()->access_delay(video_.time_since_flush() + HalfCycles(1)); advance(cycle.length + delay); return delay; } } else { - // TODO. + switch(cycle.operation) { + default: + advance(cycle.length); + return HalfCycles(0); + + case PartialMachineCycle::ReadOpcodeStart: + case PartialMachineCycle::ReadStart: + case PartialMachineCycle::WriteStart: + break; + + case CPU::Z80::PartialMachineCycle::InputStart: + case CPU::Z80::PartialMachineCycle::OutputStart: + break; + + case PartialMachineCycle::Internal: + break; + + case CPU::Z80::PartialMachineCycle::Input: + case CPU::Z80::PartialMachineCycle::Output: + case CPU::Z80::PartialMachineCycle::Read: + case CPU::Z80::PartialMachineCycle::Write: + case CPU::Z80::PartialMachineCycle::ReadOpcode: + // For these, carry on into the actual handler, below. + break; + } } // For all other machine cycles, model the action as happening at the end of the machine cycle; @@ -315,6 +337,7 @@ template class ConcreteMachine: } } + // Route to the AY if one is fitted. if constexpr (model >= Model::OneTwoEightK) { if((address & 0xc002) == 0xc000) { // Select AY register. @@ -329,6 +352,7 @@ template class ConcreteMachine: } } + // Check for FDC accesses. if constexpr (model == Model::Plus3) { switch(address) { default: break; @@ -370,17 +394,21 @@ template class ConcreteMachine: } } - if((address & 0xc002) == 0xc000) { - // Read from AY register. - update_audio(); - *cycle.value &= GI::AY38910::Utility::read(ay_); + if constexpr (model >= Model::OneTwoEightK) { + if((address & 0xc002) == 0xc000) { + // Read from AY register. + update_audio(); + *cycle.value &= GI::AY38910::Utility::read(ay_); + } } - // Check for a floating bus read; these are particularly arcane - // on the +2a/+3. See footnote to https://spectrumforeveryone.com/technical/memory-contention-floating-bus/ - // and, much more rigorously, http://sky.relative-path.com/zx/floating_bus.html - if(!disable_paging_ && (address & 0xf003) == 0x0001) { - *cycle.value &= video_->get_floating_value(); + if constexpr (model >= Model::Plus2a) { + // Check for a +2a/+3 floating bus read; these are particularly arcane. + // See footnote to https://spectrumforeveryone.com/technical/memory-contention-floating-bus/ + // and, much more rigorously, http://sky.relative-path.com/zx/floating_bus.html + if(!disable_paging_ && (address & 0xf003) == 0x0001) { + *cycle.value &= video_->get_floating_value(); + } } if constexpr (model == Model::Plus3) { From d1bb3aada431ea0966be2dca0c7f2ddb35cbdcc7 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 15 Apr 2021 18:57:34 -0400 Subject: [PATCH 05/12] Attempts to complete the in-machine application of contention. --- Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp | 65 ++++++++++++++++++--- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp index 7684a78cf..d8f5109e7 100644 --- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp +++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp @@ -228,17 +228,62 @@ template class ConcreteMachine: advance(cycle.length); return HalfCycles(0); + case CPU::Z80::PartialMachineCycle::InputStart: + case CPU::Z80::PartialMachineCycle::OutputStart: { + // The port address is loaded prior to IOREQ being visible; a contention + // always occurs if it is in the $4000–$8000 range regardless of current + // memory mapping. + HalfCycles delay; + HalfCycles time = video_.time_since_flush() + HalfCycles(1); + + if((address & 0xc000) == 0x4000) { + for(int c = 0; c < ((address & 1) ? 4 : 2); c++) { + const auto next_delay = video_.last_valid()->access_delay(time); + delay += next_delay; + time += next_delay + 2; + } + } else { + if(!(address & 1)) { + delay = video_.last_valid()->access_delay(time + HalfCycles(2)); + } + } + + advance(cycle.length + delay); + return delay; + } + case PartialMachineCycle::ReadOpcodeStart: case PartialMachineCycle::ReadStart: - case PartialMachineCycle::WriteStart: - break; + case PartialMachineCycle::WriteStart: { + // These all start by loading the address bus, then set MREQ + // half a cycle later. + if(is_contended_[address >> 14]) { + const HalfCycles delay = video_.last_valid()->access_delay(video_.time_since_flush() + HalfCycles(1)); - case CPU::Z80::PartialMachineCycle::InputStart: - case CPU::Z80::PartialMachineCycle::OutputStart: - break; + advance(cycle.length + delay); + return delay; + } + } - case PartialMachineCycle::Internal: - break; + case PartialMachineCycle::Internal: { + // Whatever's on the address bus will remain there, without IOREQ or + // MREQ interceding, for this entire bus cycle. So apply contentions + // all the way along. + if(is_contended_[address >> 14]) { + const auto half_cycles = cycle.length.as(); + assert(!(half_cycles & 1)); + + HalfCycles time = video_.time_since_flush() + HalfCycles(1); + HalfCycles delay; + for(int c = 0; c < half_cycles; c += 2) { + const auto next_delay = video_.last_valid()->access_delay(time); + delay += next_delay; + time += next_delay + 2; + } + + return delay; + } + } case CPU::Z80::PartialMachineCycle::Input: case CPU::Z80::PartialMachineCycle::Output: @@ -630,7 +675,11 @@ template class ConcreteMachine: } void set_memory(int bank, uint8_t source) { - is_contended_[bank] = (source >= 4 && source < 8); + if constexpr (model >= Model::Plus2a) { + is_contended_[bank] = (source >= 4 && source < 8); + } else { + is_contended_[bank] = source & 1; + } pages_[bank] = source; uint8_t *const read = (source < 0x80) ? &ram_[source * 16384] : &rom_[(source & 0x7f) * 16384]; From 71cf63bd35ca5b63c37cb4427dac3ea29463c461 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 15 Apr 2021 19:17:11 -0400 Subject: [PATCH 06/12] Corrects internal cycle contention. --- Machines/Sinclair/ZXSpectrum/Video.hpp | 10 +--------- Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp | 1 + 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/Machines/Sinclair/ZXSpectrum/Video.hpp b/Machines/Sinclair/ZXSpectrum/Video.hpp index a37536f00..7ac67758c 100644 --- a/Machines/Sinclair/ZXSpectrum/Video.hpp +++ b/Machines/Sinclair/ZXSpectrum/Video.hpp @@ -122,13 +122,9 @@ template class Video { .cycles_per_line = 228 * 2, .lines_per_frame = 311, - // i.e. video fetching begins five cycles after the start of the - // contended memory pattern below; that should put a clear two - // cycles between a Z80 access and the first video fetch. .contention_leadin = 5 * 2, .contention_duration = 128 * 2, - // i.e. interrupt is first signalled 14368 cycles before the first video fetch. .interrupt_time = (228*311 - 14361) * 2, .delays = { @@ -151,14 +147,10 @@ template class Video { .cycles_per_line = 224 * 2, .lines_per_frame = 312, - // i.e. video fetching begins five cycles after the start of the - // contended memory pattern below; that should put a clear two - // cycles between a Z80 access and the first video fetch. .contention_leadin = 5 * 2, .contention_duration = 128 * 2, - // i.e. interrupt is first signalled 14368 cycles before the first video fetch. - .interrupt_time = (224*312 - 14361) * 2, + .interrupt_time = (224*312 - 14330) * 2, .delays = { 12, 11, diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp index d8f5109e7..dd82ab0ac 100644 --- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp +++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp @@ -281,6 +281,7 @@ template class ConcreteMachine: time += next_delay + 2; } + advance(cycle.length + delay); return delay; } } From b2cf121410c3116995f08d1b053791e974e700f2 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 15 Apr 2021 19:31:45 -0400 Subject: [PATCH 07/12] Regresses default to the more-compatible +2. --- Analyser/Static/ZXSpectrum/Target.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Analyser/Static/ZXSpectrum/Target.hpp b/Analyser/Static/ZXSpectrum/Target.hpp index 4996d21fa..76eada20e 100644 --- a/Analyser/Static/ZXSpectrum/Target.hpp +++ b/Analyser/Static/ZXSpectrum/Target.hpp @@ -27,7 +27,7 @@ struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl< Plus3, ); - Model model = Model::Plus2a; + Model model = Model::Plus2; bool should_hold_enter = false; Target(): Analyser::Static::Target(Machine::ZXSpectrum) { From 349b9ce5023b32da2bbd00938b826415fd83863b Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 15 Apr 2021 21:13:06 -0400 Subject: [PATCH 08/12] Don't post contended accesses other than on the +2a/+3. Those machines have an actual latch for this stuff, the others don't. --- Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp index dd82ab0ac..d4e289c2a 100644 --- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp +++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp @@ -323,8 +323,10 @@ template class ConcreteMachine: case PartialMachineCycle::Read: *cycle.value = read_pointers_[address >> 14][address]; - if(is_contended_[address >> 14]) { - video_->set_last_contended_area_access(*cycle.value); + if constexpr (model >= Model::Plus2a) { + if(is_contended_[address >> 14]) { + video_->set_last_contended_area_access(*cycle.value); + } } break; @@ -336,9 +338,11 @@ template class ConcreteMachine: write_pointers_[address >> 14][address] = *cycle.value; - // Fill the floating bus buffer if this write is within the contended area. - if(is_contended_[address >> 14]) { - video_->set_last_contended_area_access(*cycle.value); + if constexpr (model >= Model::Plus2a) { + // Fill the floating bus buffer if this write is within the contended area. + if(is_contended_[address >> 14]) { + video_->set_last_contended_area_access(*cycle.value); + } } break; From fa18b06dbfc9416b014c7eaf6951b70253c8774a Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 15 Apr 2021 21:13:36 -0400 Subject: [PATCH 09/12] Correct get_floating_value to be consistent in out-of-bounds behaviour. --- Machines/Sinclair/ZXSpectrum/Video.hpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Machines/Sinclair/ZXSpectrum/Video.hpp b/Machines/Sinclair/ZXSpectrum/Video.hpp index 7ac67758c..eafbb90d7 100644 --- a/Machines/Sinclair/ZXSpectrum/Video.hpp +++ b/Machines/Sinclair/ZXSpectrum/Video.hpp @@ -401,20 +401,24 @@ template class Video { */ uint8_t get_floating_value() const { constexpr auto timings = get_timings(); + const uint8_t out_of_bounds = (timing == VideoTiming::Plus3) ? last_contended_access_ : 0xff; + const int line = time_into_frame_ / timings.cycles_per_line; - if(line >= 192) return 0xff; + if(line >= 192) { + return out_of_bounds; + } const int time_into_line = time_into_frame_ % timings.cycles_per_line; if(time_into_line >= 256 || (time_into_line&8)) { - return last_contended_access_; + return out_of_bounds; } // The +2a and +3 always return the low bit as set. + const uint8_t value = last_fetches_[(time_into_line >> 1) & 3]; if constexpr (timing == VideoTiming::Plus3) { - return last_fetches_[(time_into_line >> 1) & 3] | 1; + return value | 1; } - - return last_fetches_[(time_into_line >> 1) & 3]; + return value; } /*! From ef636da86620ebc489c126af2bcde7cd4dbb19d1 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 15 Apr 2021 21:19:21 -0400 Subject: [PATCH 10/12] Attempts 48/128kb floating bus behaviour. --- Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp | 24 +++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp index d4e289c2a..165099989 100644 --- a/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp +++ b/Machines/Sinclair/ZXSpectrum/ZXSpectrum.cpp @@ -321,6 +321,15 @@ template class ConcreteMachine: } case PartialMachineCycle::Read: + if constexpr (model == Model::SixteenK) { + // Assumption: with nothing mapped above 0x8000 on the 16kb Spectrum, + // read the floating bus. + if(address >= 0x8000) { + *cycle.value = video_->get_floating_value(); + break; + } + } + *cycle.value = read_pointers_[address >> 14][address]; if constexpr (model >= Model::Plus2a) { @@ -413,10 +422,13 @@ template class ConcreteMachine: } break; - case PartialMachineCycle::Input: + case PartialMachineCycle::Input: { + bool did_match = false; *cycle.value = 0xff; if(!(address&1)) { + did_match = true; + // Port FE: // // address b8+: mask of keyboard lines to select @@ -446,6 +458,8 @@ template class ConcreteMachine: if constexpr (model >= Model::OneTwoEightK) { if((address & 0xc002) == 0xc000) { + did_match = true; + // Read from AY register. update_audio(); *cycle.value &= GI::AY38910::Utility::read(ay_); @@ -469,7 +483,13 @@ template class ConcreteMachine: break; } } - break; + + if constexpr (model < Model::Plus2) { + if(!did_match) { + *cycle.value = video_->get_floating_value(); + } + } + } break; } return HalfCycles(0); From d7954a4cb16fa882d13c6b9133dfee319b90f142 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 15 Apr 2021 21:51:49 -0400 Subject: [PATCH 11/12] Tweaks timing a little. --- Machines/Sinclair/ZXSpectrum/Video.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Machines/Sinclair/ZXSpectrum/Video.hpp b/Machines/Sinclair/ZXSpectrum/Video.hpp index eafbb90d7..0da1e59c2 100644 --- a/Machines/Sinclair/ZXSpectrum/Video.hpp +++ b/Machines/Sinclair/ZXSpectrum/Video.hpp @@ -122,10 +122,10 @@ template class Video { .cycles_per_line = 228 * 2, .lines_per_frame = 311, - .contention_leadin = 5 * 2, + .contention_leadin = 2 * 2, .contention_duration = 128 * 2, - .interrupt_time = (228*311 - 14361) * 2, + .interrupt_time = (228*311 - 14366) * 2, .delays = { 12, 11, @@ -147,10 +147,10 @@ template class Video { .cycles_per_line = 224 * 2, .lines_per_frame = 312, - .contention_leadin = 5 * 2, + .contention_leadin = 2 * 2, .contention_duration = 128 * 2, - .interrupt_time = (224*312 - 14330) * 2, + .interrupt_time = (224*312 - 14339) * 2, .delays = { 12, 11, From eb99a64b29da705246ec4b4bef15e3c4e65c3a69 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Thu, 15 Apr 2021 22:20:34 -0400 Subject: [PATCH 12/12] Adds new Spectrum models to Qt UI. --- OSBindings/Qt/mainwindow.cpp | 10 +++++++--- OSBindings/Qt/mainwindow.ui | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/OSBindings/Qt/mainwindow.cpp b/OSBindings/Qt/mainwindow.cpp index 630bff4a6..6836844f1 100644 --- a/OSBindings/Qt/mainwindow.cpp +++ b/OSBindings/Qt/mainwindow.cpp @@ -1258,9 +1258,13 @@ void MainWindow::start_spectrum() { using Target = Analyser::Static::ZXSpectrum::Target; auto target = std::make_unique(); - switch(ui->oricModelComboBox->currentIndex()) { - default: target->model = Target::Model::Plus2a; break; - case 1: target->model = Target::Model::Plus3; break; + switch(ui->spectrumModelComboBox->currentIndex()) { + default: target->model = Target::Model::SixteenK; break; + case 1: target->model = Target::Model::FortyEightK; break; + case 2: target->model = Target::Model::OneTwoEightK; break; + case 3: target->model = Target::Model::Plus2; break; + case 4: target->model = Target::Model::Plus2a; break; + case 5: target->model = Target::Model::Plus3; break; } launchTarget(std::move(target)); diff --git a/OSBindings/Qt/mainwindow.ui b/OSBindings/Qt/mainwindow.ui index 95a835436..4e576e4c3 100644 --- a/OSBindings/Qt/mainwindow.ui +++ b/OSBindings/Qt/mainwindow.ui @@ -551,6 +551,26 @@ + + + 16kb + + + + + 48kb + + + + + 128kb + + + + + +2 + + +2a