1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-10-25 09:27:01 +00:00

Compare commits

...

506 Commits

Author SHA1 Message Date
Thomas Harte
5810a1a98e Merge pull request #971 from TomHarte/ChaseHQ
Flip meaning of INT1 input read.
2021-07-09 22:48:01 -04:00
Thomas Harte
a4c011e3c0 Flip meaning of INT1 input read. 2021-07-09 22:39:51 -04:00
Thomas Harte
337fd15dc0 Merge pull request #970 from TomHarte/SwiftUniformity
Swift: be consisted on `.selectedTag()`.
2021-07-08 22:43:57 -04:00
Thomas Harte
9bc94f4536 Be consisted on .selectedTag(). 2021-07-08 22:38:54 -04:00
Thomas Harte
3f4cf35384 Merge pull request #969 from TomHarte/SixMHz
Adds the option of running an Enterprise at 6MHz.
2021-07-08 22:36:22 -04:00
Thomas Harte
4dd7f2cc09 Add 6Mhz option to Qt UI. 2021-07-08 22:30:35 -04:00
Thomas Harte
1b29cc34c4 Correct input list. 2021-07-08 22:22:48 -04:00
Thomas Harte
53c3c1f5ab Allows macOS users to select the 6MHz Enterprise. 2021-07-08 18:50:37 -04:00
Thomas Harte
6225abd751 Adds 6MHz Enterprise option. 2021-07-07 20:57:04 -04:00
Thomas Harte
c6fcd9a1eb Merge pull request #968 from TomHarte/DaveAudio
Dave: apply ring modulation during sync, too.
2021-07-06 23:41:37 -04:00
Thomas Harte
30fbb6ea53 Ensure run command is issued. 2021-07-06 23:16:16 -04:00
Thomas Harte
0e49258546 Remove caveman debugging. 2021-07-06 23:15:53 -04:00
Thomas Harte
264b8dfb28 Dave: apply ring modulation even in sync mode. 2021-07-06 23:11:30 -04:00
Thomas Harte
6a15b8f695 Merge pull request #967 from TomHarte/EnterpriseTiming
Correct Enterprise timing error.
2021-07-06 22:48:58 -04:00
Thomas Harte
5167d256cc Remove detritus. 2021-07-06 22:43:17 -04:00
Thomas Harte
16bd826491 Reduce nesting. 2021-07-06 22:32:59 -04:00
Thomas Harte
55af8fa5d9 Avoid erroneous Nick delays. 2021-07-06 22:28:44 -04:00
Thomas Harte
1ec8ff20af Ensure data bus is 0xff during interrupts. 2021-07-06 21:58:17 -04:00
Thomas Harte
99a65d3297 Merge pull request #966 from TomHarte/DaveUnifiedTimer
Switches to a unified counter for 1/50/1000Hz Dave interrupts.
2021-07-06 21:50:32 -04:00
Thomas Harte
94907b51aa Remove redundant parameter. 2021-07-06 20:47:49 -04:00
Thomas Harte
0085265d13 Test for a longer period; fix expected tone 1 count. 2021-07-06 20:46:22 -04:00
Thomas Harte
8e0893bd42 Clarifies control flow. 2021-07-06 20:28:32 -04:00
Thomas Harte
704dc9bdcb Improves test, to assert that state toggles happen at interrupts. 2021-07-06 20:25:32 -04:00
Thomas Harte
7a673a2448 Avoid confusing temporary storage. 2021-07-06 20:23:09 -04:00
Thomas Harte
33e2a4b21c Minor cleanups. 2021-07-06 20:20:13 -04:00
Thomas Harte
3e6b804896 Switches to linked 1/50/1000 Hz timers, and per-interrupt state toggling. 2021-07-06 20:12:44 -04:00
Thomas Harte
e98165a657 Merge pull request #965 from TomHarte/DaveDivider
Ensure two-cycle pauses in 12MHz mode.
2021-07-04 21:13:05 -04:00
Thomas Harte
2a7727d12b Merge branch 'master' into DaveDivider 2021-07-04 21:02:09 -04:00
Thomas Harte
c20e8f4062 Honours 8/12Mhz selection in non-video delays. 2021-07-03 23:05:09 -04:00
Thomas Harte
4ca9db7d49 Merge pull request #963 from TomHarte/DaveDivider
Obey Dave's 8/12MHz programmable divider.
2021-07-03 23:00:11 -04:00
Thomas Harte
4add48cffb Obey Dave's 8/12MHz programmable divider. 2021-07-03 22:43:20 -04:00
Thomas Harte
adbfb009f8 Merge pull request #960 from TomHarte/QtLEDs
Ensure LEDs are cleared between machines in Qt.
2021-07-03 19:17:51 -04:00
Thomas Harte
43ceca8711 Use type alias. 2021-07-03 19:10:39 -04:00
Thomas Harte
3ef28a4f03 Remove unused instance variable. 2021-07-03 19:10:29 -04:00
Thomas Harte
adcd580d5b Ensure LEDs are cleared upon a new machine. 2021-07-03 19:06:15 -04:00
Thomas Harte
5715c9183f The target is now definitely used. 2021-07-03 15:20:37 -04:00
Thomas Harte
ceb62ac7f9 Reenable the hardened runtime for macOS. 2021-07-03 13:41:32 -04:00
Thomas Harte
bda0756620 Merge pull request #959 from TomHarte/WriteCrash
Corrects buffer placement of decoded sectors.
2021-07-03 13:41:00 -04:00
Thomas Harte
6b47fb38c6 Corrects buffer placement of decoded sectors. 2021-07-03 13:36:01 -04:00
Thomas Harte
38bf8a06a7 Merge pull request #958 from TomHarte/EnterpriseFloatingBus
Makes a guess re: the Enterprise floating bus
2021-07-03 13:26:19 -04:00
Thomas Harte
196651d9aa Consolidates TODO. 2021-07-03 13:08:53 -04:00
Thomas Harte
6b46212a4e Deal with dangling TODO. 2021-07-03 13:07:41 -04:00
Thomas Harte
2a6fff2008 Takes a stab at what might happen if you read from Nick. 2021-07-03 13:06:07 -04:00
Thomas Harte
c5944efe50 Adds various method definitions. 2021-07-03 12:56:56 -04:00
Thomas Harte
f384370b18 Switch what's left of Enterprise logging to actual LOGs. 2021-07-03 12:50:46 -04:00
Thomas Harte
0c09275a9f Merge pull request #957 from TomHarte/EnterpriseTimingWindow
Correct various Enterprise timing discrepancies.
2021-07-03 12:47:29 -04:00
Thomas Harte
278671cdb9 Correct Nick interrupt prediction. 2021-07-03 00:05:13 -04:00
Thomas Harte
964d2d4fa4 Be consistent in expression of logic. 2021-07-03 00:00:00 -04:00
Thomas Harte
f371221dba Add a quick test of tone generator 1. 2021-07-02 23:57:11 -04:00
Thomas Harte
27b0579ec6 Avoid stack-error test case.
Also test that the interrupt is generated on the downward stroke.
2021-07-02 23:55:43 -04:00
Thomas Harte
283092cfbc With a unit test in aid, corrects some lingering TimedInterruptSource issues. 2021-07-02 23:41:19 -04:00
Thomas Harte
614953a222 Allows the low-pass filter to react to high-pass effects. 2021-07-02 22:36:35 -04:00
Thomas Harte
4fffb3cf19 Allow that final Z80 cycle to start anywhere in the first three of Nick's window of six. 2021-07-02 22:29:35 -04:00
Thomas Harte
850aa2b23a Merge pull request #956 from TomHarte/EnterpriseComposite
Adds Enterprise composite video option.
2021-07-02 22:22:09 -04:00
Thomas Harte
d715e5fd1d Expose composite/RGB option in Qt. 2021-07-02 21:51:48 -04:00
Thomas Harte
7826a26c7b Adds Enterprise composite video option.
While enabling more pixels on the left for RGB mode.
2021-07-02 21:42:09 -04:00
Thomas Harte
dc0a82cf9a Merge pull request #955 from TomHarte/FAT12
Adds a FAT12 parser.
2021-07-02 21:33:54 -04:00
Thomas Harte
2e60c81bd6 Enter :dir as a complete command. 2021-07-02 21:15:48 -04:00
Thomas Harte
763b9ba0ec Ensure the splash screen is skipped for self-booting disks. 2021-07-02 21:11:54 -04:00
Thomas Harte
bae8bb0c00 Gives the FAT parser responsibility for right trims. 2021-07-02 19:50:27 -04:00
Thomas Harte
bcf483fb7e Adds some basic loading command assistance. 2021-07-02 19:42:43 -04:00
Thomas Harte
a5b7d819a7 Correct FAT parser. 2021-07-02 19:28:13 -04:00
Thomas Harte
fe07a0b1d8 Starts to add a FAT[12] parser. 2021-07-02 18:56:43 -04:00
Thomas Harte
d9231e5d4a Merge pull request #954 from TomHarte/stddefRedux
The FIRFilter interface depends upon size_t.
2021-07-02 17:26:37 -04:00
Thomas Harte
b7aa1a1c84 The FIRFilter interface depends upon size_t. 2021-07-02 17:21:53 -04:00
Thomas Harte
32e144115d Add missing article, plus other minor corrections. 2021-07-02 11:03:14 -04:00
Thomas Harte
177cc96f49 Merge pull request #953 from TomHarte/stddef
Add missing stddef header where size_t is used.
2021-07-01 23:29:56 -04:00
Thomas Harte
51d98ef9ab Add missing stddef header where size_t is used. 2021-07-01 23:15:32 -04:00
Thomas Harte
2327c48cc4 Merge pull request #952 from TomHarte/EnterpriseTyper
Add typer support for the Enterprise.
2021-07-01 22:59:04 -04:00
Thomas Harte
742d44a532 Switch to an activity-based typing trigger; add a target loading command. 2021-07-01 22:53:23 -04:00
Thomas Harte
52b96db2b9 Correct syntax, mapping and inter-key timing. 2021-07-01 21:18:15 -04:00
Thomas Harte
0b9de78c38 Add typer support for the Enterprise. 2021-07-01 21:05:03 -04:00
Thomas Harte
2c28cb8c57 Merge pull request #951 from TomHarte/EnterpriseMention
Add Enterprise screenshots
2021-06-30 22:24:43 -04:00
Thomas Harte
483fe82e9d Add a second image, to even things out. 2021-06-30 22:23:26 -04:00
Thomas Harte
29492d6138 Add an Enterprise screenshot. 2021-06-30 22:18:29 -04:00
Thomas Harte
19310e32c4 Adds the Enterprise 64/128 as a bullet-pointed item.
No relevant screenshots yet.
2021-06-30 08:06:20 -04:00
Thomas Harte
c04a395499 Merge pull request #950 from TomHarte/Enterprise
Adds emulation of the Enterprise
2021-06-29 21:37:38 -04:00
Thomas Harte
1c424833a9 Correct EXDOS ROM name. 2021-06-29 21:04:53 -04:00
Thomas Harte
a46ff5590d Adds Enterprise new machine dialogue for Qt. 2021-06-29 21:04:17 -04:00
Thomas Harte
ab059b63fd Add Enterprise to Qt project file. 2021-06-29 20:36:28 -04:00
Thomas Harte
3d8fc9952d Remove dead TODO, correct for overflow position. 2021-06-29 15:44:02 -04:00
Thomas Harte
8ce8fbd977 Provide correct input when one of the tone generators is the interrupt source. 2021-06-29 15:41:08 -04:00
Thomas Harte
7f08218b28 The Nick interrupt input also seems to be a live poll, not a retrieval of the mask.
This corrects the two pieces of software I knew not to be working.
2021-06-28 22:10:11 -04:00
Thomas Harte
2c139ad931 Adds some notes to self.
I think I'm starting to find enough information to handle tapes.
2021-06-28 22:03:06 -04:00
Thomas Harte
1119779c8b Ensure EXDOS card is completely disabled if no FDC is present. 2021-06-28 21:47:53 -04:00
Thomas Harte
5351ac560f Ensure the motor goes off for unselected drives. 2021-06-28 21:40:12 -04:00
Thomas Harte
49f0ab0f15 Add note to self.
Although I still think there may be some issue lurking.
2021-06-28 21:31:55 -04:00
Thomas Harte
a5c57e777e VRES appears to work negatively in attribute mode too. 2021-06-28 21:24:13 -04:00
Thomas Harte
3c59042388 Fixes initial state for 1kHz. 2021-06-28 21:08:41 -04:00
Thomas Harte
919e211bc4 Reduces number of interrupt-related sequence points. 2021-06-28 19:30:12 -04:00
Thomas Harte
daa0737ce4 Ensure addresses tick upwards even during sync/burst; correct 2/4/8bpp character sizing. 2021-06-28 19:00:51 -04:00
Thomas Harte
36805cb120 Correct tone channel interrupts, remove dead warning. 2021-06-27 23:21:00 -04:00
Thomas Harte
7de69e9874 Makes an attempt to round out the timed interrupts. 2021-06-27 23:09:11 -04:00
Thomas Harte
b93575bbcc Spots that b0 and b2 of 0xb4 are 'dividers', not enables. 2021-06-27 22:33:20 -04:00
Thomas Harte
116e0f0105 Interupts 1kHz and 50Hz interrupts, while edging towards tone generator interrupts. 2021-06-27 22:08:38 -04:00
Thomas Harte
e4a650aaff Implements the 1Hz interrupt. 2021-06-27 21:47:21 -04:00
Thomas Harte
b5312b9ba0 get_interrupt_line can be const. 2021-06-27 21:37:11 -04:00
Thomas Harte
6afee7bb9b Captures appropriate fields.
No action yet.
2021-06-27 21:36:55 -04:00
Thomas Harte
5729e6e13a Corrects potential JustInTimeActor overflow. 2021-06-27 21:36:41 -04:00
Thomas Harte
2f53b105bb The Enterprise is now an Activity::Source; also sketches out the owner of Dave's timed interrupt logic. 2021-06-27 21:02:04 -04:00
Thomas Harte
b698056f78 Correct divisor. 2021-06-27 17:39:13 -04:00
Thomas Harte
95c906f03d Takes a serious shot at back_map. 2021-06-27 17:36:25 -04:00
Thomas Harte
be19fa9dde This mapping needs to know where it will occur. 2021-06-27 17:30:09 -04:00
Thomas Harte
81e9ba5608 This is correct from the Enterprise's side of things, I think.
I just need to complete the missing part of JustInTimeActor. After I do some empirical testing of this.
2021-06-27 17:24:21 -04:00
Thomas Harte
f2d7b9f6a9 Apply a crop, allow time until Z80 slot to be a future-based query. 2021-06-27 17:13:07 -04:00
Thomas Harte
1ea034310a Edge up very close to video waits.
I just need to implement back conversions that include marginal phase over in the JustInTimeActor.
2021-06-27 16:28:01 -04:00
Thomas Harte
bdcab447f9 Add a further accessor. 2021-06-27 16:27:26 -04:00
Thomas Harte
10bf6744aa Correct typo. 2021-06-27 16:26:55 -04:00
Thomas Harte
895d98e266 Implements out-of-video-area pauses. 2021-06-27 16:11:22 -04:00
Thomas Harte
903e343895 Attempts to complete Dave's audio duties. 2021-06-27 14:06:49 -04:00
Thomas Harte
f8b7c59616 Corrects tone frequency. 2021-06-26 23:51:43 -04:00
Thomas Harte
fcd267a3f9 Starts implementing noise. 2021-06-26 23:48:53 -04:00
Thomas Harte
f8bb66d2a0 Attempts an essentially-complete implementation of tone channels. 2021-06-26 23:39:59 -04:00
Thomas Harte
90782d3c27 Corrects for IntType != int. 2021-06-26 23:39:37 -04:00
Thomas Harte
f2336d2efc I think reloads occur after overflow, not before. 2021-06-26 23:16:00 -04:00
Thomas Harte
c2d093fa3c Respect user volume input.
Basic tones are now present. Neato!
2021-06-24 22:27:02 -04:00
Thomas Harte
1a97cc8a91 Start making some effort towards audio generation. 2021-06-24 22:21:01 -04:00
Thomas Harte
c34a548fa0 Ensure character pixel reads can't go out of bounds. 2021-06-24 22:19:50 -04:00
Thomas Harte
d1b89392a2 Improve exposiiton. 2021-06-24 22:18:31 -04:00
Thomas Harte
ed734754e5 Adds a through route for IMG files. 2021-06-24 21:04:21 -04:00
Thomas Harte
520c3c9218 Corrects colour deserialisation.
Long story short: the documentation I'm reading inexplicably lists the bits in reverse order. Luckily, a lot of the other documentation doesn't.
2021-06-24 20:59:04 -04:00
Thomas Harte
9230cf1726 Corrects bug when left_ or right_margin_ = 0. 2021-06-24 20:28:50 -04:00
Thomas Harte
6e616972a5 Better binds margin tests to window movements; simplifies line parameter addressing. 2021-06-24 18:55:15 -04:00
Thomas Harte
f98888824d Switches to an overt active/inactive state machine. 2021-06-24 18:34:21 -04:00
Thomas Harte
6c8b23e708 Alters 4bpp mapping; adds character mode 4bpp and 8bpp. 2021-06-23 19:35:47 -04:00
Thomas Harte
2c2bb3765f Withdraws the EPDOS option.
At least for now; it's something to worry about later.
2021-06-23 19:32:34 -04:00
Thomas Harte
0d165740ea Honours memory size request. 2021-06-22 21:48:55 -04:00
Thomas Harte
88f0f2b623 Adds to the macOS UI and wires through all Enterprise options. 2021-06-22 21:39:07 -04:00
Thomas Harte
0afa143375 Add missing include. 2021-06-22 21:31:46 -04:00
Thomas Harte
8319aca351 Correct syntax errors. 2021-06-22 20:50:03 -04:00
Thomas Harte
a66734883a Starts sketching out Dave. 2021-06-22 19:33:41 -04:00
Thomas Harte
d2ab0dd839 Adds a quick way to get the compiler to pick an integral type. 2021-06-22 19:33:29 -04:00
Thomas Harte
2574407afb Relocates MinIntTypeValue to Numeric. 2021-06-22 19:33:02 -04:00
Thomas Harte
83a54fd6d2 Use the FAT12 boot sector to determine geometry. 2021-06-22 06:54:17 -04:00
Thomas Harte
e062780968 Extends back to 128kb and stops halting on unrecognised ports. 2021-06-22 06:15:42 -04:00
Thomas Harte
3acd0be1f7 Copy and paste 2bpp character support. 2021-06-21 23:27:13 -04:00
Thomas Harte
69c0734975 WD1770: switch motor on even if spin-up is disabled. 2021-06-21 23:26:55 -04:00
Thomas Harte
c1678d7be7 Corrects exposition and transmission of drive selection.
What a klutz I've been.
2021-06-21 22:56:25 -04:00
Thomas Harte
117f9a9794 Adds notes on intended meaning of status register. 2021-06-21 07:31:58 -04:00
Thomas Harte
b49cc407c6 Adds some guesses as to the EXDos expansion.
... with plenty left to do.
2021-06-20 22:30:27 -04:00
Thomas Harte
954386f1cc Creates a shell for the disk-drive add-on card. 2021-06-20 20:50:23 -04:00
Thomas Harte
d7ff6bd04d Adds necessary declarations to install a DOS ROM. 2021-06-20 20:30:54 -04:00
Thomas Harte
6025516f9f Ensure addresses increment even when there's no target for pixels. 2021-06-20 14:31:02 -04:00
Thomas Harte
d8b9cdf7a2 Correct multiplier. 2021-06-20 14:25:37 -04:00
Thomas Harte
09dbff39f2 Also map keypad to F keys.
This is a pragmatic and arguably Apple-centric decision. But also it's fairly arbitrary, as the Enterprise doesn't have a number pad.
2021-06-20 14:25:28 -04:00
Thomas Harte
2fe15a6168 Switch to idealised Nick clock rate. 2021-06-20 14:21:56 -04:00
Thomas Harte
07dc26f8fa Adds TODO to resolve screen jumping. 2021-06-19 23:41:29 -04:00
Thomas Harte
a08d65b1ff Adds IMG -> Enterprise connection.
Albeit still without an Enterprise static analyser.
2021-06-19 23:16:33 -04:00
Thomas Harte
199621db08 Observes that the actual guess here is MS-DOS-style. 2021-06-19 23:11:51 -04:00
Thomas Harte
0e1e8c7faa Attempts to support the panoply of EXOS and BASIC versions. 2021-06-19 22:59:09 -04:00
Thomas Harte
42a98e1676 Fix composition with empty nodes. 2021-06-19 22:13:17 -04:00
Thomas Harte
23e26e0333 Attempts to complete handling of VRES. 2021-06-19 22:00:19 -04:00
Thomas Harte
fadb04f3f3 Attempts to implement LSBALT and MSBALT. 2021-06-19 21:57:26 -04:00
Thomas Harte
4968ccf46d Corrects attributed mode. 2021-06-19 13:08:14 -04:00
Thomas Harte
1dcac304d3 Implements the ALTIND bits and attempts ATTR mode. 2021-06-19 13:04:18 -04:00
Thomas Harte
1651efe4fc Ensures all keys are initially unpressed. 2021-06-19 13:03:31 -04:00
Thomas Harte
8f24aed43e Slightly reduces logging. 2021-06-18 23:17:44 -04:00
Thomas Harte
a381374e31 Drops back down to 64kb. 2021-06-18 23:14:44 -04:00
Thomas Harte
9411c37d23 Fleshes out the keyboard map. 2021-06-18 23:14:35 -04:00
Thomas Harte
6af6f21868 Attempts to implement interrupt latches and clears. 2021-06-18 22:59:41 -04:00
Thomas Harte
9a0022cfcb Removes temporary work. 2021-06-18 18:44:07 -04:00
Thomas Harte
266310d9c2 Fixes automatic flushing for non-1/1-clocked actors. 2021-06-18 18:43:08 -04:00
Thomas Harte
fbf1adef05 Introduces unit test and thereby seemingly fixes get_next_sequence_point.
There's still improper output in the actual machine though, so maybe something else is afoot?
2021-06-18 17:44:17 -04:00
Thomas Harte
311ddfb05a Add note to self for tomorrow. 2021-06-17 22:34:52 -04:00
Thomas Harte
2fd8a8aa66 Begins addition of interrupt feedback from Nick.
Also fixes clock rate. Though clearly get_next_sequence_point isn't quite right yet.
2021-06-17 22:30:24 -04:00
Thomas Harte
0c3e9dca28 Adds some basic keyboard inputs.
I think the next thing required is interrupts though.
2021-06-17 20:47:56 -04:00
Thomas Harte
c331d15429 Makes space to allow for 64kb EXOS ROMs.
I think some of the later ROMs have a more thorough memory test, which might provide better detail on whatever's going on here.
2021-06-16 22:25:00 -04:00
Thomas Harte
4414e96710 Adds enough text mode for now.
Discovered: a memory fault is being reported at startup.
2021-06-16 21:42:20 -04:00
Thomas Harte
7161783a4f Adds lpixel output. 2021-06-16 21:16:26 -04:00
Thomas Harte
cbac48da86 Attempts full run at pixel mode. 2021-06-16 20:43:22 -04:00
Thomas Harte
d9142d5427 Adjusts declared scale, accurately to communicate pixel clock. 2021-06-15 22:39:44 -04:00
Thomas Harte
e5e988b28f Adds an incorrect assumed-pixel-mode serialiser.
This actually shows something a bit like the Enterprise boot logo.
2021-06-15 22:31:50 -04:00
Thomas Harte
e94e051c87 Adds an allocator for pixels. 2021-06-15 22:03:41 -04:00
Thomas Harte
5fc91effb5 Corrects top border. 2021-06-15 21:48:11 -04:00
Thomas Harte
6c9dacbe89 Stabilises display, albeit that top border mode now appears to be off. 2021-06-15 21:31:07 -04:00
Thomas Harte
6a7eb832cc Introduces almost-stable block-level frame generation. 2021-06-15 20:55:58 -04:00
Thomas Harte
60cf8486bb Makes a genuine attempt at real line counting.
No output yet though.
2021-06-15 18:57:14 -04:00
Thomas Harte
90b8163d54 Add exposition. 2021-06-15 17:44:39 -04:00
Thomas Harte
a1e4389f63 Give Nick some RAM to inspect and just enough sense to know when it should reload its line parameter table. 2021-06-15 17:43:13 -04:00
Thomas Harte
440b11708b Adds an unused CRT. 2021-06-14 23:11:48 -04:00
Thomas Harte
f90dce5c54 Take a guess at Nick timing. 2021-06-14 22:56:26 -04:00
Thomas Harte
606c7709cf Shims in enough to get the Z80 to run perpetually.
Notably I don't actually currently know how the interrupt registers work, but getting some sort of display running might be the first order of business.
2021-06-14 22:28:31 -04:00
Thomas Harte
1d1e6d1820 Adds a shell of a Nick. 2021-06-14 22:19:25 -04:00
Thomas Harte
3eb4dd74a2 Exposes memory control.
Machine now runs as far as trying to interact with Nick.
2021-06-14 21:45:12 -04:00
Thomas Harte
853914480c Revised guess; there's a jump to C02E almost immediately. 2021-06-14 21:40:19 -04:00
Thomas Harte
fe04410681 Merge branch 'master' into Enterprise 2021-06-14 21:30:49 -04:00
Thomas Harte
1f686c4e6b Add missing AppleIIOptionsPanel class. 2021-06-14 21:30:30 -04:00
Thomas Harte
2a2ac1227b Makes first attempt at giving the Z80 something to do. 2021-06-14 21:29:56 -04:00
Thomas Harte
b5340c8f74 Correct syntax. 2021-06-14 21:17:35 -04:00
Thomas Harte
196c4dcdd9 Adds an appropriate ROM request. 2021-06-14 21:17:09 -04:00
Thomas Harte
c5a86f0ef7 Add Enterprise parts of the static analyser. 2021-06-14 21:11:06 -04:00
Thomas Harte
88f2a2940b Add Enterprise source paths. 2021-06-14 21:07:35 -04:00
Thomas Harte
26b019a4d4 Removes assumption that all machines produce audio. 2021-06-14 21:02:55 -04:00
Thomas Harte
5f7b3ae313 Adds bare minimum to get a non-functional machine. 2021-06-14 21:02:40 -04:00
Thomas Harte
61c127ed2e Adds Enterprise as a File -> New... machine.
And, similarly, exposes it for the route used by SDL.
2021-06-14 20:55:39 -04:00
Thomas Harte
333981e2a7 Merge branch 'master' into Enterprise 2021-06-13 22:22:44 -04:00
Thomas Harte
423fbc9ac7 Merge pull request #949 from TomHarte/QtSnapshotDragAndDrop
Adds drag-and-drop snapshot support for Qt.
2021-06-13 21:48:42 -04:00
Thomas Harte
1c1719e561 Adds drag-and-drop snapshot support for Qt. 2021-06-13 21:41:20 -04:00
Thomas Harte
56c30e1651 Merge pull request #948 from TomHarte/QtAspectRatio
Ensures Apple II square pixels work correctly under OpenGL
2021-06-13 21:23:28 -04:00
Thomas Harte
1ea4130035 Avoid OpenGL restretching bug. 2021-06-13 19:46:47 -04:00
Thomas Harte
57713d63fa Avoids regression of selected tab upon app restart. 2021-06-13 19:38:56 -04:00
Thomas Harte
d18a537509 Fiddles with the preprocessor to make kiosk mode match other OSes even on macOS. 2021-06-13 19:28:05 -04:00
Thomas Harte
8e0a6df03b Merge branch 'master' into Enterprise 2021-06-10 21:41:57 -04:00
Thomas Harte
95a52a9f62 Merge pull request #947 from TomHarte/AppleIISquarePixels
Adds optional square pixel output for the Apple II
2021-06-08 18:04:08 -04:00
Thomas Harte
ae2993625c Add missing header. 2021-06-08 17:54:30 -04:00
Thomas Harte
0982141442 Corrects many minor errors. 2021-06-08 17:52:39 -04:00
Thomas Harte
85fab2abc4 Takes a swing at adding a square pixels toggle for Qt. 2021-06-08 17:37:46 -04:00
Thomas Harte
de3b37799c Switches to a static_cast. No need for reflection here. 2021-06-08 17:37:28 -04:00
Thomas Harte
70851f3b2d Resolve misplacement. 2021-06-07 21:43:26 -04:00
Thomas Harte
462bbf2e40 Exposes square pixels option on macOS. 2021-06-07 21:21:45 -04:00
Thomas Harte
778b9ef683 Ensures set_square_pixels is exposed, works around OpenGL aspect ratio bug. 2021-06-07 20:41:02 -04:00
Thomas Harte
96e7eb1bed Adds a use-square-pixels option for the Apple II. 2021-06-07 20:16:01 -04:00
Thomas Harte
05671f3553 Merge pull request #946 from TomHarte/OptionalOricColourROM
Introduces a new grammar for ROM requests.
2021-06-06 22:47:46 -04:00
Thomas Harte
6e4832f999 Ensures Oric honours absence of the colour ROM. 2021-06-06 22:43:53 -04:00
Thomas Harte
54e3332673 Ensure optionals appear at the end of any ROM request list. 2021-06-06 22:36:26 -04:00
Thomas Harte
6c559d7556 Fix lead-in text. 2021-06-06 22:02:11 -04:00
Thomas Harte
9165a85484 Correct wstring conversion. 2021-06-06 21:58:38 -04:00
Thomas Harte
98ada2588a Resolve name confusion. 2021-06-06 21:51:51 -04:00
Thomas Harte
43f686c22d Correct return type and map insertion. 2021-06-06 21:44:37 -04:00
Thomas Harte
4a2673d757 Make a prima facie attempt to adapt the Qt build. 2021-06-06 20:47:25 -04:00
Thomas Harte
f27e331462 Updates autotests to new RomFetcher world. 2021-06-06 20:34:55 -04:00
Thomas Harte
dd64aef910 Improves request construction and improves descriptions. 2021-06-06 20:25:26 -04:00
Thomas Harte
95971f39f1 Reintroduces full messaging to macOS. 2021-06-06 20:02:13 -04:00
Thomas Harte
83beb3c0e6 Introduces slightly-less manual ROM::Request::visit. 2021-06-06 18:28:02 -04:00
Thomas Harte
76335e5cf2 Factors out and slightly generalises textual descriptions of ROM::Descriptions. 2021-06-06 18:15:00 -04:00
Thomas Harte
4494320238 Corrects the macOS Swift side of things. 2021-06-06 14:56:43 -04:00
Thomas Harte
5acd97c860 Puts enough in place for a GUI-led installation process.
... and provides a lot of the Objective-C wiring necessary to expose that to Swift.
2021-06-06 14:24:38 -04:00
Thomas Harte
b0f551c307 Ensures only _missing_ ROMs are reported. 2021-06-05 21:09:35 -04:00
Thomas Harte
b6b3d845a3 Correct Apple IIe and Enhanced IIe startup. 2021-06-04 22:48:08 -04:00
Thomas Harte
505d84f336 Corrects Amstrad 664 and 6128 ROM collection. 2021-06-04 22:43:26 -04:00
Thomas Harte
1d5144b912 Correct no-interrupt signal. 2021-06-04 22:38:07 -04:00
Thomas Harte
deff91e460 Correct Electron name mapping. 2021-06-04 22:25:11 -04:00
Thomas Harte
afd8dc0915 Nudge just far enough to be able to launch again under macOS. 2021-06-04 22:24:31 -04:00
Thomas Harte
fbee74e1fe Avoids storing or printing a CRC if none is known. 2021-06-04 22:03:08 -04:00
Thomas Harte
ccd82591aa Reinstates SDL error message; adds expansion of ~. 2021-06-04 21:53:56 -04:00
Thomas Harte
64931e476d Completes transcription of ROM details with the Oric and MSX. 2021-06-04 19:50:49 -04:00
Thomas Harte
604a715a49 Transcribes the Spectrum, Electron, Master System and Vic-20 ROMs. 2021-06-04 19:45:47 -04:00
Thomas Harte
24757ef20c Transcribes the Macintosh, Atari ST, ColecoVision and ZX80/81 ROMs. 2021-06-04 19:24:57 -04:00
Thomas Harte
e36cc9e777 Transcribes the Apple II ROM descriptions. 2021-06-04 19:19:55 -04:00
Thomas Harte
2e999889bd Attempts to implement tree construction. 2021-06-04 19:03:07 -04:00
Thomas Harte
f4db4c3a73 Implements ROM::Request::validate.
It now also validates ROM sizes, so can no longer take a const Map.
2021-06-04 18:54:50 -04:00
Thomas Harte
d923fe72c0 Resolves various ROM selection warnings. 2021-06-03 22:46:47 -04:00
Thomas Harte
f05cdd5e34 With large swathes of implementation missing, compiles. 2021-06-03 22:39:18 -04:00
Thomas Harte
f9954619d4 Add missing header file. 2021-06-03 22:28:30 -04:00
Thomas Harte
0aa8c3c40d For SDL at least, advances to failed linking.
... and with error reporting currently AWOL.
2021-06-03 22:22:56 -04:00
Thomas Harte
a30eeaab6a Starts to introduce a new grammar for ROM requests.
They can be optional, and chained together in AND or OR combinations. A central catalogue knows the definitions of all ROMs.
2021-06-03 21:55:59 -04:00
Thomas Harte
3858e79579 Merge pull request #944 from TomHarte/SDLErrorReporting
Improve SDL failed-ROM reporting.
2021-05-30 19:50:53 -04:00
Thomas Harte
b4a5fa33b0 Improve SDL failed-ROM reporting.
Specifically to include all paths tried, and not use the plural for 'crc32' when only one is present.
2021-05-30 19:40:29 -04:00
Thomas Harte
2a6e9c5e8a Add readme for Enterprise ROM names. 2021-05-30 19:28:26 -04:00
Thomas Harte
488c2aed51 Merge pull request #939 from TomHarte/DragAndDropState
Accept insertion of state snapshots into existing windows
2021-05-16 20:47:36 -04:00
Thomas Harte
5483f979dc Merge branch 'master' into DragAndDropState 2021-05-16 20:42:44 -04:00
Thomas Harte
ea11f3826a Merge pull request #941 from TomHarte/LargeDSK
Adds support for Macintosh SCSI drive images.
2021-05-13 19:17:18 -04:00
Thomas Harte
ceae81a332 Add missing header. 2021-05-13 19:11:19 -04:00
Thomas Harte
50ea56e908 Adds support for Macintosh SCSI device images.
This is now in addition to the single-partition images previously supported.
2021-05-13 19:06:00 -04:00
Thomas Harte
bfb2f79cff That's two learning curves. 2021-05-10 21:33:40 -04:00
Thomas Harte
8268e8ee4c Ensures music survives a machine switch. 2021-05-08 20:46:17 -04:00
Thomas Harte
cb31e22f59 Merge branch 'master' into DragAndDropState 2021-05-08 20:41:44 -04:00
Thomas Harte
6752f4fd73 Merge pull request #940 from TomHarte/TighterTapeStop
Tightens automatic tape control timing.
2021-05-08 18:21:14 -04:00
Thomas Harte
22c31e4f55 Tightens automatic tape control timing. 2021-05-08 17:34:59 -04:00
Thomas Harte
c2ff64c1e0 Removes dangling OpenGL reference, attempts to ensure audio handover upon a machine change. 2021-05-08 14:42:43 -04:00
Thomas Harte
4db792591a macOS: ensure activity and options panels change upon a drag-and-drop state. 2021-05-08 14:35:57 -04:00
Thomas Harte
1290a8e32b SDL: Ensures joysticks, mouse, LEDs, etc, all update to a dragged state snapshot. 2021-05-08 13:30:07 -04:00
Thomas Harte
8ae38991b0 Factor out machine wiring. 2021-05-08 13:15:18 -04:00
Thomas Harte
6d40549c0c Merge branch 'master' into DragAndDropState 2021-05-07 21:56:36 -04:00
Thomas Harte
93d5c9a3c7 Tighten wording further. 2021-05-07 18:55:15 -04:00
Thomas Harte
9af6c0b37a Improves comment. 2021-05-06 12:57:32 -04:00
Thomas Harte
7e3528c692 Shunt the tech/URL stuff below the headline feature list. 2021-05-06 09:44:40 -04:00
Thomas Harte
41f2fc51be Clarify second sentence.
As per discussion at https://www.retrogameboards.com/t/clock-signal-a-multi-platform-emulator-that-focuses-on-a-better-user-experience/2375 — the previous could be read as "no emulator | or per-emulated-machine learning curve". But there is an emulator.
2021-05-06 09:43:19 -04:00
Thomas Harte
11228dc265 Merge pull request #937 from TomHarte/XKeySyms
Eliminate magic constants in Qt/X11 keyboard code.
2021-05-05 22:21:31 -04:00
Thomas Harte
ef50967793 Limit X11 linkage to Linux. 2021-05-05 22:17:24 -04:00
Thomas Harte
5f6c08b7e0 Avoid partial struct instantiation. 2021-05-05 22:00:50 -04:00
Thomas Harte
6cb23ec5be Tidy up and comment. 2021-05-05 21:58:54 -04:00
Thomas Harte
1bae70bcf8 Correct capitalisation. 2021-05-05 21:49:01 -04:00
Thomas Harte
9820591ba4 Corrects enum references. 2021-05-05 21:46:34 -04:00
Thomas Harte
77071b3c69 Adds KeySym -> key lookup. 2021-05-05 21:41:59 -04:00
Thomas Harte
335e839b31 Wrangles a single working call to XKeysymToKeycode. 2021-05-05 21:35:08 -04:00
Thomas Harte
6fe947b8b9 Fix class name, add constructor. 2021-05-05 19:17:23 -04:00
Thomas Harte
22b29e77a7 Add keyboard.cpp/h to the Qt project. 2021-05-05 19:06:25 -04:00
Thomas Harte
4858cfce6b Starts to factor out the keyboard mapper.
The more easily to clarify as to #includes, etc, and to allow for a relevant constructor.
2021-05-05 18:56:10 -04:00
Thomas Harte
8da3e91f5e Merge branch 'master' into XKeySyms 2021-05-03 22:23:55 -04:00
Thomas Harte
012235bfeb Merge pull request #936 from TomHarte/Style
Correct minor style errors.
2021-05-03 22:23:27 -04:00
Thomas Harte
052e284c33 Add overt fallthrough. 2021-05-03 22:17:43 -04:00
Thomas Harte
32e3dd71b1 Be overt in empty std::string construction. 2021-05-03 22:17:32 -04:00
Thomas Harte
95f4272919 Make sure size_t is visible. 2021-05-03 22:17:25 -04:00
Thomas Harte
00679b6135 t may be unused, per the if constexpr. 2021-05-03 22:17:19 -04:00
Thomas Harte
2c18bb4508 Make it overt that this can't return without a value. 2021-05-03 22:17:12 -04:00
Thomas Harte
0cf1c9040a Add missing fallthrough declaration. 2021-05-03 22:17:06 -04:00
Thomas Harte
9196341482 Retrenches: it seems nativeVirtualKey does what I want.
Hooray!
2021-05-03 21:45:53 -04:00
Thomas Harte
685140a4c2 Correct Qt -> QT. 2021-05-03 21:18:14 -04:00
Thomas Harte
1465b0ee4d Shunt X11 code to bottom of file, to avoid #include interference. 2021-05-03 21:15:20 -04:00
Thomas Harte
0bf6b765d3 Further namespace/name corrections. 2021-05-03 21:11:47 -04:00
Thomas Harte
4774676e2a Correct keypad symbols, push X11 into a namespace. 2021-05-03 21:09:01 -04:00
Thomas Harte
9c29655da2 Add x11extras as per use of <QX11Info>. 2021-05-03 20:43:22 -04:00
Thomas Harte
c8ab18f2b6 Add overt fallthrough. 2021-05-03 20:38:50 -04:00
Thomas Harte
8ebce466db Be overt in empty std::string construction. 2021-05-03 20:35:23 -04:00
Thomas Harte
1b39b17125 Make sure size_t is visible. 2021-05-03 20:33:25 -04:00
Thomas Harte
5a46853075 t may be unused, per the if constexpr. 2021-05-03 20:32:16 -04:00
Thomas Harte
48ad4d4c4c Make it overt that this can't return without a value. 2021-05-03 20:31:39 -04:00
Thomas Harte
056a036712 Add missing fallthrough declaration. 2021-05-03 20:31:13 -04:00
Thomas Harte
70eaa79108 Makes an attempt to use X11 KeySyms.
Rather than hard-coding a mapping.
2021-05-03 18:51:58 -04:00
Thomas Harte
20c814a4dd Factors out boilerplate around full-device sector images. 2021-05-01 21:10:46 -04:00
Thomas Harte
6a052e1900 Starts working on SDL drag-and-drop support for snapshots. 2021-04-30 22:56:13 -04:00
Thomas Harte
cecdf8584a Ensures proper propagation of will_change_owner. 2021-04-30 22:51:26 -04:00
Thomas Harte
4758bc8615 Attempts to support insertion of states into existing windows. 2021-04-30 21:37:41 -04:00
Thomas Harte
c906dc3c0a Merge pull request #935 from TomHarte/OricJoystick
Adds Altai-style joystick support for the Oric.
2021-04-29 20:15:42 -04:00
Thomas Harte
d1dcb41b6f Adds Altai-style joystick support. 2021-04-29 18:29:29 -04:00
Thomas Harte
96ac86a757 Merge pull request #934 from TomHarte/OricTapes
Relaxes Oric .tap signature check.
2021-04-29 18:14:36 -04:00
Thomas Harte
4919786825 Relaxes Oric .tap signature check. 2021-04-29 18:00:02 -04:00
Thomas Harte
24b4185714 Merge pull request #933 from TomHarte/SpectrumJoystick
Adds ZX Spectrum joystick support.
2021-04-28 21:08:36 -04:00
Thomas Harte
ad10d0037a Inverts the Game Controller Framework value of the y axis. 2021-04-28 20:31:35 -04:00
Thomas Harte
b6554c8255 Adds joystick support. 2021-04-28 20:19:01 -04:00
Thomas Harte
01dc83d0d6 Merge pull request #932 from MaddTheSane/xcodemaintenance
Xcode maintenance.
2021-04-27 19:53:51 -04:00
C.W. Betts
2fd08789ab Xcode maintenance. 2021-04-27 12:50:26 -06:00
Thomas Harte
bc9e529995 Merge pull request #931 from TomHarte/FieldName
This field is counted in half-cycles.
2021-04-26 21:33:38 -04:00
Thomas Harte
708c24cc57 This field is counted in half-cycles. 2021-04-26 21:20:32 -04:00
Thomas Harte
7fb3048257 Update AllDisk and AllTape. 2021-04-26 21:04:25 -04:00
Thomas Harte
9319f0525a Merge pull request #930 from TomHarte/SZX
Adds SZX support.
2021-04-26 20:57:06 -04:00
Thomas Harte
b7a62e0121 Adds SZX support.
Tweaking exposed Spectrum state object as relevant.
2021-04-26 20:47:28 -04:00
Thomas Harte
bd5dd9b9a3 Merge pull request #929 from TomHarte/SpectrumSnapshots
Adds loading of state snapshots for the ZX Spectrum
2021-04-26 17:44:02 -04:00
Thomas Harte
3348167c46 Ensures AY registers are conveyed. 2021-04-26 17:39:11 -04:00
Thomas Harte
700c505974 Ensures the ZX Spectrum properly reports its display type. 2021-04-25 21:16:22 -04:00
Thomas Harte
d403036d86 Reduce bounce at Spectrum startup. 2021-04-25 20:56:57 -04:00
Thomas Harte
5e08d7db39 Carries through paging state; avoids file rereads. 2021-04-25 20:46:49 -04:00
Thomas Harte
c34cb310a8 Switches to more straightforward handler for .z80-style compression. 2021-04-25 18:07:36 -04:00
Thomas Harte
8d86aa69bc Adds an assert to check handling of compressed data. 2021-04-25 18:02:31 -04:00
Thomas Harte
cc41ccc5f1 Adds RAM deserialisation. 2021-04-25 17:55:52 -04:00
Thomas Harte
e6252fe0ed Sneaks up towards loading RAM. 2021-04-25 17:34:43 -04:00
Thomas Harte
03577de675 Adds an empty vessel for .z80 support. 2021-04-25 16:54:34 -04:00
Thomas Harte
205518ba75 Switch to more efficient copy. 2021-04-25 16:51:07 -04:00
Thomas Harte
2510064218 Completes state object.
Subject to not yet dealing with last_fetches_ and last_contended_access_ correctly. Thought required.
2021-04-25 14:20:40 -04:00
Thomas Harte
0ef2806970 Adds just enough to ensure that border state gets through. 2021-04-25 14:16:35 -04:00
Thomas Harte
d80f03e369 Corrects longstanding deviation from naming convention. 2021-04-25 14:11:36 -04:00
Thomas Harte
fd271d920b Adds capture and forwarding of border colour. 2021-04-25 14:00:12 -04:00
Thomas Harte
2bbf8bc9fa Ensures 16/48kb snapshots are properly copied into place. 2021-04-25 13:27:11 -04:00
Thomas Harte
9b65d56ed0 Resolves potential flaw in POPping here. 2021-04-25 13:26:53 -04:00
Thomas Harte
a5098a60ec Attempts to get in-SNA software to start. 2021-04-25 13:18:26 -04:00
Thomas Harte
0ebd900e40 Baby steps: apply Z80 state.
As far as it currently is. Since SNA is leaving the PC at the default of 0x0000, this currently has no visible effect.
2021-04-25 13:03:24 -04:00
Thomas Harte
7aeb17ac92 Corrects HeaderDoc/etc directive. 2021-04-25 13:01:23 -04:00
Thomas Harte
cc78bfb229 Forwards most of the Z80 state. 2021-04-25 13:00:43 -04:00
Thomas Harte
485c2a866c Without yet a struct for Spectrum states, at least checks general wiring. 2021-04-24 23:38:00 -04:00
Thomas Harte
5b419ca5bf Add State folder to Scons and Qt projects. 2021-04-24 23:25:08 -04:00
Thomas Harte
14ae579fca Add further note to future self. 2021-04-24 23:19:41 -04:00
Thomas Harte
1c2ea0d7fe unique_ptr makes more sense here. 2021-04-24 23:19:30 -04:00
Thomas Harte
e7a9ae18a1 Introduce further default state. 2021-04-24 23:18:00 -04:00
Thomas Harte
d61f478a39 Basic sketch for state snapshots: an extra field on Target.
I think it doesn't make sense for states to own a target as that complicates the concept of Media. Plus they're distinct because it makes sense to have only one per Target. Let's see how this pans out.
2021-04-24 23:17:47 -04:00
Thomas Harte
9cc747b3e2 Resolves potential source of errors: specifying incorrect table size.
(Having made exactly this mistake with the ZX Spectrum)
2021-04-24 12:10:28 -04:00
Thomas Harte
2f223f7db2 Spectrum emulation is no longer +2a/+3 specific. 2021-04-23 22:55:54 -04:00
Thomas Harte
17f11a3be3 Merge pull request #928 from TomHarte/ContentionTests
Add timing tests, fix +3 discrepancy.
2021-04-23 22:54:34 -04:00
Thomas Harte
37dcf61130 Add timing tests, fix +3 discrepancy. 2021-04-23 22:29:57 -04:00
Thomas Harte
856ebfacca Merge pull request #927 from TomHarte/SimplifiedTiming
Moves horizontal sync on the 48kb.
2021-04-21 19:50:40 -04:00
Thomas Harte
9731fdd33b Moves horizontal sync on the 48kb. 2021-04-21 19:46:44 -04:00
Thomas Harte
5ea605ccf7 Merge pull request #926 from TomHarte/SimplifiedTiming
Attempts more cleanly to express ZX Spectrum timing.
2021-04-21 19:46:23 -04:00
Thomas Harte
d0c789ff9a Locks declarative form of contention closer to regular expressions. 2021-04-21 19:37:36 -04:00
Thomas Harte
9baa861742 Simplifies timing calculation expression. 2021-04-21 19:18:07 -04:00
Thomas Harte
30a1a53c97 Merge pull request #925 from TomHarte/ZXROMSpeed
Corrects timing error in Spectrum 48kb and 128kb ROM accesses.
2021-04-21 18:54:16 -04:00
Thomas Harte
bdb1b7e77c Reinstate the +2 as the default Spectrum. 2021-04-21 18:49:39 -04:00
Thomas Harte
9293bcbc88 Exclude the ROM from contention on 48kb and 128kb models. 2021-04-21 18:49:18 -04:00
Thomas Harte
c481f475e7 Merge pull request #923 from TomHarte/STStartup
Resolves failure of ST to startup
2021-04-20 22:43:55 -04:00
Thomas Harte
ef01471e17 Ensures the DMA controller remains clocked. 2021-04-20 22:34:13 -04:00
Thomas Harte
73c8157197 Retain 6850 time tracking at all times. 2021-04-20 22:26:43 -04:00
Thomas Harte
af1dc2d3b2 Switches to correct non-value sentinel. 2021-04-20 21:56:58 -04:00
Thomas Harte
8f6b3feee1 Merge pull request #921 from TomHarte/Plus2aDefault
Switches default machine back to +2a.
2021-04-19 22:15:48 -04:00
Thomas Harte
a20f5528b7 Switches default machine back to +2a. 2021-04-19 22:04:49 -04:00
Thomas Harte
f48876d80e Merge pull request #920 from TomHarte/AppleIIVirtual
Disambiguates `reset_all_keys`.
2021-04-19 22:03:01 -04:00
Thomas Harte
db52f13c32 Disambiguates reset_all_keys. 2021-04-19 21:49:06 -04:00
Thomas Harte
2590769d3f Merge pull request #919 from TomHarte/XcodeProjectTweaks
Increases warnings, cleans up a touch.
2021-04-19 21:33:04 -04:00
Thomas Harte
5667dcac36 Increases warnings, cleans up a touch. 2021-04-19 21:28:12 -04:00
Thomas Harte
bec71ead39 Merge pull request #918 from TomHarte/macOS13
Reintroduces macOS 10.13 support.
2021-04-19 21:12:57 -04:00
Thomas Harte
e4d9022d37 Returns deployment target to 10.13. 2021-04-19 20:57:56 -04:00
Thomas Harte
572be48f38 Attempts to add an early exit for non-Metal Macs.
This will be necessary only prior to 10.14.
2021-04-19 20:55:25 -04:00
Thomas Harte
6f4ccebfa1 Merge pull request #917 from TomHarte/InterruptAddress
Put the program counter on the bus during interrupt acknowledge.
2021-04-19 20:08:22 -04:00
Thomas Harte
77fcf52d27 Purely style: remove some redundant nullptrs. 2021-04-19 18:53:00 -04:00
Thomas Harte
79c2bc1fd7 Put the program counter on the bus during interrupt acknowledge. 2021-04-19 18:43:50 -04:00
Thomas Harte
76370d9418 Merge pull request #916 from TomHarte/OffByOne
Corrects off-by-one timing errors in the ZX Spectrum.
2021-04-18 20:25:13 -04:00
Thomas Harte
7bac18bd65 Address bus load time is not + 1/2. 2021-04-18 18:41:24 -04:00
Thomas Harte
704737144a Corrects all interrupt timing for sign and off-by-one errors. 2021-04-18 18:40:44 -04:00
Thomas Harte
2a9c73a1d3 Merge pull request #915 from TomHarte/SpectrumSDLOptions
Adds display of Spectrum command-line options.
2021-04-18 12:08:02 -04:00
Thomas Harte
e87e851401 Add a redundant but idiomatic initial value. 2021-04-18 11:56:22 -04:00
Thomas Harte
80d4846a27 Respond with 0xff during an interrupt acknowledge. 2021-04-18 11:56:00 -04:00
Thomas Harte
9fd53c9c91 Adds the ZX Spectrum to ::AllMachines. 2021-04-17 23:06:37 -04:00
Thomas Harte
53eae873d8 Merge pull request #913 from TomHarte/LowerModelTiming
Brings timings into line with WoS specs.
2021-04-16 22:45:54 -04:00
Thomas Harte
93422f4b1c Brings timings into line with WoS specs. 2021-04-16 22:40:51 -04:00
Thomas Harte
06cedb2e50 Merge pull request #912 from TomHarte/128kDecoding
Corrects Spectrum 128kb partial decoding.
2021-04-16 22:02:25 -04:00
Thomas Harte
7fdb1d848b Corrects Spectrum 128kb partial decoding. 2021-04-16 21:54:52 -04:00
Thomas Harte
246fd9442f Merge pull request #911 from TomHarte/48kbSpectrum
Adds the 48kb and 128kb Spectrums.
2021-04-15 22:25:07 -04:00
Thomas Harte
eb99a64b29 Adds new Spectrum models to Qt UI. 2021-04-15 22:20:34 -04:00
Thomas Harte
d7954a4cb1 Tweaks timing a little. 2021-04-15 21:51:49 -04:00
Thomas Harte
ef636da866 Attempts 48/128kb floating bus behaviour. 2021-04-15 21:19:21 -04:00
Thomas Harte
fa18b06dbf Correct get_floating_value to be consistent in out-of-bounds behaviour. 2021-04-15 21:13:36 -04:00
Thomas Harte
349b9ce502 Don't post contended accesses other than on the +2a/+3.
Those machines have an actual latch for this stuff, the others don't.
2021-04-15 21:13:06 -04:00
Thomas Harte
b2cf121410 Regresses default to the more-compatible +2. 2021-04-15 19:31:45 -04:00
Thomas Harte
71cf63bd35 Corrects internal cycle contention. 2021-04-15 19:17:11 -04:00
Thomas Harte
d1bb3aada4 Attempts to complete the in-machine application of contention. 2021-04-15 18:57:34 -04:00
Thomas Harte
b4214c6e08 Blocks off the AY from inputs in 48kb mode. 2021-04-15 18:04:16 -04:00
Thomas Harte
f5c7746493 Extends fast loading support to the just-introduced models. 2021-04-15 17:31:42 -04:00
Thomas Harte
f10ec80153 Gets started on different video timings. 2021-04-14 22:23:27 -04:00
Thomas Harte
0af405aa46 Starts working in the 48kb and 128kb Spectrums. 2021-04-14 21:37:10 -04:00
Thomas Harte
cf481effa6 Merge pull request #910 from TomHarte/FastContention
Establishes that the 48/128kb contention patterns can be derived from my partial machine cycles alone.
2021-04-14 20:21:52 -04:00
Thomas Harte
a1511f9600 Establishes that the 48/128kb contention patterns can be derived from my partial machine cycles alone. 2021-04-14 20:15:40 -04:00
Thomas Harte
325e2b3941 Merge pull request #902 from TomHarte/Z80Lines
Spell out, test and correct Z80 bus activity.
2021-04-13 22:22:26 -04:00
Thomas Harte
7017324d60 r_step is obsolete now that I know that [DD/FD]CB don't have a refresh cycle. 2021-04-13 22:17:30 -04:00
Thomas Harte
deb5d69ac7 Consolidates macros. 2021-04-13 22:11:28 -04:00
Thomas Harte
68a04f4e6a Adds IN/OUT I/D [R] to complete tests. 2021-04-13 22:00:24 -04:00
Thomas Harte
0d61902b10 Adds CP[I/D/IR/DR] tests. 2021-04-13 20:03:11 -04:00
Thomas Harte
3eec210b30 Adds LDI/LDD/LDIR/LDDR tests. 2021-04-13 20:00:29 -04:00
Thomas Harte
5998f3b35b Corrects LD[I/D/IR/DR] timing.
Macro cleanup to come.
2021-04-13 20:00:18 -04:00
Thomas Harte
869567fdd9 Corrects EX (SP), HL breakdown. 2021-04-13 19:45:48 -04:00
Thomas Harte
2e70b5eb9f Advances to EX (SP), HL, leaving only [LD/CP/IN/OT][I/D]{R}. 2021-04-13 19:45:29 -04:00
Thomas Harte
8a3bfb8672 Adds an IN/OUT test. 2021-04-13 17:55:51 -04:00
Thomas Harte
06f1e64177 Advances to IO. 2021-04-12 21:41:20 -04:00
Thomas Harte
b42780173a Establishes that there really is no Read4 and Read4Pre distinction.
Will finish these unit tests, then clean up.
2021-04-12 20:54:10 -04:00
Thomas Harte
36c8821c4c Reaches the halfway point in tests. 2021-04-12 17:29:03 -04:00
Thomas Harte
947de2d54a Switches five-cycle read to a post hoc pause. 2021-04-12 17:17:08 -04:00
Thomas Harte
9347fe5f44 Advances to next failing test: LD (ii+n), n. 2021-04-12 17:11:58 -04:00
Thomas Harte
e82367def3 Switches to test-conformant behaviour for (IX/IY+n) opcode fetches. 2021-04-11 23:01:00 -04:00
Thomas Harte
9cde7c12ba Shifts responsibility for refresh into the fetch-decode-execute sequence. 2021-04-11 22:50:24 -04:00
Thomas Harte
015556cc91 Switch (ii+n) to Read4Pre. 2021-04-11 10:26:14 -04:00
Thomas Harte
47c5a243aa Restructures, the better to explore errors. 2021-04-10 21:32:42 -04:00
Thomas Harte
070e359d82 Introduces failing test for BIT b, (ii+n). 2021-04-10 18:00:23 -04:00
Thomas Harte
b397059d5e Moves read time in Read4Pre. 2021-04-10 17:54:20 -04:00
Thomas Harte
400f54e508 Introduces failing test for bit b, (hl). 2021-04-10 12:04:48 -04:00
Thomas Harte
e0736435f8 Makes assumption that the address bus just holds its value during an internal operation. 2021-04-10 12:00:53 -04:00
Thomas Harte
b09c5538c6 Adds failing test for simple (ii+n) tests. 2021-04-09 21:28:35 -04:00
Thomas Harte
ce3d2913bf Advances to 9 source table rows tested out of 37. 2021-04-09 20:38:17 -04:00
Thomas Harte
87202a2a27 Add two further tests, add checking of collected data size for all tests. 2021-04-09 18:32:03 -04:00
Thomas Harte
818a4dff25 Corrects ADD HL, dd test.
Or, at least, likely corrects. The bus cycle breakdown in the Z80 data sheet implies these accesses should come after completion of the refresh cycle, not during its long tail, so I think +1 is correct.
2021-04-08 22:23:15 -04:00
Thomas Harte
eacffa49f5 Exposes IR during 'internal' operations. 2021-04-08 22:22:26 -04:00
Thomas Harte
9e506c3206 Adds failing ADD hl, dd test. 2021-04-08 22:19:22 -04:00
Thomas Harte
29cf80339a Corrects too-short buffer. 2021-04-08 22:15:03 -04:00
Thomas Harte
50f53f7d97 Adds INC/DEC rr and LD SP, HL tests. 2021-04-08 22:14:53 -04:00
Thomas Harte
73fbd89c85 Correct opcodes, ability to terminate on a single-cycle contention. 2021-04-08 22:09:33 -04:00
Thomas Harte
f74fa06f2d Introduces failing test for LD [A/I/R], [A/I/R]. 2021-04-08 20:28:55 -04:00
Thomas Harte
ee989ab762 Fills in the rest of the simple two-byte instructions. 2021-04-08 20:13:52 -04:00
Thomas Harte
818655a9b6 Starts on two-bus-cycle instructions, correcting validators. 2021-04-08 20:01:46 -04:00
Thomas Harte
57a7e0834f Corrects sampling of MREQ. 2021-04-08 19:21:35 -04:00
Thomas Harte
cd787486d2 Tests all of the single-byte, no-access opcodes. 2021-04-07 22:07:52 -04:00
Thomas Harte
67fd6787a6 Builds what I think I need to validate Z80 address, MREQ, IOREQ and RFSH. 2021-04-07 21:57:40 -04:00
Thomas Harte
627b96f73c Merge branch 'master' into Z80Lines 2021-04-07 21:02:15 -04:00
Thomas Harte
8a6985c2e8 Merge pull request #909 from TomHarte/BackToFive
Tweaks video timing, again.
2021-04-06 21:16:50 -04:00
Thomas Harte
60e8273de2 Tweaks video timing, again. 2021-04-06 21:04:54 -04:00
Thomas Harte
aa8ce5c1ac Merge pull request #908 from TomHarte/ZXSpectrumInterrupts
Better indicate ZX Spectrum interrupt timing.
2021-04-06 13:49:25 -04:00
Thomas Harte
dd28246f9f Better indicate interrupt timing. 2021-04-06 12:06:13 -04:00
Thomas Harte
dc25a60b9b Merge pull request #907 from TomHarte/TMSSequencePoints
Makes the TMS a sequence-point-generating JustInTimeActor.
2021-04-06 12:03:22 -04:00
Thomas Harte
094d623485 Updates unit tests. 2021-04-05 21:33:04 -04:00
Thomas Harte
1266bbb224 Makes the TMS a sequence-point-generating JustInTimeActor. 2021-04-05 21:02:37 -04:00
Thomas Harte
bd1ea5740a Merge pull request #906 from TomHarte/LoadingImprovements
Attempts to improve ZX fast-loading compatibility
2021-04-05 19:20:46 -04:00
Thomas Harte
3e04b51122 Walks back pretty names. Probably a bad idea. 2021-04-05 17:26:18 -04:00
Thomas Harte
76f2aba51a Makes use of pretty names in descriptions optional. 2021-04-05 17:24:16 -04:00
Thomas Harte
fd88071c0a Remove further detritus. 2021-04-05 17:21:26 -04:00
Thomas Harte
16bfe1a55c Resolves use-after-return memory error. 2021-04-04 22:45:56 -04:00
Thomas Harte
90c3d6a1e8 Attempts a later interception of tape loading. 2021-04-04 22:39:30 -04:00
Thomas Harte
18d6197d6c Makes provision for pretty-printed key names.
i.e. keys that don't fit C++ naming rules.
2021-04-04 22:20:35 -04:00
Thomas Harte
27eddf6dff Merge pull request #905 from TomHarte/JustInTimeOric
Adopts JustInTimeActor in the Oric.
2021-04-04 20:57:52 -04:00
Thomas Harte
57b32d9537 Avoid adding additional threading constraints. 2021-04-04 20:48:15 -04:00
Thomas Harte
837b9499d5 Translates Oric video and Disk II into JustInTimeActors. 2021-04-04 20:43:16 -04:00
Thomas Harte
c2fde2b147 Merge pull request #900 from TomHarte/SpeccyTiming
Further tweaks Spectrum timing.
2021-04-04 20:19:54 -04:00
Thomas Harte
f26bf4b9e4 Splitting here isn't achieving anything. 2021-04-04 19:52:38 -04:00
Thomas Harte
1da51bee6c 14368 and six seem to be the proper numbers, per my comprehension of Patrick Rak. 2021-04-04 19:52:19 -04:00
Thomas Harte
5a66956221 Merge branch 'master' into SpeccyTiming 2021-04-04 19:12:37 -04:00
Thomas Harte
91d973c4a9 Merge pull request #904 from TomHarte/JITElectron
Moves the Electron to JustInTimeActor video.
2021-04-04 19:12:06 -04:00
Thomas Harte
fa79589db8 Minor style improvements. 2021-04-04 18:59:46 -04:00
Thomas Harte
e52649f74d Normalises logging. 2021-04-04 17:39:49 -04:00
Thomas Harte
d77ddaf4fa Switches the Electron to JustInTimeActor video.
Also reorders template parameters; I think that specifying a different time base is likely to be more common than using a divider.
2021-04-04 17:33:49 -04:00
Thomas Harte
9ff392279a Merge pull request #895 from TomHarte/JITSleeper
Works `ClockingHint` logic into `JustInTimeActor`.
2021-04-04 17:11:05 -04:00
Thomas Harte
448d9dc3e1 Correct article. 2021-04-04 16:14:47 -04:00
Thomas Harte
afb4e6d37d Merge branch 'master' into JITSleeper 2021-04-04 15:37:19 -04:00
Thomas Harte
158122fbf4 Determine TargetTimeScale automatically. 2021-04-04 15:37:07 -04:00
Thomas Harte
417ece2386 Adds a couple of TODOs and some further documentation. 2021-04-04 00:25:22 -04:00
Thomas Harte
77b241af4f Eliminates unused RealTimeActor, provides more feedback from +=, gets specific as to nodiscards. 2021-04-03 21:26:43 -04:00
Thomas Harte
25b8c4c062 Provide clearer failure case. 2021-04-03 21:04:44 -04:00
Thomas Harte
1be88a5308 Remove first draft. 2021-04-02 07:39:22 -04:00
Thomas Harte
294280a94e Spells out everything except interrupt acknowledge. 2021-04-02 07:38:06 -04:00
Thomas Harte
32aebfebe0 Starts spelling out meaning of the Z80's partial machine cycles. 2021-04-02 07:37:56 -04:00
Thomas Harte
14663bd06b I think 3 is what I'm aiming for here.
But this probably isn't correct for IO cycles.
2021-04-02 07:36:57 -04:00
Thomas Harte
68abd197aa 'Dock' is a common noun here. 2021-04-02 07:11:28 -04:00
Thomas Harte
18fd21eae7 Merge pull request #901 from TomHarte/ReadMeClarity
Clarifies the object in the 'Single-click Loading' readme section
2021-04-01 23:25:01 -04:00
Thomas Harte
3296347370 Hit this point even harder. 2021-04-01 23:22:47 -04:00
Thomas Harte
28c9463e0d Clarify object. 2021-04-01 23:18:20 -04:00
Thomas Harte
044ac949ba Rearrange fields. 2021-04-01 12:44:00 -04:00
Thomas Harte
87317f5673 Improve documentation, pin down read/write times. 2021-04-01 12:38:58 -04:00
Thomas Harte
5e21a49841 Merge pull request #898 from TomHarte/LoadingImprovements
Include AF' in Z80 state.
2021-03-31 23:08:43 -04:00
Thomas Harte
687c05365e Flushes before set_last_contended_area_access. 2021-03-31 22:52:41 -04:00
Thomas Harte
4f80523828 Tweaks contended timing. 2021-03-31 22:51:20 -04:00
Thomas Harte
76299a2add Include AF' in Z80 state. 2021-03-29 22:58:52 -04:00
Thomas Harte
48f794dc2d Merge pull request #897 from TomHarte/LoadingImprovements
Corrects Spectrum TAP `is_at_end`.
2021-03-29 15:27:06 -04:00
Thomas Harte
51b8dcd011 Fixes is_at_end — must be at end of file and have finished final block. 2021-03-28 23:25:29 -04:00
Thomas Harte
acdbd88b9e Merge pull request #896 from TomHarte/FastLoadUponInsert
Ensure CPC and Spectrum update fast-tape flag upon media insertion.
2021-03-28 11:44:01 -04:00
Thomas Harte
00a3a3c724 Merge pull request #894 from TomHarte/AYCleanup
Uses `GI::AY38910::Utility` far and wide.
2021-03-28 11:43:43 -04:00
Thomas Harte
729edeac6c Ensure CPC and Spectrum update fast-tape flag upon media insertion. 2021-03-27 18:08:46 -04:00
Thomas Harte
faaa4961ed Attempts to rely on JustInTimeActor's built-in ClockingHint::Observer. 2021-03-26 23:54:08 -04:00
Thomas Harte
7937cc2d0f Imputes ClockingHint::Observer logic into JustInTimeActor. 2021-03-26 23:44:15 -04:00
Thomas Harte
8a11a5832c Uses GI::AY38910::Utility far and wide. 2021-03-26 23:19:47 -04:00
Thomas Harte
53ba0e67d1 Revert change to screenshot destination.
For a sandboxed app, there's a lot more to it than this.
2021-03-25 22:44:18 -04:00
Thomas Harte
e8825aeada Merge pull request #893 from TomHarte/DesktopScreenshots
Switches to a more compact macOS machine picker
2021-03-25 22:42:56 -04:00
Thomas Harte
e90e30e766 Enables start by double-click. 2021-03-25 17:53:07 -04:00
Thomas Harte
9f6bb325e6 Fixes longstanding issue with initial target for input. 2021-03-25 17:48:48 -04:00
Thomas Harte
6e2c65435a Tweaks cell height slightly further. 2021-03-25 17:44:46 -04:00
Thomas Harte
052ab44f1c Adds a title and adjusts aspect ratio. 2021-03-25 17:37:40 -04:00
Thomas Harte
daa5679241 Don't allow cell editing, lock size. 2021-03-25 16:48:11 -04:00
Thomas Harte
e055668554 With no space constraint, this can be 'ZX Spectrum'. 2021-03-25 16:27:12 -04:00
Thomas Harte
c96829c29e Adds a table view to control tab selection.
This should allow the new machine dialogue to retain a sensible width from here onwards.
2021-03-25 16:25:11 -04:00
Thomas Harte
c88abed2dc Merge branch 'master' into DesktopScreenshots 2021-03-24 21:47:40 -04:00
Thomas Harte
e42b6cb3c8 Merge pull request #892 from TomHarte/FixedFloatingBus
Attempts to implement proper floating bus behaviour.
2021-03-24 21:44:01 -04:00
Thomas Harte
465ecc4a78 Attempts to implement proper floating bus behaviour.
As per http://sky.relative-path.com/zx/floating_bus.html
2021-03-24 20:23:33 -04:00
Thomas Harte
ae4ccdf5e6 Merge branch 'master' into DesktopScreenshots 2021-03-24 18:40:20 -04:00
Thomas Harte
fbe479c43f Switch to saving screenshots to the desktop.
Or, at least, try. User permission would be required. More reading necessary.
2020-01-26 17:36:16 -05:00
171 changed files with 10622 additions and 2300 deletions

View File

@@ -19,6 +19,7 @@ enum class Machine {
AtariST,
ColecoVision,
Electron,
Enterprise,
Macintosh,
MasterSystem,
MSX,

View File

@@ -0,0 +1,81 @@
//
// StaticAnalyser.cpp
// Clock Signal
//
// Created by Thomas Harte on 24/06/2021.
// Copyright 2021 Thomas Harte. All rights reserved.
//
#include "StaticAnalyser.hpp"
#include "Target.hpp"
#include "../../../Storage/Disk/Parsers/FAT.hpp"
#include <algorithm>
namespace {
bool insensitive_equal(const std::string &lhs, const std::string &rhs) {
return std::equal(
lhs.begin(), lhs.end(),
rhs.begin(), rhs.end(),
[] (char l, char r) {
return tolower(l) == tolower(r);
});
}
}
Analyser::Static::TargetList Analyser::Static::Enterprise::GetTargets(const Media &media, const std::string &, TargetPlatform::IntType) {
// This analyser can comprehend disks only.
if(media.disks.empty()) return {};
// Otherwise, assume a return will happen.
Analyser::Static::TargetList targets;
using Target = Analyser::Static::Enterprise::Target;
auto *const target = new Target;
target->media = media;
// Always require a BASIC.
target->basic_version = Target::BASICVersion::Any;
// Inspect any supplied disks.
if(!media.disks.empty()) {
// DOS will be needed.
target->dos = Target::DOS::EXDOS;
// Grab the volume information, which includes the root directory.
auto volume = Storage::Disk::FAT::GetVolume(media.disks.front());
if(volume) {
// If there's an EXDOS.INI then this disk should be able to boot itself.
// If not but if there's only one .COM or .BAS, automatically load that.
// Failing that, issue a :DIR and give the user a clue as to how to load.
const Storage::Disk::FAT::File *selected_file = nullptr;
bool has_exdos_ini = false;
bool did_pick_file = false;
for(const auto &file: (*volume).root_directory) {
if(insensitive_equal(file.name, "exdos") && insensitive_equal(file.extension, "ini")) {
has_exdos_ini = true;
break;
}
if(insensitive_equal(file.extension, "com") || insensitive_equal(file.extension, "bas")) {
did_pick_file = !selected_file;
selected_file = &file;
}
}
if(!has_exdos_ini) {
if(did_pick_file) {
target->loading_command = std::string("run \"") + selected_file->name + "." + selected_file->extension + "\"\n";
} else {
target->loading_command = ":dir\n";
}
}
}
}
targets.push_back(std::unique_ptr<Analyser::Static::Target>(target));
return targets;
}

View File

@@ -0,0 +1,27 @@
//
// StaticAnalyser.hpp
// Clock Signal
//
// Created by Thomas Harte on 24/06/2021.
// Copyright 2018 Thomas Harte. All rights reserved.
//
#ifndef Analyser_Static_Enterprise_StaticAnalyser_hpp
#define Analyser_Static_Enterprise_StaticAnalyser_hpp
#include "../StaticAnalyser.hpp"
#include "../../../Storage/TargetPlatforms.hpp"
#include <string>
namespace Analyser {
namespace Static {
namespace Enterprise {
TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms);
}
}
}
#endif /* Analyser_Static_Enterprise_StaticAnalyser_hpp */

View File

@@ -0,0 +1,57 @@
//
// Target.hpp
// Clock Signal
//
// Created by Thomas Harte on 14/06/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef Analyser_Static_Enterprise_Target_h
#define Analyser_Static_Enterprise_Target_h
#include "../../../Reflection/Enum.hpp"
#include "../../../Reflection/Struct.hpp"
#include "../StaticAnalyser.hpp"
#include <string>
namespace Analyser {
namespace Static {
namespace Enterprise {
struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Target> {
ReflectableEnum(Model, Enterprise64, Enterprise128, Enterprise256);
ReflectableEnum(EXOSVersion, v10, v20, v21, v23, Any);
ReflectableEnum(BASICVersion, v10, v11, v21, Any, None);
ReflectableEnum(DOS, EXDOS, None);
ReflectableEnum(Speed, FourMHz, SixMHz);
Model model = Model::Enterprise128;
EXOSVersion exos_version = EXOSVersion::Any;
BASICVersion basic_version = BASICVersion::None;
DOS dos = DOS::None;
Speed speed = Speed::FourMHz;
std::string loading_command;
Target() : Analyser::Static::Target(Machine::Enterprise) {
if(needs_declare()) {
AnnounceEnum(Model);
AnnounceEnum(EXOSVersion);
AnnounceEnum(BASICVersion);
AnnounceEnum(DOS);
AnnounceEnum(Speed);
DeclareField(model);
DeclareField(exos_version);
DeclareField(basic_version);
DeclareField(dos);
DeclareField(speed);
}
}
};
}
}
}
#endif /* Analyser_Static_Enterprise_Target_h */

View File

@@ -23,6 +23,7 @@
#include "Coleco/StaticAnalyser.hpp"
#include "Commodore/StaticAnalyser.hpp"
#include "DiskII/StaticAnalyser.hpp"
#include "Enterprise/StaticAnalyser.hpp"
#include "Macintosh/StaticAnalyser.hpp"
#include "MSX/StaticAnalyser.hpp"
#include "Oric/StaticAnalyser.hpp"
@@ -43,9 +44,9 @@
#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/MSA.hpp"
#include "../../Storage/Disk/DiskImage/Formats/MSXDSK.hpp"
#include "../../Storage/Disk/DiskImage/Formats/NIB.hpp"
#include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp"
#include "../../Storage/Disk/DiskImage/Formats/SSD.hpp"
@@ -55,8 +56,14 @@
// Mass Storage Devices (i.e. usually, hard disks)
#include "../../Storage/MassStorage/Formats/DAT.hpp"
#include "../../Storage/MassStorage/Formats/DSK.hpp"
#include "../../Storage/MassStorage/Formats/HFV.hpp"
// State Snapshots
#include "../../Storage/State/SNA.hpp"
#include "../../Storage/State/SZX.hpp"
#include "../../Storage/State/Z80.hpp"
// Tapes
#include "../../Storage/Tape/Formats/CAS.hpp"
#include "../../Storage/Tape/Formats/CommodoreTAP.hpp"
@@ -73,15 +80,23 @@
using namespace Analyser::Static;
static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::IntType &potential_platforms) {
Media result;
namespace {
std::string get_extension(const std::string &name) {
// Get the extension, if any; it will be assumed that extensions are reliable, so an extension is a broad-phase
// test as to file format.
std::string::size_type final_dot = file_name.find_last_of(".");
if(final_dot == std::string::npos) return result;
std::string extension = file_name.substr(final_dot + 1);
std::string::size_type final_dot = name.find_last_of(".");
if(final_dot == std::string::npos) return name;
std::string extension = name.substr(final_dot + 1);
std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
return extension;
}
}
static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::IntType &potential_platforms) {
Media result;
const std::string extension = get_extension(file_name);
#define InsertInstance(list, instance, platforms) \
list.emplace_back(instance);\
@@ -131,8 +146,9 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
TargetPlatform::AmstradCPC | TargetPlatform::Oric | TargetPlatform::ZXSpectrum) // DSK (Amstrad CPC, etc)
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::AppleDSK>, TargetPlatform::DiskII) // DSK (Apple II)
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MacintoshIMG>, TargetPlatform::Macintosh) // DSK (Macintosh, floppy disk)
Format("dsk", result.mass_storage_devices, MassStorage::HFV, TargetPlatform::Macintosh) // DSK (Macintosh, hard disk)
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::MSXDSK>, TargetPlatform::MSX) // DSK (MSX)
Format("dsk", result.mass_storage_devices, MassStorage::HFV, TargetPlatform::Macintosh) // DSK (Macintosh, hard disk, single volume image)
Format("dsk", result.mass_storage_devices, MassStorage::DSK, TargetPlatform::Macintosh) // DSK (Macintosh, hard disk, full device image)
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::FAT12>, TargetPlatform::MSX) // DSK (MSX)
Format("dsk", result.disks, Disk::DiskImageHolder<Storage::Disk::OricMFMDSK>, TargetPlatform::Oric) // DSK (Oric)
Format("g64", result.disks, Disk::DiskImageHolder<Storage::Disk::G64>, TargetPlatform::Commodore) // G64
Format( "hfe",
@@ -142,6 +158,7 @@ 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("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
@@ -199,14 +216,35 @@ Media Analyser::Static::GetMedia(const std::string &file_name) {
TargetList Analyser::Static::GetTargets(const std::string &file_name) {
TargetList targets;
const std::string extension = get_extension(file_name);
// Check whether the file directly identifies a target; if so then just return that.
#define Format(ext, class) \
if(extension == ext) { \
try { \
auto target = Storage::State::class::load(file_name); \
if(target) { \
targets.push_back(std::move(target)); \
return targets; \
} \
} catch(...) {} \
}
Format("sna", SNA);
Format("szx", SZX);
Format("z80", Z80);
#undef TryInsert
// Otherwise:
//
// Collect all disks, tapes ROMs, etc as can be extrapolated from this file, forming the
// union of all platforms this file might be a target for.
TargetPlatform::IntType potential_platforms = 0;
Media media = GetMediaAndPlatforms(file_name, potential_platforms);
// Hand off to platform-specific determination of whether these things are actually compatible and,
// if so, how to load them.
// Hand off to platform-specific determination of whether these
// things are actually compatible and, if so, how to load them.
#define Append(x) if(potential_platforms & TargetPlatform::x) {\
auto new_targets = x::GetTargets(media, file_name, potential_platforms);\
std::move(new_targets.begin(), new_targets.end(), std::back_inserter(targets));\
@@ -220,6 +258,7 @@ TargetList Analyser::Static::GetTargets(const std::string &file_name) {
Append(Coleco);
Append(Commodore);
Append(DiskII);
Append(Enterprise);
Append(Macintosh);
Append(MSX);
Append(Oric);

View File

@@ -15,6 +15,7 @@
#include "../../Storage/Disk/Disk.hpp"
#include "../../Storage/MassStorage/MassStorageDevice.hpp"
#include "../../Storage/Tape/Tape.hpp"
#include "../../Reflection/Struct.hpp"
#include <memory>
#include <string>
@@ -23,8 +24,10 @@
namespace Analyser {
namespace Static {
struct State;
/*!
A list of disks, tapes and cartridges.
A list of disks, tapes and cartridges, and possibly a state snapshot.
*/
struct Media {
std::vector<std::shared_ptr<Storage::Disk::Disk>> disks;
@@ -48,13 +51,16 @@ struct Media {
};
/*!
A list of disks, tapes and cartridges plus information about the machine to which to attach them and its configuration,
and instructions on how to launch the software attached, plus a measure of confidence in this target's correctness.
Describes a machine and possibly its state; conventionally subclassed to add other machine-specific configuration fields and any
necessary instructions on how to launch any software provided, plus a measure of confidence in this target's correctness.
*/
struct Target {
Target(Machine machine) : machine(machine) {}
virtual ~Target() {}
// This field is entirely optional.
std::unique_ptr<Reflection::Struct> state;
Machine machine;
Media media;
float confidence = 0.0f;

View File

@@ -19,11 +19,15 @@ namespace ZXSpectrum {
struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<Target> {
ReflectableEnum(Model,
SixteenK,
FortyEightK,
OneTwoEightK,
Plus2,
Plus2a,
Plus3,
);
Model model = Model::Plus2a;
Model model = Model::Plus2;
bool should_hold_enter = false;
Target(): Analyser::Static::Target(Machine::ZXSpectrum) {

View File

@@ -10,7 +10,9 @@
#define ClockReceiver_hpp
#include "ForceInline.hpp"
#include <cstdint>
#include <limits>
/*
Informal pattern for all classes that run from a clock cycle:
@@ -176,6 +178,9 @@ class Cycles: public WrappedInt<Cycles> {
public:
forceinline constexpr Cycles(IntType l) noexcept : WrappedInt<Cycles>(l) {}
forceinline constexpr Cycles() noexcept : WrappedInt<Cycles>() {}
forceinline static constexpr Cycles max() {
return Cycles(std::numeric_limits<IntType>::max());
}
private:
friend WrappedInt;
@@ -195,6 +200,9 @@ class HalfCycles: public WrappedInt<HalfCycles> {
public:
forceinline constexpr HalfCycles(IntType l) noexcept : WrappedInt<HalfCycles>(l) {}
forceinline constexpr HalfCycles() noexcept : WrappedInt<HalfCycles>() {}
forceinline static constexpr HalfCycles max() {
return HalfCycles(std::numeric_limits<IntType>::max());
}
forceinline constexpr HalfCycles(const Cycles &cycles) noexcept : WrappedInt<HalfCycles>(cycles.as_integral() * 2) {}
@@ -205,7 +213,7 @@ class HalfCycles: public WrappedInt<HalfCycles> {
/*!
Severs from @c this the effect of dividing by @c divisor; @c this will end up with
the value of @c this modulo @c divisor and @c divided by @c divisor is returned.
the value of @c this modulo @c divisor . @c this divided by @c divisor is returned.
*/
forceinline Cycles divide_cycles(const Cycles &divisor) {
const HalfCycles half_divisor = HalfCycles(divisor);

View File

@@ -10,6 +10,7 @@
#define JustInTime_h
#include "../Concurrency/AsyncTaskQueue.hpp"
#include "ClockingHintSource.hpp"
#include "ForceInline.hpp"
/*!
@@ -24,29 +25,69 @@
If the held object implements get_next_sequence_point() then it'll be used to flush implicitly
as and when sequence points are hit. Callers can use will_flush() to predict these.
If the held object is a subclass of ClockingHint::Source, this template will register as an
observer and potentially stop clocking or stop delaying clocking until just-in-time references
as directed.
TODO: incorporate and codify AsyncJustInTimeActor.
*/
template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class JustInTimeActor {
template <class T, class LocalTimeScale = HalfCycles, int multiplier = 1, int divider = 1> class JustInTimeActor:
public ClockingHint::Observer {
private:
/*!
A std::unique_ptr deleter which causes an update_sequence_point to occur on the actor supplied
to it at construction if it implements get_next_sequence_point(). Otherwise destruction is a no-op.
**Does not delete the object.**
This is used by the -> operators below, which provide a unique pointer to the enclosed object and
update their sequence points upon its destruction — i.e. after the caller has made whatever call
or calls as were relevant to the enclosed object.
*/
class SequencePointAwareDeleter {
public:
explicit SequencePointAwareDeleter(JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *actor) : actor_(actor) {}
explicit SequencePointAwareDeleter(JustInTimeActor<T, LocalTimeScale, multiplier, divider> *actor) noexcept
: actor_(actor) {}
void operator ()(const T *const) const {
forceinline void operator ()(const T *const) const {
if constexpr (has_sequence_points<T>::value) {
actor_->update_sequence_point();
}
}
private:
JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *const actor_;
JustInTimeActor<T, LocalTimeScale, multiplier, divider> *const actor_;
};
// This block of SFINAE determines whether objects of type T accepts Cycles or HalfCycles.
using HalfRunFor = void (T::*const)(HalfCycles);
static uint8_t half_sig(...);
static uint16_t half_sig(HalfRunFor);
using TargetTimeScale =
std::conditional_t<
sizeof(half_sig(&T::run_for)) == sizeof(uint16_t),
HalfCycles,
Cycles>;
public:
/// Constructs a new JustInTimeActor using the same construction arguments as the included object.
template<typename... Args> JustInTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
template<typename... Args> JustInTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {
if constexpr (std::is_base_of<ClockingHint::Source, T>::value) {
object_.set_clocking_hint_observer(this);
}
}
/// Adds time to the actor.
forceinline void operator += (LocalTimeScale rhs) {
///
/// @returns @c true if adding time caused a flush; @c false otherwise.
forceinline bool operator += (LocalTimeScale rhs) {
if constexpr (std::is_base_of<ClockingHint::Source, T>::value) {
if(clocking_preference_ == ClockingHint::Preference::None) {
return false;
}
}
if constexpr (multiplier != 1) {
time_since_update_ += rhs * multiplier;
} else {
@@ -54,20 +95,31 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
}
is_flushed_ = false;
if constexpr (has_sequence_points<T>::value) {
time_until_event_ -= rhs;
if(time_until_event_ <= LocalTimeScale(0)) {
if constexpr (std::is_base_of<ClockingHint::Source, T>::value) {
if (clocking_preference_ == ClockingHint::Preference::RealTime) {
flush();
update_sequence_point();
return true;
}
}
if constexpr (has_sequence_points<T>::value) {
time_until_event_ -= rhs * multiplier;
if(time_until_event_ <= LocalTimeScale(0)) {
time_overrun_ = time_until_event_ / divider;
flush();
update_sequence_point();
return true;
}
}
return false;
}
/// Flushes all accumulated time and returns a pointer to the included object.
///
/// If this object provides sequence points, checks for changes to the next
/// sequence point upon deletion of the pointer.
forceinline auto operator->() {
[[nodiscard]] forceinline auto operator->() {
flush();
return std::unique_ptr<T, SequencePointAwareDeleter>(&object_, SequencePointAwareDeleter(this));
}
@@ -75,26 +127,39 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
/// Acts exactly as per the standard ->, but preserves constness.
///
/// Despite being const, this will flush the object and, if relevant, update the next sequence point.
forceinline auto operator -> () const {
auto non_const_this = const_cast<JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *>(this);
[[nodiscard]] forceinline auto operator -> () const {
auto non_const_this = const_cast<JustInTimeActor<T, LocalTimeScale, multiplier, divider> *>(this);
non_const_this->flush();
return std::unique_ptr<const T, SequencePointAwareDeleter>(&object_, SequencePointAwareDeleter(non_const_this));
}
/// @returns a pointer to the included object, without flushing time.
forceinline T *last_valid() {
[[nodiscard]] forceinline T *last_valid() {
return &object_;
}
/// @returns a const pointer to the included object, without flushing time.
[[nodiscard]] forceinline const T *last_valid() const {
return &object_;
}
/// @returns the amount of time since the object was last flushed, in the target time scale.
forceinline TargetTimeScale time_since_flush() const {
// TODO: does this handle conversions properly where TargetTimeScale != LocalTimeScale?
[[nodiscard]] forceinline TargetTimeScale time_since_flush() const {
if constexpr (divider == 1) {
return time_since_update_;
}
return TargetTimeScale(time_since_update_.as_integral() / divider);
}
/// @returns the amount of time since the object was last flushed, plus the local time scale @c offset,
/// converted to the target time scale.
[[nodiscard]] forceinline TargetTimeScale time_since_flush(LocalTimeScale offset) const {
if constexpr (divider == 1) {
return time_since_update_ + offset;
}
return TargetTimeScale((time_since_update_ + offset).as_integral() / divider);
}
/// Flushes all accumulated time.
///
/// This does not affect this actor's record of when the next sequence point will occur.
@@ -113,88 +178,95 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
}
/// Indicates whether a flush has occurred since the last call to did_flush().
forceinline bool did_flush() {
[[nodiscard]] forceinline bool did_flush() {
const bool did_flush = did_flush_;
did_flush_ = false;
return did_flush;
}
/// @returns a number in the range [-max, 0] indicating the offset of the most recent sequence
/// point from the final time at the end of the += that triggered the sequence point.
[[nodiscard]] forceinline LocalTimeScale last_sequence_point_overrun() {
return time_overrun_;
}
/// @returns the number of cycles until the next sequence-point-based flush, if the embedded object
/// supports sequence points; @c LocalTimeScale() otherwise.
LocalTimeScale cycles_until_implicit_flush() const {
return time_until_event_;
[[nodiscard]] LocalTimeScale cycles_until_implicit_flush() const {
return time_until_event_ / divider;
}
/// Indicates whether a sequence-point-caused flush will occur if the specified period is added.
forceinline bool will_flush(LocalTimeScale rhs) const {
[[nodiscard]] forceinline bool will_flush(LocalTimeScale rhs) const {
if constexpr (!has_sequence_points<T>::value) {
return false;
}
return rhs >= time_until_event_;
}
/// Indicates the amount of time, in the local time scale, until the first local slot that falls wholly
/// after @c duration, if that delay were to occur in @c offset units of time from now.
[[nodiscard]] forceinline LocalTimeScale back_map(TargetTimeScale duration, TargetTimeScale offset) const {
// A 1:1 mapping is easy.
if constexpr (multiplier == 1 && divider == 1) {
return duration;
}
// Work out when this query is placed, and the time to which it relates
const auto base = time_since_update_ + offset * divider;
const auto target = base + duration * divider;
// Figure out the number of whole input steps that is required to get
// past target, and subtract the number of whole input steps necessary
// to get to base.
const auto steps_to_base = base.as_integral() / multiplier;
const auto steps_to_target = (target.as_integral() + divider - 1) / multiplier;
return LocalTimeScale(steps_to_target - steps_to_base);
}
/// Updates this template's record of the next sequence point.
void update_sequence_point() {
if constexpr (has_sequence_points<T>::value) {
time_until_event_ = object_.get_next_sequence_point();
// Keep a fast path where no conversions will be applied; if conversions are
// going to be applied then do a direct max -> max translation rather than
// allowing the arithmetic to overflow.
if constexpr (divider == 1 && std::is_same_v<LocalTimeScale, TargetTimeScale>) {
time_until_event_ = object_.get_next_sequence_point();
} else {
const auto time = object_.get_next_sequence_point();
if(time == TargetTimeScale::max()) {
time_until_event_ = LocalTimeScale::max();
} else {
time_until_event_ = time * divider;
}
}
assert(time_until_event_ > LocalTimeScale(0));
}
}
/// @returns A cached copy of the object's clocking preference.
ClockingHint::Preference clocking_preference() const {
return clocking_preference_;
}
private:
T object_;
LocalTimeScale time_since_update_, time_until_event_;
LocalTimeScale time_since_update_, time_until_event_, time_overrun_;
bool is_flushed_ = true;
bool did_flush_ = false;
template <typename S, typename = void> struct has_sequence_points : std::false_type {};
template <typename S> struct has_sequence_points<S, decltype(void(std::declval<S &>().get_next_sequence_point()))> : std::true_type {};
};
/*!
A RealTimeActor presents the same interface as a JustInTimeActor but doesn't defer work.
Time added will be performed immediately.
Its primary purpose is to allow consumers to remain flexible in their scheduling.
*/
template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class RealTimeActor {
public:
template<typename... Args> RealTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
forceinline void operator += (const LocalTimeScale &rhs) {
if constexpr (multiplier == 1 && divider == 1) {
object_.run_for(TargetTimeScale(rhs));
return;
}
if constexpr (multiplier == 1) {
accumulated_time_ += rhs;
} else {
accumulated_time_ += rhs * multiplier;
}
if constexpr (divider == 1) {
const auto duration = accumulated_time_.template flush<TargetTimeScale>();
object_.run_for(duration);
} else {
const auto duration = accumulated_time_.template divide<TargetTimeScale>(LocalTimeScale(divider));
if(duration > TargetTimeScale(0))
object_.run_for(duration);
}
ClockingHint::Preference clocking_preference_ = ClockingHint::Preference::JustInTime;
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference clocking) {
clocking_preference_ = clocking;
}
forceinline T *operator->() { return &object_; }
forceinline const T *operator->() const { return &object_; }
forceinline T *last_valid() { return &object_; }
forceinline void flush() {}
private:
T object_;
LocalTimeScale accumulated_time_;
};
/*!
A AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue.
An AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue.
Any time the amount of accumulated time crosses a threshold provided at construction time,
the object will be updated on the AsyncTaskQueue.
*/

View File

@@ -276,7 +276,10 @@ void WD1770::posit_event(int new_event_type) {
goto test_type1_type;
begin_type1_spin_up:
if((command_&0x08) || get_drive().get_motor_on()) goto test_type1_type;
if((command_&0x08) || get_drive().get_motor_on()) {
set_motor_on(true);
goto test_type1_type;
}
SPIN_UP();
test_type1_type:
@@ -387,7 +390,10 @@ void WD1770::posit_event(int new_event_type) {
distance_into_section_ = 0;
if((command_&0x08) && has_motor_on_line()) goto test_type2_delay;
if(!has_motor_on_line() && !has_head_load_line()) goto test_type2_delay;
if(!has_motor_on_line() && !has_head_load_line()) {
if(has_motor_on_line()) set_motor_on(true);
goto test_type2_delay;
}
if(has_motor_on_line()) goto begin_type2_spin_up;
goto begin_type2_load_head;

View File

@@ -129,8 +129,8 @@ ClockingHint::Preference ACIA::preferred_clocking() const {
// because it's unclear when the interrupt might come.
if(bits_incoming_ && receive_interrupt_enabled_) return ClockingHint::Preference::RealTime;
// No clocking required then.
return ClockingHint::Preference::None;
// Real-time clocking not required then.
return ClockingHint::Preference::JustInTime;
}
bool ACIA::get_interrupt_line() const {

View File

@@ -208,7 +208,7 @@ void MFP68901::run_for(HalfCycles time) {
}
HalfCycles MFP68901::get_next_sequence_point() {
return HalfCycles(-1);
return HalfCycles::max();
}
// MARK: - Timers

View File

@@ -708,9 +708,9 @@ HalfCycles Base::half_cycles_before_internal_cycles(int internal_cycles) {
return HalfCycles(((internal_cycles << 2) + (2 - cycles_error_)) / 3);
}
HalfCycles TMS9918::get_time_until_interrupt() {
if(!generate_interrupts_ && !enable_line_interrupts_) return HalfCycles(-1);
if(get_interrupt_line()) return HalfCycles(0);
HalfCycles TMS9918::get_next_sequence_point() {
if(!generate_interrupts_ && !enable_line_interrupts_) return HalfCycles::max();
if(get_interrupt_line()) return HalfCycles::max();
// Calculate the amount of time until the next end-of-frame interrupt.
const int frame_length = 342 * mode_timing_.total_lines;
@@ -750,7 +750,7 @@ HalfCycles TMS9918::get_time_until_interrupt() {
if(next_line_interrupt_row == -1) {
return generate_interrupts_ ?
half_cycles_before_internal_cycles(time_until_frame_interrupt) :
HalfCycles(-1);
HalfCycles::max();
}
// Figure out the number of internal cycles until the next line interrupt, which is the amount

View File

@@ -75,13 +75,13 @@ class TMS9918: public Base {
void latch_horizontal_counter();
/*!
Returns the amount of time until @c get_interrupt_line would next return true if
Returns the amount of time until @c get_interrupt_line would next change if
there are no interceding calls to @c write or to @c read.
If get_interrupt_line is true now, returns zero. If get_interrupt_line would
never return true, returns -1.
If get_interrupt_line is true now of if get_interrupt_line would
never return true, returns HalfCycles::max().
*/
HalfCycles get_time_until_interrupt();
HalfCycles get_next_sequence_point();
/*!
Returns the amount of time until the nominated line interrupt position is

View File

@@ -12,6 +12,8 @@
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
#include "../../Concurrency/AsyncTaskQueue.hpp"
#include "../../Reflection/Struct.hpp"
namespace GI {
namespace AY38910 {
@@ -162,6 +164,8 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
uint8_t a_left_ = 255, a_right_ = 255;
uint8_t b_left_ = 255, b_right_ = 255;
uint8_t c_left_ = 255, c_right_ = 255;
friend struct State;
};
/*!
@@ -169,19 +173,21 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
AY-deploying machines of the era.
*/
struct Utility {
template <typename AY> static void select_register(AY &ay, uint8_t reg) {
ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2 | GI::AY38910::BC1));
ay.set_data_input(reg);
template <typename AY> static void write(AY &ay, bool is_data_write, uint8_t data) {
ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2 | (is_data_write ? 0 : GI::AY38910::BC1)));
ay.set_data_input(data);
ay.set_control_lines(GI::AY38910::ControlLines(0));
}
template <typename AY> static void select_register(AY &ay, uint8_t reg) {
write(ay, false, reg);
}
template <typename AY> static void write_data(AY &ay, uint8_t reg) {
ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2));
ay.set_data_input(reg);
ay.set_control_lines(GI::AY38910::ControlLines(0));
write(ay, true, reg);
}
template <typename AY> static uint8_t read_data(AY &ay) {
template <typename AY> static uint8_t read(AY &ay) {
ay.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
const uint8_t result = ay.get_data_output();
ay.set_control_lines(GI::AY38910::ControlLines(0));
@@ -190,6 +196,29 @@ struct Utility {
};
struct State: public Reflection::StructImpl<State> {
uint8_t registers[16]{};
uint8_t selected_register = 0;
// TODO: all audio-production thread state.
State() {
if(needs_declare()) {
DeclareField(registers);
DeclareField(selected_register);
}
}
template <typename AY> void apply(AY &target) {
// Establish emulator-thread state
for(uint8_t c = 0; c < 16; c++) {
target.select_register(c);
target.set_register_value(registers[c]);
}
target.select_register(selected_register);
}
};
}
}

View File

@@ -11,11 +11,11 @@
using namespace Concurrency;
AsyncTaskQueue::AsyncTaskQueue()
#ifndef __APPLE__
#ifndef USE_GCD
: should_destruct_(false)
#endif
{
#ifdef __APPLE__
#ifdef USE_GCD
serial_dispatch_queue_ = dispatch_queue_create("com.thomasharte.clocksignal.asyntaskqueue", DISPATCH_QUEUE_SERIAL);
#else
thread_ = std::make_unique<std::thread>([this]() {
@@ -44,7 +44,7 @@ AsyncTaskQueue::AsyncTaskQueue()
}
AsyncTaskQueue::~AsyncTaskQueue() {
#ifdef __APPLE__
#ifdef USE_GCD
flush();
dispatch_release(serial_dispatch_queue_);
serial_dispatch_queue_ = nullptr;
@@ -57,7 +57,7 @@ AsyncTaskQueue::~AsyncTaskQueue() {
}
void AsyncTaskQueue::enqueue(std::function<void(void)> function) {
#ifdef __APPLE__
#ifdef USE_GCD
dispatch_async(serial_dispatch_queue_, ^{function();});
#else
std::lock_guard lock(queue_mutex_);
@@ -67,7 +67,7 @@ void AsyncTaskQueue::enqueue(std::function<void(void)> function) {
}
void AsyncTaskQueue::flush() {
#ifdef __APPLE__
#ifdef USE_GCD
dispatch_sync(serial_dispatch_queue_, ^{});
#else
auto flush_mutex = std::make_shared<std::mutex>();

View File

@@ -16,8 +16,9 @@
#include <memory>
#include <thread>
#ifdef __APPLE__
#if defined(__APPLE__) && !defined(IGNORE_APPLE)
#include <dispatch/dispatch.h>
#define USE_GCD
#endif
namespace Concurrency {
@@ -47,7 +48,7 @@ class AsyncTaskQueue {
void flush();
private:
#ifdef __APPLE__
#ifdef USE_GCD
dispatch_queue_t serial_dispatch_queue_;
#else
std::unique_ptr<std::thread> thread_;

View File

@@ -9,6 +9,7 @@
#ifndef Joystick_hpp
#define Joystick_hpp
#include <cstddef>
#include <vector>
namespace Inputs {

View File

@@ -8,6 +8,8 @@
#include "Keyboard.hpp"
#include <cstddef>
using namespace Inputs;
Keyboard::Keyboard(const std::set<Key> &essential_modifiers) : essential_modifiers_(essential_modifiers) {

View File

@@ -9,7 +9,7 @@
#ifndef CachingExecutor_hpp
#define CachingExecutor_hpp
#include "Sizes.hpp"
#include "../Numeric/Sizes.hpp"
#include <array>
#include <cstdint>

View File

@@ -9,7 +9,7 @@
#ifndef Disassembler_hpp
#define Disassembler_hpp
#include "Sizes.hpp"
#include "../Numeric/Sizes.hpp"
#include <list>
#include <map>

View File

@@ -787,45 +787,43 @@ template <bool has_fdc> class ConcreteMachine:
ay_.ay().set_port_handler(&key_state_);
// construct the list of necessary ROMs
const std::string machine_name = "AmstradCPC";
std::vector<ROMMachine::ROM> required_roms = {
ROMMachine::ROM(machine_name, "the Amstrad Disk Operating System", "amsdos.rom", 16*1024, 0x1fe22ecd)
};
std::string model_number;
uint32_t crcs[2];
bool has_amsdos = false;
ROM::Name firmware, basic;
switch(target.model) {
default:
model_number = "6128";
has_128k_ = true;
crcs[0] = 0x0219bb74;
crcs[1] = 0xca6af63d;
break;
case Analyser::Static::AmstradCPC::Target::Model::CPC464:
model_number = "464";
has_128k_ = false;
crcs[0] = 0x815752df;
crcs[1] = 0x7d9a3bac;
firmware = ROM::Name::CPC464Firmware;
basic = ROM::Name::CPC464BASIC;
break;
case Analyser::Static::AmstradCPC::Target::Model::CPC664:
model_number = "664";
has_128k_ = false;
crcs[0] = 0x3f5a6dc4;
crcs[1] = 0x32fee492;
firmware = ROM::Name::CPC664Firmware;
basic = ROM::Name::CPC664BASIC;
has_amsdos = true;
break;
default:
firmware = ROM::Name::CPC6128Firmware;
basic = ROM::Name::CPC6128BASIC;
has_amsdos = true;
break;
}
required_roms.emplace_back(machine_name, "the CPC " + model_number + " firmware", "os" + model_number + ".rom", 16*1024, crcs[0]);
required_roms.emplace_back(machine_name, "the CPC " + model_number + " BASIC ROM", "basic" + model_number + ".rom", 16*1024, crcs[1]);
// fetch and verify the ROMs
const auto roms = rom_fetcher(required_roms);
for(std::size_t index = 0; index < roms.size(); ++index) {
auto &data = roms[index];
if(!data) throw ROMMachine::Error::MissingROMs;
roms_[index] = std::move(*data);
roms_[index].resize(16384);
ROM::Request request = ROM::Request(firmware) && ROM::Request(basic);
if(has_amsdos) {
request = request && ROM::Request(ROM::Name::AMSDOS);
}
// Fetch and verify the ROMs.
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
if(has_amsdos) {
roms_[ROMType::AMSDOS] = roms.find(ROM::Name::AMSDOS)->second;
}
roms_[ROMType::OS] = roms.find(firmware)->second;
roms_[ROMType::BASIC] = roms.find(basic)->second;
// Establish default memory map
upper_rom_is_paged_ = true;
upper_rom_ = ROMType::BASIC;
@@ -1088,6 +1086,7 @@ template <bool has_fdc> class ConcreteMachine:
// If there are any tapes supplied, use the first of them.
if(!media.tapes.empty()) {
tape_player_.set_tape(media.tapes.front());
set_use_fast_tape_hack();
}
// Insert up to four disks.

View File

@@ -151,7 +151,7 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
#undef SHIFT
#undef X
return table_lookup_sequence_for_character(key_sequences, sizeof(key_sequences), character);
return table_lookup_sequence_for_character(key_sequences, character);
}
bool CharacterMapper::needs_pause_after_key(uint16_t key) const {

View File

@@ -16,6 +16,7 @@ void Keyboard::perform_command(const Command &command) {
switch(command.type) {
case Command::Type::Reset:
modifiers_ = 0xffff;
[[fallthrough]];
case Command::Type::Flush: {
std::lock_guard lock_guard(keys_mutex_);
pending_events_.clear();

View File

@@ -48,7 +48,6 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
public MachineTypes::MappedKeyboardMachine,
public MachineTypes::JoystickMachine,
public CPU::MOS6502::BusHandler,
public Inputs::Keyboard,
public Configurable::Device,
public Activity::Source,
public Apple::II::Card::Delegate {
@@ -66,7 +65,11 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
uint8_t *ram_, *aux_ram_;
};
CPU::MOS6502::Processor<(model == Analyser::Static::AppleII::Target::Model::EnhancedIIe) ? CPU::MOS6502::Personality::PSynertek65C02 : CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
using Processor = CPU::MOS6502::Processor<
(model == Analyser::Static::AppleII::Target::Model::EnhancedIIe) ? CPU::MOS6502::Personality::PSynertek65C02 : CPU::MOS6502::Personality::P6502,
ConcreteMachine,
false>;
Processor m6502_;
VideoBusHandler video_bus_handler_;
Apple::II::Video::Video<VideoBusHandler, is_iie()> video_;
int cycles_into_current_line_ = 0;
@@ -91,16 +94,6 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
uint8_t ram_[65536], aux_ram_[65536];
std::vector<uint8_t> rom_;
uint8_t keyboard_input_ = 0x00;
bool key_is_down_ = false;
uint8_t get_keyboard_input() {
if(string_serialiser_) {
return string_serialiser_->head() | 0x80;
} else {
return keyboard_input_;
}
}
Concurrency::DeferringAsyncTaskQueue audio_queue_;
Audio::Toggle audio_toggle_;
@@ -258,16 +251,99 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
state.region_20_40.write ? &aux_ram_[0x2000] : &ram_[0x2000]);
}
// MARK - typing
std::unique_ptr<Utility::StringSerialiser> string_serialiser_;
// MARK: - Keyboard and typing.
// MARK - Joysticks.
struct Keyboard: public Inputs::Keyboard {
Keyboard(Processor *m6502) : m6502_(m6502) {}
void reset_all_keys() final {
open_apple_is_pressed = closed_apple_is_pressed = key_is_down = false;
}
bool set_key_pressed(Key key, char value, bool is_pressed) final {
// If no ASCII value is supplied, look for a few special cases.
switch(key) {
case Key::Left: value = 0x08; break;
case Key::Right: value = 0x15; break;
case Key::Down: value = 0x0a; break;
case Key::Up: value = 0x0b; break;
case Key::Backspace: value = 0x7f; break;
case Key::Enter: value = 0x0d; break;
case Key::Tab: value = '\t'; break;
case Key::Escape: value = 0x1b; break;
case Key::LeftOption:
case Key::RightMeta:
open_apple_is_pressed = is_pressed;
return true;
case Key::RightOption:
case Key::LeftMeta:
closed_apple_is_pressed = is_pressed;
return true;
case Key::F1: case Key::F2: case Key::F3: case Key::F4:
case Key::F5: case Key::F6: case Key::F7: case Key::F8:
case Key::F9: case Key::F10: case Key::F11: case Key::F12:
case Key::PrintScreen:
case Key::ScrollLock:
case Key::Pause:
case Key::Insert:
case Key::Home:
case Key::PageUp:
case Key::PageDown:
case Key::End:
// Accept a bunch non-symbolic other keys, as
// reset, in the hope that the user can find
// at least one usable key.
m6502_->set_reset_line(is_pressed);
return true;
default:
if(!value) {
return false;
}
// Prior to the IIe, the keyboard could produce uppercase only.
if(!is_iie()) value = char(toupper(value));
break;
}
if(is_pressed) {
keyboard_input = uint8_t(value | 0x80);
key_is_down = true;
} else {
if((keyboard_input & 0x7f) == value) {
key_is_down = false;
}
}
return true;
}
uint8_t get_keyboard_input() {
if(string_serialiser) {
return string_serialiser->head() | 0x80;
} else {
return keyboard_input;
}
}
// The IIe has three keys that are wired directly to the same input as the joystick buttons.
bool open_apple_is_pressed = false;
bool closed_apple_is_pressed = false;
uint8_t keyboard_input = 0x00;
bool key_is_down = false;
std::unique_ptr<Utility::StringSerialiser> string_serialiser;
private:
Processor *const m6502_;
};
Keyboard keyboard_;
// MARK: - Joysticks.
JoystickPair joysticks_;
// The IIe has three keys that are wired directly to the same input as the joystick buttons.
bool open_apple_is_pressed_ = false;
bool closed_apple_is_pressed_ = false;
public:
ConcreteMachine(const Analyser::Static::AppleII::Target &target, const ROMMachine::ROMFetcher &rom_fetcher):
m6502_(*this),
@@ -276,7 +352,8 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
audio_toggle_(audio_queue_),
speaker_(audio_toggle_),
language_card_(*this),
auxiliary_switches_(*this) {
auxiliary_switches_(*this),
keyboard_(&m6502_) {
// The system's master clock rate.
constexpr float master_clock = 14318180.0;
@@ -300,49 +377,55 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
// Pick the required ROMs.
using Target = Analyser::Static::AppleII::Target;
const std::string machine_name = "AppleII";
std::vector<ROMMachine::ROM> rom_descriptions;
size_t rom_size = 12*1024;
ROM::Name character, system;
switch(target.model) {
default:
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::II));
rom_descriptions.emplace_back(machine_name, "the original Apple II ROM", "apple2o.rom", 12*1024, 0xba210588);
character = ROM::Name::AppleIICharacter;
system = ROM::Name::AppleIIOriginal;
break;
case Target::Model::IIplus:
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::II));
rom_descriptions.emplace_back(machine_name, "the Apple II+ ROM", "apple2.rom", 12*1024, 0xf66f9c26);
character = ROM::Name::AppleIICharacter;
system = ROM::Name::AppleIIPlus;
break;
case Target::Model::IIe:
rom_size += 3840;
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::IIe));
rom_descriptions.emplace_back(machine_name, "the Apple IIe ROM", "apple2eu.rom", 32*1024, 0xe12be18d);
character = ROM::Name::AppleIIeCharacter;
system = ROM::Name::AppleIIe;
break;
case Target::Model::EnhancedIIe:
rom_size += 3840;
rom_descriptions.push_back(video_.rom_description(Video::VideoBase::CharacterROM::EnhancedIIe));
rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe ROM", "apple2e.rom", 32*1024, 0x65989942);
character = ROM::Name::AppleIIEnhancedECharacter;
system = ROM::Name::AppleIIEnhancedE;
break;
}
const auto roms = rom_fetcher(rom_descriptions);
// Try to install a Disk II card now, before checking the ROM list,
// to make sure that Disk II dependencies have been communicated.
if(target.disk_controller != Target::DiskController::None) {
ROM::Request request = ROM::Request(character) && ROM::Request(system);
// Add the necessary Disk II requests if appropriate.
const bool has_disk_controller = target.disk_controller != Target::DiskController::None;
const bool is_sixteen_sector = target.disk_controller == Target::DiskController::SixteenSector;
if(has_disk_controller) {
// Apple recommended slot 6 for the (first) Disk II.
install_card(6, new Apple::II::DiskIICard(rom_fetcher, target.disk_controller == Target::DiskController::SixteenSector));
request = request && DiskIICard::rom_request(is_sixteen_sector);
}
// Now, check and move the ROMs.
if(!roms[0] || !roms[1]) {
// Request, validate and install ROMs.
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
rom_ = std::move(*roms[1]);
if(rom_.size() > rom_size) {
rom_.erase(rom_.begin(), rom_.end() - off_t(rom_size));
if(has_disk_controller) {
install_card(6, new Apple::II::DiskIICard(roms, is_sixteen_sector));
}
video_.set_character_rom(*roms[0]);
rom_ = std::move(roms.find(system)->second);
// The IIe and Enhanced IIe ROMs often distributed are oversized; trim if necessary.
if(system == ROM::Name::AppleIIe || system == ROM::Name::AppleIIEnhancedE) {
if(rom_.size() > 16128) {
rom_.erase(rom_.begin(), rom_.end() - off_t(16128));
}
}
video_.set_character_rom(roms.find(character)->second);
// Set up the default memory blocks. On a II or II+ these values will never change.
// On a IIe they'll be affected by selection of auxiliary RAM.
@@ -435,18 +518,18 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
default: break;
case 0xc000:
*value = get_keyboard_input();
*value = keyboard_.get_keyboard_input();
break;
case 0xc001: case 0xc002: case 0xc003: case 0xc004: case 0xc005: case 0xc006: case 0xc007:
case 0xc008: case 0xc009: case 0xc00a: case 0xc00b: case 0xc00c: case 0xc00d: case 0xc00e: case 0xc00f:
*value = (*value & 0x80) | (get_keyboard_input() & 0x7f);
*value = (*value & 0x80) | (keyboard_.get_keyboard_input() & 0x7f);
break;
case 0xc061: // Switch input 0.
*value &= 0x7f;
if(
joysticks_.button(0) ||
(is_iie() && open_apple_is_pressed_)
(is_iie() && keyboard_.open_apple_is_pressed)
)
*value |= 0x80;
break;
@@ -454,7 +537,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
*value &= 0x7f;
if(
joysticks_.button(1) ||
(is_iie() && closed_apple_is_pressed_)
(is_iie() && keyboard_.closed_apple_is_pressed)
)
*value |= 0x80;
break;
@@ -476,7 +559,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
} break;
// The IIe-only state reads follow...
#define IIeSwitchRead(s) *value = get_keyboard_input(); if(is_iie()) *value = (*value & 0x7f) | (s ? 0x80 : 0x00);
#define IIeSwitchRead(s) *value = keyboard_.get_keyboard_input(); if(is_iie()) *value = (*value & 0x7f) | (s ? 0x80 : 0x00);
case 0xc011: IIeSwitchRead(language_card_.state().bank2); break;
case 0xc012: IIeSwitchRead(language_card_.state().read); break;
case 0xc013: IIeSwitchRead(auxiliary_switches_.switches().read_auxiliary_memory); break;
@@ -559,15 +642,15 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
break;
case 0xc010:
keyboard_input_ &= 0x7f;
if(string_serialiser_) {
if(!string_serialiser_->advance())
string_serialiser_.reset();
keyboard_.keyboard_input &= 0x7f;
if(keyboard_.string_serialiser) {
if(!keyboard_.string_serialiser->advance())
keyboard_.string_serialiser.reset();
}
// On the IIe, reading C010 returns additional key info.
if(is_iie() && isReadOperation(operation)) {
*value = (key_is_down_ ? 0x80 : 0x00) | (keyboard_input_ & 0x7f);
*value = (keyboard_.key_is_down ? 0x80 : 0x00) | (keyboard_.keyboard_input & 0x7f);
}
break;
@@ -683,81 +766,16 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
m6502_.run_for(cycles);
}
void reset_all_keys() final {
open_apple_is_pressed_ = closed_apple_is_pressed_ = key_is_down_ = false;
}
bool prefers_logical_input() final {
return true;
}
bool set_key_pressed(Key key, char value, bool is_pressed) final {
// If no ASCII value is supplied, look for a few special cases.
switch(key) {
case Key::Left: value = 0x08; break;
case Key::Right: value = 0x15; break;
case Key::Down: value = 0x0a; break;
case Key::Up: value = 0x0b; break;
case Key::Backspace: value = 0x7f; break;
case Key::Enter: value = 0x0d; break;
case Key::Tab: value = '\t'; break;
case Key::Escape: value = 0x1b; break;
case Key::LeftOption:
case Key::RightMeta:
open_apple_is_pressed_ = is_pressed;
return true;
case Key::RightOption:
case Key::LeftMeta:
closed_apple_is_pressed_ = is_pressed;
return true;
case Key::F1: case Key::F2: case Key::F3: case Key::F4:
case Key::F5: case Key::F6: case Key::F7: case Key::F8:
case Key::F9: case Key::F10: case Key::F11: case Key::F12:
case Key::PrintScreen:
case Key::ScrollLock:
case Key::Pause:
case Key::Insert:
case Key::Home:
case Key::PageUp:
case Key::PageDown:
case Key::End:
// Accept a bunch non-symbolic other keys, as
// reset, in the hope that the user can find
// at least one usable key.
m6502_.set_reset_line(is_pressed);
return true;
default:
if(!value) {
return false;
}
// Prior to the IIe, the keyboard could produce uppercase only.
if(!is_iie()) value = char(toupper(value));
break;
}
if(is_pressed) {
keyboard_input_ = uint8_t(value | 0x80);
key_is_down_ = true;
} else {
if((keyboard_input_ & 0x7f) == value) {
key_is_down_ = false;
}
}
return true;
}
Inputs::Keyboard &get_keyboard() final {
return *this;
return keyboard_;
}
void type_string(const std::string &string) final {
string_serialiser_ = std::make_unique<Utility::StringSerialiser>(string, true);
keyboard_.string_serialiser = std::make_unique<Utility::StringSerialiser>(string, true);
}
bool can_type(char c) const final {
@@ -769,12 +787,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
std::unique_ptr<Reflection::Struct> get_options() final {
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
options->output = get_video_signal_configurable();
options->use_square_pixels = video_.get_use_square_pixels();
return options;
}
void set_options(const std::unique_ptr<Reflection::Struct> &str) {
const auto options = dynamic_cast<Options *>(str.get());
set_video_signal_configurable(options->output);
video_.set_use_square_pixels(options->use_square_pixels);
}
// MARK: MediaTarget

View File

@@ -30,8 +30,11 @@ class Machine {
class Options: public Reflection::StructImpl<Options>, public Configurable::DisplayOption<Options> {
friend Configurable::DisplayOption<Options>;
public:
Options(Configurable::OptionsType) : Configurable::DisplayOption<Options>(Configurable::Display::CompositeColour) {
bool use_square_pixels = false;
Options(Configurable::OptionsType) : Configurable::DisplayOption<Options>(Configurable::Display::CompositeColour) {
if(needs_declare()) {
DeclareField(use_square_pixels);
declare_display_option();
limit_enum(&output, Configurable::Display::CompositeMonochrome, Configurable::Display::CompositeColour, -1);
}

View File

@@ -10,27 +10,34 @@
using namespace Apple::II;
DiskIICard::DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector) : diskii_(2045454) {
std::vector<std::unique_ptr<std::vector<uint8_t>>> roms;
ROM::Request DiskIICard::rom_request(bool is_16_sector) {
if(is_16_sector) {
roms = rom_fetcher({
{"DiskII", "the Disk II 16-sector boot ROM", "boot-16.rom", 256, 0xce7144f6},
{"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 } }
});
return ROM::Request(ROM::Name::DiskIIBoot16Sector) && ROM::Request(ROM::Name::DiskIIStateMachine16Sector);
} else {
roms = rom_fetcher({
{"DiskII", "the Disk II 13-sector boot ROM", "boot-13.rom", 256, 0xd34eb2ff},
{"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 } }
// {"DiskII", "the Disk II 13-sector state machine ROM", "state-machine-13.rom", 256, 0x62e22620 }
/* TODO: once the DiskII knows how to decode common images of the 13-sector state machine, use that instead of the 16-sector. */
});
/* TODO: once the DiskII knows how to decode common images of the 13-sector state machine, use that instead of the 16-sector. */
return ROM::Request(ROM::Name::DiskIIBoot13Sector) && ROM::Request(ROM::Name::DiskIIStateMachine16Sector);
}
if(!roms[0] || !roms[1]) {
}
DiskIICard::DiskIICard(ROM::Map &map, bool is_16_sector) : diskii_(2045454) {
std::vector<std::unique_ptr<std::vector<uint8_t>>> roms;
ROM::Map::iterator state_machine, boot;
if(is_16_sector) {
state_machine = map.find(ROM::Name::DiskIIStateMachine16Sector);
boot = map.find(ROM::Name::DiskIIBoot16Sector);
} else {
// TODO: see above re: 13-sector state machine.
state_machine = map.find(ROM::Name::DiskIIStateMachine16Sector);
boot = map.find(ROM::Name::DiskIIBoot13Sector);
}
if(state_machine == map.end() || boot == map.end()) {
throw ROMMachine::Error::MissingROMs;
}
boot_ = std::move(*roms[0]);
diskii_.set_state_machine(*roms[1]);
boot_ = std::move(boot->second);
diskii_.set_state_machine(state_machine->second);
set_select_constraints(None);
diskii_.set_clocking_hint_observer(this);
}

View File

@@ -25,7 +25,8 @@ namespace II {
class DiskIICard: public Card, public ClockingHint::Observer {
public:
DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector);
static ROM::Request rom_request(bool is_16_sector);
DiskIICard(ROM::Map &, bool is_16_sector);
void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) final;
void run_for(Cycles cycles, int stretches) final;

View File

@@ -15,9 +15,8 @@ VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) :
crt_(910, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance1),
is_iie_(is_iie) {
// Show only the centre 75% of the TV frame.
crt_.set_display_type(Outputs::Display::DisplayType::CompositeColour);
crt_.set_visible_area(Outputs::Display::Rect(0.118f, 0.122f, 0.77f, 0.77f));
set_use_square_pixels(use_square_pixels_);
// TODO: there seems to be some sort of bug whereby switching modes can cause
// a signal discontinuity that knocks phase out of whack. So it isn't safe to
@@ -26,6 +25,41 @@ VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) :
// crt_.set_immediate_default_phase(0.5f);
}
void VideoBase::set_use_square_pixels(bool use_square_pixels) {
use_square_pixels_ = use_square_pixels;
// HYPER-UGLY HACK. See correlated hack in the Macintosh.
#if defined(__APPLE__) && !defined(IGNORE_APPLE)
crt_.set_visible_area(Outputs::Display::Rect(0.128f, 0.122f, 0.75f, 0.77f));
#else
if(use_square_pixels) {
crt_.set_visible_area(Outputs::Display::Rect(0.128f, 0.09f, 0.75f, 0.77f));
} else {
crt_.set_visible_area(Outputs::Display::Rect(0.128f, 0.12f, 0.75f, 0.77f));
}
#endif
if(use_square_pixels) {
// From what I can make out, many contemporary Apple II monitors were
// calibrated slightly to stretch the Apple II's display slightly wider
// than it should be per the NTSC standards, for approximately square
// pixels. This reproduces that.
// 243 lines and 52µs are visible.
// i.e. to be square, 1 pixel should be: (1/243 * 52) * (3/4) = 156/972 = 39/243 µs
// On an Apple II each pixel is actually 1/7µs.
// Therefore the adjusted aspect ratio should be (4/3) * (39/243)/(1/7) = (4/3) * 273/243 = 1092/729 = 343/243 ~= 1.412
crt_.set_aspect_ratio(343.0f / 243.0f);
} else {
// Standard NTSC aspect ratio.
crt_.set_aspect_ratio(4.0f / 3.0f);
}
}
bool VideoBase::get_use_square_pixels() {
return use_square_pixels_;
}
void VideoBase::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}

View File

@@ -51,8 +51,14 @@ class VideoBase: public VideoSwitches<Cycles> {
/// Gets the type of output.
Outputs::Display::DisplayType get_display_type() const;
/// Sets whether the current CRT should be recalibrated away from normative NTSC
/// to produce square pixels in 40-column text mode.
void set_use_square_pixels(bool);
bool get_use_square_pixels();
protected:
Outputs::CRT::CRT crt_;
bool use_square_pixels_ = false;
// State affecting output video stream generation.
uint8_t *pixel_pointer_ = nullptr;
@@ -393,7 +399,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
// The OpenGL scan target introduces a phase error of 1/8th of a wave. The Metal one does not.
// Supply the real phase value if this is an Apple build.
// TODO: eliminate UGLY HACK.
#ifdef __APPLE__
#if defined(__APPLE__) && !defined(IGNORE_APPLE)
constexpr int phase = 224;
#else
constexpr int phase = 0;

View File

@@ -228,36 +228,6 @@ template <typename TimeUnit> class VideoSwitches {
return external_.annunciator_3;
}
enum class CharacterROM {
/// The ROM that shipped with both the Apple II and the II+.
II,
/// The ROM that shipped with the original IIe.
IIe,
/// The ROM that shipped with the Enhanced IIe.
EnhancedIIe,
/// The ROM that shipped with the IIgs.
IIgs
};
/// @returns A file-level description of @c rom.
static ROMMachine::ROM rom_description(CharacterROM rom) {
const std::string machine_name = "AppleII";
switch(rom) {
case CharacterROM::II:
return ROMMachine::ROM(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6);
case CharacterROM::IIe:
return ROMMachine::ROM(machine_name, "the Apple IIe character ROM", "apple2eu-character.rom", 4*1024, 0x816a86f1);
default: // To appease GCC.
case CharacterROM::EnhancedIIe:
return ROMMachine::ROM(machine_name, "the Enhanced Apple IIe character ROM", "apple2e-character.rom", 4*1024, 0x2651014d);
case CharacterROM::IIgs:
return ROMMachine::ROM(machine_name, "the Apple IIgs character ROM", "apple2gs.chr", 4*1024, 0x91e53cd8);
}
}
/// Set the character ROM for this video output.
void set_character_rom(const std::vector<uint8_t> &rom) {
character_rom_ = rom;

View File

@@ -189,31 +189,23 @@ class ConcreteMachine:
speaker_.set_input_rate(float(CLOCK_RATE) / float(audio_divider));
using Target = Analyser::Static::AppleIIgs::Target;
std::vector<ROMMachine::ROM> rom_descriptions;
const std::string machine_name = "AppleIIgs";
ROM::Name system;
switch(target.model) {
case Target::Model::ROM00:
/* TODO */
case Target::Model::ROM01:
rom_descriptions.emplace_back(machine_name, "the Apple IIgs ROM01", "apple2gs.rom", 128*1024, 0x42f124b0);
break;
case Target::Model::ROM03:
rom_descriptions.emplace_back(machine_name, "the Apple IIgs ROM03", "apple2gs.rom2", 256*1024, 0xde7ddf29);
break;
case Target::Model::ROM00: system = ROM::Name::AppleIIgsROM00; break;
case Target::Model::ROM01: system = ROM::Name::AppleIIgsROM01; break;
default: system = ROM::Name::AppleIIgsROM03; break;
}
rom_descriptions.push_back(video_->rom_description(Video::Video::CharacterROM::EnhancedIIe));
constexpr ROM::Name characters = ROM::Name::AppleIIEnhancedECharacter;
constexpr ROM::Name microcontroller = ROM::Name::AppleIIgsMicrocontrollerROM03;
// TODO: pick a different ADB ROM for earlier machine revisions?
rom_descriptions.emplace_back(machine_name, "the Apple IIgs ADB microcontroller ROM", "341s0632-2", 4*1024, 0xe1c11fb0);
const auto roms = rom_fetcher(rom_descriptions);
if(!roms[0] || !roms[1] || !roms[2]) {
ROM::Request request = ROM::Request(system) && ROM::Request(characters) && ROM::Request(microcontroller);
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
rom_ = *roms[0];
video_->set_character_rom(*roms[1]);
adb_glu_->set_microcontroller_rom(*roms[2]);
rom_ = roms.find(system)->second;
video_->set_character_rom(roms.find(characters)->second);
adb_glu_->set_microcontroller_rom(roms.find(microcontroller)->second);
// Run only the currently-interesting self test.
// rom_[0x36402] = 2;
@@ -1093,10 +1085,10 @@ class ConcreteMachine:
// MARK: - Other components.
Apple::Clock::ParallelClock clock_;
JustInTimeActor<Apple::IIgs::Video::Video, 1, 2, Cycles> video_; // i.e. run video at 7Mhz.
JustInTimeActor<Apple::IIgs::ADB::GLU, 1, 4, Cycles> adb_glu_; // i.e. 3,579,545Mhz.
JustInTimeActor<Apple::IIgs::Video::Video, Cycles, 1, 2> video_; // i.e. run video at 7Mhz.
JustInTimeActor<Apple::IIgs::ADB::GLU, Cycles, 1, 4> adb_glu_; // i.e. 3,579,545Mhz.
Zilog::SCC::z8530 scc_;
JustInTimeActor<Apple::IWM, 1, 2, Cycles> iwm_;
JustInTimeActor<Apple::IWM, Cycles, 1, 2> iwm_;
Cycles cycles_since_clock_tick_;
Apple::Macintosh::DoubleDensityDrive drives35_[2];
Apple::Disk::DiskIIDrive drives525_[2];

View File

@@ -576,7 +576,7 @@ class MemoryMap {
if(region.write) { \
region.write[address] = *value; \
const bool _mm_is_shadowed = IsShadowed(map, region, address); \
map.shadow_base[is_shadowed][(&region.write[address] - map.ram_base) & map.shadow_mask[_mm_is_shadowed]] = *value; \
map.shadow_base[_mm_is_shadowed][(&region.write[address] - map.ram_base) & map.shadow_mask[_mm_is_shadowed]] = *value; \
}
// Quick notes on ::IsShadowed contortions:

View File

@@ -271,7 +271,7 @@ class Keyboard {
/// so this may not be valid prior to Mode::PerformingCommand.
int command_ = 0;
/// Populated during PerformingCommand as the response to the most-recently-received command, this
/// is then shifted out to teh host computer. So it is guaranteed valid at the beginning of Mode::SendingResponse,
/// is then shifted out to the host computer. So it is guaranteed valid at the beginning of Mode::SendingResponse,
/// but not afterwards.
int response_ = 0;

View File

@@ -96,25 +96,24 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
using Model = Analyser::Static::Macintosh::Target::Model;
const std::string machine_name = "Macintosh";
uint32_t ram_size, rom_size;
std::vector<ROMMachine::ROM> rom_descriptions;
ROM::Name rom_name;
switch(model) {
default:
case Model::Mac128k:
ram_size = 128*1024;
rom_size = 64*1024;
rom_descriptions.emplace_back(machine_name, "the Macintosh 128k ROM", "mac128k.rom", 64*1024, 0x6d0c8a28);
rom_name = ROM::Name::Macintosh128k;
break;
case Model::Mac512k:
ram_size = 512*1024;
rom_size = 64*1024;
rom_descriptions.emplace_back(machine_name, "the Macintosh 512k ROM", "mac512k.rom", 64*1024, 0xcf759e0d);
rom_name = ROM::Name::Macintosh512k;
break;
case Model::Mac512ke:
case Model::MacPlus: {
ram_size = ((model == Model::MacPlus) ? 4096 : 512)*1024;
rom_size = 128*1024;
const std::initializer_list<uint32_t> crc32s = { 0x4fa5b399, 0x7cacd18f, 0xb2102e8e };
rom_descriptions.emplace_back(machine_name, "the Macintosh Plus ROM", "macplus.rom", 128*1024, crc32s);
rom_name = ROM::Name::MacintoshPlus;
} break;
}
ram_mask_ = ram_size - 1;
@@ -123,12 +122,12 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
video_.set_ram(reinterpret_cast<uint16_t *>(ram_.data()), ram_mask_ >> 1);
// Grab a copy of the ROM and convert it into big-endian data.
const auto roms = rom_fetcher(rom_descriptions);
if(!roms[0]) {
ROM::Request request(rom_name);
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
roms[0]->resize(rom_size);
Memory::PackBigEndian16(*roms[0], rom_);
Memory::PackBigEndian16(roms.find(rom_name)->second, rom_);
// Randomise memory contents.
Memory::Fuzz(ram_);
@@ -647,7 +646,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
return mouse_;
}
using IWMActor = JustInTimeActor<IWM, 1, 1, HalfCycles, Cycles>;
using IWMActor = JustInTimeActor<IWM>;
class VIAPortHandler: public MOS::MOS6522::PortHandler {
public:

View File

@@ -35,7 +35,7 @@ Video::Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulato
// The [newer] Metal scan target has no such issue. So assume that Apple => Metal,
// and set a visible area to work around the OpenGL issue if required.
// TODO: eliminate UGLY HACK.
#ifdef __APPLE__
#if defined(__APPLE__) && !defined(IGNORE_APPLE)
crt_.set_visible_area(Outputs::Display::Rect(0.08f, 10.0f / 368.0f, 0.82f, 344.0f / 368.0f));
#else
crt_.set_visible_area(Outputs::Display::Rect(0.08f, -0.025f, 0.82f, 0.82f));

View File

@@ -74,15 +74,13 @@ class ConcreteMachine:
video_->set_ram(reinterpret_cast<uint16_t *>(ram_.data()), ram_.size());
Memory::Fuzz(ram_);
std::vector<ROMMachine::ROM> rom_descriptions = {
{"AtariST", "the UK TOS 1.00 ROM", "tos100.img", 192*1024, 0x1a586c64}
// {"AtariST", "the UK TOS 1.04 ROM", "tos104.img", 192*1024, 0xa50d1d43}
};
const auto roms = rom_fetcher(rom_descriptions);
if(!roms[0]) {
constexpr ROM::Name rom_name = ROM::Name::AtariSTTOS100;
ROM::Request request(rom_name);
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
Memory::PackBigEndian16(*roms[0], rom_);
Memory::PackBigEndian16(roms.find(rom_name)->second, rom_);
// Set up basic memory map.
memory_map_[0] = BusDevice::MostlyRAM;
@@ -347,18 +345,11 @@ class ConcreteMachine:
update_audio();
if(cycle.operation & Microcycle::Read) {
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
cycle.set_value8_high(ay_.get_data_output());
ay_.set_control_lines(GI::AY38910::ControlLines(0));
cycle.set_value8_high(GI::AY38910::Utility::read(ay_));
} else {
// Net effect here: addresses with bit 1 set write to a register,
// addresses with bit 1 clear select a register.
ay_.set_control_lines(GI::AY38910::ControlLines(
GI::AY38910::BC2 | GI::AY38910::BDIR
| ((address&2) ? 0 : GI::AY38910::BC1)
));
ay_.set_data_input(cycle.value8_high());
ay_.set_control_lines(GI::AY38910::ControlLines(0));
GI::AY38910::Utility::write(ay_, address&2, cycle.value8_high());
}
return delay + HalfCycles(2);
@@ -486,9 +477,9 @@ class ConcreteMachine:
JustInTimeActor<Video> video_;
// The MFP runs at 819200/2673749ths of the CPU clock rate.
JustInTimeActor<Motorola::MFP68901::MFP68901, 819200, 2673749> mfp_;
JustInTimeActor<Motorola::ACIA::ACIA, 16> keyboard_acia_;
JustInTimeActor<Motorola::ACIA::ACIA, 16> midi_acia_;
JustInTimeActor<Motorola::MFP68901::MFP68901, HalfCycles, 819200, 2673749> mfp_;
JustInTimeActor<Motorola::ACIA::ACIA, HalfCycles, 16> keyboard_acia_;
JustInTimeActor<Motorola::ACIA::ACIA, HalfCycles, 16> midi_acia_;
Concurrency::DeferringAsyncTaskQueue audio_queue_;
GI::AY38910::AY38910<false> ay_;

View File

@@ -252,7 +252,7 @@ void DMAController::set_component_prefers_clocking(ClockingHint::Source *, Clock
}
ClockingHint::Preference DMAController::preferred_clocking() const {
return (fdc_.preferred_clocking() == ClockingHint::Preference::None) ? ClockingHint::Preference::None : ClockingHint::Preference::RealTime;
return (fdc_.preferred_clocking() == ClockingHint::Preference::None) ? ClockingHint::Preference::JustInTime : ClockingHint::Preference::RealTime;
}
void DMAController::set_activity_observer(Activity::Observer *observer) {

View File

@@ -127,15 +127,13 @@ class ConcreteMachine:
joysticks_.emplace_back(new Joystick);
joysticks_.emplace_back(new Joystick);
const auto roms = rom_fetcher(
{ {"ColecoVision", "the ColecoVision BIOS", "coleco.rom", 8*1024, 0x3aa93ef3} });
if(!roms[0]) {
constexpr ROM::Name rom_name = ROM::Name::ColecoVisionBIOS;
const ROM::Request request(rom_name);
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
bios_ = *roms[0];
bios_.resize(8192);
bios_ = roms.find(rom_name)->second;
if(!target.media.cartridges.empty()) {
const auto &segment = target.media.cartridges.front()->get_segments().front();
@@ -215,7 +213,9 @@ class ConcreteMachine:
}
const HalfCycles length = cycle.length + penalty;
vdp_ += length;
if(vdp_ += length) {
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
}
time_since_sn76489_update_ += length;
// Act only if necessary.
@@ -263,7 +263,6 @@ class ConcreteMachine:
case 5:
*cycle.value = vdp_->read(address);
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
case 7: {
@@ -287,9 +286,7 @@ class ConcreteMachine:
case 0x52:
// Read AY data.
update_audio();
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
*cycle.value = ay_.get_data_output();
ay_.set_control_lines(GI::AY38910::ControlLines(0));
*cycle.value = GI::AY38910::Utility::read(ay_);
break;
}
break;
@@ -306,7 +303,6 @@ class ConcreteMachine:
case 5:
vdp_->write(address, *cycle.value);
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
case 7:
@@ -324,16 +320,12 @@ class ConcreteMachine:
case 0x50:
// Set AY address.
update_audio();
ay_.set_control_lines(GI::AY38910::BC1);
ay_.set_data_input(*cycle.value);
ay_.set_control_lines(GI::AY38910::ControlLines(0));
GI::AY38910::Utility::select_register(ay_, *cycle.value);
break;
case 0x51:
// Set AY data.
update_audio();
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BDIR));
ay_.set_data_input(*cycle.value);
ay_.set_control_lines(GI::AY38910::ControlLines(0));
GI::AY38910::Utility::write_data(ay_, *cycle.value);
break;
case 0x53:
super_game_module_.replace_ram = !!((*cycle.value)&0x1);
@@ -347,13 +339,6 @@ class ConcreteMachine:
}
}
if(time_until_interrupt_ > 0) {
time_until_interrupt_ -= length;
if(time_until_interrupt_ <= HalfCycles(0)) {
z80_.set_non_maskable_interrupt_line(true, time_until_interrupt_);
}
}
return penalty;
}
@@ -390,7 +375,7 @@ class ConcreteMachine:
}
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
JustInTimeActor<TI::TMS::TMS9918, 1, 1, HalfCycles> vdp_;
JustInTimeActor<TI::TMS::TMS9918> vdp_;
Concurrency::DeferringAsyncTaskQueue audio_queue_;
TI::SN76489 sn76489_;
@@ -414,7 +399,6 @@ class ConcreteMachine:
bool joysticks_in_keypad_mode_ = false;
HalfCycles time_since_sn76489_update_;
HalfCycles time_until_interrupt_;
Analyser::Dynamic::ConfidenceCounter confidence_counter_;
int pc_zero_accesses_ = 0;

View File

@@ -41,7 +41,8 @@ namespace C1540 {
*/
class Machine final: public MachineBase {
public:
Machine(Personality personality, const ROMMachine::ROMFetcher &rom_fetcher);
static ROM::Request rom_request(Personality personality);
Machine(Personality personality, const ROM::Map &roms);
/*!
Sets the serial bus to which this drive should attach itself.

View File

@@ -16,7 +16,15 @@
using namespace Commodore::C1540;
MachineBase::MachineBase(Personality personality, const ROMMachine::ROMFetcher &rom_fetcher) :
ROM::Request Machine::rom_request(Personality personality) {
switch(personality) {
default:
case Personality::C1540: return ROM::Request(ROM::Name::Commodore1540);
case Personality::C1541: return ROM::Request(ROM::Name::Commodore1541);
}
}
MachineBase::MachineBase(Personality personality, const ROM::Map &roms) :
Storage::Disk::Controller(1000000),
m6502_(*this),
serial_port_VIA_port_handler_(new SerialPortVIA(serial_port_VIA_)),
@@ -39,28 +47,22 @@ MachineBase::MachineBase(Personality personality, const ROMMachine::ROMFetcher &
emplace_drive(1000000, 300, 2);
set_drive(1);
std::string device_name;
uint32_t crc = 0;
ROM::Name rom_name;
switch(personality) {
case Personality::C1540:
device_name = "1540";
crc = 0x718d42b1;
break;
case Personality::C1541:
device_name = "1541";
crc = 0xfb760019;
break;
default:
case Personality::C1540: rom_name = ROM::Name::Commodore1540; break;
case Personality::C1541: rom_name = ROM::Name::Commodore1541; break;
}
auto roms = rom_fetcher({ {"Commodore1540", "the " + device_name + " ROM", device_name + ".bin", 16*1024, crc} });
if(!roms[0]) {
auto rom = roms.find(rom_name);
if(rom == roms.end()) {
throw ROMMachine::Error::MissingROMs;
}
std::memcpy(rom_, roms[0]->data(), std::min(sizeof(rom_), roms[0]->size()));
std::memcpy(rom_, roms.find(rom_name)->second.data(), std::min(sizeof(rom_), roms.find(rom_name)->second.size()));
}
Machine::Machine(Personality personality, const ROMMachine::ROMFetcher &rom_fetcher) :
MachineBase(personality, rom_fetcher) {}
Machine::Machine(Personality personality, const ROM::Map &roms) :
MachineBase(personality, roms) {}
void Machine::set_serial_bus(std::shared_ptr<::Commodore::Serial::Bus> serial_bus) {
Commodore::Serial::AttachPortAndBus(serial_port_, serial_bus);

View File

@@ -127,7 +127,7 @@ class MachineBase:
public Storage::Disk::Controller {
public:
MachineBase(Personality personality, const ROMMachine::ROMFetcher &rom_fetcher);
MachineBase(Personality personality, const ROM::Map &roms);
// to satisfy CPU::MOS6502::Processor
Cycles perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value);

View File

@@ -151,5 +151,5 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
#undef SHIFT
#undef X
return table_lookup_sequence_for_character(key_sequences, sizeof(key_sequences), character);
return table_lookup_sequence_for_character(key_sequences, character);
}

View File

@@ -316,53 +316,48 @@ class ConcreteMachine:
// Install a joystick.
joysticks_.emplace_back(new Joystick(*user_port_via_port_handler_, *keyboard_via_port_handler_));
const std::string machine_name = "Vic20";
std::vector<ROMMachine::ROM> rom_names = {
{machine_name, "the VIC-20 BASIC ROM", "basic.bin", 8*1024, 0xdb4c43c1}
};
ROM::Request request(ROM::Name::Vic20BASIC);
ROM::Name kernel, character;
switch(target.region) {
default:
rom_names.emplace_back(machine_name, "the English-language VIC-20 character ROM", "characters-english.bin", 4*1024, 0x83e032a6);
rom_names.emplace_back(machine_name, "the English-language PAL VIC-20 kernel ROM", "kernel-pal.bin", 8*1024, 0x4be07cb4);
character = ROM::Name::Vic20EnglishCharacters;
kernel = ROM::Name::Vic20EnglishPALKernel;
break;
case Analyser::Static::Commodore::Target::Region::American:
rom_names.emplace_back(machine_name, "the English-language VIC-20 character ROM", "characters-english.bin", 4*1024, 0x83e032a6);
rom_names.emplace_back(machine_name, "the English-language NTSC VIC-20 kernel ROM", "kernel-ntsc.bin", 8*1024, 0xe5e7c174);
character = ROM::Name::Vic20EnglishCharacters;
kernel = ROM::Name::Vic20EnglishNTSCKernel;
break;
case Analyser::Static::Commodore::Target::Region::Danish:
rom_names.emplace_back(machine_name, "the Danish VIC-20 character ROM", "characters-danish.bin", 4*1024, 0x7fc11454);
rom_names.emplace_back(machine_name, "the Danish VIC-20 kernel ROM", "kernel-danish.bin", 8*1024, 0x02adaf16);
character = ROM::Name::Vic20DanishCharacters;
kernel = ROM::Name::Vic20DanishKernel;
break;
case Analyser::Static::Commodore::Target::Region::Japanese:
rom_names.emplace_back(machine_name, "the Japanese VIC-20 character ROM", "characters-japanese.bin", 4*1024, 0xfcfd8a4b);
rom_names.emplace_back(machine_name, "the Japanese VIC-20 kernel ROM", "kernel-japanese.bin", 8*1024, 0x336900d7);
character = ROM::Name::Vic20JapaneseCharacters;
kernel = ROM::Name::Vic20JapaneseKernel;
break;
case Analyser::Static::Commodore::Target::Region::Swedish:
rom_names.emplace_back(machine_name, "the Swedish VIC-20 character ROM", "characters-swedish.bin", 4*1024, 0xd808551d);
rom_names.emplace_back(machine_name, "the Swedish VIC-20 kernel ROM", "kernel-swedish.bin", 8*1024, 0xb2a60662);
character = ROM::Name::Vic20SwedishCharacters;
kernel = ROM::Name::Vic20SwedishKernel;
break;
}
const auto roms = rom_fetcher(rom_names);
if(target.has_c1540) {
request = request && Commodore::C1540::Machine::rom_request(Commodore::C1540::Personality::C1540);
}
request = request && ROM::Request(character) && ROM::Request(kernel);
for(const auto &rom: roms) {
if(!rom) {
throw ROMMachine::Error::MissingROMs;
}
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
basic_rom_ = std::move(*roms[0]);
character_rom_ = std::move(*roms[1]);
kernel_rom_ = std::move(*roms[2]);
// Characters ROMs should be 4kb.
character_rom_.resize(4096);
// Kernel ROMs and the BASIC ROM should be 8kb.
kernel_rom_.resize(8192);
basic_rom_ = std::move(roms.find(ROM::Name::Vic20BASIC)->second);
character_rom_ = std::move(roms.find(character)->second);
kernel_rom_ = std::move(roms.find(kernel)->second);
if(target.has_c1540) {
// construct the 1540
c1540_ = std::make_unique<::Commodore::C1540::Machine>(Commodore::C1540::Personality::C1540, rom_fetcher);
c1540_ = std::make_unique<::Commodore::C1540::Machine>(Commodore::C1540::Personality::C1540, roms);
// attach it to the serial bus
c1540_->set_serial_bus(serial_bus_);

View File

@@ -25,6 +25,8 @@
#include "../Utility/Typer.hpp"
#include "../../Analyser/Static/Acorn/Target.hpp"
#include "../../ClockReceiver/JustInTime.hpp"
#include "Interrupts.hpp"
#include "Keyboard.hpp"
#include "Plus3.hpp"
@@ -54,7 +56,7 @@ template <bool has_scsi_bus> class ConcreteMachine:
scsi_bus_(4'000'000),
hard_drive_(scsi_bus_, 0),
scsi_device_(scsi_bus_.add_device()),
video_output_(ram_),
video_(ram_),
sound_generator_(audio_queue_),
speaker_(sound_generator_) {
memset(key_states_, 0, sizeof(key_states_));
@@ -67,37 +69,25 @@ template <bool has_scsi_bus> class ConcreteMachine:
speaker_.set_input_rate(2000000 / SoundGenerator::clock_rate_divider);
speaker_.set_high_frequency_cutoff(6000);
const std::string machine_name = "Electron";
std::vector<ROMMachine::ROM> required_roms = {
{machine_name, "the Acorn BASIC II ROM", "basic.rom", 16*1024, 0x79434781},
{machine_name, "the Electron MOS ROM", "os.rom", 16*1024, 0xbf63fb1f}
};
const size_t pres_adfs_rom_position = required_roms.size();
::ROM::Request request = ::ROM::Request(::ROM::Name::AcornBASICII) && ::ROM::Request(::ROM::Name::AcornElectronMOS100);
if(target.has_pres_adfs) {
required_roms.emplace_back(machine_name, "the E00 ADFS ROM, first slot", "ADFS-E00_1.rom", 16*1024, 0x51523993);
required_roms.emplace_back(machine_name, "the E00 ADFS ROM, second slot", "ADFS-E00_2.rom", 16*1024, 0x8d17de0e);
request = request && ::ROM::Request(::ROM::Name::PRESADFSSlot1) && ::ROM::Request(::ROM::Name::PRESADFSSlot2);
}
const size_t acorn_adfs_rom_position = required_roms.size();
if(target.has_acorn_adfs) {
required_roms.emplace_back(machine_name, "the Acorn ADFS ROM", "adfs.rom", 16*1024, 0x3289bdc6);
request = request && ::ROM::Request(::ROM::Name::AcornADFS);
}
const size_t dfs_rom_position = required_roms.size();
if(target.has_dfs) {
required_roms.emplace_back(machine_name, "the 1770 DFS ROM", "DFS-1770-2.20.rom", 16*1024, 0xf3dc9bc5);
request = request && ::ROM::Request(::ROM::Name::Acorn1770DFS);
}
const size_t ap6_rom_position = required_roms.size();
if(target.has_ap6_rom) {
required_roms.emplace_back(machine_name, "the 8kb Advanced Plus 6 ROM", "AP6v133.rom", 8*1024, 0xe0013cfc);
request = request && ::ROM::Request(::ROM::Name::PRESAdvancedPlus6);
}
const auto roms = rom_fetcher(required_roms);
for(const auto &rom: roms) {
if(!rom) {
throw ROMMachine::Error::MissingROMs;
}
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
set_rom(ROM::BASIC, *roms[0], false);
set_rom(ROM::OS, *roms[1], false);
set_rom(ROM::BASIC, roms.find(::ROM::Name::AcornBASICII)->second, false);
set_rom(ROM::OS, roms.find(::ROM::Name::AcornElectronMOS100)->second, false);
/*
ROM slot mapping applied:
@@ -113,19 +103,18 @@ template <bool has_scsi_bus> class ConcreteMachine:
plus3_ = std::make_unique<Plus3>();
if(target.has_dfs) {
set_rom(ROM::Slot0, *roms[dfs_rom_position], true);
set_rom(ROM::Slot0, roms.find(::ROM::Name::Acorn1770DFS)->second, true);
}
if(target.has_pres_adfs) {
set_rom(ROM::Slot4, *roms[pres_adfs_rom_position], true);
set_rom(ROM::Slot5, *roms[pres_adfs_rom_position+1], true);
set_rom(ROM::Slot4, roms.find(::ROM::Name::PRESADFSSlot1)->second, true);
set_rom(ROM::Slot5, roms.find(::ROM::Name::PRESADFSSlot2)->second, true);
}
if(target.has_acorn_adfs) {
set_rom(ROM::Slot6, *roms[acorn_adfs_rom_position], true);
set_rom(ROM::Slot6, roms.find(::ROM::Name::AcornADFS)->second, true);
}
}
if(target.has_ap6_rom) {
set_rom(ROM::Slot15, *roms[ap6_rom_position], true);
set_rom(ROM::Slot15, roms.find(::ROM::Name::PRESAdvancedPlus6)->second, true);
}
if(target.has_sideways_ram) {
@@ -230,13 +219,15 @@ template <bool has_scsi_bus> class ConcreteMachine:
if(isReadOperation(operation)) {
*value = ram_[address];
} else {
if(address >= video_access_range_.low_address && address <= video_access_range_.high_address) update_display();
if(address >= video_access_range_.low_address && address <= video_access_range_.high_address) {
video_.flush();
}
ram_[address] = *value;
}
// for the entire frame, RAM is accessible only on odd cycles; in modes below 4
// it's also accessible only outside of the pixel regions
cycles += video_output_.get_cycles_until_next_ram_availability(int(cycles_since_display_update_.as_integral()) + 1);
// For the entire frame, RAM is accessible only on odd cycles; in modes below 4
// it's also accessible only outside of the pixel regions.
cycles += video_.last_valid()->get_cycles_until_next_ram_availability(video_.time_since_flush().template as<int>() + 1);
} else {
switch(address & 0xff0f) {
case 0xfe00:
@@ -274,10 +265,8 @@ template <bool has_scsi_bus> class ConcreteMachine:
case 0xfe08: case 0xfe09: case 0xfe0a: case 0xfe0b:
case 0xfe0c: case 0xfe0d: case 0xfe0e: case 0xfe0f:
if(!isReadOperation(operation)) {
update_display();
video_output_.write(address, *value);
video_access_range_ = video_output_.get_memory_access_range();
queue_next_display_interrupt();
video_->write(address, *value);
video_access_range_ = video_.last_valid()->get_memory_access_range();
}
break;
case 0xfe04:
@@ -425,14 +414,14 @@ template <bool has_scsi_bus> class ConcreteMachine:
(address == 0xf0a8) // 0xf0a8 is from where a service call would normally be
// dispatched; we can check whether it would be call 14
// (i.e. read byte) and, if so, whether the OS was about to
// issue a read byte call to a ROM despite being the tape
// issue a read byte call to a ROM despite the tape
// FS being selected. If so then this is a get byte that
// we should service synthetically. Put the byte into Y
// and set A to zero to report that action was taken, then
// allow the PC read to return an RTS.
)
) {
uint8_t service_call = uint8_t(m6502_.get_value_of_register(CPU::MOS6502::Register::X));
const auto service_call = uint8_t(m6502_.get_value_of_register(CPU::MOS6502::Register::X));
if(address == 0xf0a8) {
if(!ram_[0x247] && service_call == 14) {
tape_.set_delegate(nullptr);
@@ -441,7 +430,7 @@ template <bool has_scsi_bus> class ConcreteMachine:
tape_.clear_interrupts(Interrupt::ReceiveDataFull);
while(!tape_.get_tape()->is_at_end()) {
tape_.run_for_input_pulse();
cycles_left_while_plausibly_in_data--;
--cycles_left_while_plausibly_in_data;
if(!cycles_left_while_plausibly_in_data) fast_load_is_in_data_ = false;
if( (tape_.get_interrupt_status() & Interrupt::ReceiveDataFull) &&
(fast_load_is_in_data_ || tape_.get_data_register() == 0x2a)
@@ -483,18 +472,14 @@ template <bool has_scsi_bus> class ConcreteMachine:
}
}
cycles_since_display_update_ += Cycles(int(cycles));
if(video_ += Cycles(int(cycles))) {
signal_interrupt(video_.last_valid()->get_interrupts());
}
cycles_since_audio_update_ += Cycles(int(cycles));
if(cycles_since_audio_update_ > Cycles(16384)) update_audio();
tape_.run_for(Cycles(int(cycles)));
cycles_until_display_interrupt_ -= cycles;
if(cycles_until_display_interrupt_ < 0) {
signal_interrupt(next_display_interrupt_);
update_display();
queue_next_display_interrupt();
}
if(typer_) typer_->run_for(Cycles(int(cycles)));
if(plus3_) plus3_->run_for(Cycles(4*int(cycles)));
if(shift_restart_counter_) {
@@ -517,25 +502,25 @@ template <bool has_scsi_bus> class ConcreteMachine:
}
forceinline void flush() {
update_display();
video_.flush();
update_audio();
audio_queue_.perform();
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
video_output_.set_scan_target(scan_target);
video_.last_valid()->set_scan_target(scan_target);
}
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return video_output_.get_scaled_scan_status();
return video_.last_valid()->get_scaled_scan_status();
}
void set_display_type(Outputs::Display::DisplayType display_type) final {
video_output_.set_display_type(display_type);
video_.last_valid()->set_display_type(display_type);
}
Outputs::Display::DisplayType get_display_type() const final {
return video_output_.get_display_type();
return video_.last_valid()->get_display_type();
}
Outputs::Speaker::Speaker *get_speaker() final {
@@ -693,18 +678,6 @@ template <bool has_scsi_bus> class ConcreteMachine:
}
// MARK: - Work deferral updates.
inline void update_display() {
if(cycles_since_display_update_ > 0) {
video_output_.run_for(cycles_since_display_update_.flush<Cycles>());
}
}
inline void queue_next_display_interrupt() {
VideoOutput::Interrupt next_interrupt = video_output_.get_next_interrupt();
cycles_until_display_interrupt_ = next_interrupt.cycles;
next_display_interrupt_ = next_interrupt.interrupt;
}
inline void update_audio() {
speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(SoundGenerator::clock_rate_divider)));
}
@@ -754,10 +727,7 @@ template <bool has_scsi_bus> class ConcreteMachine:
Electron::KeyboardMapper keyboard_mapper_;
// Counters related to simultaneous subsystems
Cycles cycles_since_display_update_ = 0;
Cycles cycles_since_audio_update_ = 0;
int cycles_until_display_interrupt_ = 0;
Interrupt next_display_interrupt_ = Interrupt::RealTimeClock;
VideoOutput::Range video_access_range_ = {0, 0xffff};
// Tape
@@ -794,7 +764,7 @@ template <bool has_scsi_bus> class ConcreteMachine:
}
// Outputs
VideoOutput video_output_;
JustInTimeActor<VideoOutput, Cycles> video_;
Concurrency::DeferringAsyncTaskQueue audio_queue_;
SoundGenerator sound_generator_;

View File

@@ -145,7 +145,7 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
#undef SHIFT
#undef X
return table_lookup_sequence_for_character(key_sequences, sizeof(key_sequences), character);
return table_lookup_sequence_for_character(key_sequences, character);
}
bool CharacterMapper::needs_pause_after_key(uint16_t key) const {

View File

@@ -234,7 +234,23 @@ void VideoOutput::output_pixels(int number_of_cycles) {
void VideoOutput::run_for(const Cycles cycles) {
int number_of_cycles = int(cycles.as_integral());
const auto start_position = output_position_;
output_position_ = (output_position_ + number_of_cycles) % cycles_per_frame;
if(
(start_position < real_time_clock_interrupt_1 && output_position_ >= real_time_clock_interrupt_1) ||
(start_position < real_time_clock_interrupt_2 && output_position_ >= real_time_clock_interrupt_2)
) {
interrupts_ = Electron::Interrupt(interrupts_ | Electron::Interrupt::RealTimeClock);
}
if(
(start_position < display_end_interrupt_1 && output_position_ >= display_end_interrupt_1) ||
(start_position < display_end_interrupt_2 && output_position_ >= display_end_interrupt_2)
) {
interrupts_ = Electron::Interrupt(interrupts_ | Electron::Interrupt::DisplayEnd);
}
while(number_of_cycles) {
int draw_action_length = screen_map_[screen_map_pointer_].length;
int time_left_in_action = std::min(number_of_cycles, draw_action_length - cycles_into_draw_action_);
@@ -354,36 +370,30 @@ void VideoOutput::setup_base_address() {
// MARK: - Interrupts
VideoOutput::Interrupt VideoOutput::get_next_interrupt() {
VideoOutput::Interrupt interrupt;
Cycles VideoOutput::get_next_sequence_point() {
if(output_position_ < real_time_clock_interrupt_1) {
interrupt.cycles = real_time_clock_interrupt_1 - output_position_;
interrupt.interrupt = RealTimeClock;
return interrupt;
return real_time_clock_interrupt_1 - output_position_;
}
if(output_position_ < display_end_interrupt_1) {
interrupt.cycles = display_end_interrupt_1 - output_position_;
interrupt.interrupt = DisplayEnd;
return interrupt;
return display_end_interrupt_1 - output_position_;
}
if(output_position_ < real_time_clock_interrupt_2) {
interrupt.cycles = real_time_clock_interrupt_2 - output_position_;
interrupt.interrupt = RealTimeClock;
return interrupt;
return real_time_clock_interrupt_2 - output_position_;
}
if(output_position_ < display_end_interrupt_2) {
interrupt.cycles = display_end_interrupt_2 - output_position_;
interrupt.interrupt = DisplayEnd;
return interrupt;
return display_end_interrupt_2 - output_position_;
}
interrupt.cycles = real_time_clock_interrupt_1 + cycles_per_frame - output_position_;
interrupt.interrupt = RealTimeClock;
return interrupt;
return real_time_clock_interrupt_1 + cycles_per_frame - output_position_;
}
Electron::Interrupt VideoOutput::get_interrupts() {
const auto interrupts = interrupts_;
interrupts_ = Electron::Interrupt(0);
return interrupts;
}
// MARK: - RAM timing and access information

View File

@@ -54,15 +54,6 @@ class VideoOutput {
*/
void write(int address, uint8_t value);
/*!
Describes an interrupt the video hardware will generate by its identity and scheduling time.
*/
struct Interrupt {
/// The interrupt that will be signalled.
Electron::Interrupt interrupt;
/// The number of cycles until it is signalled.
int cycles;
};
/*!
@returns the next interrupt that should be generated as a result of the video hardware.
The time until signalling returned is the number of cycles after the final one triggered
@@ -70,7 +61,12 @@ class VideoOutput {
This result may be mutated by calls to @c write.
*/
Interrupt get_next_interrupt();
Cycles get_next_sequence_point();
/*!
@returns a bit mask of all interrupts that have been triggered since the last call to get_interrupt().
*/
Electron::Interrupt get_interrupts();
/*!
@returns the number of cycles after (final cycle of last run_for batch + @c from_time)
@@ -136,6 +132,8 @@ class VideoOutput {
void emplace_pixel_line();
std::size_t screen_map_pointer_ = 0;
int cycles_into_draw_action_ = 0;
Electron::Interrupt interrupts_ = Electron::Interrupt(0);
};
}

View File

@@ -0,0 +1,337 @@
//
// Dave.cpp
// Clock Signal
//
// Created by Thomas Harte on 22/06/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "Dave.hpp"
using namespace Enterprise::Dave;
// MARK: - Audio generator
Audio::Audio(Concurrency::DeferringAsyncTaskQueue &audio_queue) :
audio_queue_(audio_queue) {}
void Audio::write(uint16_t address, uint8_t value) {
address &= 0x1f;
audio_queue_.defer([address, value, this] {
switch(address) {
case 0: case 2: case 4:
channels_[address >> 1].reload = (channels_[address >> 1].reload & 0xff00) | value;
break;
case 1: case 3: case 5:
channels_[address >> 1].reload = uint16_t((channels_[address >> 1].reload & 0x00ff) | ((value & 0xf) << 8));
channels_[address >> 1].distortion = Channel::Distortion((value >> 4)&3);
channels_[address >> 1].high_pass = value & 0x40;
channels_[address >> 1].ring_modulate = value & 0x80;
break;
case 6:
noise_.frequency = Noise::Frequency(value&3);
noise_.polynomial = Noise::Polynomial((value >> 2)&3);
noise_.swap_polynomial = value & 0x10;
noise_.low_pass = value & 0x20;
noise_.high_pass = value & 0x40;
noise_.ring_modulate = value & 0x80;
break;
case 7:
channels_[0].sync = value & 0x01;
channels_[1].sync = value & 0x02;
channels_[2].sync = value & 0x04;
use_direct_output_[0] = value & 0x08;
use_direct_output_[1] = value & 0x10;
// Interrupt bits are handled separately.
break;
case 8: case 9: case 10:
channels_[address - 8].amplitude[0] = value & 0x3f;
break;
case 12: case 13: case 14:
channels_[address - 12].amplitude[1] = value & 0x3f;
break;
case 11: noise_.amplitude[0] = value & 0x3f; break;
case 15: noise_.amplitude[1] = value & 0x3f; break;
case 31:
global_divider_reload_ = 2 + ((value >> 1)&1);
break;
}
});
}
void Audio::set_sample_volume_range(int16_t range) {
audio_queue_.defer([range, this] {
volume_ = range / (63*4);
});
}
void Audio::update_channel(int c) {
auto output = channels_[c].output & 1;
channels_[c].output <<= 1;
if(channels_[c].sync) {
channels_[c].count = channels_[c].reload;
output = 0;
} else {
if(!channels_[c].count) {
channels_[c].count = channels_[c].reload;
if(channels_[c].distortion == Channel::Distortion::None)
output ^= 1;
else
output = poly_state_[int(channels_[c].distortion)];
} else {
--channels_[c].count;
}
if(channels_[c].high_pass && (channels_[(c+1)%3].output&3) == 2) {
output = 0;
}
}
// Ring modulation applies even when sync is enabled, per SIDBasic.
if(channels_[c].ring_modulate) {
output = ~(output ^ channels_[(c+2)%3].output) & 1;
}
channels_[c].output |= output;
}
void Audio::get_samples(std::size_t number_of_samples, int16_t *target) {
int16_t output_level[2];
size_t c = 0;
while(c < number_of_samples) {
// I'm unclear on the details of the time division multiplexing so,
// for now, just sum the outputs.
output_level[0] =
volume_ *
(use_direct_output_[0] ?
channels_[0].amplitude[0]
: (
channels_[0].amplitude[0] * (channels_[0].output & 1) +
channels_[1].amplitude[0] * (channels_[1].output & 1) +
channels_[2].amplitude[0] * (channels_[2].output & 1) +
noise_.amplitude[0] * noise_.final_output
));
output_level[1] =
volume_ *
(use_direct_output_[1] ?
channels_[0].amplitude[1]
: (
channels_[0].amplitude[1] * (channels_[0].output & 1) +
channels_[1].amplitude[1] * (channels_[1].output & 1) +
channels_[2].amplitude[1] * (channels_[2].output & 1) +
noise_.amplitude[1] * noise_.final_output
));
while(global_divider_ && c < number_of_samples) {
--global_divider_;
*reinterpret_cast<uint32_t *>(&target[c << 1]) = *reinterpret_cast<uint32_t *>(output_level);
++c;
}
global_divider_ = global_divider_reload_;
if(!global_divider_) {
global_divider_ = global_divider_reload_;
}
poly_state_[int(Channel::Distortion::FourBit)] = poly4_.next();
poly_state_[int(Channel::Distortion::FiveBit)] = poly5_.next();
poly_state_[int(Channel::Distortion::SevenBit)] = poly7_.next();
if(noise_.swap_polynomial) {
poly_state_[int(Channel::Distortion::SevenBit)] = poly_state_[int(Channel::Distortion::None)];
}
// Update tone channels.
update_channel(0);
update_channel(1);
update_channel(2);
// Update noise channel.
// Step 1: decide whether there is a tick to apply.
bool noise_tick = false;
if(noise_.frequency == Noise::Frequency::DivideByFour) {
if(!noise_.count) {
noise_tick = true;
noise_.count = 3;
} else {
--noise_.count;
}
} else {
noise_tick = (channels_[int(noise_.frequency) - 1].output&3) == 2;
}
// Step 2: tick if necessary.
int noise_output = noise_.output & 1;
noise_.output <<= 1;
if(noise_tick) {
switch(noise_.polynomial) {
case Noise::Polynomial::SeventeenBit:
poly_state_[int(Channel::Distortion::None)] = uint8_t(poly17_.next());
break;
case Noise::Polynomial::FifteenBit:
poly_state_[int(Channel::Distortion::None)] = uint8_t(poly15_.next());
break;
case Noise::Polynomial::ElevenBit:
poly_state_[int(Channel::Distortion::None)] = uint8_t(poly11_.next());
break;
case Noise::Polynomial::NineBit:
poly_state_[int(Channel::Distortion::None)] = uint8_t(poly9_.next());
break;
}
noise_output = poly_state_[int(Channel::Distortion::None)];
}
noise_.output |= noise_output;
// Low pass: sample channel 2 on downward transitions of the prima facie output.
if(noise_.low_pass && (noise_.output & 3) == 2) {
noise_.output = (noise_.output & ~1) | (channels_[2].output & 1);
}
// Apply noise high-pass.
if(noise_.high_pass && (channels_[0].output & 3) == 2) {
noise_.output &= ~1;
}
// Update noise ring modulation, if any.
if(noise_.ring_modulate) {
noise_.final_output = !((noise_.output ^ channels_[1].output) & 1);
} else {
noise_.final_output = noise_.output & 1;
}
}
}
// MARK: - Interrupt source
uint8_t TimedInterruptSource::get_new_interrupts() {
const uint8_t result = interrupts_;
interrupts_ = 0;
return result;
}
void TimedInterruptSource::write(uint16_t address, uint8_t value) {
address &= 0x1f;
switch(address) {
default: break;
case 0: case 2:
channels_[address >> 1].reload = (channels_[address >> 1].reload & 0xff00) | value;
break;
case 1: case 3:
channels_[address >> 1].reload = uint16_t((channels_[address >> 1].reload & 0x00ff) | ((value & 0xf) << 8));
break;
case 7:
channels_[0].sync = value & 0x01;
channels_[1].sync = value & 0x02;
rate_ = InterruptRate((value >> 5) & 3);
break;
case 31:
global_divider_ = Cycles(2 + ((value >> 1)&1));
break;
}
}
void TimedInterruptSource::update_channel(int c, bool is_linked, int decrement) {
if(channels_[c].sync) {
channels_[c].value = channels_[c].reload;
} else {
if(decrement <= channels_[c].value) {
channels_[c].value -= decrement;
} else {
// The decrement is greater than the current value, therefore
// there'll be at least one flip.
//
// After decreasing the decrement by the current value + 1,
// it'll be clear how many decrements are left after reload.
//
// Dividing that by the number of decrements necessary for a
// flip will provide the total number of flips.
const int decrements_after_flip = decrement - (channels_[c].value + 1);
const int num_flips = 1 + decrements_after_flip / (channels_[c].reload + 1);
// If this is a linked channel, set the interrupt mask if a transition
// from high to low is amongst the included flips.
if(is_linked && num_flips + channels_[c].level >= 2) {
interrupts_ |= uint8_t(Interrupt::VariableFrequency);
programmable_level_ ^= true;
}
channels_[c].level ^= (num_flips & 1);
// Apply the modulo number of decrements to the reload value to
// figure out where things stand now.
channels_[c].value = channels_[c].reload - decrements_after_flip % (channels_[c].reload + 1);
}
}
}
void TimedInterruptSource::run_for(Cycles duration) {
// Determine total number of ticks.
run_length_ += duration;
const Cycles cycles = run_length_.divide(global_divider_);
if(cycles == Cycles(0)) {
return;
}
// Update the two-second counter, from which the 1Hz, 50Hz and 1000Hz signals
// are derived.
const int previous_counter = two_second_counter_;
two_second_counter_ = (two_second_counter_ + cycles.as<int>()) % 500'000;
// Check for a 1Hz rollover.
if(previous_counter / 250'000 != two_second_counter_ / 250'000) {
interrupts_ |= uint8_t(Interrupt::OneHz);
}
// Check for 1kHz or 50Hz rollover;
switch(rate_) {
default: break;
case InterruptRate::OnekHz:
if(previous_counter / 250 != two_second_counter_ / 250) {
interrupts_ |= uint8_t(Interrupt::VariableFrequency);
programmable_level_ ^= true;
}
break;
case InterruptRate::FiftyHz:
if(previous_counter / 5'000 != two_second_counter_ / 5'000) {
interrupts_ |= uint8_t(Interrupt::VariableFrequency);
programmable_level_ ^= true;
}
break;
}
// Update the two tone channels.
update_channel(0, rate_ == InterruptRate::ToneGenerator0, cycles.as<int>());
update_channel(1, rate_ == InterruptRate::ToneGenerator1, cycles.as<int>());
}
Cycles TimedInterruptSource::get_next_sequence_point() const {
// Since both the 1kHz and 50Hz timers are integer dividers of the 1Hz timer, there's no need
// to factor that one in when determining the next sequence point for either of those.
switch(rate_) {
default:
case InterruptRate::OnekHz: return Cycles(250 - (two_second_counter_ % 250));
case InterruptRate::FiftyHz: return Cycles(5000 - (two_second_counter_ % 5000));
case InterruptRate::ToneGenerator0:
case InterruptRate::ToneGenerator1: {
const auto &channel = channels_[int(rate_) - int(InterruptRate::ToneGenerator0)];
const int cycles_until_interrupt = channel.value + 1 + (!channel.level) * (channel.reload + 1);
return Cycles(std::min(
250'000 - (two_second_counter_ % 250'000),
cycles_until_interrupt
));
}
}
}
uint8_t TimedInterruptSource::get_divider_state() {
return uint8_t((two_second_counter_ / 250'000) * 4 | programmable_level_);
}

View File

@@ -0,0 +1,188 @@
//
// Dave.hpp
// Clock Signal
//
// Created by Thomas Harte on 22/06/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef Dave_hpp
#define Dave_hpp
#include <cstdint>
#include "../../ClockReceiver/ClockReceiver.hpp"
#include "../../Concurrency/AsyncTaskQueue.hpp"
#include "../../Numeric/LFSR.hpp"
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
namespace Enterprise {
namespace Dave {
enum class Interrupt: uint8_t {
VariableFrequency = 0x02,
OneHz = 0x08,
Nick = 0x20,
};
/*!
Models the audio-production subset of Dave's behaviour.
*/
class Audio: public Outputs::Speaker::SampleSource {
public:
Audio(Concurrency::DeferringAsyncTaskQueue &audio_queue);
/// Modifies an register in the audio range; only the low 4 bits are
/// used for register decoding so it's assumed that the caller has
/// already identified this write as being to an audio register.
void write(uint16_t address, uint8_t value);
// MARK: - SampleSource.
void set_sample_volume_range(int16_t range);
static constexpr bool get_is_stereo() { return true; } // Dave produces stereo sound.
void get_samples(std::size_t number_of_samples, int16_t *target);
private:
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
// Global divider (i.e. 8MHz/12Mhz switch).
uint8_t global_divider_;
uint8_t global_divider_reload_ = 2;
// Tone channels.
struct Channel {
// User-set values.
uint16_t reload = 0;
bool high_pass = false;
bool ring_modulate = false;
enum class Distortion {
None = 0,
FourBit = 1,
FiveBit = 2,
SevenBit = 3,
} distortion = Distortion::None;
uint8_t amplitude[2]{};
bool sync = false;
// Current state.
uint16_t count = 0;
int output = 0;
} channels_[3];
void update_channel(int);
// Noise channel.
struct Noise {
// User-set values.
uint8_t amplitude[2]{};
enum class Frequency {
DivideByFour,
ToneChannel0,
ToneChannel1,
ToneChannel2,
} frequency = Frequency::DivideByFour;
enum class Polynomial {
SeventeenBit,
FifteenBit,
ElevenBit,
NineBit
} polynomial = Polynomial::SeventeenBit;
bool swap_polynomial = false;
bool low_pass = false;
bool high_pass = false;
bool ring_modulate = false;
// Current state.
int count = 0;
int output = 0;
bool final_output = false;
} noise_;
void update_noise();
bool use_direct_output_[2]{};
// Global volume, per SampleSource obligations.
int16_t volume_ = 0;
// Polynomials that are always running.
Numeric::LFSRv<0xc> poly4_;
Numeric::LFSRv<0x14> poly5_;
Numeric::LFSRv<0x60> poly7_;
// The selectable, noise-related polynomial.
Numeric::LFSRv<0x110> poly9_;
Numeric::LFSRv<0x500> poly11_;
Numeric::LFSRv<0x6000> poly15_;
Numeric::LFSRv<0x12000> poly17_;
// Current state of the active polynomials.
uint8_t poly_state_[4];
};
/*!
Provides Dave's timed interrupts — those that are provided at 1 kHz,
50 Hz or according to the rate of tone generators 0 or 1, plus the fixed
1 Hz interrupt.
*/
class TimedInterruptSource {
public:
/// Modifies an register in the audio range; only the low 4 bits are
/// used for register decoding so it's assumed that the caller has
/// already identified this write as being to an audio register.
void write(uint16_t address, uint8_t value);
/// Returns a bitmask of interrupts that have become active since
/// the last time this method was called; flags are as defined in
/// @c Enterprise::Dave::Interrupt
uint8_t get_new_interrupts();
/// Returns the current high or low states of the inputs that trigger
/// the interrupts modelled here, as a bit mask compatible with that
/// exposed by Dave as the register at 0xb4.
uint8_t get_divider_state();
/// Advances the interrupt source.
void run_for(Cycles);
/// @returns The amount of time from now until the earliest that
/// @c get_new_interrupts() _might_ have new interrupts to report.
Cycles get_next_sequence_point() const;
private:
static constexpr Cycles clock_rate{250000};
static constexpr Cycles half_clock_rate{125000};
// Global divider (i.e. 8MHz/12Mhz switch).
Cycles global_divider_ = Cycles(2);
Cycles run_length_;
// Interrupts that have fired since get_new_interrupts()
// was last called.
uint8_t interrupts_ = 0;
// A counter for the 1Hz interrupt.
int two_second_counter_ = 0;
// A counter specific to the 1kHz and 50Hz timers, if in use.
enum class InterruptRate {
OnekHz,
FiftyHz,
ToneGenerator0,
ToneGenerator1,
} rate_ = InterruptRate::OnekHz;
bool programmable_level_ = false;
// A local duplicate of the counting state of the first two audio
// channels, maintained in case either of those is used as an
// interrupt source.
struct Channel {
int value = 100, reload = 100;
bool sync = false;
bool level = false;
} channels_[2];
void update_channel(int c, bool is_linked, int decrement);
};
}
}
#endif /* Dave_hpp */

View File

@@ -0,0 +1,78 @@
//
// EXDos.cpp
// Clock Signal
//
// Created by Thomas Harte on 20/06/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "EXDos.hpp"
// TODO: disk_did_change_ should be on the drive. Some drives report it.
using namespace Enterprise;
EXDos::EXDos() : WD1770(P1770) {
emplace_drives(4, 8000000, 300, 2);
set_control_register(0x00);
}
void EXDos::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
get_drive(drive).set_disk(disk);
disk_did_change_ = true;
}
// Documentation for the control register:
//
// Write:
// b7 in use (???)
// b6 disk change reset
// b5 0 = double density, 1 = single density
// b4 side 1 select
// b3, b3, b1, b0 select drive 3, 2, 1, 0
//
// Read:
// b7 data request from WD1770
// b6 disk change
// b5, b4, b3, b2: not used
// b1 interrupt request from WD1770
// b0 drive ready
void EXDos::set_control_register(uint8_t control) {
if(control & 0x40) disk_did_change_ = false;
set_is_double_density(!(control & 0x20));
// Set side.
const int head = (control >> 4) & 1;
for(size_t c = 0; c < 4; c++) {
get_drive(c).set_head(head);
}
// Select drive, ensuring handover of the motor-on state.
const bool motor_state = get_drive().get_motor_on();
for_all_drives([] (Storage::Disk::Drive &drive, size_t) {
drive.set_motor_on(false);
});
set_drive(control & 0xf);
get_drive().set_motor_on(motor_state);
}
uint8_t EXDos::get_control_register() {
const uint8_t status =
(get_data_request_line() ? 0x80 : 0x00) |
(disk_did_change_ ? 0x40 : 0x00) |
(get_interrupt_request_line() ? 0x02 : 0x00) |
(get_drive().get_is_ready() ? 0x01 : 0x00);
return status;
}
void EXDos::set_motor_on(bool on) {
get_drive().set_motor_on(on);
}
void EXDos::set_activity_observer(Activity::Observer *observer) {
for_all_drives([observer] (Storage::Disk::Drive &drive, size_t index) {
drive.set_activity_observer(observer, "Drive " + std::to_string(index+1), true);
});
}

View File

@@ -0,0 +1,36 @@
//
// EXDos.hpp
// Clock Signal
//
// Created by Thomas Harte on 20/06/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef EXDos_hpp
#define EXDos_hpp
#include "../../Components/1770/1770.hpp"
#include "../../Activity/Observer.hpp"
namespace Enterprise {
class EXDos final : public WD::WD1770 {
public:
EXDos();
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive);
void set_control_register(uint8_t control);
uint8_t get_control_register();
void set_activity_observer(Activity::Observer *observer);
private:
bool disk_did_change_ = false;
void set_motor_on(bool on) override;
};
}
#endif /* EXDos_hpp */

View File

@@ -0,0 +1,765 @@
//
// Enterprise.cpp
// Clock Signal
//
// Created by Thomas Harte on 10/06/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "Enterprise.hpp"
#include "Dave.hpp"
#include "EXDos.hpp"
#include "Keyboard.hpp"
#include "Nick.hpp"
#include "../MachineTypes.hpp"
#include "../Utility/Typer.hpp"
#include "../../Analyser/Static/Enterprise/Target.hpp"
#include "../../ClockReceiver/JustInTime.hpp"
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "../../Processors/Z80/Z80.hpp"
#define LOG_PREFIX "[Enterprise] "
#include "../../Outputs/Log.hpp"
namespace Enterprise {
/*
Notes to self on timing:
Nick divides each line into 57 windows; each window lasts 16 cycles and dedicates the
first 10 of those to VRAM accesses, leaving the final six for a Z80 video RAM access
if one has been requested.
The Z80 has a separate, asynchronous 4Mhz clock. That's that.
The documentation is also very forward in emphasising that Nick generates phaselocked
(i.e. in-phase) PAL video.
So: 57*16 = 912 cycles/line.
A standard PAL line lasts 64µs and during that time outputs 283.7516 colour cycles.
I shall _guess_ that the Enterprise stretches each line to 284 colour cycles rather than
reducing it to 283.
Therefore 912 cycles occurs in 284/283.7516 * 64 µs.
So one line = 181760000 / 2837516 µs = 45440000 / 709379 µs
=> one cycle = 45440000 / 709379*912 = 45440000 / 646953648 = 2840000 / 40434603 µs
=> clock rate of 40434603 / 2840000 Mhz
And, therefore, the ratio to a 4Mhz Z80 clock is:
40434603 / (2840000 * 4)
= 40434603 / 11360000
i.e. roughly 3.55 Nick cycles per Z80 cycle.
If that's true then the 6-cycle window is around 1.69 Z80 cycles long. Given that the Z80
clock in an Enterprise can be stopped in half-cycle increments only, the Z80 can only be
guaranteed to have around a 1.19 cycle minimum for its actual access. I'm therefore further
postulating that the clock stoppage takes place so as to align the final cycle of a relevant
access over the available window.
*/
template <bool has_disk_controller, bool is_6mhz> class ConcreteMachine:
public Activity::Source,
public Configurable::Device,
public CPU::Z80::BusHandler,
public Machine,
public MachineTypes::AudioProducer,
public MachineTypes::MappedKeyboardMachine,
public MachineTypes::MediaTarget,
public MachineTypes::ScanProducer,
public MachineTypes::TimedMachine,
public Utility::TypeRecipient<CharacterMapper> {
private:
constexpr uint8_t min_ram_slot(const Analyser::Static::Enterprise::Target &target) {
size_t ram_size = 128*1024;
switch(target.model) {
case Analyser::Static::Enterprise::Target::Model::Enterprise64: ram_size = 64*1024; break;
case Analyser::Static::Enterprise::Target::Model::Enterprise128: ram_size = 128*1024; break;
case Analyser::Static::Enterprise::Target::Model::Enterprise256: ram_size = 256*1024; break;
}
return uint8_t(0x100 - ram_size / 0x4000);
}
static constexpr double clock_rate = is_6mhz ? 6'000'000.0 : 4'000'000.0;
using NickType =
std::conditional_t<is_6mhz,
JustInTimeActor<Nick, HalfCycles, 13478201, 5680000>,
JustInTimeActor<Nick, HalfCycles, 40434603, 11360000>>;
public:
ConcreteMachine(const Analyser::Static::Enterprise::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
min_ram_slot_(min_ram_slot(target)),
z80_(*this),
nick_(ram_.end() - 65536),
dave_audio_(audio_queue_),
speaker_(dave_audio_) {
// Request a clock of 4Mhz; this'll be mapped upwards for Nick and downwards for Dave elsewhere.
set_clock_rate(clock_rate);
speaker_.set_input_rate(float(clock_rate) / float(dave_divider));
ROM::Request request;
using Target = Analyser::Static::Enterprise::Target;
// Pick one or more EXOS ROMs.
switch(target.exos_version) {
case Target::EXOSVersion::v10: request = request && ROM::Request(ROM::Name::EnterpriseEXOS10); break;
case Target::EXOSVersion::v20: request = request && ROM::Request(ROM::Name::EnterpriseEXOS20); break;
case Target::EXOSVersion::v21: request = request && ROM::Request(ROM::Name::EnterpriseEXOS21); break;
case Target::EXOSVersion::v23: request = request && ROM::Request(ROM::Name::EnterpriseEXOS23); break;
case Target::EXOSVersion::Any:
request =
request && (
ROM::Request(ROM::Name::EnterpriseEXOS10) || ROM::Request(ROM::Name::EnterpriseEXOS20) ||
ROM::Request(ROM::Name::EnterpriseEXOS21) || ROM::Request(ROM::Name::EnterpriseEXOS23)
);
break;
default: break;
}
// Similarly pick one or more BASIC ROMs.
switch(target.basic_version) {
case Target::BASICVersion::v10:
request = request && (
ROM::Request(ROM::Name::EnterpriseBASIC10) ||
(ROM::Request(ROM::Name::EnterpriseBASIC10Part1) && ROM::Request(ROM::Name::EnterpriseBASIC10Part2))
);
break;
case Target::BASICVersion::v11:
request = request && (
ROM::Request(ROM::Name::EnterpriseBASIC11) ||
ROM::Request(ROM::Name::EnterpriseBASIC11Suffixed)
);
case Target::BASICVersion::v21:
request = request && ROM::Request(ROM::Name::EnterpriseBASIC21);
break;
case Target::BASICVersion::Any:
request =
request && (
ROM::Request(ROM::Name::EnterpriseBASIC10) ||
(ROM::Request(ROM::Name::EnterpriseBASIC10Part1) && ROM::Request(ROM::Name::EnterpriseBASIC10Part2)) ||
ROM::Request(ROM::Name::EnterpriseBASIC11) ||
ROM::Request(ROM::Name::EnterpriseBASIC21)
);
break;
default: break;
}
// Possibly add in a DOS.
switch(target.dos) {
case Target::DOS::EXDOS: request = request && ROM::Request(ROM::Name::EnterpriseEXDOS); break;
default: break;
}
// Get and validate ROMs.
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
// Extract the appropriate EXOS ROM.
exos_.fill(0xff);
for(const auto rom_name: { ROM::Name::EnterpriseEXOS10, ROM::Name::EnterpriseEXOS20, ROM::Name::EnterpriseEXOS21, ROM::Name::EnterpriseEXOS23 }) {
const auto exos = roms.find(rom_name);
if(exos != roms.end()) {
memcpy(exos_.data(), exos->second.data(), std::min(exos_.size(), exos->second.size()));
break;
}
}
// Extract the appropriate BASIC ROM[s] (if any).
basic_.fill(0xff);
bool has_basic = false;
for(const auto rom_name: { ROM::Name::EnterpriseBASIC10, ROM::Name::EnterpriseBASIC11, ROM::Name::EnterpriseBASIC11Suffixed, ROM::Name::EnterpriseBASIC21 }) {
const auto basic = roms.find(rom_name);
if(basic != roms.end()) {
memcpy(basic_.data(), basic->second.data(), std::min(basic_.size(), basic->second.size()));
has_basic = true;
break;
}
}
if(!has_basic) {
const auto basic1 = roms.find(ROM::Name::EnterpriseBASIC10Part1);
const auto basic2 = roms.find(ROM::Name::EnterpriseBASIC10Part2);
if(basic1 != roms.end() && basic2 != roms.end()) {
memcpy(&basic_[0x0000], basic1->second.data(), std::min(size_t(8192), basic1->second.size()));
memcpy(&basic_[0x2000], basic2->second.data(), std::min(size_t(8192), basic2->second.size()));
}
}
// Extract the appropriate DOS ROMs.
epdos_rom_.fill(0xff);
const auto epdos = roms.find(ROM::Name::EnterpriseEPDOS);
if(epdos != roms.end()) {
memcpy(epdos_rom_.data(), epdos->second.data(), std::min(epdos_rom_.size(), epdos->second.size()));
}
exdos_rom_.fill(0xff);
const auto exdos = roms.find(ROM::Name::EnterpriseEXDOS);
if(exdos != roms.end()) {
memcpy(exdos_rom_.data(), exdos->second.data(), std::min(exdos_rom_.size(), exdos->second.size()));
}
// Seed key state.
clear_all_keys();
// Take a reasonable guess at the initial memory configuration:
// put EXOS into the first bank since this is a Z80 and therefore
// starts from address 0; the third instruction in EXOS is a jump
// to $c02e so it's reasonable to assume EXOS is in the highest bank
// too, and it appears to act correctly if it's the first 16kb that's
// in the highest bank. From there I guess: all banks are initialised
// to 0.
page<0>(0x00);
page<1>(0x00);
page<2>(0x00);
page<3>(0x00);
// Pass on any media.
insert_media(target.media);
if(!target.loading_command.empty()) {
type_string(target.loading_command);
}
// Ensure the splash screen is automatically skipped if any media has been provided.
if(!target.media.empty()) {
should_skip_splash_screen_ = !target.media.empty();
typer_delay_ = 2;
}
}
~ConcreteMachine() {
audio_queue_.flush();
}
// MARK: - Z80::BusHandler.
forceinline void advance_nick(HalfCycles duration) {
if(nick_ += duration) {
const auto nick = nick_.last_valid();
const bool nick_interrupt_line = nick->get_interrupt_line();
if(nick_interrupt_line && !previous_nick_interrupt_line_) {
set_interrupts(uint8_t(Dave::Interrupt::Nick), nick_.last_sequence_point_overrun());
}
previous_nick_interrupt_line_ = nick_interrupt_line;
}
}
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
using PartialMachineCycle = CPU::Z80::PartialMachineCycle;
const uint16_t address = cycle.address ? *cycle.address : 0x0000;
// Calculate an access penalty, if applicable.
//
// Rule applied here, which is slightly inferred:
//
// Non-video reads and writes are delayed by exactly a cycle or not delayed at all,
// depending on the programmer's configuration of Dave.
//
// Video reads and writes, and Nick port accesses, are delayed so that the last
// clock cycle of the machine cycle falls wholly inside the designated Z80 access
// window, per Nick.
//
// The switch statement below just attempts to implement that logic.
//
HalfCycles penalty;
switch(cycle.operation) {
default: break;
// For non-video pauses, insert during the initial part of the bus cycle.
case PartialMachineCycle::ReadStart:
case PartialMachineCycle::WriteStart:
if(!is_video_[address >> 14] && wait_mode_ == WaitMode::OnAllAccesses) {
penalty = dave_delay_;
}
break;
case PartialMachineCycle::ReadOpcodeStart: {
if(is_video_[address >> 14]) {
// Query Nick for the amount of delay that would occur with one cycle left
// in this read opcode.
const auto delay_time = nick_.time_since_flush(HalfCycles(2));
const auto delay = nick_.last_valid()->get_time_until_z80_slot(delay_time);
penalty = nick_.back_map(delay, delay_time);
} else if(wait_mode_ != WaitMode::None) {
penalty = dave_delay_;
}
} break;
// Video pauses: insert right at the end of the bus cycle.
case PartialMachineCycle::Write:
// Ensure all video that should have been collected prior to
// this write has been.
if(is_video_[address >> 14]) {
nick_.flush();
}
[[fallthrough]];
case PartialMachineCycle::Read:
if(is_video_[address >> 14]) {
// Get delay, in Nick cycles, for a Z80 access that occurs in 0.5
// cycles from now (i.e. with one cycle left to run).
const auto delay_time = nick_.time_since_flush(HalfCycles(1));
const auto delay = nick_.last_valid()->get_time_until_z80_slot(delay_time);
penalty = nick_.back_map(delay, delay_time);
}
break;
case PartialMachineCycle::Input:
case PartialMachineCycle::Output: {
if((address & 0xf0) == 0x80) {
// Get delay, in Nick cycles, for a Z80 access that occurs in 0.5
// cycles from now (i.e. with one cycle left to run).
const auto delay_time = nick_.time_since_flush(HalfCycles(1));
const auto delay = nick_.last_valid()->get_time_until_z80_slot(delay_time);
penalty = nick_.back_map(delay, delay_time);
}
}
}
const HalfCycles full_length = cycle.length + penalty;
time_since_audio_update_ += full_length;
advance_nick(full_length);
if(dave_timer_ += full_length) {
set_interrupts(dave_timer_.last_valid()->get_new_interrupts(), dave_timer_.last_sequence_point_overrun());
}
// The WD/etc runs at a nominal 8Mhz.
if constexpr (has_disk_controller) {
exdos_.run_for(Cycles(full_length.as_integral()));
}
switch(cycle.operation) {
default: break;
case PartialMachineCycle::Interrupt:
*cycle.value = 0xff;
break;
case PartialMachineCycle::Input:
switch(address & 0xff) {
default:
LOG("Unhandled input from " << PADHEX(2) << (address & 0xff));
*cycle.value = 0xff;
break;
case 0x10: case 0x11: case 0x12: case 0x13:
case 0x14: case 0x15: case 0x16: case 0x17:
if constexpr (has_disk_controller) {
*cycle.value = exdos_.read(address);
} else {
*cycle.value = 0xff;
}
break;
case 0x18: case 0x19: case 0x1a: case 0x1b:
case 0x1c: case 0x1d: case 0x1e: case 0x1f:
if constexpr (has_disk_controller) {
*cycle.value = exdos_.get_control_register();
} else {
*cycle.value = 0xff;
}
break;
case 0x80: case 0x81: case 0x82: case 0x83:
case 0x84: case 0x85: case 0x86: case 0x87:
case 0x88: case 0x89: case 0x8a: case 0x8b:
case 0x8c: case 0x8d: case 0x8e: case 0x8f:
*cycle.value = nick_->read();
break;
case 0xb0: *cycle.value = pages_[0]; break;
case 0xb1: *cycle.value = pages_[1]; break;
case 0xb2: *cycle.value = pages_[2]; break;
case 0xb3: *cycle.value = pages_[3]; break;
case 0xb4:
*cycle.value =
(nick_->get_interrupt_line() ? 0x10 : 0x00) |
dave_timer_->get_divider_state() |
interrupt_state_;
break;
case 0xb5:
if(active_key_line_ < key_lines_.size()) {
*cycle.value = key_lines_[active_key_line_];
} else {
*cycle.value = 0xff;
}
break;
case 0xb6: {
// TODO: selected keyboard row, 0 to 9, should return one bit of joystick
// input. That being the case:
//
// b0: joystick input
// b1, b2: unused (in theory read from control port, but not used by any hardware)
// b3: 0 = printer ready; 1 = not ready
// b4: serial, data in
// b5: serial, status in
// b6: tape input volume level, 0 = high, 1 = low
// b7: tape data input
*cycle.value = 0xff;
} break;
}
break;
case PartialMachineCycle::Output:
switch(address & 0xff) {
default:
LOG("Unhandled output: " << PADHEX(2) << *cycle.value << " to " << PADHEX(2) << (address & 0xff));
break;
case 0x10: case 0x11: case 0x12: case 0x13:
case 0x14: case 0x15: case 0x16: case 0x17:
if constexpr(has_disk_controller) {
exdos_.write(address, *cycle.value);
}
break;
case 0x18: case 0x19: case 0x1a: case 0x1b:
case 0x1c: case 0x1d: case 0x1e: case 0x1f:
if constexpr(has_disk_controller) {
exdos_.set_control_register(*cycle.value);
}
break;
case 0x80: case 0x81: case 0x82: case 0x83:
case 0x84: case 0x85: case 0x86: case 0x87:
case 0x88: case 0x89: case 0x8a: case 0x8b:
case 0x8c: case 0x8d: case 0x8e: case 0x8f:
nick_->write(address, *cycle.value);
break;
case 0xb0: page<0>(*cycle.value); break;
case 0xb1: page<1>(*cycle.value); break;
case 0xb2: page<2>(*cycle.value); break;
case 0xb3: page<3>(*cycle.value); break;
case 0xbf:
switch((*cycle.value >> 2)&3) {
default: wait_mode_ = WaitMode::None; break;
case 0: wait_mode_ = WaitMode::OnAllAccesses; break;
case 1: wait_mode_ = WaitMode::OnM1; break;
}
// Dave delays (i.e. those affecting memory areas not associated with Nick)
// are one cycle in 8Mhz mode, two cycles in 12Mhz mode.
dave_delay_ = HalfCycles(2 + (*cycle.value)&2);
[[fallthrough]];
case 0xa0: case 0xa1: case 0xa2: case 0xa3:
case 0xa4: case 0xa5: case 0xa6: case 0xa7:
case 0xa8: case 0xa9: case 0xaa: case 0xab:
case 0xac: case 0xad: case 0xae: case 0xaf:
update_audio();
dave_audio_.write(address, *cycle.value);
dave_timer_->write(address, *cycle.value);
break;
case 0xb4:
interrupt_mask_ = *cycle.value & 0x55;
interrupt_state_ &= ~*cycle.value;
update_interrupts();
break;
case 0xb5:
// Logic here: the ROM scans the keyboard by checking ascending
// lines. It also seems to provide a line of 0 when using port B5
// for non-keyboard uses.
//
// So: use the rollover from line 9 back to line 0 as a trigger to
// spot that a scan of the keyboard just finished. Which makes it
// time to enqueue the next keypress.
//
// Re: should_skip_splash_screen_ and typer_delay_, assume that a
// single keypress is necessary to get past the Enterprise splash
// screen, then a pause in keypressing while BASIC or whatever
// starts up, then presses can resume.
if(active_key_line_ == 9 && !(*cycle.value & 0xf) && (should_skip_splash_screen_ || typer_)) {
if(should_skip_splash_screen_) {
set_key_state(uint16_t(Key::Space), typer_delay_);
if(typer_delay_) {
--typer_delay_;
} else {
typer_delay_ = 60;
should_skip_splash_screen_ = false;
}
} else {
if(!typer_delay_) {
if(!typer_->type_next_character()) {
clear_all_keys();
typer_ = nullptr;
}
} else {
--typer_delay_;
}
}
}
active_key_line_ = *cycle.value & 0xf;
// TODO:
//
// b4: strobe output for printer
// b5: tape sound control (?)
// b6: tape motor control 1, 1 = on
// b7: tape motor control 2, 1 = on
break;
case 0xb6:
// Just 8 bits of printer data.
LOG("TODO: printer output " << PADHEX(2) << *cycle.value);
break;
case 0xb7:
// b0 = serial data out
// b1 = serial status out
LOG("TODO: serial output " << PADHEX(2) << *cycle.value);
break;
}
break;
case PartialMachineCycle::Read:
case PartialMachineCycle::ReadOpcode:
if(read_pointers_[address >> 14]) {
*cycle.value = read_pointers_[address >> 14][address];
} else {
*cycle.value = 0xff;
}
break;
case PartialMachineCycle::Write:
if(write_pointers_[address >> 14]) {
write_pointers_[address >> 14][address] = *cycle.value;
}
break;
}
return penalty;
}
void flush() {
nick_.flush();
update_audio();
audio_queue_.perform();
}
private:
// MARK: - Memory layout
std::array<uint8_t, 256 * 1024> ram_{};
std::array<uint8_t, 64 * 1024> exos_;
std::array<uint8_t, 16 * 1024> basic_;
std::array<uint8_t, 16 * 1024> exdos_rom_;
std::array<uint8_t, 32 * 1024> epdos_rom_;
const uint8_t min_ram_slot_;
const uint8_t *read_pointers_[4] = {nullptr, nullptr, nullptr, nullptr};
uint8_t *write_pointers_[4] = {nullptr, nullptr, nullptr, nullptr};
uint8_t pages_[4] = {0x80, 0x80, 0x80, 0x80};
template <size_t slot> void page(uint8_t offset) {
pages_[slot] = offset;
#define Map(location, source) \
if(offset >= location && offset < location + source.size() / 0x4000) { \
page<slot>(&source[(offset - location) * 0x4000], nullptr); \
is_video_[slot] = false; \
return; \
}
Map(0, exos_);
Map(16, basic_);
Map(32, exdos_rom_);
Map(48, epdos_rom_);
#undef Map
// Of whatever size of RAM I've declared above, use only the final portion.
// This correlated with Nick always having been handed the final 64kb and,
// at least while the RAM is the first thing declared above, does a little
// to benefit data locality. Albeit not in a useful sense.
if(offset >= min_ram_slot_) {
const auto ram_floor = 4194304 - ram_.size();
const size_t address = offset * 0x4000 - ram_floor;
is_video_[slot] = offset >= 0xfc; // TODO: this hard-codes a 64kb video assumption.
page<slot>(&ram_[address], &ram_[address]);
return;
}
page<slot>(nullptr, nullptr);
}
template <size_t slot> void page(const uint8_t *read, uint8_t *write) {
read_pointers_[slot] = read ? read - (slot * 0x4000) : nullptr;
write_pointers_[slot] = write ? write - (slot * 0x4000) : nullptr;
}
// MARK: - Memory Timing
// The wait mode affects all memory accesses _outside of the video area_.
enum class WaitMode {
None,
OnM1,
OnAllAccesses
} wait_mode_ = WaitMode::OnAllAccesses;
bool is_video_[4]{};
// MARK: - ScanProducer
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
nick_.last_valid()->set_scan_target(scan_target);
}
Outputs::Display::ScanStatus get_scaled_scan_status() const override {
return nick_.last_valid()->get_scaled_scan_status();
}
void set_display_type(Outputs::Display::DisplayType display_type) final {
nick_.last_valid()->set_display_type(display_type);
}
Outputs::Display::DisplayType get_display_type() const final {
return nick_.last_valid()->get_display_type();
}
// MARK: - AudioProducer
Outputs::Speaker::Speaker *get_speaker() final {
return &speaker_;
}
// MARK: - TimedMachine
void run_for(const Cycles cycles) override {
z80_.run_for(cycles);
}
// MARK: - KeyboardMachine
Enterprise::KeyboardMapper keyboard_mapper_;
KeyboardMapper *get_keyboard_mapper() final {
return &keyboard_mapper_;
}
uint8_t active_key_line_ = 0;
std::array<uint8_t, 10> key_lines_;
void set_key_state(uint16_t key, bool is_pressed) final {
if(is_pressed) {
key_lines_[key >> 8] &= ~uint8_t(key);
} else {
key_lines_[key >> 8] |= uint8_t(key);
}
}
void clear_all_keys() final {
key_lines_.fill(0xff);
}
// MARK: - Utility::TypeRecipient
void type_string(const std::string &string) final {
Utility::TypeRecipient<CharacterMapper>::add_typer(string);
if(z80_.get_is_resetting()) {
should_skip_splash_screen_ = true;
typer_delay_ = 1;
} else {
should_skip_splash_screen_ = false;
typer_delay_ = 0;
}
}
bool can_type(char c) const final {
return Utility::TypeRecipient<CharacterMapper>::can_type(c);
}
bool should_skip_splash_screen_ = false;
int typer_delay_ = 30;
// MARK: - MediaTarget
bool insert_media(const Analyser::Static::Media &media) final {
if constexpr (has_disk_controller) {
if(!media.disks.empty()) {
exdos_.set_disk(media.disks.front(), 0);
}
}
return true;
}
// MARK: - Interrupts
uint8_t interrupt_mask_ = 0x00, interrupt_state_ = 0x00;
void set_interrupts(uint8_t mask, HalfCycles offset = HalfCycles(0)) {
interrupt_state_ |= uint8_t(mask);
update_interrupts(offset);
}
void update_interrupts(HalfCycles offset = HalfCycles(0)) {
z80_.set_interrupt_line((interrupt_state_ >> 1) & interrupt_mask_, offset);
}
// MARK: - Chips.
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
NickType nick_;
bool previous_nick_interrupt_line_ = false;
// Cf. timing guesses above.
Concurrency::DeferringAsyncTaskQueue audio_queue_;
Dave::Audio dave_audio_;
Outputs::Speaker::LowpassSpeaker<Dave::Audio> speaker_;
HalfCycles time_since_audio_update_;
HalfCycles dave_delay_ = HalfCycles(2);
// The divider supplied to the JustInTimeActor and the manual divider used in
// update_audio() should match.
static constexpr int dave_divider = 8;
JustInTimeActor<Dave::TimedInterruptSource, HalfCycles, 1, dave_divider> dave_timer_;
inline void update_audio() {
speaker_.run_for(audio_queue_, time_since_audio_update_.divide_cycles(Cycles(dave_divider)));
}
// MARK: - EXDos card.
EXDos exdos_;
// MARK: - Activity Source
void set_activity_observer(Activity::Observer *observer) final {
if constexpr (has_disk_controller) {
exdos_.set_activity_observer(observer);
}
}
// MARK: - Configuration options.
std::unique_ptr<Reflection::Struct> get_options() final {
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
options->output = get_video_signal_configurable();
return options;
}
void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
const auto options = dynamic_cast<Options *>(str.get());
set_video_signal_configurable(options->output);
}
};
}
using namespace Enterprise;
Machine *Machine::Enterprise(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
using Target = Analyser::Static::Enterprise::Target;
const Target *const enterprise_target = dynamic_cast<const Target *>(target);
#define BuildMachine(exdos, sixmhz) \
if((enterprise_target->dos == Target::DOS::None) != exdos && (enterprise_target->speed == Target::Speed::SixMHz) == sixmhz) { \
return new Enterprise::ConcreteMachine<exdos, sixmhz>(*enterprise_target, rom_fetcher); \
}
BuildMachine(false, false);
BuildMachine(false, true);
BuildMachine(true, false);
BuildMachine(true, true);
#undef BuildMachine
return nullptr;
}
Machine::~Machine() {}

View File

@@ -0,0 +1,47 @@
//
// Enterprise.hpp
// Clock Signal
//
// Created by Thomas Harte on 10/06/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef Enterprise_hpp
#define Enterprise_hpp
#include "../../Analyser/Static/StaticAnalyser.hpp"
#include "../../Configurable/Configurable.hpp"
#include "../../Configurable/StandardOptions.hpp"
#include "../ROMMachine.hpp"
namespace Enterprise {
/*!
@abstract Represents an Elan Enterprise.
@discussion An instance of Enterprise::Machine represents the current state of an
Elan Enterprise.
*/
class Machine {
public:
virtual ~Machine();
static Machine *Enterprise(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);
/// Defines the runtime options available for an Enterprise.
class Options: public Reflection::StructImpl<Options>, public Configurable::DisplayOption<Options> {
friend Configurable::DisplayOption<Options>;
public:
Options(Configurable::OptionsType type) :
Configurable::DisplayOption<Options>(type == Configurable::OptionsType::UserFriendly ? Configurable::Display::RGB : Configurable::Display::CompositeColour) {
if(needs_declare()) {
declare_display_option();
limit_enum(&output, Configurable::Display::RGB, Configurable::Display::CompositeColour, Configurable::Display::CompositeMonochrome, -1);
}
}
};
};
};
#endif /* Enterprise_hpp */

View File

@@ -0,0 +1,151 @@
//
// Keyboard.cpp
// Clock Signal
//
// Created by Thomas Harte on 17/06/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "Keyboard.hpp"
using namespace Enterprise;
uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) const {
#define BIND(source, dest) case Inputs::Keyboard::Key::source: return uint16_t(Key::dest)
switch(key) {
default: break;
BIND(Backslash, Backslash);
BIND(CapsLock, Lock);
BIND(Tab, Tab);
BIND(Escape, Escape);
BIND(Hyphen, Hyphen);
BIND(Equals, Caret);
BIND(Backspace, Erase);
BIND(Delete, Delete);
BIND(Semicolon, Semicolon);
BIND(Quote, Colon);
BIND(OpenSquareBracket, OpenSquareBracket);
BIND(CloseSquareBracket, CloseSquareBracket);
BIND(End, Stop);
BIND(Insert, Insert);
BIND(BackTick, At);
BIND(k1, k1); BIND(k2, k2); BIND(k3, k3); BIND(k4, k4); BIND(k5, k5);
BIND(k6, k6); BIND(k7, k7); BIND(k8, k8); BIND(k9, k9); BIND(k0, k0);
BIND(F1, F1); BIND(F2, F2); BIND(F3, F3); BIND(F4, F4);
BIND(F5, F5); BIND(F6, F6); BIND(F7, F7); BIND(F8, F8);
BIND(Keypad1, F1); BIND(Keypad2, F2); BIND(Keypad3, F3); BIND(Keypad4, F4);
BIND(Keypad5, F5); BIND(Keypad6, F6); BIND(Keypad7, F7); BIND(Keypad8, F8);
BIND(Q, Q); BIND(W, W); BIND(E, E); BIND(R, R); BIND(T, T);
BIND(Y, Y); BIND(U, U); BIND(I, I); BIND(O, O); BIND(P, P);
BIND(A, A); BIND(S, S); BIND(D, D); BIND(F, F); BIND(G, G);
BIND(H, H); BIND(J, J); BIND(K, K); BIND(L, L);
BIND(Z, Z); BIND(X, X); BIND(C, C); BIND(V, V);
BIND(B, B); BIND(N, N); BIND(M, M);
BIND(FullStop, FullStop);
BIND(Comma, Comma);
BIND(ForwardSlash, ForwardSlash);
BIND(Space, Space); BIND(Enter, Enter);
BIND(LeftShift, LeftShift);
BIND(RightShift, RightShift);
BIND(LeftOption, Alt);
BIND(RightOption, Alt);
BIND(LeftControl, Control);
BIND(RightControl, Control);
BIND(Left, Left);
BIND(Right, Right);
BIND(Up, Up);
BIND(Down, Down);
}
#undef BIND
return MachineTypes::MappedKeyboardMachine::KeyNotMapped;
}
const uint16_t *CharacterMapper::sequence_for_character(char character) const {
#define KEYS(x) {uint16_t(x), MachineTypes::MappedKeyboardMachine::KeyEndSequence}
#define SHIFT(x) {uint16_t(Key::LeftShift), uint16_t(x), MachineTypes::MappedKeyboardMachine::KeyEndSequence}
#define _ {MachineTypes::MappedKeyboardMachine::KeyNotMapped}
static KeySequence key_sequences[] = {
/* NUL */ _, /* SOH */ _,
/* STX */ _, /* ETX */ _,
/* EOT */ _, /* ENQ */ _,
/* ACK */ _, /* BEL */ _,
/* BS */ KEYS(Key::Erase), /* HT */ KEYS(Key::Tab),
/* LF */ KEYS(Key::Enter), /* VT */ _,
/* FF */ _, /* CR */ KEYS(Key::Enter),
/* SO */ _, /* SI */ _,
/* DLE */ _, /* DC1 */ _,
/* DC2 */ _, /* DC3 */ _,
/* DC4 */ _, /* NAK */ _,
/* SYN */ _, /* ETB */ _,
/* CAN */ _, /* EM */ _,
/* SUB */ _, /* ESC */ KEYS(Key::Escape),
/* FS */ _, /* GS */ _,
/* RS */ _, /* US */ _,
/* space */ KEYS(Key::Space), /* ! */ SHIFT(Key::k1),
/* " */ SHIFT(Key::k2), /* # */ _,
/* $ */ SHIFT(Key::k4), /* % */ SHIFT(Key::k5),
/* & */ SHIFT(Key::k6), /* ' */ SHIFT(Key::k7),
/* ( */ SHIFT(Key::k8), /* ) */ SHIFT(Key::k9),
/* * */ SHIFT(Key::Colon), /* + */ SHIFT(Key::Semicolon),
/* , */ KEYS(Key::Comma), /* - */ KEYS(Key::Hyphen),
/* . */ KEYS(Key::FullStop), /* / */ KEYS(Key::ForwardSlash),
/* 0 */ KEYS(Key::k0), /* 1 */ KEYS(Key::k1),
/* 2 */ KEYS(Key::k2), /* 3 */ KEYS(Key::k3),
/* 4 */ KEYS(Key::k4), /* 5 */ KEYS(Key::k5),
/* 6 */ KEYS(Key::k6), /* 7 */ KEYS(Key::k7),
/* 8 */ KEYS(Key::k8), /* 9 */ KEYS(Key::k9),
/* : */ KEYS(Key::Colon), /* ; */ KEYS(Key::Semicolon),
/* < */ SHIFT(Key::Comma), /* = */ SHIFT(Key::Hyphen),
/* > */ SHIFT(Key::FullStop), /* ? */ SHIFT(Key::ForwardSlash),
/* @ */ KEYS(Key::At), /* A */ SHIFT(Key::A),
/* B */ SHIFT(Key::B), /* C */ SHIFT(Key::C),
/* D */ SHIFT(Key::D), /* E */ SHIFT(Key::E),
/* F */ SHIFT(Key::F), /* G */ SHIFT(Key::G),
/* H */ SHIFT(Key::H), /* I */ SHIFT(Key::I),
/* J */ SHIFT(Key::J), /* K */ SHIFT(Key::K),
/* L */ SHIFT(Key::L), /* M */ SHIFT(Key::M),
/* N */ SHIFT(Key::N), /* O */ SHIFT(Key::O),
/* P */ SHIFT(Key::P), /* Q */ SHIFT(Key::Q),
/* R */ SHIFT(Key::R), /* S */ SHIFT(Key::S),
/* T */ SHIFT(Key::T), /* U */ SHIFT(Key::U),
/* V */ SHIFT(Key::V), /* W */ SHIFT(Key::W),
/* X */ SHIFT(Key::X), /* Y */ SHIFT(Key::Y),
/* Z */ SHIFT(Key::Z), /* [ */ KEYS(Key::OpenSquareBracket),
/* \ */ KEYS(Key::Backslash), /* ] */ KEYS(Key::CloseSquareBracket),
/* ^ */ SHIFT(Key::Caret), /* _ */ SHIFT(Key::k0),
/* ` */ SHIFT(Key::At), /* a */ KEYS(Key::A),
/* b */ KEYS(Key::B), /* c */ KEYS(Key::C),
/* d */ KEYS(Key::D), /* e */ KEYS(Key::E),
/* f */ KEYS(Key::F), /* g */ KEYS(Key::G),
/* h */ KEYS(Key::H), /* i */ KEYS(Key::I),
/* j */ KEYS(Key::J), /* k */ KEYS(Key::K),
/* l */ KEYS(Key::L), /* m */ KEYS(Key::M),
/* n */ KEYS(Key::N), /* o */ KEYS(Key::O),
/* p */ KEYS(Key::P), /* q */ KEYS(Key::Q),
/* r */ KEYS(Key::R), /* s */ KEYS(Key::S),
/* t */ KEYS(Key::T), /* u */ KEYS(Key::U),
/* v */ KEYS(Key::V), /* w */ KEYS(Key::W),
/* x */ KEYS(Key::X), /* y */ KEYS(Key::Y),
/* z */ KEYS(Key::Z), /* { */ SHIFT(Key::OpenSquareBracket),
/* | */ SHIFT(Key::Backslash), /* } */ SHIFT(Key::CloseSquareBracket),
/* ~ */ SHIFT(Key::Caret)
};
#undef _
#undef SHIFT
#undef KEYS
return table_lookup_sequence_for_character(key_sequences, character);
}

View File

@@ -0,0 +1,66 @@
//
// Keyboard.hpp
// Clock Signal
//
// Created by Thomas Harte on 17/06/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef Machines_Enterprise_Keyboard_hpp
#define Machines_Enterprise_Keyboard_hpp
#include "../KeyboardMachine.hpp"
#include "../Utility/Typer.hpp"
namespace Enterprise {
#define KeyCode(line, mask) (line << 8) | mask
enum class Key: uint16_t {
N = 0x0000 | 0x01, Backslash = 0x0000 | 0x02, B = 0x0000 | 0x04, C = 0x0000 | 0x08,
V = 0x0000 | 0x10, X = 0x0000 | 0x20, Z = 0x0000 | 0x40, LeftShift = 0x0000 | 0x80,
H = 0x0100 | 0x01, Lock = 0x0100 | 0x02, G = 0x0100 | 0x04, D = 0x0100 | 0x08,
F = 0x0100 | 0x10, S = 0x0100 | 0x20, A = 0x0100 | 0x40, Control = 0x0100 | 0x80,
U = 0x0200 | 0x01, Q = 0x0200 | 0x02, Y = 0x0200 | 0x04, R = 0x0200 | 0x08,
T = 0x0200 | 0x10, E = 0x0200 | 0x20, W = 0x0200 | 0x40, Tab = 0x0200 | 0x80,
k7 = 0x0300 | 0x01, k1 = 0x0300 | 0x02, k6 = 0x0300 | 0x04, k4 = 0x0300 | 0x08,
k5 = 0x0300 | 0x10, k3 = 0x0300 | 0x20, k2 = 0x0300 | 0x40, Escape = 0x0300 | 0x80,
F4 = 0x0400 | 0x01, F8 = 0x0400 | 0x02, F3 = 0x0400 | 0x04, F6 = 0x0400 | 0x08,
F5 = 0x0400 | 0x10, F7 = 0x0400 | 0x20, F2 = 0x0400 | 0x40, F1 = 0x0400 | 0x80,
k8 = 0x0500 | 0x01, k9 = 0x0500 | 0x04, Hyphen = 0x0500 | 0x08,
k0 = 0x0500 | 0x10, Caret = 0x0500 | 0x20, Erase = 0x0500 | 0x40,
J = 0x0600 | 0x01, K = 0x0600 | 0x04, Semicolon = 0x0600 | 0x08,
L = 0x0600 | 0x10, Colon = 0x0600 | 0x20, CloseSquareBracket = 0x0600 | 0x40,
Stop = 0x0700 | 0x01, Down = 0x0700 | 0x02, Right = 0x0700 | 0x04, Up = 0x0700 | 0x08,
Hold = 0x0700 | 0x10, Left = 0x0700 | 0x20, Enter = 0x0700 | 0x40, Alt = 0x0700 | 0x80,
M = 0x0800 | 0x01, Delete = 0x0800 | 0x02, Comma = 0x0800 | 0x04,
ForwardSlash = 0x0800 | 0x08,
FullStop = 0x0800 | 0x10,
RightShift = 0x0800 | 0x20, Space = 0x0800 | 0x40, Insert = 0x0800 | 0x80,
I = 0x0900 | 0x01, O = 0x0900 | 0x04, At = 0x0900 | 0x08,
P = 0x0900 | 0x10,
OpenSquareBracket = 0x0900 | 0x20
};
#undef KeyCode
struct KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const final;
};
struct CharacterMapper: public ::Utility::CharacterMapper {
const uint16_t *sequence_for_character(char character) const override;
};
}
#endif /* Keyboard_hpp */

View File

@@ -0,0 +1,629 @@
//
// Nick.cpp
// Clock Signal
//
// Created by Thomas Harte on 14/06/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "Nick.hpp"
#include <cstdio>
namespace {
uint16_t mapped_colour(uint8_t source) {
// On the Enterprise, red and green are 3-bit quantities; blue is a 2-bit quantity.
int red = ((source&0x01) << 2) | ((source&0x08) >> 2) | ((source&0x40) >> 6);
int green = ((source&0x02) << 1) | ((source&0x10) >> 3) | ((source&0x80) >> 7);
int blue = ((source&0x04) >> 1) | ((source&0x20) >> 5);
assert(red <= 7);
assert(green <= 7);
assert(blue <= 3);
red = (red << 1) + (red >> 3);
green = (green << 1) + (green >> 3);
blue = (blue << 2) + blue;
assert(red <= 15);
assert(green <= 15);
assert(blue <= 15);
// Duplicate bits where necessary to map to a full 4-bit range per channel.
const uint8_t parts[2] = {
uint8_t(
red
),
uint8_t(
(green << 4) + blue
)
};
return *reinterpret_cast<const uint16_t *>(parts);
}
}
using namespace Enterprise;
Nick::Nick(const uint8_t *ram) :
crt_(57*16, 16, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red4Green4Blue4),
ram_(ram) {
// Just use RGB for now.
set_display_type(Outputs::Display::DisplayType::RGB);
// Crop to the centre 90% of the display.
crt_.set_visible_area(Outputs::Display::Rect(0.05f, 0.05f, 0.9f, 0.9f));
}
void Nick::write(uint16_t address, uint8_t value) {
switch(address & 3) {
case 0:
// Ignored: everything to do with external colour.
for(int c = 0; c < 8; c++) {
palette_[c + 8] = mapped_colour(uint8_t(((value & 0x1f) << 3) + c));
}
break;
case 1:
if(output_type_ == OutputType::Border) {
set_output_type(OutputType::Border, true);
}
border_colour_ = mapped_colour(value);
break;
case 2:
line_parameter_base_ = uint16_t((line_parameter_base_ & 0xf000) | (value << 4));
break;
case 3:
line_parameter_base_ = uint16_t((line_parameter_base_ & 0x0ff0) | (value << 12));
// Still a mystery to me: the exact meaning of the top two bits here. For now
// just treat a 0 -> 1 transition of the MSB as a forced frame restart.
if((value^line_parameter_control_) & value & 0x80) {
// For now: just force this to be the final line of this mode block.
// I'm unclear whether I should also reset the horizontal counter
// (i.e. completely abandon current video phase).
lines_remaining_ = 0xff;
should_reload_line_parameters_ = true;
}
line_parameter_control_ = value & 0xc0;
break;
}
}
uint8_t Nick::read() {
return last_read_;
}
Cycles Nick::get_time_until_z80_slot(Cycles after_period) const {
// Place Z80 accesses in the first six cycles in each sixteen-cycle window.
// That models video accesses as being the final ten. Which has the net effect
// of responding to the line parameter table interrupt flag as soon as it's
// loaded.
// Assumed below: the Z80 can start its final cycle anywhere in the first three
// of the permitted six.
const int offset = (horizontal_counter_ + after_period.as<int>()) & 15;
if(offset < 3) {
return 0;
} else {
return 16 - offset;
}
}
void Nick::run_for(Cycles duration) {
constexpr int line_length = 912;
#define add_window(x) \
line_data_pointer_[0] += is_sync_or_pixels_ * line_data_per_column_increments_[0] * (x); \
line_data_pointer_[1] += is_sync_or_pixels_ * line_data_per_column_increments_[1] * (x); \
window += x; \
if(window != 57 && window == left_margin_) is_sync_or_pixels_ = true; \
if(window != 57 && window == right_margin_) is_sync_or_pixels_ = false;
int clocks_remaining = duration.as<int>();
while(clocks_remaining) {
// Determine how many cycles are left this line.
const int clocks_this_line = std::min(clocks_remaining, line_length - horizontal_counter_);
// Convert that into a [start/current] and end window.
int window = horizontal_counter_ >> 4;
const int end_window = (horizontal_counter_ + clocks_this_line) >> 4;
// Advance the line counters.
clocks_remaining -= clocks_this_line;
horizontal_counter_ = (horizontal_counter_ + clocks_this_line) % line_length;
// Do nothing if a window boundary isn't crossed.
if(window == end_window) continue;
// HSYNC is signalled for four windows at the start of the line.
// I currently believe this happens regardless of Vsync mode.
if(!window) {
set_output_type(OutputType::Sync);
// There's no increment to get to 0, it happens when the horizontal_counter_
// is reset. So test for active bit effect manually.
if(!left_margin_) is_sync_or_pixels_ = true;
if(!right_margin_) is_sync_or_pixels_ = false;
}
// Default to noting read.
last_read_ = 0xff;
while(window < 4 && window < end_window) {
if(should_reload_line_parameters_) {
switch(window) {
// First slot: line count, mode and interrupt flag.
case 0:
// Byte 0: lines remaining.
lines_remaining_ = ram_[line_parameter_pointer_];
// Byte 1: current interrupt output plus graphics modes...
last_read_ = ram_[line_parameter_pointer_ + 1];
// Set the new interrupt line output.
interrupt_line_ = ram_[line_parameter_pointer_ + 1] & 0x80;
// Determine the mode and depth, and hence the column size.
mode_ = Mode((ram_[line_parameter_pointer_ + 1] >> 1)&7);
bpp_ = 1 << ((ram_[line_parameter_pointer_ + 1] >> 5)&3);
switch(mode_) {
default:
case Mode::Pixel:
column_size_ = 16 / bpp_;
line_data_per_column_increments_[0] = 2;
line_data_per_column_increments_[1] = 0;
break;
case Mode::LPixel:
case Mode::CH64:
case Mode::CH128:
case Mode::CH256:
column_size_ = 8 / bpp_;
line_data_per_column_increments_[0] = 1;
line_data_per_column_increments_[1] = 0;
break;
case Mode::Attr:
column_size_ = 8;
line_data_per_column_increments_[0] = 1;
line_data_per_column_increments_[1] = 1;
break;
}
vres_ = ram_[line_parameter_pointer_ + 1] & 0x10;
reload_line_parameter_pointer_ = ram_[line_parameter_pointer_ + 1] & 0x01;
break;
// Second slot: margins and ALT/IND bits.
case 1:
// Determine the margins.
left_margin_ = ram_[line_parameter_pointer_ + 2] & 0x3f;
right_margin_ = ram_[line_parameter_pointer_ + 3] & 0x3f;
last_read_ = ram_[line_parameter_pointer_ + 3];
// Set up the alternative palettes,
switch(mode_) {
default:
break;
// NB: LSBALT/MSBALT and ALTIND0/ALTIND1 appear to have opposite effects on palette selection.
case Mode::Pixel:
case Mode::LPixel: {
const uint8_t flags = ram_[line_parameter_pointer_ + 2];
// Use MSBALT and LSBALT to pick the alt_ind_palettes.
//
// LSBALT = b6 of params[2], if set => character codes with bit 6 set should use palette indices 4... instead of 0... .
// MSBALT = b7 of params[2], if set => character codes with bit 7 set should use palette indices 2 and 3.
two_colour_mask_ = 0xff &~ (((flags&0x80) >> 7) | ((flags&0x40) << 1));
alt_ind_palettes[0] = palette_;
alt_ind_palettes[2] = alt_ind_palettes[0] + ((flags & 0x80) ? 2 : 0);
alt_ind_palettes[1] = alt_ind_palettes[0] + ((flags & 0x40) ? 4 : 0);
alt_ind_palettes[3] = alt_ind_palettes[2] + ((flags & 0x40) ? 4 : 0);
} break;
case Mode::CH64:
case Mode::CH128:
case Mode::CH256: {
const uint8_t flags = ram_[line_parameter_pointer_ + 3];
// Use ALTIND0 and ALTIND1 to pick the alt_ind_palettes.
//
// ALTIND1 = b6 of params[3], if set => character codes with bit 7 set should use palette indices 2 and 3.
// ALTIND0 = b7 of params[3], if set => character codes with bit 6 set should use palette indices 4... instead of 0... .
alt_ind_palettes[0] = palette_;
alt_ind_palettes[2] = alt_ind_palettes[0] + ((flags & 0x40) ? 2 : 0);
alt_ind_palettes[1] = alt_ind_palettes[0] + ((flags & 0x80) ? 4 : 0);
alt_ind_palettes[3] = alt_ind_palettes[2] + ((flags & 0x80) ? 4 : 0);
} break;
}
break;
// Third slot: Line data pointer 1.
case 2:
start_line_data_pointer_[0] = ram_[line_parameter_pointer_ + 4];
start_line_data_pointer_[0] |= ram_[line_parameter_pointer_ + 5] << 8;
line_data_pointer_[0] = start_line_data_pointer_[0];
last_read_ = ram_[line_parameter_pointer_ + 5];
break;
// Fourth slot: Line data pointer 2.
case 3:
start_line_data_pointer_[1] = ram_[line_parameter_pointer_ + 6];
start_line_data_pointer_[1] |= ram_[line_parameter_pointer_ + 7] << 8;
line_data_pointer_[1] = start_line_data_pointer_[1];
last_read_ = ram_[line_parameter_pointer_ + 7];
break;
}
}
++output_duration_;
add_window(1);
}
if(window == 4) {
if(mode_ == Mode::Vsync) {
set_output_type(is_sync_or_pixels_ ? OutputType::Sync : OutputType::Blank);
} else {
set_output_type(OutputType::Blank);
}
}
// Deal with vsync mode out here.
if(mode_ == Mode::Vsync) {
if(window >= 4) {
while(window < end_window) {
// Skip straight to the next event.
int next_event = end_window;
if(window < left_margin_) next_event = std::min(next_event, left_margin_);
if(window < right_margin_) next_event = std::min(next_event, right_margin_);
output_duration_ += next_event - window;
add_window(next_event - window);
set_output_type(is_sync_or_pixels_ ? OutputType::Sync : OutputType::Blank);
}
}
} else {
// If present then the colour burst is output for the period from
// the start of window 6 to the end of window 10.
//
// The first 8 palette entries also need to be fetched here.
while(window < first_pixel_window_ && window < end_window) {
if(window == 6) {
set_output_type(OutputType::ColourBurst);
}
if(should_reload_line_parameters_ && window < 8) {
const int base = (window - 4) << 1;
assert(base < 7);
palette_[base] = mapped_colour(ram_[line_parameter_pointer_ + base + 8]);
palette_[base + 1] = mapped_colour(ram_[line_parameter_pointer_ + base + 9]);
last_read_ = ram_[line_parameter_pointer_ + base + 9];
}
++output_duration_;
add_window(1);
}
if(window >= first_pixel_window_) {
if(window == first_pixel_window_) {
set_output_type(is_sync_or_pixels_ ? OutputType::Pixels : OutputType::Border);
}
while(window < end_window) {
int next_event = end_window;
if(window < left_margin_) next_event = std::min(next_event, left_margin_);
if(window < right_margin_) next_event = std::min(next_event, right_margin_);
if(is_sync_or_pixels_) {
#define DispatchBpp(func) \
switch(bpp_) { \
default: \
case 1: func(1)(pixel_pointer_, output_duration); break; \
case 2: func(2)(pixel_pointer_, output_duration); break; \
case 4: func(4)(pixel_pointer_, output_duration); break; \
case 8: func(8)(pixel_pointer_, output_duration); break; \
}
#define pixel(x) output_pixel<x, false>
#define lpixel(x) output_pixel<x, true>
#define ch256(x) output_character<x, 8>
#define ch128(x) output_character<x, 7>
#define ch64(x) output_character<x, 6>
#define attr(x) output_attributed<x>
int columns_remaining = next_event - window;
while(columns_remaining) {
if(!pixel_pointer_) {
if(output_duration_) {
set_output_type(OutputType::Pixels, true);
}
pixel_pointer_ = allocated_pointer_ = reinterpret_cast<uint16_t *>(crt_.begin_data(allocation_size));
}
if(allocated_pointer_) {
const int output_duration = std::min(columns_remaining, int(allocated_pointer_ + allocation_size - pixel_pointer_) / column_size_);
switch(mode_) {
default:
case Mode::Pixel: DispatchBpp(pixel); break;
case Mode::LPixel: DispatchBpp(lpixel); break;
case Mode::CH256: DispatchBpp(ch256); break;
case Mode::CH128: DispatchBpp(ch128); break;
case Mode::CH64: DispatchBpp(ch64); break;
case Mode::Attr: DispatchBpp(attr); break;
}
pixel_pointer_ += output_duration * column_size_;
output_duration_ += output_duration;
if(pixel_pointer_ - allocated_pointer_ == allocation_size) {
set_output_type(OutputType::Pixels, true);
}
columns_remaining -= output_duration;
add_window(output_duration);
} else {
output_duration_ += columns_remaining;
add_window(columns_remaining);
columns_remaining = 0;
}
}
#undef attr
#undef ch64
#undef ch128
#undef ch256
#undef pixel
#undef lpixel
#undef DispatchBpp
} else {
output_duration_ += next_event - window;
add_window(next_event - window);
}
set_output_type(is_sync_or_pixels_ ? OutputType::Pixels : OutputType::Border);
}
}
}
// Check for end of line.
if(!horizontal_counter_) {
assert(window == 57);
++lines_remaining_;
if(!lines_remaining_) {
should_reload_line_parameters_ = true;
// Check for end-of-frame.
if(reload_line_parameter_pointer_) {
line_parameter_pointer_ = line_parameter_base_;
} else {
line_parameter_pointer_ += 16;
}
} else {
should_reload_line_parameters_ = false;
}
// Deal with VRES and other address reloading, dependant upon mode.
switch(mode_) {
default: break;
case Mode::CH64:
case Mode::CH128:
case Mode::CH256:
line_data_pointer_[0] = start_line_data_pointer_[0];
++line_data_pointer_[1];
break;
case Mode::Pixel:
case Mode::LPixel:
case Mode::Attr:
// Reload the pixel or attribute address if VRES is clear.
if(!vres_) {
line_data_pointer_[0] = start_line_data_pointer_[0];
}
break;
}
}
}
#undef add_window
}
void Nick::set_output_type(OutputType type, bool force_flush) {
if(type == output_type_ && !force_flush) {
return;
}
if(output_duration_) {
switch(output_type_) {
case OutputType::Border: {
uint16_t *const colour_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(1));
if(colour_pointer) *colour_pointer = border_colour_;
crt_.output_level(output_duration_*16);
} break;
case OutputType::Pixels: {
crt_.output_data(output_duration_*16, size_t(output_duration_*column_size_));
pixel_pointer_ = nullptr;
allocated_pointer_ = nullptr;
} break;
case OutputType::Sync: crt_.output_sync(output_duration_*16); break;
case OutputType::Blank: crt_.output_blank(output_duration_*16); break;
case OutputType::ColourBurst: crt_.output_colour_burst(output_duration_*16, 0); break;
}
}
output_duration_ = 0;
output_type_ = type;
}
// MARK: - Sequence points.
Cycles Nick::get_next_sequence_point() const {
constexpr int load_point = 16; // i.e. 16 cycles after the start of the line, the
// interrupt line may change. That is, after the
// second byte of the mode line has been read.
// Any mode line may cause a change in the interrupt output, so as a first blush
// just always report the time until the end of the mode line.
if(lines_remaining_ || horizontal_counter_ >= load_point) {
return Cycles(load_point + (912 - horizontal_counter_) + (0xff - lines_remaining_) * 912);
} else {
return Cycles(load_point - horizontal_counter_);
}
}
// MARK: - CRT passthroughs.
void Nick::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}
Outputs::Display::ScanStatus Nick::get_scaled_scan_status() const {
return crt_.get_scaled_scan_status();
}
void Nick::set_display_type(Outputs::Display::DisplayType display_type) {
first_pixel_window_ = display_type == Outputs::Display::DisplayType::RGB ? 8 : 10;
crt_.set_display_type(display_type);
}
Outputs::Display::DisplayType Nick::get_display_type() const {
return crt_.get_display_type();
}
// MARK: - Specific pixel outputters.
#define output1bpp(x) \
target[0] = palette[(x & 0x80) >> 7]; \
target[1] = palette[(x & 0x40) >> 6]; \
target[2] = palette[(x & 0x20) >> 5]; \
target[3] = palette[(x & 0x10) >> 4]; \
target[4] = palette[(x & 0x08) >> 3]; \
target[5] = palette[(x & 0x04) >> 2]; \
target[6] = palette[(x & 0x02) >> 1]; \
target[7] = palette[(x & 0x01) >> 0]; \
target += 8
#define output2bpp(x) \
target[0] = palette_[((x & 0x80) >> 7) | ((x & 0x08) >> 2)]; \
target[1] = palette_[((x & 0x40) >> 6) | ((x & 0x04) >> 1)]; \
target[2] = palette_[((x & 0x20) >> 5) | ((x & 0x02) >> 0)]; \
target[3] = palette_[((x & 0x10) >> 4) | ((x & 0x01) << 1)]; \
target += 4
#define output4bpp(x) \
target[0] = palette_[((x & 0x02) << 2) | ((x & 0x20) >> 3) | ((x & 0x08) >> 2) | ((x & 0x80) >> 7)]; \
target[1] = palette_[((x & 0x01) << 3) | ((x & 0x10) >> 2) | ((x & 0x04) >> 1) | ((x & 0x40) >> 6)]; \
target += 2
#define output8bpp(x) \
target[0] = mapped_colour(x); \
++target
template <int bpp, bool is_lpixel> void Nick::output_pixel(uint16_t *target, int columns) const {
static_assert(bpp == 1 || bpp == 2 || bpp == 4 || bpp == 8);
int index = 0;
for(int c = 0; c < columns; c++) {
uint8_t pixels[2] = {
ram_[(line_data_pointer_[0] + index) & 0xffff],
ram_[(line_data_pointer_[0] + index + 1) & 0xffff]
};
index += is_lpixel ? 1 : 2;
last_read_ = pixels[1];
switch(bpp) {
default:
case 1: {
const uint16_t *palette = alt_ind_palettes[((pixels[0] >> 6) & 0x02) | (pixels[0]&1)];
pixels[0] &= two_colour_mask_;
output1bpp(pixels[0]);
if constexpr (!is_lpixel) {
palette = alt_ind_palettes[((pixels[1] >> 6) & 0x02) | (pixels[1]&1)];
pixels[1] &= two_colour_mask_;
output1bpp(pixels[1]);
}
} break;
case 2:
output2bpp(pixels[0]);
if constexpr (!is_lpixel) {
output2bpp(pixels[1]);
}
break;
case 4:
output4bpp(pixels[0]);
if constexpr (!is_lpixel) {
output4bpp(pixels[1]);
}
break;
case 8:
output8bpp(pixels[0]);
if constexpr (!is_lpixel) {
output8bpp(pixels[1]);
}
break;
}
}
}
template <int bpp, int index_bits> void Nick::output_character(uint16_t *target, int columns) const {
static_assert(bpp == 1 || bpp == 2 || bpp == 4 || bpp == 8);
for(int c = 0; c < columns; c++) {
const uint8_t character = ram_[(line_data_pointer_[0] + c) & 0xffff];
const uint8_t pixels = ram_[(
(line_data_pointer_[1] << index_bits) +
(character & ((1 << index_bits) - 1))
) & 0xffff];
last_read_ = pixels;
switch(bpp) {
default:
assert(false);
break;
case 1: {
// This applies ALTIND0 and ALTIND1.
const uint16_t *palette = alt_ind_palettes[character >> 6];
output1bpp(pixels);
} break;
case 2: output2bpp(pixels); break;
case 4: output4bpp(pixels); break;
case 8: output8bpp(pixels); break;
}
}
}
template <int bpp> void Nick::output_attributed(uint16_t *target, int columns) const {
static_assert(bpp == 1 || bpp == 2 || bpp == 4 || bpp == 8);
for(int c = 0; c < columns; c++) {
const uint8_t pixels = ram_[(line_data_pointer_[1] + c) & 0xffff];
const uint8_t attributes = ram_[(line_data_pointer_[0] + c) & 0xffff];
last_read_ = pixels;
const uint16_t palette[2] = {
palette_[attributes >> 4], palette_[attributes & 0x0f]
};
output1bpp(pixels);
}
}
#undef output1bpp
#undef output2bpp
#undef output4bpp
#undef output8bpp

View File

@@ -0,0 +1,126 @@
//
// Nick.hpp
// Clock Signal
//
// Created by Thomas Harte on 14/06/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef Nick_hpp
#define Nick_hpp
#include <cstdint>
#include "../../ClockReceiver/ClockReceiver.hpp"
#include "../../Outputs/CRT/CRT.hpp"
namespace Enterprise {
class Nick {
public:
Nick(const uint8_t *ram);
/// Writes to a Nick register; only the low two bits are decoded.
void write(uint16_t address, uint8_t value);
/// Reads from the Nick range. Nobody seems to be completely clear what
/// this should return; I've set it up to return the last fetched video or mode
/// line byte during periods when those things are being fetched, 0xff at all
/// other times. Including during refresh, since I don't know what addresses
/// are generated then.
///
/// This likely isn't accurate, but is the most accurate guess I could make.
uint8_t read();
void run_for(Cycles);
Cycles get_time_until_z80_slot(Cycles after_period) const;
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
Outputs::Display::ScanStatus get_scaled_scan_status() const;
/// @returns The amount of time until the next potential change in interrupt output.
Cycles get_next_sequence_point() const;
/*!
@returns The current state of the interrupt line — @c true for active;
@c false for inactive.
*/
inline bool get_interrupt_line() const {
return interrupt_line_;
}
/// Sets the type of output.
void set_display_type(Outputs::Display::DisplayType);
/// Gets the type of output.
Outputs::Display::DisplayType get_display_type() const;
private:
Outputs::CRT::CRT crt_;
const uint8_t *const ram_;
// CPU-provided state.
uint8_t line_parameter_control_ = 0xc0;
uint16_t line_parameter_base_ = 0x0000;
uint16_t border_colour_ = 0;
// Ephemerals, related to current video position.
int horizontal_counter_ = 0;
uint16_t line_parameter_pointer_ = 0x0000;
bool should_reload_line_parameters_ = true;
uint16_t line_data_pointer_[2];
uint16_t start_line_data_pointer_[2];
mutable uint8_t last_read_ = 0xff;
// Current mode line parameters.
uint8_t lines_remaining_ = 0x00;
uint8_t two_colour_mask_ = 0xff;
int left_margin_ = 0, right_margin_ = 0;
const uint16_t *alt_ind_palettes[4];
enum class Mode {
Vsync,
Pixel,
Attr,
CH256,
CH128,
CH64,
Unused,
LPixel,
} mode_ = Mode::Vsync;
bool is_sync_or_pixels_ = false;
int bpp_ = 0;
int column_size_ = 0;
bool interrupt_line_ = true;
int line_data_per_column_increments_[2] = {0, 0};
bool vres_ = false;
bool reload_line_parameter_pointer_ = false;
// The destination for new pixels.
static constexpr int allocation_size = 336;
static_assert((allocation_size % 16) == 0, "Allocation size must be a multiple of 16");
uint16_t *pixel_pointer_ = nullptr, *allocated_pointer_ = nullptr;
// Output transitions.
enum class OutputType {
Sync, Blank, Pixels, Border, ColourBurst
};
void set_output_type(OutputType, bool force_flush = false);
int output_duration_ = 0;
OutputType output_type_ = OutputType::Sync;
// Current palette.
uint16_t palette_[16]{};
// The first column with pixels on it; will be either 8 or 10 depending
// on whether the colour burst is meaningful to the current display type.
int first_pixel_window_ = 10;
// Specific outputters.
template <int bpp, bool is_lpixel> void output_pixel(uint16_t *target, int columns) const;
template <int bpp, int index_bits> void output_character(uint16_t *target, int columns) const;
template <int bpp> void output_attributed(uint16_t *target, int columns) const;
};
}
#endif /* Nick_hpp */

View File

@@ -169,19 +169,22 @@ class ConcreteMachine:
// Install the proper TV standard and select an ideal BIOS name.
const std::string machine_name = "MSX";
std::vector<ROMMachine::ROM> required_roms = {
{machine_name, "any MSX BIOS", "msx.rom", 32*1024, 0x94ee12f3}
};
ROM::Request bios_request = ROM::Request(ROM::Name::MSXGenericBIOS);
// std::vector<ROMMachine::ROM> required_roms = {
// {machine_name, "any MSX BIOS", "msx.rom", 32*1024, 0x94ee12f3u}
// };
bool is_ntsc = true;
uint8_t character_generator = 1; /* 0 = Japan, 1 = USA, etc, 2 = USSR */
uint8_t date_format = 1; /* 0 = Y/M/D, 1 = M/D/Y, 2 = D/M/Y */
uint8_t keyboard = 1; /* 0 = Japan, 1 = USA, 2 = France, 3 = UK, 4 = Germany, 5 = USSR, 6 = Spain */
ROM::Name regional_bios_name;
// TODO: CRCs below are incomplete, at best.
switch(target.region) {
default:
case Target::Region::Japan:
required_roms.emplace_back(machine_name, "a Japanese MSX BIOS", "msx-japanese.rom", 32*1024, 0xee229390);
regional_bios_name = ROM::Name::MSXJapaneseBIOS;
vdp_->set_tv_standard(TI::TMS::TVStandard::NTSC);
is_ntsc = true;
@@ -189,7 +192,7 @@ class ConcreteMachine:
date_format = 0;
break;
case Target::Region::USA:
required_roms.emplace_back(machine_name, "an American MSX BIOS", "msx-american.rom", 32*1024, 0);
regional_bios_name = ROM::Name::MSXAmericanBIOS;
vdp_->set_tv_standard(TI::TMS::TVStandard::NTSC);
is_ntsc = true;
@@ -197,7 +200,7 @@ class ConcreteMachine:
date_format = 1;
break;
case Target::Region::Europe:
required_roms.emplace_back(machine_name, "a European MSX BIOS", "msx-european.rom", 32*1024, 0);
regional_bios_name = ROM::Name::MSXEuropeanBIOS;
vdp_->set_tv_standard(TI::TMS::TVStandard::PAL);
is_ntsc = false;
@@ -205,27 +208,30 @@ class ConcreteMachine:
date_format = 2;
break;
}
bios_request = bios_request || ROM::Request(regional_bios_name);
// Fetch the necessary ROMs; try the region-specific ROM first,
// but failing that fall back on patching the main one.
size_t disk_index = 0;
ROM::Request request;
if(target.has_disk_drive) {
disk_index = required_roms.size();
required_roms.emplace_back(machine_name, "the MSX-DOS ROM", "disk.rom", 16*1024, 0x721f61df);
request = ROM::Request(ROM::Name::MSXDOS) && bios_request;
} else {
request = bios_request;
}
const auto roms = rom_fetcher(required_roms);
if((!roms[0] && !roms[1]) || (target.has_disk_drive && !roms[2])) {
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
// Figure out which BIOS to use, either a specific one or the generic
// one appropriately patched.
if(roms[1]) {
memory_slots_[0].source = std::move(*roms[1]);
const auto regional_bios = roms.find(regional_bios_name);
if(regional_bios != roms.end()) {
memory_slots_[0].source = std::move(regional_bios->second);
memory_slots_[0].source.resize(32768);
} else {
memory_slots_[0].source = std::move(*roms[0]);
memory_slots_[0].source = std::move(roms.find(ROM::Name::MSXGenericBIOS)->second);
memory_slots_[0].source.resize(32768);
memory_slots_[0].source[0x2b] = uint8_t(
@@ -252,7 +258,7 @@ class ConcreteMachine:
// Add a disk cartridge if any disks were supplied.
if(target.has_disk_drive) {
memory_slots_[2].set_handler(new DiskROM(memory_slots_[2].source));
memory_slots_[2].source = std::move(*roms[disk_index]);
memory_slots_[2].source = std::move(roms.find(ROM::Name::MSXDOS)->second);
memory_slots_[2].source.resize(16384);
map(2, 0, 0x4000, 0x2000);
@@ -419,7 +425,9 @@ class ConcreteMachine:
// but otherwise runs without pause.
const HalfCycles addition((cycle.operation == CPU::Z80::PartialMachineCycle::ReadOpcode) ? 2 : 0);
const HalfCycles total_length = addition + cycle.length;
vdp_ += total_length;
if(vdp_ += total_length) {
z80_.set_interrupt_line(vdp_->get_interrupt_line(), vdp_.last_sequence_point_overrun());
}
time_since_ay_update_ += total_length;
memory_slots_[0].cycles_since_update += total_length;
memory_slots_[1].cycles_since_update += total_length;
@@ -520,14 +528,11 @@ class ConcreteMachine:
case 0x98: case 0x99:
*cycle.value = vdp_->read(address);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
case 0xa2:
update_audio();
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
*cycle.value = ay_.get_data_output();
ay_.set_control_lines(GI::AY38910::ControlLines(0));
*cycle.value = GI::AY38910::Utility::read(ay_);
break;
case 0xa8: case 0xa9:
@@ -547,14 +552,11 @@ class ConcreteMachine:
case 0x98: case 0x99:
vdp_->write(address, *cycle.value);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
case 0xa0: case 0xa1:
update_audio();
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BDIR | GI::AY38910::BC2 | ((port == 0xa0) ? GI::AY38910::BC1 : 0)));
ay_.set_data_input(*cycle.value);
ay_.set_control_lines(GI::AY38910::ControlLines(0));
GI::AY38910::Utility::write(ay_, port == 0xa1, *cycle.value);
break;
case 0xa8: case 0xa9:
@@ -610,12 +612,6 @@ class ConcreteMachine:
if(!tape_player_is_sleeping_)
tape_player_.run_for(int(cycle.length.as_integral()));
if(time_until_interrupt_ > 0) {
time_until_interrupt_ -= total_length;
if(time_until_interrupt_ <= HalfCycles(0)) {
z80_.set_interrupt_line(true, time_until_interrupt_);
}
}
return addition;
}
@@ -789,7 +785,6 @@ class ConcreteMachine:
uint8_t unpopulated_[8192];
HalfCycles time_since_ay_update_;
HalfCycles time_until_interrupt_;
uint8_t key_states_[16];
int selected_key_line_ = 0;

View File

@@ -20,6 +20,7 @@
#include "MediaTarget.hpp"
#include "MouseMachine.hpp"
#include "ScanProducer.hpp"
#include "StateProducer.hpp"
#include "TimedMachine.hpp"
#endif /* MachineTypes_h */

View File

@@ -141,23 +141,20 @@ class ConcreteMachine:
// 0072ed54 = US/European BIOS 1.3
// 48d44a13 = Japanese BIOS 2.1
const bool is_japanese = target.region == Target::Region::Japan;
const auto roms = rom_fetcher(
{ {"MasterSystem",
is_japanese ? "the Japanese Master System BIOS" : "the European/US Master System BIOS",
is_japanese ? "japanese-bios.sms" : "bios.sms",
8*1024,
{ is_japanese ? 0x48d44a13u : 0x0072ed54u }
} }
);
if(!roms[0]) {
const ROM::Name bios_name = is_japanese ? ROM::Name::MasterSystemJapaneseBIOS : ROM::Name::MasterSystemWesternBIOS;
ROM::Request request(bios_name, true);
auto roms = rom_fetcher(request);
request.validate(roms);
const auto rom = roms.find(bios_name);
if(rom == roms.end()) {
// No BIOS found; attempt to boot as though it has already disabled itself.
has_bios_ = false;
memory_control_ |= 0x08;
std::cerr << "No BIOS found; attempting to start cartridge directly" << std::endl;
} else {
has_bios_ = true;
roms[0]->resize(8*1024);
memcpy(&bios_, roms[0]->data(), roms[0]->size());
memcpy(&bios_, rom->second.data(), std::min(sizeof(bios_), rom->second.size()));
}
page_cartridge();
@@ -214,7 +211,9 @@ class ConcreteMachine:
}
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
vdp_ += cycle.length;
if(vdp_ += cycle.length) {
z80_.set_interrupt_line(vdp_->get_interrupt_line(), vdp_.last_sequence_point_overrun());
}
time_since_sn76489_update_ += cycle.length;
if(cycle.is_terminal()) {
@@ -266,7 +265,6 @@ class ConcreteMachine:
case 0x80: case 0x81:
*cycle.value = vdp_->read(address);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
case 0xc0: {
if(memory_control_ & 0x4) {
@@ -330,7 +328,6 @@ class ConcreteMachine:
case 0x80: case 0x81: // i.e. ports 0x800xbf.
vdp_->write(address, *cycle.value);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
case 0xc1: case 0xc0: // i.e. ports 0xc00xff.
if(has_fm_audio_) {
@@ -374,13 +371,6 @@ class ConcreteMachine:
}
}
if(time_until_interrupt_ > 0) {
time_until_interrupt_ -= cycle.length;
if(time_until_interrupt_ <= HalfCycles(0)) {
z80_.set_interrupt_line(true, time_until_interrupt_);
}
}
// The pause button is debounced and takes effect only one line before pixels
// begin; time_until_debounce_ keeps track of the time until then.
time_until_debounce_ -= cycle.length;
@@ -505,7 +495,6 @@ class ConcreteMachine:
bool reset_is_pressed_ = false, pause_is_pressed_ = false;
HalfCycles time_since_sn76489_update_;
HalfCycles time_until_interrupt_;
HalfCycles time_until_debounce_;
uint8_t ram_[8*1024];

View File

@@ -127,5 +127,5 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
#undef SHIFT
#undef X
return table_lookup_sequence_for_character(key_sequences, sizeof(key_sequences), character);
return table_lookup_sequence_for_character(key_sequences, character);
}

View File

@@ -34,10 +34,51 @@
#include "../../Analyser/Static/Oric/Target.hpp"
#include "../../ClockReceiver/JustInTime.hpp"
#include <cstdint>
#include <memory>
#include <vector>
namespace {
/*!
Provides an Altai-style joystick.
*/
class Joystick: public Inputs::ConcreteJoystick {
public:
Joystick() :
ConcreteJoystick({
Input(Input::Up),
Input(Input::Down),
Input(Input::Left),
Input(Input::Right),
Input(Input::Fire)
}) {}
void did_set_input(const Input &digital_input, bool is_active) final {
#define APPLY(b) if(is_active) state_ &= ~b; else state_ |= b;
switch(digital_input.type) {
default: return;
case Input::Right: APPLY(0x02); break;
case Input::Left: APPLY(0x01); break;
case Input::Down: APPLY(0x08); break;
case Input::Up: APPLY(0x10); break;
case Input::Fire: APPLY(0x20); break;
}
#undef APPLY
}
uint8_t get_state() {
return state_;
}
private:
uint8_t state_ = 0xff;
};
}
namespace Oric {
using DiskInterface = Analyser::Static::Oric::Target::DiskInterface;
@@ -138,7 +179,12 @@ class TapePlayer: public Storage::Tape::BinaryTapePlayer {
class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
public:
VIAPortHandler(Concurrency::DeferringAsyncTaskQueue &audio_queue, AY &ay8910, Speaker &speaker, TapePlayer &tape_player, Keyboard &keyboard) :
audio_queue_(audio_queue), ay8910_(ay8910), speaker_(speaker), tape_player_(tape_player), keyboard_(keyboard) {}
audio_queue_(audio_queue), ay8910_(ay8910), speaker_(speaker), tape_player_(tape_player), keyboard_(keyboard)
{
// Attach a couple of joysticks.
joysticks_.emplace_back(new Joystick);
joysticks_.emplace_back(new Joystick);
}
/*!
Reponds to the 6522's control line output change signal; on an Oric A2 is connected to
@@ -163,6 +209,7 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
} else {
update_ay();
ay8910_.set_data_input(value);
porta_output_ = value;
}
}
@@ -174,7 +221,10 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
uint8_t column = ay8910_.get_port_output(false) ^ 0xff;
return keyboard_.query_column(column) ? 0x08 : 0x00;
} else {
return ay8910_.get_data_output();
uint8_t result = ay8910_.get_data_output();
if(porta_output_ & 0x40) result &= static_cast<Joystick *>(joysticks_[0].get())->get_state();
if(porta_output_ & 0x80) result &= static_cast<Joystick *>(joysticks_[1].get())->get_state();
return result;
}
}
@@ -191,12 +241,17 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
audio_queue_.perform();
}
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() {
return joysticks_;
}
private:
void update_ay() {
speaker_.run_for(audio_queue_, cycles_since_ay_update_.flush<Cycles>());
}
bool ay_bdir_ = false;
bool ay_bc1_ = false;
uint8_t porta_output_ = 0xff;
HalfCycles cycles_since_ay_update_;
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
@@ -204,12 +259,15 @@ class VIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
Speaker &speaker_;
TapePlayer &tape_player_;
Keyboard &keyboard_;
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
};
template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS6502Esque::Type processor_type> class ConcreteMachine:
public MachineTypes::TimedMachine,
public MachineTypes::ScanProducer,
public MachineTypes::AudioProducer,
public MachineTypes::JoystickMachine,
public MachineTypes::MediaTarget,
public MachineTypes::MappedKeyboardMachine,
public Configurable::Device,
@@ -217,7 +275,6 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
public MOS::MOS6522::IRQDelegatePortHandler::Delegate,
public Storage::Tape::BinaryTapePlayer::Delegate,
public DiskController::Delegate,
public ClockingHint::Observer,
public Activity::Source,
public Machine,
public Keyboard::SpecialKeyHandler {
@@ -225,7 +282,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
public:
ConcreteMachine(const Analyser::Static::Oric::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
m6502_(*this),
video_output_(ram_),
video_(ram_),
ay8910_(GI::AY38910::Personality::AY38910, audio_queue_),
speaker_(ay8910_),
via_port_handler_(audio_queue_, ay8910_, speaker_, tape_player_, keyboard_),
@@ -247,77 +304,64 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
ram_[c] |= 0x40;
}
if constexpr (disk_interface == DiskInterface::Pravetz) {
diskii_.set_clocking_hint_observer(this);
}
const std::string machine_name = "Oric";
std::vector<ROMMachine::ROM> rom_names = { {machine_name, "the Oric colour ROM", "colour.rom", 128, 0xd50fca65} };
::ROM::Request request = ::ROM::Request(::ROM::Name::OricColourROM, true);
::ROM::Name basic;
switch(target.rom) {
case Analyser::Static::Oric::Target::ROM::BASIC10:
rom_names.emplace_back(machine_name, "Oric BASIC 1.0", "basic10.rom", 16*1024, 0xf18710b4);
break;
case Analyser::Static::Oric::Target::ROM::BASIC11:
rom_names.emplace_back(machine_name, "Oric BASIC 1.1", "basic11.rom", 16*1024, 0xc3a92bef);
break;
case Analyser::Static::Oric::Target::ROM::Pravetz:
rom_names.emplace_back(machine_name, "Pravetz BASIC", "pravetz.rom", 16*1024, 0x58079502);
break;
case Analyser::Static::Oric::Target::ROM::BASIC10: basic = ::ROM::Name::OricBASIC10; break;
default:
case Analyser::Static::Oric::Target::ROM::BASIC11: basic = ::ROM::Name::OricBASIC11; break;
case Analyser::Static::Oric::Target::ROM::Pravetz: basic = ::ROM::Name::OricPravetzBASIC; break;
}
size_t diskii_state_machine_index = 0;
request = request && ::ROM::Request(basic);
switch(disk_interface) {
default: break;
case DiskInterface::BD500:
rom_names.emplace_back(machine_name, "the Oric Byte Drive 500 ROM", "bd500.rom", 8*1024, 0x61952e34);
request = request && ::ROM::Request(::ROM::Name::OricByteDrive500);
break;
case DiskInterface::Jasmin:
rom_names.emplace_back(machine_name, "the Oric Jasmin ROM", "jasmin.rom", 2*1024, 0x37220e89);
request = request && ::ROM::Request(::ROM::Name::OricJasmin);
break;
case DiskInterface::Microdisc:
rom_names.emplace_back(machine_name, "the Oric Microdisc ROM", "microdisc.rom", 8*1024, 0xa9664a9c);
request = request && ::ROM::Request(::ROM::Name::OricMicrodisc);
break;
case DiskInterface::Pravetz:
rom_names.emplace_back(machine_name, "the 8DOS boot ROM", "8dos.rom", 512, 0x49a74c06);
// These ROM details are coupled with those in the DiskIICard.
diskii_state_machine_index = rom_names.size();
rom_names.push_back({"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 }});
request = request && ::ROM::Request(::ROM::Name::Oric8DOSBoot) && ::ROM::Request(::ROM::Name::DiskIIStateMachine16Sector);
break;
}
const auto roms = rom_fetcher(rom_names);
for(std::size_t index = 0; index < roms.size(); ++index) {
if(!roms[index]) {
throw ROMMachine::Error::MissingROMs;
}
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
video_output_.set_colour_rom(*roms[0]);
rom_ = std::move(*roms[1]);
// The colour ROM is optional; an alternative composite encoding can be used if
// it is absent.
const auto colour_rom = roms.find(::ROM::Name::OricColourROM);
if(colour_rom != roms.end()) {
video_->set_colour_rom(colour_rom->second);
}
rom_ = std::move(roms.find(basic)->second);
switch(disk_interface) {
default: break;
case DiskInterface::BD500:
disk_rom_ = std::move(*roms[2]);
disk_rom_.resize(8192);
disk_rom_ = std::move(roms.find(::ROM::Name::OricByteDrive500)->second);
break;
case DiskInterface::Jasmin:
disk_rom_ = std::move(*roms[2]);
disk_rom_.resize(2048);
disk_rom_ = std::move(roms.find(::ROM::Name::OricJasmin)->second);
break;
case DiskInterface::Microdisc:
disk_rom_ = std::move(*roms[2]);
disk_rom_.resize(8192);
disk_rom_ = std::move(roms.find(::ROM::Name::OricMicrodisc)->second);
break;
case DiskInterface::Pravetz: {
pravetz_rom_ = std::move(*roms[2]);
pravetz_rom_ = std::move(roms.find(::ROM::Name::Oric8DOSBoot)->second);
pravetz_rom_.resize(512);
diskii_.set_state_machine(*roms[diskii_state_machine_index]);
diskii_->set_state_machine(roms.find(::ROM::Name::DiskIIStateMachine16Sector)->second);
} break;
}
rom_.resize(16384);
paged_rom_ = rom_.data();
switch(target.disk_interface) {
@@ -402,7 +446,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
case DiskInterface::BD500: inserted |= insert_disks(media, bd500_, 4); break;
case DiskInterface::Jasmin: inserted |= insert_disks(media, jasmin_, 4); break;
case DiskInterface::Microdisc: inserted |= insert_disks(media, microdisc_, 4); break;
case DiskInterface::Pravetz: inserted |= insert_disks(media, diskii_, 2); break;
case DiskInterface::Pravetz: inserted |= insert_disks(media, *diskii_.last_valid(), 2); break;
default: break;
}
}
@@ -474,9 +518,8 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
}
}
} else {
flush_diskii();
const int disk_value = diskii_.read_address(address);
if(!isWriteOperation(operation) && disk_value != diskii_.DidNotLoad) *value = uint8_t(disk_value);
const int disk_value = diskii_->read_address(address);
if(!isWriteOperation(operation) && disk_value != Apple::DiskII::DidNotLoad) *value = uint8_t(disk_value);
}
break;
}
@@ -485,7 +528,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
if(!isWriteOperation(operation))
*value = ram_[address];
else {
if(address >= 0x9800 && address <= 0xc000) update_video();
if(address >= 0x9800 && address <= 0xc000) video_.flush();
ram_[address] = *value;
}
}
@@ -508,7 +551,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
bd500_.run_for(Cycles(9)); // i.e. effective clock rate of 9Mhz.
break;
case DiskInterface::Jasmin:
jasmin_.run_for(Cycles(8));; // i.e. effective clock rate of 8Mhz.
jasmin_.run_for(Cycles(8)); // i.e. effective clock rate of 8Mhz.
// Jasmin autostart hack: wait for a period, then trigger a reset, having forced
// the Jasmin to page its ROM in first. I assume the latter being what the Jasmin's
@@ -521,43 +564,41 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
}
break;
case DiskInterface::Microdisc:
microdisc_.run_for(Cycles(8));; // i.e. effective clock rate of 8Mhz.
microdisc_.run_for(Cycles(8)); // i.e. effective clock rate of 8Mhz.
break;
case DiskInterface::Pravetz:
if(diskii_clocking_preference_ == ClockingHint::Preference::RealTime) {
diskii_.set_data_input(*value);
diskii_.run_for(Cycles(2));; // i.e. effective clock rate of 2Mhz.
} else {
cycles_since_diskii_update_ += Cycles(2);
if(diskii_.clocking_preference() == ClockingHint::Preference::RealTime) {
diskii_->set_data_input(*value);
}
diskii_ += Cycles(2); // i.e. effective clock rate of 2Mhz.
break;
}
cycles_since_video_update_++;
video_ += Cycles(1);
return Cycles(1);
}
forceinline void flush() {
update_video();
video_.flush();
via_.flush();
flush_diskii();
diskii_.flush();
}
// to satisfy CRTMachine::Machine
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
video_output_.set_scan_target(scan_target);
video_.last_valid()->set_scan_target(scan_target);
}
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return video_output_.get_scaled_scan_status();
return video_.last_valid()->get_scaled_scan_status();
}
void set_display_type(Outputs::Display::DisplayType display_type) final {
video_output_.set_display_type(display_type);
video_.last_valid()->set_display_type(display_type);
}
Outputs::Display::DisplayType get_display_type() const final {
return video_output_.get_display_type();
return video_.last_valid()->get_display_type();
}
Outputs::Speaker::Speaker *get_speaker() final {
@@ -644,15 +685,11 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
microdisc_.set_activity_observer(observer);
break;
case DiskInterface::Pravetz:
diskii_.set_activity_observer(observer);
diskii_->set_activity_observer(observer);
break;
}
}
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference) final {
diskii_clocking_preference_ = diskii_.preferred_clocking();
}
private:
const uint16_t basic_invisible_ram_top_ = 0xffff;
const uint16_t basic_visible_ram_top_ = 0xbfff;
@@ -662,17 +699,13 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
// RAM and ROM
std::vector<uint8_t> rom_, disk_rom_;
uint8_t ram_[65536];
Cycles cycles_since_video_update_;
inline void update_video() {
video_output_.run_for(cycles_since_video_update_.flush<Cycles>());
}
// ROM bookkeeping
uint16_t tape_get_byte_address_ = 0, tape_speed_address_ = 0;
int keyboard_read_count_ = 0;
// Outputs
VideoOutput video_output_;
JustInTimeActor<VideoOutput, Cycles> video_;
Concurrency::DeferringAsyncTaskQueue audio_queue_;
GI::AY38910::AY38910<false> ay8910_;
@@ -700,14 +733,9 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
BD500 bd500_;
// the Pravetz/Disk II, if in use.
Apple::DiskII diskii_;
Cycles cycles_since_diskii_update_;
void flush_diskii() {
diskii_.run_for(cycles_since_diskii_update_.flush<Cycles>());
}
JustInTimeActor<Apple::DiskII, Cycles> diskii_;
std::vector<uint8_t> pravetz_rom_;
std::size_t pravetz_rom_base_pointer_ = 0;
ClockingHint::Preference diskii_clocking_preference_ = ClockingHint::Preference::RealTime;
// Overlay RAM
uint16_t ram_top_ = basic_visible_ram_top_;
@@ -750,8 +778,13 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface, CPU::MOS
}
}
// MARK - typing
// MARK: - typing
std::unique_ptr<Utility::StringSerialiser> string_serialiser_;
// MARK: - Joysticks
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
return via_port_handler_.get_joysticks();
}
};
}

View File

@@ -54,7 +54,7 @@ void VideoOutput::set_display_type(Outputs::Display::DisplayType display_type) {
#ifdef SUPPLY_COMPOSITE
const auto data_type =
(display_type == Outputs::Display::DisplayType::RGB) ?
(!has_colour_rom_ || display_type == Outputs::Display::DisplayType::RGB) ?
Outputs::Display::InputDataType::Red1Green1Blue1 :
Outputs::Display::InputDataType::PhaseLinkedLuminance8;
#else
@@ -80,6 +80,7 @@ Outputs::Display::ScanStatus VideoOutput::get_scaled_scan_status() const {
}
void VideoOutput::set_colour_rom(const std::vector<uint8_t> &rom) {
has_colour_rom_ = true;
for(std::size_t c = 0; c < 8; c++) {
colour_forms_[c] = 0;

View File

@@ -37,6 +37,7 @@ class VideoOutput {
Outputs::CRT::CRT crt_;
Outputs::CRT::CRTFrequencyMismatchWarner<VideoOutput> frequency_mismatch_warner_;
bool crt_is_60Hz_ = false;
bool has_colour_rom_ = false;
void update_crt_frequency();

View File

@@ -10,38 +10,16 @@
#define ROMMachine_hpp
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "Utility/ROMCatalogue.hpp"
namespace ROMMachine {
/*!
Describes a ROM image; this term is used in this emulator strictly in the sense of firmware —
system software that is an inherent part of a machine.
*/
struct ROM {
/// The machine with which this ROM is associated, in a form that is safe for using as
/// part of a file name.
std::string machine_name;
/// A descriptive name for this ROM, suitable for use in a bullet-point list, a bracket
/// clause, etc, e.g. "the Electron MOS 1.0".
std::string descriptive_name;
/// An idiomatic file name for this ROM, e.g. "os10.rom".
std::string file_name;
/// The expected size of this ROM in bytes, e.g. 32768.
size_t size = 0;
/// CRC32s for all known acceptable copies of this ROM; intended to allow a host platform
/// to test user-provided ROMs of unknown provenance. **Not** intended to be used
/// to exclude ROMs where the user's intent is otherwise clear.
std::vector<uint32_t> crc32s;
ROM(std::string machine_name, std::string descriptive_name, std::string file_name, size_t size, uint32_t crc32) :
machine_name(machine_name), descriptive_name(descriptive_name), file_name(file_name), size(size), crc32s({crc32}) {}
ROM(std::string machine_name, std::string descriptive_name, std::string file_name, size_t size, std::initializer_list<uint32_t> crc32s) :
machine_name(machine_name), descriptive_name(descriptive_name), file_name(file_name), size(size), crc32s(crc32s) {}
};
/*!
Defines the signature for a function that must be supplied by the host environment in order to give machines
a route for fetching any system ROMs they might need.
@@ -50,7 +28,7 @@ struct ROM {
return a vector of unique_ptrs that either contain the contents of the ROM from @c names that corresponds by
index, or else are @c nullptr.
*/
typedef std::function<std::vector<std::unique_ptr<std::vector<uint8_t>>>(const std::vector<ROM> &roms)> ROMFetcher;
typedef std::function<ROM::Map(const ROM::Request &request)> ROMFetcher;
enum class Error {
MissingROMs

View File

@@ -72,7 +72,7 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
#define SHIFT(...) {KeyShift, __VA_ARGS__, MachineTypes::MappedKeyboardMachine::KeyEndSequence}
#define SYMSHIFT(...) {KeySymbolShift, __VA_ARGS__, MachineTypes::MappedKeyboardMachine::KeyEndSequence}
#define X {MachineTypes::MappedKeyboardMachine::KeyNotMapped}
constexpr KeySequence spectrum_key_sequences[] = {
static constexpr KeySequence spectrum_key_sequences[] = {
/* NUL */ X, /* SOH */ X,
/* STX */ X, /* ETX */ X,
/* EOT */ X, /* ENQ */ X,
@@ -137,7 +137,7 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
/* z */ KEYS(KeyZ),
};
constexpr KeySequence zx81_key_sequences[] = {
static constexpr KeySequence zx81_key_sequences[] = {
/* NUL */ X, /* SOH */ X,
/* STX */ X, /* ETX */ X,
/* EOT */ X, /* ENQ */ X,
@@ -203,7 +203,7 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
/* | */ X, /* } */ X,
};
static KeySequence zx80_key_sequences[] = {
static constexpr KeySequence zx80_key_sequences[] = {
/* NUL */ X, /* SOH */ X,
/* STX */ X, /* ETX */ X,
/* EOT */ X, /* ENQ */ X,
@@ -275,13 +275,14 @@ const uint16_t *CharacterMapper::sequence_for_character(char character) const {
switch(machine_) {
case Machine::ZX80:
return table_lookup_sequence_for_character(zx80_key_sequences, sizeof(zx80_key_sequences), character);
return table_lookup_sequence_for_character(zx80_key_sequences, character);
case Machine::ZX81:
return table_lookup_sequence_for_character(zx81_key_sequences, sizeof(zx81_key_sequences), character);
return table_lookup_sequence_for_character(zx81_key_sequences, character);
default:
case Machine::ZXSpectrum:
return table_lookup_sequence_for_character(spectrum_key_sequences, sizeof(zx81_key_sequences), character);
return table_lookup_sequence_for_character(spectrum_key_sequences, character);
}
}

View File

@@ -74,15 +74,13 @@ template<bool is_zx81> class ConcreteMachine:
speaker_.set_input_rate(float(ZX8081ClockRate) / 2.0f);
const bool use_zx81_rom = target.is_ZX81 || target.ZX80_uses_ZX81_ROM;
const auto roms =
use_zx81_rom ?
rom_fetcher({ {"ZX8081", "the ZX81 BASIC ROM", "zx81.rom", 8 * 1024, 0x4b1dd6eb} }) :
rom_fetcher({ {"ZX8081", "the ZX80 BASIC ROM", "zx80.rom", 4 * 1024, 0x4c7fc597} });
if(!roms[0]) throw ROMMachine::Error::MissingROMs;
rom_ = std::move(*roms[0]);
rom_.resize(use_zx81_rom ? 8192 : 4096);
const ROM::Name rom_name = use_zx81_rom ? ROM::Name::ZX81 : ROM::Name::ZX80;
const ROM::Request request(rom_name);
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
rom_ = std::move(roms.find(rom_name)->second);
rom_mask_ = uint16_t(rom_.size() - 1);
@@ -472,22 +470,15 @@ template<bool is_zx81> class ConcreteMachine:
HalfCycles time_since_ay_update_;
inline void ay_set_register(uint8_t value) {
update_audio();
ay_.set_control_lines(GI::AY38910::BC1);
ay_.set_data_input(value);
ay_.set_control_lines(GI::AY38910::ControlLines(0));
GI::AY38910::Utility::select_register(ay_, value);
}
inline void ay_set_data(uint8_t value) {
update_audio();
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BDIR));
ay_.set_data_input(value);
ay_.set_control_lines(GI::AY38910::ControlLines(0));
GI::AY38910::Utility::write_data(ay_, value);
}
inline uint8_t ay_read_data() {
update_audio();
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BC1));
const uint8_t value = ay_.get_data_output();
ay_.set_control_lines(GI::AY38910::ControlLines(0));
return value;
return GI::AY38910::Utility::read(ay_);
}
inline void update_audio() {
speaker_.run_for(audio_queue_, time_since_ay_update_.divide_cycles(Cycles(2)));

View File

@@ -32,7 +32,7 @@ class Machine {
class Options: public Reflection::StructImpl<Options>, public Configurable::QuickloadOption<Options> {
friend Configurable::QuickloadOption<Options>;
public:
bool automatic_tape_motor_control;
bool automatic_tape_motor_control = true;
Options(Configurable::OptionsType type):
Configurable::QuickloadOption<Options>(type == Configurable::OptionsType::UserFriendly),

View File

@@ -0,0 +1,53 @@
//
// State.hpp
// Clock Signal
//
// Created by Thomas Harte on 25/04/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef State_hpp
#define State_hpp
#include "../../../Reflection/Struct.hpp"
#include "../../../Processors/Z80/State/State.hpp"
#include "Video.hpp"
#include "../../../Components/AY38910/AY38910.hpp"
namespace Sinclair {
namespace ZXSpectrum {
struct State: public Reflection::StructImpl<State> {
CPU::Z80::State z80;
Video::State video;
// In 16kb or 48kb mode, RAM will be 16kb or 48kb and represent
// memory in standard linear order. In 128kb mode, RAM will be
// 128kb with the first 16kb representing bank 0, the next bank 1, etc.
std::vector<uint8_t> ram;
// Meaningful for 128kb machines only.
uint8_t last_7ffd = 0;
GI::AY38910::State ay;
// Meaningful for the +2a and +3 only.
uint8_t last_1ffd = 0;
State() {
if(needs_declare()) {
DeclareField(z80);
DeclareField(video);
DeclareField(ram);
DeclareField(last_7ffd);
DeclareField(last_1ffd);
DeclareField(ay);
}
}
};
}
}
#endif /* State_h */

View File

@@ -12,13 +12,18 @@
#include "../../../Outputs/CRT/CRT.hpp"
#include "../../../ClockReceiver/ClockReceiver.hpp"
#include "../../../Reflection/Struct.hpp"
#include <algorithm>
namespace Sinclair {
namespace ZXSpectrum {
namespace Video {
enum class VideoTiming {
Plus3
enum class Timing {
FortyEightK,
OneTwoEightK,
Plus3,
};
/*
@@ -45,87 +50,72 @@ enum class VideoTiming {
*/
template <VideoTiming timing> class Video {
template <Timing timing> class Video {
private:
struct Timings {
// Number of cycles per line. Will be 224 or 228.
int cycles_per_line;
int half_cycles_per_line;
// Number of lines comprising a whole frame. Will be 311 or 312.
int lines_per_frame;
// Number of cycles after first pixel fetch at which interrupt is first signalled.
int interrupt_time;
// Number of cycles before first pixel fetch that contention starts to be applied.
int contention_leadin;
// Period in a line for which contention is applied.
int contention_duration;
// Contention to apply, in half-cycles, as a function of number of half cycles since
// Number of cycles after first pixel fetch at which interrupt is first signalled.
int interrupt_time;
// Contention to apply, in whole cycles, as a function of number of whole cycles since
// contention began.
int delays[16];
int delays[8];
constexpr Timings(int cycles_per_line, int lines_per_frame, int contention_leadin, int contention_duration, int interrupt_offset, const int *delays) noexcept :
half_cycles_per_line(cycles_per_line * 2),
lines_per_frame(lines_per_frame),
contention_leadin(contention_leadin * 2),
contention_duration(contention_duration * 2),
interrupt_time((cycles_per_line * lines_per_frame - interrupt_offset - contention_leadin) * 2),
delays{ delays[0] * 2, delays[1] * 2, delays[2] * 2, delays[3] * 2, delays[4] * 2, delays[5] * 2, delays[6] * 2, delays[7] * 2}
{}
};
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 1436514368 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 == Timing::Plus3) {
constexpr int delays[] = {1, 0, 7, 6, 5, 4, 3, 2};
return Timings(228, 311, 6, 129, 14361, delays);
}
.interrupt_time = 56545 * 2,
if constexpr (timing == Timing::OneTwoEightK) {
constexpr int delays[] = {6, 5, 4, 3, 2, 1, 0, 0};
return Timings(228, 311, 4, 128, 14361, delays);
}
.contention_leadin = 2 * 2, // TODO: is this 2? Or 4? Or... ?
.contention_duration = 129 * 2,
.delays = {
2, 1,
0, 0,
14, 13,
12, 11,
10, 9,
8, 7,
6, 5,
4, 3,
}
};
return result;
if constexpr (timing == Timing::FortyEightK) {
constexpr int delays[] = {6, 5, 4, 3, 2, 1, 0, 0};
return Timings(224, 312, 4, 128, 14335, delays);
}
}
// TODO: how long is the interrupt line held for?
static constexpr int interrupt_duration = 48;
// Interrupt should be held for 32 cycles.
static constexpr int interrupt_duration = 64;
public:
void run_for(HalfCycles duration) {
constexpr auto timings = get_timings();
constexpr int sync_line = (timings.interrupt_time / timings.cycles_per_line) + 1;
constexpr int sync_line = (timings.interrupt_time / timings.half_cycles_per_line) + 1;
constexpr int sync_position = 166 * 2;
constexpr int sync_position = (timing == Timing::FortyEightK) ? 164 * 2 : 166 * 2;
constexpr int sync_length = 17 * 2;
constexpr int burst_position = sync_position + 40;
constexpr int burst_length = 17;
int cycles_remaining = duration.as<int>();
while(cycles_remaining) {
int line = time_into_frame_ / timings.cycles_per_line;
int offset = time_into_frame_ % timings.cycles_per_line;
const int cycles_this_line = std::min(cycles_remaining, timings.cycles_per_line - offset);
int line = time_into_frame_ / timings.half_cycles_per_line;
int offset = time_into_frame_ % timings.half_cycles_per_line;
const int cycles_this_line = std::min(cycles_remaining, timings.half_cycles_per_line - offset);
const int end_offset = offset + cycles_this_line;
if(!offset) {
@@ -167,6 +157,8 @@ template <VideoTiming timing> class Video {
last_fetches_[1] = memory_[attribute_address_];
last_fetches_[2] = memory_[pixel_address_+1];
last_fetches_[3] = memory_[attribute_address_+1];
set_last_contended_area_access(last_fetches_[3]);
pixel_address_ += 2;
attribute_address_ += 2;
@@ -230,9 +222,14 @@ template <VideoTiming timing> 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 >= Timing::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) {
@@ -242,7 +239,7 @@ template <VideoTiming timing> class Video {
}
cycles_remaining -= cycles_this_line;
time_into_frame_ = (time_into_frame_ + cycles_this_line) % (timings.cycles_per_line * timings.lines_per_frame);
time_into_frame_ = (time_into_frame_ + cycles_this_line) % (timings.half_cycles_per_line * timings.lines_per_frame);
}
}
@@ -253,14 +250,64 @@ template <VideoTiming timing> class Video {
crt_.output_level(duration);
}
static constexpr int half_cycles_per_line() {
if constexpr (timing == Timing::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;
}
}
static constexpr HalfCycles frame_duration() {
const auto timings = get_timings();
return HalfCycles(timings.half_cycles_per_line * timings.lines_per_frame);
}
HalfCycles time_since_interrupt() {
const auto timings = get_timings();
if(time_into_frame_ >= timings.interrupt_time) {
return HalfCycles(time_into_frame_ - timings.interrupt_time);
} else {
return HalfCycles(time_into_frame_) + frame_duration() - HalfCycles(timings.interrupt_time);
}
}
void set_time_since_interrupt(const HalfCycles time) {
// Advance using run_for to ensure that all proper CRT interactions occurred.
const auto timings = get_timings();
const auto target = (time + timings.interrupt_time) % frame_duration();
const auto now = HalfCycles(time_into_frame_);
// Maybe this is easy?
if(target == now) return;
// Is the time within this frame?
if(time > now) {
run_for(target - time);
return;
}
// Then it's necessary to finish this frame and run into the next.
run_for(frame_duration() - now + time);
}
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);
crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.1f, 0.8f, 0.8f));
// Get the CRT roughly into phase.
//
// TODO: this is coupled to an assumption about the initial CRT. Fix.
const auto timings = get_timings();
crt_.output_blank(timings.lines_per_frame*timings.half_cycles_per_line - timings.interrupt_time);
}
void set_video_source(const uint8_t *source) {
@@ -284,7 +331,7 @@ template <VideoTiming timing> class Video {
}
// If not, it'll be in the next batch.
return timings.interrupt_time + timings.cycles_per_line * timings.lines_per_frame - time_into_frame_;
return timings.interrupt_time + timings.half_cycles_per_line * timings.lines_per_frame - time_into_frame_;
}
/*!
@@ -299,41 +346,65 @@ template <VideoTiming timing> class Video {
@returns How many cycles the [ULA/gate array] would delay the CPU for if it were to recognise that contention
needs to be applied in @c offset half-cycles from now.
*/
int access_delay(HalfCycles offset) const {
HalfCycles access_delay(HalfCycles offset) const {
constexpr auto timings = get_timings();
const int delay_time = (time_into_frame_ + offset.as<int>() + timings.contention_leadin) % (timings.cycles_per_line * timings.lines_per_frame);
const int delay_time = (time_into_frame_ + offset.as<int>() + timings.contention_leadin) % (timings.half_cycles_per_line * timings.lines_per_frame);
assert(!(delay_time&1));
// Check for a time within the no-contention window.
if(delay_time >= (191*timings.cycles_per_line + timings.contention_duration)) {
if(delay_time >= (191*timings.half_cycles_per_line + timings.contention_duration)) {
return 0;
}
const int time_into_line = delay_time % timings.cycles_per_line;
if(time_into_line >= timings.contention_duration) return 0;
const int time_into_line = delay_time % timings.half_cycles_per_line;
if(time_into_line >= timings.contention_duration) {
return 0;
}
return timings.delays[time_into_line & 15];
return HalfCycles(timings.delays[(time_into_line >> 1) & 7]);
}
/*!
@returns Whatever the ULA or gate array has fetched this cycle, or 0xff if it has fetched nothing.
@returns Whatever the ULA or gate array would expose via the floating bus, this cycle.
*/
uint8_t get_current_fetch() const {
uint8_t get_floating_value() const {
constexpr auto timings = get_timings();
const int line = time_into_frame_ / timings.cycles_per_line;
if(line >= 192) return 0xff;
const uint8_t out_of_bounds = (timing == Timing::Plus3) ? last_contended_access_ : 0xff;
const int time_into_line = time_into_frame_ % timings.cycles_per_line;
if(time_into_line >= 256 || (time_into_line&8)) {
return 0xff;
const int line = time_into_frame_ / timings.half_cycles_per_line;
if(line >= 192) {
return out_of_bounds;
}
return last_fetches_[(time_into_line >> 1) & 3];
const int time_into_line = time_into_frame_ % timings.half_cycles_per_line;
if(time_into_line >= 256 || (time_into_line&8)) {
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 == Timing::Plus3) {
return value | 1;
}
return value;
}
/*!
Relevant to the +2a and +3 only, sets the most recent value read from or
written to contended memory. This is what will be returned if the floating
bus is accessed when the gate array isn't currently reading.
*/
void set_last_contended_area_access([[maybe_unused]] uint8_t value) {
if constexpr (timing == Timing::Plus3) {
last_contended_access_ = value | 1;
}
}
/*!
Sets the current border colour.
*/
void set_border_colour(uint8_t colour) {
border_byte_ = colour;
border_colour_ = palette[colour];
}
@@ -352,11 +423,17 @@ template <VideoTiming timing> class Video {
crt_.set_display_type(type);
}
/*! Gets the display type. */
Outputs::Display::DisplayType get_display_type() const {
return crt_.get_display_type();
}
private:
int time_into_frame_ = 0;
Outputs::CRT::CRT crt_;
const uint8_t *memory_ = nullptr;
uint8_t border_colour_ = 0;
uint8_t border_byte_ = 0;
uint8_t *pixel_target_ = nullptr;
int attribute_address_ = 0;
@@ -367,6 +444,9 @@ template <VideoTiming timing> class Video {
bool is_alternate_line_ = false;
uint8_t last_fetches_[4] = {0xff, 0xff, 0xff, 0xff};
uint8_t last_contended_access_ = 0xff;
friend struct State;
#define RGB(r, g, b) (r << 4) | (g << 2) | b
static constexpr uint8_t palette[] = {
@@ -378,6 +458,41 @@ template <VideoTiming timing> class Video {
#undef RGB
};
struct State: public Reflection::StructImpl<State> {
uint8_t border_colour = 0;
int half_cycles_since_interrupt = 0;
bool flash = 0;
int flash_counter = 0;
bool is_alternate_line = false;
State() {
if(needs_declare()) {
DeclareField(border_colour);
DeclareField(half_cycles_since_interrupt);
DeclareField(flash);
DeclareField(flash_counter);
DeclareField(is_alternate_line);
}
}
template <typename Video> State(const Video &source) : State() {
border_colour = source.border_byte_;
flash = source.flash_mask_;
flash_counter = source.flash_counter_;
is_alternate_line = source. is_alternate_line_;
half_cycles_since_interrupt = source.time_since_interrupt().template as<int>();
}
template <typename Video> void apply(Video &target) {
target.set_border_colour(border_colour);
target.flash_mask_ = flash ? 0xff : 0x00;
target.flash_counter_ = flash_counter;
target.is_alternate_line_ = is_alternate_line;
target.set_time_since_interrupt(HalfCycles(half_cycles_since_interrupt));
}
};
}
}
}

View File

@@ -8,9 +8,9 @@
#include "ZXSpectrum.hpp"
#include "State.hpp"
#include "Video.hpp"
#define LOG_PREFIX "[Spectrum] "
#include "../Keyboard/Keyboard.hpp"
#include "../../../Activity/Source.hpp"
#include "../../MachineTypes.hpp"
@@ -24,7 +24,9 @@
// just grab the CPC's version of an FDC.
#include "../../AmstradCPC/FDC.hpp"
#define LOG_PREFIX "[Spectrum] "
#include "../../../Outputs/Log.hpp"
#include "../../../Outputs/Speaker/Implementation/CompoundSource.hpp"
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "../../../Outputs/Speaker/Implementation/SampleSource.hpp"
@@ -39,10 +41,76 @@
#include "../../../ClockReceiver/JustInTime.hpp"
#include "../Keyboard/Keyboard.hpp"
#include <array>
namespace {
/*!
Provides a simultaneous Kempston and Interface 2-style joystick.
*/
class Joystick: public Inputs::ConcreteJoystick {
public:
Joystick() :
ConcreteJoystick({
Input(Input::Up),
Input(Input::Down),
Input(Input::Left),
Input(Input::Right),
Input(Input::Fire)
}) {}
void did_set_input(const Input &digital_input, bool is_active) final {
#define APPLY_KEMPSTON(b) if(is_active) kempston_ |= b; else kempston_ &= ~b;
#define APPLY_SINCLAIR(b) if(is_active) sinclair_ &= ~b; else sinclair_ |= b;
switch(digital_input.type) {
default: return;
case Input::Right:
APPLY_KEMPSTON(0x01);
APPLY_SINCLAIR(0x0208);
break;
case Input::Left:
APPLY_KEMPSTON(0x02);
APPLY_SINCLAIR(0x0110);
break;
case Input::Down:
APPLY_KEMPSTON(0x04);
APPLY_SINCLAIR(0x0404);
break;
case Input::Up:
APPLY_KEMPSTON(0x08);
APPLY_SINCLAIR(0x0802);
break;
case Input::Fire:
APPLY_KEMPSTON(0x10);
APPLY_SINCLAIR(0x1001);
break;
}
#undef APPLY_KEMPSTON
#undef APPLY_SINCLAIR
}
/// @returns The value that a Kempston joystick interface would report if this joystick
/// were plugged into it.
uint8_t get_kempston() {
return kempston_;
}
/// @returns The value that a Sinclair interface would report if this joystick
/// were plugged into it via @c port (which should be either 0 or 1, for ports 1 or 2).
uint8_t get_sinclair(int port) {
return uint8_t(sinclair_ >> (port * 8));
}
private:
uint8_t kempston_ = 0x00;
uint16_t sinclair_ = 0xffff;
};
}
namespace Sinclair {
namespace ZXSpectrum {
@@ -56,6 +124,7 @@ template<Model model> class ConcreteMachine:
public CPU::Z80::BusHandler,
public Machine,
public MachineTypes::AudioProducer,
public MachineTypes::JoystickMachine,
public MachineTypes::MappedKeyboardMachine,
public MachineTypes::MediaTarget,
public MachineTypes::ScanProducer,
@@ -77,17 +146,32 @@ template<Model model> class ConcreteMachine:
set_clock_rate(clock_rate());
speaker_.set_input_rate(float(clock_rate()) / 2.0f);
// 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} });
if(!roms[0]) throw ROMMachine::Error::MissingROMs;
memcpy(rom_.data(), roms[0]->data(), std::min(rom_.size(), roms[0]->size()));
ROM::Name rom_name;
switch(model) {
case Model::SixteenK:
case Model::FortyEightK: rom_name = ROM::Name::Spectrum48k; break;
case Model::OneTwoEightK: rom_name = ROM::Name::Spectrum128k; break;
case Model::Plus2: rom_name = ROM::Name::SpecrumPlus2; break;
case Model::Plus2a:
case Model::Plus3: rom_name = ROM::Name::SpectrumPlus3; break;
// TODO: possibly accept the +3 ROM in multiple parts?
}
const auto request = ROM::Request(rom_name);
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
const auto &rom = roms.find(rom_name)->second;
memcpy(rom_.data(), rom.data(), std::min(rom_.size(), rom.size()));
// Register for sleeping notifications.
fdc_->set_clocking_hint_observer(this);
tape_player_.set_clocking_hint_observer(this);
// Attach a couple of joysticks.
joysticks_.emplace_back(new Joystick);
joysticks_.emplace_back(new Joystick);
// Set up initial memory map.
update_memory_map();
set_video_address();
@@ -102,6 +186,29 @@ template<Model model> class ConcreteMachine:
duration_to_press_enter_ = Cycles(5 * clock_rate());
keyboard_.set_key_state(ZX::Keyboard::KeyEnter, true);
}
// Install state if supplied.
if(target.state) {
const auto state = static_cast<State *>(target.state.get());
state->z80.apply(z80_);
state->video.apply(*video_.last_valid());
state->ay.apply(ay_);
// If this is a 48k or 16k machine, remap source data from its original
// linear form to whatever the banks end up being; otherwise copy as is.
if(model <= Model::FortyEightK) {
const size_t num_banks = std::min(size_t(48*1024), state->ram.size()) >> 14;
for(size_t c = 0; c < num_banks; c++) {
memcpy(&write_pointers_[c + 1][(c+1) * 0x4000], &state->ram[c * 0x4000], 0x4000);
}
} else {
memcpy(ram_.data(), state->ram.data(), std::min(ram_.size(), state->ram.size()));
port1ffd_ = state->last_1ffd;
port7ffd_ = state->last_7ffd;
update_memory_map();
}
}
}
~ConcreteMachine() {
@@ -109,7 +216,7 @@ template<Model model> 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:
@@ -136,7 +243,7 @@ template<Model model> 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.
@@ -180,49 +287,149 @@ template<Model model> class ConcreteMachine:
video_->set_display_type(display_type);
}
Outputs::Display::DisplayType get_display_type() const override {
return video_->get_display_type();
}
// MARK: - BusHandler.
forceinline HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) {
using PartialMachineCycle = CPU::Z80::PartialMachineCycle;
HalfCycles delay(0);
const uint16_t address = cycle.address ? *cycle.address : 0x0000;
// 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) {
const auto delay = video_.last_valid()->access_delay(video_.time_since_flush());
advance(cycle.length + delay);
return delay;
}
} else {
switch(cycle.operation) {
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:
case CPU::Z80::PartialMachineCycle::Interrupt:
// For these, carry on into the actual handler, below.
break;
// For anything else that isn't listed below, just advance
// time and conclude here.
default:
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();
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;
} break;
case PartialMachineCycle::ReadOpcodeStart:
case PartialMachineCycle::ReadStart:
case PartialMachineCycle::WriteStart: {
// These all start by loading the address bus, then set MREQ
// half a cycle later.
if(is_contended_[address >> 14]) {
const auto delay = video_.last_valid()->access_delay(video_.time_since_flush());
advance(cycle.length + delay);
return delay;
}
} 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<int>();
assert(!(half_cycles & 1));
HalfCycles time = video_.time_since_flush();
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;
}
advance(cycle.length + delay);
return delay;
}
} break;
}
}
// For all other machine cycles, model the action as happening at the end of the machine cycle;
// that means advancing time now.
advance(cycle.length);
switch(cycle.operation) {
default: break;
case PartialMachineCycle::ReadOpcodeStart:
case PartialMachineCycle::ReadStart:
case PartialMachineCycle::WriteStart:
// Apply contention if necessary.
//
// 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(is_contended_[address >> 14]) {
delay = video_.last_valid()->access_delay(video_.time_since_flush() + HalfCycles(1));
}
break;
case PartialMachineCycle::ReadOpcode:
// Fast loading: ROM version.
//
// The below patches over the 'LD-BYTES' routine from the 48kb ROM.
if(use_fast_tape_hack_ && address == 0x0556 && read_pointers_[0] == &rom_[0xc000]) {
if(perform_rom_ld_bytes()) {
// Stop pressing enter, if neccessry.
if(duration_to_press_enter_ > Cycles(0)) {
duration_to_press_enter_ = Cycles(0);
keyboard_.set_key_state(ZX::Keyboard::KeyEnter, false);
}
// 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_[classic_rom_offset()]) {
// Stop pressing enter, if neccessry.
if(duration_to_press_enter_ > Cycles(0)) {
duration_to_press_enter_ = Cycles(0);
keyboard_.set_key_state(ZX::Keyboard::KeyEnter, false);
}
if(perform_rom_ld_bytes_56b()) {
*cycle.value = 0xc9; // i.e. RET.
break;
}
}
[[fallthrough]];
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) {
if(is_contended_[address >> 14]) {
video_->set_last_contended_area_access(*cycle.value);
}
}
break;
case PartialMachineCycle::Write:
@@ -230,9 +437,20 @@ template<Model model> class ConcreteMachine:
if(is_video_[address >> 14] && (address & 0x3fff) < 6912) {
video_.flush();
}
write_pointers_[address >> 14][address] = *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;
// Partial port decodings here and in ::Input are as documented
// at https://worldofspectrum.org/faq/reference/ports.htm
case PartialMachineCycle::Output:
// Test for port FE.
if(!(address&1)) {
@@ -247,55 +465,70 @@ template<Model model> class ConcreteMachine:
}
// Test for classic 128kb paging register (i.e. port 7ffd).
if((address & 0xc002) == 0x4000) {
if (
(model >= Model::OneTwoEightK && model <= Model::Plus2 && (address & 0x8002) == 0x0000) ||
(model >= Model::Plus2a && (address & 0xc002) == 0x4000)
) {
port7ffd_ = *cycle.value;
update_memory_map();
// 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::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 constexpr (model == Model::Plus3) {
fdc_->set_motor_on(*cycle.value & 0x08);
}
}
}
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);
// Route to the AY if one is fitted.
if constexpr (model >= Model::OneTwoEightK) {
switch(address & 0xc002) {
case 0xc000:
// Select AY register.
update_audio();
GI::AY38910::Utility::select_register(ay_, *cycle.value);
break;
case 0x8000:
// Write to AY register.
update_audio();
GI::AY38910::Utility::write_data(ay_, *cycle.value);
break;
}
}
// Check for FDC accesses.
if constexpr (model == Model::Plus3) {
switch(address) {
switch(address & 0xf002) {
default: break;
case 0x3ffd: case 0x2ffd:
case 0x3000: case 0x2000:
fdc_->write((address >> 12) & 1, *cycle.value);
break;
}
}
break;
case PartialMachineCycle::Input:
case PartialMachineCycle::Input: {
bool did_match = false;
*cycle.value = 0xff;
if(!(address&32)) {
did_match = true;
*cycle.value &= static_cast<Joystick *>(joysticks_[0].get())->get_kempston();
}
if(!(address&1)) {
did_match = true;
// Port FE:
//
// address b8+: mask of keyboard lines to select
@@ -305,11 +538,15 @@ template<Model model> class ConcreteMachine:
*cycle.value &= keyboard_.read(address);
*cycle.value &= tape_player_.get_input() ? 0xbf : 0xff;
// If this read is within 200 cycles of the previous,
// count it as an adjacent hit; if 20 of those have
// occurred then start the tape motor.
// Add Joystick input on top.
if(!(address&0x1000)) *cycle.value &= static_cast<Joystick *>(joysticks_[0].get())->get_sinclair(0);
if(!(address&0x0800)) *cycle.value &= static_cast<Joystick *>(joysticks_[1].get())->get_sinclair(1);
// If this read is between 50 and 200 cycles since the
// previous, count it as an adjacent hit; if 20 of those
// have occurred then start the tape motor.
if(use_automatic_tape_motor_control_) {
if(cycles_since_tape_input_read_ < HalfCycles(400)) {
if(cycles_since_tape_input_read_ >= HalfCycles(100) && cycles_since_tape_input_read_ < HalfCycles(200)) {
++recent_tape_hits_;
if(recent_tape_hits_ == 20) {
@@ -323,31 +560,50 @@ template<Model model> class ConcreteMachine:
}
}
if((address & 0xc002) == 0xc000) {
// Read from AY register.
update_audio();
*cycle.value &= GI::AY38910::Utility::read_data(ay_);
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_);
}
}
// 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/
if((address & 0xf003) == 0x0001) {
*cycle.value &= video_->get_current_fetch();
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) {
switch(address) {
switch(address & 0xf002) {
default: break;
case 0x3ffd: case 0x2ffd:
case 0x3000: case 0x2000:
*cycle.value &= fdc_->read((address >> 12) & 1);
break;
}
}
if constexpr (model <= Model::Plus2) {
if(!did_match) {
*cycle.value = video_->get_floating_value();
}
}
} break;
case PartialMachineCycle::Interrupt:
// At least one piece of Spectrum software, Escape from M.O.N.J.A.S. explicitly
// assumes that a 0xff value will be on the bus during an interrupt acknowledgment.
// I wasn't otherwise aware that this value is reliable.
*cycle.value = 0xff;
break;
}
advance(cycle.length + delay);
return delay;
return HalfCycles(0);
}
private:
@@ -356,24 +612,24 @@ template<Model model> class ConcreteMachine:
video_ += duration;
if(video_.did_flush()) {
z80_.set_interrupt_line(video_.last_valid()->get_interrupt_line());
z80_.set_interrupt_line(video_.last_valid()->get_interrupt_line(), video_.last_sequence_point_overrun());
}
if(!tape_player_is_sleeping_) tape_player_.run_for(duration.as_integral());
// Update automatic tape motor control, if enabled; if it's been
// 3 seconds since software last possibly polled the tape, stop it.
if(use_automatic_tape_motor_control_ && cycles_since_tape_input_read_ < HalfCycles(clock_rate() * 6)) {
// 0.5 seconds since software last possibly polled the tape, stop it.
if(use_automatic_tape_motor_control_ && cycles_since_tape_input_read_ < HalfCycles(clock_rate())) {
cycles_since_tape_input_read_ += duration;
if(cycles_since_tape_input_read_ >= HalfCycles(clock_rate() * 6)) {
if(cycles_since_tape_input_read_ >= HalfCycles(clock_rate())) {
tape_player_.set_motor_control(false);
recent_tape_hits_ = 0;
}
}
if constexpr (model == Model::Plus3) {
if(!fdc_is_sleeping_) fdc_ += Cycles(duration.as_integral());
fdc_ += Cycles(duration.as_integral());
}
if(typer_) typer_->run_for(duration);
@@ -421,6 +677,7 @@ template<Model model> class ConcreteMachine:
// If there are any tapes supplied, use the first of them.
if(!media.tapes.empty()) {
tape_player_.set_tape(media.tapes.front());
set_use_fast_tape();
}
// Insert up to four disks.
@@ -437,7 +694,6 @@ template<Model model> class ConcreteMachine:
// MARK: - ClockingHint::Observer.
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference) override {
fdc_is_sleeping_ = fdc_.last_valid()->preferred_clocking() == ClockingHint::Preference::None;
tape_player_is_sleeping_ = tape_player_.preferred_clocking() == ClockingHint::Preference::None;
}
@@ -464,6 +720,7 @@ template<Model model> class ConcreteMachine:
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly); // OptionsType is arbitrary, but not optional.
options->automatic_tape_motor_control = use_automatic_tape_motor_control_;
options->quickload = allow_fast_tape_hack_;
options->output = get_video_signal_configurable();
return options;
}
@@ -552,13 +809,21 @@ template<Model model> class ConcreteMachine:
set_memory(2, 2);
set_memory(3, port7ffd_ & 7);
}
// Potentially lock paging, _after_ the current
// port values have taken effect.
disable_paging_ = port7ffd_ & 0x20;
}
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 < 0x80 && source & 1;
}
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;
@@ -591,8 +856,15 @@ template<Model model> class ConcreteMachine:
}
// MARK: - Video.
static constexpr VideoTiming video_timing = VideoTiming::Plus3;
JustInTimeActor<Video<video_timing>> video_;
using VideoType =
std::conditional_t<
model <= Model::FortyEightK, Video::Video<Video::Timing::FortyEightK>,
std::conditional_t<
model <= Model::Plus2, Video::Video<Video::Timing::OneTwoEightK>,
Video::Video<Video::Timing::Plus3>
>
>;
JustInTimeActor<VideoType> video_;
// MARK: - Keyboard.
Sinclair::ZX::Keyboard::Keyboard keyboard_;
@@ -613,31 +885,42 @@ template<Model model> class ConcreteMachine:
}
// Reimplements the 'LD-BYTES' routine, as documented at
// https://skoolkid.github.io/rom/asm/0556.html i.e.
// https://skoolkid.github.io/rom/asm/0556.html but picking
// up from address 56b i.e.
//
// In:
// A: 0x00 or 0xff for block type;
// F: carry set if loading, clear if verifying;
// A': 0x00 or 0xff for block type;
// F': carry set if loading, clear if verifying;
// DE: block length;
// IX: start address.
//
// Out:
// F: carry set for success, clear for error.
bool perform_rom_ld_bytes() {
//
// And, empirically:
// IX: one beyond final address written;
// DE: 0;
// L: parity byte;
// H: 0 for no error, 0xff for error;
// A: same as H.
// BC: ???
bool perform_rom_ld_bytes_56b() {
using Parser = Storage::Tape::ZXSpectrum::Parser;
Parser parser(Parser::MachineType::ZXSpectrum);
using Register = CPU::Z80::Register;
uint8_t flags = uint8_t(z80_.get_value_of_register(Register::Flags));
uint8_t flags = uint8_t(z80_.get_value_of_register(Register::FlagsDash));
if(!(flags & 1)) return false;
const uint8_t block_type = uint8_t(z80_.get_value_of_register(Register::A));
const uint8_t block_type = uint8_t(z80_.get_value_of_register(Register::ADash));
const auto block = parser.find_block(tape_player_.get_tape());
if(!block || block_type != (*block).type) return false;
uint16_t length = z80_.get_value_of_register(Register::DE);
uint16_t target = z80_.get_value_of_register(Register::IX);
flags = 0x93;
uint8_t parity = 0x00;
while(length--) {
auto next = parser.get_byte(tape_player_.get_tape());
if(!next) {
@@ -646,19 +929,55 @@ template<Model model> class ConcreteMachine:
}
write_pointers_[target >> 14][target] = *next;
parity ^= *next;
++target;
}
auto stored_parity = parser.get_byte(tape_player_.get_tape());
if(!stored_parity) {
flags &= ~1;
} else {
z80_.set_value_of_register(Register::L, *stored_parity);
}
z80_.set_value_of_register(Register::Flags, flags);
z80_.set_value_of_register(Register::DE, length);
z80_.set_value_of_register(Register::IX, target);
const uint8_t h = (flags & 1) ? 0x00 : 0xff;
z80_.set_value_of_register(Register::H, h);
z80_.set_value_of_register(Register::A, h);
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<Amstrad::FDC, 1, 1, Cycles> fdc_;
bool fdc_is_sleeping_ = false;
JustInTimeActor<Amstrad::FDC, Cycles> fdc_;
// MARK: - Automatic startup.
Cycles duration_to_press_enter_;
// MARK: - Joysticks
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
return joysticks_;
}
};
@@ -671,8 +990,12 @@ Machine *Machine::ZXSpectrum(const Analyser::Static::Target *target, const ROMMa
const auto zx_target = dynamic_cast<const Analyser::Static::ZXSpectrum::Target *>(target);
switch(zx_target->model) {
case Model::Plus2a: return new ConcreteMachine<Model::Plus2a>(*zx_target, rom_fetcher);
case Model::Plus3: return new ConcreteMachine<Model::Plus3>(*zx_target, rom_fetcher);
case Model::SixteenK: return new ConcreteMachine<Model::SixteenK>(*zx_target, rom_fetcher);
case Model::FortyEightK: return new ConcreteMachine<Model::FortyEightK>(*zx_target, rom_fetcher);
case Model::OneTwoEightK: return new ConcreteMachine<Model::OneTwoEightK>(*zx_target, rom_fetcher);
case Model::Plus2: return new ConcreteMachine<Model::Plus2>(*zx_target, rom_fetcher);
case Model::Plus2a: return new ConcreteMachine<Model::Plus2a>(*zx_target, rom_fetcher);
case Model::Plus3: return new ConcreteMachine<Model::Plus3>(*zx_target, rom_fetcher);
}
return nullptr;

View File

@@ -31,7 +31,7 @@ class Machine {
friend Configurable::DisplayOption<Options>;
friend Configurable::QuickloadOption<Options>;
public:
bool automatic_tape_motor_control;
bool automatic_tape_motor_control = true;
Options(Configurable::OptionsType type) :
Configurable::DisplayOption<Options>(type == Configurable::OptionsType::UserFriendly ? Configurable::Display::RGB : Configurable::Display::CompositeColour),

View File

@@ -0,0 +1,26 @@
//
// StateProducer.hpp
// Clock Signal
//
// Created by Thomas Harte on 24/04/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef State_h
#define State_h
#include <memory>
#include "../Analyser/Static/StaticAnalyser.hpp"
namespace MachineTypes {
struct StateProducer {
// TODO.
// virtual bool get_state(Analyser::Static::State *, [[maybe_unused]] bool advance_to_simple = false) {
// return false;
// }
};
};
#endif /* State_h */

View File

@@ -20,6 +20,7 @@
#include "../ColecoVision/ColecoVision.hpp"
#include "../Commodore/Vic-20/Vic20.hpp"
#include "../Electron/Electron.hpp"
#include "../Enterprise/Enterprise.hpp"
#include "../MasterSystem/MasterSystem.hpp"
#include "../MSX/MSX.hpp"
#include "../Oric/Oric.hpp"
@@ -34,6 +35,7 @@
#include "../../Analyser/Static/Atari2600/Target.hpp"
#include "../../Analyser/Static/AtariST/Target.hpp"
#include "../../Analyser/Static/Commodore/Target.hpp"
#include "../../Analyser/Static/Enterprise/Target.hpp"
#include "../../Analyser/Static/Macintosh/Target.hpp"
#include "../../Analyser/Static/MSX/Target.hpp"
#include "../../Analyser/Static/Oric/Target.hpp"
@@ -49,7 +51,7 @@ Machine::DynamicMachine *Machine::MachineForTarget(const Analyser::Static::Targe
Machine::DynamicMachine *machine = nullptr;
try {
#define BindD(name, m) case Analyser::Machine::m: machine = new Machine::TypedDynamicMachine<name::Machine>(name::Machine::m(target, rom_fetcher)); break;
#define BindD(name, m) case Analyser::Machine::m: machine = new Machine::TypedDynamicMachine<::name::Machine>(name::Machine::m(target, rom_fetcher)); break;
#define Bind(m) BindD(m, m)
switch(target->machine) {
Bind(AmstradCPC)
@@ -61,6 +63,7 @@ Machine::DynamicMachine *Machine::MachineForTarget(const Analyser::Static::Targe
BindD(Coleco::Vision, ColecoVision)
BindD(Commodore::Vic20, Vic20)
Bind(Electron)
Bind(Enterprise)
Bind(MSX)
Bind(Oric)
BindD(Sega::MasterSystem, MasterSystem)
@@ -127,6 +130,7 @@ std::string Machine::ShortNameForTargetMachine(const Analyser::Machine machine)
case Analyser::Machine::AtariST: return "AtariST";
case Analyser::Machine::ColecoVision: return "ColecoVision";
case Analyser::Machine::Electron: return "Electron";
case Analyser::Machine::Enterprise: return "Enterprise";
case Analyser::Machine::Macintosh: return "Macintosh";
case Analyser::Machine::MasterSystem: return "MasterSystem";
case Analyser::Machine::MSX: return "MSX";
@@ -148,6 +152,7 @@ std::string Machine::LongNameForTargetMachine(Analyser::Machine machine) {
case Analyser::Machine::AtariST: return "Atari ST";
case Analyser::Machine::ColecoVision: return "ColecoVision";
case Analyser::Machine::Electron: return "Acorn Electron";
case Analyser::Machine::Enterprise: return "Enterprise";
case Analyser::Machine::Macintosh: return "Apple Macintosh";
case Analyser::Machine::MasterSystem: return "Sega Master System";
case Analyser::Machine::MSX: return "MSX";
@@ -177,11 +182,13 @@ std::vector<std::string> Machine::AllMachines(Type type, bool long_names) {
AddName(AppleIIgs);
AddName(AtariST);
AddName(Electron);
AddName(Enterprise);
AddName(Macintosh);
AddName(MSX);
AddName(Oric);
AddName(Vic20);
AddName(ZX8081);
AddName(ZXSpectrum);
}
#undef AddName
@@ -200,6 +207,7 @@ std::map<std::string, std::unique_ptr<Reflection::Struct>> Machine::AllOptionsBy
Emplace(AtariST, Atari::ST::Machine);
Emplace(ColecoVision, Coleco::Vision::Machine);
Emplace(Electron, Electron::Machine);
Emplace(Enterprise, Enterprise::Machine);
Emplace(Macintosh, Apple::Macintosh::Machine);
Emplace(MasterSystem, Sega::MasterSystem::Machine);
Emplace(MSX, MSX::Machine);
@@ -225,6 +233,7 @@ std::map<std::string, std::unique_ptr<Analyser::Static::Target>> Machine::Target
Add(AppleIIgs);
Add(AtariST);
AddMapped(Electron, Acorn);
Add(Enterprise);
Add(Macintosh);
Add(MSX);
Add(Oric);

View File

@@ -8,6 +8,8 @@
#include "MemoryPacker.hpp"
#include <cstddef>
void Memory::PackBigEndian16(const std::vector<uint8_t> &source, uint16_t *target) {
for(size_t c = 0; c < source.size(); c += 2) {
target[c >> 1] = uint16_t(source[c] << 8) | uint16_t(source[c+1]);

View File

@@ -0,0 +1,545 @@
//
// ROMCatalogue.cpp
// Clock Signal
//
// Created by Thomas Harte on 01/06/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "ROMCatalogue.hpp"
#include <algorithm>
#include <cassert>
#include <codecvt>
#include <iomanip>
#include <locale>
#include <sstream>
using namespace ROM;
namespace {
constexpr Name MaxName = Name::SpectrumPlus3;
}
Request::Request(Name name, bool optional) {
node.name = name;
node.is_optional = optional;
}
Request Request::append(Node::Type type, const Request &rhs) {
// If either side is empty, act appropriately.
if(node.empty() && !rhs.node.empty()) {
return rhs;
}
if(rhs.node.empty()) {
return *this;
}
// Just copy in the RHS child nodes if types match.
if(node.type == type && rhs.node.type == type) {
Request new_request = *this;
new_request.node.children.insert(new_request.node.children.end(), rhs.node.children.begin(), rhs.node.children.end());
new_request.node.sort();
return new_request;
}
// Possibly: left is appropriate request and rhs is just one more thing?
if(node.type == type && rhs.node.type == Node::Type::One) {
Request new_request = *this;
new_request.node.children.push_back(rhs.node);
new_request.node.sort();
return new_request;
}
// Or: right is appropriate request and this is just one more thing?
if(rhs.node.type == type && node.type == Node::Type::One) {
Request new_request = rhs;
new_request.node.children.push_back(node);
new_request.node.sort();
return new_request;
}
// Otherwise create a new parent node.
Request parent;
parent.node.type = type;
parent.node.children.push_back(this->node);
parent.node.children.push_back(rhs.node);
return parent;
}
Request Request::operator &&(const Request &rhs) {
return append(Node::Type::All, rhs);
}
Request Request::operator ||(const Request &rhs) {
return append(Node::Type::Any, rhs);
}
bool Request::validate(Map &map) const {
return node.validate(map);
}
std::vector<ROM::Description> Request::all_descriptions() const {
std::vector<Description> result;
node.add_descriptions(result);
return result;
}
void Request::Node::add_descriptions(std::vector<Description> &result) const {
if(type == Type::One) {
result.push_back(name);
return;
}
for(const auto &child: children) {
child.add_descriptions(result);
}
}
Request Request::subtract(const ROM::Map &map) const {
Request copy(*this);
if(copy.node.subtract(map)) {
copy.node.name = Name::None;
copy.node.type = Node::Type::One;
}
return copy;
}
bool Request::Node::subtract(const ROM::Map &map) {
switch(type) {
case Type::One:
return map.find(name) != map.end();
default: {
bool has_all = true;
bool has_any = false;
auto iterator = children.begin();
while(iterator != children.end()) {
const bool did_subtract = iterator->subtract(map);
has_all &= did_subtract;
has_any |= did_subtract;
if(did_subtract) {
iterator = children.erase(iterator);
} else {
++iterator;
}
}
return (type == Type::All && has_all) || (type == Type::Any && has_any);
}
}
}
bool Request::empty() {
return node.type == Node::Type::One && node.name == Name::None;
}
bool Request::Node::validate(Map &map) const {
// Leaf nodes are easy: check that the named ROM is present,
// unless it's optional, in which case it is always valid.
//
// If it is present, make sure it's the proper size.
if(type == Type::One) {
auto rom = map.find(name);
if(rom == map.end()) {
return is_optional;
}
const Description description(name);
rom->second.resize(description.size);
return true;
}
// This is a collection node then. Check for both any or all
// simultaneously, since all nodes will need to be visited
// regardless of any/all in order to ensure proper sizing.
bool has_all = true;
bool has_any = false;
for(const auto &child: children) {
const bool is_valid = child.validate(map);
has_all &= is_valid;
has_any |= is_valid;
}
return (type == Type::Any && has_any) || (type == Type::All && has_all);
}
void Request::visit(
const std::function<void(ListType, size_t)> &enter_list,
const std::function<void(void)> &exit_list,
const std::function<void(ROM::Request::ListType, const ROM::Description &, bool, size_t)> &add_item
) const {
node.visit(enter_list, exit_list, add_item);
}
void Request::visit(
const std::function<void(LineItem, ListType, int, const ROM::Description *, bool, size_t)> &add_item
) const {
int indentation_level = 0;
node.visit(
[&indentation_level, &add_item] (ROM::Request::ListType type, size_t size) {
add_item(LineItem::NewList, type, indentation_level, nullptr, false, size);
++indentation_level;
},
[&indentation_level] {
--indentation_level;
},
[&indentation_level, &add_item] (ROM::Request::ListType type, const ROM::Description &rom, bool is_optional, size_t remaining) {
add_item(LineItem::Description, type, indentation_level, &rom, is_optional, remaining);
}
);
}
void Request::Node::visit(
const std::function<void(ListType, size_t)> &enter_list,
const std::function<void(void)> &exit_list,
const std::function<void(ROM::Request::ListType type, const ROM::Description &, bool is_optional, size_t remaining)> &add_item
) const {
switch(type) {
case Type::One:
enter_list(ListType::Single, 1);
add_item(ROM::Request::ListType::Any, Description(name), is_optional, 0);
exit_list();
break;
case Type::Any:
case Type::All: {
const ListType list_type = type == Type::Any ? ListType::Any : ListType::All;
enter_list(list_type, children.size());
for(size_t index = 0; index < children.size(); index++) {
auto &child = children[index];
if(child.type == Type::One) {
add_item(list_type, Description(child.name), child.is_optional, children.size() - 1 - index);
} else {
child.visit(enter_list, exit_list, add_item);
}
}
exit_list();
} break;
}
}
std::vector<Description> ROM::all_descriptions() {
std::vector<Description> result;
for(int name = 1; name <= MaxName; name++) {
result.push_back(Description(ROM::Name(name)));
}
return result;
}
std::optional<Description> Description::from_crc(uint32_t crc32) {
for(int name = 1; name <= MaxName; name++) {
const Description candidate = Description(ROM::Name(name));
const auto found_crc = std::find(candidate.crc32s.begin(), candidate.crc32s.end(), crc32);
if(found_crc != candidate.crc32s.end()) {
return candidate;
}
}
return std::nullopt;
}
std::string Description::description(int flags) const {
std::stringstream output;
// If there are no CRCs, don't output them.
if(crc32s.empty()) flags &= ~ DescriptionFlag::CRC;
// Print the file name(s) and the descriptive name.
if(flags & DescriptionFlag::Filename) {
flags &= ~DescriptionFlag::Filename;
output << machine_name << '/';
if(file_names.size() == 1) {
output << file_names[0];
} else {
output << "{";
bool is_first = true;
for(const auto &file_name: file_names) {
if(!is_first) output << " or ";
output << file_name;
is_first = false;
}
output << "}";
}
output << " (" << descriptive_name;
if(!flags) {
output << ")";
return output.str();
}
output << "; ";
} else {
output << descriptive_name;
if(!flags) {
return output.str();
}
output << " (";
}
// Print the size.
if(flags & DescriptionFlag::Size) {
flags &= ~DescriptionFlag::Size;
output << size << " bytes";
if(!flags) {
output << ")";
return output.str();
}
output << "; ";
}
// Print the CRC(s).
if(flags & DescriptionFlag::CRC) {
flags &= ~DescriptionFlag::CRC;
output << ((crc32s.size() > 1) ? "usual crc32s: " : "usual crc32: ");
bool is_first = true;
for(const auto crc32: crc32s) {
if(!is_first) output << ", ";
is_first = false;
output << std::hex << std::setfill('0') << std::setw(8) << crc32;
}
if(!flags) {
output << ")";
return output.str();
}
}
return output.str();
}
std::wstring Request::description(int description_flags, wchar_t bullet_point) {
std::wstringstream output;
std::wstring_convert<std::codecvt_utf8<wchar_t>> wstring_converter;
visit(
[&output, description_flags, bullet_point, &wstring_converter] (ROM::Request::LineItem item, ROM::Request::ListType type, int indentation_level, const ROM::Description *description, bool is_optional, size_t remaining) {
if(indentation_level) {
output << std::endl;
for(int c = 0; c < indentation_level; c++) output << '\t';
output << bullet_point << ' ';
}
switch(item) {
case ROM::Request::LineItem::NewList:
if(remaining > 1) {
if(!indentation_level) output << " ";
switch(type) {
default:
case ROM::Request::ListType::All: output << "all of:"; break;
case ROM::Request::ListType::Any:
if(remaining == 2) {
output << "either of:";
} else {
output << "any of:";
}
break;
}
} else {
output << ":";
}
break;
case ROM::Request::LineItem::Description:
if(is_optional) output << "optionally, ";
output << wstring_converter.from_bytes(description->description(description_flags));
if(remaining) {
output << ";";
if(remaining == 1) {
output << ((type == ROM::Request::ListType::All) ? " and" : " or");
}
} else {
output << ".";
}
break;
}
}
);
return output.str();
}
Description::Description(Name name) {
switch(name) {
default: assert(false); break;
case Name::AMSDOS: *this = Description(name, "AmstradCPC", "the Amstrad Disk Operating System", "amsdos.rom", 16*1024, 0x1fe22ecdu); break;
case Name::CPC464Firmware: *this = Description(name, "AmstradCPC", "the CPC 464 firmware", "os464.rom", 16*1024, 0x815752dfu); break;
case Name::CPC464BASIC: *this = Description(name, "AmstradCPC", "the CPC 464 BASIC ROM", "basic464.rom", 16*1024, 0x7d9a3bacu); break;
case Name::CPC664Firmware: *this = Description(name, "AmstradCPC", "the CPC 664 firmware", "os664.rom", 16*1024, 0x3f5a6dc4u); break;
case Name::CPC664BASIC: *this = Description(name, "AmstradCPC", "the CPC 664 BASIC ROM", "basic664.rom", 16*1024, 0x32fee492u); break;
case Name::CPC6128Firmware: *this = Description(name, "AmstradCPC", "the CPC 6128 firmware", "os6128.rom", 16*1024, 0x0219bb74u); break;
case Name::CPC6128BASIC: *this = Description(name, "AmstradCPC", "the CPC 6128 BASIC ROM", "basic6128.rom", 16*1024, 0xca6af63du); break;
case Name::AppleIIEnhancedE: *this = Description(name, "AppleII", "the Enhanced Apple IIe ROM", "apple2e.rom", 32*1024, 0x65989942u); break;
case Name::AppleIIe: *this = Description(name, "AppleII", "the Apple IIe ROM", "apple2eu.rom", 32*1024, 0xe12be18du); break;
case Name::AppleIIPlus: *this = Description(name, "AppleII", "the Apple II+ ROM", "apple2.rom", 12*1024, 0xf66f9c26u); break;
case Name::AppleIIOriginal: *this = Description(name, "AppleII", "the original Apple II ROM", "apple2o.rom", 12*1024, 0xba210588u); break;
case Name::AppleIICharacter: *this = Description(name, "AppleII", "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6u); break;
case Name::AppleIIeCharacter: *this = Description(name, "AppleII", "the Apple IIe character ROM", "apple2eu-character.rom", 4*1024, 0x816a86f1u); break;
case Name::AppleIIEnhancedECharacter:
*this = Description(name, "AppleII", "the Enhanced Apple IIe character ROM", "apple2e-character.rom", 4*1024, 0x2651014du);
break;
case Name::AppleIIgsROM00: /* TODO */
case Name::AppleIIgsROM01: *this = Description(name, "AppleIIgs", "the Apple IIgs ROM01", "apple2gs.rom", 128*1024, 0x42f124b0u); break;
case Name::AppleIIgsROM03: *this = Description(name, "AppleIIgs", "the Apple IIgs ROM03", "apple2gs.rom2", 256*1024, 0xde7ddf29u); break;
case Name::AppleIIgsCharacter: *this = Description(name, "AppleIIgs", "the Apple IIgs character ROM", "apple2gs.chr", 4*1024, 0x91e53cd8u); break;
case AppleIIgsMicrocontrollerROM03:
*this = Description(name, "AppleIIgs", "the Apple IIgs ROM03 ADB microcontroller ROM", "341s0632-2", 4*1024, 0xe1c11fb0u);
break;
case Name::DiskIIBoot16Sector:
*this = Description(name, "DiskII", "the Disk II 16-sector boot ROM", "boot-16.rom", 256, 0xce7144f6u);
break;
case Name::DiskIIStateMachine16Sector:
*this = Description(name, "DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, std::initializer_list<uint32_t>{ 0x9796a238, 0xb72a2c70 } );
break;
case Name::DiskIIBoot13Sector:
*this = Description(name, "DiskII", "the Disk II 13-sector boot ROM", "boot-13.rom", 256, 0xd34eb2ffu);
break;
case Name::DiskIIStateMachine13Sector:
*this = Description(name, "DiskII", "the Disk II 13-sector state machine ROM", "state-machine-13.rom", 256, 0x62e22620u);
break;
case Name::EnterpriseEXOS10: {
const std::initializer_list<std::string> filenames = {"exos10.bin", "Exos (198x)(Enterprise).bin"};
*this = Description(name, "Enterprise", "the Enterprise EXOS ROM v1.0", filenames, 32 * 1024, 0x30b26387u);
} break;
case Name::EnterpriseEXOS20: {
const std::initializer_list<std::string> filenames = {"exos20.bin", "Expandible OS v2.0 (1984)(Intelligent Software).bin"};
*this = Description(name, "Enterprise", "the Enterprise EXOS ROM v2.0", filenames, 32 * 1024, 0xd421795fu);
} break;
case Name::EnterpriseEXOS21: {
const std::initializer_list<std::string> filenames = {"exos21.bin", "Expandible OS v2.1 (1985)(Intelligent Software).bin"};
*this = Description(name, "Enterprise", "the Enterprise EXOS ROM v2.1", filenames, 32 * 1024, 0x982a3b44u);
} break;
case Name::EnterpriseEXOS23: {
const std::initializer_list<std::string> filenames = {"exos23.bin", "Expandible OS v2.3 (1987)(Intelligent Software).bin"};
*this = Description(name, "Enterprise", "the Enterprise EXOS ROM v2.1", filenames, 64 * 1024, 0x24838410u);
} break;
case Name::EnterpriseBASIC10: {
const std::initializer_list<std::string> filenames = {"basic10.bin"};
*this = Description(name, "Enterprise", "the Enterprise BASIC ROM v1.0", filenames, 16 * 1024, 0xd62e4fb7u);
} break;
case Name::EnterpriseBASIC10Part1: {
const std::initializer_list<std::string> filenames = {"BASIC 1.0 - EPROM 1-2 (198x)(Enterprise).bin"};
*this = Description(name, "Enterprise", "the Enterprise BASIC ROM v1.0, Part 1", filenames, 8193, 0x37bf48e1u);
} break;
case Name::EnterpriseBASIC10Part2: {
const std::initializer_list<std::string> filenames = {"BASIC 1.0 - EPROM 2-2 (198x)(Enterprise).bin"};
*this = Description(name, "Enterprise", "the Enterprise BASIC ROM v1.0, Part 2", filenames, 8193, 0xc5298c79u);
} break;
case Name::EnterpriseBASIC11: {
const std::initializer_list<std::string> filenames = {"basic11.bin"};
*this = Description(name, "Enterprise", "the Enterprise BASIC ROM v1.1", filenames, 16 * 1024, 0x683cf455u);
} break;
case Name::EnterpriseBASIC11Suffixed: {
const std::initializer_list<std::string> filenames = {"BASIC 1.1 - EPROM 1.1 (198x)(Enterprise).bin"};
*this = Description(name, "Enterprise", "the Enterprise BASIC ROM v1.1, with trailing byte", filenames, 16385, 0xc96b7602u);
} break;
case Name::EnterpriseBASIC21: {
const std::initializer_list<std::string> filenames = {
"basic21.bin",
"BASIC Interpreter v2.1 (1985)(Intelligent Software).bin",
"BASIC Interpreter v2.1 (1985)(Intelligent Software)[a].bin"
};
const std::initializer_list<uint32_t> crcs = { 0x55f96251, 0x683cf455 };
*this = Description(name, "Enterprise", "the Enterprise BASIC ROM v2.1", filenames, 16 * 1024, crcs);
} break;
case Name::EnterpriseEPDOS: {
const std::initializer_list<std::string> filenames = {"epdos.bin", "EPDOS v1.7 (19xx)(Haluska, Laszlo).bin"};
*this = Description(name, "Enterprise", "the Enterprise EPDOS ROM", filenames, 32 * 1024, 0x201319ebu);
} break;
case Name::EnterpriseEXDOS: {
const std::initializer_list<std::string> filenames = {"exdos.bin", "EX-DOS EPROM (198x)(Enterprise).bin"};
*this = Description(name, "Enterprise", "the Enterprise EXDOS ROM", filenames, 16 * 1024, 0xe6daa0e9u);
} break;
case Name::Macintosh128k: *this = Description(name, "Macintosh", "the Macintosh 128k ROM", "mac128k.rom", 64*1024, 0x6d0c8a28u); break;
case Name::Macintosh512k: *this = Description(name, "Macintosh", "the Macintosh 512k ROM", "mac512k.rom", 64*1024, 0xcf759e0d); break;
case Name::MacintoshPlus: {
const std::initializer_list<uint32_t> crcs = { 0x4fa5b399, 0x7cacd18f, 0xb2102e8e };
*this = Description(name, "Macintosh", "the Macintosh Plus ROM", "macplus.rom", 128*1024, crcs);
} break;
case Name::AtariSTTOS100: *this = Description(name, "AtariST", "the UK TOS 1.00 ROM", "tos100.img", 192*1024, 0x1a586c64u); break;
case Name::AtariSTTOS104: *this = Description(name, "AtariST", "the UK TOS 1.04 ROM", "tos104.img", 192*1024, 0xa50d1d43u); break;
case Name::AtariSTEmuTOS192: *this = Description(name, "AtariST", "the UK EmuTOS 1.92 ROM", "etos192uk.img", 192*1024, 0xfc3b9e61u); break;
case Name::ColecoVisionBIOS:
*this = Description(name, "ColecoVision", "the ColecoVision BIOS", "coleco.rom", 8*1024, 0x3aa93ef3u);
break;
case Name::ZX80: *this = Description(name, "ZX8081", "the ZX80 BASIC ROM", "zx80.rom", 4 * 1024, 0x4c7fc597u); break;
case Name::ZX81: *this = Description(name, "ZX8081", "the ZX81 BASIC ROM", "zx81.rom", 8 * 1024, 0x4b1dd6ebu); break;
case Name::Spectrum48k: *this = Description(name, "ZXSpectrum", "the 48kb ROM", "48.rom", 16 * 1024, 0xddee531fu); break;
case Name::Spectrum128k: *this = Description(name, "ZXSpectrum", "the 128kb ROM", "128.rom", 32 * 1024, 0x2cbe8995u); break;
case Name::SpecrumPlus2: *this = Description(name, "ZXSpectrum", "the +2 ROM", "plus2.rom", 32 * 1024, 0xe7a517dcu); break;
case Name::SpectrumPlus3: {
const std::initializer_list<uint32_t> crcs = { 0x96e3c17a, 0xbe0d9ec4 };
*this = Description(name, "ZXSpectrum", "the +2a/+3 ROM", "plus3.rom", 64 * 1024, crcs);
} break;
case Name::AcornBASICII: *this = Description(name, "Electron", "the Acorn BASIC II ROM", "basic.rom", 16*1024, 0x79434781u); break;
case Name::PRESADFSSlot1: *this = Description(name, "Electron", "the E00 ADFS ROM, first slot", "ADFS-E00_1.rom", 16*1024, 0x51523993u); break;
case Name::PRESADFSSlot2: *this = Description(name, "Electron", "the E00 ADFS ROM, second slot", "ADFS-E00_2.rom", 16*1024, 0x8d17de0eu); break;
case Name::AcornADFS: *this = Description(name, "Electron", "the Acorn ADFS ROM", "adfs.rom", 16*1024, 0x3289bdc6u); break;
case Name::Acorn1770DFS: *this = Description(name, "Electron", "the 1770 DFS ROM", "DFS-1770-2.20.rom", 16*1024, 0xf3dc9bc5u); break;
case Name::PRESAdvancedPlus6:
*this = Description(name, "Electron", "the 8kb Advanced Plus 6 ROM", "AP6v133.rom", 8*1024, 0xe0013cfcu);
break;
case Name::AcornElectronMOS100:
*this = Description(name, "Electron", "the Electron MOS ROM v1.00", "os.rom", 16*1024, 0xbf63fb1fu);
break;
case Name::MasterSystemJapaneseBIOS: *this = Description(name, "MasterSystem", "the Japanese Master System BIOS", "japanese-bios.sms", 8*1024, 0x48d44a13u); break;
case Name::MasterSystemWesternBIOS: *this = Description(name, "MasterSystem", "the European/US Master System BIOS", "bios.sms", 8*1024, 0x0072ed54u); break;
case Name::Commodore1540: *this = Description(name, "Commodore1540", "the 1540 ROM", "1540.bin", 16*1024, 0x718d42b1u); break;
case Name::Commodore1541: *this = Description(name, "Commodore1540", "the 1541 ROM", "1541.bin", 16*1024, 0xfb760019); break;
case Name::Vic20BASIC: *this = Description(name, "Vic20", "the VIC-20 BASIC ROM", "basic.bin", 8*1024, 0xdb4c43c1u); break;
case Name::Vic20EnglishCharacters: *this = Description(name, "Vic20", "the English-language VIC-20 character ROM", "characters-english.bin", 4*1024, 0x83e032a6u); break;
case Name::Vic20EnglishPALKernel: *this = Description(name, "Vic20", "the English-language PAL VIC-20 kernel ROM", "kernel-pal.bin", 8*1024, 0x4be07cb4u); break;
case Name::Vic20EnglishNTSCKernel: *this = Description(name, "Vic20", "the English-language NTSC VIC-20 kernel ROM", "kernel-ntsc.bin", 8*1024, 0xe5e7c174u); break;
case Name::Vic20DanishCharacters: *this = Description(name, "Vic20", "the Danish VIC-20 character ROM", "characters-danish.bin", 4*1024, 0x7fc11454u); break;
case Name::Vic20DanishKernel: *this = Description(name, "Vic20", "the Danish VIC-20 kernel ROM", "kernel-danish.bin", 8*1024, 0x02adaf16u); break;
case Name::Vic20JapaneseCharacters: *this = Description(name, "Vic20", "the Japanese VIC-20 character ROM", "characters-japanese.bin", 4*1024, 0xfcfd8a4bu); break;
case Name::Vic20JapaneseKernel: *this = Description(name, "Vic20", "the Japanese VIC-20 kernel ROM", "kernel-japanese.bin", 8*1024, 0x336900d7u); break;
case Name::Vic20SwedishCharacters: *this = Description(name, "Vic20", "the Swedish VIC-20 character ROM", "characters-swedish.bin", 4*1024, 0xd808551du); break;
case Name::Vic20SwedishKernel: *this = Description(name, "Vic20", "the Swedish VIC-20 kernel ROM", "kernel-swedish.bin", 8*1024, 0xb2a60662u); break;
case Name::OricColourROM: *this = Description(name, "Oric", "the Oric colour ROM", "colour.rom", 128, 0xd50fca65u); break;
case Name::OricBASIC10: *this = Description(name, "Oric", "Oric BASIC 1.0", "basic10.rom", 16*1024, 0xf18710b4u); break;
case Name::OricBASIC11: *this = Description(name, "Oric", "Oric BASIC 1.1", "basic11.rom", 16*1024, 0xc3a92befu); break;
case Name::OricPravetzBASIC: *this = Description(name, "Oric", "Pravetz BASIC", "pravetz.rom", 16*1024, 0x58079502u); break;
case Name::OricByteDrive500: *this = Description(name, "Oric", "the Oric Byte Drive 500 ROM", "bd500.rom", 8*1024, 0x61952e34u); break;
case Name::OricJasmin: *this = Description(name, "Oric", "the Oric Jasmin ROM", "jasmin.rom", 2*1024, 0x37220e89u); break;
case Name::OricMicrodisc: *this = Description(name, "Oric", "the Oric Microdisc ROM", "microdisc.rom", 8*1024, 0xa9664a9cu); break;
case Name::Oric8DOSBoot: *this = Description(name, "Oric", "the 8DOS boot ROM", "8dos.rom", 512, 0x49a74c06u); break;
case Name::MSXGenericBIOS: *this = Description(name, "MSX", "any MSX BIOS", "msx.rom", 32*1024, 0x94ee12f3u); break;
case Name::MSXJapaneseBIOS: *this = Description(name, "MSX", "a Japanese MSX BIOS", "msx-japanese.rom", 32*1024, 0xee229390u); break;
case Name::MSXAmericanBIOS: *this = Description(name, "MSX", "an American MSX BIOS", "msx-american.rom", 32*1024, 0u); break;
case Name::MSXEuropeanBIOS: *this = Description(name, "MSX", "a European MSX BIOS", "msx-european.rom", 32*1024, 0u); break;
case Name::MSXDOS: *this = Description(name, "MSX", "the MSX-DOS ROM", "disk.rom", 16*1024, 0x721f61dfu); break;
case Name::SinclairQLJS:
*this = Description(name, "SinclairQL", "the Sinclair QL 'JS' ROM", "js.rom", 48*1024, 0x0f95aab5u);
break;
}
}

View File

@@ -0,0 +1,289 @@
//
// ROMCatalogue.hpp
// Clock Signal
//
// Created by Thomas Harte on 01/06/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#ifndef ROMCatalogue_hpp
#define ROMCatalogue_hpp
#include <algorithm>
#include <functional>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <vector>
namespace ROM {
enum Name {
None,
// Acorn.
AcornBASICII,
AcornElectronMOS100,
PRESADFSSlot1,
PRESADFSSlot2,
AcornADFS,
PRESAdvancedPlus6,
Acorn1770DFS,
// Amstrad CPC.
AMSDOS,
CPC464Firmware, CPC464BASIC,
CPC664Firmware, CPC664BASIC,
CPC6128Firmware, CPC6128BASIC,
// Apple II.
AppleIIOriginal,
AppleIIPlus,
AppleIICharacter,
AppleIIe,
AppleIIeCharacter,
AppleIIEnhancedE,
AppleIIEnhancedECharacter,
// Apple IIgs.
AppleIIgsROM00,
AppleIIgsROM01,
AppleIIgsROM03,
AppleIIgsMicrocontrollerROM03,
AppleIIgsCharacter,
// Atari ST.
AtariSTTOS100,
AtariSTTOS104,
AtariSTEmuTOS192,
// ColecoVision.
ColecoVisionBIOS,
// Commodore 1540/1541.
Commodore1540,
Commodore1541,
// Disk II.
DiskIIStateMachine16Sector,
DiskIIBoot16Sector,
DiskIIStateMachine13Sector,
DiskIIBoot13Sector,
// Enterprise.
EnterpriseEXOS10,
EnterpriseEXOS20,
EnterpriseEXOS21,
EnterpriseEXOS23,
EnterpriseBASIC10,
EnterpriseBASIC10Part1,
EnterpriseBASIC10Part2,
EnterpriseBASIC11,
EnterpriseBASIC11Suffixed,
EnterpriseBASIC21,
EnterpriseEPDOS,
EnterpriseEXDOS,
// Macintosh.
Macintosh128k,
Macintosh512k,
MacintoshPlus,
// Master System.
MasterSystemJapaneseBIOS,
MasterSystemWesternBIOS,
// MSX.
MSXGenericBIOS,
MSXJapaneseBIOS,
MSXAmericanBIOS,
MSXEuropeanBIOS,
MSXDOS,
// Oric.
OricColourROM,
OricBASIC10,
OricBASIC11,
OricPravetzBASIC,
OricByteDrive500,
OricJasmin,
OricMicrodisc,
Oric8DOSBoot,
// Sinclair QL.
SinclairQLJS,
// Vic-20.
Vic20BASIC,
Vic20EnglishCharacters,
Vic20EnglishPALKernel,
Vic20EnglishNTSCKernel,
Vic20DanishCharacters,
Vic20DanishKernel,
Vic20JapaneseCharacters,
Vic20JapaneseKernel,
Vic20SwedishCharacters,
Vic20SwedishKernel,
// ZX80/81.
ZX80,
ZX81,
// ZX Spectrum.
Spectrum48k,
Spectrum128k,
SpecrumPlus2,
SpectrumPlus3,
};
using Map = std::map<ROM::Name, std::vector<uint8_t>>;
struct Description {
/// The ROM's enum name.
Name name = Name::None;
/// The machine with which this ROM is associated, in a form that is safe for using as
/// part of a file name.
std::string machine_name;
/// A descriptive name for this ROM, suitable for use in a bullet-point list, a bracket
/// clause, etc, e.g. "the Electron MOS 1.0".
std::string descriptive_name;
/// All idiomatic file name for this ROM, e.g. "os10.rom".
std::vector<std::string> file_names;
/// The expected size of this ROM in bytes, e.g. 32768.
size_t size = 0;
/// CRC32s for all known acceptable copies of this ROM; intended to allow a host platform
/// to test user-provided ROMs of unknown provenance. **Not** intended to be used
/// to exclude ROMs where the user's intent is otherwise clear.
std::set<uint32_t> crc32s;
/// Constructs the @c Description that correlates to @c name.
Description(Name name);
/// Constructs the @c Description that correlates to @c crc32, if any.
static std::optional<Description> from_crc(uint32_t crc32);
enum DescriptionFlag {
Size = 1 << 0,
CRC = 1 << 1,
Filename = 1 << 2,
};
/// Provides a single-line of text describing this ROM, including the usual base text
/// plus all the fields provided as @c flags .
std::string description(int flags) const;
private:
template <typename FileNameT, typename CRC32T> Description(
Name name, std::string machine_name, std::string descriptive_name, FileNameT file_names, size_t size, CRC32T crc32s = uint32_t(0)
) : name{name}, machine_name{machine_name}, descriptive_name{descriptive_name}, file_names{file_names}, size{size}, crc32s{crc32s} {
// Slightly lazy: deal with the case where the constructor wasn't provided with any
// CRCs by spotting that the set has exactly one member, which has value 0. The alternative
// would be to provide a partial specialisation that never put anything into the set.
if(this->crc32s.size() == 1 && !*this->crc32s.begin()) {
this->crc32s.clear();
}
}
};
/// @returns a vector of all possible instances of ROM::Description — i.e. descriptions of every ROM
/// currently known to the ROM catalogue.
std::vector<Description> all_descriptions();
struct Request {
Request(Name name, bool optional = false);
Request() {}
/// Forms the request that would be satisfied by @c this plus the right-hand side.
Request operator &&(const Request &);
/// Forms the request that would be satisfied by either @c this or the right-hand side.
Request operator ||(const Request &);
/// Inspects the ROMMap to ensure that it satisfies this @c Request.
/// @c returns @c true if the request is satisfied; @c false otherwise.
///
/// All ROMs in the map will be resized to their idiomatic sizes.
bool validate(Map &) const;
/// Returns a flattened array of all @c ROM::Descriptions that relate to anything
/// anywhere in this ROM request.
std::vector<Description> all_descriptions() const;
/// @returns @c true if this request is empty, i.e. would be satisfied with no ROMs; @c false otherwise.
bool empty();
/// @returns what remains of this ROM request given that everything in @c map has been found.
Request subtract(const ROM::Map &map) const;
enum class ListType {
Any, All, Single
};
void visit(
const std::function<void(ListType, size_t size)> &enter_list,
const std::function<void(void)> &exit_list,
const std::function<void(ROM::Request::ListType type, const ROM::Description &, bool is_optional, size_t remaining)> &add_item
) const;
enum class LineItem {
NewList, Description
};
void visit(
const std::function<void(LineItem, ListType, int level, const ROM::Description *, bool is_optional, size_t remaining)> &add_item
) const;
/// @returns a full bullet-pointed list of the requirements of this request, including
/// appropriate conjuntives. This text is intended to be glued to the end of an opening
/// portion of a sentence, e.g. "Please supply" + request.description(0, L'*').
std::wstring description(int description_flags, wchar_t bullet_point);
private:
struct Node {
enum class Type {
Any, All, One
};
Type type = Type::One;
Name name = Name::None;
/// @c true if this ROM is optional for machine startup. Generally indicates something
/// that would make emulation more accurate, but not sufficiently so to make it
/// a necessity.
bool is_optional = false;
std::vector<Node> children;
bool empty() const {
return type == Type::One && name == Name::None;
}
void add_descriptions(std::vector<Description> &) const;
bool validate(Map &) const;
void visit(
const std::function<void(ListType, size_t)> &enter_list,
const std::function<void(void)> &exit_list,
const std::function<void(ROM::Request::ListType type, const ROM::Description &, bool is_optional, size_t remaining)> &add_item
) const;
bool subtract(const ROM::Map &map);
void sort() {
// Don't do a full sort, but move anything optional to the back.
// This makes them print more nicely; it's a human-facing tweak only.
ssize_t index = ssize_t(children.size() - 1);
bool has_seen_non_optional = false;
while(index >= 0) {
has_seen_non_optional |= !children[size_t(index)].is_optional;
if(children[size_t(index)].is_optional && has_seen_non_optional) {
std::rotate(children.begin() + index, children.begin() + index + 1, children.end());
}
--index;
}
}
};
Node node;
Request append(Node::Type type, const Request &rhs);
};
}
#endif /* ROMCatalogue_hpp */

View File

@@ -131,13 +131,3 @@ bool Typer::type_next_character() {
return true;
}
// MARK: - Character mapper
const uint16_t *CharacterMapper::table_lookup_sequence_for_character(const KeySequence *sequences, std::size_t length, char character) const {
std::size_t ucharacter = size_t((unsigned char)character);
if(ucharacter >= (length / sizeof(KeySequence))) return nullptr;
if(sequences[ucharacter][0] == MachineTypes::MappedKeyboardMachine::KeyNotMapped) return nullptr;
return sequences[ucharacter];
}

View File

@@ -44,11 +44,15 @@ class CharacterMapper {
typedef uint16_t KeySequence[16];
/*!
Provided in the base class as a convenience: given the lookup table of key sequences @c sequences,
with @c length entries, returns the sequence for character @c character if it exists; otherwise
returns @c nullptr.
Provided in the base class as a convenience: given the C array of key sequences @c sequences,
returns the sequence for character @c character if it exists; otherwise returns @c nullptr.
*/
const uint16_t *table_lookup_sequence_for_character(const KeySequence *sequences, std::size_t length, char character) const;
template <typename Collection> const uint16_t *table_lookup_sequence_for_character(const Collection &sequences, char character) const {
std::size_t ucharacter = size_t((unsigned char)character);
if(ucharacter >= sizeof(sequences) / sizeof(KeySequence)) return nullptr;
if(sequences[ucharacter][0] == MachineTypes::MappedKeyboardMachine::KeyNotMapped) return nullptr;
return sequences[ucharacter];
}
};
/*!

View File

@@ -12,6 +12,8 @@
#include <cstdint>
#include <cstdlib>
#include "Sizes.hpp"
namespace Numeric {
template <typename IntType> struct LSFRPolynomial {};
@@ -67,8 +69,8 @@ template <typename IntType = uint64_t, IntType polynomial = LSFRPolynomial<IntTy
determining the bit that was just shifted out.
*/
IntType next() {
const auto result = value_ & 1;
value_ = (value_ >> 1) ^ (result * polynomial);
const auto result = IntType(value_ & 1);
value_ = IntType((value_ >> 1) ^ (result * polynomial));
return result;
}
@@ -76,6 +78,8 @@ template <typename IntType = uint64_t, IntType polynomial = LSFRPolynomial<IntTy
IntType value_ = 0;
};
template <uint64_t polynomial> class LFSRv: public LFSR<typename MinIntTypeValue<polynomial>::type, polynomial> {};
}
#endif /* LFSR_h */

View File

@@ -10,6 +10,7 @@
#define Sizes_h
#include <limits>
#include <type_traits>
/*!
Maps to the smallest integral type that can contain max_value, from the following options:

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1200"
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -56,6 +56,18 @@
argument = "/Users/thomasharte/Downloads/test-dsk-for-rw-and-50-60-hz/TEST-RW-60Hz.DSK"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "&quot;/Users/thomasharte/Desktop/Soft/Apple II/WOZs/Prince of Persia side A.woz&quot;"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--rompath=~/ROMs"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--has-disk-drive"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "&quot;/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/Macintosh/MusicWorks 0.42.image&quot;"
isEnabled = "NO">
@@ -70,11 +82,15 @@
</CommandLineArgument>
<CommandLineArgument
argument = "--volume=0.001"
isEnabled = "NO">
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--new=amstradcpc"
isEnabled = "NO">
argument = "--new=enterprise"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--basic-version=any"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "&quot;/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/ColecoVision/Galaxian (1983)(Atari).col&quot;"
@@ -106,14 +122,22 @@
</CommandLineArgument>
<CommandLineArgument
argument = "--rompath=/Users/thomasharte/Projects/CLK/ROMImages"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--help"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--model=cpc6128"
argument = "--help"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--model=mac512k"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--new=macintosh"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--use-square-pixels"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1200"
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -73,6 +73,7 @@
debugDocumentVersioning = "YES"
migratedStopOnEveryIssue = "YES"
debugServiceExtension = "internal"
enableGPUShaderValidationMode = "2"
allowLocationSimulation = "NO">
<BuildableProductRunnable
runnableDebuggingMode = "0">

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1200"
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -11,12 +11,24 @@ import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
private var failedMetalCheck = false
func applicationDidFinishLaunching(_ notification: Notification) {
// Insert code here to initialize your application.
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application.
// Check for at least one Metal-capable GPU; this check
// will become unnecessary if/when the minimum OS version
// that this project supports reascends to 10.14.
if (MTLCopyAllDevices().count == 0) {
self.failedMetalCheck = true
let alert = NSAlert()
alert.messageText = "This application requires a Metal-capable GPU"
alert.addButton(withTitle: "Exit")
alert.runModal()
let application = notification.object as! NSApplication
application.terminate(self)
}
}
private var hasShownOpenDocument = false

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="18122" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="18122"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@@ -13,17 +13,27 @@
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="ZW7-Bw-4RP" customClass="MachinePanel" customModule="Clock_Signal" customModuleProvider="target">
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="ZW7-Bw-4RP" customClass="AppleIIOptionsPanel" customModule="Clock_Signal" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES" nonactivatingPanel="YES" HUD="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="80" y="150" width="200" height="61"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="900"/>
<rect key="contentRect" x="80" y="150" width="200" height="159"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1440"/>
<view key="contentView" id="tpZ-0B-QQu">
<rect key="frame" x="0.0" y="0.0" width="200" height="61"/>
<rect key="frame" x="0.0" y="0.0" width="200" height="84"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="kDb-7g-cVx">
<rect key="frame" x="18" y="47" width="162" height="18"/>
<buttonCell key="cell" type="check" title="Use Square Pixels" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="h9q-Wb-em8">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="optionDidChange:" target="ZW7-Bw-4RP" id="pNS-aK-0no"/>
</connections>
</button>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ex3-VM-58z">
<rect key="frame" x="18" y="17" width="165" height="25"/>
<rect key="frame" x="17" y="16" width="167" height="25"/>
<popUpButtonCell key="cell" type="push" title="Colour" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="1" imageScaling="proportionallyDown" inset="2" selectedItem="gOu-dv-tre" id="u3N-Je-c2L">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
@@ -40,16 +50,20 @@
</popUpButton>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="ex3-VM-58z" secondAttribute="bottom" constant="20" id="4ZS-q5-TJL"/>
<constraint firstAttribute="bottom" secondItem="ex3-VM-58z" secondAttribute="bottom" constant="20" symbolic="YES" id="4ZS-q5-TJL"/>
<constraint firstItem="ex3-VM-58z" firstAttribute="leading" secondItem="tpZ-0B-QQu" secondAttribute="leading" constant="20" id="8Pj-Ns-TrJ"/>
<constraint firstAttribute="trailing" secondItem="kDb-7g-cVx" secondAttribute="trailing" constant="20" symbolic="YES" id="KHa-of-eY7"/>
<constraint firstItem="kDb-7g-cVx" firstAttribute="leading" secondItem="tpZ-0B-QQu" secondAttribute="leading" constant="20" symbolic="YES" id="OcZ-Xa-394"/>
<constraint firstAttribute="trailing" secondItem="ex3-VM-58z" secondAttribute="trailing" constant="20" id="QWA-lY-ugz"/>
<constraint firstItem="ex3-VM-58z" firstAttribute="top" secondItem="tpZ-0B-QQu" secondAttribute="top" constant="20" id="szw-WO-3tS"/>
<constraint firstItem="ex3-VM-58z" firstAttribute="top" secondItem="kDb-7g-cVx" secondAttribute="bottom" constant="8" id="jDW-N8-c4V"/>
<constraint firstItem="kDb-7g-cVx" firstAttribute="top" secondItem="tpZ-0B-QQu" secondAttribute="top" constant="20" symbolic="YES" id="wdj-PF-zxO"/>
</constraints>
</view>
<connections>
<outlet property="displayTypeButton" destination="ex3-VM-58z" id="lmZ-aN-lcj"/>
<outlet property="squarePixelButton" destination="kDb-7g-cVx" id="zbx-o8-7yC"/>
</connections>
<point key="canvasLocation" x="-161" y="38.5"/>
<point key="canvasLocation" x="-161" y="104.5"/>
</window>
</objects>
</document>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="18122" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="18122"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@@ -13,17 +13,17 @@
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="gsl-7V-TTU" customClass="Atari2600OptionsPanel" customModule="Clock_Signal" customModuleProvider="target">
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="gsl-7V-TTU" customClass="Atari2600OptionsPanel" customModule="Clock_Signal" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES" nonactivatingPanel="YES" HUD="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="80" y="150" width="200" height="121"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="900"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1440"/>
<view key="contentView" id="aQh-Pm-DEo">
<rect key="frame" x="0.0" y="0.0" width="200" height="121"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="rQO-uD-fwn">
<rect key="frame" x="14" y="73" width="86" height="32"/>
<rect key="frame" x="13" y="74" width="88" height="32"/>
<buttonCell key="cell" type="push" title="Reset" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="l3H-0m-aK0">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@@ -33,7 +33,7 @@
</connections>
</button>
<button translatesAutoresizingMaskIntoConstraints="NO" id="3qw-C1-NYW">
<rect key="frame" x="18" y="58" width="164" height="18"/>
<rect key="frame" x="18" y="58" width="162" height="18"/>
<buttonCell key="cell" type="check" title="Black and White" bezelStyle="regularSquare" imagePosition="left" inset="2" id="UP7-mf-IKo">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
@@ -43,7 +43,7 @@
</connections>
</button>
<button translatesAutoresizingMaskIntoConstraints="NO" id="Xbc-cw-Sc2">
<rect key="frame" x="18" y="38" width="164" height="18"/>
<rect key="frame" x="18" y="36" width="162" height="18"/>
<buttonCell key="cell" type="check" title="Left Player Difficulty" bezelStyle="regularSquare" imagePosition="left" inset="2" id="wlJ-8s-PEh">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
@@ -53,7 +53,7 @@
</connections>
</button>
<button translatesAutoresizingMaskIntoConstraints="NO" id="kPV-Tm-TTc">
<rect key="frame" x="18" y="18" width="164" height="18"/>
<rect key="frame" x="18" y="14" width="162" height="18"/>
<buttonCell key="cell" type="check" title="Right Player Difficulty" bezelStyle="regularSquare" imagePosition="left" inset="2" id="F05-cA-66S">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
@@ -63,7 +63,7 @@
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="nt7-8K-xY9">
<rect key="frame" x="100" y="73" width="86" height="32"/>
<rect key="frame" x="99" y="74" width="88" height="32"/>
<buttonCell key="cell" type="push" title="Select" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="8Na-Z1-EXS">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>

View File

@@ -0,0 +1,37 @@
//
// AppleIIOptionsPanel.swift
// Clock Signal
//
// Created by Thomas Harte on 07/06/2021.
// Copyright 2021 Thomas Harte. All rights reserved.
//
class AppleIIOptionsPanel: MachinePanel {
var appleII: CSAppleII! {
get {
return self.machine.appleII
}
}
var squarePixelsUserDefaultsKey: String {
return prefixedUserDefaultsKey("useSquarePixels")
}
@IBOutlet var squarePixelButton: NSButton!
@IBAction func optionDidChange(_ sender: AnyObject!) {
let useSquarePixels = squarePixelButton.state == .on
appleII.useSquarePixels = useSquarePixels
let standardUserDefaults = UserDefaults.standard
standardUserDefaults.set(useSquarePixels, forKey: squarePixelsUserDefaultsKey)
}
override func establishStoredOptions() {
super.establishStoredOptions()
let standardUserDefaults = UserDefaults.standard
let useSquarePixels = standardUserDefaults.bool(forKey: squarePixelsUserDefaultsKey)
appleII.useSquarePixels = useSquarePixels
squarePixelButton.state = useSquarePixels ? .on : .off
}
}

View File

@@ -87,6 +87,14 @@ class MachineDocument:
}
}
private func dismissPanels() {
activityPanel?.setIsVisible(false)
activityPanel = nil
optionsPanel?.setIsVisible(false)
optionsPanel = nil
}
override func close() {
// Close any dangling sheets.
//
@@ -105,11 +113,7 @@ class MachineDocument:
machine?.stop()
// Dismiss panels.
activityPanel?.setIsVisible(false)
activityPanel = nil
optionsPanel?.setIsVisible(false)
optionsPanel = nil
dismissPanels()
// End the update cycle.
actionLock.lock()
@@ -133,23 +137,27 @@ class MachineDocument:
volumeSlider.floatValue = userDefaultsVolume()
}
private var missingROMs: [CSMissingROM] = []
private var missingROMs: String = ""
func configureAs(_ analysis: CSStaticAnalyser) {
self.machineDescription = analysis
let missingROMs = NSMutableArray()
if let machine = CSMachine(analyser: analysis, missingROMs: missingROMs) {
self.machine = machine
setupMachineOutput()
setupActivityDisplay()
machine.setVolume(userDefaultsVolume())
} else {
// Store the selected machine and list of missing ROMs, and
// show the missing ROMs dialogue.
self.missingROMs = missingROMs.map({$0 as! CSMissingROM})
actionLock.lock()
drawLock.lock()
let missingROMs = NSMutableString()
if let machine = CSMachine(analyser: analysis, missingROMs: missingROMs) {
setRomRequesterIsVisible(false)
self.machine = machine
machine.setVolume(userDefaultsVolume())
setupMachineOutput()
} else {
self.missingROMs = missingROMs as String
requestRoms()
}
actionLock.unlock()
drawLock.unlock()
}
enum InteractionMode {
@@ -200,6 +208,9 @@ class MachineDocument:
let aspectRatio = self.aspectRatio()
machine.setView(scanTargetView, aspectRatio: Float(aspectRatio.width / aspectRatio.height))
// Get rid of all existing accessory panels.
dismissPanels()
// Attach an options panel if one is available.
if let optionsPanelNibName = self.machineDescription?.optionsPanelNibName {
Bundle.main.loadNibNamed(optionsPanelNibName, owner: self, topLevelObjects: nil)
@@ -208,11 +219,10 @@ class MachineDocument:
showOptions(self)
}
machine.delegate = self
// Create and populate an activity display if required.
setupActivityDisplay()
// Callbacks from the OpenGL may come on a different thread, immediately following the .delegate set;
// hence the full setup of the best-effort updater prior to setting self as a delegate.
// scanTargetView.delegate = self
machine.delegate = self
scanTargetView.responderDelegate = self
// If this machine has a mouse, enable mouse capture; also indicate whether usurption
@@ -252,7 +262,7 @@ class MachineDocument:
let isStereo = self.machine.isStereo
if selectedSamplingRate > 0 {
// [Re]create the audio queue only if necessary.
if self.audioQueue == nil || self.audioQueue.samplingRate != selectedSamplingRate {
if self.audioQueue == nil || self.audioQueue.samplingRate != selectedSamplingRate || self.audioQueue != self.machine.audioQueue {
self.machine.audioQueue = nil
self.audioQueue = CSAudioQueue(samplingRate: Float64(selectedSamplingRate), isStereo:isStereo)
self.audioQueue.delegate = self
@@ -280,8 +290,7 @@ class MachineDocument:
/// Delegate message to receive drag and drop files.
final func scanTargetView(_ view: CSScanTargetView, didReceiveFileAt URL: URL) {
let mediaSet = CSMediaSet(fileAt: URL)
mediaSet.apply(to: self.machine)
insertFile(URL)
}
/// Action for the insert menu command; displays an NSOpenPanel and then segues into the same process
@@ -292,13 +301,30 @@ class MachineDocument:
openPanel.beginSheetModal(for: self.windowControllers[0].window!) { (response) in
if response == .OK {
for url in openPanel.urls {
let mediaSet = CSMediaSet(fileAt: url)
mediaSet.apply(to: self.machine)
self.insertFile(url)
}
}
}
}
private func insertFile(_ URL: URL) {
// Try to insert media.
let mediaSet = CSMediaSet(fileAt: URL)
if !mediaSet.empty {
mediaSet.apply(to: self.machine)
return
}
// Failing that see whether a new machine is required.
// TODO.
if let newMachine = CSStaticAnalyser(fileAt: URL) {
machine?.stop()
self.interactionMode = .notStarted
self.scanTargetView.willChangeScanTargetOwner()
configureAs(newMachine)
}
}
// MARK: - Input Management.
/// Upon a resign key, immediately releases all ongoing input mechanisms any currently pressed keys,
@@ -373,6 +399,10 @@ class MachineDocument:
self.configureAs(selectedMachine)
}
@IBAction func tableViewDoubleClick(_ sender: NSTableView?) {
createMachine(nil)
}
@IBAction func cancelCreateMachine(_ sender: NSButton?) {
close()
}
@@ -383,23 +413,42 @@ class MachineDocument:
@IBOutlet var romReceiverErrorField: NSTextField?
@IBOutlet var romReceiverView: CSROMReceiverView?
private var romRequestBaseText = ""
private func setRomRequesterIsVisible(_ visible : Bool) {
if !visible && self.romRequesterPanel == nil {
return;
}
if self.romRequesterPanel!.isVisible == visible {
return
}
if visible {
self.windowControllers[0].window?.beginSheet(self.romRequesterPanel!, completionHandler: nil)
} else {
self.windowControllers[0].window?.endSheet(self.romRequesterPanel!)
}
}
func requestRoms() {
// Don't act yet if there's no window controller yet.
if self.windowControllers.count == 0 {
return
}
// Load the ROM requester dialogue.
Bundle.main.loadNibNamed("ROMRequester", owner: self, topLevelObjects: nil)
self.romReceiverView!.delegate = self
self.romRequestBaseText = romRequesterText!.stringValue
romReceiverErrorField?.alphaValue = 0.0
// Load the ROM requester dialogue if it's not already loaded.
if self.romRequesterPanel == nil {
Bundle.main.loadNibNamed("ROMRequester", owner: self, topLevelObjects: nil)
self.romReceiverView!.delegate = self
self.romRequestBaseText = romRequesterText!.stringValue
romReceiverErrorField?.alphaValue = 0.0
}
// Populate the current absentee list.
populateMissingRomList()
// Show the thing.
self.windowControllers[0].window?.beginSheet(self.romRequesterPanel!, completionHandler: nil)
setRomRequesterIsVisible(true)
}
@IBAction func cancelRequestROMs(_ sender: NSButton?) {
@@ -407,89 +456,17 @@ class MachineDocument:
}
func populateMissingRomList() {
// Fill in the missing details; first build a list of all the individual
// line items.
var requestLines: [String] = []
for missingROM in self.missingROMs {
if let descriptiveName = missingROM.descriptiveName {
requestLines.append("" + descriptiveName)
} else {
requestLines.append("" + missingROM.fileName)
}
}
// Suffix everything up to the penultimate line with a semicolon;
// the penultimate line with a semicolon and a conjunctive; the final
// line with a full stop.
for x in 0 ..< requestLines.count {
if x < requestLines.count - 2 {
requestLines[x].append(";")
} else if x < requestLines.count - 1 {
requestLines[x].append("; and")
} else {
requestLines[x].append(".")
}
}
romRequesterText!.stringValue = self.romRequestBaseText + requestLines.joined(separator: "\n")
romRequesterText!.stringValue = self.romRequestBaseText + self.missingROMs
}
func romReceiverView(_ view: CSROMReceiverView, didReceiveFileAt URL: URL) {
// Test whether the file identified matches any of the currently missing ROMs.
// If so then remove that ROM from the missing list and update the request screen.
// If no ROMs are still missing, start the machine.
do {
let fileData = try Data(contentsOf: URL)
var didInstallRom = false
// Try to match by size first, CRC second. Accept that some ROMs may have
// some additional appended data. Arbitrarily allow them to be up to 10kb
// too large.
var index = 0
for missingROM in self.missingROMs {
if fileData.count >= missingROM.size && fileData.count < missingROM.size + 10*1024 {
// Trim to size.
let trimmedData = fileData[0 ..< missingROM.size]
// Get CRC.
if missingROM.crc32s.contains( (trimmedData as NSData).crc32 ) {
// This ROM matches; copy it into the application library,
// strike it from the missing ROM list and decide how to
// proceed.
let fileManager = FileManager.default
let targetPath = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("ROMImages")
.appendingPathComponent(missingROM.machineName)
let targetFile = targetPath
.appendingPathComponent(missingROM.fileName)
do {
try fileManager.createDirectory(atPath: targetPath.path, withIntermediateDirectories: true, attributes: nil)
try trimmedData.write(to: targetFile)
} catch let error {
showRomReceiverError(error: "Couldn't write to application support directory: \(error)")
}
self.missingROMs.remove(at: index)
didInstallRom = true
break
}
}
index = index + 1
}
if didInstallRom {
if self.missingROMs.count == 0 {
self.windowControllers[0].window?.endSheet(self.romRequesterPanel!)
configureAs(self.machineDescription!)
} else {
populateMissingRomList()
}
} else {
showRomReceiverError(error: "Didn't recognise contents of \(URL.lastPathComponent)")
}
} catch let error {
showRomReceiverError(error: "Couldn't read file at \(URL.absoluteString): \(error)")
if CSMachine.attemptInstallROM(URL) {
configureAs(self.machineDescription!)
} else {
showRomReceiverError(error: "Didn't recognise contents of \(URL.lastPathComponent)")
}
}
@@ -593,7 +570,7 @@ class MachineDocument:
return super.validateUserInterfaceItem(item)
}
/// Saves a screenshot of the
/// Saves a screenshot of the machine's current display.
@IBAction func saveScreenshot(_ sender: AnyObject!) {
// Grab a date formatter and form a file name.
let dateFormatter = DateFormatter()

View File

@@ -592,6 +592,66 @@
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>sna</string>
</array>
<key>CFBundleTypeName</key>
<string>ZX Spectrum SNA snapshot</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>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>z80</string>
</array>
<key>CFBundleTypeName</key>
<string>ZX Spectrum Z80 snapshot</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>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>szx</string>
</array>
<key>CFBundleTypeName</key>
<string>ZX Spectrum SZX snapshot</string>
<key>CFBundleTypeOSTypes</key>
<array>
<string>????</string>
</array>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSTypeIsPackage</key>
<false/>
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>

View File

@@ -368,7 +368,7 @@ API_AVAILABLE(macos(11.0))
return self;
}
-(void)update {
- (void)update {
// Update buttons.
for(CSGCJoystickButton *button in _buttons) {
// This assumes that the values provided by GCDeviceButtonInput are
@@ -379,7 +379,11 @@ API_AVAILABLE(macos(11.0))
float val = axis.axis.value;
val += 1;
val /= 2;
axis.position = val;
if(axis.type == CSJoystickAxisTypeY) {
axis.position = 1 - val;
} else {
axis.position = val;
}
}
for(CSGCJoystickHat *hat in _hats) {
// This assumes that the values provided by GCDeviceDirectionPad are

View File

@@ -33,20 +33,15 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
CSMachineKeyboardInputModeJoystick,
};
@interface CSMissingROM: NSObject
@property (nonatomic, readonly, nonnull) NSString *machineName;
@property (nonatomic, readonly, nonnull) NSString *fileName;
@property (nonatomic, readonly, nullable) NSString *descriptiveName;
@property (nonatomic, readonly) NSUInteger size;
@property (nonatomic, readonly, nonnull) NSArray<NSNumber *> *crc32s;
@end
// Deliberately low; to ensure CSMachine has been declared as an @class already.
#import "CSAtari2600.h"
#import "CSZX8081.h"
#import "CSAppleII.h"
@interface CSMachine : NSObject
+ (BOOL)attemptInstallROM:(nonnull NSURL *)url;
- (nonnull instancetype)init NS_UNAVAILABLE;
/*!
@@ -56,7 +51,7 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
@param missingROMs An array that is filled with a list of ROMs that the machine requested but which
were not found; populated only if this `init` has failed.
*/
- (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result missingROMs:(nullable inout NSMutableArray<CSMissingROM *> *)missingROMs NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result missingROMs:(nullable inout NSMutableString *)missingROMs NS_DESIGNATED_INITIALIZER;
- (float)idealSamplingRateFromRange:(NSRange)range;
@property (readonly, getter=isStereo) BOOL stereo;
@@ -109,5 +104,6 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
// Special-case accessors; undefined behaviour if accessed for a machine not of the corresponding type.
@property (nonatomic, readonly, nullable) CSAtari2600 *atari2600;
@property (nonatomic, readonly, nullable) CSZX8081 *zx8081;
@property (nonatomic, readonly, nullable) CSAppleII *appleII;
@end

View File

@@ -31,6 +31,8 @@
#include <atomic>
#include <bitset>
#include <codecvt>
#include <locale>
@interface CSMachine() <CSScanTargetViewDisplayLinkDelegate>
- (void)speaker:(Outputs::Speaker::Speaker *)speaker didCompleteSamples:(const int16_t *)samples length:(int)length;
@@ -74,28 +76,6 @@ struct ActivityObserver: public Activity::Observer {
__unsafe_unretained CSMachine *machine;
};
@interface CSMissingROM (/*Mutability*/)
@property (nonatomic, nonnull, copy) NSString *machineName;
@property (nonatomic, nonnull, copy) NSString *fileName;
@property (nonatomic, nullable, copy) NSString *descriptiveName;
@property (nonatomic, readwrite) NSUInteger size;
@property (nonatomic, copy) NSArray<NSNumber *> *crc32s;
@end
@implementation CSMissingROM
@synthesize machineName=_machineName;
@synthesize fileName=_fileName;
@synthesize descriptiveName=_descriptiveName;
@synthesize size=_size;
@synthesize crc32s=_crc32s;
- (NSString *)description {
return [NSString stringWithFormat:@"%@/%@, %lu bytes, CRCs: %@", _fileName, _descriptiveName, (unsigned long)_size, _crc32s];
}
@end
@implementation CSMachine {
SpeakerDelegate _speakerDelegate;
ActivityObserver _activityObserver;
@@ -126,35 +106,18 @@ struct ActivityObserver: public Activity::Observer {
NSMutableArray<dispatch_block_t> *_inputEvents;
}
- (instancetype)initWithAnalyser:(CSStaticAnalyser *)result missingROMs:(inout NSMutableArray<CSMissingROM *> *)missingROMs {
- (instancetype)initWithAnalyser:(CSStaticAnalyser *)result missingROMs:(inout NSMutableString *)missingROMs {
self = [super init];
if(self) {
_analyser = result;
Machine::Error error;
std::vector<ROMMachine::ROM> missing_roms;
ROM::Request missing_roms;
_machine.reset(Machine::MachineForTargets(_analyser.targets, CSROMFetcher(&missing_roms), error));
if(!_machine) {
for(const auto &missing_rom : missing_roms) {
CSMissingROM *rom = [[CSMissingROM alloc] init];
// Copy/convert the primitive fields.
rom.machineName = @(missing_rom.machine_name.c_str());
rom.fileName = @(missing_rom.file_name.c_str());
rom.descriptiveName = missing_rom.descriptive_name.empty() ? nil : @(missing_rom.descriptive_name.c_str());
rom.size = missing_rom.size;
// Convert the CRC list.
NSMutableArray<NSNumber *> *crc32s = [[NSMutableArray alloc] initWithCapacity:missing_rom.crc32s.size()];
for(const auto &crc : missing_rom.crc32s) {
[crc32s addObject:@(crc)];
}
rom.crc32s = crc32s;
// Add to the missing list.
[missingROMs addObject:rom];
}
std::wstring_convert<std::codecvt_utf8<wchar_t>> wstring_converter;
const std::wstring description = missing_roms.description(0, L'•');
[missingROMs appendString:[NSString stringWithUTF8String:wstring_converter.to_bytes(description).c_str()]];
return nil;
}
@@ -614,6 +577,10 @@ struct ActivityObserver: public Activity::Observer {
return [[CSZX8081 alloc] initWithZX8081:_machine->raw_pointer() owner:self];
}
- (CSAppleII *)appleII {
return [[CSAppleII alloc] initWithAppleII:_machine->raw_pointer() owner:self];
}
#pragma mark - Input device queries
- (BOOL)hasJoystick {
@@ -783,4 +750,8 @@ struct ActivityObserver: public Activity::Observer {
_timer = nil;
}
+ (BOOL)attemptInstallROM:(NSURL *)url {
return CSInstallROM(url);
}
@end

View File

@@ -8,4 +8,5 @@
#include "ROMMachine.hpp"
ROMMachine::ROMFetcher CSROMFetcher(std::vector<ROMMachine::ROM> *missing_roms = nullptr);
ROMMachine::ROMFetcher CSROMFetcher(ROM::Request *missing = nullptr);
BOOL CSInstallROM(NSURL *);

View File

@@ -11,47 +11,96 @@
#import "NSBundle+DataResource.h"
#import "NSData+StdVector.h"
#import "NSData+CRC32.h"
#include <string>
ROMMachine::ROMFetcher CSROMFetcher(std::vector<ROMMachine::ROM> *missing_roms) {
return [missing_roms] (const std::vector<ROMMachine::ROM> &roms) -> std::vector<std::unique_ptr<std::vector<std::uint8_t>>> {
NSArray<NSURL *> *const supportURLs = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask];
namespace {
std::vector<std::unique_ptr<std::vector<std::uint8_t>>> results;
for(const auto &rom: roms) {
NSData *fileData;
NSString *const subdirectory = [@"ROMImages/" stringByAppendingString:[NSString stringWithUTF8String:rom.machine_name.c_str()]];
NSString *directoryFor(const ROM::Description &description) {
return [@"ROMImages/" stringByAppendingString:[NSString stringWithUTF8String:description.machine_name.c_str()]];
}
// Check for this file first within the application support directories.
for(NSURL *supportURL in supportURLs) {
NSURL *const fullURL = [[supportURL URLByAppendingPathComponent:subdirectory]
URLByAppendingPathComponent:[NSString stringWithUTF8String:rom.file_name.c_str()]];
fileData = [NSData dataWithContentsOfURL:fullURL];
if(fileData) break;
NSArray<NSURL *> *urlsFor(const ROM::Description &description, const std::string &file_name) {
NSMutableArray<NSURL *> *const urls = [[NSMutableArray alloc] init];
NSArray<NSURL *> *const supportURLs = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask];
NSString *const subdirectory = directoryFor(description);
for(NSURL *supportURL in supportURLs) {
[urls addObject:[[supportURL URLByAppendingPathComponent:subdirectory]
URLByAppendingPathComponent:[NSString stringWithUTF8String:file_name.c_str()]]];
}
return urls;
}
}
BOOL CSInstallROM(NSURL *url) {
NSData *const data = [NSData dataWithContentsOfURL:url];
if(!data) return NO;
// Try for a direct CRC match.
std::optional<ROM::Description> target_description;
target_description = ROM::Description::from_crc(uint32_t(data.crc32.integerValue));
// See whether there's an acceptable trimming that creates a CRC match.
if(!target_description) {
const std::vector<ROM::Description> descriptions = ROM::all_descriptions();
for(const auto &description: descriptions) {
if(description.size > data.length) continue;
NSData *const trimmedData = [data subdataWithRange:NSMakeRange(0, description.size)];
if(description.crc32s.find(uint32_t(trimmedData.crc32.unsignedIntValue)) != description.crc32s.end()) {
target_description = description;
break;
}
}
}
// Failing that, check inside the application bundle.
if(!fileData) {
fileData = [[NSBundle mainBundle]
dataForResource:[NSString stringWithUTF8String:rom.file_name.c_str()]
withExtension:nil
subdirectory:subdirectory];
}
// If no destination was found, stop.
if(!target_description) {
return NO;
}
// Store an appropriate result, accumulating a list of the missing if requested.
if(!fileData) {
results.emplace_back(nullptr);
// Copy the data to its destination and report success.
NSURL *const targetURL = [urlsFor(*target_description, target_description->file_names[0]) firstObject];
[[NSFileManager defaultManager] createDirectoryAtPath:targetURL.URLByDeletingLastPathComponent.path withIntermediateDirectories:YES attributes:nil error:nil];
[data writeToURL:targetURL atomically:NO];
if(missing_roms) {
missing_roms->push_back(rom);
return YES;
}
ROMMachine::ROMFetcher CSROMFetcher(ROM::Request *missing) {
return [missing] (const ROM::Request &roms) -> ROM::Map {
ROM::Map results;
for(const auto &description: roms.all_descriptions()) {
for(const auto &file_name: description.file_names) {
NSData *fileData;
// Check for this file first within the application support directories.
for(NSURL *fileURL in urlsFor(description, file_name)) {
fileData = [NSData dataWithContentsOfURL:fileURL];
if(fileData) break;
}
// Failing that, check inside the application bundle.
if(!fileData) {
fileData = [[NSBundle mainBundle]
dataForResource:[NSString stringWithUTF8String:file_name.c_str()]
withExtension:nil
subdirectory:directoryFor(description)];
}
// Store an appropriate result.
if(fileData) {
results[description.name] = fileData.stdVector8;
}
}
else {
auto data = std::make_unique<std::vector<std::uint8_t>>();
*data = fileData.stdVector8;
results.emplace_back(std::move(data));
}
}
if(missing) {
*missing = roms.subtract(results);
}
return results;

View File

@@ -41,6 +41,35 @@ typedef NS_ENUM(NSInteger, CSMachineCPCModel) {
CSMachineCPCModel6128
};
typedef NS_ENUM(NSInteger, CSMachineEnterpriseModel) {
CSMachineEnterpriseModel64,
CSMachineEnterpriseModel128,
CSMachineEnterpriseModel256,
};
typedef NS_ENUM(NSInteger, CSMachineEnterpriseSpeed) {
CSMachineEnterpriseSpeed4MHz,
CSMachineEnterpriseSpeed6MHz
};
typedef NS_ENUM(NSInteger, CSMachineEnterpriseEXOS) {
CSMachineEnterpriseEXOSVersion21,
CSMachineEnterpriseEXOSVersion20,
CSMachineEnterpriseEXOSVersion10,
};
typedef NS_ENUM(NSInteger, CSMachineEnterpriseBASIC) {
CSMachineEnterpriseBASICVersion21,
CSMachineEnterpriseBASICVersion11,
CSMachineEnterpriseBASICVersion10,
CSMachineEnterpriseBASICNone,
};
typedef NS_ENUM(NSInteger, CSMachineEnterpriseDOS) {
CSMachineEnterpriseDOSEXDOS,
CSMachineEnterpriseDOSNone,
};
typedef NS_ENUM(NSInteger, CSMachineMacintoshModel) {
CSMachineMacintoshModel128k,
CSMachineMacintoshModel512k,
@@ -63,6 +92,10 @@ typedef NS_ENUM(NSInteger, CSMachineOricDiskInterface) {
};
typedef NS_ENUM(NSInteger, CSMachineSpectrumModel) {
CSMachineSpectrumModelSixteenK,
CSMachineSpectrumModelFortyEightK,
CSMachineSpectrumModelOneTwoEightK,
CSMachineSpectrumModelPlus2,
CSMachineSpectrumModelPlus2a,
CSMachineSpectrumModelPlus3,
};
@@ -92,6 +125,7 @@ typedef int Kilobytes;
- (instancetype)initWithAppleIIgsModel:(CSMachineAppleIIgsModel)model memorySize:(Kilobytes)memorySize;
- (instancetype)initWithAtariSTModel:(CSMachineAtariSTModel)model;
- (instancetype)initWithElectronDFS:(BOOL)dfs adfs:(BOOL)adfs ap6:(BOOL)ap6 sidewaysRAM:(BOOL)sidewaysRAM;
- (instancetype)initWithEnterpriseModel:(CSMachineEnterpriseModel)model speed:(CSMachineEnterpriseSpeed)speed exosVersion:(CSMachineEnterpriseEXOS)exosVersion basicVersion:(CSMachineEnterpriseBASIC)basicVersion dos:(CSMachineEnterpriseDOS)dos;
- (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model;
- (instancetype)initWithMSXRegion:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive;
- (instancetype)initWithOricModel:(CSMachineOricModel)model diskInterface:(CSMachineOricDiskInterface)diskInterface;
@@ -110,6 +144,8 @@ typedef int Kilobytes;
- (instancetype)initWithFileAtURL:(NSURL *)url;
- (void)applyToMachine:(CSMachine *)machine;
@property(nonatomic, readonly) BOOL empty;
@end
NS_ASSUME_NONNULL_END

View File

@@ -19,6 +19,7 @@
#include "../../../../../Analyser/Static/AppleIIgs/Target.hpp"
#include "../../../../../Analyser/Static/AtariST/Target.hpp"
#include "../../../../../Analyser/Static/Commodore/Target.hpp"
#include "../../../../../Analyser/Static/Enterprise/Target.hpp"
#include "../../../../../Analyser/Static/Macintosh/Target.hpp"
#include "../../../../../Analyser/Static/MSX/Target.hpp"
#include "../../../../../Analyser/Static/Oric/Target.hpp"
@@ -130,6 +131,51 @@
return self;
}
- (instancetype)initWithEnterpriseModel:(CSMachineEnterpriseModel)model speed:(CSMachineEnterpriseSpeed)speed exosVersion:(CSMachineEnterpriseEXOS)exosVersion basicVersion:(CSMachineEnterpriseBASIC)basicVersion dos:(CSMachineEnterpriseDOS)dos {
self = [super init];
if(self) {
using Target = Analyser::Static::Enterprise::Target;
auto target = std::make_unique<Target>();
switch(model) {
case CSMachineEnterpriseModel64: target->model = Target::Model::Enterprise64; break;
default:
case CSMachineEnterpriseModel128: target->model = Target::Model::Enterprise128; break;
case CSMachineEnterpriseModel256: target->model = Target::Model::Enterprise256; break;
}
switch(speed) {
case CSMachineEnterpriseSpeed6MHz: target->speed = Target::Speed::SixMHz; break;
default:
case CSMachineEnterpriseSpeed4MHz: target->speed = Target::Speed::FourMHz; break;
}
switch(exosVersion) {
case CSMachineEnterpriseEXOSVersion21: target->exos_version = Target::EXOSVersion::v21; break;
default:
case CSMachineEnterpriseEXOSVersion20: target->exos_version = Target::EXOSVersion::v20; break;
case CSMachineEnterpriseEXOSVersion10: target->exos_version = Target::EXOSVersion::v10; break;
}
switch(basicVersion) {
case CSMachineEnterpriseBASICNone: target->basic_version = Target::BASICVersion::None; break;
default:
case CSMachineEnterpriseBASICVersion21: target->basic_version = Target::BASICVersion::v21; break;
case CSMachineEnterpriseBASICVersion11: target->basic_version = Target::BASICVersion::v11; break;
case CSMachineEnterpriseBASICVersion10: target->basic_version = Target::BASICVersion::v10; break;
}
switch(dos) {
case CSMachineEnterpriseDOSEXDOS: target->dos = Target::DOS::EXDOS; break;
default:
case CSMachineEnterpriseDOSNone: target->dos = Target::DOS::None; break;
}
_targets.push_back(std::move(target));
}
return self;
}
- (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model {
self = [super init];
if(self) {
@@ -194,8 +240,12 @@
using Target = Analyser::Static::ZXSpectrum::Target;
auto target = std::make_unique<Target>();
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));
}
@@ -272,6 +322,7 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K
case Analyser::Machine::AtariST: return @"CompositeOptions";
case Analyser::Machine::ColecoVision: return @"CompositeOptions";
case Analyser::Machine::Electron: return @"QuickLoadCompositeOptions";
case Analyser::Machine::Enterprise: return @"CompositeOptions";
case Analyser::Machine::Macintosh: return @"MacintoshOptions";
case Analyser::Machine::MasterSystem: return @"CompositeOptions";
case Analyser::Machine::MSX: return @"QuickLoadCompositeOptions";
@@ -305,4 +356,8 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K
[machine applyMedia:_media];
}
- (BOOL)empty {
return _media.empty();
}
@end

View File

@@ -0,0 +1,18 @@
//
// CSAppleII.h
// Clock Signal
//
// Created by Thomas Harte on 07/06/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
@class CSAppleII;
#import "CSMachine.h"
@interface CSAppleII : NSObject
- (instancetype)initWithAppleII:(void *)appleII owner:(CSMachine *)machine;
@property (nonatomic, assign) BOOL useSquarePixels;
@end

Some files were not shown because too many files have changed in this diff Show More